每日菜谱修改优化
This commit is contained in:
1 parent
3cd0bf56b8
commit
96c08c61bf
6 files changed
+535
-118
No files matched your search
+1
@@ -8,6 +8,7 @@ import java.math.BigDecimal;
|
||||
/**
|
||||
* 每日菜谱导入/导出 Excel 行(导出含菜谱 ID,导入时可按 ID 更新)
|
||||
* <p>列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、库存、配料、菜谱ID、填写说明
|
||||
* <p>配料:单品写单品 SPU;套餐仅在首行填写,写入套餐 SPU
|
||||
*/
|
||||
@Data
|
||||
public class CanteenDailyMenuImportExcel {
|
||||
|
||||
+3
@@ -27,6 +27,9 @@ public class DailyMenuAdminVO {
|
||||
|
||||
private String name;
|
||||
|
||||
/** 套餐标签(canteen_spu.introduction),导出套餐名优先用此字段,避免名称中的 `-` 被截断 */
|
||||
private String introduction;
|
||||
|
||||
private String picUrl;
|
||||
|
||||
private Long categoryId;
|
||||
|
||||
+8
-4
@@ -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<CanteenDailyMenuImportExcel> exportImportExcel(@RequestParam("merchantId") Long merchantId,
|
||||
@RequestParam("bizDate") String bizDate) {
|
||||
List<CanteenDailyMenuImportExcel> 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<CanteenDailyMenuImportExcel> rows = dailyMenuImportHelper.exportRows(merchantId, rangeStart, rangeEnd);
|
||||
// @ResponseExcel 空 List 不会落盘,导致前端收到空 blob;至少写一行空对象以输出表头
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return List.of(new CanteenDailyMenuImportExcel());
|
||||
|
||||
+33
-6
@@ -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) {
|
||||
|
||||
+489
-108
@@ -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<CanteenDailyMenuImportExcel> templateRows() {
|
||||
List<CanteenDailyMenuImportExcel> 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<CanteenDailyMenuImportExcel> exportRows(Long merchantId, String bizDate) {
|
||||
if (merchantId == null || StrUtil.isBlank(bizDate)) {
|
||||
public List<CanteenDailyMenuImportExcel> 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<DailyMenuAdminVO> 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<CanteenDailyMenuImportExcel> rows = new ArrayList<>();
|
||||
for (DailyMenuAdminVO menu : menus) {
|
||||
rows.addAll(toExportRows(menu));
|
||||
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
|
||||
List<DailyMenuAdminVO> 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<CanteenDailyMenuImportExcel> 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<CanteenDailyMenuImportExcel> 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<String, List<CanteenDailyMenuImportExcel>> groups = new LinkedHashMap<>();
|
||||
Set<LocalDate> 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<String> clearedDayHour = new LinkedHashSet<>();
|
||||
List<CanteenDailyMenuEntity> 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<CanteenDailyMenuImportExcel> readRowsStreaming(MultipartFile file) throws IOException {
|
||||
List<CanteenDailyMenuImportExcel> rows = new ArrayList<>();
|
||||
EasyExcel.read(file.getInputStream(), CanteenDailyMenuImportExcel.class,
|
||||
new ReadListener<CanteenDailyMenuImportExcel>() {
|
||||
@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<LocalDate> dates) {
|
||||
ImportCache cache = new ImportCache();
|
||||
// 只取导入比对所需字段;更新时用 UpdateWrapper,避免 pic_url 等 ALWAYS 字段被写成 null
|
||||
List<CanteenSpuEntity> spus = canteenSpuService.list(Wrappers.<CanteenSpuEntity>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<CanteenDailyMenuEntity> menus = canteenDailyMenuService.list(
|
||||
Wrappers.<CanteenDailyMenuEntity>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<CanteenDailyMenuEntity> pendingMenus) {
|
||||
if (CollUtil.isEmpty(pendingMenus)) {
|
||||
return;
|
||||
}
|
||||
canteenDailyMenuService.saveBatch(pendingMenus, MENU_BATCH_SIZE);
|
||||
pendingMenus.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按菜谱 ID 更新一组行(单品一行,套餐多行)
|
||||
*/
|
||||
private String upsertGroupById(Long menuId, List<CanteenDailyMenuImportExcel> items, Long merchantId, Long deptId,
|
||||
Long defaultCategoryId, Map<String, Long> categoryMap) {
|
||||
Long defaultCategoryId, Map<String, Long> 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<String, Long> categoryMap, LocalDate date, Integer hourType) {
|
||||
Long deptId, Long defaultCategoryId, Map<String, Long> 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<CanteenDailyMenuImportExcel> items,
|
||||
Long merchantId, Long deptId, Long defaultCategoryId, Map<String, Long> categoryMap, String comboName,
|
||||
LocalDate date, Integer hourType) {
|
||||
LocalDate date, Integer hourType, ImportCache cache) {
|
||||
List<CanteenSpuEntity> 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<String> clearedDayHour) {
|
||||
/**
|
||||
* @return true 表示新增了一条排菜
|
||||
*/
|
||||
private boolean schedule(Long merchantId, Long deptId, LocalDate date, Integer hourType, Long spuId,
|
||||
boolean replaceDay, Set<String> clearedDayHour, ImportCache cache,
|
||||
List<CanteenDailyMenuEntity> pendingMenus) {
|
||||
String dh = date + "|" + hourType;
|
||||
if (replaceDay && !clearedDayHour.contains(dh)) {
|
||||
canteenDailyMenuService.remove(Wrappers.<CanteenDailyMenuEntity>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.<CanteenDailyMenuEntity>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<String, Long> categoryMap, String name, String ingredients, Integer priceFen, Integer stock,
|
||||
ImportCache cache) {
|
||||
String clean = cleanDishName(name);
|
||||
CanteenSpuEntity exist = canteenSpuService.getOne(Wrappers.<CanteenSpuEntity>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.<CanteenSpuEntity>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<CanteenSpuEntity> children) {
|
||||
CanteenSpuEntity exist = canteenSpuService.getOne(Wrappers.<CanteenSpuEntity>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<String, Long> categoryMap, String fullName, String pkgLabel, Integer priceFen, Integer stock,
|
||||
List<CanteenSpuEntity> children, String ingredients, ImportCache cache) {
|
||||
CanteenSpuEntity exist = cache.comboByName.get(fullName);
|
||||
List<CanteenSpuComboItemEntity> 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<String, Long> loadCategoryMap(Long merchantId) {
|
||||
List<CanteenCategoryEntity> list = canteenCategoryMapper.selectList(Wrappers.<CanteenCategoryEntity>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<String, Long> categoryMap) {
|
||||
if (StrUtil.isNotBlank(categoryName) && categoryMap != null) {
|
||||
Long id = categoryMap.get(categoryName.trim());
|
||||
private Long resolveOrCreateCategoryId(String categoryName, Long defaultCategoryId, Map<String, Long> 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<String, Long> 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<CanteenDailyMenuImportExcel> 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 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单品标记(不按套餐组块处理):
|
||||
* <ul>
|
||||
* <li>无 / - / (无) / (无)</li>
|
||||
* <li>单品、单品-xxx、单品1、【单品】等以「单品」开头</li>
|
||||
* <li>单点、单点-xxx、单点1、【单点】等以「单点」开头</li>
|
||||
* </ul>
|
||||
*/
|
||||
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 <p><br></p>} 被当成“已有配料”。
|
||||
*/
|
||||
private static String toPlainIngredients(String raw) {
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return "";
|
||||
}
|
||||
String text = raw.replaceAll("(?is)<br\\s*/?>", "\n")
|
||||
.replaceAll("(?is)</p>", "\n")
|
||||
.replaceAll("(?is)<p[^>]*>", "")
|
||||
.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<String, CanteenSpuEntity> dishByName = new HashMap<>();
|
||||
final Map<String, CanteenSpuEntity> comboByName = new HashMap<>();
|
||||
final Set<String> existingMenuKeys = new HashSet<>();
|
||||
}
|
||||
}
|
||||
+1
@@ -180,6 +180,7 @@
|
||||
t.merchant_id,
|
||||
sp.id AS spu_id,
|
||||
sp.name,
|
||||
sp.introduction,
|
||||
sp.price,
|
||||
sp.market_price,
|
||||
sp.pic_url,
|
||||
|
||||
Reference in new issue
Block a user