From 49ba5cea2982a7f0fab92d638cbf36b536f62dac Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Thu, 8 Oct 2026 17:11:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?1.=E6=AF=8F=E6=97=A5=E8=8F=9C=E5=93=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BF=AE=E6=94=B9=202.=E9=85=92=E5=BA=97?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/CanteenDailyMenuImportExcel.java | 53 ++-- .../dto/CanteenSpuUpdateStockReqDTO.java | 4 +- .../canteen/entity/CanteenCategoryEntity.java | 4 +- .../entity/CanteenDailyMenuEntity.java | 14 +- .../canteen/entity/CanteenSpuEntity.java | 2 +- .../vo/CanteenDailyMenuImportPreviewVO.java | 9 +- .../pjcloud/canteen/vo/DailyMenuAdminVO.java | 26 +- .../bsd/pjcloud/canteen/vo/DailyMenuVO.java | 2 +- .../pjcloud/canteen/vo/SpuPrintNameVo.java | 2 +- .../canteen/vo/app/DailyMenuAppVO.java | 14 +- .../controller/CanteenCategoryController.java | 10 +- .../CanteenOnlineCategoryController.java | 5 +- .../CanteenDailyMenuTemplateMergeHandler.java | 39 +-- .../impl/CanteenDailyMenuImportHelper.java | 230 ++++++++++++---- .../impl/CanteenDailyMenuServiceImpl.java | 53 +++- .../impl/CanteenDailyMenuStockSupport.java | 248 +++++++++++++++--- .../service/impl/CanteenSpuServiceImpl.java | 188 +++++++++++-- .../mapper/CanteenDailyMenuMapper.xml | 32 ++- .../SmHotelCallbackTaskController.java | 4 +- .../shangyou/ShangyouCallbackQueueWorker.java | 38 ++- 20 files changed, 776 insertions(+), 201 deletions(-) diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenDailyMenuImportExcel.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenDailyMenuImportExcel.java index 541824a2e..fde920572 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenDailyMenuImportExcel.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenDailyMenuImportExcel.java @@ -7,11 +7,9 @@ import java.math.BigDecimal; /** * 每日菜谱导入/导出 Excel 行(导出含菜谱 ID,导入时可按 ID 更新) - *

列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、线上库存、线下库存、配料、菜谱ID、菜品SPU ID、子SPU ID、填写说明 - *

线上/线下库存挂在每日菜谱行上,不再写入菜品列表 {@code canteen_spu.stock}。 + *

列顺序:日期、餐段、套餐名/别名、类型、菜品名、分类、单价、套餐价、总库存、移动端可预定数量、配料、菜谱ID、菜品SPU ID、子SPU ID、填写说明 + *

库存:总库存→{@code offline_stock};移动端可预定数量→{@code online_stock};消费机可售=总库存−移动端已预订。 *

菜谱ID / 菜品SPU ID / 子SPU ID 必须用文本导出(带制表符前缀),否则 Excel 对超长雪花 ID 会丢精度。 - *

业务列与 ID 列导出时每行写全,均不合并。 - *

无「菜品SPU ID」时一律新建菜品/套餐;无「子SPU ID」时组成菜按菜名+配料回退或新建。 */ @Data public class CanteenDailyMenuImportExcel { @@ -22,63 +20,70 @@ public class CanteenDailyMenuImportExcel { @ExcelProperty(value = "餐段", index = 1) private String mealName; - @ExcelProperty(value = "套餐名", index = 2) + /** + * 套餐:同一套餐标签(多行组成菜共用);单品:填「无」或本排菜别名。 + */ + @ExcelProperty(value = "套餐名/别名", index = 2) private String comboName; - @ExcelProperty(value = "菜品名", index = 3) + /** + * 排菜类型:套餐 / 单品。留空则自动识别。 + */ + @ExcelProperty(value = "类型", index = 3) + private String menuType; + + @ExcelProperty(value = "菜品名", index = 4) private String dishName; /** 分类名称;新建菜品时优先按此匹配,匹配不到再用默认分类 */ - @ExcelProperty(value = "分类", index = 4) + @ExcelProperty(value = "分类", index = 5) private String categoryName; /** 菜品自身单价;不填默认 0 */ - @ExcelProperty(value = "单价(元)", index = 5) + @ExcelProperty(value = "单价(元)", index = 6) private BigDecimal dishPriceYuan; /** 套餐售价(套餐首行填写);单品可留空 */ - @ExcelProperty(value = "套餐价(元)", index = 6) + @ExcelProperty(value = "套餐价(元)", index = 7) private BigDecimal priceYuan; /** - * 当日线上可预约库存(挂每日菜谱)。单品填本行;套餐组成行建议每行都填相同值。 - * 不填:默认 600。 + * 总库存(入库 {@code offline_stock})。单品填本行;套餐组成行建议每行同值。不填默认 600。 */ - @ExcelProperty(value = "线上库存", index = 7) - private Integer onlineStock; - - /** - * 当日线下销售库存(挂每日菜谱,安卓消费机读此池)。单品填本行;套餐组成行建议每行都填相同值。 - * 不填:默认 600。 - */ - @ExcelProperty(value = "线下库存", index = 8) + @ExcelProperty(value = "总库存", index = 8) private Integer offlineStock; - @ExcelProperty(value = "配料", index = 9) + /** + * 移动端可预定数量上限(入库 {@code online_stock})。不填默认 600。 + */ + @ExcelProperty(value = "移动端可预定数量", index = 9) + private Integer onlineStock; + + @ExcelProperty(value = "配料", index = 10) private String ingredients; /** * 每日菜谱记录 ID(文本)。导入时有则更新,无则新增。 * 勿改成数字格式,否则 Excel 会丢精度。 */ - @ExcelProperty(value = "菜谱ID", index = 10, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class) + @ExcelProperty(value = "菜谱ID", index = 11, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class) private String id; /** * 关联菜品 SPU ID(文本)。套餐块各行填同一套餐本体 ID(导出可合并单元格)。 * 单品行=该单品 ID。导入:有则按 ID 更新;无则新建。 */ - @ExcelProperty(value = "菜品SPU ID", index = 11, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class) + @ExcelProperty(value = "菜品SPU ID", index = 12, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class) private String spuId; /** * 套餐组成菜 SPU ID(文本,每行一个,不合并)。 * 有则按 ID 更新/复用组成菜;无则按「菜名+配料」回退或新建。单品行可留空。 */ - @ExcelProperty(value = "子SPU ID", index = 12, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class) + @ExcelProperty(value = "子SPU ID", index = 13, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class) private String childSpuId; /** 仅示例说明用,导入逻辑忽略 */ - @ExcelProperty(value = "填写说明", index = 13) + @ExcelProperty(value = "填写说明", index = 14) private String tip; } diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuUpdateStockReqDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuUpdateStockReqDTO.java index 8e27c48fb..6ef408ff2 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuUpdateStockReqDTO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuUpdateStockReqDTO.java @@ -34,13 +34,13 @@ public class CanteenSpuUpdateStockReqDTO { /** * 每日菜谱分池:ONLINE=线上可预约;OFFLINE=线下消费机。 - * 空则仍扣 {@code canteen_spu.stock}(兼容非每日菜谱)。 + * 空则默认 OFFLINE(兼容未传通道的安卓消费机),扣每日菜谱线下销量。 */ @Pattern(regexp = "ONLINE|OFFLINE", message = "库存通道错误") private String stockChannel; /** - * 每日菜谱所属商户;stockChannel 非空时必填。 + * 每日菜谱所属商户;分池扣减时建议传,空则从 SPU 回填。 */ private Long merchantId; diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenCategoryEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenCategoryEntity.java index 0d520452e..4aef1cfc7 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenCategoryEntity.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenCategoryEntity.java @@ -54,9 +54,9 @@ public class CanteenCategoryEntity extends Model { private String picUrl; /** - * 分类排序 + * 分类序号(越大越靠前) */ - @Schema(description="分类排序") + @Schema(description="分类序号,越大越靠前") private Integer sort; /** diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDailyMenuEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDailyMenuEntity.java index c173cd4c2..4a57aebb5 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDailyMenuEntity.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDailyMenuEntity.java @@ -43,6 +43,12 @@ public class CanteenDailyMenuEntity extends Model { @Schema(description="商品 SPU 编号") private Long spuId; + /** + * 本排菜展示名:套餐名或单品别名(仅挂菜谱行,不写回菜品) + */ + @Schema(description = "套餐名或单品别名") + private String displayName; + /** * 归属时间 1早餐 2午餐 3晚餐 */ @@ -50,15 +56,15 @@ public class CanteenDailyMenuEntity extends Model { private Integer hourType; /** - * 线上可预约库存配额(人工设置;下单不改;剩余=配额−onlineSold) + * 移动端可预订数量上限(人工;下单不改) */ - @Schema(description = "线上可预约库存配额") + @Schema(description = "移动端可预订数量上限") private Integer onlineStock; /** - * 线下销售库存配额(人工设置;下单不改;剩余=配额−offlineSold) + * 总库存(人工;下单不改)。消费机可售 = 总库存 − 移动端已预订。 */ - @Schema(description = "线下销售库存配额") + @Schema(description = "总库存") private Integer offlineStock; /** diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java index ac5c94838..3c0c46104 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuEntity.java @@ -46,7 +46,7 @@ public class CanteenSpuEntity extends Model { private String nameEn; /** - * 套餐名(仅 spuType=2 时使用) + * 套餐名(仅 spuType=2 套餐本体;每日排菜展示名见 canteen_daily_menu.display_name) */ @Schema(description = "套餐名") private String comboName; diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/CanteenDailyMenuImportPreviewVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/CanteenDailyMenuImportPreviewVO.java index 1d4b27102..d861b49c0 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/CanteenDailyMenuImportPreviewVO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/CanteenDailyMenuImportPreviewVO.java @@ -44,7 +44,7 @@ public class CanteenDailyMenuImportPreviewVO { @Schema(description = "餐段") private String mealName; - @Schema(description = "套餐名") + @Schema(description = "套餐名/别名") private String comboName; @Schema(description = "菜品名") @@ -59,10 +59,10 @@ public class CanteenDailyMenuImportPreviewVO { @Schema(description = "套餐价(元)") private BigDecimal priceYuan; - @Schema(description = "线上库存(每日)") + @Schema(description = "移动端可预定数量") private Integer onlineStock; - @Schema(description = "线下库存(每日)") + @Schema(description = "总库存") private Integer offlineStock; @Schema(description = "配料") @@ -77,6 +77,9 @@ public class CanteenDailyMenuImportPreviewVO { @Schema(description = "子SPU ID(套餐组成菜)") private String childSpuId; + @Schema(description = "类型:套餐/单品,空则自动识别") + private String menuType; + /** * 排菜分组键:同一套餐多行组成菜共用,前端按此统计「一道菜」而不是 Excel 行数。 */ diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java index 957122a0a..b8069a8d3 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java @@ -27,9 +27,14 @@ public class DailyMenuAdminVO { private String name; - /** 套餐名(canteen_spu.combo_name),导出 Excel「套餐名」列优先 */ + /** + * 展示名:优先排菜 display_name,空则回落菜品 combo_name(列表/App 展示) + */ private String comboName; + /** 排菜 display_name 原值(导出单品「套餐名/别名」列用,不含菜品回落) */ + private String displayName; + /** 套餐标签兜底(canteen_spu.introduction),combo_name 为空时导出用 */ private String introduction; @@ -44,31 +49,36 @@ public class DailyMenuAdminVO { private Integer sort; - /** 线下库存配额(人工) */ + /** + * 编辑提交时传「总库存」写入库 {@code offline_stock}(列表不再单独展示消费机可销售数量)。 + */ private Integer offlineStock; - /** 线上可预约库存配额(人工) */ + /** 移动端可预订数量上限(库 {@code online_stock}) */ private Integer onlineStock; + /** 总库存(库 {@code offline_stock}) */ + private Integer totalStock; + /** * 兼容旧字段:对安卓/列表默认展示「线下剩余」。 * 管理端同时看 remainOnlineStock / remainOfflineStock。 */ private Integer stock; - /** 线上净销量(仅服务端填剩余用,可不展示) */ + /** 移动端已订数量(管理端列表展示) */ private Integer onlineSold; - /** 线下净销量(仅服务端填剩余用,可不展示) */ + /** 消费机已售净销量(管理端列表展示) */ private Integer offlineSold; - /** 线上剩余 = onlineStock − 预约净销量 */ + /** 移动端可预订剩余(服务端计算用,管理端列表改展示 onlineSold) */ private Integer remainOnlineStock; - /** 线下剩余 = offlineStock − 线下净销量 */ + /** 剩余总库存(与 remainStock 同义,兼容字段) */ private Integer remainOfflineStock; - /** 剩余合计(线上剩余+线下剩余),兼容旧列 remainStock */ + /** 剩余总库存 */ private Integer remainStock; private Integer price; diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuVO.java index ed20e4797..2576cc0c0 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuVO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuVO.java @@ -54,7 +54,7 @@ public class DailyMenuVO { private Integer spuType; /** - * 套餐名(canteen_spu.combo_name) + * 套餐名或单品别名(canteen_spu.combo_name) */ private String comboName; diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java index e44f2d44b..3ecdc186f 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/SpuPrintNameVo.java @@ -27,7 +27,7 @@ public class SpuPrintNameVo { private Long productId; /** - * 套餐名(仅 spuType=2 套餐有值),打印凭证优先展示。 + * 套餐名(canteen_spu.combo_name);每日排菜别名见 display_name,打印若需排菜名需另查菜谱。 */ @Schema(description = "套餐名") private String comboName; diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java index 054402af2..fe00ceef7 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java @@ -93,26 +93,26 @@ public class DailyMenuAppVO { /** * 当前可售库存。 - *

App/移动端:线上剩余;安卓消费机 AD 接口:线下剩余。字段名保持 stock 兼容旧客户端。 + *

App/移动端:移动端可预订剩余;AD:总库存 − 移动端已预订。字段名保持 stock 兼容旧客户端。 */ private Integer stock; - /** 线上配额(人工;下单不改) */ + /** 移动端可预订上限(库 online_stock) */ private Integer onlineStock; - /** 线下配额(人工;下单不改) */ + /** 总库存(库 offline_stock) */ private Integer offlineStock; - /** 线上净销量(服务端计算剩余用) */ + /** 移动端已预订净销量 */ private Integer onlineSold; - /** 线下净销量(服务端计算剩余用) */ + /** 消费机已售净销量 */ private Integer offlineSold; - /** 线上剩余 = onlineStock − onlineSold */ + /** 移动端可预订剩余 */ private Integer remainOnlineStock; - /** 线下剩余 = offlineStock − offlineSold */ + /** App:剩余总库存;AD:与 stock 同为 总库存−移动端已预订 */ private Integer remainOfflineStock; /** diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java index 554e76954..84ac42a10 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java @@ -55,6 +55,8 @@ public class CanteenCategoryController { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.like(StrUtil.isNotBlank(canteenCategory.getName()), CanteenCategoryEntity::getName, canteenCategory.getName()); merchantScopeHelper.applyMerchantIdScope(wrapper, canteenCategory.getMerchantId(), CanteenCategoryEntity::getMerchantId); + // 分类序号越大越靠前 + wrapper.orderByDesc(CanteenCategoryEntity::getSort).orderByDesc(CanteenCategoryEntity::getId); return R.ok(canteenCategoryService.selectPageByScope(page, wrapper)); } @@ -136,7 +138,10 @@ public class CanteenCategoryController { @GetMapping("/export") @PreAuthorize("@pms.hasPermission('canteen_canteenCategory_export')") public List export(CanteenCategoryEntity canteenCategory, Long[] ids) { - return canteenCategoryService.list(Wrappers.lambdaQuery(canteenCategory).in(ArrayUtil.isNotEmpty(ids), CanteenCategoryEntity::getId, ids)); + return canteenCategoryService.list(Wrappers.lambdaQuery(canteenCategory) + .in(ArrayUtil.isNotEmpty(ids), CanteenCategoryEntity::getId, ids) + .orderByDesc(CanteenCategoryEntity::getSort) + .orderByDesc(CanteenCategoryEntity::getId)); } @@ -159,7 +164,8 @@ public class CanteenCategoryController { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.like(StringUtils.isNotBlank(name), CanteenCategoryEntity::getName, name); merchantScopeHelper.applyMerchantIdScope(wrapper, merchantId, CanteenCategoryEntity::getMerchantId); - wrapper.orderByAsc(CanteenCategoryEntity::getSort); + // 分类序号越大越靠前 + wrapper.orderByDesc(CanteenCategoryEntity::getSort).orderByDesc(CanteenCategoryEntity::getId); wrapper.apply(StringUtils.isNotEmpty(categoryType), "FIND_IN_SET({0}, category_type)", categoryType); List list = canteenCategoryService.listScope(wrapper); return R.ok(list); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineCategoryController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineCategoryController.java index 5300b5309..66830c4f3 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineCategoryController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenOnlineCategoryController.java @@ -56,6 +56,8 @@ public class CanteenOnlineCategoryController { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.like(StrUtil.isNotBlank(canteenOnlineCategory.getName()),CanteenOnlineCategoryEntity::getName,canteenOnlineCategory.getName()); merchantScopeHelper.applyMerchantIdScope(wrapper, canteenOnlineCategory.getMerchantId(), CanteenOnlineCategoryEntity::getMerchantId); + // 分类序号越大越靠前 + wrapper.orderByDesc(CanteenOnlineCategoryEntity::getSort).orderByDesc(CanteenOnlineCategoryEntity::getId); return R.ok(canteenOnlineCategoryService.pageScope(page, wrapper)); } @@ -153,7 +155,8 @@ public class CanteenOnlineCategoryController { @GetMapping("/app/getAllSimple") public R getAllSimple(@RequestParam(value = "merchantId")Long merchantId) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); - wrapper.orderByAsc(CanteenOnlineCategoryEntity::getSort); + // 分类序号越大越靠前 + wrapper.orderByDesc(CanteenOnlineCategoryEntity::getSort).orderByDesc(CanteenOnlineCategoryEntity::getId); wrapper.eq(CanteenOnlineCategoryEntity::getMerchantId,merchantId); // wrapper.apply(StringUtils.isNotEmpty(categoryType),"FIND_IN_SET({0}, your_column_name)",categoryType); List list = canteenOnlineCategoryService.list(wrapper); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenDailyMenuTemplateMergeHandler.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenDailyMenuTemplateMergeHandler.java index 9fa844628..03b7b0a7d 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenDailyMenuTemplateMergeHandler.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/excel/CanteenDailyMenuTemplateMergeHandler.java @@ -12,8 +12,8 @@ import org.apache.poi.ss.usermodel.VerticalAlignment; /** * 每日菜谱导入模板 / 导出写后处理。 - *

列:0日期 1餐段 2套餐名 3菜品名 4分类 5单价 6套餐价 7线上库存 8线下库存 9配料 - * 10菜谱ID 11菜品SPU ID 12子SPU ID 13填写说明 + *

列:0日期 1餐段 2套餐名/别名 3类型 4菜品名 5分类 6单价 7套餐价 8总库存 9移动端可预定数量 10配料 + * 11菜谱ID 12菜品SPU ID 13子SPU ID 14填写说明 *

业务列每行写全、不合并。雪花 ID 三列也每行写全、不合并 * (合并会导致 EasyExcel 读空;Excel 把 19 位 ID 当数字会丢精度并串号)。 *

须配合 {@code @ResponseExcel(inMemory = true)},否则 SXSSF 刷盘后改单元格会失败。 @@ -23,12 +23,14 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle private static final int COL_DATE = 0; private static final int COL_MEAL = 1; private static final int COL_COMBO = 2; - private static final int COL_COMBO_PRICE = 6; - private static final int COL_ONLINE_STOCK = 7; - private static final int COL_OFFLINE_STOCK = 8; - private static final int COL_ID = 10; - private static final int COL_SPU = 11; - private static final int COL_CHILD_SPU = 12; + private static final int COL_COMBO_PRICE = 7; + /** 总库存(Excel index 8 → offlineStock 字段) */ + private static final int COL_TOTAL_STOCK = 8; + /** 移动端可预定数量(Excel index 9 → onlineStock 字段) */ + private static final int COL_MOBILE_QUOTA = 9; + private static final int COL_ID = 11; + private static final int COL_SPU = 12; + private static final int COL_CHILD_SPU = 13; /** 写入 ID 前缀,迫使 Excel/WPS 保持文本,避免 19 位雪花被当数字丢精度 */ public static final String ID_TEXT_PREFIX = "\t"; @@ -82,8 +84,8 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle String spuId = cellText(sheet, r, COL_SPU); String childSpuId = cellText(sheet, r, COL_CHILD_SPU); String comboPrice = cellText(sheet, r, COL_COMBO_PRICE); - String online = cellText(sheet, r, COL_ONLINE_STOCK); - String offline = cellText(sheet, r, COL_OFFLINE_STOCK); + String online = cellText(sheet, r, COL_TOTAL_STOCK); + String offline = cellText(sheet, r, COL_MOBILE_QUOTA); if (isBlank(date)) { date = lastDate; @@ -194,8 +196,8 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle writeText(sheet, r, COL_COMBO, combo, center); if (inCombo && !isSingleCombo(combo)) { writeText(sheet, r, COL_COMBO_PRICE, comboPrice, center); - writeText(sheet, r, COL_ONLINE_STOCK, online, center); - writeText(sheet, r, COL_OFFLINE_STOCK, offline, center); + writeText(sheet, r, COL_TOTAL_STOCK, online, center); + writeText(sheet, r, COL_MOBILE_QUOTA, offline, center); } writeIdText(sheet, r, COL_ID, menuId, idTextStyle); writeIdText(sheet, r, COL_SPU, spuId, idTextStyle); @@ -332,17 +334,18 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle private static void setColumnWidths(Sheet sheet) { sheet.setColumnWidth(0, 12 * 256); sheet.setColumnWidth(1, 8 * 256); - sheet.setColumnWidth(2, 10 * 256); - sheet.setColumnWidth(3, 16 * 256); - sheet.setColumnWidth(4, 12 * 256); + sheet.setColumnWidth(2, 12 * 256); + sheet.setColumnWidth(3, 8 * 256); + sheet.setColumnWidth(4, 16 * 256); sheet.setColumnWidth(5, 12 * 256); sheet.setColumnWidth(6, 12 * 256); - sheet.setColumnWidth(7, 10 * 256); + sheet.setColumnWidth(7, 12 * 256); sheet.setColumnWidth(8, 10 * 256); - sheet.setColumnWidth(9, 22 * 256); + sheet.setColumnWidth(9, 10 * 256); sheet.setColumnWidth(10, 22 * 256); sheet.setColumnWidth(11, 22 * 256); sheet.setColumnWidth(12, 22 * 256); - sheet.setColumnWidth(13, 48 * 256); + sheet.setColumnWidth(13, 22 * 256); + sheet.setColumnWidth(14, 48 * 256); } } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuImportHelper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuImportHelper.java index 27e88bcc3..6e28f7bb4 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuImportHelper.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuImportHelper.java @@ -97,26 +97,28 @@ public class CanteenDailyMenuImportHelper { public List templateRows() { List list = new ArrayList<>(); // 业务列与雪花 ID 均每行写全(文本),避免 Excel 数字精度串号 - list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "80", "100", - "套餐多行:日期/餐段/套餐名/套餐价/库存/菜谱ID 每行都填;菜品SPU ID=套餐本体;子SPU ID=本行组成菜;ID列必须为文本")); - list.add(row("2026-08-17", "午餐", "A套餐", "蒜苔炒腊肉", "荤菜", null, "8", "21", "80", "100", null)); - list.add(row("2026-08-17", "午餐", "A套餐", "清炒时蔬", "素菜", null, "3", "21", "80", "100", null)); - list.add(row("2026-08-17", "午餐", "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "60", "80", - "换套餐名即另一套;无菜谱ID时新建;有菜谱ID/套餐SPU则更新;打开Excel后勿把ID列改成数字")); - list.add(row("2026-08-17", "午餐", "B套餐", "土豆丝", "素菜", null, "4", "18", "60", "80", null)); - list.add(row("2026-08-17", "午餐", "无", "千张大骨汤", "汤类", null, "0", null, "50", "50", - "单品:套餐名填「无」;菜品SPU ID=本行单品;子SPU ID可空")); - list.add(row("2026-08-17", "早餐", "单品-1", "榨菜肉丝面", "面食", "榨菜,肉丝", "6.4", null, "30", "40", - "单品-xxx / 单点-xxx 一律按单品,套餐价可留空;不填库存则默认600")); - list.add(row("2026-08-17", "早餐", "单点-2", "豆浆", "饮品", null, "2", null, "100", "100", null)); - list.add(row("2026-08-18", "午餐,晚餐", "无", "番茄蛋汤", "汤类", null, "2", null, "80", "80", - "多餐拆行:无菜品SPU ID时各餐段各自新建菜品;每日线上/线下库存各自独立")); + // 库存列:总库存、移动端可预定数量(消费机可售=总库存−移动端已预订,不单独导入) + list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "100", "80", "套餐", + "总库存+移动端可预定;类型填套餐/单品;菜品SPU ID=套餐本体;子SPU ID=组成菜")); + list.add(row("2026-08-17", "午餐", "A套餐", "蒜苔炒腊肉", "荤菜", null, "8", "21", "100", "80", "套餐", null)); + list.add(row("2026-08-17", "午餐", "A套餐", "清炒时蔬", "素菜", null, "3", "21", "100", "80", "套餐", null)); + list.add(row("2026-08-17", "午餐", "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "80", "60", "套餐", + "换套餐名即另一套;无菜谱ID时新建;有菜谱ID/套餐SPU则更新")); + list.add(row("2026-08-17", "午餐", "B套餐", "土豆丝", "素菜", null, "4", "18", "80", "60", "套餐", null)); + list.add(row("2026-08-17", "午餐", "招牌汤", "千张大骨汤", "汤类", null, "0", null, "50", "50", "单品", + "单品:类型=单品;套餐名/别名可填别名或「无」;勿填套餐价")); + list.add(row("2026-08-17", "早餐", "单品-1", "榨菜肉丝面", "面食", "榨菜,肉丝", "6.4", null, "40", "30", "单品", + "不填库存则总库存/移动端可预定默认各600")); + list.add(row("2026-08-17", "早餐", "单点-2", "豆浆", "饮品", null, "2", null, "100", "100", "单品", null)); + list.add(row("2026-08-18", "午餐,晚餐", "无", "番茄蛋汤", "汤类", null, "2", null, "80", "80", "单品", + "多餐拆行:无菜品SPU ID时各餐段各自新建;线上未订完可线下继续卖")); return list; } + /** @param totalStock 总库存;@param mobileQuota 移动端可预定数量 */ private static CanteenDailyMenuImportExcel row(String date, String meal, String combo, String dish, - String categoryName, String ingredients, String dishPrice, String comboPrice, String onlineStock, - String offlineStock, String tip) { + String categoryName, String ingredients, String dishPrice, String comboPrice, String totalStock, + String mobileQuota, String menuType, String tip) { CanteenDailyMenuImportExcel r = new CanteenDailyMenuImportExcel(); r.setBizDate(date); r.setMealName(meal); @@ -130,12 +132,13 @@ public class CanteenDailyMenuImportHelper { if (StrUtil.isNotBlank(comboPrice)) { r.setPriceYuan(new BigDecimal(comboPrice)); } - if (StrUtil.isNotBlank(onlineStock)) { - r.setOnlineStock(Integer.valueOf(onlineStock)); + if (StrUtil.isNotBlank(totalStock)) { + r.setOfflineStock(Integer.valueOf(totalStock)); } - if (StrUtil.isNotBlank(offlineStock)) { - r.setOfflineStock(Integer.valueOf(offlineStock)); + if (StrUtil.isNotBlank(mobileQuota)) { + r.setOnlineStock(Integer.valueOf(mobileQuota)); } + r.setMenuType(menuType); r.setTip(tip); return r; } @@ -212,8 +215,9 @@ public class CanteenDailyMenuImportHelper { row.setCategoryName(menu.getCategoryName()); row.setDishName(StrUtil.blankToDefault(item.getName(), "")); row.setDishPriceYuan(fenToYuan(item.getPrice())); + row.setMenuType("套餐"); if (written == 0) { - row.setTip("套餐块:同套餐名多行=一道排菜;菜谱ID/菜品SPU ID 每行都填(文本),勿改成数字"); + row.setTip("套餐块:类型=套餐;同套餐名多行=一道排菜;菜谱ID/菜品SPU ID 每行都填(文本)"); } rows.add(row); written++; @@ -225,13 +229,15 @@ public class CanteenDailyMenuImportHelper { row.setSpuId(menu.getSpuId() == null ? null : String.valueOf(menu.getSpuId())); row.setBizDate(dateStr); row.setMealName(meal); - row.setComboName("无"); + // 单品:有排菜别名则写入本列,否则「无」 + row.setComboName(StrUtil.blankToDefault(StrUtil.trim(menu.getDisplayName()), "无")); row.setDishName(menu.getName()); row.setCategoryName(menu.getCategoryName()); row.setDishPriceYuan(fenToYuan(menu.getPrice())); row.setOnlineStock(menu.getOnlineStock()); row.setOfflineStock(menu.getOfflineStock()); row.setIngredients(toExcelIngredients(menu.getIngredients())); + row.setMenuType("单品"); rows.add(row); } return rows; @@ -611,16 +617,7 @@ public class CanteenDailyMenuImportHelper { if (menuId != null) { excelMenuIds.add(menuId); } - String groupKey; - if (menuId != null) { - groupKey = "id:" + menuId + "|" + date + "|" + hourType; - } - else if (StrUtil.isNotBlank(comboKey) && !isSingleComboMarker(comboKey)) { - groupKey = "new:" + date + "|" + hourType + "|" + comboKey; - } - else { - groupKey = "new:" + date + "|" + hourType + "|" + dish; - } + String groupKey = resolveImportGroupKey(menuId, date, hourType, comboKey, dish, row); groups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(row); } if (CollUtil.isNotEmpty(errors)) { @@ -727,6 +724,7 @@ public class CanteenDailyMenuImportHelper { previewRow.setId(StrUtil.trim(item.getId())); previewRow.setSpuId(StrUtil.trim(item.getSpuId())); previewRow.setChildSpuId(StrUtil.trim(item.getChildSpuId())); + previewRow.setMenuType(resolvePreviewMenuType(items, item, cache)); previewRow.setGroupKey(groupKey); previewRow.setAction(action); previewRow.setActionLabel(actionLabel); @@ -784,16 +782,7 @@ public class CanteenDailyMenuImportHelper { if (menuId != null) { excelMenuIds.add(menuId); } - String groupKey; - if (menuId != null) { - groupKey = "id:" + menuId + "|" + date + "|" + hourType; - } - else if (StrUtil.isNotBlank(comboKey) && !isSingleComboMarker(comboKey)) { - groupKey = "new:" + date + "|" + hourType + "|" + comboKey; - } - else { - groupKey = "new:" + date + "|" + hourType + "|" + dish; - } + String groupKey = resolveImportGroupKey(menuId, date, hourType, comboKey, dish, row); groups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(row); } if (CollUtil.isNotEmpty(errors)) { @@ -887,7 +876,7 @@ public class CanteenDailyMenuImportHelper { Integer hourType = groupHour; String comboName = StrUtil.trim(first.getComboName()); - if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) { + if (!isComboImportGroup(items, comboName, cache)) { for (CanteenDailyMenuImportExcel item : items) { // 每日线上/线下库存挂菜谱行,不再写入 canteen_spu.stock FindOrCreate r = findOrCreateDish(merchantId, deptId, @@ -898,7 +887,8 @@ public class CanteenDailyMenuImportHelper { createdDish++; } if (schedule(merchantId, deptId, date, hourType, r.spu.getId(), replaceDay, clearedDayHour, - cache, pendingMenus, item.getOnlineStock(), item.getOfflineStock())) { + cache, pendingMenus, item.getOnlineStock(), item.getOfflineStock(), + resolveSingleDisplayName(item))) { scheduled++; } appendDish(st, item.getDishName(), hourType); @@ -955,7 +945,7 @@ public class CanteenDailyMenuImportHelper { createdCombo++; } if (schedule(merchantId, deptId, date, hourType, combo.spu.getId(), replaceDay, clearedDayHour, - cache, pendingMenus, onlineStock, offlineStock)) { + cache, pendingMenus, onlineStock, offlineStock, sanitizeMenuDisplayName(comboName))) { scheduled++; } } @@ -1510,7 +1500,8 @@ public class CanteenDailyMenuImportHelper { } CanteenDailyMenuImportExcel first = items.get(0); String comboName = StrUtil.trim(first.getComboName()); - boolean wantsCombo = StrUtil.isNotBlank(comboName) && !isSingleComboMarker(comboName); + // 与导入一致:单行别名≠套餐;多行/套餐价/子SPU/套餐SPU 才算套餐 + boolean wantsCombo = isComboImportGroup(items, comboName, cache); CanteenSpuEntity menuSpu = menu.getSpuId() == null ? null : resolveSpuById(menu.getSpuId(), cache); int menuSpuType = menuSpu == null || menuSpu.getSpuType() == null ? 1 : menuSpu.getSpuType(); boolean menuIsCombo = menuSpuType == 2; @@ -1593,7 +1584,7 @@ public class CanteenDailyMenuImportHelper { } String comboName = StrUtil.trim(first.getComboName()); try { - if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) { + if (!isComboImportGroup(items, comboName, cache)) { return upsertSingleById(menu, first, merchantId, deptId, defaultCategoryId, categoryMap, date, hourType, cache); } @@ -1618,6 +1609,7 @@ public class CanteenDailyMenuImportHelper { menu.setStartTime(date); menu.setHourType(hourType); menu.setSpuId(created.spu.getId()); + menu.setDisplayName(resolveSingleDisplayName(row)); menu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(row.getOnlineStock())); menu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(row.getOfflineStock())); canteenDailyMenuService.updateById(menu); @@ -1710,6 +1702,8 @@ public class CanteenDailyMenuImportHelper { targetMenu.setStartTime(date); targetMenu.setHourType(hourType); targetMenu.setSpuId(combo.spu.getId()); + // 套餐展示名挂排菜;Excel「套餐名」列即短标签 + targetMenu.setDisplayName(sanitizeMenuDisplayName(comboName)); targetMenu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(onlineStock)); targetMenu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(offlineStock)); canteenDailyMenuService.updateById(targetMenu); @@ -1829,7 +1823,7 @@ public class CanteenDailyMenuImportHelper { */ private boolean schedule(Long merchantId, Long deptId, LocalDate date, Integer hourType, Long spuId, boolean replaceDay, Set clearedDayHour, ImportCache cache, - List pendingMenus, Integer onlineStock, Integer offlineStock) { + List pendingMenus, Integer onlineStock, Integer offlineStock, String displayName) { String dh = date + "|" + hourType; if (replaceDay && !clearedDayHour.contains(dh)) { canteenDailyMenuService.remove(Wrappers.lambdaQuery() @@ -1849,6 +1843,7 @@ public class CanteenDailyMenuImportHelper { menu.setStartTime(date); menu.setHourType(hourType); menu.setSpuId(spuId); + menu.setDisplayName(displayName); menu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(onlineStock)); menu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(offlineStock)); pendingMenus.add(menu); @@ -1856,6 +1851,144 @@ public class CanteenDailyMenuImportHelper { return true; } + /** + * 导入分组键:显式「单品」按菜名分组;显式「套餐」或非标记套餐名按套餐名分组。 + */ + private static String resolveImportGroupKey(Long menuId, LocalDate date, Integer hourType, String comboKey, + String dish, CanteenDailyMenuImportExcel row) { + if (menuId != null) { + return "id:" + menuId + "|" + date + "|" + hourType; + } + Boolean explicit = parseMenuType(row == null ? null : row.getMenuType()); + if (Boolean.FALSE.equals(explicit)) { + return "new:" + date + "|" + hourType + "|dish:" + dish; + } + if (Boolean.TRUE.equals(explicit) + || (StrUtil.isNotBlank(comboKey) && !isSingleComboMarker(comboKey))) { + String key = StrUtil.blankToDefault(comboKey, dish); + return "new:" + date + "|" + hourType + "|combo:" + key; + } + return "new:" + date + "|" + hourType + "|dish:" + dish; + } + + /** + * 是否按套餐导入:类型列优先;否则多行同名 / 子SPU / 套餐价 / 菜品SPU为套餐 → 套餐。 + */ + private boolean isComboImportGroup(List items, String comboName, ImportCache cache) { + if (CollUtil.isEmpty(items)) { + return false; + } + Boolean explicit = resolveGroupMenuType(items); + if (Boolean.FALSE.equals(explicit)) { + return false; + } + if (Boolean.TRUE.equals(explicit)) { + return true; + } + if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) { + return false; + } + if (items.size() >= 2) { + return true; + } + CanteenDailyMenuImportExcel first = items.get(0); + if (first == null) { + return false; + } + if (StrUtil.isNotBlank(first.getChildSpuId())) { + return true; + } + if (first.getPriceYuan() != null) { + return true; + } + Long spuId = parseExcelLong(first.getSpuId()); + if (spuId != null) { + CanteenSpuEntity spu = resolveSpuById(spuId, cache); + if (spu != null && spu.getSpuType() != null && spu.getSpuType() == 2) { + return true; + } + } + return false; + } + + /** 组内类型列:有明确值时以首个非空为准 */ + private static Boolean resolveGroupMenuType(List items) { + if (CollUtil.isEmpty(items)) { + return null; + } + for (CanteenDailyMenuImportExcel item : items) { + Boolean t = parseMenuType(item == null ? null : item.getMenuType()); + if (t != null) { + return t; + } + } + return null; + } + + /** + * 解析类型列:true=套餐,false=单品,null=未填/无法识别(走自动规则)。 + * 旧模板把「填写说明」读到本列时按未填处理。 + */ + private static Boolean parseMenuType(String raw) { + String s = StrUtil.trim(raw); + if (StrUtil.isBlank(s)) { + return null; + } + if (s.length() > 16 || StrUtil.contains(s, "填写") || StrUtil.contains(s, "导入") + || StrUtil.contains(s, "Excel") || StrUtil.contains(s, "菜谱ID") || StrUtil.contains(s, "类型")) { + return null; + } + String n = s.toLowerCase(); + if ("套餐".equals(s) || "combo".equals(n) || "2".equals(s) || "是".equals(s) || "y".equals(n) + || "yes".equals(n)) { + return Boolean.TRUE; + } + if ("单品".equals(s) || "单点".equals(s) || "single".equals(n) || "1".equals(s) || "否".equals(s) + || "n".equals(n) || "no".equals(n)) { + return Boolean.FALSE; + } + return null; + } + + /** 预览展示用类型文案 */ + private String resolvePreviewMenuType(List items, CanteenDailyMenuImportExcel item, + ImportCache cache) { + String raw = item == null ? null : StrUtil.trim(item.getMenuType()); + Boolean explicit = parseMenuType(raw); + if (Boolean.TRUE.equals(explicit)) { + return "套餐"; + } + if (Boolean.FALSE.equals(explicit)) { + return "单品"; + } + String comboName = StrUtil.trim(item == null ? null : item.getComboName()); + return isComboImportGroup(items, comboName, cache) ? "套餐" : "单品"; + } + + /** 单品排菜展示名:取「套餐名/别名」列(「无」等标记视为无别名) */ + private static String resolveSingleDisplayName(CanteenDailyMenuImportExcel row) { + if (row == null) { + return null; + } + return sanitizeMenuDisplayName(row.getComboName()); + } + + /** 排菜展示名清洗:过滤说明文案与单品标记 */ + private static String sanitizeMenuDisplayName(String raw) { + String s = StrUtil.trim(raw); + if (StrUtil.isBlank(s)) { + return null; + } + if (s.length() > 64 || StrUtil.contains(s, "填写") || StrUtil.contains(s, "导入") + || StrUtil.contains(s, "Excel") || StrUtil.contains(s, "菜谱ID")) { + return null; + } + if (isSingleComboMarker(s)) { + return null; + } + return s; + } + /** * 解析/创建单品。 *

有「菜品SPU ID」/「子SPU ID」→ 按 ID 复用并按需更新名称、配料、价格、分类;无 ID → 一律新建,不再按菜名复用。 @@ -2550,6 +2683,7 @@ public class CanteenDailyMenuImportHelper { copy.setId(src.getId()); copy.setSpuId(src.getSpuId()); copy.setChildSpuId(src.getChildSpuId()); + copy.setMenuType(src.getMenuType()); copy.setTip(src.getTip()); return copy; } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java index f2234db08..d8b411975 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java @@ -543,6 +543,54 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl menuSpuIds = menus.stream().map(CanteenDailyMenuEntity::getSpuId).filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map> comboMap = + canteenSpuService.loadComboItemVos(new ArrayList<>(menuSpuIds)); + Set menuKeys = menus.stream() + .filter(m -> m.getHourType() != null && m.getSpuId() != null) + .map(m -> m.getHourType() + "|" + m.getSpuId()) + .collect(Collectors.toSet()); + Map linkedSold = new HashMap<>(); + for (Map.Entry e : soldByHourProduct.entrySet()) { + String[] parts = e.getKey().split("\\|", 2); + if (parts.length != 2) { + continue; + } + Long comboSpuId; + try { + comboSpuId = Long.valueOf(parts[1]); + } + catch (Exception ignored) { + continue; + } + List lines = comboMap.get(comboSpuId); + if (CollUtil.isEmpty(lines)) { + continue; + } + String hour = parts[0]; + int[] comboPair = e.getValue(); + for (com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO line : lines) { + if (line == null || line.getChildSpuId() == null) { + continue; + } + String childKey = hour + "|" + line.getChildSpuId(); + if (!menuKeys.contains(childKey)) { + continue; + } + int qty = line.getQuantity() == null || line.getQuantity() < 1 ? 1 : line.getQuantity(); + int[] childPair = linkedSold.computeIfAbsent(childKey, ignored -> new int[] { 0, 0 }); + childPair[0] = Math.addExact(childPair[0], Math.multiplyExact(comboPair[0], qty)); + childPair[1] = Math.addExact(childPair[1], Math.multiplyExact(comboPair[1], qty)); + } + } + for (Map.Entry e : linkedSold.entrySet()) { + int[] pair = soldByHourProduct.computeIfAbsent(e.getKey(), ignored -> new int[] { 0, 0 }); + pair[0] = Math.addExact(pair[0], e.getValue()[0]); + pair[1] = Math.addExact(pair[1], e.getValue()[1]); + } + List toUpdate = new ArrayList<>(); for (CanteenDailyMenuEntity menu : menus) { if (menu.getHourType() == null || menu.getSpuId() == null) { @@ -641,8 +689,8 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl - * {@code online_stock}/{@code offline_stock} 为人工配额(下单不改); - * {@code online_sold}/{@code offline_sold} 为净销量;剩余 = 配额 − 销量。 - * App 的 {@code stock}=线上剩余,安卓 AD 的 {@code stock}=线下剩余。 + * 库字段约定(不改表结构): + *

+ * 共享总库存:线上未订完可继续线下卖。 + *
+ * 剩余总库存 = max(0, 总库存 − 移动端已预订 − 消费机已销售)
+ * 移动端可预订剩余 = min(移动端上限 − 已预订, 剩余总库存)
+ * AD stock = max(0, 总库存 − 移动端已预订)
+ * 
+ * PC 管理端只展示总库存、移动端上限、移动端已订、消费机已售、剩余总库存。 + * 扣减仍受「剩余总库存」边界约束,防止超卖。 */ @Component @RequiredArgsConstructor @@ -37,20 +49,73 @@ public class CanteenDailyMenuStockSupport { return sold == null || sold < 0 ? 0 : sold; } - /** 剩余 = max(0, 配额 − 销量) */ + /** @deprecated 独立分池已废弃,请用 {@link #sharedRemain} / {@link #onlineRemain} */ public static int remainOf(Integer quota, Integer sold) { return Math.max(0, quotaOrDefault(quota) - soldOrDefault(sold)); } + /** 总库存(库字段 offline_stock) */ + public static int totalStockOf(Integer offlineStockAsTotal) { + return quotaOrDefault(offlineStockAsTotal); + } + + public static int totalStockOf(CanteenDailyMenuEntity menu) { + return menu == null ? 0 : totalStockOf(menu.getOfflineStock()); + } + + /** 移动端可预订上限(库字段 online_stock) */ + public static int mobileQuotaOf(CanteenDailyMenuEntity menu) { + return menu == null ? 0 : quotaOrDefault(menu.getOnlineStock()); + } + + /** 剩余总库存 = 总库存 − 线上已订 − 线下已售 */ + public static int sharedRemain(Integer totalStock, Integer onlineSold, Integer offlineSold) { + return Math.max(0, totalStockOf(totalStock) - soldOrDefault(onlineSold) - soldOrDefault(offlineSold)); + } + + public static int sharedRemain(CanteenDailyMenuEntity menu) { + if (menu == null) { + return 0; + } + return sharedRemain(menu.getOfflineStock(), menu.getOnlineSold(), menu.getOfflineSold()); + } + + /** 移动端可预订剩余 */ + public static int onlineRemain(Integer totalStock, Integer mobileQuota, Integer onlineSold, Integer offlineSold) { + int shared = sharedRemain(totalStock, onlineSold, offlineSold); + int mobileLeft = Math.max(0, quotaOrDefault(mobileQuota) - soldOrDefault(onlineSold)); + return Math.min(mobileLeft, shared); + } + + public static int onlineRemain(CanteenDailyMenuEntity menu) { + if (menu == null) { + return 0; + } + return onlineRemain(menu.getOfflineStock(), menu.getOnlineStock(), menu.getOnlineSold(), menu.getOfflineSold()); + } + + /** + * AD 返回的 stock:总库存 − 移动端已预订(不含消费机已售;已售由 offline_sold 单独体现)。 + */ + public static int adStockOf(Integer totalStock, Integer onlineSold) { + return Math.max(0, totalStockOf(totalStock) - soldOrDefault(onlineSold)); + } + + public static int adStockOf(CanteenDailyMenuEntity menu) { + if (menu == null) { + return 0; + } + return adStockOf(menu.getOfflineStock(), menu.getOnlineSold()); + } + public static int channelRemain(CanteenDailyMenuEntity menu, boolean offline) { if (menu == null) { return 0; } - return offline ? remainOf(menu.getOfflineStock(), menu.getOfflineSold()) - : remainOf(menu.getOnlineStock(), menu.getOnlineSold()); + return offline ? adStockOf(menu) : onlineRemain(menu); } - /** App:stock = 线上剩余;onlineStock/offlineStock 仍透出配额 */ + /** App:stock = 线上剩余 */ public void fillAppOnlineRemain(List rows, Long merchantId, LocalDate mealDate) { if (CollUtil.isEmpty(rows)) { return; @@ -58,9 +123,10 @@ public class CanteenDailyMenuStockSupport { for (DailyMenuAppVO row : rows) { applyAppQuotasAndRemains(row, true); } + applyAppComboLinkedRemain(rows, true); } - /** 安卓消费机:stock = 线下剩余(字段名不变) */ + /** 安卓消费机:stock = 总库存 − 移动端已预订 */ public void fillAdOfflineRemain(List rows, Long merchantId, LocalDate mealDate) { if (CollUtil.isEmpty(rows)) { return; @@ -68,6 +134,7 @@ public class CanteenDailyMenuStockSupport { for (DailyMenuAppVO row : rows) { applyAppQuotasAndRemains(row, false); } + applyAppComboLinkedRemain(rows, false); } public void fillAdOfflineRemainByRowDate(List rows, Long merchantId) { @@ -87,45 +154,158 @@ public class CanteenDailyMenuStockSupport { } } - /** PC:配额与剩余分列展示 */ + /** PC:总库存 / 移动端上限 / 已订与已售 / 剩余总库存 */ public void fillAdminRemain(List rows, Long merchantId, LocalDate mealDate) { if (CollUtil.isEmpty(rows)) { return; } + Map byMealSpu = new HashMap<>(); for (DailyMenuAdminVO row : rows) { - int onlineQuota = quotaOrDefault(row.getOnlineStock()); - int offlineQuota = quotaOrDefault(row.getOfflineStock()); - int onlineRemain = remainOf(onlineQuota, row.getOnlineSold()); - int offlineRemain = remainOf(offlineQuota, row.getOfflineSold()); - row.setOnlineStock(onlineQuota); - row.setOfflineStock(offlineQuota); + // 入库:offlineStock=总库存,onlineStock=移动端上限 + int total = totalStockOf(row.getOfflineStock()); + int mobile = quotaOrDefault(row.getOnlineStock()); + int onlineSold = soldOrDefault(row.getOnlineSold()); + int offlineSold = soldOrDefault(row.getOfflineSold()); + int shared = sharedRemain(total, onlineSold, offlineSold); + int onlineRemain = onlineRemain(total, mobile, onlineSold, offlineSold); + row.setTotalStock(total); + row.setOnlineStock(mobile); + // offlineStock 保持总库存原值,便于编辑回传;列表展示用 totalStock / offlineSold + row.setOfflineStock(total); + row.setOfflineSold(offlineSold); + row.setOnlineSold(onlineSold); row.setRemainOnlineStock(onlineRemain); - row.setRemainOfflineStock(offlineRemain); - row.setStock(offlineRemain); - row.setRemainStock(onlineRemain + offlineRemain); + row.setRemainOfflineStock(shared); + row.setStock(shared); + row.setRemainStock(shared); + if (row.getSpuId() != null) { + byMealSpu.put(mealSpuKey(row.getHourType(), row.getSpuId()), row); + } + } + for (DailyMenuAdminVO row : rows) { + if (!Objects.equals(row.getSpuType(), 2) || CollUtil.isEmpty(row.getComboItems())) { + continue; + } + int formableOnline = formableFromChildren(row.getComboItems(), byMealSpu, row.getHourType(), true); + int formableOffline = formableFromChildren(row.getComboItems(), byMealSpu, row.getHourType(), false); + if (formableOnline >= 0) { + row.setRemainOnlineStock(Math.min(row.getRemainOnlineStock(), formableOnline)); + } + if (formableOffline >= 0) { + row.setRemainOfflineStock(Math.min(row.getRemainOfflineStock(), formableOffline)); + } + row.setStock(row.getRemainOfflineStock()); + row.setRemainStock(row.getRemainOfflineStock()); } } private static void applyAppQuotasAndRemains(DailyMenuAppVO row, boolean stockAsOnline) { - int onlineQuota = quotaOrDefault(row.getOnlineStock()); - int offlineQuota = quotaOrDefault(row.getOfflineStock()); - int onlineRemain = remainOf(onlineQuota, row.getOnlineSold()); - int offlineRemain = remainOf(offlineQuota, row.getOfflineSold()); - row.setOnlineStock(onlineQuota); - row.setOfflineStock(offlineQuota); + int total = totalStockOf(row.getOfflineStock()); + int mobile = quotaOrDefault(row.getOnlineStock()); + int onlineSold = soldOrDefault(row.getOnlineSold()); + int offlineSold = soldOrDefault(row.getOfflineSold()); + int shared = sharedRemain(total, onlineSold, offlineSold); + int onlineRemain = onlineRemain(total, mobile, onlineSold, offlineSold); + int adStock = adStockOf(total, onlineSold); + row.setOnlineStock(mobile); + row.setOfflineStock(total); + row.setOnlineSold(onlineSold); + row.setOfflineSold(offlineSold); row.setRemainOnlineStock(onlineRemain); - row.setRemainOfflineStock(offlineRemain); - row.setStock(stockAsOnline ? onlineRemain : offlineRemain); + // AD:remainOfflineStock / stock = 总库存 − 线上已订;App 侧 remainOffline 仍给剩余总库存供参考 + row.setRemainOfflineStock(stockAsOnline ? shared : adStock); + row.setStock(stockAsOnline ? onlineRemain : adStock); } /** - * 安卓 SPU 查看接口:若当日菜谱存在对应行,则 {@code stock} 覆盖为线下剩余;无菜谱行则保留 canteen_spu.stock。 - * - * @param rows 已组装的 AD VO(含 merchantId、categoryType) - * @param menus 当日(或指定日)相关每日菜谱行 - * @param hourTypeOverride 请求显式餐段;为空时从 VO.categoryType 解析 - * @param offline true=线下剩余(AD),false=线上剩余(App 详情复用) + * 套餐有效剩余 = min(套餐行剩余, 各组成菜在当日同餐段可组成份数)。 */ + private static void applyAppComboLinkedRemain(List rows, boolean stockAsOnline) { + if (CollUtil.isEmpty(rows)) { + return; + } + Map byMealSpu = new HashMap<>(); + for (DailyMenuAppVO row : rows) { + if (row != null && row.getSpuId() != null) { + byMealSpu.put(mealSpuKey(row.getHourType(), row.getSpuId()), row); + } + } + for (DailyMenuAppVO row : rows) { + if (row == null || !Objects.equals(row.getSpuType(), 2) || CollUtil.isEmpty(row.getComboItems())) { + continue; + } + int formable = formableFromAppChildren(row.getComboItems(), byMealSpu, row.getHourType(), stockAsOnline); + if (formable < 0) { + continue; + } + int own = stockAsOnline ? nullToZero(row.getRemainOnlineStock()) : nullToZero(row.getRemainOfflineStock()); + int effective = Math.min(own, formable); + if (stockAsOnline) { + row.setRemainOnlineStock(effective); + } + else { + row.setRemainOfflineStock(effective); + } + row.setStock(effective); + } + } + + private static int formableFromAppChildren(List items, Map byMealSpu, + Integer hourType, boolean online) { + if (CollUtil.isEmpty(items) || byMealSpu == null || byMealSpu.isEmpty()) { + return -1; + } + int formable = Integer.MAX_VALUE; + boolean any = false; + for (OnlineSpuComboItemVO item : items) { + if (item == null || item.getChildSpuId() == null) { + continue; + } + DailyMenuAppVO child = byMealSpu.get(mealSpuKey(hourType, item.getChildSpuId())); + if (child == null) { + continue; + } + any = true; + int qty = item.getQuantity() == null || item.getQuantity() < 1 ? 1 : item.getQuantity(); + int childRemain = online ? nullToZero(child.getRemainOnlineStock()) + : nullToZero(child.getRemainOfflineStock()); + formable = Math.min(formable, childRemain / qty); + } + return any ? formable : -1; + } + + private static int formableFromChildren(List items, Map byMealSpu, + Integer hourType, boolean online) { + if (CollUtil.isEmpty(items) || byMealSpu == null || byMealSpu.isEmpty()) { + return -1; + } + int formable = Integer.MAX_VALUE; + boolean any = false; + for (OnlineSpuComboItemVO item : items) { + if (item == null || item.getChildSpuId() == null) { + continue; + } + DailyMenuAdminVO child = byMealSpu.get(mealSpuKey(hourType, item.getChildSpuId())); + if (child == null) { + continue; + } + any = true; + int qty = item.getQuantity() == null || item.getQuantity() < 1 ? 1 : item.getQuantity(); + int childRemain = online ? nullToZero(child.getRemainOnlineStock()) + : nullToZero(child.getRemainOfflineStock()); + formable = Math.min(formable, childRemain / qty); + } + return any ? formable : -1; + } + + private static String mealSpuKey(Integer hourType, Long spuId) { + return (hourType == null ? "_" : hourType) + "|" + spuId; + } + + private static int nullToZero(Integer v) { + return v == null ? 0 : v; + } + public void overlaySpuViewStock(List rows, List menus, Integer hourTypeOverride, boolean offline) { if (CollUtil.isEmpty(rows) || CollUtil.isEmpty(menus)) { @@ -144,7 +324,6 @@ public class CanteenDailyMenuStockSupport { } Integer hourType = hourTypeOverride != null ? hourTypeOverride : parseHourType(row.getCategoryType()); if (hourType == null) { - // 分类未标餐段时:同一商户+SPU 当日若只有一行菜谱,仍可覆盖 CanteenDailyMenuEntity only = findUniqueMenu(menus, row.getMerchantId(), row.getId()); if (only != null) { row.setStock(channelRemain(only, offline)); @@ -178,7 +357,6 @@ public class CanteenDailyMenuStockSupport { return merchantId + "|" + hourType + "|" + spuId; } - /** categoryType 支持 "2" 或 "2,3";取第一个有效餐段。 */ public static Integer parseHourType(String categoryType) { if (StrUtil.isBlank(categoryType)) { return null; diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java index 9a2d6766c..4e42cc7d2 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenSpuServiceImpl.java @@ -84,6 +84,8 @@ public class CanteenSpuServiceImpl extends ServiceImpl items = expandDailyMenuLinkedStockItems(dto.getMerchantId(), offline, + dto.getItems(), false); + if (CollUtil.isEmpty(items)) { + return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_ITEMS_EMPTY)); + } + Map remainCursor = new HashMap<>(); + for (CanteenSpuUpdateStockReqDTO.Item item : items) { if (item == null || item.getId() == null || item.getIncrCount() == null || item.getIncrCount() == 0 || item.getIncrCount() == Integer.MIN_VALUE || item.getMealDate() == null || item.getHourType() == null) { @@ -233,17 +273,19 @@ public class CanteenSpuServiceImpl extends ServiceImpl CanteenDailyMenuStockSupport.channelRemain(menu, offline)); if (item.getIncrCount() < 0 && Math.abs(item.getIncrCount()) > current) { CanteenSpuEntity spu = baseMapper.selectById(item.getId()); return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_PRODUCT_STOCK_LOW, StrUtil.blankToDefault(spu == null ? null : spu.getName(), "商品"))); } + remainCursor.put(key, Math.addExact(current, item.getIncrCount())); } - for (CanteenSpuUpdateStockReqDTO.Item item : dto.getItems()) { + for (CanteenSpuUpdateStockReqDTO.Item item : items) { int updated = offline ? canteenDailyMenuMapper.updateOfflineStockDelta(dto.getMerchantId(), item.getMealDate(), item.getHourType(), item.getId(), item.getIncrCount(), null) @@ -259,11 +301,11 @@ public class CanteenSpuServiceImpl extends ServiceImpl item.getMealDate() == null || item.getHourType() == null)) { return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_BIZ_PARAM_BAD)); } - boolean offline = "OFFLINE".equals(StrUtil.trim(dto.getStockChannel())); + boolean offline = !"ONLINE".equals(StrUtil.trim(dto.getStockChannel())); String stateKey = ORDER_STOCK_BIZ_KEY_PREFIX + "daily:" + dto.getStockChannel() + ":" + dto.getBizId(); String state = stringRedisTemplate.opsForValue().get(stateKey); if (applyOperation && ("APPLIED".equals(state) || "ROLLED_BACK".equals(state))) { @@ -272,13 +314,19 @@ public class CanteenSpuServiceImpl extends ServiceImpl items = expandDailyMenuLinkedStockItems(dto.getMerchantId(), offline, + dto.getItems(), true); + Map remainCursor = new HashMap<>(); + for (CanteenSpuUpdateStockReqDTO.Item item : items) { CanteenDailyMenuEntity menu = findDailyMenuRow(dto.getMerchantId(), item.getMealDate(), item.getHourType(), item.getId()); if (menu == null) { return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_DAILY_MENU_PRODUCT_BAD)); } - int current = CanteenDailyMenuStockSupport.channelRemain(menu, offline); + String key = dailyStockItemKey(item); + int current = remainCursor.computeIfAbsent(key, + k -> CanteenDailyMenuStockSupport.channelRemain(menu, offline)); int expectedStock = item.getBeforeStock(); if (expectedStock < 0) { return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_SNAPSHOT_BAD)); @@ -288,8 +336,6 @@ public class CanteenSpuServiceImpl extends ServiceImpl 0 ? null : item.getBeforeStock(); updated = offline ? canteenDailyMenuMapper.updateOfflineStockDelta(dto.getMerchantId(), item.getMealDate(), @@ -341,6 +386,113 @@ public class CanteenSpuServiceImpl extends ServiceImpl expandDailyMenuLinkedStockItems(Long merchantId, boolean offline, + List source, boolean fillMissingBeforeStock) { + if (CollUtil.isEmpty(source)) { + return List.of(); + } + Map callerBefore = new HashMap<>(); + for (CanteenSpuUpdateStockReqDTO.Item item : source) { + if (item != null && item.getId() != null && item.getMealDate() != null && item.getHourType() != null + && item.getBeforeStock() != null) { + callerBefore.put(dailyStockItemKey(item), item.getBeforeStock()); + } + } + Set spuIds = source.stream().filter(Objects::nonNull).map(CanteenSpuUpdateStockReqDTO.Item::getId) + .filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); + Map spuMap = spuIds.isEmpty() ? Map.of() + : listByIds(spuIds).stream().collect(Collectors.toMap(CanteenSpuEntity::getId, s -> s, (a, b) -> a)); + List comboIds = spuMap.values().stream() + .filter(s -> s != null && Objects.equals(s.getSpuType(), 2) && s.getId() != null) + .map(CanteenSpuEntity::getId).distinct().collect(Collectors.toList()); + Map> comboMap = loadComboItems(comboIds); + + List expanded = new ArrayList<>(); + for (CanteenSpuUpdateStockReqDTO.Item item : source) { + if (item == null) { + continue; + } + expanded.add(copyStockItem(item)); + CanteenSpuEntity spu = spuMap.get(item.getId()); + if (spu == null || !Objects.equals(spu.getSpuType(), 2)) { + continue; + } + List lines = comboMap.get(item.getId()); + if (CollUtil.isEmpty(lines)) { + continue; + } + for (CanteenSpuComboItemEntity line : lines) { + if (line == null || line.getChildSpuId() == null) { + continue; + } + // 组成菜未排进当日同餐段则不联动 + if (findDailyMenuRow(merchantId, item.getMealDate(), item.getHourType(), + line.getChildSpuId()) == null) { + continue; + } + int qty = line.getQuantity() == null || line.getQuantity() < 1 ? 1 : line.getQuantity(); + CanteenSpuUpdateStockReqDTO.Item child = new CanteenSpuUpdateStockReqDTO.Item(); + child.setId(line.getChildSpuId()); + child.setIncrCount(Math.multiplyExact(item.getIncrCount(), qty)); + child.setMealDate(item.getMealDate()); + child.setHourType(item.getHourType()); + expanded.add(child); + } + } + + Map merged = new LinkedHashMap<>(); + for (CanteenSpuUpdateStockReqDTO.Item item : expanded) { + if (item.getId() == null || item.getMealDate() == null || item.getHourType() == null + || item.getIncrCount() == null || item.getIncrCount() == 0) { + continue; + } + String key = dailyStockItemKey(item); + CanteenSpuUpdateStockReqDTO.Item exist = merged.get(key); + if (exist == null) { + merged.put(key, copyStockItem(item)); + } + else { + exist.setIncrCount(Math.addExact(exist.getIncrCount(), item.getIncrCount())); + } + } + + List result = new ArrayList<>(merged.size()); + for (CanteenSpuUpdateStockReqDTO.Item item : merged.values()) { + if (item.getIncrCount() == null || item.getIncrCount() == 0) { + continue; + } + Integer before = callerBefore.get(dailyStockItemKey(item)); + if (before == null && fillMissingBeforeStock) { + CanteenDailyMenuEntity menu = findDailyMenuRow(merchantId, item.getMealDate(), item.getHourType(), + item.getId()); + before = menu == null ? 0 : CanteenDailyMenuStockSupport.channelRemain(menu, offline); + } + if (before != null) { + item.setBeforeStock(before); + } + result.add(item); + } + return result; + } + + private static CanteenSpuUpdateStockReqDTO.Item copyStockItem(CanteenSpuUpdateStockReqDTO.Item src) { + CanteenSpuUpdateStockReqDTO.Item copy = new CanteenSpuUpdateStockReqDTO.Item(); + copy.setId(src.getId()); + copy.setIncrCount(src.getIncrCount()); + copy.setBeforeStock(src.getBeforeStock()); + copy.setMealDate(src.getMealDate()); + copy.setHourType(src.getHourType()); + return copy; + } + + private static String dailyStockItemKey(CanteenSpuUpdateStockReqDTO.Item item) { + return item.getMealDate() + "|" + item.getHourType() + "|" + item.getId(); + } + private CanteenDailyMenuEntity findDailyMenuRow(Long merchantId, java.time.LocalDate mealDate, Integer hourType, Long spuId) { return canteenDailyMenuMapper.selectOne(Wrappers.lambdaQuery() diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml index f0081ee1b..aeca63884 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml @@ -6,6 +6,7 @@ + @@ -62,7 +63,7 @@ sp.dept_id, t.create_by, IFNULL(sp.spu_type, 1) as spu_type, - sp.combo_name, + COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name, sp.introduction FROM canteen_daily_menu t LEFT JOIN canteen_spu sp @@ -86,7 +87,7 @@ sp.id AS spu_id, sp.name, sp.name_en, - sp.combo_name, + COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name, sp.price, sp.market_price, sp.pic_url, @@ -141,7 +142,7 @@ sp.id AS spu_id, sp.name, sp.name_en, - sp.combo_name, + COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name, sp.price, sp.market_price, sp.pic_url, @@ -184,7 +185,7 @@ sp.id AS spu_id, sp.name, sp.name_en, - sp.combo_name, + COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name, sp.price, sp.market_price, sp.pic_url, @@ -232,7 +233,7 @@ s.id as spu_id, s.`name`, s.name_en, - s.combo_name, + COALESCE(NULLIF(TRIM(m.display_name), ''), NULLIF(TRIM(s.combo_name), '')) AS combo_name, s.price, IFNULL(m.online_stock, 600) as stock, IFNULL(m.online_stock, 600) as online_stock, @@ -274,7 +275,7 @@ s.id AS spu_id, s.name, s.name_en, - s.combo_name, + COALESCE(NULLIF(TRIM(m.display_name), ''), NULLIF(TRIM(s.combo_name), '')) AS combo_name, s.price, s.market_price, IFNULL(m.online_stock, 600) AS stock, @@ -309,7 +310,8 @@ t.merchant_id, sp.id AS spu_id, sp.name, - sp.combo_name, + COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name, + t.display_name AS display_name, sp.introduction, sp.price, sp.market_price, @@ -350,6 +352,12 @@ sold 可能小于恢复量,退款/补偿不能被统计口径阻断),配额为 0 等脏数据同样不得阻断; 负增量扣减保留原「不扣成负销量、不超配额」边界,语义不变。 --> + UPDATE canteen_daily_menu SET online_sold = GREATEST(IFNULL(online_sold, 0) - #{delta}, 0) @@ -361,9 +369,13 @@ AND IFNULL(online_sold, 0) - #{delta} >= 0 AND IFNULL(online_sold, 0) - #{delta} <= IFNULL(online_stock, 600) + AND IFNULL(online_sold, 0) + IFNULL(offline_sold, 0) - #{delta} <= IFNULL(offline_stock, 600) - AND (IFNULL(online_stock, 600) - IFNULL(online_sold, 0)) = #{expected} + AND LEAST( + GREATEST(IFNULL(online_stock, 600) - IFNULL(online_sold, 0), 0), + GREATEST(IFNULL(offline_stock, 600) - IFNULL(online_sold, 0) - IFNULL(offline_sold, 0), 0) + ) = #{expected} @@ -377,10 +389,10 @@ AND spu_id = #{spuId} AND IFNULL(offline_sold, 0) - #{delta} >= 0 - AND IFNULL(offline_sold, 0) - #{delta} <= IFNULL(offline_stock, 600) + AND IFNULL(online_sold, 0) + IFNULL(offline_sold, 0) - #{delta} <= IFNULL(offline_stock, 600) - AND (IFNULL(offline_stock, 600) - IFNULL(offline_sold, 0)) = #{expected} + AND GREATEST(IFNULL(offline_stock, 600) - IFNULL(online_sold, 0), 0) = #{expected} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelCallbackTaskController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelCallbackTaskController.java index d87e01cc8..737e4e3cb 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelCallbackTaskController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelCallbackTaskController.java @@ -61,7 +61,7 @@ public class SmHotelCallbackTaskController { @SysLog("按时间区间删除酒店回调任务") @Operation(summary = "按创建时间区间删除回调任务") - @DeleteMapping("/range") + @PostMapping("/range") @PreAuthorize("@pms.hasPermission('supermarket_smHotelCallback_del')") public R removeRange(@RequestParam(required = false) String createTimeBegin, @RequestParam(required = false) String createTimeEnd) { @@ -70,7 +70,7 @@ public class SmHotelCallbackTaskController { @SysLog("清空酒店回调任务") @Operation(summary = "清空全部回调任务并清理 Redis 队列") - @DeleteMapping("/clear") + @PostMapping("/clear") @PreAuthorize("@pms.hasPermission('supermarket_smHotelCallback_del')") public R clearAll() { return callbackTaskService.clearAllTasks(); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouCallbackQueueWorker.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouCallbackQueueWorker.java index f10cef582..156399f48 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouCallbackQueueWorker.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouCallbackQueueWorker.java @@ -7,10 +7,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.stereotype.Component; -import java.time.Duration; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -64,16 +65,9 @@ public class ShangyouCallbackQueueWorker implements ApplicationRunner { int consecutiveFail = 0; while (running.get()) { try { - // BRPOP 多 key:先高优(订单状态)再普通;阻塞期间新到的高优也会被优先弹出 - List popped = redisTemplate.opsForList().rightPop( - List.of(ShangyouHotelApi.REDIS_CALLBACK_QUEUE_HIGH, - ShangyouHotelApi.REDIS_CALLBACK_QUEUE), - Duration.ofSeconds(5)); - if (popped == null || popped.size() < 2 || popped.get(1) == null) { - continue; - } - String taskId = popped.get(1).trim(); - if (taskId.isBlank() || "null".equalsIgnoreCase(taskId)) { + // 底层 BRPOP 多 key:先高优再普通(ListOperations 无多 key 重载) + String taskId = brpopPreferHigh(5); + if (taskId == null || taskId.isBlank() || "null".equalsIgnoreCase(taskId)) { continue; } Long id; @@ -125,6 +119,28 @@ public class ShangyouCallbackQueueWorker implements ApplicationRunner { } } + /** + * Redis BRPOP:按 key 顺序优先弹出高优队列。 + * 当前 Spring Data Redis 的 {@code ListOperations#rightPop} 不支持多 key,走 connection 原生命令。 + */ + @SuppressWarnings("unchecked") + private String brpopPreferHigh(int timeoutSeconds) { + RedisSerializer keySer = (RedisSerializer) redisTemplate.getKeySerializer(); + RedisSerializer valSer = (RedisSerializer) redisTemplate.getValueSerializer(); + byte[] highKey = keySer.serialize(ShangyouHotelApi.REDIS_CALLBACK_QUEUE_HIGH); + byte[] normalKey = keySer.serialize(ShangyouHotelApi.REDIS_CALLBACK_QUEUE); + if (highKey == null || normalKey == null) { + return null; + } + List popped = redisTemplate.execute((RedisCallback>) connection -> + connection.listCommands().bRPop(timeoutSeconds, highKey, normalKey)); + if (popped == null || popped.size() < 2 || popped.get(1) == null) { + return null; + } + String taskId = valSer.deserialize(popped.get(1)); + return taskId == null ? null : taskId.trim(); + } + /** 不依赖 Worker 空闲:定时扫卡死的 RUNNING */ private void watchdogLoop() { while (running.get()) { From 658b0999522f06b62c449a074a4a8673ef208a93 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Thu, 8 Oct 2026 19:09:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?1.=E6=AF=8F=E6=97=A5=E8=8F=9C=E5=93=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BF=AE=E6=94=B9=202.=E9=85=92=E5=BA=97?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../constant/SmHotelDataSource.java | 26 +++ .../supermarket/entity/SmHotelEntity.java | 28 ++- .../config/ShangyouHotelProperties.java | 6 + .../controller/SmHotelController.java | 4 +- .../controller/SmHotelMidController.java | 67 ++++++- .../shangyou/ShangyouHotelApi.java | 3 + .../service/SmHotelMidService.java | 27 ++- .../supermarket/service/SmHotelService.java | 2 +- .../service/SmHotelSyncService.java | 14 +- .../service/impl/SmHotelMidServiceImpl.java | 189 +++++++++++++++++- .../service/impl/SmHotelServiceImpl.java | 22 +- .../service/impl/SmHotelSyncServiceImpl.java | 59 ++++-- 12 files changed, 411 insertions(+), 36 deletions(-) create mode 100644 pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDataSource.java diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDataSource.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDataSource.java new file mode 100644 index 000000000..0876cbda0 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDataSource.java @@ -0,0 +1,26 @@ +package com.bsd.pjcloud.supermarket.constant; + +/** + * 酒店数据来源(存 canteen_hotel.sheet_name,字典类型 {@link #DICT_TYPE})。 + */ +public final class SmHotelDataSource { + + /** 字典类型 */ + public static final String DICT_TYPE = "sm_hotel_data_source"; + + /** 本地手工新增 */ + public static final String LOCAL = "local"; + + /** 商游上游同步/导入 */ + public static final String SHANGYOU = "shangyou"; + + /** 同步导入时暂存所选来源(租户 Redis 自动加前缀) */ + public static final String REDIS_IMPORT_DATA_SOURCE = "supermarket:shangyou:hotel:import:dataSource"; + + private SmHotelDataSource() { + } + + public static boolean isLocal(String dataSource) { + return LOCAL.equalsIgnoreCase(dataSource == null ? null : dataSource.trim()); + } +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java index 197bbb4d2..f9d17a117 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import java.math.BigDecimal; import java.time.LocalDateTime; @Data @@ -42,7 +43,11 @@ public class SmHotelEntity extends Model { @Schema(description = "酒店名称") private String hotelName; - @Schema(description = "导入来源Sheet名") + /** + * 数据来源(库字段 sheet_name)。存字典 {@code sm_hotel_data_source} 的 item_value, + * 如 local=本地、shangyou=商游上游;历史 Excel Sheet 名仍可能原样存于此列。 + */ + @Schema(description = "数据来源(字典 sm_hotel_data_source)") private String sheetName; @Schema(description = "ACTIVE启用 / OFFLINE上游下线停用") @@ -51,6 +56,27 @@ public class SmHotelEntity extends Model { @Schema(description = "备注") private String remark; + @Schema(description = "酒店主图") + private String coverImage; + + @Schema(description = "轮播图URL,逗号分隔") + private String carouselImages; + + @Schema(description = "详细地址") + private String address; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "联系电话") + private String phone; + + @Schema(description = "酒店简介") + private String intro; + @TableField(fill = FieldFill.INSERT) private String createBy; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java index 2c772fc4c..4f4ffaa75 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java @@ -133,6 +133,12 @@ public class ShangyouHotelProperties { /** 列表同步后是否自动把「上游有本地无」写入本地主数据(会按批循环导完差集) */ private boolean autoImportNewHotels = true; + /** + * 定时/XXL 列表同步自动导入时的默认数据来源(字典 sm_hotel_data_source)。 + * 人工同步须在前端显式选择;定时任务用此配置。 + */ + private String defaultImportDataSource = "shangyou"; + /** * 自动导入时每批条数(控内存/事务体积)。同步任务会循环多批直到差集清空,不是「总共只导这么多」。 */ diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java index b054fa850..d72365d19 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java @@ -75,10 +75,10 @@ public class SmHotelController { } @SysLog("新增酒店") - @Operation(summary = "新增") + @Operation(summary = "新增(返回已落库实体,便于继续维护房型等)") @PostMapping @PreAuthorize("@pms.hasPermission('supermarket_smHotel_add')") - public R save(@RequestBody SmHotelEntity entity) { + public R save(@RequestBody SmHotelEntity entity) { return R.ok(hotelService.saveHotel(entity)); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java index 92e9abe9e..24bd1367e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java @@ -3,6 +3,8 @@ package com.bsd.pjcloud.supermarket.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelPriceConfigEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelSyncLogEntity; import com.bsd.pjcloud.supermarket.service.SmHotelMidService; import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService; @@ -135,20 +137,23 @@ public class SmHotelMidController { } @SysLog("导入上游授权酒店到本地") - @Operation(summary = "将上游授权酒店写入本地") + @Operation(summary = "将上游授权酒店写入本地(须选数据来源)") @PostMapping("/mid/authorizedHotels/import") @PreAuthorize("@pms.hasPermission('supermarket_smHotel_add')") - public R> importAuthorizedHotels(@RequestBody List> hotels) { - return midService.importAuthorizedHotels(hotels); + public R> importAuthorizedHotels( + @RequestParam("dataSource") String dataSource, + @RequestBody List> hotels) { + return midService.importAuthorizedHotels(hotels, dataSource); } @SysLog("同步上游酒店列表并差集") - @Operation(summary = "异步同步 getHotelList + Redis 差集(立刻返回 logId,轮询进度)") + @Operation(summary = "异步同步 getHotelList + Redis 差集(立刻返回 logId;须选数据来源)") @PostMapping("/mid/syncHotelList") @PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')") public R> syncHotelList( - @RequestParam(required = false, defaultValue = "true") boolean autoImport) { - return syncService.syncHotelListAndDiff(autoImport); + @RequestParam(required = false, defaultValue = "true") boolean autoImport, + @RequestParam("dataSource") String dataSource) { + return syncService.syncHotelListAndDiff(autoImport, dataSource); } @Operation(summary = "同步日志分页") @@ -199,11 +204,55 @@ public class SmHotelMidController { } @SysLog("导入差集新增酒店") - @Operation(summary = "将差集中的新增酒店导入本地") + @Operation(summary = "将差集中的新增酒店导入本地(dataSource 空则用同步时暂存的来源)") @PostMapping("/mid/hotelDiff/importNew") @PreAuthorize("@pms.hasPermission('supermarket_smHotel_add')") - public R> importDiffNew(@RequestParam(required = false) Integer limit) { - return syncService.importDiffNewHotels(limit); + public R> importDiffNew(@RequestParam(required = false) Integer limit, + @RequestParam(required = false) String dataSource) { + return syncService.importDiffNewHotels(limit, dataSource); + } + + @SysLog("本地酒店保存房型") + @PostMapping("/local/{hotelId}/room") + @PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')") + public R saveLocalRoom(@PathVariable Long hotelId, @RequestBody SmHotelRoomEntity room) { + return midService.saveLocalRoom(hotelId, room); + } + + @SysLog("本地酒店删除房型") + @DeleteMapping("/local/{hotelId}/room/{roomId}") + @PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')") + public R removeLocalRoom(@PathVariable Long hotelId, @PathVariable Long roomId) { + return midService.removeLocalRoom(hotelId, roomId); + } + + @SysLog("本地酒店保存产品") + @PostMapping("/local/{hotelId}/product") + @PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')") + public R saveLocalProduct(@PathVariable Long hotelId, + @RequestBody SmHotelProductEntity product) { + return midService.saveLocalProduct(hotelId, product); + } + + @SysLog("本地酒店删除产品") + @DeleteMapping("/local/{hotelId}/product/{productId}") + @PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')") + public R removeLocalProduct(@PathVariable Long hotelId, @PathVariable Long productId) { + return midService.removeLocalProduct(hotelId, productId); + } + + @SysLog("本地酒店保存日历") + @PostMapping("/local/{hotelId}/daily") + @PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')") + public R saveLocalDaily(@PathVariable Long hotelId, @RequestBody SmHotelDailyEntity daily) { + return midService.saveLocalDaily(hotelId, daily); + } + + @SysLog("本地酒店删除日历") + @DeleteMapping("/local/{hotelId}/daily/{dailyId}") + @PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')") + public R removeLocalDaily(@PathVariable Long hotelId, @PathVariable Long dailyId) { + return midService.removeLocalDaily(hotelId, dailyId); } @SysLog("手动执行滚一天房价") diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java index fdadeae8b..c226d329f 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java @@ -59,6 +59,9 @@ public final class ShangyouHotelApi { /** 差集:本地有上游无(疑似下线) */ public static final String REDIS_DIFF_OFFLINE_SET = "supermarket:shangyou:hotel:diff:offline:v1"; + /** 列表同步/差集导入所选数据来源(字典值) */ + public static final String REDIS_IMPORT_DATA_SOURCE = "supermarket:shangyou:hotel:import:dataSource"; + /** 今日全量房价同步次数 */ public static final String REDIS_FULL_PRICE_SYNC_COUNT = "supermarket:shangyou:hotel:fullprice:count"; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java index 44da4e81c..2e4e1da76 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java @@ -2,6 +2,8 @@ package com.bsd.pjcloud.supermarket.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity; import com.bsd.pjcloud.supermarket.vo.SmHotelDetailVO; import com.bsd.pjcloud.common.core.util.R; @@ -56,6 +58,27 @@ public interface SmHotelMidService { /** 重建本地 hotel_code Redis SET(分批,防 OOM);供差集与列表标记复用 */ long rebuildLocalHotelCodeSet(); - /** 将选中的上游 hotelId 写入本地(不存在则新增) */ - R> importAuthorizedHotels(List> hotels); + /** + * 将选中的上游 hotelId 写入本地(不存在则新增)。 + * @param dataSource 数据来源字典值(必填,不可为 local) + */ + R> importAuthorizedHotels(List> hotels, String dataSource); + + /** 本地数据来源:保存房型 */ + R saveLocalRoom(Long hotelId, SmHotelRoomEntity room); + + /** 本地数据来源:删除房型 */ + R removeLocalRoom(Long hotelId, Long roomId); + + /** 本地数据来源:保存产品 */ + R saveLocalProduct(Long hotelId, SmHotelProductEntity product); + + /** 本地数据来源:删除产品 */ + R removeLocalProduct(Long hotelId, Long productId); + + /** 本地数据来源:保存日历行 */ + R saveLocalDaily(Long hotelId, SmHotelDailyEntity daily); + + /** 本地数据来源:删除日历行 */ + R removeLocalDaily(Long hotelId, Long dailyId); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelService.java index 5b96b5ebb..b513de3b2 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelService.java @@ -12,7 +12,7 @@ public interface SmHotelService extends IService { Page pageScope(Page page, LambdaQueryWrapper wrapper); - Boolean saveHotel(SmHotelEntity entity); + SmHotelEntity saveHotel(SmHotelEntity entity); Boolean updateHotel(SmHotelEntity entity); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java index 64b9382c0..a55e049b8 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java @@ -14,9 +14,10 @@ public interface SmHotelSyncService { /** * 提交列表同步(立刻返回 logId,后台异步执行)。 * @param autoImport 是否自动导入新增 + * @param dataSource 自动导入时写入的数据来源(字典值);空则用配置 defaultImportDataSource * @param triggerType MANUAL / XXL / CRON */ - R startListSyncAsync(boolean autoImport, String triggerType); + R startListSyncAsync(boolean autoImport, String dataSource, String triggerType); /** 查询同步任务进度/结果 */ R getSyncLog(Long id); @@ -27,8 +28,11 @@ public interface SmHotelSyncService { /** 分页查看差集:type=new|offline */ R> pageHotelDiff(String type, long current, long size); - /** 把差集「新增」全部/限额导入本地主数据 */ - R> importDiffNewHotels(Integer limit); + /** + * 把差集「新增」全部/限额导入本地主数据。 + * @param dataSource 数据来源字典值;空则读 Redis 中同步时暂存的来源 + */ + R> importDiffNewHotels(Integer limit, String dataSource); /** 提交滚价异步任务 */ R startPriceRollAsync(String triggerType); @@ -53,8 +57,8 @@ public interface SmHotelSyncService { */ int abortStaleRunningSyncLogs(); - /** 兼容旧同步入口(内部转异步) */ - R> syncHotelListAndDiff(boolean autoImport); + /** 兼容旧同步入口(内部转异步);人工同步须传 dataSource */ + R> syncHotelListAndDiff(boolean autoImport, String dataSource); /** 当前日更时间配置(给前端展示) */ Map getSyncSchedule(); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java index 395a73821..f4fd64736 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java @@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.toolkit.Db; import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties; import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties; +import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource; import com.bsd.pjcloud.supermarket.entity.*; import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelClient; import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi; @@ -603,10 +604,17 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { @Override @Transactional(rollbackFor = Exception.class) - public R> importAuthorizedHotels(List> hotels) { + public R> importAuthorizedHotels(List> hotels, String dataSource) { if (CollUtil.isEmpty(hotels)) { return R.failed("请选择要导入的酒店"); } + String source = StrUtil.trim(dataSource); + if (StrUtil.isBlank(source)) { + return R.failed("请选择数据来源"); + } + if (SmHotelDataSource.isLocal(source)) { + return R.failed("上游同步不能使用「本地」数据来源,请选择上游类来源"); + } int insert = 0; int skip = 0; for (Map row : hotels) { @@ -625,7 +633,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { SmHotelEntity hotel = new SmHotelEntity(); hotel.setHotelCode(hotelId); hotel.setHotelName(StrUtil.blankToDefault(hotelName, hotelId)); - hotel.setSheetName("上游授权列表"); + hotel.setSheetName(source); hotel.setUpstreamStatus("ACTIVE"); hotelMapper.insert(hotel); stringRedisTemplate.opsForSet().add(ShangyouHotelApi.REDIS_LOCAL_CODE_SET, hotelId); @@ -634,9 +642,186 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { Map result = new HashMap<>(4); result.put("insertCount", insert); result.put("skipCount", skip); + result.put("dataSource", source); return R.ok(result); } + @Override + @Transactional(rollbackFor = Exception.class) + public R saveLocalRoom(Long hotelId, SmHotelRoomEntity room) { + SmHotelEntity hotel = requireLocalHotel(hotelId); + if (room == null || StrUtil.isBlank(room.getRoomTypeCode())) { + return R.failed("房型编码不能为空"); + } + room.setHotelId(hotel.getId()); + room.setHotelCode(hotel.getHotelCode()); + room.setGroupCode(hotel.getGroupCode()); + if (room.getId() == null) { + Long exists = roomMapper.selectCount(Wrappers.lambdaQuery() + .eq(SmHotelRoomEntity::getHotelId, hotelId) + .eq(SmHotelRoomEntity::getRoomTypeCode, room.getRoomTypeCode().trim())); + if (exists != null && exists > 0) { + return R.failed("房型编码已存在:" + room.getRoomTypeCode()); + } + room.setRoomTypeCode(room.getRoomTypeCode().trim()); + roomMapper.insert(room); + } + else { + SmHotelRoomEntity old = roomMapper.selectById(room.getId()); + if (old == null || !Objects.equals(old.getHotelId(), hotelId)) { + return R.failed("房型不存在"); + } + room.setRoomTypeCode(room.getRoomTypeCode().trim()); + roomMapper.updateById(room); + } + return R.ok(roomMapper.selectById(room.getId())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R removeLocalRoom(Long hotelId, Long roomId) { + requireLocalHotel(hotelId); + SmHotelRoomEntity old = roomMapper.selectById(roomId); + if (old == null || !Objects.equals(old.getHotelId(), hotelId)) { + return R.failed("房型不存在"); + } + Long productCnt = productMapper.selectCount(Wrappers.lambdaQuery() + .eq(SmHotelProductEntity::getHotelId, hotelId) + .eq(SmHotelProductEntity::getRoomTypeCode, old.getRoomTypeCode())); + if (productCnt != null && productCnt > 0) { + return R.failed("请先删除该房型下的产品"); + } + return R.ok(roomMapper.deleteById(roomId) > 0); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R saveLocalProduct(Long hotelId, SmHotelProductEntity product) { + SmHotelEntity hotel = requireLocalHotel(hotelId); + if (product == null || StrUtil.isBlank(product.getProductCode())) { + return R.failed("产品编码不能为空"); + } + if (StrUtil.isBlank(product.getRoomTypeCode())) { + return R.failed("所属房型编码不能为空"); + } + Long roomCnt = roomMapper.selectCount(Wrappers.lambdaQuery() + .eq(SmHotelRoomEntity::getHotelId, hotelId) + .eq(SmHotelRoomEntity::getRoomTypeCode, product.getRoomTypeCode().trim())); + if (roomCnt == null || roomCnt <= 0) { + return R.failed("所属房型不存在,请先维护房型"); + } + product.setHotelId(hotel.getId()); + product.setHotelCode(hotel.getHotelCode()); + product.setGroupCode(hotel.getGroupCode()); + product.setProductCode(product.getProductCode().trim()); + product.setRoomTypeCode(product.getRoomTypeCode().trim()); + SmHotelRoomEntity room = roomMapper.selectOne(Wrappers.lambdaQuery() + .eq(SmHotelRoomEntity::getHotelId, hotelId) + .eq(SmHotelRoomEntity::getRoomTypeCode, product.getRoomTypeCode()) + .last("LIMIT 1")); + if (room != null) { + product.setRoomId(room.getId()); + } + if (product.getId() == null) { + Long exists = productMapper.selectCount(Wrappers.lambdaQuery() + .eq(SmHotelProductEntity::getHotelId, hotelId) + .eq(SmHotelProductEntity::getProductCode, product.getProductCode())); + if (exists != null && exists > 0) { + return R.failed("产品编码已存在:" + product.getProductCode()); + } + productMapper.insert(product); + } + else { + SmHotelProductEntity old = productMapper.selectById(product.getId()); + if (old == null || !Objects.equals(old.getHotelId(), hotelId)) { + return R.failed("产品不存在"); + } + productMapper.updateById(product); + } + return R.ok(productMapper.selectById(product.getId())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R removeLocalProduct(Long hotelId, Long productId) { + requireLocalHotel(hotelId); + SmHotelProductEntity old = productMapper.selectById(productId); + if (old == null || !Objects.equals(old.getHotelId(), hotelId)) { + return R.failed("产品不存在"); + } + dailyMapper.delete(Wrappers.lambdaQuery() + .eq(SmHotelDailyEntity::getHotelId, hotelId) + .eq(SmHotelDailyEntity::getProductId, productId)); + return R.ok(productMapper.deleteById(productId) > 0); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R saveLocalDaily(Long hotelId, SmHotelDailyEntity daily) { + SmHotelEntity hotel = requireLocalHotel(hotelId); + if (daily == null || daily.getStayDate() == null) { + return R.failed("入住日期不能为空"); + } + if (StrUtil.isBlank(daily.getProductCode())) { + return R.failed("产品编码不能为空"); + } + SmHotelProductEntity product = productMapper.selectOne(Wrappers.lambdaQuery() + .eq(SmHotelProductEntity::getHotelId, hotelId) + .eq(SmHotelProductEntity::getProductCode, daily.getProductCode().trim()) + .last("LIMIT 1")); + if (product == null) { + return R.failed("产品不存在,请先维护产品"); + } + daily.setHotelId(hotel.getId()); + daily.setProductId(product.getId()); + daily.setProductCode(product.getProductCode()); + daily.setRoomTypeCode(product.getRoomTypeCode()); + if (daily.getDailyStatus() == null) { + daily.setDailyStatus(1); + } + if (daily.getId() == null) { + SmHotelDailyEntity exist = dailyMapper.selectOne(Wrappers.lambdaQuery() + .eq(SmHotelDailyEntity::getHotelId, hotelId) + .eq(SmHotelDailyEntity::getProductCode, daily.getProductCode()) + .eq(SmHotelDailyEntity::getStayDate, daily.getStayDate()) + .last("LIMIT 1")); + if (exist != null) { + daily.setId(exist.getId()); + dailyMapper.updateById(daily); + } + else { + dailyMapper.insert(daily); + } + } + else { + SmHotelDailyEntity old = dailyMapper.selectById(daily.getId()); + if (old == null || !Objects.equals(old.getHotelId(), hotelId)) { + return R.failed("日历行不存在"); + } + dailyMapper.updateById(daily); + } + return R.ok(dailyMapper.selectById(daily.getId())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R removeLocalDaily(Long hotelId, Long dailyId) { + requireLocalHotel(hotelId); + SmHotelDailyEntity old = dailyMapper.selectById(dailyId); + if (old == null || !Objects.equals(old.getHotelId(), hotelId)) { + return R.failed("日历行不存在"); + } + return R.ok(dailyMapper.deleteById(dailyId) > 0); + } + + private SmHotelEntity requireLocalHotel(Long hotelId) { + SmHotelEntity hotel = requireHotel(hotelId); + if (!SmHotelDataSource.isLocal(hotel.getSheetName())) { + throw new IllegalArgumentException("仅「本地」数据来源的酒店可手工维护房型/产品/日历"); + } + return hotel; + } + private SmHotelEntity requireHotel(Long hotelId) { if (hotelId == null) { throw new IllegalArgumentException("酒店ID不能为空"); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java index f284f5fd2..7ada144b2 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java @@ -14,6 +14,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource; import com.bsd.pjcloud.supermarket.entity.SmHotelEntity; import com.bsd.pjcloud.supermarket.mapper.SmHotelDailyMapper; import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper; @@ -111,7 +112,7 @@ public class SmHotelServiceImpl extends ServiceImpl 0; + baseMapper.insert(entity); + return entity; } @Override @@ -142,6 +151,10 @@ public class SmHotelServiceImpl extends ServiceImpl 0; } @@ -378,6 +391,11 @@ public class SmHotelServiceImpl extends ServiceImpl startListSyncAsync(boolean autoImport, String triggerType) { + public R startListSyncAsync(boolean autoImport, String dataSource, String triggerType) { abortStaleRunningSyncLogs(); Long running = syncLogMapper.selectCount(Wrappers.lambdaQuery() .eq(SmHotelSyncLogEntity::getSyncType, TYPE_LIST) @@ -84,9 +85,19 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { if (running != null && running > 0) { return R.failed("已有列表同步任务进行中,请稍候查看进度(若长时间卡住请在同步日志点「终止」)"); } + String source = resolveImportDataSource(dataSource, triggerType); + if (StrUtil.isBlank(source)) { + return R.failed("请选择数据来源(字典 sm_hotel_data_source)"); + } + if (SmHotelDataSource.isLocal(source)) { + return R.failed("上游同步不能使用「本地」数据来源"); + } + // 供差集「补导剩余新增」复用本次选择的来源 + redisTemplate.opsForValue().set(ShangyouHotelApi.REDIS_IMPORT_DATA_SOURCE, source, 2, TimeUnit.DAYS); + SmHotelSyncLogEntity logEntity = newLog(TYPE_LIST, triggerType); logEntity.setStage("QUEUED"); - logEntity.setStageMsg("已提交,等待执行…"); + logEntity.setStageMsg("已提交,数据来源=" + source + ",等待执行…"); logEntity.setProgressPct(1); syncLogMapper.insert(logEntity); @@ -97,7 +108,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { final Long tid = tenantId; final Long logId = logEntity.getId(); final boolean doImport = autoImport; - EXECUTOR.execute(() -> TenantBroker.runAs(tid, id -> runListSync(logId, doImport))); + final String importSource = source; + EXECUTOR.execute(() -> TenantBroker.runAs(tid, id -> runListSync(logId, doImport, importSource))); return R.ok(logEntity, "已提交异步同步"); } @@ -274,8 +286,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { } @Override - public R> syncHotelListAndDiff(boolean autoImport) { - R started = startListSyncAsync(autoImport, "MANUAL"); + public R> syncHotelListAndDiff(boolean autoImport, String dataSource) { + R started = startListSyncAsync(autoImport, dataSource, "MANUAL"); if (started.getCode() != 0) { return R.failed(started.getMsg()); } @@ -283,11 +295,12 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { data.put("async", true); data.put("logId", started.getData().getId()); data.put("status", started.getData().getStatus()); + data.put("dataSource", StrUtil.trim(dataSource)); data.put("message", "已异步执行,请轮询 /mid/syncLog/{id} 或打开同步日志"); return R.ok(data, started.getMsg()); } - private void runListSync(Long logId, boolean autoImport) { + private void runListSync(Long logId, boolean autoImport, String dataSource) { long t0 = System.currentTimeMillis(); try { progress(logId, 5, "REFRESH_LIST", "正在拉取上游授权酒店列表…"); @@ -318,9 +331,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { } int pct = Math.min(78, 55 + round); progress(logId, pct, "IMPORT_NEW", - "正在自动导入新增酒店…已导入 " + imported + ",剩余约 " + remain - + "(每批 " + batchSize + ")"); - R> imp = importDiffNewHotels(batchSize); + "正在自动导入新增酒店(来源 " + dataSource + ")…已导入 " + imported + ",剩余约 " + + remain + "(每批 " + batchSize + ")"); + R> imp = importDiffNewHotels(batchSize, dataSource); int n = 0; if (imp != null && imp.getData() != null && imp.getData().get("insertCount") != null) { n = Integer.parseInt(String.valueOf(imp.getData().get("insertCount"))); @@ -947,7 +960,17 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { } @Override - public R> importDiffNewHotels(Integer limit) { + public R> importDiffNewHotels(Integer limit, String dataSource) { + String source = StrUtil.trim(dataSource); + if (StrUtil.isBlank(source)) { + source = redisTemplate.opsForValue().get(ShangyouHotelApi.REDIS_IMPORT_DATA_SOURCE); + } + if (StrUtil.isBlank(source)) { + source = StrUtil.trim(properties.getDefaultImportDataSource()); + } + if (StrUtil.isBlank(source)) { + return R.failed("请选择数据来源后再导入"); + } int max = limit == null || limit <= 0 ? properties.getAutoImportBatchSize() : limit; max = Math.min(Math.max(max, 1), 5000); List> batch = new ArrayList<>(); @@ -973,7 +996,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { empty.put("skipCount", 0); return R.ok(empty, "无待导入酒店"); } - R> r = midService.importAuthorizedHotels(batch); + R> r = midService.importAuthorizedHotels(batch, source); if (r != null && r.getCode() == 0) { for (Map row : batch) { Object id = row.get("hotelId"); @@ -986,6 +1009,18 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { return r; } + /** 人工同步必须显式传;定时任务用配置 defaultImportDataSource */ + private String resolveImportDataSource(String dataSource, String triggerType) { + String source = StrUtil.trim(dataSource); + if (StrUtil.isNotBlank(source)) { + return source; + } + if (!"MANUAL".equalsIgnoreCase(StrUtil.trim(triggerType))) { + return StrUtil.trim(properties.getDefaultImportDataSource()); + } + return null; + } + @Override public Map getSyncSchedule() { Map m = new LinkedHashMap<>(); @@ -1014,7 +1049,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { return Map.of("skipped", true, "reason", "hotel-list-daily-sync-enabled=false"); } R r = startListSyncAsync(properties.isAutoImportNewHotels(), - StrUtil.blankToDefault(triggerType, "XXL")); + properties.getDefaultImportDataSource(), StrUtil.blankToDefault(triggerType, "XXL")); if (r.getCode() != 0) { return Map.of("ok", false, "msg", r.getMsg()); }