报餐完善

This commit is contained in:
zjf committed 2026-05-27 13:34:08 +08:00
1 parent c295653ea0
commit dd0da90af3
8 files changed
+218 -15

No files matched your search

@@ -16,4 +16,7 @@ public class OrderingFoodSpuItemDTO {
@NotBlank(message = "菜品名称不能为空")
@Schema(description = "菜品名称")
private String name;
@Schema(description = "餐段序号(与 jobTime 解析的 segmentIndex 一致;不传则兼容旧版全餐段共用")
private Integer segmentIndex;
}
@@ -93,6 +93,7 @@ public class CanteenOnlineSpuEntity extends Model<CanteenOnlineSpuEntity> {
* 择一/组合分组 id,与同组 SPU 共享 {@link CanteenOnlineSpuChoiceGroupEntity} 规则;null 表示不按组校验
*/
@Schema(description = "择一/组合分组 id,同组商品一起参与 minKinds/maxKinds 等校验")
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Long choiceGroupId;
/**
@@ -54,6 +54,12 @@ public class APPOnlineSpuVO {
@Schema(description="商品分类编号")
private Long categoryId;
/**
* 分类餐段类型(来自在线分类 categoryType,逗号分隔:1早餐 2午餐 3晚餐)
*/
@Schema(description = "分类餐段类型")
private String categoryType;
/**
* 商品封面图
*/
@@ -246,9 +246,10 @@ public class CanteenOnlineSpuController {
@RequestParam(value = "maxPrice", required = false) @Parameter(description = "最高价(分)") Integer maxPrice,
@RequestParam(value = "status", required = false) @Parameter(description = "0 上架 1 下架") Integer status,
@RequestParam(value = "stockFilter", required = false) @Parameter(description = "in_stock | sold_out") String stockFilter,
@RequestParam(value = "priceOrder", required = false) @Parameter(description = "asc | desc") String priceOrder) {
@RequestParam(value = "priceOrder", required = false) @Parameter(description = "asc | desc") String priceOrder,
@RequestParam(value = "mealSegmentType", required = false) @Parameter(description = "餐段类型:1早餐 2午餐 3晚餐 4宵夜") Integer mealSegmentType) {
return canteenOnlineSpuService.getCanteenOnlineSpuList(categoryId, merchantId, time,
minPrice, maxPrice, status, stockFilter, priceOrder);
minPrice, maxPrice, status, stockFilter, priceOrder, mealSegmentType);
}
@@ -33,10 +33,11 @@ public interface CanteenOnlineSpuService extends IService<CanteenOnlineSpuEntity
* @param status 上架状态:0 上架 1 下架,可空表示不限
* @param stockFilter 库存:in_stock 仅非售罄,sold_out 仅售罄,可空不限
* @param priceOrder 价格排序:asc / desc,可空则按 sort、id
* @param mealSegmentType 餐段类型:1早餐 2午餐 3晚餐 4宵夜;可空则不过滤
*/
R<List<APPOnlineSpuVO>> getCanteenOnlineSpuList(String categoryId, String merchantId, String time,
Integer minPrice, Integer maxPrice, Integer status,
String stockFilter, String priceOrder);
String stockFilter, String priceOrder, Integer mealSegmentType);
/**
* 为 App 列表/详情填充 choiceGroup 规则对象(与 entities 按序一一对应)
@@ -19,6 +19,7 @@ import com.bsd.pjcloud.canteen.mapper.CanteenOnlineCategoryMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenOnlineSpuMapper;
import com.bsd.pjcloud.canteen.service.CanteenOnlineSpuChoiceGroupService;
import com.bsd.pjcloud.canteen.service.CanteenOnlineSpuService;
import com.bsd.pjcloud.canteen.util.MealSegmentCategoryUtils;
import com.bsd.pjcloud.canteen.vo.app.CanteenOnlineSpuAppVO;
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuChoiceGroupRuleVO;
import com.bsd.pjcloud.canteen.vo.APPOnlineSpuVO;
@@ -65,7 +66,8 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
@Override
public R<List<APPOnlineSpuVO>> getCanteenOnlineSpuList(String categoryId, String merchantId, String time,
Integer minPrice, Integer maxPrice, Integer status,
String stockFilter, String priceOrder) {
String stockFilter, String priceOrder,
Integer mealSegmentType) {
if (minPrice != null && maxPrice != null && minPrice > maxPrice) {
return R.failed("价格区间无效:minPrice 不能大于 maxPrice");
}
@@ -93,6 +95,16 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
canteenOnlineSpuEntityLambdaQueryWrapper.orderByAsc(CanteenOnlineSpuEntity::getSort)
.orderByDesc(CanteenOnlineSpuEntity::getId);
}
if (mealSegmentType != null) {
LambdaQueryWrapper<CanteenOnlineCategoryEntity> categoryWrapper = new LambdaQueryWrapper<>();
categoryWrapper.eq(CanteenOnlineCategoryEntity::getMerchantId, merchantId);
List<CanteenOnlineCategoryEntity> categories = canteenOnlineCategoryMapper.selectList(categoryWrapper);
List<Long> categoryIds = MealSegmentCategoryUtils.filterCategoryIdsByMealSegment(categories, mealSegmentType);
if (CollUtil.isEmpty(categoryIds)) {
return R.ok(Collections.emptyList());
}
canteenOnlineSpuEntityLambdaQueryWrapper.in(CanteenOnlineSpuEntity::getCategoryId, categoryIds);
}
List<CanteenOnlineSpuEntity> canteenSpuEntities = baseMapper.selectList(canteenOnlineSpuEntityLambdaQueryWrapper);
//根据商户查询当前日期的每日菜谱,查询每日菜谱上架的菜的id,然后在从在线订餐的列表筛选出今天有上新的数据
@@ -112,6 +124,7 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
return c.getLinkSpuId() == null || linkSpuIds.contains(c.getLinkSpuId());
}).collect(Collectors.toList());
List<APPOnlineSpuVO> bean = BeanUtils.toBean(finallyCanteenSpuEntities, APPOnlineSpuVO.class);
fillCategoryType(bean, finallyCanteenSpuEntities);
for (int i = 0; i < bean.size(); i++) {
Integer st = finallyCanteenSpuEntities.get(i).getStock();
bean.get(i).setSoldOut(st == null || st <= 0);
@@ -149,6 +162,30 @@ public class CanteenOnlineSpuServiceImpl extends ServiceImpl<CanteenOnlineSpuMap
}
}
private void fillCategoryType(List<APPOnlineSpuVO> vos, List<CanteenOnlineSpuEntity> entities) {
if (CollUtil.isEmpty(vos) || CollUtil.isEmpty(entities)) {
return;
}
Set<Long> categoryIds = entities.stream()
.map(CanteenOnlineSpuEntity::getCategoryId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (CollUtil.isEmpty(categoryIds)) {
return;
}
List<CanteenOnlineCategoryEntity> categories = canteenOnlineCategoryMapper.selectBatchIds(categoryIds);
Map<Long, String> typeMap = categories == null ? Collections.emptyMap() : categories.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CanteenOnlineCategoryEntity::getId,
CanteenOnlineCategoryEntity::getCategoryType, (a, b) -> a));
for (int i = 0; i < vos.size() && i < entities.size(); i++) {
Long cid = entities.get(i).getCategoryId();
if (cid != null) {
vos.get(i).setCategoryType(typeMap.get(cid));
}
}
}
private Map<Long, OnlineSpuChoiceGroupRuleVO> loadChoiceGroupRuleMap(Set<Long> groupIds) {
if (CollUtil.isEmpty(groupIds)) {
return Collections.emptyMap();
@@ -16,10 +16,15 @@ import com.bsd.pjcloud.canteen.dto.ordering.OrderingFoodSelectionDTO;
import com.bsd.pjcloud.canteen.dto.ordering.OrderingFoodSelectionItemDTO;
import com.bsd.pjcloud.canteen.dto.ordering.OrderingFoodSpuItemDTO;
import com.bsd.pjcloud.canteen.dto.ordering.OrderingFoodSubmitDTO;
import com.bsd.pjcloud.canteen.entity.CanteenOnlineCategoryEntity;
import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity;
import com.bsd.pjcloud.canteen.entity.CanteenOrderingFoodEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenOnlineCategoryMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenOnlineSpuMapper;
import com.bsd.pjcloud.canteen.mapper.CanteenOrderingFoodMapper;
import com.bsd.pjcloud.canteen.service.CanteenOrderingFoodService;
import com.bsd.pjcloud.canteen.util.MchJobTimeMealSegmentUtils;
import com.bsd.pjcloud.canteen.util.MealSegmentCategoryUtils;
import com.bsd.pjcloud.canteen.vo.app.OrderingFoodAppVO;
import com.bsd.pjcloud.canteen.vo.app.OrderingFoodMealSegmentVO;
import com.bsd.pjcloud.canteen.vo.app.OrderingFoodStatsDayDetailVO;
@@ -48,6 +53,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -73,6 +79,8 @@ public class CanteenOrderingFoodServiceImpl extends ServiceImpl<CanteenOrderingF
private final RemoteUserService remoteUserService;
private final RemoteDeptService remoteDeptService;
private final ObjectMapper objectMapper;
private final CanteenOnlineSpuMapper canteenOnlineSpuMapper;
private final CanteenOnlineCategoryMapper canteenOnlineCategoryMapper;
@Override
public R<List<OrderingFoodMealSegmentVO>> listMealSegments(String merchantId) {
@@ -120,10 +128,6 @@ public class CanteenOrderingFoodServiceImpl extends ServiceImpl<CanteenOrderingF
if (CollUtil.isEmpty(dto.getSpuItems())) {
return R.failed("请选择报餐菜品");
}
String spuErr = validateSpuItems(dto.getSpuItems());
if (spuErr != null) {
return R.failed(spuErr);
}
if (dto.getPeopleCount() == null || dto.getPeopleCount() < 1) {
return R.failed("请填写报餐人数");
}
@@ -139,6 +143,10 @@ public class CanteenOrderingFoodServiceImpl extends ServiceImpl<CanteenOrderingF
return R.failed(buildR.getMsg());
}
List<OrderingFoodSelectionItemDTO> items = buildR.getData();
String spuErr = validateSpuItems(dto.getSpuItems(), dto.getMerchantId(), jobTime, items);
if (spuErr != null) {
return R.failed(spuErr);
}
String conflict = findSelectionConflict(login.getId(), dto.getMerchantId(), items);
if (conflict != null) {
return R.failed(conflict);
@@ -617,12 +625,12 @@ public class CanteenOrderingFoodServiceImpl extends ServiceImpl<CanteenOrderingF
dayTotalPeople += people;
detail.setPeopleCount(detail.getPeopleCount() + people);
detail.setRecordCount(detail.getRecordCount() + 1);
mergeDishCounts(detail.getDishes(), spuItems, people);
mergeDishCounts(detail.getDishes(), spuItems, people, item.getSegmentIndex());
OrderingFoodStatsRecordBriefVO brief = new OrderingFoodStatsRecordBriefVO();
brief.setId(e.getId());
brief.setUserName(e.getUserName());
brief.setPeopleCount(people);
brief.setSpuItems(spuItems);
brief.setSpuItems(filterSpuItemsForSegment(spuItems, item.getSegmentIndex()));
brief.setRemark(e.getRemark());
detail.getRecords().add(brief);
}
@@ -691,11 +699,12 @@ public class CanteenOrderingFoodServiceImpl extends ServiceImpl<CanteenOrderingF
}
private void mergeDishCounts(List<OrderingFoodStatsDishItemVO> dishes, List<OrderingFoodSpuItemDTO> spuItems,
int people) {
if (CollUtil.isEmpty(spuItems)) {
int people, Integer segmentIndex) {
List<OrderingFoodSpuItemDTO> applicable = filterSpuItemsForSegment(spuItems, segmentIndex);
if (CollUtil.isEmpty(applicable)) {
return;
}
for (OrderingFoodSpuItemDTO spu : spuItems) {
for (OrderingFoodSpuItemDTO spu : applicable) {
if (spu == null || spu.getId() == null) {
continue;
}
@@ -740,7 +749,7 @@ public class CanteenOrderingFoodServiceImpl extends ServiceImpl<CanteenOrderingF
if (item.getDiningDate().isBefore(startDate) || item.getDiningDate().isAfter(endDate)) {
continue;
}
mergeDishCounts(dishes, spuItems, people);
mergeDishCounts(dishes, spuItems, people, item.getSegmentIndex());
}
}
return sortDishItems(dishes);
@@ -939,15 +948,110 @@ public class CanteenOrderingFoodServiceImpl extends ServiceImpl<CanteenOrderingF
return scope != null && Boolean.TRUE.equals(scope.getOrderingFoodEnable());
}
private String validateSpuItems(List<OrderingFoodSpuItemDTO> items) {
private String validateSpuItems(List<OrderingFoodSpuItemDTO> items, String merchantId, String jobTime,
List<OrderingFoodSelectionItemDTO> selections) {
for (OrderingFoodSpuItemDTO item : items) {
if (item == null || item.getId() == null || StrUtil.isBlank(item.getName())) {
return "报餐菜品信息不完整";
}
}
boolean hasSegmentTag = items.stream().anyMatch(i -> i != null && i.getSegmentIndex() != null);
Set<Integer> requiredSegments = selections.stream()
.map(OrderingFoodSelectionItemDTO::getSegmentIndex)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (hasSegmentTag) {
for (Integer segIdx : requiredSegments) {
boolean hasDish = items.stream()
.anyMatch(i -> i != null && Objects.equals(i.getSegmentIndex(), segIdx));
if (!hasDish) {
OrderingFoodMealSegmentVO seg = MchJobTimeMealSegmentUtils.findSegment(jobTime, segIdx);
String segName = seg != null ? seg.getMealSegmentName() : ("餐段" + (segIdx + 1));
return "请为「" + segName + "」选择菜品";
}
}
}
Map<Long, String> categoryTypeMap = loadOnlineSpuCategoryTypeMap(items, merchantId);
for (OrderingFoodSpuItemDTO item : items) {
if (item == null || item.getId() == null) {
continue;
}
String categoryType = categoryTypeMap.get(item.getId());
if (item.getSegmentIndex() != null) {
OrderingFoodMealSegmentVO seg = MchJobTimeMealSegmentUtils.findSegment(jobTime, item.getSegmentIndex());
if (seg == null) {
return "菜品餐段无效";
}
if (!MealSegmentCategoryUtils.matchesMealSegment(categoryType, seg.getMealSegmentType())) {
return "「" + item.getName() + "」不适用于「" + seg.getMealSegmentName() + "」";
}
continue;
}
if (!hasSegmentTag && CollUtil.isNotEmpty(requiredSegments)) {
for (Integer segIdx : requiredSegments) {
OrderingFoodMealSegmentVO seg = MchJobTimeMealSegmentUtils.findSegment(jobTime, segIdx);
if (seg != null && !MealSegmentCategoryUtils.matchesMealSegment(categoryType, seg.getMealSegmentType())) {
return "「" + item.getName() + "」不适用于「" + seg.getMealSegmentName() + "」";
}
}
}
}
return null;
}
private Map<Long, String> loadOnlineSpuCategoryTypeMap(List<OrderingFoodSpuItemDTO> items, String merchantId) {
List<Long> spuIds = items.stream()
.filter(Objects::nonNull)
.map(OrderingFoodSpuItemDTO::getId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (CollUtil.isEmpty(spuIds)) {
return Collections.emptyMap();
}
List<CanteenOnlineSpuEntity> spus = canteenOnlineSpuMapper.selectBatchIds(spuIds);
if (CollUtil.isEmpty(spus)) {
return Collections.emptyMap();
}
Set<Long> categoryIds = spus.stream()
.filter(s -> s.getMerchantId() != null && String.valueOf(s.getMerchantId()).equals(String.valueOf(merchantId)))
.map(CanteenOnlineSpuEntity::getCategoryId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, String> categoryTypeById = Collections.emptyMap();
if (CollUtil.isNotEmpty(categoryIds)) {
List<CanteenOnlineCategoryEntity> categories = canteenOnlineCategoryMapper.selectBatchIds(categoryIds);
categoryTypeById = categories == null ? Collections.emptyMap() : categories.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CanteenOnlineCategoryEntity::getId,
CanteenOnlineCategoryEntity::getCategoryType, (a, b) -> a));
}
Map<Long, String> result = new HashMap<>();
for (CanteenOnlineSpuEntity spu : spus) {
if (spu.getId() == null) {
continue;
}
if (spu.getCategoryId() != null) {
result.put(spu.getId(), categoryTypeById.get(spu.getCategoryId()));
}
}
return result;
}
private List<OrderingFoodSpuItemDTO> filterSpuItemsForSegment(List<OrderingFoodSpuItemDTO> spuItems,
Integer segmentIndex) {
if (CollUtil.isEmpty(spuItems)) {
return List.of();
}
boolean hasSegmentTag = spuItems.stream().anyMatch(s -> s != null && s.getSegmentIndex() != null);
if (!hasSegmentTag) {
return spuItems;
}
return spuItems.stream()
.filter(s -> s != null && Objects.equals(s.getSegmentIndex(), segmentIndex))
.collect(Collectors.toList());
}
private String serializeSpuItems(List<OrderingFoodSpuItemDTO> items) {
if (CollUtil.isEmpty(items)) {
return null;
@@ -0,0 +1,50 @@
package com.bsd.pjcloud.canteen.util;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.canteen.entity.CanteenOnlineCategoryEntity;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 在线分类 {@code categoryType} 与报餐餐段 {@code mealSegmentType} 的匹配工具。
* categoryType 为逗号分隔,如 "1,2" 表示早餐+午餐;mealSegmentType:1早餐 2午餐 3晚餐 4宵夜。
*/
public final class MealSegmentCategoryUtils {
private MealSegmentCategoryUtils() {
}
public static boolean matchesMealSegment(String categoryType, Integer mealSegmentType) {
if (mealSegmentType == null) {
return true;
}
if (StrUtil.isBlank(categoryType)) {
return true;
}
String typeStr = String.valueOf(mealSegmentType);
for (String part : categoryType.split(",")) {
if (typeStr.equals(StrUtil.trim(part))) {
return true;
}
}
return false;
}
public static List<Long> filterCategoryIdsByMealSegment(List<CanteenOnlineCategoryEntity> categories,
Integer mealSegmentType) {
if (categories == null || categories.isEmpty()) {
return Collections.emptyList();
}
if (mealSegmentType == null) {
return categories.stream().map(CanteenOnlineCategoryEntity::getId).collect(Collectors.toList());
}
return categories.stream()
.filter(c -> matchesMealSegment(c.getCategoryType(), mealSegmentType))
.map(CanteenOnlineCategoryEntity::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}