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