套餐名移到每日菜谱
This commit is contained in:
1 parent
65b35b897b
commit
09da5208ba
11 files changed
+271
-159
No files matched your search
+2
-3
@@ -9,9 +9,8 @@ import java.math.BigDecimal;
|
||||
* 每日菜谱导入/导出 Excel 行(导出含菜谱 ID,导入时可按 ID 更新)
|
||||
* <p>列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、线上库存、线下库存、配料、菜谱ID、菜品SPU ID、子SPU ID、填写说明
|
||||
* <p>线上/线下库存挂在每日菜谱行上,不再写入菜品列表 {@code canteen_spu.stock}。
|
||||
* <p>菜谱ID / 菜品SPU ID / 子SPU ID 必须用文本导出,否则 Excel 对超长雪花 ID 会丢精度。
|
||||
* <p>业务列(日期/餐段/套餐名/套餐价/库存等)导出时每行写全,不合并;套餐块仅「菜谱ID」「菜品SPU ID」可合并。
|
||||
* 「子SPU ID」=每行组成菜 ID,不合并。单品行「菜品SPU ID」=该单品,「子SPU ID」可空。
|
||||
* <p>菜谱ID / 菜品SPU ID / 子SPU ID 必须用文本导出(带制表符前缀),否则 Excel 对超长雪花 ID 会丢精度。
|
||||
* <p>业务列与 ID 列导出时每行写全,均不合并。
|
||||
* <p>无「菜品SPU ID」时一律新建菜品/套餐;无「子SPU ID」时组成菜按菜名+配料回退或新建。
|
||||
*/
|
||||
@Data
|
||||
|
||||
+7
@@ -58,6 +58,10 @@ public class ExcelSnowflakeIdConverter implements Converter<String> {
|
||||
|
||||
private static WriteCellData<?> toTextCell(String value) {
|
||||
String text = value == null ? "" : value.trim();
|
||||
// 前导制表符:Excel/WPS 打开时更不容易自动改成数字
|
||||
if (!text.isEmpty() && !text.startsWith("\t") && !text.startsWith("'")) {
|
||||
text = "\t" + text;
|
||||
}
|
||||
WriteCellData<String> cell = new WriteCellData<>(text);
|
||||
cell.setType(CellDataTypeEnum.STRING);
|
||||
return cell;
|
||||
@@ -82,6 +86,9 @@ public class ExcelSnowflakeIdConverter implements Converter<String> {
|
||||
return null;
|
||||
}
|
||||
String s = raw.trim();
|
||||
while (s.startsWith("\t") || s.startsWith("'") || s.startsWith("\u200B")) {
|
||||
s = s.substring(1).trim();
|
||||
}
|
||||
if (s.endsWith(".0")) {
|
||||
s = s.substring(0, s.length() - 2);
|
||||
}
|
||||
|
||||
-6
@@ -30,12 +30,6 @@ public class ADCanteenSpuListVO {
|
||||
@Schema(description = "菜品英文名称")
|
||||
private String nameEn;
|
||||
|
||||
/**
|
||||
* 套餐名(仅套餐时有值)
|
||||
*/
|
||||
@Schema(description = "套餐名")
|
||||
private String comboName;
|
||||
|
||||
/**
|
||||
* 商品简介
|
||||
*/
|
||||
|
||||
+4
-1
@@ -27,7 +27,10 @@ public class DailyMenuAdminVO {
|
||||
|
||||
private String name;
|
||||
|
||||
/** 套餐标签(canteen_spu.introduction),导出套餐名优先用此字段,避免名称中的 `-` 被截断 */
|
||||
/** 套餐名(canteen_spu.combo_name),导出 Excel「套餐名」列优先 */
|
||||
private String comboName;
|
||||
|
||||
/** 套餐标签兜底(canteen_spu.introduction),combo_name 为空时导出用 */
|
||||
private String introduction;
|
||||
|
||||
private String picUrl;
|
||||
|
||||
+10
@@ -53,6 +53,16 @@ public class DailyMenuVO {
|
||||
*/
|
||||
private Integer spuType;
|
||||
|
||||
/**
|
||||
* 套餐名(canteen_spu.combo_name)
|
||||
*/
|
||||
private String comboName;
|
||||
|
||||
/**
|
||||
* 套餐标签兜底(canteen_spu.introduction)
|
||||
*/
|
||||
private String introduction;
|
||||
|
||||
/**
|
||||
* 套餐组成
|
||||
*/
|
||||
|
||||
+55
-122
@@ -9,15 +9,14 @@ import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
/**
|
||||
* 每日菜谱导入模板 / 导出写后处理。
|
||||
* <p>列:0日期 1餐段 2套餐名 3菜品名 4分类 5单价 6套餐价 7线上库存 8线下库存 9配料
|
||||
* 10菜谱ID 11菜品SPU ID 12子SPU ID 13填写说明
|
||||
* <p>为减少填写混乱:日期/餐段/套餐名/套餐价/库存/说明等<strong>每行写全、不合并</strong>;
|
||||
* 仅套餐块内合并「菜谱ID」「菜品SPU ID」(同一排菜共用)。子SPU ID 每行独立、不合并。
|
||||
* <p>须配合 {@code @ResponseExcel(inMemory = true)},否则 SXSSF 刷盘后日期段导出会在改单元格/合并时失败。
|
||||
* <p>业务列每行写全、不合并。雪花 ID 三列也<strong>每行写全、不合并</strong>
|
||||
* (合并会导致 EasyExcel 读空;Excel 把 19 位 ID 当数字会丢精度并串号)。
|
||||
* <p>须配合 {@code @ResponseExcel(inMemory = true)},否则 SXSSF 刷盘后改单元格会失败。
|
||||
*/
|
||||
public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandler {
|
||||
|
||||
@@ -31,6 +30,9 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
private static final int COL_SPU = 11;
|
||||
private static final int COL_CHILD_SPU = 12;
|
||||
|
||||
/** 写入 ID 前缀,迫使 Excel/WPS 保持文本,避免 19 位雪花被当数字丢精度 */
|
||||
public static final String ID_TEXT_PREFIX = "\t";
|
||||
|
||||
@Override
|
||||
public void afterWorkbookDispose(WriteWorkbookHolder writeWorkbookHolder) {
|
||||
Sheet sheet = writeWorkbookHolder.getWorkbook().getSheetAt(0);
|
||||
@@ -48,49 +50,20 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
center.setAlignment(HorizontalAlignment.CENTER);
|
||||
center.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
|
||||
fillEveryRowAndMergeIdsOnly(sheet, center);
|
||||
forceIdColumnsAsText(sheet, writeWorkbookHolder);
|
||||
CellStyle idTextStyle = writeWorkbookHolder.getWorkbook().createCellStyle();
|
||||
idTextStyle.setDataFormat(writeWorkbookHolder.getWorkbook().createDataFormat().getFormat("@"));
|
||||
idTextStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||
idTextStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
|
||||
fillEveryRowNoMerge(sheet, center, idTextStyle);
|
||||
setColumnWidths(sheet);
|
||||
}
|
||||
|
||||
/** 菜谱ID / 菜品SPU / 子SPU 强制文本格式,降低 Excel 自动改成数字丢精度的概率 */
|
||||
private static void forceIdColumnsAsText(Sheet sheet, WriteWorkbookHolder holder) {
|
||||
CellStyle textStyle = holder.getWorkbook().createCellStyle();
|
||||
textStyle.setDataFormat(holder.getWorkbook().createDataFormat().getFormat("@"));
|
||||
textStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||
textStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int r = 1; r <= lastRow; r++) {
|
||||
for (int col : new int[] { COL_ID, COL_SPU, COL_CHILD_SPU }) {
|
||||
Cell cell = getCell(sheet, r, col);
|
||||
if (cell == null) {
|
||||
continue;
|
||||
}
|
||||
if (cell.getCellType() == CellType.NUMERIC) {
|
||||
double n = cell.getNumericCellValue();
|
||||
String text = (n == Math.rint(n)) ? String.valueOf((long) n) : String.valueOf(n);
|
||||
cell.setCellValue(text);
|
||||
}
|
||||
cell.setCellStyle(textStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将套餐续行空白补全为与首行一致的业务值(便于直接改表),并仅合并菜谱ID / 菜品SPU ID。
|
||||
* 业务列与 ID 列均每行写全;ID 列强制文本格式 + 制表符前缀,禁止经 double 转写。
|
||||
*/
|
||||
private static void fillEveryRowAndMergeIdsOnly(Sheet sheet, CellStyle center) {
|
||||
private static void fillEveryRowNoMerge(Sheet sheet, CellStyle center, CellStyle idTextStyle) {
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
String[] dates = new String[lastRow + 1];
|
||||
String[] meals = new String[lastRow + 1];
|
||||
String[] combos = new String[lastRow + 1];
|
||||
String[] menuIds = new String[lastRow + 1];
|
||||
String[] spuIds = new String[lastRow + 1];
|
||||
String[] comboPrices = new String[lastRow + 1];
|
||||
String[] onlineStocks = new String[lastRow + 1];
|
||||
String[] offlineStocks = new String[lastRow + 1];
|
||||
boolean[] comboGroup = new boolean[lastRow + 1];
|
||||
|
||||
String lastDate = null;
|
||||
String lastMeal = null;
|
||||
String lastCombo = null;
|
||||
@@ -107,6 +80,7 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
String combo = cellText(sheet, r, COL_COMBO);
|
||||
String menuId = cellText(sheet, r, COL_ID);
|
||||
String spuId = cellText(sheet, r, COL_SPU);
|
||||
String childSpuId = cellText(sheet, r, COL_CHILD_SPU);
|
||||
String comboPrice = cellText(sheet, r, COL_COMBO_PRICE);
|
||||
String online = cellText(sheet, r, COL_ONLINE_STOCK);
|
||||
String offline = cellText(sheet, r, COL_OFFLINE_STOCK);
|
||||
@@ -215,85 +189,51 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
}
|
||||
}
|
||||
|
||||
dates[r] = date;
|
||||
meals[r] = meal;
|
||||
combos[r] = combo;
|
||||
menuIds[r] = menuId;
|
||||
spuIds[r] = spuId;
|
||||
comboPrices[r] = comboPrice;
|
||||
onlineStocks[r] = online;
|
||||
offlineStocks[r] = offline;
|
||||
comboGroup[r] = inCombo && !isSingleCombo(combo);
|
||||
}
|
||||
|
||||
// 业务列每行写全,不再合并(避免只改首行/续行读空)
|
||||
for (int r = 1; r <= lastRow; r++) {
|
||||
writeText(sheet, r, COL_DATE, dates[r], center);
|
||||
writeText(sheet, r, COL_MEAL, meals[r], center);
|
||||
writeText(sheet, r, COL_COMBO, combos[r], center);
|
||||
if (comboGroup[r]) {
|
||||
writeText(sheet, r, COL_COMBO_PRICE, comboPrices[r], center);
|
||||
writeText(sheet, r, COL_ONLINE_STOCK, onlineStocks[r], center);
|
||||
writeText(sheet, r, COL_OFFLINE_STOCK, offlineStocks[r], center);
|
||||
writeText(sheet, r, COL_ID, menuIds[r], center);
|
||||
writeText(sheet, r, COL_SPU, spuIds[r], center);
|
||||
}
|
||||
}
|
||||
|
||||
// 仅套餐块合并菜谱ID、菜品SPU ID
|
||||
int runStart = 1;
|
||||
for (int r = 2; r <= lastRow + 1; r++) {
|
||||
boolean same = r <= lastRow
|
||||
&& comboGroup[runStart]
|
||||
&& comboGroup[r]
|
||||
&& eq(dates[r], dates[runStart])
|
||||
&& eq(meals[r], meals[runStart])
|
||||
&& eq(combos[r], combos[runStart])
|
||||
&& eq(menuIds[r], menuIds[runStart]);
|
||||
if (!same) {
|
||||
if (comboGroup[runStart] && runStart <= lastRow) {
|
||||
int endRow = Math.min(r - 1, lastRow);
|
||||
mergeRange(sheet, runStart, endRow, COL_ID, center);
|
||||
mergeRange(sheet, runStart, endRow, COL_SPU, center);
|
||||
}
|
||||
runStart = r;
|
||||
writeText(sheet, r, COL_DATE, date, center);
|
||||
writeText(sheet, r, COL_MEAL, meal, center);
|
||||
writeText(sheet, r, COL_COMBO, combo, center);
|
||||
if (inCombo && !isSingleCombo(combo)) {
|
||||
writeText(sheet, r, COL_COMBO_PRICE, comboPrice, center);
|
||||
writeText(sheet, r, COL_ONLINE_STOCK, online, center);
|
||||
writeText(sheet, r, COL_OFFLINE_STOCK, offline, center);
|
||||
}
|
||||
writeIdText(sheet, r, COL_ID, menuId, idTextStyle);
|
||||
writeIdText(sheet, r, COL_SPU, spuId, idTextStyle);
|
||||
writeIdText(sheet, r, COL_CHILD_SPU, childSpuId, idTextStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeText(Sheet sheet, int rowIdx, int col, String value, CellStyle center) {
|
||||
private static void writeText(Sheet sheet, int rowIdx, int col, String value, CellStyle style) {
|
||||
if (isBlank(value)) {
|
||||
return;
|
||||
}
|
||||
Cell cell = getOrCreateCell(sheet, rowIdx, col);
|
||||
cell.setCellValue(value);
|
||||
cell.setCellStyle(center);
|
||||
cell.setCellStyle(style);
|
||||
}
|
||||
|
||||
private static void mergeRange(Sheet sheet, int firstRow, int lastRow, int col, CellStyle center) {
|
||||
if (firstRow > lastRow) {
|
||||
private static void writeIdText(Sheet sheet, int rowIdx, int col, String value, CellStyle idTextStyle) {
|
||||
Cell cell = getOrCreateCell(sheet, rowIdx, col);
|
||||
if (isBlank(value)) {
|
||||
cell.setBlank();
|
||||
cell.setCellStyle(idTextStyle);
|
||||
return;
|
||||
}
|
||||
if (firstRow == lastRow) {
|
||||
Cell cell = getCell(sheet, firstRow, col);
|
||||
if (cell != null) {
|
||||
cell.setCellStyle(center);
|
||||
}
|
||||
return;
|
||||
String plain = stripIdPrefix(value.trim());
|
||||
cell.setCellValue(ID_TEXT_PREFIX + plain);
|
||||
cell.setCellStyle(idTextStyle);
|
||||
}
|
||||
|
||||
/** 导入读回时去掉导出加的制表符/引号/零宽前缀 */
|
||||
public static String stripIdPrefix(String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
CellRangeAddress region = new CellRangeAddress(firstRow, lastRow, col, col);
|
||||
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
||||
if (sheet.getMergedRegion(i).intersects(region)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
sheet.addMergedRegion(region);
|
||||
for (int r = firstRow; r <= lastRow; r++) {
|
||||
Cell cell = getCell(sheet, r, col);
|
||||
if (cell != null) {
|
||||
cell.setCellStyle(center);
|
||||
}
|
||||
String s = raw.trim();
|
||||
while (s.startsWith("\t") || s.startsWith("'") || s.startsWith("\u200B")) {
|
||||
s = s.substring(1).trim();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static Cell getOrCreateCell(Sheet sheet, int rowIdx, int col) {
|
||||
@@ -313,6 +253,9 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
return row == null ? null : row.getCell(col);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读单元格文本。雪花 ID 绝不能走 {@code (long) double},否则相邻 ID 会串号。
|
||||
*/
|
||||
private static String cellText(Sheet sheet, int rowIdx, int col) {
|
||||
Cell cell = getCell(sheet, rowIdx, col);
|
||||
if (cell == null) {
|
||||
@@ -320,21 +263,18 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
}
|
||||
CellType type = cell.getCellType();
|
||||
if (type == CellType.STRING) {
|
||||
return trimToNull(cell.getStringCellValue());
|
||||
return trimToNull(stripIdPrefix(cell.getStringCellValue()));
|
||||
}
|
||||
if (type == CellType.NUMERIC) {
|
||||
double n = cell.getNumericCellValue();
|
||||
if (n == Math.rint(n)) {
|
||||
return String.valueOf((long) n);
|
||||
}
|
||||
return String.valueOf(n);
|
||||
return new java.math.BigDecimal(String.valueOf(cell.getNumericCellValue())).toPlainString()
|
||||
.replaceAll("\\.0+$", "");
|
||||
}
|
||||
if (type == CellType.BOOLEAN) {
|
||||
return String.valueOf(cell.getBooleanCellValue());
|
||||
}
|
||||
if (type == CellType.FORMULA) {
|
||||
try {
|
||||
return trimToNull(cell.getStringCellValue());
|
||||
return trimToNull(stripIdPrefix(cell.getStringCellValue()));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
@@ -355,13 +295,6 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
return s == null || s.isBlank();
|
||||
}
|
||||
|
||||
private static boolean eq(String a, String b) {
|
||||
if (a == null) {
|
||||
return b == null;
|
||||
}
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
private static boolean isSingleCombo(String combo) {
|
||||
if (isBlank(combo)) {
|
||||
return true;
|
||||
@@ -407,9 +340,9 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
sheet.setColumnWidth(7, 10 * 256);
|
||||
sheet.setColumnWidth(8, 10 * 256);
|
||||
sheet.setColumnWidth(9, 22 * 256);
|
||||
sheet.setColumnWidth(10, 20 * 256);
|
||||
sheet.setColumnWidth(11, 20 * 256);
|
||||
sheet.setColumnWidth(12, 20 * 256);
|
||||
sheet.setColumnWidth(10, 22 * 256);
|
||||
sheet.setColumnWidth(11, 22 * 256);
|
||||
sheet.setColumnWidth(12, 22 * 256);
|
||||
sheet.setColumnWidth(13, 48 * 256);
|
||||
}
|
||||
}
|
||||
+165
-22
@@ -96,13 +96,13 @@ public class CanteenDailyMenuImportHelper {
|
||||
|
||||
public List<CanteenDailyMenuImportExcel> templateRows() {
|
||||
List<CanteenDailyMenuImportExcel> list = new ArrayList<>();
|
||||
// 业务列每行写全(不靠合并续行);仅菜谱ID/菜品SPU ID 在套餐块内可能合并展示
|
||||
// 业务列与雪花 ID 均每行写全(文本),避免 Excel 数字精度串号
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "80", "100",
|
||||
"套餐多行:日期/餐段/套餐名/套餐价/库存每行都填;菜品SPU ID=套餐本体;子SPU ID=本行组成菜"));
|
||||
"套餐多行:日期/餐段/套餐名/套餐价/库存/菜谱ID 每行都填;菜品SPU ID=套餐本体;子SPU ID=本行组成菜;ID列必须为文本"));
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "蒜苔炒腊肉", "荤菜", null, "8", "21", "80", "100", null));
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "清炒时蔬", "素菜", null, "3", "21", "80", "100", null));
|
||||
list.add(row("2026-08-17", "午餐", "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "60", "80",
|
||||
"换套餐名即另一套;无菜谱ID时新建;有菜谱ID/套餐SPU则更新"));
|
||||
"换套餐名即另一套;无菜谱ID时新建;有菜谱ID/套餐SPU则更新;打开Excel后勿把ID列改成数字"));
|
||||
list.add(row("2026-08-17", "午餐", "B套餐", "土豆丝", "素菜", null, "4", "18", "60", "80", null));
|
||||
list.add(row("2026-08-17", "午餐", "无", "千张大骨汤", "汤类", null, "0", null, "50", "50",
|
||||
"单品:套餐名填「无」;菜品SPU ID=本行单品;子SPU ID可空"));
|
||||
@@ -213,7 +213,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
row.setDishName(StrUtil.blankToDefault(item.getName(), ""));
|
||||
row.setDishPriceYuan(fenToYuan(item.getPrice()));
|
||||
if (written == 0) {
|
||||
row.setTip("套餐块:同套餐名多行=一道排菜;仅菜谱ID/菜品SPU可合并展示");
|
||||
row.setTip("套餐块:同套餐名多行=一道排菜;菜谱ID/菜品SPU ID 每行都填(文本),勿改成数字");
|
||||
}
|
||||
rows.add(row);
|
||||
written++;
|
||||
@@ -238,10 +238,13 @@ public class CanteenDailyMenuImportHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出套餐名:优先 introduction(导入时写入的原始套餐标签,可含 -);
|
||||
* 导出套餐名:优先 combo_name(打印/展示用套餐标签),其次 introduction,
|
||||
* 否则尝试按「套餐名-首道菜名」还原,避免把「套餐A-周二午」截成「套餐A」。
|
||||
*/
|
||||
private static String resolveComboLabel(DailyMenuAdminVO menu) {
|
||||
if (StrUtil.isNotBlank(menu.getComboName())) {
|
||||
return menu.getComboName().trim();
|
||||
}
|
||||
if (StrUtil.isNotBlank(menu.getIntroduction())) {
|
||||
return menu.getIntroduction().trim();
|
||||
}
|
||||
@@ -711,7 +714,9 @@ public class CanteenDailyMenuImportHelper {
|
||||
// 预览日期一律用规范化后的 yyyy-MM-dd,与上方日期 Tab 一致
|
||||
previewRow.setBizDate(dateStr);
|
||||
previewRow.setMealName(StrUtil.trim(item.getMealName()));
|
||||
previewRow.setComboName(StrUtil.trim(item.getComboName()));
|
||||
// 单品在 normalize 后套餐名为空;预览回填「无」,确认导入再 normalize 时不会误继承上一套餐
|
||||
String previewCombo = StrUtil.trim(item.getComboName());
|
||||
previewRow.setComboName(StrUtil.isBlank(previewCombo) ? "无" : previewCombo);
|
||||
previewRow.setDishName(StrUtil.trim(item.getDishName()));
|
||||
previewRow.setCategoryName(StrUtil.trim(item.getCategoryName()));
|
||||
previewRow.setDishPriceYuan(item.getDishPriceYuan());
|
||||
@@ -848,6 +853,20 @@ public class CanteenDailyMenuImportHelper {
|
||||
items.forEach(r -> r.setId(null));
|
||||
menuId = null;
|
||||
}
|
||||
else {
|
||||
// 菜谱ID 与 Excel 套餐/单品意图不一致时纠偏(误继承套餐名或 ID 串号)
|
||||
Long healed = healMenuIdTypeMismatch(menuId, items, merchantId, groupDate, groupHour, cache);
|
||||
if (healed == null) {
|
||||
idAsNew++;
|
||||
items.forEach(r -> r.setId(null));
|
||||
menuId = null;
|
||||
}
|
||||
else if (!Objects.equals(healed, menuId)) {
|
||||
menuId = healed;
|
||||
String healedStr = String.valueOf(healed);
|
||||
items.forEach(r -> r.setId(healedStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
DayImportStat st = dayStats.computeIfAbsent(dateStr, DayImportStat::new);
|
||||
if (menuId != null) {
|
||||
@@ -1116,6 +1135,9 @@ public class CanteenDailyMenuImportHelper {
|
||||
if (s.startsWith(ExcelSnowflakeIdConverter.NUMERIC_CORRUPT_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
while (s.startsWith("\t") || s.startsWith("'") || s.startsWith("\u200B")) {
|
||||
s = s.substring(1).trim();
|
||||
}
|
||||
if (s.endsWith(".0")) {
|
||||
s = s.substring(0, s.length() - 2);
|
||||
}
|
||||
@@ -1476,6 +1498,78 @@ public class CanteenDailyMenuImportHelper {
|
||||
pendingMenus.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜谱ID 指向的排菜类型与 Excel 套餐名意图不一致时纠偏。
|
||||
* @return 可用的菜谱ID;无法纠偏时返回 null(调用方按新增处理)
|
||||
*/
|
||||
private Long healMenuIdTypeMismatch(Long menuId, List<CanteenDailyMenuImportExcel> items, Long merchantId,
|
||||
LocalDate date, Integer hourType, ImportCache cache) {
|
||||
CanteenDailyMenuEntity menu = resolveMenu(menuId, cache);
|
||||
if (menu == null || CollUtil.isEmpty(items)) {
|
||||
return menuId;
|
||||
}
|
||||
CanteenDailyMenuImportExcel first = items.get(0);
|
||||
String comboName = StrUtil.trim(first.getComboName());
|
||||
boolean wantsCombo = StrUtil.isNotBlank(comboName) && !isSingleComboMarker(comboName);
|
||||
CanteenSpuEntity menuSpu = menu.getSpuId() == null ? null : resolveSpuById(menu.getSpuId(), cache);
|
||||
int menuSpuType = menuSpu == null || menuSpu.getSpuType() == null ? 1 : menuSpu.getSpuType();
|
||||
boolean menuIsCombo = menuSpuType == 2;
|
||||
if (wantsCombo == menuIsCombo) {
|
||||
return menuId;
|
||||
}
|
||||
if (wantsCombo) {
|
||||
Long comboSpuId = resolveComboExcelSpuId(items, cache);
|
||||
if (comboSpuId != null && date != null && hourType != null) {
|
||||
CanteenDailyMenuEntity alt = findMenuByDateHourSpu(merchantId, date, hourType, comboSpuId, cache);
|
||||
if (alt != null) {
|
||||
return alt.getId();
|
||||
}
|
||||
}
|
||||
// 找不到正确套餐排菜:清空 ID 走新增,避免把单品排菜改成套餐
|
||||
return null;
|
||||
}
|
||||
// Excel 要单品,菜谱却是套餐:按本行菜品 SPU 重挂
|
||||
Long dishSpuId = parseExcelLong(first.getSpuId());
|
||||
if (dishSpuId != null && date != null && hourType != null) {
|
||||
CanteenSpuEntity dish = resolveSpuById(dishSpuId, cache);
|
||||
int dishType = dish == null || dish.getSpuType() == null ? 1 : dish.getSpuType();
|
||||
if (dishType != 2) {
|
||||
CanteenDailyMenuEntity alt = findMenuByDateHourSpu(merchantId, date, hourType, dishSpuId, cache);
|
||||
if (alt != null) {
|
||||
return alt.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 按商户+日期+餐段+SPU 查找已有排菜 */
|
||||
private CanteenDailyMenuEntity findMenuByDateHourSpu(Long merchantId, LocalDate date, Integer hourType, Long spuId,
|
||||
ImportCache cache) {
|
||||
if (merchantId == null || date == null || hourType == null || spuId == null) {
|
||||
return null;
|
||||
}
|
||||
for (CanteenDailyMenuEntity m : cache.menuById.values()) {
|
||||
if (m != null
|
||||
&& merchantId.equals(m.getMerchantId())
|
||||
&& date.equals(m.getStartTime())
|
||||
&& hourType.equals(m.getHourType())
|
||||
&& spuId.equals(m.getSpuId())) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
CanteenDailyMenuEntity db = canteenDailyMenuService.getOne(Wrappers.<CanteenDailyMenuEntity>lambdaQuery()
|
||||
.eq(CanteenDailyMenuEntity::getMerchantId, merchantId)
|
||||
.eq(CanteenDailyMenuEntity::getStartTime, date)
|
||||
.eq(CanteenDailyMenuEntity::getHourType, hourType)
|
||||
.eq(CanteenDailyMenuEntity::getSpuId, spuId)
|
||||
.last("LIMIT 1"), false);
|
||||
if (db != null && db.getId() != null) {
|
||||
cache.menuById.put(db.getId(), db);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按菜谱 ID 更新一组行(单品一行,套餐多行)
|
||||
*/
|
||||
@@ -1538,6 +1632,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
Integer onlineStock = items.get(0).getOnlineStock();
|
||||
Integer offlineStock = items.get(0).getOfflineStock();
|
||||
Long comboExcelSpuId = resolveComboExcelSpuId(items, cache);
|
||||
CanteenDailyMenuEntity target = menu;
|
||||
if (comboExcelSpuId == null && menu.getSpuId() != null) {
|
||||
CanteenSpuEntity menuSpu = resolveSpuById(menu.getSpuId(), cache);
|
||||
int menuSpuType = menuSpu == null || menuSpu.getSpuType() == null ? 1 : menuSpu.getSpuType();
|
||||
@@ -1545,13 +1640,33 @@ public class CanteenDailyMenuImportHelper {
|
||||
comboExcelSpuId = menu.getSpuId();
|
||||
}
|
||||
else {
|
||||
return "菜谱ID " + menu.getId() + " 当前挂的是单品「"
|
||||
+ StrUtil.blankToDefault(menuSpu == null ? null : menuSpu.getName(), String.valueOf(menu.getSpuId()))
|
||||
+ "」,但 Excel 套餐名为「" + comboName + "」。"
|
||||
+ "常见原因:菜谱ID/SPU在Excel被当成数字丢了精度,套餐与单品串到同一菜谱ID。"
|
||||
+ "请重新导出,并确保「菜谱ID」「菜品SPU ID」列为文本后再导入。";
|
||||
// 与 Excel 套餐意图不一致:尝试按套餐 SPU + 日期餐段重挂到正确排菜
|
||||
Long remappedSpu = null;
|
||||
for (CanteenDailyMenuImportExcel item : items) {
|
||||
Long sid = parseExcelLong(item == null ? null : item.getSpuId());
|
||||
if (sid == null) {
|
||||
continue;
|
||||
}
|
||||
CanteenSpuEntity s = resolveSpuById(sid, cache);
|
||||
if (s != null && s.getSpuType() != null && s.getSpuType() == 2) {
|
||||
remappedSpu = sid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CanteenDailyMenuEntity alt = remappedSpu == null ? null
|
||||
: findMenuByDateHourSpu(merchantId, date, hourType, remappedSpu, cache);
|
||||
if (alt == null) {
|
||||
return "菜谱ID " + menu.getId() + " 当前挂的是单品「"
|
||||
+ StrUtil.blankToDefault(menuSpu == null ? null : menuSpu.getName(),
|
||||
String.valueOf(menu.getSpuId()))
|
||||
+ "」,但 Excel 套餐名为「" + comboName
|
||||
+ "」。请清空本行「菜谱ID」按新增导入,或检查套餐名是否误填到了单品行。";
|
||||
}
|
||||
target = alt;
|
||||
comboExcelSpuId = remappedSpu;
|
||||
}
|
||||
}
|
||||
final CanteenDailyMenuEntity targetMenu = target;
|
||||
Map<String, Long> existChildByKey = existingComboChildKeyMap(comboExcelSpuId, cache);
|
||||
boolean uniformSpu = isUniformSpuId(items);
|
||||
Set<String> seenDishKeys = new LinkedHashSet<>();
|
||||
@@ -1562,7 +1677,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
}
|
||||
String dish = StrUtil.trim(item.getDishName());
|
||||
if (StrUtil.isBlank(dish)) {
|
||||
return "菜谱ID " + menu.getId() + ":套餐组成菜品名不能为空";
|
||||
return "菜谱ID " + targetMenu.getId() + ":套餐组成菜品名不能为空";
|
||||
}
|
||||
String dishKey = dishIdentityKey(dish, item.getIngredients());
|
||||
if (!seenDishKeys.add(dishKey)) {
|
||||
@@ -1577,13 +1692,13 @@ public class CanteenDailyMenuImportHelper {
|
||||
}
|
||||
children = dedupeComboChildren(children);
|
||||
if (children.isEmpty()) {
|
||||
return "菜谱ID " + menu.getId() + ":套餐没有有效组成菜";
|
||||
return "菜谱ID " + targetMenu.getId() + ":套餐没有有效组成菜";
|
||||
}
|
||||
if (comboPrice == null) {
|
||||
CanteenSpuEntity existCombo = cache.comboByName.values().stream()
|
||||
.filter(s -> Objects.equals(s.getId(), menu.getSpuId()))
|
||||
.filter(s -> Objects.equals(s.getId(), targetMenu.getSpuId()))
|
||||
.findFirst()
|
||||
.orElseGet(() -> canteenSpuService.getById(menu.getSpuId()));
|
||||
.orElseGet(() -> canteenSpuService.getById(targetMenu.getSpuId()));
|
||||
comboPrice = existCombo != null && existCombo.getPrice() != null ? existCombo.getPrice() : 0;
|
||||
}
|
||||
String mainName = children.get(0).getName();
|
||||
@@ -1592,12 +1707,12 @@ public class CanteenDailyMenuImportHelper {
|
||||
items.get(0).getCategoryName(), defaultCategoryId, categoryMap, fullComboName,
|
||||
comboName, comboPrice, null, children, resolveComboIngredients(items.get(0)),
|
||||
comboExcelSpuId, cache);
|
||||
menu.setStartTime(date);
|
||||
menu.setHourType(hourType);
|
||||
menu.setSpuId(combo.spu.getId());
|
||||
menu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(onlineStock));
|
||||
menu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(offlineStock));
|
||||
canteenDailyMenuService.updateById(menu);
|
||||
targetMenu.setStartTime(date);
|
||||
targetMenu.setHourType(hourType);
|
||||
targetMenu.setSpuId(combo.spu.getId());
|
||||
targetMenu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(onlineStock));
|
||||
targetMenu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(offlineStock));
|
||||
canteenDailyMenuService.updateById(targetMenu);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1887,6 +2002,10 @@ public class CanteenDailyMenuImportHelper {
|
||||
if (changeIntro) {
|
||||
exist.setIntroduction(pkgLabel);
|
||||
}
|
||||
boolean changeComboName = StrUtil.isNotBlank(pkgLabel) && !Objects.equals(exist.getComboName(), pkgLabel);
|
||||
if (changeComboName) {
|
||||
exist.setComboName(pkgLabel);
|
||||
}
|
||||
boolean changeCategory = false;
|
||||
if (StrUtil.isNotBlank(normalizeCategoryName(categoryName))) {
|
||||
Long newCatId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId,
|
||||
@@ -1912,7 +2031,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
canteenSpuService.updateDeep(exist);
|
||||
cache.comboChildIds.put(exist.getId(), new ArrayList<>(newChildIds));
|
||||
}
|
||||
else if (changePrice || changeStock || changeIntro || changeDesc || changeCategory) {
|
||||
else if (changePrice || changeStock || changeIntro || changeComboName || changeDesc || changeCategory) {
|
||||
var uw = Wrappers.<CanteenSpuEntity>lambdaUpdate().eq(CanteenSpuEntity::getId, exist.getId());
|
||||
if (changePrice) {
|
||||
uw.set(CanteenSpuEntity::getPrice, exist.getPrice());
|
||||
@@ -1924,6 +2043,9 @@ public class CanteenDailyMenuImportHelper {
|
||||
if (changeIntro) {
|
||||
uw.set(CanteenSpuEntity::getIntroduction, exist.getIntroduction());
|
||||
}
|
||||
if (changeComboName) {
|
||||
uw.set(CanteenSpuEntity::getComboName, exist.getComboName());
|
||||
}
|
||||
if (changeDesc) {
|
||||
uw.set(CanteenSpuEntity::getDescription, exist.getDescription());
|
||||
}
|
||||
@@ -1941,6 +2063,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
}
|
||||
CanteenSpuEntity combo = newBaseSpu(merchantId, deptId, categoryId, fullName, 2);
|
||||
combo.setIntroduction(pkgLabel);
|
||||
combo.setComboName(pkgLabel);
|
||||
applyComboIngredients(combo, ingredients);
|
||||
combo.setPrice(priceFen);
|
||||
combo.setMarketPrice(priceFen);
|
||||
@@ -2130,6 +2253,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
* 合并单元格旧模板读入后,非首行日期/餐段/套餐名/分类为空,需向下填充(新导出已每行写全,仍兼容旧表)。
|
||||
* 套餐名填「无」「单品」「单点」或「单品-xxx / 单点-xxx」表示单品,不参与套餐继承。
|
||||
* <p>换日/换餐段时必须清空菜谱ID继承,否则「同名套餐」跨天会被合成一组,组成菜看起来像重复。
|
||||
* <p>确认导入时单品「无」常被清成空串;若本行已有独立菜谱ID,禁止再继承上一套餐名,否则会把套餐名串到单品 ID 上。
|
||||
*/
|
||||
private static void normalizeMergedCells(List<CanteenDailyMenuImportExcel> rows) {
|
||||
String lastDate = null;
|
||||
@@ -2182,7 +2306,26 @@ public class CanteenDailyMenuImportHelper {
|
||||
row.setCategoryName(lastCategory);
|
||||
}
|
||||
String combo = StrUtil.trim(row.getComboName());
|
||||
String thisMenuId = StrUtil.trim(row.getId());
|
||||
if (StrUtil.isBlank(combo)) {
|
||||
/*
|
||||
* 确认导入时单品「无」会变成空串再走一遍本方法;若仍继承上一套餐名,
|
||||
* 会把 C2 等套餐名挂到下一道单品的菜谱ID 上(如 …5895),导入直接失败。
|
||||
* 规则:本行已有独立菜谱ID且与上一套餐不同 → 按单品,切断继承。
|
||||
*/
|
||||
if (StrUtil.isNotBlank(thisMenuId) && (lastMenuId == null || !thisMenuId.equals(lastMenuId))) {
|
||||
row.setComboName(null);
|
||||
lastCombo = null;
|
||||
lastMenuId = thisMenuId;
|
||||
if (StrUtil.isNotBlank(row.getSpuId())) {
|
||||
lastSpuId = StrUtil.trim(row.getSpuId());
|
||||
row.setSpuId(lastSpuId);
|
||||
}
|
||||
else {
|
||||
lastSpuId = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
row.setComboName(lastCombo);
|
||||
combo = lastCombo;
|
||||
}
|
||||
|
||||
+5
@@ -641,11 +641,16 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
|
||||
for (DailyMenuAppVO vo : list) {
|
||||
int type = vo.getSpuType() == null ? 1 : vo.getSpuType();
|
||||
vo.setSpuType(type);
|
||||
// 旧数据仅写了 introduction:App/收银展示套餐名时回落
|
||||
if (type == 2 && StrUtil.isBlank(vo.getComboName()) && StrUtil.isNotBlank(vo.getIntroduction())) {
|
||||
vo.setComboName(vo.getIntroduction().trim());
|
||||
}
|
||||
if (type == 2) {
|
||||
vo.setComboItems(comboMap.getOrDefault(vo.getSpuId(), Collections.emptyList()));
|
||||
}
|
||||
else {
|
||||
vo.setComboItems(Collections.emptyList());
|
||||
vo.setComboName(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -128,7 +128,7 @@ public class CanteenSpuImportHelper {
|
||||
List<OnlineSpuComboItemVO> items = comboMap.getOrDefault(spu.getId(), List.of());
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
CanteenSpuImportExcel row = baseExportRow(spu, categoryNameMap);
|
||||
row.setComboName(spu.getName());
|
||||
row.setComboName(exportComboLabel(spu));
|
||||
row.setComboId(String.valueOf(spu.getId()));
|
||||
row.setDishId(null);
|
||||
row.setName("");
|
||||
@@ -140,7 +140,7 @@ public class CanteenSpuImportHelper {
|
||||
OnlineSpuComboItemVO item = items.get(i);
|
||||
CanteenSpuEntity child = childCache.get(item.getChildSpuId());
|
||||
CanteenSpuImportExcel row = new CanteenSpuImportExcel();
|
||||
row.setComboName(spu.getName());
|
||||
row.setComboName(exportComboLabel(spu));
|
||||
row.setComboId(String.valueOf(spu.getId()));
|
||||
row.setName(item.getName() != null ? item.getName() : (child != null ? child.getName() : null));
|
||||
Long childId = child != null ? child.getId() : item.getChildSpuId();
|
||||
@@ -658,6 +658,8 @@ public class CanteenSpuImportHelper {
|
||||
existCombo.setIsShow(parseIsShow(first.getIsShowName()));
|
||||
}
|
||||
existCombo.setSpuType(2);
|
||||
existCombo.setComboName(comboName);
|
||||
existCombo.setIntroduction(comboName);
|
||||
existCombo.setComboItems(comboItems);
|
||||
if (existCombo.getPicUrl() == null) {
|
||||
existCombo.setPicUrl("");
|
||||
@@ -678,6 +680,7 @@ public class CanteenSpuImportHelper {
|
||||
combo.setStatus(parseStatus(first.getStatusName()));
|
||||
combo.setIsShow(parseIsShow(first.getIsShowName()));
|
||||
combo.setIntroduction(comboName);
|
||||
combo.setComboName(comboName);
|
||||
combo.setComboItems(comboItems);
|
||||
canteenSpuService.saveDeep(combo);
|
||||
createStock(combo);
|
||||
@@ -1123,6 +1126,20 @@ public class CanteenSpuImportHelper {
|
||||
return comboName.trim();
|
||||
}
|
||||
|
||||
/** 导出「套餐名」列:优先 combo_name,其次 introduction,最后商品名 */
|
||||
private static String exportComboLabel(CanteenSpuEntity spu) {
|
||||
if (spu == null) {
|
||||
return "";
|
||||
}
|
||||
if (StrUtil.isNotBlank(spu.getComboName())) {
|
||||
return spu.getComboName().trim();
|
||||
}
|
||||
if (StrUtil.isNotBlank(spu.getIntroduction())) {
|
||||
return spu.getIntroduction().trim();
|
||||
}
|
||||
return StrUtil.blankToDefault(spu.getName(), "");
|
||||
}
|
||||
|
||||
private static boolean isSingleMarker(String combo) {
|
||||
return "无".equals(combo) || "-".equals(combo) || "单品".equals(combo)
|
||||
|| "(无)".equals(combo) || "(无)".equals(combo);
|
||||
|
||||
-2
@@ -409,7 +409,6 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
item.setIntroduction(entity.getIntroduction());
|
||||
item.setDescription(entity.getDescription());
|
||||
item.setNameEn(entity.getNameEn());
|
||||
item.setComboName(entity.getComboName());
|
||||
int type = entity.getSpuType() == null ? 1 : entity.getSpuType();
|
||||
item.setSpuType(type);
|
||||
if (type == 2) {
|
||||
@@ -442,7 +441,6 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
vo.setIntroduction(detail.getIntroduction());
|
||||
vo.setDescription(detail.getDescription());
|
||||
vo.setNameEn(detail.getNameEn());
|
||||
vo.setComboName(detail.getComboName());
|
||||
int type = detail.getSpuType() == null ? 1 : detail.getSpuType();
|
||||
vo.setSpuType(type);
|
||||
if (type == 2) {
|
||||
|
||||
+4
-1
@@ -61,7 +61,9 @@
|
||||
<!-- 数据权限拦截器外层按 create_by / dept_id 过滤,须透传到临时结果集 -->
|
||||
sp.dept_id,
|
||||
t.create_by,
|
||||
IFNULL(sp.spu_type, 1) as spu_type
|
||||
IFNULL(sp.spu_type, 1) as spu_type,
|
||||
sp.combo_name,
|
||||
sp.introduction
|
||||
FROM canteen_daily_menu t
|
||||
LEFT JOIN canteen_spu sp
|
||||
on t.spu_id=sp.id
|
||||
@@ -307,6 +309,7 @@
|
||||
t.merchant_id,
|
||||
sp.id AS spu_id,
|
||||
sp.name,
|
||||
sp.combo_name,
|
||||
sp.introduction,
|
||||
sp.price,
|
||||
sp.market_price,
|
||||
|
||||
Reference in new issue
Block a user