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 44157f298..7c8f753fc 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、填写说明 *

配料:单品写单品 SPU;套餐仅在首行填写,写入套餐 SPU *

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

无「菜品SPU ID」时一律新建菜品(不按菜名复用);有 ID 才按 ID 更新。 */ @Data public class CanteenDailyMenuImportExcel { 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 b940fa100..538098cf9 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 @@ -81,11 +81,11 @@ public class CanteenDailyMenuImportHelper { public List templateRows() { List list = new ArrayList<>(); list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "100", - "分类按名称匹配;套餐价/库存/配料只填首行。菜谱ID/SPU ID为文本勿改成数字,否则Excel会丢精度")); + "无菜品SPU ID则新建菜品(不按菜名复用);有ID才更新。套餐价/库存/配料填首行。ID列勿改数字格式")); list.add(row(null, null, null, "蒜苔炒腊肉", "荤菜", null, "8", null, null, null)); list.add(row(null, null, null, "清炒时蔬", "素菜", null, "3", null, null, null)); list.add(row(null, null, "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "80", - "换套餐名即另一套;名称已存在不新增")); + "换套餐名即另一套;无套餐SPU ID则新建套餐")); list.add(row(null, null, null, "土豆丝", "素菜", null, "4", null, null, null)); list.add(row(null, null, "无", "千张大骨汤", "汤类", null, "0", null, "50", "单品:套餐名填「无」「单品」「单点」或「单品-xxx/单点-xxx」;价格填单价列")); @@ -93,7 +93,7 @@ public class CanteenDailyMenuImportHelper { "单品-xxx / 单点-xxx 一律按单品,套餐价可留空")); list.add(row(null, null, "单点-2", "豆浆", "饮品", null, "2", null, "100", null)); list.add(row("2026-08-18", "午餐,晚餐", "无", "番茄蛋汤", "汤类", null, "2", null, "80", - "同一道菜多餐:餐段可写「午餐,晚餐」或「三餐/全天」,系统拆成多条排菜且菜品只建一份")); + "多餐拆行:无菜品SPU ID时各餐段各自新建菜品(库存独立)")); return list; } @@ -522,7 +522,7 @@ public class CanteenDailyMenuImportHelper { else { action = "INSERT"; actionLabel = "新增"; - tip = menuId != null ? "菜谱ID未匹配,将按新增排菜" : "新增排菜(菜品不存在时自动创建)"; + tip = menuId != null ? "菜谱ID未匹配,将按新增排菜" : "新增排菜(无菜品SPU ID则新建菜品,不按菜名复用)"; selected = true; totalInsert++; } @@ -708,7 +708,7 @@ public class CanteenDailyMenuImportHelper { FindOrCreate r = findOrCreateDish(merchantId, deptId, item.getCategoryName(), categoryId, categoryMap, item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), item.getStock(), - cache); + parseExcelLong(item.getSpuId()), cache); if (r.created) { createdDish++; } @@ -738,7 +738,8 @@ public class CanteenDailyMenuImportHelper { } FindOrCreate r = findOrCreateDish(merchantId, deptId, item.getCategoryName(), categoryId, categoryMap, - item.getDishName(), null, resolveDishPriceFen(item), null, cache); + item.getDishName(), null, resolveDishPriceFen(item), null, + parseExcelLong(item.getSpuId()), cache); if (r.created) { createdDish++; } @@ -758,7 +759,7 @@ public class CanteenDailyMenuImportHelper { FindOrCreate combo = findOrCreateCombo(merchantId, deptId, first.getCategoryName(), categoryId, categoryMap, fullComboName, comboName, comboPrice, comboStock, children, - resolveComboIngredients(first), cache); + resolveComboIngredients(first), parseExcelLong(first.getSpuId()), cache); if (combo.created) { createdCombo++; } @@ -1001,6 +1002,9 @@ public class CanteenDailyMenuImportHelper { if (spu.getPicUrl() == null) { spu.setPicUrl(""); } + if (spu.getId() != null) { + cache.spuById.put(spu.getId(), spu); + } int type = spu.getSpuType() == null ? 1 : spu.getSpuType(); if (type == 2) { cache.comboByName.putIfAbsent(spu.getName(), spu); @@ -1126,7 +1130,7 @@ public class CanteenDailyMenuImportHelper { } FindOrCreate created = findOrCreateDish(merchantId, deptId, row.getCategoryName(), defaultCategoryId, categoryMap, dish, row.getIngredients(), - resolveDishPriceFen(row), row.getStock(), cache); + resolveDishPriceFen(row), row.getStock(), parseExcelLong(row.getSpuId()), cache); menu.setStartTime(date); menu.setHourType(hourType); menu.setSpuId(created.spu.getId()); @@ -1156,7 +1160,7 @@ public class CanteenDailyMenuImportHelper { } FindOrCreate r = findOrCreateDish(merchantId, deptId, item.getCategoryName(), defaultCategoryId, categoryMap, dish, - null, resolveDishPriceFen(item), null, cache); + null, resolveDishPriceFen(item), null, parseExcelLong(item.getSpuId()), cache); children.add(r.spu); } children = dedupeComboChildren(children); @@ -1174,7 +1178,8 @@ public class CanteenDailyMenuImportHelper { String fullComboName = comboName + "-" + mainName; FindOrCreate combo = findOrCreateCombo(merchantId, deptId, items.get(0).getCategoryName(), defaultCategoryId, categoryMap, fullComboName, - comboName, comboPrice, comboStock, children, resolveComboIngredients(items.get(0)), cache); + comboName, comboPrice, comboStock, children, resolveComboIngredients(items.get(0)), + parseExcelLong(items.get(0).getSpuId()), cache); menu.setStartTime(date); menu.setHourType(hourType); menu.setSpuId(combo.spu.getId()); @@ -1212,62 +1217,32 @@ public class CanteenDailyMenuImportHelper { return true; } + /** + * 解析/创建单品。 + *

有「菜品SPU ID」→ 按 ID 复用并按需更新;无 ID → 一律新建,不再按菜名复用(避免多日导入互相覆盖库存)。 + */ private FindOrCreate findOrCreateDish(Long merchantId, Long deptId, String categoryName, Long defaultCategoryId, Map categoryMap, String name, String ingredients, Integer priceFen, Integer stock, - ImportCache cache) { + Long excelSpuId, ImportCache cache) { String clean = cleanDishName(name); - CanteenSpuEntity exist = cache.dishByName.get(clean); - if (exist != null) { - String importIngredients = normalizeImportIngredients(ingredients); - boolean changeDesc = false; - boolean changePrice = false; - boolean changeStock = false; - boolean changeCategory = false; - if (StrUtil.isNotBlank(importIngredients)) { - String existPlain = toPlainIngredients(exist.getDescription()); - if (!importIngredients.equals(existPlain)) { - exist.setDescription(importIngredients); - changeDesc = true; - } + if (excelSpuId != null) { + CanteenSpuEntity exist = resolveSpuById(excelSpuId, cache); + if (exist == null) { + throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不存在"); } - if (priceFen != null && priceFen > 0 && !Objects.equals(exist.getPrice(), priceFen)) { - exist.setPrice(priceFen); - exist.setMarketPrice(priceFen); - changePrice = true; + if (!merchantId.equals(exist.getMerchantId())) { + throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不属于当前商户"); } - if (stock != null && stock >= 0 && !Objects.equals(exist.getStock(), stock)) { - exist.setStock(stock); - changeStock = true; - } - // Excel 明确填了分类才改;空着不覆盖已有分类 - if (StrUtil.isNotBlank(normalizeCategoryName(categoryName))) { - Long newCatId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, - deptId); - if (newCatId != null && !Objects.equals(exist.getCategoryId(), newCatId)) { - exist.setCategoryId(newCatId); - changeCategory = true; - } - } - if (changeDesc || changePrice || changeStock || changeCategory) { - // 禁止 updateById(半截实体):pic_url 等为 ALWAYS,null 会写库报错 - var uw = Wrappers.lambdaUpdate().eq(CanteenSpuEntity::getId, exist.getId()); - if (changeDesc) { - uw.set(CanteenSpuEntity::getDescription, exist.getDescription()); - } - if (changePrice) { - uw.set(CanteenSpuEntity::getPrice, exist.getPrice()); - uw.set(CanteenSpuEntity::getMarketPrice, exist.getMarketPrice()); - } - if (changeStock) { - uw.set(CanteenSpuEntity::getStock, exist.getStock()); - } - if (changeCategory) { - uw.set(CanteenSpuEntity::getCategoryId, exist.getCategoryId()); - } - canteenSpuService.update(uw); + int type = exist.getSpuType() == null ? 1 : exist.getSpuType(); + if (type == 2) { + throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 是套餐,单品行请填写单品 SPU ID"); } + applyDishUpdates(exist, categoryName, defaultCategoryId, categoryMap, merchantId, deptId, ingredients, + priceFen, stock); return new FindOrCreate(exist, false); } + + // 无 SPU ID:始终新建,不按菜名命中已有菜品 Long categoryId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, deptId); if (categoryId == null) { throw new IllegalArgumentException(missingCategoryMessage("新建菜品「" + clean + "」", categoryName, categoryMap)); @@ -1280,14 +1255,65 @@ public class CanteenDailyMenuImportHelper { spu.setStock(stock); } canteenSpuService.save(spu); - cache.dishByName.put(clean, spu); return new FindOrCreate(spu, true); } + private void applyDishUpdates(CanteenSpuEntity exist, String categoryName, Long defaultCategoryId, + Map categoryMap, Long merchantId, Long deptId, String ingredients, Integer priceFen, + Integer stock) { + String importIngredients = normalizeImportIngredients(ingredients); + boolean changeDesc = false; + boolean changePrice = false; + boolean changeStock = false; + boolean changeCategory = false; + if (StrUtil.isNotBlank(importIngredients)) { + String existPlain = toPlainIngredients(exist.getDescription()); + if (!importIngredients.equals(existPlain)) { + exist.setDescription(importIngredients); + changeDesc = true; + } + } + if (priceFen != null && priceFen > 0 && !Objects.equals(exist.getPrice(), priceFen)) { + exist.setPrice(priceFen); + exist.setMarketPrice(priceFen); + changePrice = true; + } + if (stock != null && stock >= 0 && !Objects.equals(exist.getStock(), stock)) { + exist.setStock(stock); + changeStock = true; + } + if (StrUtil.isNotBlank(normalizeCategoryName(categoryName))) { + Long newCatId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, deptId); + if (newCatId != null && !Objects.equals(exist.getCategoryId(), newCatId)) { + exist.setCategoryId(newCatId); + changeCategory = true; + } + } + if (changeDesc || changePrice || changeStock || changeCategory) { + var uw = Wrappers.lambdaUpdate().eq(CanteenSpuEntity::getId, exist.getId()); + if (changeDesc) { + uw.set(CanteenSpuEntity::getDescription, exist.getDescription()); + } + if (changePrice) { + uw.set(CanteenSpuEntity::getPrice, exist.getPrice()); + uw.set(CanteenSpuEntity::getMarketPrice, exist.getMarketPrice()); + } + if (changeStock) { + uw.set(CanteenSpuEntity::getStock, exist.getStock()); + } + if (changeCategory) { + uw.set(CanteenSpuEntity::getCategoryId, exist.getCategoryId()); + } + canteenSpuService.update(uw); + } + } + + /** + * 解析/创建套餐。有套餐 SPU ID 则复用;无 ID 一律新建(不按套餐全名复用)。 + */ private FindOrCreate findOrCreateCombo(Long merchantId, Long deptId, String categoryName, Long defaultCategoryId, Map categoryMap, String fullName, String pkgLabel, Integer priceFen, Integer stock, - List children, String ingredients, ImportCache cache) { - CanteenSpuEntity exist = cache.comboByName.get(fullName); + List children, String ingredients, Long excelSpuId, ImportCache cache) { List items = new ArrayList<>(); int sort = 0; List newChildIds = new ArrayList<>(children.size()); @@ -1299,7 +1325,19 @@ public class CanteenDailyMenuImportHelper { items.add(line); newChildIds.add(child.getId()); } - if (exist != null) { + + if (excelSpuId != null) { + CanteenSpuEntity exist = resolveSpuById(excelSpuId, cache); + if (exist == null) { + throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不存在"); + } + if (!merchantId.equals(exist.getMerchantId())) { + throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不属于当前商户"); + } + int type = exist.getSpuType() == null ? 1 : exist.getSpuType(); + if (type != 2) { + throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不是套餐,套餐行请填写套餐 SPU ID"); + } boolean changePrice = priceFen != null && !Objects.equals(exist.getPrice(), priceFen); if (changePrice) { exist.setPrice(priceFen); @@ -1358,9 +1396,9 @@ public class CanteenDailyMenuImportHelper { } canteenSpuService.update(uw); } - cache.comboByName.put(fullName, exist); return new FindOrCreate(exist, false); } + Long categoryId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, deptId); if (categoryId == null) { throw new IllegalArgumentException(missingCategoryMessage("新建套餐「" + fullName + "」", categoryName, categoryMap)); @@ -1375,13 +1413,30 @@ public class CanteenDailyMenuImportHelper { } combo.setComboItems(items); canteenSpuService.saveDeep(combo); - cache.comboByName.put(fullName, combo); if (combo.getId() != null) { cache.comboChildIds.put(combo.getId(), new ArrayList<>(newChildIds)); } return new FindOrCreate(combo, true); } + private CanteenSpuEntity resolveSpuById(Long spuId, ImportCache cache) { + if (spuId == null) { + return null; + } + CanteenSpuEntity cached = cache.spuById.get(spuId); + if (cached != null) { + return cached; + } + CanteenSpuEntity db = canteenSpuService.getById(spuId); + if (db != null) { + cache.spuById.put(spuId, db); + if (db.getPicUrl() == null) { + db.setPicUrl(""); + } + } + return db; + } + private static String menuKey(LocalDate date, Integer hourType, Long spuId) { return date + "|" + hourType + "|" + spuId; } @@ -1675,7 +1730,7 @@ public class CanteenDailyMenuImportHelper { *

  • 多餐:午餐,晚餐 / 午餐、晚餐 / 午餐/晚餐
  • *
  • 快捷:三餐、全天、早中晚 → 早+午+晚;中晚餐 → 午+晚
  • * - * 同一道菜多餐时菜品库仍按菜名复用一份 SPU,排菜按餐段各挂一条。 + * 无菜品SPU ID 时各餐段各自新建 SPU(库存互不影响);有 SPU ID 则按 ID 复用。 */ private static List parseMeals(String meal) { if (StrUtil.isBlank(meal)) { @@ -1887,6 +1942,8 @@ public class CanteenDailyMenuImportHelper { private static class ImportCache { final Map dishByName = new HashMap<>(); final Map comboByName = new HashMap<>(); + /** 按 ID 解析 Excel「菜品SPU ID」 */ + final Map spuById = new HashMap<>(); final Set existingMenuKeys = new HashSet<>(); /** 套餐组成 childSpuId 有序列表,用于判断是否需要 updateDeep */ final Map> comboChildIds = new HashMap<>();