菜谱列表支持套餐

This commit is contained in:
zjf committed 2026-08-04 16:40:02 +08:00
1 parent 7234134a65
commit bb88b7c549
11 files changed
+384 -19

No files matched your search

@@ -0,0 +1,48 @@
package com.bsd.pjcloud.canteen.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("canteen_spu_combo_item")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "菜品库套餐组成明细")
public class CanteenSpuComboItemEntity extends Model<CanteenSpuComboItemEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@Schema(description = "套餐 SPU id")
private Long comboSpuId;
@Schema(description = "组成菜品 SPU id")
private Long childSpuId;
@Schema(description = "份数")
private Integer quantity;
@Schema(description = "排序")
private Integer sortOrder;
private LocalDateTime createTime;
@TableField(exist = false)
@Schema(description = "组成菜品名称")
private String childSpuName;
@TableField(exist = false)
@Schema(description = "组成菜品封面图")
private String childSpuPicUrl;
@TableField(exist = false)
@Schema(description = "组成菜品价格(分)")
private Integer childSpuPrice;
}
@@ -83,6 +83,12 @@ public class CanteenSpuEntity extends Model<CanteenSpuEntity> {
@Schema(description="商品状态: 1 上架(开启) 0 下架(禁用) -1 回收")
private Integer status;
/**
* 商品类型:1单品 2套餐
*/
@Schema(description = "商品类型:1单品 2套餐")
private Integer spuType;
/**
* 规格类型:0 单规格 1 多规格
*/
@@ -204,4 +210,11 @@ public class CanteenSpuEntity extends Model<CanteenSpuEntity> {
@Schema(description="是否显示")
private String isShow;
/**
* 套餐组成(仅 spuType=2 时使用)
*/
@TableField(exist = false)
@Schema(description = "套餐组成")
private java.util.List<CanteenSpuComboItemEntity> comboItems;
}
@@ -1,7 +1,10 @@
package com.bsd.pjcloud.canteen.vo;
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
import lombok.Data;
import java.util.List;
/**
* @author ml
* 菜品下拉框
@@ -44,4 +47,14 @@ public class DailyMenuVO {
* 分类id
*/
private Long categoryId;
/**
* 商品类型:1单品 2套餐
*/
private Integer spuType;
/**
* 套餐组成
*/
private List<OnlineSpuComboItemVO> comboItems;
}
@@ -2,6 +2,8 @@ package com.bsd.pjcloud.canteen.vo.app;
import lombok.Data;
import java.util.List;
@Data
public class DailyMenuAppVO {
@@ -45,4 +47,14 @@ public class DailyMenuAppVO {
* 是否显示
*/
private String isShow;
/**
* 商品类型:1单品 2套餐
*/
private Integer spuType;
/**
* 套餐组成
*/
private List<OnlineSpuComboItemVO> comboItems;
}
@@ -28,5 +28,8 @@ public class CanteenSpuSimpleVO {
@Schema(description="菜品封面图")
private String picUrl;
@Schema(description = "菜品价格(分)")
private Integer price;
}
@@ -128,7 +128,7 @@ public class CanteenSpuController {
@GetMapping("/{id}")
@PreAuthorize("@pms.hasPermission('canteen_canteenSpu_view')")
public R getById(@PathVariable("id") Long id) {
return R.ok(canteenSpuService.getById(id));
return R.ok(canteenSpuService.getDetail(id));
}
/**
@@ -145,17 +145,22 @@ public class CanteenSpuController {
public R save(@RequestBody CanteenSpuEntity canteenSpu) {
Long deptId = SecurityUtils.getUser().getDeptId();
canteenSpu.setDeptId(deptId);
boolean save = canteenSpuService.save(canteenSpu);
CanteenStockEntity canteenStockEntity = new CanteenStockEntity();
canteenStockEntity.setItemId(canteenSpu.getId());
canteenStockEntity.setItemType(StockTypeEnum.DISH.getType());
canteenStockEntity.setBaseStock(0D);
canteenStockEntity.setInventoryUpperLimit(0);
canteenStockEntity.setInventoryLowerLimit(0);
canteenStockEntity.setCurrentStock(0D);
canteenStockEntity.setDeptId(deptId);
canteenStockService.save(canteenStockEntity);
return R.ok(save);
try {
boolean save = canteenSpuService.saveDeep(canteenSpu);
CanteenStockEntity canteenStockEntity = new CanteenStockEntity();
canteenStockEntity.setItemId(canteenSpu.getId());
canteenStockEntity.setItemType(StockTypeEnum.DISH.getType());
canteenStockEntity.setBaseStock(0D);
canteenStockEntity.setInventoryUpperLimit(0);
canteenStockEntity.setInventoryLowerLimit(0);
canteenStockEntity.setCurrentStock(0D);
canteenStockEntity.setDeptId(deptId);
canteenStockService.save(canteenStockEntity);
return R.ok(save);
}
catch (IllegalArgumentException ex) {
return R.failed(ex.getMessage());
}
}
/**
@@ -169,7 +174,12 @@ public class CanteenSpuController {
@PutMapping
@PreAuthorize("@pms.hasPermission('canteen_canteenSpu_edit')")
public R updateById(@RequestBody CanteenSpuEntity canteenSpu) {
return R.ok(canteenSpuService.updateById(canteenSpu));
try {
return R.ok(canteenSpuService.updateDeep(canteenSpu));
}
catch (IllegalArgumentException ex) {
return R.failed(ex.getMessage());
}
}
/**
@@ -197,7 +207,9 @@ public class CanteenSpuController {
@DeleteMapping
@PreAuthorize("@pms.hasPermission('canteen_canteenSpu_del')")
public R removeById(@RequestBody Long[] ids) {
return R.ok(canteenSpuService.removeBatchByIds(CollUtil.toList(ids)));
List<Long> idList = CollUtil.toList(ids);
canteenSpuService.removeComboItemsByComboIds(idList);
return R.ok(canteenSpuService.removeBatchByIds(idList));
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.canteen.mapper;
import com.bsd.pjcloud.canteen.entity.CanteenSpuComboItemEntity;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CanteenSpuComboItemMapper extends PjcloudBaseMapper<CanteenSpuComboItemEntity> {
}
@@ -8,9 +8,11 @@ import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
import com.bsd.pjcloud.common.core.util.R;
import java.util.List;
import java.util.Map;
public interface CanteenSpuService extends IService<CanteenSpuEntity> {
@@ -75,4 +77,29 @@ public interface CanteenSpuService extends IService<CanteenSpuEntity> {
* @return
*/
Object selectPage(Page page, CanteenSpuEntity canteenSpu);
/**
* 详情(含套餐组成)
*/
CanteenSpuEntity getDetail(Long id);
/**
* 新增(含套餐组成)
*/
Boolean saveDeep(CanteenSpuEntity entity);
/**
* 修改(含套餐组成)
*/
Boolean updateDeep(CanteenSpuEntity entity);
/**
* 删除套餐组成
*/
void removeComboItemsByComboIds(List<Long> comboIds);
/**
* 批量加载套餐组成(App/每日菜谱展示)
*/
Map<Long, List<OnlineSpuComboItemVO>> loadComboItemVos(List<Long> comboSpuIds);
}
@@ -56,6 +56,7 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
public List<DailyMenuAll> selectTimeFood(String time, Long merchantId, Long categoryId) {
List<DailyMenuAll> dailyMenuAllES = new ArrayList<>();
List<DailyMenuVO> dailyMenuVOS = baseMapper.selectTimeFood(time, merchantId, categoryId, DataScope.of().only(true));
fillDailyMenuCombo(dailyMenuVOS);
Map<Integer, List<DailyMenuVO>> collect = dailyMenuVOS.stream().collect(Collectors.groupingBy(DailyMenuVO::getHourType));
collect.keySet().forEach(x -> {
DailyMenuAll dailyMenuAll = new DailyMenuAll();
@@ -145,9 +146,56 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
@Override
public Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO) {
Page<DailyMenuAppVO> pageData = baseMapper.selectTimeFoodPage(page, dailyMenuDTO);
fillDailyMenuAppCombo(pageData.getRecords());
return pageData;
}
private void fillDailyMenuCombo(List<DailyMenuVO> list) {
if (CollectionUtils.isEmpty(list)) {
return;
}
List<Long> comboIds = list.stream()
.filter(v -> Objects.equals(v.getSpuType(), 2) && v.getId() != null)
.map(DailyMenuVO::getId)
.distinct()
.collect(Collectors.toList());
Map<Long, List<com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO>> comboMap =
canteenSpuService.loadComboItemVos(comboIds);
for (DailyMenuVO vo : list) {
int type = vo.getSpuType() == null ? 1 : vo.getSpuType();
vo.setSpuType(type);
if (type == 2) {
vo.setComboItems(comboMap.getOrDefault(vo.getId(), Collections.emptyList()));
}
else {
vo.setComboItems(Collections.emptyList());
}
}
}
private void fillDailyMenuAppCombo(List<DailyMenuAppVO> list) {
if (CollectionUtils.isEmpty(list)) {
return;
}
List<Long> comboIds = list.stream()
.filter(v -> Objects.equals(v.getSpuType(), 2) && v.getSpuId() != null)
.map(DailyMenuAppVO::getSpuId)
.distinct()
.collect(Collectors.toList());
Map<Long, List<com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO>> comboMap =
canteenSpuService.loadComboItemVos(comboIds);
for (DailyMenuAppVO vo : list) {
int type = vo.getSpuType() == null ? 1 : vo.getSpuType();
vo.setSpuType(type);
if (type == 2) {
vo.setComboItems(comboMap.getOrDefault(vo.getSpuId(), Collections.emptyList()));
}
else {
vo.setComboItems(Collections.emptyList());
}
}
}
@Override
@Transactional
public R cv(CanteenDailyMenuCVDTO creenDailyMenuCVDTO) {
@@ -1,5 +1,6 @@
package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -10,15 +11,15 @@ import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO;
import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity;
import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
import com.bsd.pjcloud.canteen.entity.CanteenOnlineSpuEntity;
import com.bsd.pjcloud.canteen.entity.CanteenSpuComboItemEntity;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.entity.CanteenStockEntity;
import com.bsd.pjcloud.canteen.mapper.*;
import com.bsd.pjcloud.canteen.service.CanteenDailyMenuService;
import com.bsd.pjcloud.canteen.service.CanteenSpuService;
import com.bsd.pjcloud.canteen.service.CanteenStockService;
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
@@ -28,6 +29,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@@ -46,6 +48,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
private final CanteenCategoryMapper canteenCategoryMapper;
private final CanteenStockMapper canteenStockMapper;
private final CanteenOnlineSpuMapper canteenOnlineSpuMapper;
private final CanteenSpuComboItemMapper comboItemMapper;
@Override
public Object selectFoodList(LambdaQueryWrapper<CanteenSpuEntity> wrapper) {
@@ -141,6 +144,8 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
LambdaQueryWrapper<CanteenSpuEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StrUtil.isNotBlank(canteenSpu.getName()), CanteenSpuEntity::getName, canteenSpu.getName());
wrapper.eq(Objects.nonNull(canteenSpu.getCategoryId()), CanteenSpuEntity::getCategoryId, canteenSpu.getCategoryId());
// 套餐组成只可选单品(spu_type 为空或 1)
wrapper.and(w -> w.isNull(CanteenSpuEntity::getSpuType).or().eq(CanteenSpuEntity::getSpuType, 1));
wrapper.orderByAsc(CanteenSpuEntity::getSort).orderByDesc(CanteenSpuEntity::getId);
Page<CanteenSpuEntity> pageData = baseMapper.selectPageByScope(page, wrapper, DataScope.of());
@@ -240,5 +245,178 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
return pageData;
}
@Override
public CanteenSpuEntity getDetail(Long id) {
CanteenSpuEntity entity = baseMapper.selectById(id);
if (entity == null) {
return null;
}
if (entity.getSpuType() == null) {
entity.setSpuType(1);
}
if (Objects.equals(entity.getSpuType(), 2)) {
entity.setComboItems(loadComboItems(Collections.singletonList(id)).getOrDefault(id, Collections.emptyList()));
}
else {
entity.setComboItems(Collections.emptyList());
}
return entity;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean saveDeep(CanteenSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, null);
baseMapper.insert(entity);
saveComboItems(entity.getId(), entity.getSpuType(), entity.getComboItems());
return Boolean.TRUE;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateDeep(CanteenSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, entity.getId());
baseMapper.updateById(entity);
comboItemMapper.delete(Wrappers.<CanteenSpuComboItemEntity>lambdaQuery()
.eq(CanteenSpuComboItemEntity::getComboSpuId, entity.getId()));
saveComboItems(entity.getId(), entity.getSpuType(), entity.getComboItems());
return Boolean.TRUE;
}
@Override
public void removeComboItemsByComboIds(List<Long> comboIds) {
if (CollUtil.isEmpty(comboIds)) {
return;
}
comboItemMapper.delete(Wrappers.<CanteenSpuComboItemEntity>lambdaQuery()
.in(CanteenSpuComboItemEntity::getComboSpuId, comboIds));
}
@Override
public Map<Long, List<OnlineSpuComboItemVO>> loadComboItemVos(List<Long> comboSpuIds) {
Map<Long, List<CanteenSpuComboItemEntity>> map = loadComboItems(comboSpuIds);
if (map.isEmpty()) {
return Collections.emptyMap();
}
Map<Long, List<OnlineSpuComboItemVO>> result = new LinkedHashMap<>();
for (Map.Entry<Long, List<CanteenSpuComboItemEntity>> e : map.entrySet()) {
result.put(e.getKey(), toComboVos(e.getValue()));
}
return result;
}
private void normalizeSpuType(CanteenSpuEntity entity) {
if (entity.getSpuType() == null || (entity.getSpuType() != 1 && entity.getSpuType() != 2)) {
entity.setSpuType(1);
}
if (!Objects.equals(entity.getSpuType(), 2)) {
entity.setComboItems(Collections.emptyList());
}
}
private void validateCombo(CanteenSpuEntity entity, Long selfId) {
if (!Objects.equals(entity.getSpuType(), 2)) {
return;
}
List<CanteenSpuComboItemEntity> items = entity.getComboItems();
if (CollUtil.isEmpty(items) || items.stream().filter(i -> i.getChildSpuId() != null).count() < 2) {
throw new IllegalArgumentException("套餐至少选择 2 个组成菜品");
}
Set<Long> childIds = new HashSet<>();
for (CanteenSpuComboItemEntity item : items) {
if (item.getChildSpuId() == null) {
continue;
}
if (!childIds.add(item.getChildSpuId())) {
throw new IllegalArgumentException("套餐组成菜品不能重复");
}
if (selfId != null && Objects.equals(item.getChildSpuId(), selfId)) {
throw new IllegalArgumentException("套餐不能包含自身");
}
}
List<CanteenSpuEntity> children = baseMapper.selectBatchIds(childIds);
if (children.size() != childIds.size()) {
throw new IllegalArgumentException("套餐组成菜品不存在或已删除");
}
for (CanteenSpuEntity child : children) {
if (entity.getMerchantId() != null && child.getMerchantId() != null
&& !Objects.equals(child.getMerchantId(), entity.getMerchantId())) {
throw new IllegalArgumentException("套餐组成菜品必须属于同一商户:" + child.getName());
}
Integer childType = child.getSpuType() == null ? 1 : child.getSpuType();
if (Objects.equals(childType, 2)) {
throw new IllegalArgumentException("套餐组成不能再包含套餐:" + child.getName());
}
}
}
private void saveComboItems(Long comboSpuId, Integer spuType, List<CanteenSpuComboItemEntity> items) {
if (!Objects.equals(spuType, 2) || CollUtil.isEmpty(items)) {
return;
}
int sort = 0;
LocalDateTime now = LocalDateTime.now();
for (CanteenSpuComboItemEntity item : items) {
if (item.getChildSpuId() == null) {
continue;
}
CanteenSpuComboItemEntity line = new CanteenSpuComboItemEntity();
line.setComboSpuId(comboSpuId);
line.setChildSpuId(item.getChildSpuId());
line.setQuantity(item.getQuantity() == null || item.getQuantity() < 1 ? 1 : item.getQuantity());
line.setSortOrder(item.getSortOrder() != null ? item.getSortOrder() : sort++);
line.setCreateTime(now);
comboItemMapper.insert(line);
}
}
private Map<Long, List<CanteenSpuComboItemEntity>> loadComboItems(List<Long> comboIds) {
if (CollUtil.isEmpty(comboIds)) {
return Collections.emptyMap();
}
List<CanteenSpuComboItemEntity> lines = comboItemMapper.selectList(
Wrappers.<CanteenSpuComboItemEntity>lambdaQuery()
.in(CanteenSpuComboItemEntity::getComboSpuId, comboIds)
.orderByAsc(CanteenSpuComboItemEntity::getSortOrder)
.orderByAsc(CanteenSpuComboItemEntity::getId));
if (CollUtil.isEmpty(lines)) {
return Collections.emptyMap();
}
Set<Long> childIds = lines.stream().map(CanteenSpuComboItemEntity::getChildSpuId)
.filter(Objects::nonNull).collect(Collectors.toSet());
Map<Long, CanteenSpuEntity> childMap = childIds.isEmpty() ? Collections.emptyMap()
: baseMapper.selectBatchIds(childIds).stream()
.collect(Collectors.toMap(CanteenSpuEntity::getId, c -> c, (a, b) -> a));
for (CanteenSpuComboItemEntity line : lines) {
CanteenSpuEntity child = childMap.get(line.getChildSpuId());
if (child != null) {
line.setChildSpuName(child.getName());
line.setChildSpuPicUrl(child.getPicUrl());
line.setChildSpuPrice(child.getPrice());
}
}
return lines.stream().collect(Collectors.groupingBy(CanteenSpuComboItemEntity::getComboSpuId,
LinkedHashMap::new, Collectors.toList()));
}
private List<OnlineSpuComboItemVO> toComboVos(List<CanteenSpuComboItemEntity> lines) {
if (CollUtil.isEmpty(lines)) {
return Collections.emptyList();
}
List<OnlineSpuComboItemVO> list = new ArrayList<>(lines.size());
for (CanteenSpuComboItemEntity line : lines) {
OnlineSpuComboItemVO vo = new OnlineSpuComboItemVO();
vo.setChildSpuId(line.getChildSpuId());
vo.setName(line.getChildSpuName());
vo.setPicUrl(line.getChildSpuPicUrl());
vo.setQuantity(line.getQuantity());
vo.setSortOrder(line.getSortOrder());
vo.setPrice(line.getChildSpuPrice());
list.add(vo);
}
return list;
}
}
@@ -38,7 +38,8 @@
t.hour_type,
sp.price,
sp.category_id,
sp.dept_id
sp.dept_id,
IFNULL(sp.spu_type, 1) as spu_type
FROM canteen_daily_menu t
LEFT JOIN canteen_spu sp
on t.spu_id=sp.id
@@ -96,7 +97,8 @@
s.pic_url,
s.sort,
s.is_show,
c.id as category_id
c.id as category_id,
IFNULL(s.spu_type, 1) as spu_type
FROM `canteen_daily_menu` m
left JOIN canteen_spu s
on m.spu_id = s.id