From 43966c04ece3fa627da9ecc15f0da59049625d5e Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Fri, 28 Aug 2026 15:44:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AF=8F=E6=97=A5=E8=8F=9C=E8=B0=B1=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/CanteenDailyMenuImportExcel.java | 16 +- .../canteen/dto/CanteenSpuBatchPicDTO.java | 24 ++ .../CanteenDailyMenuController.java | 9 + .../controller/CanteenSpuController.java | 22 ++ .../impl/CanteenDailyMenuImportHelper.java | 313 +++++++++++++----- .../impl/CanteenOnlineSpuServiceImpl.java | 5 +- .../service/impl/CanteenSpuServiceImpl.java | 5 +- 7 files changed, 307 insertions(+), 87 deletions(-) create mode 100644 pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuBatchPicDTO.java 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 ae521dfd3..44157f298 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,8 +7,9 @@ import java.math.BigDecimal; /** * 每日菜谱导入/导出 Excel 行(导出含菜谱 ID,导入时可按 ID 更新) - *

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

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

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

菜谱ID / 菜品SPU ID 必须用文本导出,否则 Excel 对超长雪花 ID 会丢精度导致再导入报「不存在」。 */ @Data public class CanteenDailyMenuImportExcel { @@ -47,13 +48,18 @@ public class CanteenDailyMenuImportExcel { @ExcelProperty(value = "配料", index = 8) private String ingredients; - /** 每日菜谱记录 ID;导入时有则更新,无则新增 */ + /** + * 每日菜谱记录 ID(文本)。导入时有则更新,无则新增。 + * 勿改成数字格式,否则 Excel 会丢精度。 + */ @ExcelProperty(value = "菜谱ID", index = 9) - private Long id; + private String id; - /** 关联菜品 SPU ID;导出参考,导入不依赖 */ + /** + * 关联菜品/套餐 SPU ID(文本,仅导出参考,导入不依赖此列)。 + */ @ExcelProperty(value = "菜品SPU ID", index = 10) - private Long spuId; + private String spuId; /** 仅示例说明用,导入逻辑忽略 */ @ExcelProperty(value = "填写说明", index = 11) diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuBatchPicDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuBatchPicDTO.java new file mode 100644 index 000000000..1313e19ba --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/CanteenSpuBatchPicDTO.java @@ -0,0 +1,24 @@ +package com.bsd.pjcloud.canteen.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +/** + * 菜品列表:批量赋值封面图 + */ +@Data +@Schema(description = "批量赋值菜品封面图") +public class CanteenSpuBatchPicDTO { + + @NotEmpty + @Schema(description = "菜品 SPU ID 列表") + private List ids; + + @NotBlank + @Schema(description = "封面图地址(上传或图库)") + private String picUrl; +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java index d53529169..93bfea5d3 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java @@ -178,6 +178,15 @@ public class CanteenDailyMenuController { catch (IllegalArgumentException ex) { return R.failed(ex.getMessage()); } + catch (org.springframework.transaction.UnexpectedRollbackException ex) { + // 兜底:避免把 rollback-only 原文甩给前端 + Throwable root = ex.getMostSpecificCause() != null ? ex.getMostSpecificCause() : ex; + String tip = root.getMessage(); + if (tip != null && tip.contains("rollback-only")) { + tip = "导入失败并已回滚,请检查套餐组成、分类、菜名是否完整后重试"; + } + return R.failed(tip); + } } /** diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java index c9ac9e025..a2b18a94e 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java @@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.canteen.constant.StockTypeEnum; import com.bsd.pjcloud.canteen.dto.CanteenDailySpuDTO; +import com.bsd.pjcloud.canteen.dto.CanteenSpuBatchPicDTO; import com.bsd.pjcloud.canteen.dto.CanteenSpuExportQuery; import com.bsd.pjcloud.canteen.dto.CanteenSpuImportExcel; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; @@ -209,6 +210,27 @@ public class CanteenSpuController { } } + /** + * 批量赋值菜品封面图(导入无图后,从图库或上传一张图批量套用) + */ + @Operation(summary = "批量赋值菜品封面图", description = "批量赋值菜品封面图") + @SysLog("批量赋值菜品封面图") + @PutMapping("/batchPic") + @PreAuthorize("@pms.hasPermission('canteen_canteenSpu_edit')") + public R batchPic(@RequestBody CanteenSpuBatchPicDTO dto) { + if (CollUtil.isEmpty(dto.getIds())) { + return R.failed("请选择菜品"); + } + if (StrUtil.isBlank(dto.getPicUrl())) { + return R.failed("请选择或上传封面图"); + } + String picUrl = dto.getPicUrl().trim(); + boolean ok = canteenSpuService.update(Wrappers.lambdaUpdate() + .set(CanteenSpuEntity::getPicUrl, picUrl) + .in(CanteenSpuEntity::getId, dto.getIds())); + return ok ? R.ok(null, "已更新 " + dto.getIds().size() + " 道菜封面") : R.failed("更新失败"); + } + /** * 修改菜品spu * 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 c30de52c5..050576ffb 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 @@ -65,7 +65,7 @@ public class CanteenDailyMenuImportHelper { public List templateRows() { List list = new ArrayList<>(); list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "100", - "分类按名称匹配;套餐价/库存/配料只填首行。已有菜品不选默认分类也可导入")); + "分类按名称匹配;套餐价/库存/配料只填首行。菜谱ID/SPU ID为文本勿改成数字,否则Excel会丢精度")); 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", @@ -146,12 +146,20 @@ public class CanteenDailyMenuImportHelper { String comboLabel = resolveComboLabel(menu); BigDecimal comboPrice = fenToYuan(menu.getPrice()); List items = menu.getComboItems(); - for (int i = 0; i < items.size(); i++) { - OnlineSpuComboItemVO item = items.get(i); + Set exportedChildIds = new HashSet<>(); + int written = 0; + for (OnlineSpuComboItemVO item : items) { + if (item == null) { + continue; + } + Long childId = item.getChildSpuId(); + if (childId != null && !exportedChildIds.add(childId)) { + continue; + } CanteenDailyMenuImportExcel row = new CanteenDailyMenuImportExcel(); - if (i == 0) { - row.setId(menu.getId()); - row.setSpuId(menu.getSpuId()); + if (written == 0) { + row.setId(menu.getId() == null ? null : String.valueOf(menu.getId())); + row.setSpuId(menu.getSpuId() == null ? null : String.valueOf(menu.getSpuId())); row.setBizDate(dateStr); row.setMealName(meal); row.setComboName(comboLabel); @@ -164,12 +172,13 @@ public class CanteenDailyMenuImportHelper { row.setDishName(StrUtil.blankToDefault(item.getName(), "")); row.setDishPriceYuan(fenToYuan(item.getPrice())); rows.add(row); + written++; } } else { CanteenDailyMenuImportExcel row = new CanteenDailyMenuImportExcel(); - row.setId(menu.getId()); - row.setSpuId(menu.getSpuId()); + row.setId(menu.getId() == null ? null : String.valueOf(menu.getId())); + row.setSpuId(menu.getSpuId() == null ? null : String.valueOf(menu.getSpuId())); row.setBizDate(dateStr); row.setMealName(meal); row.setComboName("无"); @@ -225,6 +234,36 @@ public class CanteenDailyMenuImportHelper { }; } + /** dayHour 形如 2026-09-04|2 */ + private static String describeDayHour(String dayHour) { + if (StrUtil.isBlank(dayHour) || !dayHour.contains("|")) { + return dayHour; + } + String[] p = dayHour.split("\\|", 2); + Integer hour = null; + try { + hour = Integer.parseInt(p[1]); + } + catch (Exception ignored) { + // ignore + } + return p[0] + " " + formatMeal(hour); + } + + /** 普通校验错误:换行编号,便于前端弹窗阅读 */ + private static String formatImportErrors(List errors) { + int show = Math.min(errors.size(), 20); + StringBuilder sb = new StringBuilder(); + sb.append("导入校验未通过(").append(errors.size()).append(" 项):\n"); + for (int i = 0; i < show; i++) { + sb.append(i + 1).append(". ").append(errors.get(i)).append('\n'); + } + if (errors.size() > show) { + sb.append("…另有 ").append(errors.size() - show).append(" 项未列出"); + } + return sb.toString().trim(); + } + private static BigDecimal fenToYuan(Integer fen) { if (fen == null) { return null; @@ -278,6 +317,10 @@ public class CanteenDailyMenuImportHelper { // groupKey = date|hourType|comboOrEmpty|menuIdOrNew Map> groups = new LinkedHashMap<>(); + /** 菜谱ID → 出现过的 日期|餐段(用于汇总跨天串 ID) */ + Map> menuIdDayHours = new LinkedHashMap<>(); + /** 菜谱ID → 示例套餐/菜名(取首次) */ + Map menuIdLabel = new LinkedHashMap<>(); Set involvedDates = new HashSet<>(); int line = 1; for (CanteenDailyMenuImportExcel row : dataRows) { @@ -300,10 +343,14 @@ public class CanteenDailyMenuImportHelper { involvedDates.add(date); String combo = StrUtil.trim(row.getComboName()); String comboKey = StrUtil.blankToDefault(combo, ""); - Long menuId = row.getId(); + Long menuId = parseExcelLong(row.getId()); + // 必须带上日期+餐段:仅按菜谱ID 分组时,同名套餐跨天若串了同一 ID,会把两天组成菜并成一组报「重复」 String groupKey; if (menuId != null) { - groupKey = "id:" + menuId; + String dayHour = date + "|" + hourType; + menuIdDayHours.computeIfAbsent(menuId, k -> new LinkedHashSet<>()).add(dayHour); + menuIdLabel.putIfAbsent(menuId, StrUtil.blankToDefault(comboKey, dish)); + groupKey = "id:" + menuId + "|" + date + "|" + hourType; } else if (StrUtil.isNotBlank(comboKey) && !isSingleComboMarker(comboKey)) { groupKey = "new:" + date + "|" + hourType + "|" + comboKey; @@ -313,8 +360,30 @@ public class CanteenDailyMenuImportHelper { } groups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(row); } + List crossDayIdErrors = new ArrayList<>(); + for (Map.Entry> en : menuIdDayHours.entrySet()) { + if (en.getValue().size() <= 1) { + continue; + } + List days = en.getValue().stream().map(CanteenDailyMenuImportHelper::describeDayHour).toList(); + crossDayIdErrors.add(String.format("ID %s — %s(%s)", en.getKey(), String.join(" / ", days), + StrUtil.blankToDefault(menuIdLabel.get(en.getKey()), "-"))); + } + if (CollUtil.isNotEmpty(crossDayIdErrors)) { + StringBuilder sb = new StringBuilder(); + sb.append("发现 ").append(crossDayIdErrors.size()).append(" 处菜谱ID跨天/跨餐段重复(同一ID只能对应一天一餐):\n"); + int show = Math.min(crossDayIdErrors.size(), 15); + for (int i = 0; i < show; i++) { + sb.append(i + 1).append(". ").append(crossDayIdErrors.get(i)).append('\n'); + } + if (crossDayIdErrors.size() > show) { + sb.append("…另有 ").append(crossDayIdErrors.size() - show).append(" 处未列出\n"); + } + sb.append("请删掉错误那天的「菜谱ID」列,或重新导出后再改。"); + return R.failed(sb.toString().trim()); + } if (CollUtil.isNotEmpty(errors)) { - return R.failed(String.join(";", errors.subList(0, Math.min(errors.size(), 20)))); + return R.failed(formatImportErrors(errors)); } ImportCache cache = buildImportCache(merchantId, involvedDates); @@ -327,41 +396,65 @@ public class CanteenDailyMenuImportHelper { for (Map.Entry> e : groups.entrySet()) { List items = e.getValue(); - Long menuId = items.stream().map(CanteenDailyMenuImportExcel::getId).filter(Objects::nonNull).findFirst() - .orElse(null); + CanteenDailyMenuImportExcel firstRow = items.get(0); + LocalDate groupDate = parseDate(firstRow.getBizDate()); + Integer groupHour = parseMeal(firstRow.getMealName()); + String comboHint = StrUtil.blankToDefault(StrUtil.trim(firstRow.getComboName()), + StrUtil.trim(firstRow.getDishName())); + Long menuId = items.stream().map(CanteenDailyMenuImportExcel::getId).map(CanteenDailyMenuImportHelper::parseExcelLong) + .filter(Objects::nonNull).findFirst().orElse(null); if (menuId != null) { + CanteenDailyMenuEntity existMenu = canteenDailyMenuService.getById(menuId); + if (existMenu == null) { + throw new IllegalArgumentException(String.format( + "%s %s「%s」菜谱ID %s 不存在,请检查是否复制错了或被 Excel 改坏;若是新排菜请清空该行菜谱ID", + groupDate, formatMeal(groupHour), comboHint, menuId)); + } + if (!merchantId.equals(existMenu.getMerchantId())) { + throw new IllegalArgumentException(String.format( + "%s %s「%s」菜谱ID %s 不属于当前商户,请勿串用其他食堂的导出表", + groupDate, formatMeal(groupHour), comboHint, menuId)); + } + if (groupDate != null && existMenu.getStartTime() != null + && !groupDate.equals(existMenu.getStartTime())) { + throw new IllegalArgumentException(String.format( + "%s %s「%s」菜谱ID %s 在系统中属于 %s %s,不能用在当前日期;请改正 ID 或清空后按新增导入", + groupDate, formatMeal(groupHour), comboHint, menuId, + existMenu.getStartTime(), formatMeal(existMenu.getHourType()))); + } + if (groupHour != null && existMenu.getHourType() != null + && !groupHour.equals(existMenu.getHourType())) { + throw new IllegalArgumentException(String.format( + "%s %s「%s」菜谱ID %s 在系统中属于 %s,不能用在当前餐段;请改正 ID 或清空后按新增导入", + groupDate, formatMeal(groupHour), comboHint, menuId, + formatMeal(existMenu.getHourType()))); + } String err = upsertGroupById(menuId, items, merchantId, deptId, categoryId, categoryMap, cache); if (err != null) { - errors.add(err); - } - else { - updated++; + // 事务内勿 return R.failed:内层已写库/抛错会标 rollback-only,提交会报 UnexpectedRollback + throw new IllegalArgumentException(err); } + updated++; continue; } - CanteenDailyMenuImportExcel first = items.get(0); - LocalDate date = parseDate(first.getBizDate()); - Integer hourType = parseMeal(first.getMealName()); + CanteenDailyMenuImportExcel first = firstRow; + LocalDate date = groupDate; + Integer hourType = groupHour; String comboName = StrUtil.trim(first.getComboName()); if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) { for (CanteenDailyMenuImportExcel item : items) { - try { - FindOrCreate r = findOrCreateDish(merchantId, deptId, - item.getCategoryName(), categoryId, categoryMap, - item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), item.getStock(), - cache); - if (r.created) { - createdDish++; - } - if (schedule(merchantId, deptId, date, hourType, r.spu.getId(), replaceDay, clearedDayHour, - cache, pendingMenus)) { - scheduled++; - } + FindOrCreate r = findOrCreateDish(merchantId, deptId, + item.getCategoryName(), categoryId, categoryMap, + item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), item.getStock(), + cache); + if (r.created) { + createdDish++; } - catch (IllegalArgumentException ex) { - errors.add(ex.getMessage()); + if (schedule(merchantId, deptId, date, hourType, r.spu.getId(), replaceDay, clearedDayHour, + cache, pendingMenus)) { + scheduled++; } } } @@ -369,7 +462,7 @@ public class CanteenDailyMenuImportHelper { List children = new ArrayList<>(); Integer comboPrice = null; Integer comboStock = first.getStock(); - boolean childFailed = false; + Set seenDishKeys = new LinkedHashSet<>(); for (int i = 0; i < items.size(); i++) { CanteenDailyMenuImportExcel item = items.get(i); if (i == 0) { @@ -378,44 +471,39 @@ public class CanteenDailyMenuImportHelper { comboPrice = fen; } } - try { - FindOrCreate r = findOrCreateDish(merchantId, deptId, - item.getCategoryName(), categoryId, categoryMap, - item.getDishName(), null, resolveDishPriceFen(item), null, cache); - if (r.created) { - createdDish++; - } - children.add(r.spu); + String dishKey = cleanDishName(item.getDishName()); + if (StrUtil.isBlank(dishKey) || !seenDishKeys.add(dishKey)) { + // 同套餐块内同名组成跳过(常见于跨日串组或导出重复) + continue; } - catch (IllegalArgumentException ex) { - errors.add(ex.getMessage()); - childFailed = true; - break; + FindOrCreate r = findOrCreateDish(merchantId, deptId, + item.getCategoryName(), categoryId, categoryMap, + item.getDishName(), null, resolveDishPriceFen(item), null, cache); + if (r.created) { + createdDish++; } + children.add(r.spu); } - if (childFailed) { - continue; + children = dedupeComboChildren(children); + if (children.isEmpty()) { + throw new IllegalArgumentException(String.format("%s %s 套餐「%s」没有有效组成菜,请检查 Excel", + date, formatMeal(hourType), comboName)); } if (comboPrice == null) { comboPrice = 0; } String mainName = children.get(0).getName(); String fullComboName = comboName + "-" + mainName; - try { - FindOrCreate combo = findOrCreateCombo(merchantId, deptId, - first.getCategoryName(), categoryId, categoryMap, - fullComboName, comboName, comboPrice, comboStock, children, - resolveComboIngredients(first), cache); - if (combo.created) { - createdCombo++; - } - if (schedule(merchantId, deptId, date, hourType, combo.spu.getId(), replaceDay, clearedDayHour, - cache, pendingMenus)) { - scheduled++; - } + FindOrCreate combo = findOrCreateCombo(merchantId, deptId, + first.getCategoryName(), categoryId, categoryMap, + fullComboName, comboName, comboPrice, comboStock, children, + resolveComboIngredients(first), cache); + if (combo.created) { + createdCombo++; } - catch (IllegalArgumentException ex) { - errors.add(ex.getMessage()); + if (schedule(merchantId, deptId, date, hourType, combo.spu.getId(), replaceDay, clearedDayHour, + cache, pendingMenus)) { + scheduled++; } } if (pendingMenus.size() >= MENU_BATCH_SIZE) { @@ -423,12 +511,37 @@ public class CanteenDailyMenuImportHelper { } } flushPendingMenus(pendingMenus); - if (CollUtil.isNotEmpty(errors)) { - return R.failed(String.join(";", errors.subList(0, Math.min(errors.size(), 20)))); - } - return R.ok(null, String.format("导入成功:更新 %d,新建单品 %d,新建套餐 %d,新增排菜 %d", - updated, createdDish, createdCombo, scheduled)); + String msg = String.format("导入成功:更新 %d,新建单品 %d,新建套餐 %d,新增排菜 %d", + updated, createdDish, createdCombo, scheduled); + return R.ok(null, msg); + } + + /** + * 解析 Excel 中的雪花 ID。必须用字符串/BigDecimal,禁止经 Double,否则超过 15 位会失真。 + */ + private static Long parseExcelLong(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + String s = raw.trim(); + if (s.endsWith(".0")) { + s = s.substring(0, s.length() - 2); + } + try { + if (s.contains("E") || s.contains("e")) { + return new BigDecimal(s).toBigInteger().longValueExact(); + } + return Long.parseLong(s); + } + catch (Exception ex) { + try { + return new BigDecimal(s).toBigInteger().longValueExact(); + } + catch (Exception ignored) { + return null; + } + } } /** @@ -514,7 +627,7 @@ public class CanteenDailyMenuImportHelper { Long defaultCategoryId, Map categoryMap, ImportCache cache) { CanteenDailyMenuEntity menu = canteenDailyMenuService.getById(menuId); if (menu == null) { - return "菜谱ID " + menuId + " 不存在"; + return "菜谱ID " + menuId + " 不存在(若由Excel导出再导入,多为ID被当成数字丢了末位精度,请重新导出且保持菜谱ID列为文本)"; } if (!merchantId.equals(menu.getMerchantId())) { return "菜谱ID " + menuId + " 不属于当前商户"; @@ -565,6 +678,7 @@ public class CanteenDailyMenuImportHelper { List children = new ArrayList<>(); Integer comboPrice = null; Integer comboStock = items.get(0).getStock(); + Set seenDishKeys = new LinkedHashSet<>(); for (int i = 0; i < items.size(); i++) { CanteenDailyMenuImportExcel item = items.get(i); if (i == 0) { @@ -574,11 +688,19 @@ public class CanteenDailyMenuImportHelper { if (StrUtil.isBlank(dish)) { return "菜谱ID " + menu.getId() + ":套餐组成菜品名不能为空"; } + String dishKey = cleanDishName(dish); + if (!seenDishKeys.add(dishKey)) { + continue; + } FindOrCreate r = findOrCreateDish(merchantId, deptId, item.getCategoryName(), defaultCategoryId, categoryMap, dish, null, resolveDishPriceFen(item), null, cache); children.add(r.spu); } + children = dedupeComboChildren(children); + if (children.isEmpty()) { + return "菜谱ID " + menu.getId() + ":套餐没有有效组成菜"; + } if (comboPrice == null) { CanteenSpuEntity existCombo = cache.comboByName.values().stream() .filter(s -> Objects.equals(s.getId(), menu.getSpuId())) @@ -760,6 +882,24 @@ public class CanteenDailyMenuImportHelper { return date + "|" + hourType + "|" + spuId; } + /** Excel 同一套餐块内菜名/SPU 去重,保留首次出现 */ + private static List dedupeComboChildren(List children) { + if (CollUtil.isEmpty(children)) { + return children; + } + Set ids = new HashSet<>(); + List out = new ArrayList<>(children.size()); + for (CanteenSpuEntity child : children) { + if (child == null || child.getId() == null) { + continue; + } + if (ids.add(child.getId())) { + out.add(child); + } + } + return out; + } + private static void applyComboIngredients(CanteenSpuEntity combo, String ingredients) { String importIngredients = normalizeImportIngredients(ingredients); if (StrUtil.isBlank(importIngredients)) { @@ -883,7 +1023,7 @@ public class CanteenDailyMenuImportHelper { } private static boolean isBlankRow(CanteenDailyMenuImportExcel row) { - return row.getId() == null + return StrUtil.isBlank(row.getId()) && StrUtil.isBlank(row.getBizDate()) && StrUtil.isBlank(row.getDishName()) && StrUtil.isBlank(row.getMealName()) && StrUtil.isBlank(row.getComboName()); } @@ -891,30 +1031,41 @@ public class CanteenDailyMenuImportHelper { /** * 合并单元格模板读入后,非首行日期/餐段/套餐名/分类为空,需向下填充。 * 套餐名填「无」「单品」「单点」或「单品-xxx / 单点-xxx」表示单品,不参与套餐继承。 + *

换日/换餐段时必须清空菜谱ID继承,否则「同名套餐」跨天会被合成一组,组成菜看起来像重复。 */ private static void normalizeMergedCells(List rows) { String lastDate = null; String lastMeal = null; String lastCombo = null; String lastCategory = null; - Long lastMenuId = null; - Long lastSpuId = null; + String lastMenuId = null; + String lastSpuId = null; for (CanteenDailyMenuImportExcel row : rows) { if (isBlankRow(row)) { continue; } - if (StrUtil.isBlank(row.getBizDate())) { + String incomingDate = StrUtil.trim(row.getBizDate()); + if (StrUtil.isBlank(incomingDate)) { row.setBizDate(lastDate); } else { - lastDate = StrUtil.trim(row.getBizDate()); + if (lastDate != null && !incomingDate.equals(lastDate)) { + lastMenuId = null; + lastSpuId = null; + } + lastDate = incomingDate; row.setBizDate(lastDate); } - if (StrUtil.isBlank(row.getMealName())) { + String incomingMeal = StrUtil.trim(row.getMealName()); + if (StrUtil.isBlank(incomingMeal)) { row.setMealName(lastMeal); } else { - lastMeal = StrUtil.trim(row.getMealName()); + if (lastMeal != null && !incomingMeal.equals(lastMeal)) { + lastMenuId = null; + lastSpuId = null; + } + lastMeal = incomingMeal; row.setMealName(lastMeal); } if (StrUtil.isBlank(row.getCategoryName())) { @@ -944,10 +1095,12 @@ public class CanteenDailyMenuImportHelper { lastCombo = combo; row.setComboName(combo); } - if (row.getId() != null) { - lastMenuId = row.getId(); - if (row.getSpuId() != null) { - lastSpuId = row.getSpuId(); + if (StrUtil.isNotBlank(row.getId())) { + lastMenuId = StrUtil.trim(row.getId()); + row.setId(lastMenuId); + if (StrUtil.isNotBlank(row.getSpuId())) { + lastSpuId = StrUtil.trim(row.getSpuId()); + row.setSpuId(lastSpuId); } } else if (lastMenuId != null && StrUtil.isNotBlank(lastCombo)) { diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineSpuServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineSpuServiceImpl.java index dd0c5266e..e2da544c0 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineSpuServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenOnlineSpuServiceImpl.java @@ -202,7 +202,10 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl