package com.ruoyi.app.mendian; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.system.domain.FoodSpecs; import com.ruoyi.system.domain.FoodSpecsValue; import com.ruoyi.system.service.IFoodSpecsService; import com.ruoyi.system.service.IFoodSpecsValueService; import com.ruoyi.system.service.MerchantStoreAccessService; import com.ruoyi.system.utils.Auth; import com.ruoyi.system.utils.AuthContext; import com.ruoyi.system.utils.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * 商品规格管理 Controller(规格组 + 级联规格值) * 门店级可复用规格模板 * * @author ruoyi * @date 2026-07-15 */ @RestController @RequestMapping("/chanting/foodSpec") public class FoodSpecController extends BaseController { @Autowired private IFoodSpecsService foodSpecsService; @Autowired private IFoodSpecsValueService foodSpecsValueService; @Autowired private MerchantStoreAccessService merchantStoreAccessService; /** * 规格组分页列表(每条带 foodSpecsItems) */ @Anonymous @Auth @GetMapping("/foodSpecPageList") public AjaxResult foodSpecPageList(@RequestHeader String token, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam Long mdId, @RequestParam(required = false) String language) { merchantStoreAccessService.requireStoreAccess( AuthContext.requireUserId(), mdId); Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(FoodSpecs::getMdId, mdId); qw.eq(FoodSpecs::getIsDelete, false); if (language != null && !"".equals(language)) { qw.eq(FoodSpecs::getLanguage, language); } qw.orderByAsc(FoodSpecs::getSort).orderByDesc(FoodSpecs::getId); IPage result = foodSpecsService.page(page, qw); fillItems(result.getRecords(), false); return success(result); } /** * 保存规格组(含级联规格值):id<=0 新增,否则更新 */ @Anonymous @Auth @PostMapping("/saveFoodSpec") @Transactional(rollbackFor = Exception.class) public AjaxResult saveFoodSpec(@RequestHeader String token, @RequestBody List list) { if (list == null || list.isEmpty()) { return error(); } Long userId = AuthContext.requireUserId(); for (FoodSpecs s : list) { if (s.getId() == null || s.getId() <= 0) { merchantStoreAccessService.requireStoreAccess(userId, s.getMdId()); } else { FoodSpecs existing = requireSpecsAccess(userId, s.getId()); s.setMdId(existing.getMdId()); } if (s.getIsDelete() == null) { s.setIsDelete(false); } if (s.getIsOpen() == null) { s.setIsOpen(true); } saveOrUpdateSpecs(s); // 级联规格值(全量替换:先删前端未保留的,再保存/新增) if (s.getObjects() != null) { removeDeletedValues(s); for (FoodSpecsValue v : s.getObjects()) { v.setParentId(s.getId()); if (v.getIsOpen() == null) { v.setIsOpen(true); } saveOrUpdateValue(v); } } } return success(); } private void saveOrUpdateSpecs(FoodSpecs s) { if (s.getId() == null || s.getId() <= 0) { s.setId(null); foodSpecsService.save(s); } else { foodSpecsService.updateById(s); } } private void saveOrUpdateValue(FoodSpecsValue v) { if (v.getId() == null || v.getId() <= 0) { v.setId(null); foodSpecsValueService.save(v); } else { foodSpecsValueService.updateById(v); } } /** * 规格值全量替换:删除该规格组下 DB 有、但前端 objects 未传(id>0 集合外)的规格值(物理删除)。 * 仅对已存在规格组(id>0)生效;新增规格组无历史数据,无需 diff。 */ private void removeDeletedValues(FoodSpecs s) { if (s.getId() == null || s.getId() <= 0) { return; } // 前端保留的规格值 id(id>0 的;id<=0 是新增,不参与 diff) List keepIds = s.getObjects().stream() .map(FoodSpecsValue::getId) .filter(id -> id != null && id > 0) .collect(Collectors.toList()); // DB 中该规格组的全部规格值 id List dbIds = foodSpecsValueService.list( new LambdaQueryWrapper() .eq(FoodSpecsValue::getParentId, s.getId()) .select(FoodSpecsValue::getId) ).stream().map(FoodSpecsValue::getId).collect(Collectors.toList()); // DB 有但前端没传 → 物理删除 List toRemove = dbIds.stream() .filter(id -> !keepIds.contains(id)) .collect(Collectors.toList()); if (!toRemove.isEmpty()) { foodSpecsValueService.removeByIds(toRemove); } } /** * 规格详情(带全部规格值) */ @Anonymous @Auth @GetMapping("/getSpecs") public AjaxResult getSpecs(@RequestHeader String token, @RequestParam Long id) { requireSpecsAccess(AuthContext.requireUserId(), id); FoodSpecs s = foodSpecsService.getById(id); if (s != null) { List list = new ArrayList<>(); list.add(s); fillItems(list, false); } return success(s); } /** * 软删除规格组(is_delete=1) */ @Anonymous @Auth @GetMapping("/deleteFoodSpec") public AjaxResult deleteFoodSpec(@RequestHeader String token, @RequestParam Long id) { requireSpecsAccess(AuthContext.requireUserId(), id); FoodSpecs s = new FoodSpecs(); s.setId(id); s.setIsDelete(true); return toAjax(foodSpecsService.updateById(s)); } /** * 商品编辑页:本门店可用规格(is_open=1 & is_delete=0,仅含 is_open=1 的规格值) */ @Anonymous @Auth @GetMapping("/getAvailableSpecsList") public AjaxResult getAvailableSpecsList(@RequestHeader String token, @RequestParam Long mdId, @RequestParam(required = false) String language) { merchantStoreAccessService.requireStoreAccess( AuthContext.requireUserId(), mdId); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(FoodSpecs::getMdId, mdId); qw.eq(FoodSpecs::getIsOpen, true); qw.eq(FoodSpecs::getIsDelete, false); if (language != null && !"".equals(language)) { qw.eq(FoodSpecs::getLanguage, language); } qw.orderByAsc(FoodSpecs::getSort).orderByDesc(FoodSpecs::getId); List list = foodSpecsService.list(qw); fillItems(list, true); return success(list); } /** * 启停规格组 */ @Anonymous @Auth @GetMapping("/changeOpen") public AjaxResult changeOpen(@RequestHeader String token, @RequestParam Long id, @RequestParam Boolean isOpen) { requireSpecsAccess(AuthContext.requireUserId(), id); FoodSpecs s = new FoodSpecs(); s.setId(id); s.setIsOpen(isOpen); return toAjax(foodSpecsService.updateById(s)); } /** * 启停规格值 */ @Anonymous @Auth @GetMapping("/changeSpecValueOpen") public AjaxResult changeSpecValueOpen(@RequestHeader String token, @RequestParam Long id, @RequestParam Boolean isOpen) { Long userId = AuthContext.requireUserId(); FoodSpecsValue existingValue = foodSpecsValueService.getById(id); if (existingValue == null) { return error(); } requireSpecsAccess(userId, existingValue.getParentId()); FoodSpecsValue v = new FoodSpecsValue(); v.setId(id); v.setIsOpen(isOpen); return toAjax(foodSpecsValueService.updateById(v)); } private FoodSpecs requireSpecsAccess(Long userId, Long specsId) { FoodSpecs specs = foodSpecsService.getById(specsId); if (specs == null) { throw new com.ruoyi.common.exception.ServiceException( com.ruoyi.common.utils.MessageUtils.message("merchant.store.access.denied")); } merchantStoreAccessService.requireStoreAccess(userId, specs.getMdId()); return specs; } /** * 批量填充规格值到规格组(避免 N+1) * * @param onlyOpen true=只取 is_open=1 的规格值(用于商品可用规格) */ private void fillItems(List list, boolean onlyOpen) { if (list == null || list.isEmpty()) { return; } List ids = list.stream().map(FoodSpecs::getId).collect(Collectors.toList()); LambdaQueryWrapper qw = new LambdaQueryWrapper() .in(FoodSpecsValue::getParentId, ids) .orderByAsc(FoodSpecsValue::getId); if (onlyOpen) { qw.eq(FoodSpecsValue::getIsOpen, true); } List values = foodSpecsValueService.list(qw); Map> map = values.stream() .collect(Collectors.groupingBy(FoodSpecsValue::getParentId)); for (FoodSpecs s : list) { s.setObjects(map.getOrDefault(s.getId(), new ArrayList<>())); } } }