FoodSpecController.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package com.ruoyi.app.mendian;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.core.metadata.IPage;
  4. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  5. import com.ruoyi.common.annotation.Anonymous;
  6. import com.ruoyi.common.core.controller.BaseController;
  7. import com.ruoyi.common.core.domain.AjaxResult;
  8. import com.ruoyi.system.domain.FoodSpecs;
  9. import com.ruoyi.system.domain.FoodSpecsValue;
  10. import com.ruoyi.system.service.IFoodSpecsService;
  11. import com.ruoyi.system.service.IFoodSpecsValueService;
  12. import com.ruoyi.system.service.MerchantStoreAccessService;
  13. import com.ruoyi.system.utils.Auth;
  14. import com.ruoyi.system.utils.AuthContext;
  15. import com.ruoyi.system.utils.JwtUtil;
  16. import org.springframework.beans.factory.annotation.Autowired;
  17. import org.springframework.transaction.annotation.Transactional;
  18. import org.springframework.web.bind.annotation.*;
  19. import java.util.ArrayList;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.stream.Collectors;
  23. /**
  24. * 商品规格管理 Controller(规格组 + 级联规格值)
  25. * 门店级可复用规格模板
  26. *
  27. * @author ruoyi
  28. * @date 2026-07-15
  29. */
  30. @RestController
  31. @RequestMapping("/chanting/foodSpec")
  32. public class FoodSpecController extends BaseController
  33. {
  34. @Autowired
  35. private IFoodSpecsService foodSpecsService;
  36. @Autowired
  37. private IFoodSpecsValueService foodSpecsValueService;
  38. @Autowired
  39. private MerchantStoreAccessService merchantStoreAccessService;
  40. /**
  41. * 规格组分页列表(每条带 foodSpecsItems)
  42. */
  43. @Anonymous
  44. @Auth
  45. @GetMapping("/foodSpecPageList")
  46. public AjaxResult foodSpecPageList(@RequestHeader String token,
  47. @RequestParam(defaultValue = "1") Integer pageNum,
  48. @RequestParam(defaultValue = "10") Integer pageSize,
  49. @RequestParam Long mdId,
  50. @RequestParam(required = false) String language) {
  51. merchantStoreAccessService.requireStoreAccess(
  52. AuthContext.requireUserId(), mdId);
  53. Page<FoodSpecs> page = new Page<>(pageNum, pageSize);
  54. LambdaQueryWrapper<FoodSpecs> qw = new LambdaQueryWrapper<>();
  55. qw.eq(FoodSpecs::getMdId, mdId);
  56. qw.eq(FoodSpecs::getIsDelete, false);
  57. if (language != null && !"".equals(language)) {
  58. qw.eq(FoodSpecs::getLanguage, language);
  59. }
  60. qw.orderByAsc(FoodSpecs::getSort).orderByDesc(FoodSpecs::getId);
  61. IPage<FoodSpecs> result = foodSpecsService.page(page, qw);
  62. fillItems(result.getRecords(), false);
  63. return success(result);
  64. }
  65. /**
  66. * 保存规格组(含级联规格值):id<=0 新增,否则更新
  67. */
  68. @Anonymous
  69. @Auth
  70. @PostMapping("/saveFoodSpec")
  71. @Transactional(rollbackFor = Exception.class)
  72. public AjaxResult saveFoodSpec(@RequestHeader String token, @RequestBody List<FoodSpecs> list) {
  73. if (list == null || list.isEmpty()) {
  74. return error();
  75. }
  76. Long userId = AuthContext.requireUserId();
  77. for (FoodSpecs s : list) {
  78. if (s.getId() == null || s.getId() <= 0) {
  79. merchantStoreAccessService.requireStoreAccess(userId, s.getMdId());
  80. } else {
  81. FoodSpecs existing = requireSpecsAccess(userId, s.getId());
  82. s.setMdId(existing.getMdId());
  83. }
  84. if (s.getIsDelete() == null) {
  85. s.setIsDelete(false);
  86. }
  87. if (s.getIsOpen() == null) {
  88. s.setIsOpen(true);
  89. }
  90. saveOrUpdateSpecs(s);
  91. // 级联规格值(全量替换:先删前端未保留的,再保存/新增)
  92. if (s.getObjects() != null) {
  93. removeDeletedValues(s);
  94. for (FoodSpecsValue v : s.getObjects()) {
  95. v.setParentId(s.getId());
  96. if (v.getIsOpen() == null) {
  97. v.setIsOpen(true);
  98. }
  99. saveOrUpdateValue(v);
  100. }
  101. }
  102. }
  103. return success();
  104. }
  105. private void saveOrUpdateSpecs(FoodSpecs s) {
  106. if (s.getId() == null || s.getId() <= 0) {
  107. s.setId(null);
  108. foodSpecsService.save(s);
  109. } else {
  110. foodSpecsService.updateById(s);
  111. }
  112. }
  113. private void saveOrUpdateValue(FoodSpecsValue v) {
  114. if (v.getId() == null || v.getId() <= 0) {
  115. v.setId(null);
  116. foodSpecsValueService.save(v);
  117. } else {
  118. foodSpecsValueService.updateById(v);
  119. }
  120. }
  121. /**
  122. * 规格值全量替换:删除该规格组下 DB 有、但前端 objects 未传(id>0 集合外)的规格值(物理删除)。
  123. * 仅对已存在规格组(id>0)生效;新增规格组无历史数据,无需 diff。
  124. */
  125. private void removeDeletedValues(FoodSpecs s) {
  126. if (s.getId() == null || s.getId() <= 0) {
  127. return;
  128. }
  129. // 前端保留的规格值 id(id>0 的;id<=0 是新增,不参与 diff)
  130. List<Long> keepIds = s.getObjects().stream()
  131. .map(FoodSpecsValue::getId)
  132. .filter(id -> id != null && id > 0)
  133. .collect(Collectors.toList());
  134. // DB 中该规格组的全部规格值 id
  135. List<Long> dbIds = foodSpecsValueService.list(
  136. new LambdaQueryWrapper<FoodSpecsValue>()
  137. .eq(FoodSpecsValue::getParentId, s.getId())
  138. .select(FoodSpecsValue::getId)
  139. ).stream().map(FoodSpecsValue::getId).collect(Collectors.toList());
  140. // DB 有但前端没传 → 物理删除
  141. List<Long> toRemove = dbIds.stream()
  142. .filter(id -> !keepIds.contains(id))
  143. .collect(Collectors.toList());
  144. if (!toRemove.isEmpty()) {
  145. foodSpecsValueService.removeByIds(toRemove);
  146. }
  147. }
  148. /**
  149. * 规格详情(带全部规格值)
  150. */
  151. @Anonymous
  152. @Auth
  153. @GetMapping("/getSpecs")
  154. public AjaxResult getSpecs(@RequestHeader String token, @RequestParam Long id) {
  155. requireSpecsAccess(AuthContext.requireUserId(), id);
  156. FoodSpecs s = foodSpecsService.getById(id);
  157. if (s != null) {
  158. List<FoodSpecs> list = new ArrayList<>();
  159. list.add(s);
  160. fillItems(list, false);
  161. }
  162. return success(s);
  163. }
  164. /**
  165. * 软删除规格组(is_delete=1)
  166. */
  167. @Anonymous
  168. @Auth
  169. @GetMapping("/deleteFoodSpec")
  170. public AjaxResult deleteFoodSpec(@RequestHeader String token, @RequestParam Long id) {
  171. requireSpecsAccess(AuthContext.requireUserId(), id);
  172. FoodSpecs s = new FoodSpecs();
  173. s.setId(id);
  174. s.setIsDelete(true);
  175. return toAjax(foodSpecsService.updateById(s));
  176. }
  177. /**
  178. * 商品编辑页:本门店可用规格(is_open=1 & is_delete=0,仅含 is_open=1 的规格值)
  179. */
  180. @Anonymous
  181. @Auth
  182. @GetMapping("/getAvailableSpecsList")
  183. public AjaxResult getAvailableSpecsList(@RequestHeader String token, @RequestParam Long mdId,
  184. @RequestParam(required = false) String language) {
  185. merchantStoreAccessService.requireStoreAccess(
  186. AuthContext.requireUserId(), mdId);
  187. LambdaQueryWrapper<FoodSpecs> qw = new LambdaQueryWrapper<>();
  188. qw.eq(FoodSpecs::getMdId, mdId);
  189. qw.eq(FoodSpecs::getIsOpen, true);
  190. qw.eq(FoodSpecs::getIsDelete, false);
  191. if (language != null && !"".equals(language)) {
  192. qw.eq(FoodSpecs::getLanguage, language);
  193. }
  194. qw.orderByAsc(FoodSpecs::getSort).orderByDesc(FoodSpecs::getId);
  195. List<FoodSpecs> list = foodSpecsService.list(qw);
  196. fillItems(list, true);
  197. return success(list);
  198. }
  199. /**
  200. * 启停规格组
  201. */
  202. @Anonymous
  203. @Auth
  204. @GetMapping("/changeOpen")
  205. public AjaxResult changeOpen(@RequestHeader String token,
  206. @RequestParam Long id, @RequestParam Boolean isOpen) {
  207. requireSpecsAccess(AuthContext.requireUserId(), id);
  208. FoodSpecs s = new FoodSpecs();
  209. s.setId(id);
  210. s.setIsOpen(isOpen);
  211. return toAjax(foodSpecsService.updateById(s));
  212. }
  213. /**
  214. * 启停规格值
  215. */
  216. @Anonymous
  217. @Auth
  218. @GetMapping("/changeSpecValueOpen")
  219. public AjaxResult changeSpecValueOpen(@RequestHeader String token,
  220. @RequestParam Long id, @RequestParam Boolean isOpen) {
  221. Long userId = AuthContext.requireUserId();
  222. FoodSpecsValue existingValue = foodSpecsValueService.getById(id);
  223. if (existingValue == null) {
  224. return error();
  225. }
  226. requireSpecsAccess(userId, existingValue.getParentId());
  227. FoodSpecsValue v = new FoodSpecsValue();
  228. v.setId(id);
  229. v.setIsOpen(isOpen);
  230. return toAjax(foodSpecsValueService.updateById(v));
  231. }
  232. private FoodSpecs requireSpecsAccess(Long userId, Long specsId) {
  233. FoodSpecs specs = foodSpecsService.getById(specsId);
  234. if (specs == null) {
  235. throw new com.ruoyi.common.exception.ServiceException(
  236. com.ruoyi.common.utils.MessageUtils.message("merchant.store.access.denied"));
  237. }
  238. merchantStoreAccessService.requireStoreAccess(userId, specs.getMdId());
  239. return specs;
  240. }
  241. /**
  242. * 批量填充规格值到规格组(避免 N+1)
  243. *
  244. * @param onlyOpen true=只取 is_open=1 的规格值(用于商品可用规格)
  245. */
  246. private void fillItems(List<FoodSpecs> list, boolean onlyOpen) {
  247. if (list == null || list.isEmpty()) {
  248. return;
  249. }
  250. List<Long> ids = list.stream().map(FoodSpecs::getId).collect(Collectors.toList());
  251. LambdaQueryWrapper<FoodSpecsValue> qw = new LambdaQueryWrapper<FoodSpecsValue>()
  252. .in(FoodSpecsValue::getParentId, ids)
  253. .orderByAsc(FoodSpecsValue::getId);
  254. if (onlyOpen) {
  255. qw.eq(FoodSpecsValue::getIsOpen, true);
  256. }
  257. List<FoodSpecsValue> values = foodSpecsValueService.list(qw);
  258. Map<Long, List<FoodSpecsValue>> map = values.stream()
  259. .collect(Collectors.groupingBy(FoodSpecsValue::getParentId));
  260. for (FoodSpecs s : list) {
  261. s.setObjects(map.getOrDefault(s.getId(), new ArrayList<>()));
  262. }
  263. }
  264. }