This commit is contained in:
dzc committed 2026-09-04 17:33:26 +08:00
commit a9a5a2f8d0
2 files changed
+123 -65

No files matched your search

@@ -10,6 +10,7 @@ import java.math.BigDecimal;
* <p>列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、库存、配料、菜谱ID、菜品SPU ID、填写说明
* <p>配料:单品写单品 SPU;套餐仅在首行填写,写入套餐 SPU
* <p>菜谱ID / 菜品SPU ID 必须用文本导出,否则 Excel 对超长雪花 ID 会丢精度导致再导入报「不存在」。
* <p>无「菜品SPU ID」时一律新建菜品(不按菜名复用);有 ID 才按 ID 更新。
*/
@Data
public class CanteenDailyMenuImportExcel {
@@ -81,11 +81,11 @@ 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会丢精度"));
"无菜品SPU ID则新建菜品(不按菜名复用);有ID才更新。套餐价/库存/配料填首行。ID列勿改数字格式"));
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",
"换套餐名即另一套;名称已存在不新增"));
"换套餐名即另一套;无套餐SPU ID则新建套餐"));
list.add(row(null, null, null, "土豆丝", "素菜", null, "4", null, null, null));
list.add(row(null, null, "无", "千张大骨汤", "汤类", null, "0", null, "50",
"单品:套餐名填「无」「单品」「单点」或「单品-xxx/单点-xxx」;价格填单价列"));
@@ -93,7 +93,7 @@ public class CanteenDailyMenuImportHelper {
"单品-xxx / 单点-xxx 一律按单品,套餐价可留空"));
list.add(row(null, null, "单点-2", "豆浆", "饮品", null, "2", null, "100", null));
list.add(row("2026-08-18", "午餐,晚餐", "无", "番茄蛋汤", "汤类", null, "2", null, "80",
"同一道菜多餐:餐段可写「午餐,晚餐」或「三餐/全天」,系统拆成多条排菜且菜品只建一份"));
"多餐拆行:无菜品SPU ID时各餐段各自新建菜品(库存独立)"));
return list;
}
@@ -522,7 +522,7 @@ public class CanteenDailyMenuImportHelper {
else {
action = "INSERT";
actionLabel = "新增";
tip = menuId != null ? "菜谱ID未匹配,将按新增排菜" : "新增排菜(菜品不存在时自动创建)";
tip = menuId != null ? "菜谱ID未匹配,将按新增排菜" : "新增排菜(无菜品SPU ID则新建菜品,不按菜名复用)";
selected = true;
totalInsert++;
}
@@ -708,7 +708,7 @@ public class CanteenDailyMenuImportHelper {
FindOrCreate r = findOrCreateDish(merchantId, deptId,
item.getCategoryName(), categoryId, categoryMap,
item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), item.getStock(),
cache);
parseExcelLong(item.getSpuId()), cache);
if (r.created) {
createdDish++;
}
@@ -738,7 +738,8 @@ public class CanteenDailyMenuImportHelper {
}
FindOrCreate r = findOrCreateDish(merchantId, deptId,
item.getCategoryName(), categoryId, categoryMap,
item.getDishName(), null, resolveDishPriceFen(item), null, cache);
item.getDishName(), null, resolveDishPriceFen(item), null,
parseExcelLong(item.getSpuId()), cache);
if (r.created) {
createdDish++;
}
@@ -758,7 +759,7 @@ public class CanteenDailyMenuImportHelper {
FindOrCreate combo = findOrCreateCombo(merchantId, deptId,
first.getCategoryName(), categoryId, categoryMap,
fullComboName, comboName, comboPrice, comboStock, children,
resolveComboIngredients(first), cache);
resolveComboIngredients(first), parseExcelLong(first.getSpuId()), cache);
if (combo.created) {
createdCombo++;
}
@@ -1001,6 +1002,9 @@ public class CanteenDailyMenuImportHelper {
if (spu.getPicUrl() == null) {
spu.setPicUrl("");
}
if (spu.getId() != null) {
cache.spuById.put(spu.getId(), spu);
}
int type = spu.getSpuType() == null ? 1 : spu.getSpuType();
if (type == 2) {
cache.comboByName.putIfAbsent(spu.getName(), spu);
@@ -1126,7 +1130,7 @@ public class CanteenDailyMenuImportHelper {
}
FindOrCreate created = findOrCreateDish(merchantId, deptId,
row.getCategoryName(), defaultCategoryId, categoryMap, dish, row.getIngredients(),
resolveDishPriceFen(row), row.getStock(), cache);
resolveDishPriceFen(row), row.getStock(), parseExcelLong(row.getSpuId()), cache);
menu.setStartTime(date);
menu.setHourType(hourType);
menu.setSpuId(created.spu.getId());
@@ -1156,7 +1160,7 @@ public class CanteenDailyMenuImportHelper {
}
FindOrCreate r = findOrCreateDish(merchantId, deptId,
item.getCategoryName(), defaultCategoryId, categoryMap, dish,
null, resolveDishPriceFen(item), null, cache);
null, resolveDishPriceFen(item), null, parseExcelLong(item.getSpuId()), cache);
children.add(r.spu);
}
children = dedupeComboChildren(children);
@@ -1174,7 +1178,8 @@ public class CanteenDailyMenuImportHelper {
String fullComboName = comboName + "-" + mainName;
FindOrCreate combo = findOrCreateCombo(merchantId, deptId,
items.get(0).getCategoryName(), defaultCategoryId, categoryMap, fullComboName,
comboName, comboPrice, comboStock, children, resolveComboIngredients(items.get(0)), cache);
comboName, comboPrice, comboStock, children, resolveComboIngredients(items.get(0)),
parseExcelLong(items.get(0).getSpuId()), cache);
menu.setStartTime(date);
menu.setHourType(hourType);
menu.setSpuId(combo.spu.getId());
@@ -1212,62 +1217,32 @@ public class CanteenDailyMenuImportHelper {
return true;
}
/**
* 解析/创建单品。
* <p>有「菜品SPU ID」→ 按 ID 复用并按需更新;无 ID → 一律新建,不再按菜名复用(避免多日导入互相覆盖库存)。
*/
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) {
Long excelSpuId, ImportCache cache) {
String clean = cleanDishName(name);
CanteenSpuEntity exist = cache.dishByName.get(clean);
if (exist != null) {
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 (excelSpuId != null) {
CanteenSpuEntity exist = resolveSpuById(excelSpuId, cache);
if (exist == null) {
throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不存在");
}
if (priceFen != null && priceFen > 0 && !Objects.equals(exist.getPrice(), priceFen)) {
exist.setPrice(priceFen);
exist.setMarketPrice(priceFen);
changePrice = true;
if (!merchantId.equals(exist.getMerchantId())) {
throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不属于当前商户");
}
if (stock != null && stock >= 0 && !Objects.equals(exist.getStock(), stock)) {
exist.setStock(stock);
changeStock = true;
}
// 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);
int type = exist.getSpuType() == null ? 1 : exist.getSpuType();
if (type == 2) {
throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 是套餐,单品行请填写单品 SPU ID");
}
applyDishUpdates(exist, categoryName, defaultCategoryId, categoryMap, merchantId, deptId, ingredients,
priceFen, stock);
return new FindOrCreate(exist, false);
}
// 无 SPU ID:始终新建,不按菜名命中已有菜品
Long categoryId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, deptId);
if (categoryId == null) {
throw new IllegalArgumentException(missingCategoryMessage("新建菜品「" + clean + "」", categoryName, categoryMap));
@@ -1280,14 +1255,65 @@ public class CanteenDailyMenuImportHelper {
spu.setStock(stock);
}
canteenSpuService.save(spu);
cache.dishByName.put(clean, spu);
return new FindOrCreate(spu, true);
}
private void applyDishUpdates(CanteenSpuEntity exist, String categoryName, Long defaultCategoryId,
Map<String, Long> categoryMap, Long merchantId, Long deptId, String ingredients, Integer priceFen,
Integer stock) {
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 && !Objects.equals(exist.getPrice(), priceFen)) {
exist.setPrice(priceFen);
exist.setMarketPrice(priceFen);
changePrice = true;
}
if (stock != null && stock >= 0 && !Objects.equals(exist.getStock(), stock)) {
exist.setStock(stock);
changeStock = true;
}
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) {
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);
}
}
/**
* 解析/创建套餐。有套餐 SPU ID 则复用;无 ID 一律新建(不按套餐全名复用)。
*/
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<CanteenSpuEntity> children, String ingredients, Long excelSpuId, ImportCache cache) {
List<CanteenSpuComboItemEntity> items = new ArrayList<>();
int sort = 0;
List<Long> newChildIds = new ArrayList<>(children.size());
@@ -1299,7 +1325,19 @@ public class CanteenDailyMenuImportHelper {
items.add(line);
newChildIds.add(child.getId());
}
if (exist != null) {
if (excelSpuId != null) {
CanteenSpuEntity exist = resolveSpuById(excelSpuId, cache);
if (exist == null) {
throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不存在");
}
if (!merchantId.equals(exist.getMerchantId())) {
throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不属于当前商户");
}
int type = exist.getSpuType() == null ? 1 : exist.getSpuType();
if (type != 2) {
throw new IllegalArgumentException("菜品SPU ID " + excelSpuId + " 不是套餐,套餐行请填写套餐 SPU ID");
}
boolean changePrice = priceFen != null && !Objects.equals(exist.getPrice(), priceFen);
if (changePrice) {
exist.setPrice(priceFen);
@@ -1358,9 +1396,9 @@ public class CanteenDailyMenuImportHelper {
}
canteenSpuService.update(uw);
}
cache.comboByName.put(fullName, exist);
return new FindOrCreate(exist, false);
}
Long categoryId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId, deptId);
if (categoryId == null) {
throw new IllegalArgumentException(missingCategoryMessage("新建套餐「" + fullName + "」", categoryName, categoryMap));
@@ -1375,13 +1413,30 @@ public class CanteenDailyMenuImportHelper {
}
combo.setComboItems(items);
canteenSpuService.saveDeep(combo);
cache.comboByName.put(fullName, combo);
if (combo.getId() != null) {
cache.comboChildIds.put(combo.getId(), new ArrayList<>(newChildIds));
}
return new FindOrCreate(combo, true);
}
private CanteenSpuEntity resolveSpuById(Long spuId, ImportCache cache) {
if (spuId == null) {
return null;
}
CanteenSpuEntity cached = cache.spuById.get(spuId);
if (cached != null) {
return cached;
}
CanteenSpuEntity db = canteenSpuService.getById(spuId);
if (db != null) {
cache.spuById.put(spuId, db);
if (db.getPicUrl() == null) {
db.setPicUrl("");
}
}
return db;
}
private static String menuKey(LocalDate date, Integer hourType, Long spuId) {
return date + "|" + hourType + "|" + spuId;
}
@@ -1675,7 +1730,7 @@ public class CanteenDailyMenuImportHelper {
* <li>多餐:午餐,晚餐 / 午餐、晚餐 / 午餐/晚餐</li>
* <li>快捷:三餐、全天、早中晚 → 早+午+晚;中晚餐 → 午+晚</li>
* </ul>
* 同一道菜多餐时菜品库仍按菜名复用一份 SPU,排菜按餐段各挂一条。
* 无菜品SPU ID 时各餐段各自新建 SPU(库存互不影响);有 SPU ID 则按 ID 复用。
*/
private static List<Integer> parseMeals(String meal) {
if (StrUtil.isBlank(meal)) {
@@ -1887,6 +1942,8 @@ public class CanteenDailyMenuImportHelper {
private static class ImportCache {
final Map<String, CanteenSpuEntity> dishByName = new HashMap<>();
final Map<String, CanteenSpuEntity> comboByName = new HashMap<>();
/** 按 ID 解析 Excel「菜品SPU ID」 */
final Map<Long, CanteenSpuEntity> spuById = new HashMap<>();
final Set<String> existingMenuKeys = new HashSet<>();
/** 套餐组成 childSpuId 有序列表,用于判断是否需要 updateDeep */
final Map<Long, List<Long>> comboChildIds = new HashMap<>();