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}=线下剩余。
+ * 库字段约定(不改表结构):
+ *
+ * - {@code offline_stock} = 总库存(人工)
+ * - {@code online_stock} = 移动端可预订数量上限(人工)
+ * - {@code online_sold}/{@code offline_sold} = 移动端已预订 / 消费机已销售净销量
+ *
+ * 共享总库存:线上未订完可继续线下卖。
+ *
+ * 剩余总库存 = 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-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/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/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