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 01f10dfb0..1692e9945 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 @@ -10,6 +10,7 @@ import java.math.BigDecimal; *

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

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

菜谱ID / 菜品SPU ID 必须用文本导出,否则 Excel 对超长雪花 ID 会丢精度导致再导入报「不存在」。 + *

套餐块「菜品SPU ID」=套餐本体一个 ID(首行合并);组成菜靠菜名。单品行=该单品 ID。 *

无「菜品SPU ID」时一律新建菜品(不按菜名复用);有 ID 才按 ID 更新。 */ @Data @@ -64,8 +65,8 @@ public class CanteenDailyMenuImportExcel { private String id; /** - * 关联菜品 SPU ID(文本)。套餐导出时为本行组成菜 ID,不是套餐本身 ID。 - * 导入:有则按 ID 更新该菜;套餐主体仍靠「菜谱ID」关联已有套餐。 + * 关联菜品 SPU ID(文本)。套餐块仅填首行=套餐本体 ID(与菜谱ID一样合并展示);组成菜靠菜名,不写子菜 ID。 + * 单品行=该单品 ID。导入:有则按 ID 更新;无则新建。 */ @ExcelProperty(value = "菜品SPU ID", index = 11) private String spuId; diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuComboItemEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuComboItemEntity.java index d9aedc91e..7ad1cb6a6 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuComboItemEntity.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenSpuComboItemEntity.java @@ -45,4 +45,8 @@ public class CanteenSpuComboItemEntity extends Model @TableField(exist = false) @Schema(description = "组成菜品价格(分)") private Integer childSpuPrice; + + @TableField(exist = false) + @Schema(description = "组成菜品配料/详情") + private String childSpuIngredients; } diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/OnlineSpuComboItemVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/OnlineSpuComboItemVO.java index 4517706df..70bb59c95 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/OnlineSpuComboItemVO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/OnlineSpuComboItemVO.java @@ -24,4 +24,7 @@ public class OnlineSpuComboItemVO { @Schema(description = "组成菜品单价(分,仅展示)") private Integer price; + + @Schema(description = "组成菜品配料(纯文本/富文本,导入导出用)") + private String ingredients; } 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 258ba6dfb..37465f994 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,7 +12,7 @@ import org.apache.poi.ss.usermodel.VerticalAlignment; import org.apache.poi.ss.util.CellRangeAddress; /** - * 每日菜谱导入模板 / 导出:按数据动态合并日期、餐段、套餐名、套餐价、菜谱ID、说明。 + * 每日菜谱导入模板 / 导出:按数据动态合并日期、餐段、套餐名、套餐价、菜谱ID、套餐SPU、说明。 *

列:0日期 1餐段 2套餐名 3菜品名 4分类 5单价 6套餐价 7线上库存 8线下库存 9配料 10菜谱ID 11SPU ID 12填写说明 */ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandler { @@ -23,8 +23,8 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle 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_INGREDIENTS = 9; private static final int COL_ID = 10; + private static final int COL_SPU = 11; private static final int COL_TIP = 12; @Override @@ -49,7 +49,7 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle } /** - * 向下填充后,按连续相同值合并:日期 / 餐段 / 套餐块(套餐名+套餐价+ID+SPU+说明) + * 向下填充后,按连续相同值合并:日期 / 餐段 / 套餐块(套餐名+套餐价+库存+ID+SPU+说明;配料不合并) */ private static void mergeByRuns(Sheet sheet, CellStyle center) { int lastRow = sheet.getLastRowNum(); @@ -140,9 +140,10 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle mergeRange(sheet, runStart, endRow, COL_COMBO_PRICE, center); mergeRange(sheet, runStart, endRow, COL_ONLINE_STOCK, center); mergeRange(sheet, runStart, endRow, COL_OFFLINE_STOCK, center); - mergeRange(sheet, runStart, endRow, COL_INGREDIENTS, center); + // 配料不合并:组成菜各自配料(同名不同配料靠此区分) mergeRange(sheet, runStart, endRow, COL_ID, center); - // 菜品SPU ID 不合并:套餐每行对应组成菜自己的 ID + // 套餐 SPU ID 与菜谱 ID 一样合并:整块一个套餐本体 ID + mergeRange(sheet, runStart, endRow, COL_SPU, center); mergeRange(sheet, runStart, endRow, COL_TIP, center); } runStart = r; 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 b7c75336d..a71129496 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 @@ -95,11 +95,11 @@ public class CanteenDailyMenuImportHelper { public List templateRows() { List list = new ArrayList<>(); list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "80", "100", - "无菜品SPU ID则新建菜品;有则按本行菜品ID更新。套餐价/线上线下库存/配料填首行;套餐每行SPU=组成菜ID")); + "无菜品SPU ID则新建;有则按ID更新。套餐价/库存/套餐SPU填首行;配料按组成菜分行填(同名不同配料会建两道菜)")); list.add(row(null, null, null, "蒜苔炒腊肉", "荤菜", null, "8", null, null, null, null)); list.add(row(null, null, null, "清炒时蔬", "素菜", null, "3", null, null, null, null)); list.add(row(null, null, "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "60", "80", - "换套餐名即另一套;无菜谱ID时新建套餐;有菜谱ID则更新该排菜套餐组成")); + "换套餐名即另一套;无菜谱ID时新建套餐;有菜谱ID/套餐SPU则更新该套餐")); list.add(row(null, null, null, "土豆丝", "素菜", null, "4", null, null, null, null)); list.add(row(null, null, "无", "千张大骨汤", "汤类", null, "0", null, "50", "50", "单品:套餐名填「无」「单品」「单点」或「单品-xxx/单点-xxx」;价格填单价列;库存填本行线上/线下")); @@ -184,6 +184,7 @@ public class CanteenDailyMenuImportHelper { List items = menu.getComboItems(); Set exportedChildIds = new HashSet<>(); int written = 0; + String comboSpuId = menu.getSpuId() == null ? null : String.valueOf(menu.getSpuId()); for (OnlineSpuComboItemVO item : items) { if (item == null) { continue; @@ -195,17 +196,16 @@ public class CanteenDailyMenuImportHelper { CanteenDailyMenuImportExcel row = new CanteenDailyMenuImportExcel(); if (written == 0) { row.setId(menu.getId() == null ? null : String.valueOf(menu.getId())); + row.setSpuId(comboSpuId); row.setBizDate(dateStr); row.setMealName(meal); row.setComboName(comboLabel); row.setPriceYuan(comboPrice); row.setOnlineStock(menu.getOnlineStock()); row.setOfflineStock(menu.getOfflineStock()); - row.setIngredients(toExcelIngredients(menu.getIngredients())); } - // 每行导出组成菜自己的 SPU ID(不是套餐 SPU);便于再导入按菜更新 - row.setSpuId(childId == null ? null : String.valueOf(childId)); - // 每行都带分类,改菜名新建子菜时不丢分类 + // 套餐只导出一个套餐 SPU;配料列按组成菜各自导出(同名不同配料靠此区分) + row.setIngredients(toExcelIngredients(item.getIngredients())); row.setCategoryName(menu.getCategoryName()); row.setDishName(StrUtil.blankToDefault(item.getName(), "")); row.setDishPriceYuan(fenToYuan(item.getPrice())); @@ -859,6 +859,9 @@ public class CanteenDailyMenuImportHelper { Integer comboPrice = null; Integer onlineStock = first.getOnlineStock(); Integer offlineStock = first.getOfflineStock(); + Long comboExcelSpuId = resolveComboExcelSpuId(items, cache); + Map existChildByKey = existingComboChildKeyMap(comboExcelSpuId, cache); + boolean uniformSpu = isUniformSpuId(items); Set seenDishKeys = new LinkedHashSet<>(); for (int i = 0; i < items.size(); i++) { CanteenDailyMenuImportExcel item = items.get(i); @@ -868,14 +871,16 @@ public class CanteenDailyMenuImportHelper { comboPrice = fen; } } - String dishKey = cleanDishName(item.getDishName()); - if (StrUtil.isBlank(dishKey) || !seenDishKeys.add(dishKey)) { + String dishKey = dishIdentityKey(item.getDishName(), item.getIngredients()); + if (StrUtil.isBlank(cleanDishName(item.getDishName())) || !seenDishKeys.add(dishKey)) { continue; } + Long childSpuId = resolveComboChildExcelSpuId(item.getSpuId(), comboExcelSpuId, uniformSpu, + existChildByKey.get(dishKey), cache); FindOrCreate r = findOrCreateDish(merchantId, deptId, item.getCategoryName(), categoryId, categoryMap, - item.getDishName(), null, resolveDishPriceFen(item), null, - parseExcelLong(item.getSpuId()), cache); + item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), null, childSpuId, + cache); if (r.created) { createdDish++; } @@ -895,7 +900,7 @@ public class CanteenDailyMenuImportHelper { FindOrCreate combo = findOrCreateCombo(merchantId, deptId, first.getCategoryName(), categoryId, categoryMap, fullComboName, comboName, comboPrice, null, children, - resolveComboIngredients(first), null, cache); + resolveComboIngredients(first), comboExcelSpuId, cache); if (combo.created) { createdCombo++; } @@ -1378,6 +1383,12 @@ public class CanteenDailyMenuImportHelper { Integer comboPrice = null; Integer onlineStock = items.get(0).getOnlineStock(); Integer offlineStock = items.get(0).getOfflineStock(); + Long comboExcelSpuId = resolveComboExcelSpuId(items, cache); + if (comboExcelSpuId == null) { + comboExcelSpuId = menu.getSpuId(); + } + Map existChildByKey = existingComboChildKeyMap(comboExcelSpuId, cache); + boolean uniformSpu = isUniformSpuId(items); Set seenDishKeys = new LinkedHashSet<>(); for (int i = 0; i < items.size(); i++) { CanteenDailyMenuImportExcel item = items.get(i); @@ -1388,13 +1399,15 @@ public class CanteenDailyMenuImportHelper { if (StrUtil.isBlank(dish)) { return "菜谱ID " + menu.getId() + ":套餐组成菜品名不能为空"; } - String dishKey = cleanDishName(dish); + String dishKey = dishIdentityKey(dish, item.getIngredients()); if (!seenDishKeys.add(dishKey)) { continue; } + Long childSpuId = resolveComboChildExcelSpuId(item.getSpuId(), comboExcelSpuId, uniformSpu, + existChildByKey.get(dishKey), cache); FindOrCreate r = findOrCreateDish(merchantId, deptId, item.getCategoryName(), defaultCategoryId, categoryMap, dish, - null, resolveDishPriceFen(item), null, parseExcelLong(item.getSpuId()), cache); + item.getIngredients(), resolveDishPriceFen(item), null, childSpuId, cache); children.add(r.spu); } children = dedupeComboChildren(children); @@ -1413,7 +1426,7 @@ public class CanteenDailyMenuImportHelper { FindOrCreate combo = findOrCreateCombo(merchantId, deptId, items.get(0).getCategoryName(), defaultCategoryId, categoryMap, fullComboName, comboName, comboPrice, null, children, resolveComboIngredients(items.get(0)), - menu.getSpuId(), cache); + comboExcelSpuId, cache); menu.setStartTime(date); menu.setHourType(hourType); menu.setSpuId(combo.spu.getId()); @@ -1423,6 +1436,100 @@ public class CanteenDailyMenuImportHelper { return null; } + /** + * 套餐块「菜品SPU ID」:仅当确实是套餐类型时才当作套餐本体; + * 旧导出把组成菜 ID 写在各行时(类型为单品)返回 null,避免误当套餐。 + */ + private Long resolveComboExcelSpuId(List items, ImportCache cache) { + if (CollUtil.isEmpty(items)) { + return null; + } + for (CanteenDailyMenuImportExcel item : items) { + Long id = parseExcelLong(item == null ? null : item.getSpuId()); + if (id == null) { + continue; + } + CanteenSpuEntity exist = resolveSpuById(id, cache); + if (exist == null) { + continue; + } + int type = exist.getSpuType() == null ? 1 : exist.getSpuType(); + if (type == 2) { + return id; + } + // 整块同一单品 ID(旧导出合并继承)不算套餐本体 + return null; + } + return null; + } + + private static boolean isUniformSpuId(List items) { + String first = null; + boolean any = false; + for (CanteenDailyMenuImportExcel item : items) { + String s = StrUtil.trim(item == null ? null : item.getSpuId()); + if (StrUtil.isBlank(s)) { + continue; + } + any = true; + if (first == null) { + first = s; + } + else if (!first.equals(s)) { + return false; + } + } + return any; + } + + /** + * 组成菜 SPU:套餐块统一填套餐本体 ID 时不按该 ID 建子菜; + * 可回退到已有套餐组成中「菜名+配料」完全一致的子菜 ID。 + */ + private Long resolveComboChildExcelSpuId(String rowSpuId, Long comboSpuId, boolean uniformSpu, + Long existChildByKey, ImportCache cache) { + if (uniformSpu) { + return existChildByKey; + } + Long id = parseExcelLong(rowSpuId); + if (id == null || Objects.equals(id, comboSpuId)) { + return existChildByKey; + } + CanteenSpuEntity exist = resolveSpuById(id, cache); + if (exist != null) { + int type = exist.getSpuType() == null ? 1 : exist.getSpuType(); + if (type == 2) { + return existChildByKey; + } + } + return id; + } + + /** 已有套餐组成:key=菜名+配料,避免同名不同配料被复用错。 */ + private Map existingComboChildKeyMap(Long comboSpuId, ImportCache cache) { + Map map = new HashMap<>(); + if (comboSpuId == null || cache == null) { + return map; + } + List childIds = cache.comboChildIds.get(comboSpuId); + if (CollUtil.isEmpty(childIds)) { + return map; + } + for (Long childId : childIds) { + CanteenSpuEntity child = resolveSpuById(childId, cache); + if (child == null || StrUtil.isBlank(child.getName())) { + continue; + } + map.putIfAbsent(dishIdentityKey(child.getName(), child.getDescription()), childId); + } + return map; + } + + /** 组成菜去重/复用键:规范化菜名 + 纯文本配料 */ + private static String dishIdentityKey(String name, String ingredients) { + return cleanDishName(name) + "|" + normalizeImportIngredients(ingredients); + } + /** * @return true 表示新增了一条排菜 */ @@ -1710,8 +1817,11 @@ public class CanteenDailyMenuImportHelper { } } + /** + * 套餐块「配料」列表示组成菜配料,不再写入套餐本体 description,避免把首道菜配料盖到套餐上。 + */ private static String resolveComboIngredients(CanteenDailyMenuImportExcel first) { - return first == null ? "" : normalizeImportIngredients(first.getIngredients()); + return ""; } private Map loadCategoryMap(Long merchantId) { 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 6d0ae49ba..e10bb2ede 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 @@ -830,6 +830,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl