每日库存

This commit is contained in:
zjf committed 2026-09-11 16:17:08 +08:00
1 parent ba4a564c85
commit 430fd5e9f9
6 files changed
+143 -22

No files matched your search

@@ -10,6 +10,7 @@ import java.math.BigDecimal;
* <p>列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、线上库存、线下库存、配料、菜谱ID、菜品SPU ID、填写说明
* <p>线上/线下库存挂在每日菜谱行上,不再写入菜品列表 {@code canteen_spu.stock}。
* <p>菜谱ID / 菜品SPU ID 必须用文本导出,否则 Excel 对超长雪花 ID 会丢精度导致再导入报「不存在」。
* <p>套餐块「菜品SPU ID」=套餐本体一个 ID(首行合并);组成菜靠菜名。单品行=该单品 ID。
* <p>无「菜品SPU ID」时一律新建菜品(不按菜名复用);有 ID 才按 ID 更新。
*/
@Data
@@ -64,8 +65,8 @@ public class CanteenDailyMenuImportExcel {
private String id;
/**
* 关联菜品 SPU ID(文本)。套餐导出时为本行组成菜 ID,不是套餐本身 ID。
* 导入:有则按 ID 更新该菜;套餐主体仍靠「菜谱ID」关联已有套餐。
* 关联菜品 SPU ID(文本)。套餐块仅填首行=套餐本体 ID(与菜谱ID一样合并展示);组成菜靠菜名,不写子菜 ID。
* 单品行=该单品 ID。导入:有则按 ID 更新;无则新建。
*/
@ExcelProperty(value = "菜品SPU ID", index = 11)
private String spuId;
@@ -45,4 +45,8 @@ public class CanteenSpuComboItemEntity extends Model<CanteenSpuComboItemEntity>
@TableField(exist = false)
@Schema(description = "组成菜品价格(分)")
private Integer childSpuPrice;
@TableField(exist = false)
@Schema(description = "组成菜品配料/详情")
private String childSpuIngredients;
}
@@ -24,4 +24,7 @@ public class OnlineSpuComboItemVO {
@Schema(description = "组成菜品单价(分,仅展示)")
private Integer price;
@Schema(description = "组成菜品配料(纯文本/富文本,导入导出用)")
private String ingredients;
}
@@ -12,7 +12,7 @@ import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
/**
* 每日菜谱导入模板 / 导出:按数据动态合并日期、餐段、套餐名、套餐价、菜谱ID、说明。
* 每日菜谱导入模板 / 导出:按数据动态合并日期、餐段、套餐名、套餐价、菜谱ID、套餐SPU、说明。
* <p>列:0日期 1餐段 2套餐名 3菜品名 4分类 5单价 6套餐价 7线上库存 8线下库存 9配料 10菜谱ID 11SPU ID 12填写说明
*/
public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandler {
@@ -23,8 +23,8 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
private static final int COL_COMBO_PRICE = 6;
private static final int COL_ONLINE_STOCK = 7;
private static final int COL_OFFLINE_STOCK = 8;
private static final int COL_INGREDIENTS = 9;
private static final int COL_ID = 10;
private static final int COL_SPU = 11;
private static final int COL_TIP = 12;
@Override
@@ -49,7 +49,7 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
}
/**
* 向下填充后,按连续相同值合并:日期 / 餐段 / 套餐块(套餐名+套餐价+ID+SPU+说明)
* 向下填充后,按连续相同值合并:日期 / 餐段 / 套餐块(套餐名+套餐价+库存+ID+SPU+说明;配料不合并)
*/
private static void mergeByRuns(Sheet sheet, CellStyle center) {
int lastRow = sheet.getLastRowNum();
@@ -140,9 +140,10 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
mergeRange(sheet, runStart, endRow, COL_COMBO_PRICE, center);
mergeRange(sheet, runStart, endRow, COL_ONLINE_STOCK, center);
mergeRange(sheet, runStart, endRow, COL_OFFLINE_STOCK, center);
mergeRange(sheet, runStart, endRow, COL_INGREDIENTS, center);
// 配料不合并:组成菜各自配料(同名不同配料靠此区分)
mergeRange(sheet, runStart, endRow, COL_ID, center);
// 菜品SPU ID 不合并:套餐每行对应组成菜自己的 ID
// 套餐 SPU ID 与菜谱 ID 一样合并:整块一个套餐本体 ID
mergeRange(sheet, runStart, endRow, COL_SPU, center);
mergeRange(sheet, runStart, endRow, COL_TIP, center);
}
runStart = r;
@@ -95,11 +95,11 @@ public class CanteenDailyMenuImportHelper {
public List<CanteenDailyMenuImportExcel> templateRows() {
List<CanteenDailyMenuImportExcel> list = new ArrayList<>();
list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "80", "100",
"无菜品SPU ID则新建菜品;有则按本行菜品ID更新。套餐价/线上线下库存/配料填首行;套餐每行SPU=组成菜ID"));
"无菜品SPU ID则新建;有则按ID更新。套餐价/库存/套餐SPU填首行;配料按组成菜分行填(同名不同配料会建两道菜)"));
list.add(row(null, null, null, "蒜苔炒腊肉", "荤菜", null, "8", null, null, null, null));
list.add(row(null, null, null, "清炒时蔬", "素菜", null, "3", null, null, null, null));
list.add(row(null, null, "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "60", "80",
"换套餐名即另一套;无菜谱ID时新建套餐;有菜谱ID则更新该排菜套餐组成"));
"换套餐名即另一套;无菜谱ID时新建套餐;有菜谱ID/套餐SPU则更新该套餐"));
list.add(row(null, null, null, "土豆丝", "素菜", null, "4", null, null, null, null));
list.add(row(null, null, "无", "千张大骨汤", "汤类", null, "0", null, "50", "50",
"单品:套餐名填「无」「单品」「单点」或「单品-xxx/单点-xxx」;价格填单价列;库存填本行线上/线下"));
@@ -184,6 +184,7 @@ public class CanteenDailyMenuImportHelper {
List<OnlineSpuComboItemVO> items = menu.getComboItems();
Set<Long> exportedChildIds = new HashSet<>();
int written = 0;
String comboSpuId = menu.getSpuId() == null ? null : String.valueOf(menu.getSpuId());
for (OnlineSpuComboItemVO item : items) {
if (item == null) {
continue;
@@ -195,17 +196,16 @@ public class CanteenDailyMenuImportHelper {
CanteenDailyMenuImportExcel row = new CanteenDailyMenuImportExcel();
if (written == 0) {
row.setId(menu.getId() == null ? null : String.valueOf(menu.getId()));
row.setSpuId(comboSpuId);
row.setBizDate(dateStr);
row.setMealName(meal);
row.setComboName(comboLabel);
row.setPriceYuan(comboPrice);
row.setOnlineStock(menu.getOnlineStock());
row.setOfflineStock(menu.getOfflineStock());
row.setIngredients(toExcelIngredients(menu.getIngredients()));
}
// 每行导出组成菜自己的 SPU ID(不是套餐 SPU);便于再导入按菜更新
row.setSpuId(childId == null ? null : String.valueOf(childId));
// 每行都带分类,改菜名新建子菜时不丢分类
// 套餐只导出一个套餐 SPU;配料列按组成菜各自导出(同名不同配料靠此区分)
row.setIngredients(toExcelIngredients(item.getIngredients()));
row.setCategoryName(menu.getCategoryName());
row.setDishName(StrUtil.blankToDefault(item.getName(), ""));
row.setDishPriceYuan(fenToYuan(item.getPrice()));
@@ -859,6 +859,9 @@ public class CanteenDailyMenuImportHelper {
Integer comboPrice = null;
Integer onlineStock = first.getOnlineStock();
Integer offlineStock = first.getOfflineStock();
Long comboExcelSpuId = resolveComboExcelSpuId(items, cache);
Map<String, Long> existChildByKey = existingComboChildKeyMap(comboExcelSpuId, cache);
boolean uniformSpu = isUniformSpuId(items);
Set<String> seenDishKeys = new LinkedHashSet<>();
for (int i = 0; i < items.size(); i++) {
CanteenDailyMenuImportExcel item = items.get(i);
@@ -868,14 +871,16 @@ public class CanteenDailyMenuImportHelper {
comboPrice = fen;
}
}
String dishKey = cleanDishName(item.getDishName());
if (StrUtil.isBlank(dishKey) || !seenDishKeys.add(dishKey)) {
String dishKey = dishIdentityKey(item.getDishName(), item.getIngredients());
if (StrUtil.isBlank(cleanDishName(item.getDishName())) || !seenDishKeys.add(dishKey)) {
continue;
}
Long childSpuId = resolveComboChildExcelSpuId(item.getSpuId(), comboExcelSpuId, uniformSpu,
existChildByKey.get(dishKey), cache);
FindOrCreate r = findOrCreateDish(merchantId, deptId,
item.getCategoryName(), categoryId, categoryMap,
item.getDishName(), null, resolveDishPriceFen(item), null,
parseExcelLong(item.getSpuId()), cache);
item.getDishName(), item.getIngredients(), resolveDishPriceFen(item), null, childSpuId,
cache);
if (r.created) {
createdDish++;
}
@@ -895,7 +900,7 @@ public class CanteenDailyMenuImportHelper {
FindOrCreate combo = findOrCreateCombo(merchantId, deptId,
first.getCategoryName(), categoryId, categoryMap,
fullComboName, comboName, comboPrice, null, children,
resolveComboIngredients(first), null, cache);
resolveComboIngredients(first), comboExcelSpuId, cache);
if (combo.created) {
createdCombo++;
}
@@ -1378,6 +1383,12 @@ public class CanteenDailyMenuImportHelper {
Integer comboPrice = null;
Integer onlineStock = items.get(0).getOnlineStock();
Integer offlineStock = items.get(0).getOfflineStock();
Long comboExcelSpuId = resolveComboExcelSpuId(items, cache);
if (comboExcelSpuId == null) {
comboExcelSpuId = menu.getSpuId();
}
Map<String, Long> existChildByKey = existingComboChildKeyMap(comboExcelSpuId, cache);
boolean uniformSpu = isUniformSpuId(items);
Set<String> seenDishKeys = new LinkedHashSet<>();
for (int i = 0; i < items.size(); i++) {
CanteenDailyMenuImportExcel item = items.get(i);
@@ -1388,13 +1399,15 @@ public class CanteenDailyMenuImportHelper {
if (StrUtil.isBlank(dish)) {
return "菜谱ID " + menu.getId() + ":套餐组成菜品名不能为空";
}
String dishKey = cleanDishName(dish);
String dishKey = dishIdentityKey(dish, item.getIngredients());
if (!seenDishKeys.add(dishKey)) {
continue;
}
Long childSpuId = resolveComboChildExcelSpuId(item.getSpuId(), comboExcelSpuId, uniformSpu,
existChildByKey.get(dishKey), cache);
FindOrCreate r = findOrCreateDish(merchantId, deptId,
item.getCategoryName(), defaultCategoryId, categoryMap, dish,
null, resolveDishPriceFen(item), null, parseExcelLong(item.getSpuId()), cache);
item.getIngredients(), resolveDishPriceFen(item), null, childSpuId, cache);
children.add(r.spu);
}
children = dedupeComboChildren(children);
@@ -1413,7 +1426,7 @@ public class CanteenDailyMenuImportHelper {
FindOrCreate combo = findOrCreateCombo(merchantId, deptId,
items.get(0).getCategoryName(), defaultCategoryId, categoryMap, fullComboName,
comboName, comboPrice, null, children, resolveComboIngredients(items.get(0)),
menu.getSpuId(), cache);
comboExcelSpuId, cache);
menu.setStartTime(date);
menu.setHourType(hourType);
menu.setSpuId(combo.spu.getId());
@@ -1423,6 +1436,100 @@ public class CanteenDailyMenuImportHelper {
return null;
}
/**
* 套餐块「菜品SPU ID」:仅当确实是套餐类型时才当作套餐本体;
* 旧导出把组成菜 ID 写在各行时(类型为单品)返回 null,避免误当套餐。
*/
private Long resolveComboExcelSpuId(List<CanteenDailyMenuImportExcel> items, ImportCache cache) {
if (CollUtil.isEmpty(items)) {
return null;
}
for (CanteenDailyMenuImportExcel item : items) {
Long id = parseExcelLong(item == null ? null : item.getSpuId());
if (id == null) {
continue;
}
CanteenSpuEntity exist = resolveSpuById(id, cache);
if (exist == null) {
continue;
}
int type = exist.getSpuType() == null ? 1 : exist.getSpuType();
if (type == 2) {
return id;
}
// 整块同一单品 ID(旧导出合并继承)不算套餐本体
return null;
}
return null;
}
private static boolean isUniformSpuId(List<CanteenDailyMenuImportExcel> items) {
String first = null;
boolean any = false;
for (CanteenDailyMenuImportExcel item : items) {
String s = StrUtil.trim(item == null ? null : item.getSpuId());
if (StrUtil.isBlank(s)) {
continue;
}
any = true;
if (first == null) {
first = s;
}
else if (!first.equals(s)) {
return false;
}
}
return any;
}
/**
* 组成菜 SPU:套餐块统一填套餐本体 ID 时不按该 ID 建子菜;
* 可回退到已有套餐组成中「菜名+配料」完全一致的子菜 ID。
*/
private Long resolveComboChildExcelSpuId(String rowSpuId, Long comboSpuId, boolean uniformSpu,
Long existChildByKey, ImportCache cache) {
if (uniformSpu) {
return existChildByKey;
}
Long id = parseExcelLong(rowSpuId);
if (id == null || Objects.equals(id, comboSpuId)) {
return existChildByKey;
}
CanteenSpuEntity exist = resolveSpuById(id, cache);
if (exist != null) {
int type = exist.getSpuType() == null ? 1 : exist.getSpuType();
if (type == 2) {
return existChildByKey;
}
}
return id;
}
/** 已有套餐组成:key=菜名+配料,避免同名不同配料被复用错。 */
private Map<String, Long> existingComboChildKeyMap(Long comboSpuId, ImportCache cache) {
Map<String, Long> map = new HashMap<>();
if (comboSpuId == null || cache == null) {
return map;
}
List<Long> childIds = cache.comboChildIds.get(comboSpuId);
if (CollUtil.isEmpty(childIds)) {
return map;
}
for (Long childId : childIds) {
CanteenSpuEntity child = resolveSpuById(childId, cache);
if (child == null || StrUtil.isBlank(child.getName())) {
continue;
}
map.putIfAbsent(dishIdentityKey(child.getName(), child.getDescription()), childId);
}
return map;
}
/** 组成菜去重/复用键:规范化菜名 + 纯文本配料 */
private static String dishIdentityKey(String name, String ingredients) {
return cleanDishName(name) + "|" + normalizeImportIngredients(ingredients);
}
/**
* @return true 表示新增了一条排菜
*/
@@ -1710,8 +1817,11 @@ public class CanteenDailyMenuImportHelper {
}
}
/**
* 套餐块「配料」列表示组成菜配料,不再写入套餐本体 description,避免把首道菜配料盖到套餐上。
*/
private static String resolveComboIngredients(CanteenDailyMenuImportExcel first) {
return first == null ? "" : normalizeImportIngredients(first.getIngredients());
return "";
}
private Map<String, Long> loadCategoryMap(Long merchantId) {
@@ -830,6 +830,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
line.setChildSpuName(child.getName());
line.setChildSpuPicUrl(child.getPicUrl());
line.setChildSpuPrice(child.getPrice());
line.setChildSpuIngredients(child.getDescription());
}
}
return lines.stream().collect(Collectors.groupingBy(CanteenSpuComboItemEntity::getComboSpuId,
@@ -849,6 +850,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
vo.setQuantity(line.getQuantity());
vo.setSortOrder(line.getSortOrder());
vo.setPrice(line.getChildSpuPrice());
vo.setIngredients(line.getChildSpuIngredients());
list.add(vo);
}
return list;