From 96c08c61bf9d29fd9e008c2ed7c483b35fe34743 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Fri, 28 Aug 2026 10:42:45 +0800
Subject: [PATCH] =?UTF-8?q?=E6=AF=8F=E6=97=A5=E8=8F=9C=E8=B0=B1=E4=BF=AE?=
=?UTF-8?q?=E6=94=B9=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../dto/CanteenDailyMenuImportExcel.java | 1 +
.../pjcloud/canteen/vo/DailyMenuAdminVO.java | 3 +
.../CanteenDailyMenuController.java | 12 +-
.../CanteenDailyMenuTemplateMergeHandler.java | 39 +-
.../impl/CanteenDailyMenuImportHelper.java | 597 ++++++++++++++----
.../mapper/CanteenDailyMenuMapper.xml | 1 +
6 files changed, 535 insertions(+), 118 deletions(-)
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 dfda531c1..ae521dfd3 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
@@ -8,6 +8,7 @@ import java.math.BigDecimal;
/**
* 每日菜谱导入/导出 Excel 行(导出含菜谱 ID,导入时可按 ID 更新)
*
列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、库存、配料、菜谱ID、填写说明
+ *
配料:单品写单品 SPU;套餐仅在首行填写,写入套餐 SPU
*/
@Data
public class CanteenDailyMenuImportExcel {
diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java
index cb03778ca..9acc862b5 100644
--- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java
+++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/DailyMenuAdminVO.java
@@ -27,6 +27,9 @@ public class DailyMenuAdminVO {
private String name;
+ /** 套餐标签(canteen_spu.introduction),导出套餐名优先用此字段,避免名称中的 `-` 被截断 */
+ private String introduction;
+
private String picUrl;
private Long categoryId;
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 9da962dc3..d53529169 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
@@ -87,15 +87,19 @@ public class CanteenDailyMenuController {
}
/**
- * 导出某一天的每日菜谱(与导入模板列一致,含菜谱 ID / SPU ID)
+ * 导出日期段内的每日菜谱(与导入模板列一致,含菜谱 ID / SPU ID)
*/
- @Operation(summary = "导出当日菜谱(可再导入更新)")
+ @Operation(summary = "导出菜谱(可再导入更新)", description = "按 startDate~endDate 导出,最多 62 天")
@ResponseExcel(name = "每日菜谱", writeHandler = CanteenDailyMenuTemplateMergeHandler.class)
@GetMapping("/import/export")
@PreAuthorize("@pms.hasPermission('canteen_canteenDailyMenu_export')")
public List exportImportExcel(@RequestParam("merchantId") Long merchantId,
- @RequestParam("bizDate") String bizDate) {
- List rows = dailyMenuImportHelper.exportRows(merchantId, bizDate);
+ @RequestParam(value = "startDate", required = false) String startDate,
+ @RequestParam(value = "endDate", required = false) String endDate,
+ @RequestParam(value = "bizDate", required = false) String bizDate) {
+ String rangeStart = StrUtil.blankToDefault(startDate, bizDate);
+ String rangeEnd = StrUtil.blankToDefault(endDate, bizDate);
+ List rows = dailyMenuImportHelper.exportRows(merchantId, rangeStart, rangeEnd);
// @ResponseExcel 空 List 不会落盘,导致前端收到空 blob;至少写一行空对象以输出表头
if (rows == null || rows.isEmpty()) {
return List.of(new CanteenDailyMenuImportExcel());
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 a67004233..ade33a4f7 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
@@ -22,6 +22,7 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
private static final int COL_COMBO = 2;
private static final int COL_COMBO_PRICE = 6;
private static final int COL_STOCK = 7;
+ private static final int COL_INGREDIENTS = 8;
private static final int COL_ID = 9;
private static final int COL_SPU_ID = 10;
private static final int COL_TIP = 11;
@@ -138,6 +139,7 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
mergeRange(sheet, runStart, endRow, COL_COMBO, center);
mergeRange(sheet, runStart, endRow, COL_COMBO_PRICE, center);
mergeRange(sheet, runStart, endRow, COL_STOCK, center);
+ mergeRange(sheet, runStart, endRow, COL_INGREDIENTS, center);
mergeRange(sheet, runStart, endRow, COL_ID, center);
mergeRange(sheet, runStart, endRow, COL_SPU_ID, center);
mergeRange(sheet, runStart, endRow, COL_TIP, center);
@@ -238,12 +240,37 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
}
private static boolean isSingleCombo(String combo) {
- return isBlank(combo)
- || "无".equals(combo)
- || "-".equals(combo)
- || "单品".equals(combo)
- || "(无)".equals(combo)
- || "(无)".equals(combo);
+ if (isBlank(combo)) {
+ return true;
+ }
+ String c = stripMarkerWrap(combo.trim());
+ if (c.isEmpty()) {
+ return true;
+ }
+ if ("无".equals(c) || "-".equals(c) || "无套餐".equals(c) || "不套餐".equals(c)) {
+ return true;
+ }
+ return c.startsWith("单品") || c.startsWith("单点");
+ }
+
+ private static String stripMarkerWrap(String s) {
+ String t = s;
+ while (t.length() >= 2) {
+ char a = t.charAt(0);
+ char b = t.charAt(t.length() - 1);
+ boolean wrap = (a == '(' && b == ')')
+ || (a == '(' && b == ')')
+ || (a == '【' && b == '】')
+ || (a == '[' && b == ']')
+ || (a == '「' && b == '」')
+ || (a == '"' && b == '"')
+ || (a == '\'' && b == '\'');
+ if (!wrap) {
+ break;
+ }
+ t = t.substring(1, t.length() - 1).trim();
+ }
+ return t;
}
private static void setColumnWidths(Sheet sheet) {
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 1680ddf4f..c30de52c5 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
@@ -3,6 +3,8 @@ package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.read.listener.ReadListener;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuImportExcel;
import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity;
@@ -29,6 +31,7 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -45,6 +48,15 @@ public class CanteenDailyMenuImportHelper {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ /** 上传文件上限,防止大文件撑爆堆内存 */
+ private static final long MAX_FILE_BYTES = 5L * 1024 * 1024;
+
+ /** 数据行上限(不含表头);合并单元格场景仍需整表在内存,用行数硬顶防 OOM */
+ private static final int MAX_IMPORT_ROWS = 5000;
+
+ /** 排菜批量落库大小 */
+ private static final int MENU_BATCH_SIZE = 200;
+
private final CanteenSpuService canteenSpuService;
private final CanteenDailyMenuService canteenDailyMenuService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
@@ -53,17 +65,17 @@ public class CanteenDailyMenuImportHelper {
public List templateRows() {
List list = new ArrayList<>();
list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "100",
- "分类按名称匹配;套餐价/库存只填首行。已有菜品不选默认分类也可导入"));
- list.add(row(null, null, null, "蒜苔炒腊肉", "荤菜", "蒜苔,腊肉,胡萝卜", "8", null, null, null));
- list.add(row(null, null, null, "清炒时蔬", "素菜", "空心菜", "3", null, null, null));
- list.add(row(null, null, "B套餐", "红烧肉", "荤菜", "五花肉", "10", "18", "80",
+ "分类按名称匹配;套餐价/库存/配料只填首行。已有菜品不选默认分类也可导入"));
+ 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",
"换套餐名即另一套;名称已存在不新增"));
- list.add(row(null, null, null, "土豆丝", "素菜", "土豆", "4", null, null, null));
+ list.add(row(null, null, null, "土豆丝", "素菜", null, "4", null, null, null));
list.add(row(null, null, "无", "千张大骨汤", "汤类", null, "0", null, "50",
- "单品:套餐名填「无」;菜品已存在则复用"));
- list.add(row(null, "早餐", "无", "榨菜肉丝面", "面食", "榨菜,肉丝", "6.4", null, "30",
- "早餐单品"));
- list.add(row(null, null, "无", "豆浆", "饮品", null, "2", null, "100", null));
+ "单品:套餐名填「无」「单品」「单点」或「单品-xxx/单点-xxx」;价格填单价列"));
+ list.add(row(null, "早餐", "单品-1", "榨菜肉丝面", "面食", "榨菜,肉丝", "6.4", null, "30",
+ "单品-xxx / 单点-xxx 一律按单品,套餐价可留空"));
+ list.add(row(null, null, "单点-2", "豆浆", "饮品", null, "2", null, "100", null));
return list;
}
@@ -90,23 +102,36 @@ public class CanteenDailyMenuImportHelper {
}
/**
- * 导出指定日期的每日菜谱(与导入模板列一致,含菜谱 ID)
+ * 导出指定日期段的每日菜谱(与导入模板列一致,含菜谱 ID / SPU ID)
*/
- public List exportRows(Long merchantId, String bizDate) {
- if (merchantId == null || StrUtil.isBlank(bizDate)) {
+ public List exportRows(Long merchantId, String startDate, String endDate) {
+ if (merchantId == null || StrUtil.isBlank(startDate) || StrUtil.isBlank(endDate)) {
return List.of();
}
- LocalDate date = parseDate(bizDate.trim());
- if (date == null) {
+ LocalDate start = parseDate(startDate.trim());
+ LocalDate end = parseDate(endDate.trim());
+ if (start == null || end == null) {
return List.of();
}
- List menus = canteenDailyMenuService.selectAdminList(date.format(DATE_FMT), merchantId, null);
- if (CollUtil.isEmpty(menus)) {
- return List.of();
+ if (end.isBefore(start)) {
+ LocalDate tmp = start;
+ start = end;
+ end = tmp;
+ }
+ long days = end.toEpochDay() - start.toEpochDay() + 1;
+ if (days > 62) {
+ throw new IllegalArgumentException("导出日期段不能超过 62 天");
}
List rows = new ArrayList<>();
- for (DailyMenuAdminVO menu : menus) {
- rows.addAll(toExportRows(menu));
+ for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
+ List menus = canteenDailyMenuService.selectAdminList(date.format(DATE_FMT), merchantId,
+ null);
+ if (CollUtil.isEmpty(menus)) {
+ continue;
+ }
+ for (DailyMenuAdminVO menu : menus) {
+ rows.addAll(toExportRows(menu));
+ }
}
return rows;
}
@@ -132,8 +157,10 @@ public class CanteenDailyMenuImportHelper {
row.setComboName(comboLabel);
row.setPriceYuan(comboPrice);
row.setStock(menu.getStock());
- row.setCategoryName(menu.getCategoryName());
+ row.setIngredients(toExcelIngredients(menu.getIngredients()));
}
+ // 每行都带分类,改菜名新建子菜时不丢分类
+ row.setCategoryName(menu.getCategoryName());
row.setDishName(StrUtil.blankToDefault(item.getName(), ""));
row.setDishPriceYuan(fenToYuan(item.getPrice()));
rows.add(row);
@@ -150,17 +177,41 @@ public class CanteenDailyMenuImportHelper {
row.setCategoryName(menu.getCategoryName());
row.setDishPriceYuan(fenToYuan(menu.getPrice()));
row.setStock(menu.getStock());
- row.setIngredients(menu.getIngredients());
+ row.setIngredients(toExcelIngredients(menu.getIngredients()));
rows.add(row);
}
return rows;
}
+ /**
+ * 导出套餐名:优先 introduction(导入时写入的原始套餐标签,可含 -);
+ * 否则尝试按「套餐名-首道菜名」还原,避免把「套餐A-周二午」截成「套餐A」。
+ */
private static String resolveComboLabel(DailyMenuAdminVO menu) {
- if (StrUtil.isNotBlank(menu.getName()) && menu.getName().contains("-")) {
- return menu.getName().substring(0, menu.getName().indexOf("-"));
+ if (StrUtil.isNotBlank(menu.getIntroduction())) {
+ return menu.getIntroduction().trim();
}
- return menu.getName();
+ String name = StrUtil.trim(menu.getName());
+ if (StrUtil.isBlank(name)) {
+ return "";
+ }
+ if (CollUtil.isNotEmpty(menu.getComboItems())) {
+ String firstDish = StrUtil.trim(menu.getComboItems().get(0).getName());
+ if (StrUtil.isNotBlank(firstDish)) {
+ String firstLine = firstDish.contains("\n") ? firstDish.substring(0, firstDish.indexOf('\n')).trim()
+ : firstDish;
+ for (String dishPart : new String[] { firstDish, firstLine }) {
+ if (StrUtil.isBlank(dishPart)) {
+ continue;
+ }
+ String suffix = "-" + dishPart;
+ if (name.endsWith(suffix) && name.length() > suffix.length()) {
+ return name.substring(0, name.length() - suffix.length());
+ }
+ }
+ }
+ }
+ return name;
}
private static String formatMeal(Integer hourType) {
@@ -190,11 +241,17 @@ public class CanteenDailyMenuImportHelper {
if (file == null || file.isEmpty()) {
return R.failed("请上传 Excel 文件");
}
- List rows = EasyExcel.read(file.getInputStream())
- .head(CanteenDailyMenuImportExcel.class)
- .sheet()
- .headRowNumber(1)
- .doReadSync();
+ if (file.getSize() > MAX_FILE_BYTES) {
+ return R.failed("文件过大,请控制在 " + (MAX_FILE_BYTES / 1024 / 1024) + "MB 以内");
+ }
+
+ List rows;
+ try {
+ rows = readRowsStreaming(file);
+ }
+ catch (IllegalArgumentException ex) {
+ return R.failed(ex.getMessage());
+ }
if (CollUtil.isEmpty(rows)) {
return R.failed("导入数据为空");
}
@@ -211,12 +268,17 @@ public class CanteenDailyMenuImportHelper {
}
dataRows.add(row);
}
+ rows = null;
if (CollUtil.isEmpty(dataRows)) {
return R.failed("导入数据为空");
}
+ if (dataRows.size() > MAX_IMPORT_ROWS) {
+ return R.failed("有效数据行不能超过 " + MAX_IMPORT_ROWS + " 行");
+ }
// groupKey = date|hourType|comboOrEmpty|menuIdOrNew
Map> groups = new LinkedHashMap<>();
+ Set involvedDates = new HashSet<>();
int line = 1;
for (CanteenDailyMenuImportExcel row : dataRows) {
line++;
@@ -235,6 +297,7 @@ public class CanteenDailyMenuImportHelper {
errors.add("第" + line + "行:菜品名不能为空");
continue;
}
+ involvedDates.add(date);
String combo = StrUtil.trim(row.getComboName());
String comboKey = StrUtil.blankToDefault(combo, "");
Long menuId = row.getId();
@@ -254,7 +317,9 @@ public class CanteenDailyMenuImportHelper {
return R.failed(String.join(";", errors.subList(0, Math.min(errors.size(), 20))));
}
+ ImportCache cache = buildImportCache(merchantId, involvedDates);
Set clearedDayHour = new LinkedHashSet<>();
+ List pendingMenus = new ArrayList<>();
int updated = 0;
int createdDish = 0;
int createdCombo = 0;
@@ -265,7 +330,7 @@ public class CanteenDailyMenuImportHelper {
Long menuId = items.stream().map(CanteenDailyMenuImportExcel::getId).filter(Objects::nonNull).findFirst()
.orElse(null);
if (menuId != null) {
- String err = upsertGroupById(menuId, items, merchantId, deptId, categoryId, categoryMap);
+ String err = upsertGroupById(menuId, items, merchantId, deptId, categoryId, categoryMap, cache);
if (err != null) {
errors.add(err);
}
@@ -284,13 +349,16 @@ public class CanteenDailyMenuImportHelper {
for (CanteenDailyMenuImportExcel item : items) {
try {
FindOrCreate r = findOrCreateDish(merchantId, deptId,
- resolveCategoryId(item.getCategoryName(), categoryId, categoryMap),
- item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), item.getStock());
+ item.getCategoryName(), categoryId, categoryMap,
+ item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), item.getStock(),
+ cache);
if (r.created) {
createdDish++;
}
- schedule(merchantId, deptId, date, hourType, r.spu.getId(), replaceDay, clearedDayHour);
- scheduled++;
+ if (schedule(merchantId, deptId, date, hourType, r.spu.getId(), replaceDay, clearedDayHour,
+ cache, pendingMenus)) {
+ scheduled++;
+ }
}
catch (IllegalArgumentException ex) {
errors.add(ex.getMessage());
@@ -312,8 +380,8 @@ public class CanteenDailyMenuImportHelper {
}
try {
FindOrCreate r = findOrCreateDish(merchantId, deptId,
- resolveCategoryId(item.getCategoryName(), categoryId, categoryMap),
- item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), null);
+ item.getCategoryName(), categoryId, categoryMap,
+ item.getDishName(), null, resolveDishPriceFen(item), null, cache);
if (r.created) {
createdDish++;
}
@@ -335,19 +403,26 @@ public class CanteenDailyMenuImportHelper {
String fullComboName = comboName + "-" + mainName;
try {
FindOrCreate combo = findOrCreateCombo(merchantId, deptId,
- resolveCategoryId(first.getCategoryName(), categoryId, categoryMap),
- fullComboName, comboName, comboPrice, comboStock, children);
+ first.getCategoryName(), categoryId, categoryMap,
+ fullComboName, comboName, comboPrice, comboStock, children,
+ resolveComboIngredients(first), cache);
if (combo.created) {
createdCombo++;
}
- schedule(merchantId, deptId, date, hourType, combo.spu.getId(), replaceDay, clearedDayHour);
- scheduled++;
+ if (schedule(merchantId, deptId, date, hourType, combo.spu.getId(), replaceDay, clearedDayHour,
+ cache, pendingMenus)) {
+ scheduled++;
+ }
}
catch (IllegalArgumentException ex) {
errors.add(ex.getMessage());
}
}
+ if (pendingMenus.size() >= MENU_BATCH_SIZE) {
+ flushPendingMenus(pendingMenus);
+ }
}
+ flushPendingMenus(pendingMenus);
if (CollUtil.isNotEmpty(errors)) {
return R.failed(String.join(";", errors.subList(0, Math.min(errors.size(), 20))));
}
@@ -356,11 +431,87 @@ public class CanteenDailyMenuImportHelper {
updated, createdDish, createdCombo, scheduled));
}
+ /**
+ * EasyExcel 流式按行解析;合并单元格需整表,故仍收集 List,但用行数硬顶防 OOM。
+ */
+ private static List readRowsStreaming(MultipartFile file) throws IOException {
+ List rows = new ArrayList<>();
+ EasyExcel.read(file.getInputStream(), CanteenDailyMenuImportExcel.class,
+ new ReadListener() {
+ @Override
+ public void invoke(CanteenDailyMenuImportExcel data, AnalysisContext context) {
+ if (rows.size() >= MAX_IMPORT_ROWS) {
+ throw new IllegalArgumentException("导入行数不能超过 " + MAX_IMPORT_ROWS + " 行");
+ }
+ rows.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {
+ // no-op
+ }
+ }).sheet().headRowNumber(1).doRead();
+ return rows;
+ }
+
+ private ImportCache buildImportCache(Long merchantId, Set dates) {
+ ImportCache cache = new ImportCache();
+ // 只取导入比对所需字段;更新时用 UpdateWrapper,避免 pic_url 等 ALWAYS 字段被写成 null
+ List spus = canteenSpuService.list(Wrappers.lambdaQuery()
+ .eq(CanteenSpuEntity::getMerchantId, merchantId)
+ .select(CanteenSpuEntity::getId, CanteenSpuEntity::getName, CanteenSpuEntity::getSpuType,
+ CanteenSpuEntity::getPrice, CanteenSpuEntity::getMarketPrice, CanteenSpuEntity::getStock,
+ CanteenSpuEntity::getDescription, CanteenSpuEntity::getCategoryId,
+ CanteenSpuEntity::getMerchantId, CanteenSpuEntity::getDeptId,
+ CanteenSpuEntity::getPicUrl));
+ if (CollUtil.isNotEmpty(spus)) {
+ for (CanteenSpuEntity spu : spus) {
+ if (spu == null || StrUtil.isBlank(spu.getName())) {
+ continue;
+ }
+ if (spu.getPicUrl() == null) {
+ spu.setPicUrl("");
+ }
+ int type = spu.getSpuType() == null ? 1 : spu.getSpuType();
+ if (type == 2) {
+ cache.comboByName.putIfAbsent(spu.getName(), spu);
+ }
+ else {
+ cache.dishByName.putIfAbsent(spu.getName(), spu);
+ }
+ }
+ }
+ if (CollUtil.isNotEmpty(dates)) {
+ List menus = canteenDailyMenuService.list(
+ Wrappers.lambdaQuery()
+ .eq(CanteenDailyMenuEntity::getMerchantId, merchantId)
+ .in(CanteenDailyMenuEntity::getStartTime, dates)
+ .select(CanteenDailyMenuEntity::getStartTime, CanteenDailyMenuEntity::getHourType,
+ CanteenDailyMenuEntity::getSpuId));
+ if (CollUtil.isNotEmpty(menus)) {
+ for (CanteenDailyMenuEntity m : menus) {
+ if (m.getStartTime() != null && m.getHourType() != null && m.getSpuId() != null) {
+ cache.existingMenuKeys.add(menuKey(m.getStartTime(), m.getHourType(), m.getSpuId()));
+ }
+ }
+ }
+ }
+ return cache;
+ }
+
+ private void flushPendingMenus(List pendingMenus) {
+ if (CollUtil.isEmpty(pendingMenus)) {
+ return;
+ }
+ canteenDailyMenuService.saveBatch(pendingMenus, MENU_BATCH_SIZE);
+ pendingMenus.clear();
+ }
+
/**
* 按菜谱 ID 更新一组行(单品一行,套餐多行)
*/
private String upsertGroupById(Long menuId, List items, Long merchantId, Long deptId,
- Long defaultCategoryId, Map categoryMap) {
+ Long defaultCategoryId, Map categoryMap, ImportCache cache) {
CanteenDailyMenuEntity menu = canteenDailyMenuService.getById(menuId);
if (menu == null) {
return "菜谱ID " + menuId + " 不存在";
@@ -380,10 +531,11 @@ public class CanteenDailyMenuImportHelper {
String comboName = StrUtil.trim(first.getComboName());
try {
if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) {
- return upsertSingleById(menu, first, merchantId, deptId, defaultCategoryId, categoryMap, date, hourType);
+ return upsertSingleById(menu, first, merchantId, deptId, defaultCategoryId, categoryMap, date, hourType,
+ cache);
}
return upsertComboGroupById(menu, items, merchantId, deptId, defaultCategoryId, categoryMap, comboName, date,
- hourType);
+ hourType, cache);
}
catch (IllegalArgumentException ex) {
return ex.getMessage();
@@ -391,14 +543,15 @@ public class CanteenDailyMenuImportHelper {
}
private String upsertSingleById(CanteenDailyMenuEntity menu, CanteenDailyMenuImportExcel row, Long merchantId,
- Long deptId, Long defaultCategoryId, Map categoryMap, LocalDate date, Integer hourType) {
+ Long deptId, Long defaultCategoryId, Map categoryMap, LocalDate date, Integer hourType,
+ ImportCache cache) {
String dish = StrUtil.trim(row.getDishName());
if (StrUtil.isBlank(dish)) {
return "菜谱ID " + menu.getId() + ":菜品名不能为空";
}
FindOrCreate created = findOrCreateDish(merchantId, deptId,
- resolveCategoryId(row.getCategoryName(), defaultCategoryId, categoryMap), dish, row.getIngredients(),
- resolveDishPriceFen(row), row.getStock());
+ row.getCategoryName(), defaultCategoryId, categoryMap, dish, row.getIngredients(),
+ resolveDishPriceFen(row), row.getStock(), cache);
menu.setStartTime(date);
menu.setHourType(hourType);
menu.setSpuId(created.spu.getId());
@@ -408,7 +561,7 @@ public class CanteenDailyMenuImportHelper {
private String upsertComboGroupById(CanteenDailyMenuEntity menu, List items,
Long merchantId, Long deptId, Long defaultCategoryId, Map categoryMap, String comboName,
- LocalDate date, Integer hourType) {
+ LocalDate date, Integer hourType, ImportCache cache) {
List children = new ArrayList<>();
Integer comboPrice = null;
Integer comboStock = items.get(0).getStock();
@@ -422,19 +575,22 @@ public class CanteenDailyMenuImportHelper {
return "菜谱ID " + menu.getId() + ":套餐组成菜品名不能为空";
}
FindOrCreate r = findOrCreateDish(merchantId, deptId,
- resolveCategoryId(item.getCategoryName(), defaultCategoryId, categoryMap), dish,
- item.getIngredients(), resolveDishPriceFen(item), null);
+ item.getCategoryName(), defaultCategoryId, categoryMap, dish,
+ null, resolveDishPriceFen(item), null, cache);
children.add(r.spu);
}
if (comboPrice == null) {
- CanteenSpuEntity existCombo = canteenSpuService.getById(menu.getSpuId());
+ CanteenSpuEntity existCombo = cache.comboByName.values().stream()
+ .filter(s -> Objects.equals(s.getId(), menu.getSpuId()))
+ .findFirst()
+ .orElseGet(() -> canteenSpuService.getById(menu.getSpuId()));
comboPrice = existCombo != null && existCombo.getPrice() != null ? existCombo.getPrice() : 0;
}
String mainName = children.get(0).getName();
String fullComboName = comboName + "-" + mainName;
FindOrCreate combo = findOrCreateCombo(merchantId, deptId,
- resolveCategoryId(items.get(0).getCategoryName(), defaultCategoryId, categoryMap), fullComboName,
- comboName, comboPrice, comboStock, children);
+ items.get(0).getCategoryName(), defaultCategoryId, categoryMap, fullComboName,
+ comboName, comboPrice, comboStock, children, resolveComboIngredients(items.get(0)), cache);
menu.setStartTime(date);
menu.setHourType(hourType);
menu.setSpuId(combo.spu.getId());
@@ -442,8 +598,12 @@ public class CanteenDailyMenuImportHelper {
return null;
}
- private void schedule(Long merchantId, Long deptId, LocalDate date, Integer hourType, Long spuId,
- boolean replaceDay, Set clearedDayHour) {
+ /**
+ * @return true 表示新增了一条排菜
+ */
+ private boolean schedule(Long merchantId, Long deptId, LocalDate date, Integer hourType, Long spuId,
+ boolean replaceDay, Set clearedDayHour, ImportCache cache,
+ List pendingMenus) {
String dh = date + "|" + hourType;
if (replaceDay && !clearedDayHour.contains(dh)) {
canteenDailyMenuService.remove(Wrappers.lambdaQuery()
@@ -451,14 +611,11 @@ public class CanteenDailyMenuImportHelper {
.eq(CanteenDailyMenuEntity::getStartTime, date)
.eq(CanteenDailyMenuEntity::getHourType, hourType));
clearedDayHour.add(dh);
+ cache.existingMenuKeys.removeIf(k -> k.startsWith(date + "|" + hourType + "|"));
}
- boolean exists = canteenDailyMenuService.count(Wrappers.lambdaQuery()
- .eq(CanteenDailyMenuEntity::getMerchantId, merchantId)
- .eq(CanteenDailyMenuEntity::getStartTime, date)
- .eq(CanteenDailyMenuEntity::getHourType, hourType)
- .eq(CanteenDailyMenuEntity::getSpuId, spuId)) > 0;
- if (exists) {
- return;
+ String key = menuKey(date, hourType, spuId);
+ if (cache.existingMenuKeys.contains(key)) {
+ return false;
}
CanteenDailyMenuEntity menu = new CanteenDailyMenuEntity();
menu.setMerchantId(merchantId);
@@ -466,58 +623,87 @@ public class CanteenDailyMenuImportHelper {
menu.setStartTime(date);
menu.setHourType(hourType);
menu.setSpuId(spuId);
- canteenDailyMenuService.save(menu);
+ pendingMenus.add(menu);
+ cache.existingMenuKeys.add(key);
+ return true;
}
- private FindOrCreate findOrCreateDish(Long merchantId, Long deptId, Long categoryId, String name,
- String ingredients, Integer priceFen, Integer stock) {
+ private FindOrCreate findOrCreateDish(Long merchantId, Long deptId, String categoryName, Long defaultCategoryId,
+ Map categoryMap, String name, String ingredients, Integer priceFen, Integer stock,
+ ImportCache cache) {
String clean = cleanDishName(name);
- CanteenSpuEntity exist = canteenSpuService.getOne(Wrappers.lambdaQuery()
- .eq(CanteenSpuEntity::getMerchantId, merchantId)
- .eq(CanteenSpuEntity::getName, clean)
- .eq(CanteenSpuEntity::getSpuType, 1)
- .last("limit 1"), false);
+ CanteenSpuEntity exist = cache.dishByName.get(clean);
if (exist != null) {
- boolean needUpdate = false;
- if (StrUtil.isNotBlank(ingredients) && StrUtil.isBlank(exist.getDescription())) {
- exist.setDescription(ingredients.trim());
- needUpdate = true;
+ 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 && (exist.getPrice() == null || exist.getPrice() == 0)) {
+ if (priceFen != null && priceFen > 0 && !Objects.equals(exist.getPrice(), priceFen)) {
exist.setPrice(priceFen);
exist.setMarketPrice(priceFen);
- needUpdate = true;
+ changePrice = true;
}
if (stock != null && stock >= 0 && !Objects.equals(exist.getStock(), stock)) {
exist.setStock(stock);
- needUpdate = true;
+ changeStock = true;
}
- if (needUpdate) {
- canteenSpuService.updateById(exist);
+ // 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);
}
return new FindOrCreate(exist, false);
}
+ Long categoryId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, deptId);
if (categoryId == null) {
- throw new IllegalArgumentException("新建菜品「" + clean + "」缺少分类:请在 Excel 填写分类名,或选择默认分类");
+ throw new IllegalArgumentException(missingCategoryMessage("新建菜品「" + clean + "」", categoryName, categoryMap));
}
CanteenSpuEntity spu = newBaseSpu(merchantId, deptId, categoryId, clean, 1);
- spu.setDescription(StrUtil.trim(ingredients));
+ spu.setDescription(normalizeImportIngredients(ingredients));
spu.setPrice(priceFen == null ? 0 : priceFen);
spu.setMarketPrice(spu.getPrice());
if (stock != null && stock >= 0) {
spu.setStock(stock);
}
canteenSpuService.save(spu);
+ cache.dishByName.put(clean, spu);
return new FindOrCreate(spu, true);
}
- private FindOrCreate findOrCreateCombo(Long merchantId, Long deptId, Long categoryId, String fullName,
- String pkgLabel, Integer priceFen, Integer stock, List children) {
- CanteenSpuEntity exist = canteenSpuService.getOne(Wrappers.lambdaQuery()
- .eq(CanteenSpuEntity::getMerchantId, merchantId)
- .eq(CanteenSpuEntity::getName, fullName)
- .eq(CanteenSpuEntity::getSpuType, 2)
- .last("limit 1"), false);
+ 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 items = new ArrayList<>();
int sort = 0;
for (CanteenSpuEntity child : children) {
@@ -533,15 +719,32 @@ public class CanteenDailyMenuImportHelper {
if (stock != null && stock >= 0) {
exist.setStock(stock);
}
+ if (StrUtil.isNotBlank(pkgLabel)) {
+ exist.setIntroduction(pkgLabel);
+ }
+ if (StrUtil.isNotBlank(normalizeCategoryName(categoryName))) {
+ Long newCatId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId,
+ deptId);
+ if (newCatId != null) {
+ exist.setCategoryId(newCatId);
+ }
+ }
+ applyComboIngredients(exist, ingredients);
exist.setComboItems(items);
+ if (exist.getPicUrl() == null) {
+ exist.setPicUrl("");
+ }
canteenSpuService.updateDeep(exist);
+ cache.comboByName.put(fullName, exist);
return new FindOrCreate(exist, false);
}
+ Long categoryId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, deptId);
if (categoryId == null) {
- throw new IllegalArgumentException("新建套餐「" + fullName + "」缺少分类:请在 Excel 填写分类名,或选择默认分类");
+ throw new IllegalArgumentException(missingCategoryMessage("新建套餐「" + fullName + "」", categoryName, categoryMap));
}
CanteenSpuEntity combo = newBaseSpu(merchantId, deptId, categoryId, fullName, 2);
combo.setIntroduction(pkgLabel);
+ applyComboIngredients(combo, ingredients);
combo.setPrice(priceFen);
combo.setMarketPrice(priceFen);
if (stock != null && stock >= 0) {
@@ -549,9 +752,29 @@ public class CanteenDailyMenuImportHelper {
}
combo.setComboItems(items);
canteenSpuService.saveDeep(combo);
+ cache.comboByName.put(fullName, combo);
return new FindOrCreate(combo, true);
}
+ private static String menuKey(LocalDate date, Integer hourType, Long spuId) {
+ return date + "|" + hourType + "|" + spuId;
+ }
+
+ private static void applyComboIngredients(CanteenSpuEntity combo, String ingredients) {
+ String importIngredients = normalizeImportIngredients(ingredients);
+ if (StrUtil.isBlank(importIngredients)) {
+ return;
+ }
+ String existPlain = toPlainIngredients(combo.getDescription());
+ if (!importIngredients.equals(existPlain)) {
+ combo.setDescription(importIngredients);
+ }
+ }
+
+ private static String resolveComboIngredients(CanteenDailyMenuImportExcel first) {
+ return first == null ? "" : normalizeImportIngredients(first.getIngredients());
+ }
+
private Map loadCategoryMap(Long merchantId) {
List list = canteenCategoryMapper.selectList(Wrappers.lambdaQuery()
.eq(CanteenCategoryEntity::getMerchantId, merchantId));
@@ -560,26 +783,84 @@ public class CanteenDailyMenuImportHelper {
return map;
}
for (CanteenCategoryEntity c : list) {
- if (c != null && StrUtil.isNotBlank(c.getName()) && c.getId() != null) {
- map.put(c.getName().trim(), c.getId());
+ if (c == null || c.getId() == null || StrUtil.isBlank(c.getName())) {
+ continue;
+ }
+ String raw = c.getName().trim();
+ String normalized = normalizeCategoryName(raw);
+ map.putIfAbsent(raw, c.getId());
+ if (StrUtil.isNotBlank(normalized)) {
+ map.putIfAbsent(normalized, c.getId());
}
}
return map;
}
/**
- * Excel 分类名优先;匹配不到再用默认分类(可为 null)
+ * Excel 分类名:商户下已有则引用;没有则自动新建;分类名为空时用兜底分类。
*/
- private static Long resolveCategoryId(String categoryName, Long defaultCategoryId, Map categoryMap) {
- if (StrUtil.isNotBlank(categoryName) && categoryMap != null) {
- Long id = categoryMap.get(categoryName.trim());
+ private Long resolveOrCreateCategoryId(String categoryName, Long defaultCategoryId, Map categoryMap,
+ Long merchantId, Long deptId) {
+ String key = normalizeCategoryName(categoryName);
+ if (StrUtil.isNotBlank(key) && categoryMap != null) {
+ Long id = categoryMap.get(key);
+ if (id == null) {
+ id = categoryMap.get(StrUtil.trim(categoryName));
+ }
if (id != null) {
return id;
}
+ CanteenCategoryEntity cat = new CanteenCategoryEntity();
+ cat.setName(key);
+ cat.setMerchantId(merchantId);
+ cat.setDeptId(deptId);
+ cat.setParentId(0L);
+ cat.setSort(0);
+ cat.setStatus(1);
+ cat.setPicUrl("");
+ // 批量导入直接 insert,避免每条都打 MQTT
+ canteenCategoryMapper.insert(cat);
+ Long newId = cat.getId();
+ categoryMap.put(key, newId);
+ String raw = StrUtil.trim(categoryName);
+ if (StrUtil.isNotBlank(raw)) {
+ categoryMap.putIfAbsent(raw, newId);
+ }
+ return newId;
}
return defaultCategoryId;
}
+ private static String normalizeCategoryName(String name) {
+ if (name == null) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder(name.length());
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (c == '\r' || c == '\n' || c == '\t' || c == '\u00A0' || c == '\u3000') {
+ sb.append(' ');
+ continue;
+ }
+ // 全角数字 → 半角,避免「荤菜1」匹配不上「荤菜1」
+ if (c >= '0' && c <= '9') {
+ sb.append((char) ('0' + (c - '0')));
+ continue;
+ }
+ sb.append(c);
+ }
+ return sb.toString().replaceAll(" +", " ").trim();
+ }
+
+ private static String missingCategoryMessage(String prefix, String excelCategory, Map categoryMap) {
+ String excel = normalizeCategoryName(excelCategory);
+ if (StrUtil.isNotBlank(excel)) {
+ // 理论上走自动建分类后不会到这里;保留兜底文案
+ return prefix + ":分类「" + excel + "」处理失败,请重试或选择兜底分类";
+ }
+ return prefix + ":缺少分类,请在 Excel「分类」列填写,或导入时选择兜底分类";
+ }
+
private CanteenSpuEntity newBaseSpu(Long merchantId, Long deptId, Long categoryId, String name, int spuType) {
CanteenSpuEntity spu = new CanteenSpuEntity();
spu.setName(name);
@@ -608,13 +889,14 @@ public class CanteenDailyMenuImportHelper {
}
/**
- * 合并单元格模板读入后,非首行日期/餐段/套餐名为空,需向下填充。
- * 套餐名填「无」「-」「单品」表示单品,不参与套餐继承。
+ * 合并单元格模板读入后,非首行日期/餐段/套餐名/分类为空,需向下填充。
+ * 套餐名填「无」「单品」「单点」或「单品-xxx / 单点-xxx」表示单品,不参与套餐继承。
*/
private static void normalizeMergedCells(List rows) {
String lastDate = null;
String lastMeal = null;
String lastCombo = null;
+ String lastCategory = null;
Long lastMenuId = null;
Long lastSpuId = null;
for (CanteenDailyMenuImportExcel row : rows) {
@@ -635,6 +917,13 @@ public class CanteenDailyMenuImportHelper {
lastMeal = StrUtil.trim(row.getMealName());
row.setMealName(lastMeal);
}
+ if (StrUtil.isBlank(row.getCategoryName())) {
+ row.setCategoryName(lastCategory);
+ }
+ else {
+ lastCategory = normalizeCategoryName(row.getCategoryName());
+ row.setCategoryName(lastCategory);
+ }
String combo = StrUtil.trim(row.getComboName());
if (StrUtil.isBlank(combo)) {
row.setComboName(lastCombo);
@@ -670,9 +959,51 @@ public class CanteenDailyMenuImportHelper {
}
}
+ /**
+ * 单品标记(不按套餐组块处理):
+ *
+ * - 无 / - / (无) / (无)
+ * - 单品、单品-xxx、单品1、【单品】等以「单品」开头
+ * - 单点、单点-xxx、单点1、【单点】等以「单点」开头
+ *
+ */
private static boolean isSingleComboMarker(String combo) {
- return "无".equals(combo) || "-".equals(combo) || "单品".equals(combo)
- || "(无)".equals(combo) || "(无)".equals(combo);
+ if (StrUtil.isBlank(combo)) {
+ return false;
+ }
+ String c = stripMarkerWrap(combo.trim());
+ if (c.isEmpty()) {
+ return false;
+ }
+ if ("无".equals(c) || "-".equals(c) || "无套餐".equals(c) || "不套餐".equals(c)) {
+ return true;
+ }
+ return c.startsWith("单品") || c.startsWith("单点");
+ }
+
+ /** 去掉首尾常见括号/书名号,便于识别【单品】、(单点-1)等 */
+ private static String stripMarkerWrap(String s) {
+ String t = s;
+ while (t.length() >= 2) {
+ char a = t.charAt(0);
+ char b = t.charAt(t.length() - 1);
+ boolean wrap = (a == '(' && b == ')')
+ || (a == '(' && b == ')')
+ || (a == '【' && b == '】')
+ || (a == '[' && b == ']')
+ || (a == '「' && b == '」')
+ || (a == '"' && b == '"')
+ || (a == '\'' && b == '\'');
+ if (!wrap) {
+ break;
+ }
+ t = t.substring(1, t.length() - 1).trim();
+ }
+ // 兼容表头写成「(无)」整段
+ if ("无".equals(t) || "单品".equals(t) || "单点".equals(t)) {
+ return t;
+ }
+ return t;
}
private static Integer parseMeal(String meal) {
@@ -712,7 +1043,7 @@ public class CanteenDailyMenuImportHelper {
}
/**
- * 菜品单价:优先「菜品单价(元)」;未填则单品可回退「套餐价(元)」兼容旧模板;都无则 0。
+ * 菜品单价:优先「单价(元)」;单品未填单价时回退「套餐价(元)」兼容旧表。
*/
private static int resolveDishPriceFen(CanteenDailyMenuImportExcel item) {
Integer dishFen = toFen(item.getDishPriceYuan());
@@ -720,25 +1051,68 @@ public class CanteenDailyMenuImportHelper {
return Math.max(dishFen, 0);
}
Integer legacy = toFen(item.getPriceYuan());
- if (legacy != null && StrUtil.isBlank(item.getComboName())) {
+ if (legacy != null && (StrUtil.isBlank(item.getComboName()) || isSingleComboMarker(item.getComboName()))) {
return Math.max(legacy, 0);
}
return 0;
}
+ /**
+ * 规范化菜品名:保留中英文及换行(Excel 内多行英文说明不再截断)。
+ */
static String cleanDishName(String raw) {
if (raw == null) {
return "";
}
- String s = raw.replace("\r", "\n").trim();
- String first = s.split("\n")[0].trim();
- first = first.replaceAll("[\\p{So}\\p{Cn}]", "").trim();
- String cn = first.replaceAll("[A-Za-z].*$", "").trim();
- cn = cn.replaceAll("[@#%&&]+$", "").replaceAll("\\s+", "");
- if (cn.length() >= 2) {
- return cn;
+ String normalized = raw.replace("\r\n", "\n").replace('\r', '\n').trim();
+ if (StrUtil.isBlank(normalized)) {
+ return "";
}
- return first.replaceAll("\\s+", " ").trim();
+ StringBuilder sb = new StringBuilder();
+ for (String line : normalized.split("\n")) {
+ String trimmed = StrUtil.trim(line).replaceAll("\\s+", " ");
+ if (StrUtil.isBlank(trimmed)) {
+ continue;
+ }
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(trimmed);
+ }
+ return sb.toString();
+ }
+
+ /** 富文本配料转 Excel 纯文本 */
+ private static String toExcelIngredients(String raw) {
+ return toPlainIngredients(raw);
+ }
+
+ private static String normalizeImportIngredients(String raw) {
+ return toPlainIngredients(raw);
+ }
+
+ /**
+ * 去掉 HTML 标签与常见空白实体,避免编辑器空内容 {@code
} 被当成“已有配料”。
+ */
+ private static String toPlainIngredients(String raw) {
+ if (StrUtil.isBlank(raw)) {
+ return "";
+ }
+ String text = raw.replaceAll("(?is)
", "\n")
+ .replaceAll("(?is)
", "\n")
+ .replaceAll("(?is)]*>", "")
+ .replaceAll("(?is)<[^>]+>", "");
+ text = text.replace(" ", " ")
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\r", "\n");
+ text = text.lines()
+ .map(String::trim)
+ .filter(StrUtil::isNotBlank)
+ .reduce((a, b) -> a + "," + b)
+ .orElse("");
+ return StrUtil.trim(text);
}
private static class FindOrCreate {
@@ -750,4 +1124,11 @@ public class CanteenDailyMenuImportHelper {
this.created = created;
}
}
+
+ /** 导入过程内存缓存:避免每行菜品/排菜都打库 */
+ private static class ImportCache {
+ final Map dishByName = new HashMap<>();
+ final Map comboByName = new HashMap<>();
+ final Set existingMenuKeys = new HashSet<>();
+ }
}
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml
index 256b5b08d..db71e310b 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml
@@ -180,6 +180,7 @@
t.merchant_id,
sp.id AS spu_id,
sp.name,
+ sp.introduction,
sp.price,
sp.market_price,
sp.pic_url,