每日菜谱导入优化
This commit is contained in:
1 parent
87bec4ba73
commit
43966c04ec
7 files changed
+307
-87
No files matched your search
+11
-5
@@ -7,8 +7,9 @@ import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 每日菜谱导入/导出 Excel 行(导出含菜谱 ID,导入时可按 ID 更新)
|
||||
* <p>列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、库存、配料、菜谱ID、填写说明
|
||||
* <p>列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、库存、配料、菜谱ID、菜品SPU ID、填写说明
|
||||
* <p>配料:单品写单品 SPU;套餐仅在首行填写,写入套餐 SPU
|
||||
* <p>菜谱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)
|
||||
|
||||
+24
@@ -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<Long> ids;
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "封面图地址(上传或图库)")
|
||||
private String picUrl;
|
||||
}
|
||||
+9
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+22
@@ -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.<CanteenSpuEntity>lambdaUpdate()
|
||||
.set(CanteenSpuEntity::getPicUrl, picUrl)
|
||||
.in(CanteenSpuEntity::getId, dto.getIds()));
|
||||
return ok ? R.ok(null, "已更新 " + dto.getIds().size() + " 道菜封面") : R.failed("更新失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改菜品spu
|
||||
*
|
||||
|
||||
+233
-80
@@ -65,7 +65,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
public List<CanteenDailyMenuImportExcel> templateRows() {
|
||||
List<CanteenDailyMenuImportExcel> 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<OnlineSpuComboItemVO> items = menu.getComboItems();
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
OnlineSpuComboItemVO item = items.get(i);
|
||||
Set<Long> 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<String> 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<String, List<CanteenDailyMenuImportExcel>> groups = new LinkedHashMap<>();
|
||||
/** 菜谱ID → 出现过的 日期|餐段(用于汇总跨天串 ID) */
|
||||
Map<Long, LinkedHashSet<String>> menuIdDayHours = new LinkedHashMap<>();
|
||||
/** 菜谱ID → 示例套餐/菜名(取首次) */
|
||||
Map<Long, String> menuIdLabel = new LinkedHashMap<>();
|
||||
Set<LocalDate> 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<String> crossDayIdErrors = new ArrayList<>();
|
||||
for (Map.Entry<Long, LinkedHashSet<String>> en : menuIdDayHours.entrySet()) {
|
||||
if (en.getValue().size() <= 1) {
|
||||
continue;
|
||||
}
|
||||
List<String> 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<String, List<CanteenDailyMenuImportExcel>> e : groups.entrySet()) {
|
||||
List<CanteenDailyMenuImportExcel> 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<CanteenSpuEntity> children = new ArrayList<>();
|
||||
Integer comboPrice = null;
|
||||
Integer comboStock = first.getStock();
|
||||
boolean childFailed = false;
|
||||
Set<String> 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<String, Long> 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<CanteenSpuEntity> children = new ArrayList<>();
|
||||
Integer comboPrice = null;
|
||||
Integer comboStock = items.get(0).getStock();
|
||||
Set<String> 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<CanteenSpuEntity> dedupeComboChildren(List<CanteenSpuEntity> children) {
|
||||
if (CollUtil.isEmpty(children)) {
|
||||
return children;
|
||||
}
|
||||
Set<Long> ids = new HashSet<>();
|
||||
List<CanteenSpuEntity> 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」表示单品,不参与套餐继承。
|
||||
* <p>换日/换餐段时必须清空菜谱ID继承,否则「同名套餐」跨天会被合成一组,组成菜看起来像重复。
|
||||
*/
|
||||
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;
|
||||
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)) {
|
||||
|
||||
+4
-1
@@ -202,7 +202,10 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
|
||||
continue;
|
||||
}
|
||||
if (!childIds.add(item.getChildSpuId())) {
|
||||
throw new IllegalArgumentException("套餐组成菜品不能重复");
|
||||
CanteenOnlineSpuEntity dup = baseMapper.selectById(item.getChildSpuId());
|
||||
String dish = dup != null && StrUtil.isNotBlank(dup.getName()) ? dup.getName()
|
||||
: String.valueOf(item.getChildSpuId());
|
||||
throw new IllegalArgumentException("套餐组成菜品不能重复:「" + dish + "」");
|
||||
}
|
||||
if (selfId != null && Objects.equals(item.getChildSpuId(), selfId)) {
|
||||
throw new IllegalArgumentException("套餐不能包含自身");
|
||||
|
||||
+4
-1
@@ -549,7 +549,10 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
continue;
|
||||
}
|
||||
if (!childIds.add(item.getChildSpuId())) {
|
||||
throw new IllegalArgumentException("套餐组成菜品不能重复");
|
||||
CanteenSpuEntity dup = baseMapper.selectById(item.getChildSpuId());
|
||||
String dish = dup != null && StrUtil.isNotBlank(dup.getName()) ? dup.getName()
|
||||
: String.valueOf(item.getChildSpuId());
|
||||
throw new IllegalArgumentException("套餐组成菜品不能重复:「" + dish + "」");
|
||||
}
|
||||
if (selfId != null && Objects.equals(item.getChildSpuId(), selfId)) {
|
||||
throw new IllegalArgumentException("套餐不能包含自身");
|
||||
|
||||
Reference in new issue
Block a user