每日菜谱导入优化

This commit is contained in:
zjf committed 2026-08-28 19:53:57 +08:00
1 parent 43966c04ec
commit bf3faae01a
2 files changed
+139 -72

No files matched your search

@@ -12,6 +12,7 @@ import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
import com.bsd.pjcloud.canteen.entity.CanteenSpuComboItemEntity;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenCategoryMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenSpuComboItemMapper;
import com.bsd.pjcloud.canteen.service.CanteenDailyMenuService;
import com.bsd.pjcloud.canteen.service.CanteenSpuService;
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
@@ -61,6 +62,7 @@ public class CanteenDailyMenuImportHelper {
private final CanteenDailyMenuService canteenDailyMenuService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
private final CanteenCategoryMapper canteenCategoryMapper;
private final CanteenSpuComboItemMapper canteenSpuComboItemMapper;
public List<CanteenDailyMenuImportExcel> templateRows() {
List<CanteenDailyMenuImportExcel> list = new ArrayList<>();
@@ -317,11 +319,8 @@ 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<>();
Set<Long> excelMenuIds = new HashSet<>();
int line = 1;
for (CanteenDailyMenuImportExcel row : dataRows) {
line++;
@@ -344,12 +343,12 @@ public class CanteenDailyMenuImportHelper {
String combo = StrUtil.trim(row.getComboName());
String comboKey = StrUtil.blankToDefault(combo, "");
Long menuId = parseExcelLong(row.getId());
// 必须带上日期+餐段:仅按菜谱ID 分组时,同名套餐跨天若串了同一 ID,会把两天组成菜并成一组报「重复」
if (menuId != null) {
excelMenuIds.add(menuId);
}
// 带上日期+餐段,避免同 ID 跨天被并成一组
String groupKey;
if (menuId != null) {
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)) {
@@ -360,75 +359,42 @@ 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(formatImportErrors(errors));
}
ImportCache cache = buildImportCache(merchantId, involvedDates);
ImportCache cache = buildImportCache(merchantId, involvedDates, excelMenuIds);
Set<String> clearedDayHour = new LinkedHashSet<>();
List<CanteenDailyMenuEntity> pendingMenus = new ArrayList<>();
int updated = 0;
int createdDish = 0;
int createdCombo = 0;
int scheduled = 0;
int idAsNew = 0;
for (Map.Entry<String, List<CanteenDailyMenuImportExcel>> e : groups.entrySet()) {
List<CanteenDailyMenuImportExcel> items = e.getValue();
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())));
CanteenDailyMenuEntity existMenu = resolveMenu(menuId, cache);
boolean matched = existMenu != null
&& merchantId.equals(existMenu.getMerchantId())
&& (groupDate == null || existMenu.getStartTime() == null
|| groupDate.equals(existMenu.getStartTime()))
&& (groupHour == null || existMenu.getHourType() == null
|| groupHour.equals(existMenu.getHourType()));
if (!matched) {
// 空已走新增;ID 不存在 / 商户或日期餐段对不上 → 当新增排菜
idAsNew++;
items.forEach(r -> r.setId(null));
menuId = null;
}
}
if (menuId != null) {
String err = upsertGroupById(menuId, items, merchantId, deptId, categoryId, categoryMap, cache);
if (err != null) {
// 事务内勿 return R.failed:内层已写库/抛错会标 rollback-only,提交会报 UnexpectedRollback
@@ -514,6 +480,9 @@ public class CanteenDailyMenuImportHelper {
String msg = String.format("导入成功:更新 %d,新建单品 %d,新建套餐 %d,新增排菜 %d",
updated, createdDish, createdCombo, scheduled);
if (idAsNew > 0) {
msg += String.format("(其中 %d 组菜谱ID未匹配,已按新增排菜)", idAsNew);
}
return R.ok(null, msg);
}
@@ -567,16 +536,17 @@ public class CanteenDailyMenuImportHelper {
return rows;
}
private ImportCache buildImportCache(Long merchantId, Set<LocalDate> dates) {
private ImportCache buildImportCache(Long merchantId, Set<LocalDate> dates, Set<Long> menuIds) {
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::getDescription, CanteenSpuEntity::getIntroduction,
CanteenSpuEntity::getCategoryId, CanteenSpuEntity::getMerchantId, CanteenSpuEntity::getDeptId,
CanteenSpuEntity::getPicUrl));
List<Long> comboIds = new ArrayList<>();
if (CollUtil.isNotEmpty(spus)) {
for (CanteenSpuEntity spu : spus) {
if (spu == null || StrUtil.isBlank(spu.getName())) {
@@ -588,12 +558,33 @@ public class CanteenDailyMenuImportHelper {
int type = spu.getSpuType() == null ? 1 : spu.getSpuType();
if (type == 2) {
cache.comboByName.putIfAbsent(spu.getName(), spu);
if (spu.getId() != null) {
comboIds.add(spu.getId());
}
}
else {
cache.dishByName.putIfAbsent(spu.getName(), spu);
}
}
}
if (CollUtil.isNotEmpty(comboIds)) {
List<CanteenSpuComboItemEntity> lines = canteenSpuComboItemMapper.selectList(
Wrappers.<CanteenSpuComboItemEntity>lambdaQuery()
.in(CanteenSpuComboItemEntity::getComboSpuId, comboIds)
.orderByAsc(CanteenSpuComboItemEntity::getSortOrder)
.orderByAsc(CanteenSpuComboItemEntity::getId)
.select(CanteenSpuComboItemEntity::getComboSpuId, CanteenSpuComboItemEntity::getChildSpuId,
CanteenSpuComboItemEntity::getSortOrder, CanteenSpuComboItemEntity::getId));
if (CollUtil.isNotEmpty(lines)) {
for (CanteenSpuComboItemEntity line : lines) {
if (line.getComboSpuId() == null || line.getChildSpuId() == null) {
continue;
}
cache.comboChildIds.computeIfAbsent(line.getComboSpuId(), k -> new ArrayList<>())
.add(line.getChildSpuId());
}
}
}
if (CollUtil.isNotEmpty(dates)) {
List<CanteenDailyMenuEntity> menus = canteenDailyMenuService.list(
Wrappers.<CanteenDailyMenuEntity>lambdaQuery()
@@ -609,9 +600,34 @@ public class CanteenDailyMenuImportHelper {
}
}
}
if (CollUtil.isNotEmpty(menuIds)) {
List<CanteenDailyMenuEntity> byIds = canteenDailyMenuService.listByIds(menuIds);
if (CollUtil.isNotEmpty(byIds)) {
for (CanteenDailyMenuEntity m : byIds) {
if (m != null && m.getId() != null) {
cache.menuById.put(m.getId(), m);
}
}
}
}
return cache;
}
private CanteenDailyMenuEntity resolveMenu(Long menuId, ImportCache cache) {
if (menuId == null) {
return null;
}
CanteenDailyMenuEntity cached = cache.menuById.get(menuId);
if (cached != null) {
return cached;
}
CanteenDailyMenuEntity db = canteenDailyMenuService.getById(menuId);
if (db != null) {
cache.menuById.put(menuId, db);
}
return db;
}
private void flushPendingMenus(List<CanteenDailyMenuEntity> pendingMenus) {
if (CollUtil.isEmpty(pendingMenus)) {
return;
@@ -625,7 +641,7 @@ public class CanteenDailyMenuImportHelper {
*/
private String upsertGroupById(Long menuId, List<CanteenDailyMenuImportExcel> items, Long merchantId, Long deptId,
Long defaultCategoryId, Map<String, Long> categoryMap, ImportCache cache) {
CanteenDailyMenuEntity menu = canteenDailyMenuService.getById(menuId);
CanteenDailyMenuEntity menu = resolveMenu(menuId, cache);
if (menu == null) {
return "菜谱ID " + menuId + " 不存在(若由Excel导出再导入,多为ID被当成数字丢了末位精度,请重新导出且保持菜谱ID列为文本)";
}
@@ -828,35 +844,74 @@ public class CanteenDailyMenuImportHelper {
CanteenSpuEntity exist = cache.comboByName.get(fullName);
List<CanteenSpuComboItemEntity> items = new ArrayList<>();
int sort = 0;
List<Long> newChildIds = new ArrayList<>(children.size());
for (CanteenSpuEntity child : children) {
CanteenSpuComboItemEntity line = new CanteenSpuComboItemEntity();
line.setChildSpuId(child.getId());
line.setQuantity(1);
line.setSortOrder(sort++);
items.add(line);
newChildIds.add(child.getId());
}
if (exist != null) {
exist.setPrice(priceFen);
exist.setMarketPrice(priceFen);
if (stock != null && stock >= 0) {
boolean changePrice = priceFen != null && !Objects.equals(exist.getPrice(), priceFen);
if (changePrice) {
exist.setPrice(priceFen);
exist.setMarketPrice(priceFen);
}
boolean changeStock = stock != null && stock >= 0 && !Objects.equals(exist.getStock(), stock);
if (changeStock) {
exist.setStock(stock);
}
if (StrUtil.isNotBlank(pkgLabel)) {
boolean changeIntro = StrUtil.isNotBlank(pkgLabel) && !Objects.equals(exist.getIntroduction(), pkgLabel);
if (changeIntro) {
exist.setIntroduction(pkgLabel);
}
boolean changeCategory = false;
if (StrUtil.isNotBlank(normalizeCategoryName(categoryName))) {
Long newCatId = resolveOrCreateCategoryId(categoryName, defaultCategoryId, categoryMap, merchantId,
deptId);
if (newCatId != null) {
if (newCatId != null && !Objects.equals(exist.getCategoryId(), newCatId)) {
exist.setCategoryId(newCatId);
changeCategory = true;
}
}
String beforeDesc = exist.getDescription();
applyComboIngredients(exist, ingredients);
exist.setComboItems(items);
if (exist.getPicUrl() == null) {
exist.setPicUrl("");
boolean changeDesc = !Objects.equals(beforeDesc, exist.getDescription());
List<Long> oldChildIds = cache.comboChildIds.getOrDefault(exist.getId(), List.of());
boolean itemsChanged = !newChildIds.equals(oldChildIds);
if (itemsChanged) {
exist.setComboItems(items);
if (exist.getDeptId() == null) {
exist.setDeptId(deptId);
}
if (exist.getPicUrl() == null) {
exist.setPicUrl("");
}
canteenSpuService.updateDeep(exist);
cache.comboChildIds.put(exist.getId(), new ArrayList<>(newChildIds));
}
else if (changePrice || changeStock || changeIntro || changeDesc || changeCategory) {
var uw = Wrappers.<CanteenSpuEntity>lambdaUpdate().eq(CanteenSpuEntity::getId, exist.getId());
if (changePrice) {
uw.set(CanteenSpuEntity::getPrice, exist.getPrice());
uw.set(CanteenSpuEntity::getMarketPrice, exist.getMarketPrice());
}
if (changeStock) {
uw.set(CanteenSpuEntity::getStock, exist.getStock());
}
if (changeIntro) {
uw.set(CanteenSpuEntity::getIntroduction, exist.getIntroduction());
}
if (changeDesc) {
uw.set(CanteenSpuEntity::getDescription, exist.getDescription());
}
if (changeCategory) {
uw.set(CanteenSpuEntity::getCategoryId, exist.getCategoryId());
}
canteenSpuService.update(uw);
}
canteenSpuService.updateDeep(exist);
cache.comboByName.put(fullName, exist);
return new FindOrCreate(exist, false);
}
@@ -875,6 +930,9 @@ 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);
}
@@ -1283,5 +1341,9 @@ public class CanteenDailyMenuImportHelper {
final Map<String, CanteenSpuEntity> dishByName = new HashMap<>();
final Map<String, CanteenSpuEntity> comboByName = new HashMap<>();
final Set<String> existingMenuKeys = new HashSet<>();
/** 套餐组成 childSpuId 有序列表,用于判断是否需要 updateDeep */
final Map<Long, List<Long>> comboChildIds = new HashMap<>();
/** Excel 菜谱 ID → 实体,避免每组 getById */
final Map<Long, CanteenDailyMenuEntity> menuById = new HashMap<>();
}
}
@@ -485,7 +485,10 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
if (entity.getPicUrl() == null) {
entity.setPicUrl("");
}
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
// 调用方已带 deptId 时不再 Feign 查商户(导入场景会反复调用,极慢)
if (entity.getDeptId() == null) {
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
}
baseMapper.insert(entity);
saveComboItems(entity.getId(), entity.getSpuType(), entity.getComboItems());
return Boolean.TRUE;
@@ -496,7 +499,9 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
public Boolean updateDeep(CanteenSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, entity.getId());
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
if (entity.getDeptId() == null) {
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
}
baseMapper.updateById(entity);
comboItemMapper.delete(Wrappers.<CanteenSpuComboItemEntity>lambdaQuery()
.eq(CanteenSpuComboItemEntity::getComboSpuId, entity.getId()));