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