From a3b609e70af9fd486ab1396bca14ccab9aa34dab Mon Sep 17 00:00:00 2001 From: msh-agent Date: Sun, 3 May 2026 01:08:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(food-encyclopedia):=20=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E9=A3=9F=E7=89=A9=E7=99=BE=E7=A7=91=E5=88=97=E8=A1=A8/?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=A1=B5=E6=8E=A5=E5=85=A5=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=9B=BE=E7=89=87URL=E5=8F=8C=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FoodEncyclopediaController 及 ToolFoodAdminService,提供 /api/admin/tool/food/* CRUD - ToolFoodAdminServiceImpl 在保存前 clearPrefix 并正则修复历史脏数据中的多层 host 前缀 - 前端 list.vue/edit.vue 修复二次解包导致 listData.list 渲染崩溃 - edit.vue 加载详情时兜底归一化 image 字段,处理 https://host//https://host//crmebimage/... 形式 - content.js 注册 foodManager / foodEdit 路由 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../FoodEncyclopediaController.java | 114 ++++++ .../impl/tool/ToolFoodAdminServiceImpl.java | 134 +++++++ .../service/tool/ToolFoodAdminService.java | 51 +++ msh_single_admin/src/api/foodEncyclopedia.js | 93 +++++ .../src/router/modules/content.js | 19 + .../src/views/content/food/edit.vue | 365 ++++++++++++++++++ .../src/views/content/food/list.vue | 237 ++++++++++++ 7 files changed, 1013 insertions(+) create mode 100644 msh_crmeb_22/crmeb-admin/src/main/java/com/zbkj/admin/controller/FoodEncyclopediaController.java create mode 100644 msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodAdminServiceImpl.java create mode 100644 msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolFoodAdminService.java create mode 100644 msh_single_admin/src/api/foodEncyclopedia.js create mode 100644 msh_single_admin/src/views/content/food/edit.vue create mode 100644 msh_single_admin/src/views/content/food/list.vue diff --git a/msh_crmeb_22/crmeb-admin/src/main/java/com/zbkj/admin/controller/FoodEncyclopediaController.java b/msh_crmeb_22/crmeb-admin/src/main/java/com/zbkj/admin/controller/FoodEncyclopediaController.java new file mode 100644 index 0000000..1522e84 --- /dev/null +++ b/msh_crmeb_22/crmeb-admin/src/main/java/com/zbkj/admin/controller/FoodEncyclopediaController.java @@ -0,0 +1,114 @@ +package com.zbkj.admin.controller; + +import com.zbkj.common.model.tool.V2Food; +import com.zbkj.common.page.CommonPage; +import com.zbkj.common.request.PageParamRequest; +import com.zbkj.common.result.CommonResult; +import com.zbkj.service.service.tool.ToolFoodAdminService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 食物百科管理 后台控制器 + * +---------------------------------------------------------------------- + * | 提供食物百科 CRUD 管理接口,含图片维护 + * +---------------------------------------------------------------------- + */ +@Slf4j +@RestController +@RequestMapping("api/admin/tool/food") +@Api(tags = "食物百科管理") +public class FoodEncyclopediaController { + + @Autowired + private ToolFoodAdminService toolFoodAdminService; + + /** + * 分页列表 + */ + @ApiOperation(value = "食物列表") + @GetMapping("/list") + public CommonResult> getList( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String category, + @RequestParam(required = false) String status, + @Validated PageParamRequest pageParamRequest) { + return CommonResult.success(CommonPage.restPage( + toolFoodAdminService.getAdminList(keyword, category, status, pageParamRequest))); + } + + /** + * 食物详情 + */ + @ApiOperation(value = "食物详情") + @GetMapping("/info/{id}") + public CommonResult info(@PathVariable Long id) { + return CommonResult.success(toolFoodAdminService.getById(id)); + } + + /** + * 新增食物 + */ + @ApiOperation(value = "新增食物") + @PostMapping("/save") + public CommonResult save(@RequestBody @Validated V2Food food) { + if (toolFoodAdminService.create(food)) { + return CommonResult.success(); + } + return CommonResult.failed(); + } + + /** + * 修改食物 + */ + @ApiOperation(value = "修改食物") + @PostMapping("/update/{id}") + public CommonResult update(@PathVariable Long id, + @RequestBody @Validated V2Food food) { + food.setFoodId(id); + if (toolFoodAdminService.updateFood(food)) { + return CommonResult.success(); + } + return CommonResult.failed(); + } + + /** + * 删除食物 + */ + @ApiOperation(value = "删除食物") + @GetMapping("/delete/{id}") + public CommonResult delete(@PathVariable Long id) { + if (toolFoodAdminService.deleteById(id)) { + return CommonResult.success(); + } + return CommonResult.failed(); + } + + /** + * 修改状态(上架/下架) + */ + @ApiOperation(value = "修改上下架状态") + @PostMapping("/changeStatus/{id}") + public CommonResult changeStatus(@PathVariable Long id, + @RequestParam String status) { + if (toolFoodAdminService.changeStatus(id, status)) { + return CommonResult.success(); + } + return CommonResult.failed(); + } + + /** + * 获取分类列表(用于下拉筛选) + */ + @ApiOperation(value = "食物分类列表") + @GetMapping("/categories") + public CommonResult> getCategories() { + return CommonResult.success(toolFoodAdminService.getAllCategories()); + } +} diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodAdminServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodAdminServiceImpl.java new file mode 100644 index 0000000..8622ead --- /dev/null +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodAdminServiceImpl.java @@ -0,0 +1,134 @@ +package com.zbkj.service.service.impl.tool; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageHelper; +import com.zbkj.common.model.tool.V2Food; +import com.zbkj.common.request.PageParamRequest; +import com.zbkj.service.dao.tool.V2FoodDao; +import com.zbkj.service.service.SystemAttachmentService; +import com.zbkj.service.service.tool.ToolFoodAdminService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 食物百科管理后台服务实现类 + * +---------------------------------------------------------------------- + * | 提供后台 CRUD 操作,图片维护等功能 + * +---------------------------------------------------------------------- + */ +@Slf4j +@Service +public class ToolFoodAdminServiceImpl implements ToolFoodAdminService { + + @Resource + private V2FoodDao v2FoodDao; + + @Resource + private SystemAttachmentService systemAttachmentService; + + @Override + public List getAdminList(String keyword, String category, String status, + PageParamRequest pageParamRequest) { + PageHelper.startPage(pageParamRequest.getPage(), pageParamRequest.getLimit()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索(名称或别名) + if (StrUtil.isNotBlank(keyword)) { + wrapper.and(w -> w.like(V2Food::getName, keyword) + .or().like(V2Food::getAlias, keyword)); + } + // 分类筛选 + if (StrUtil.isNotBlank(category)) { + wrapper.eq(V2Food::getCategory, category); + } + // 状态筛选 + if (StrUtil.isNotBlank(status)) { + wrapper.eq(V2Food::getStatus, status); + } + + wrapper.orderByDesc(V2Food::getSortOrder) + .orderByDesc(V2Food::getUpdatedAt); + + return v2FoodDao.selectList(wrapper); + } + + @Override + public V2Food getById(Long id) { + return v2FoodDao.selectById(id); + } + + @Override + public boolean create(V2Food food) { + Date now = new Date(); + food.setImage(normalizeImage(food.getImage())); + food.setCreatedAt(now); + food.setUpdatedAt(now); + if (food.getStatus() == null) { + food.setStatus("active"); + } + if (food.getSortOrder() == null) { + food.setSortOrder(0); + } + if (food.getViewCount() == null) { + food.setViewCount(0); + } + return v2FoodDao.insert(food) > 0; + } + + @Override + public boolean updateFood(V2Food food) { + food.setImage(normalizeImage(food.getImage())); + food.setUpdatedAt(new Date()); + return v2FoodDao.updateById(food) > 0; + } + + /** + * 规范化图片字段: + * 1. 先 clearPrefix 剥掉单层 CDN 前缀 + * 2. 再用正则去掉残留的「http(s)://xxx/」段,修复历史脏数据中的双前缀 + */ + private String normalizeImage(String image) { + if (StrUtil.isBlank(image)) { + return image; + } + String cleaned = systemAttachmentService.clearPrefix(image); + // 处理历史脏数据:形如 "https://.../crmebimage/..." 或 "https://.../https://.../crmebimage/..." + // 去除 "crmebimage/" 之前的所有 "http(s)://...//?" 段 + return cleaned.replaceAll("^(https?://[^/]+/+)+(?=crmebimage/)", ""); + } + + @Override + public boolean deleteById(Long id) { + return v2FoodDao.deleteById(id) > 0; + } + + @Override + public boolean changeStatus(Long id, String status) { + V2Food food = new V2Food(); + food.setFoodId(id); + food.setStatus(status); + food.setUpdatedAt(new Date()); + return v2FoodDao.updateById(food) > 0; + } + + @Override + public List getAllCategories() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(V2Food::getCategory) + .isNotNull(V2Food::getCategory) + .ne(V2Food::getCategory, "") + .groupBy(V2Food::getCategory); + return v2FoodDao.selectList(wrapper).stream() + .map(V2Food::getCategory) + .filter(StrUtil::isNotBlank) + .distinct() + .sorted() + .collect(Collectors.toList()); + } +} diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolFoodAdminService.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolFoodAdminService.java new file mode 100644 index 0000000..a397246 --- /dev/null +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolFoodAdminService.java @@ -0,0 +1,51 @@ +package com.zbkj.service.service.tool; + +import com.zbkj.common.model.tool.V2Food; +import com.zbkj.common.request.PageParamRequest; + +import java.util.List; + +/** + * 食物百科管理后台服务接口 + * +---------------------------------------------------------------------- + * | 提供后台 CRUD 操作,图片维护等功能 + * +---------------------------------------------------------------------- + */ +public interface ToolFoodAdminService { + + /** + * 后台分页列表(支持关键词、分类、状态筛选) + */ + List getAdminList(String keyword, String category, String status, + PageParamRequest pageParamRequest); + + /** + * 根据ID获取食物���情 + */ + V2Food getById(Long id); + + /** + * 新增食物 + */ + boolean create(V2Food food); + + /** + * 修改食物 + */ + boolean updateFood(V2Food food); + + /** + * 删除食物 + */ + boolean deleteById(Long id); + + /** + * 修改状态(上架/下架) + */ + boolean changeStatus(Long id, String status); + + /** + * 获取所有分类列表 + */ + List getAllCategories(); +} diff --git a/msh_single_admin/src/api/foodEncyclopedia.js b/msh_single_admin/src/api/foodEncyclopedia.js new file mode 100644 index 0000000..3558527 --- /dev/null +++ b/msh_single_admin/src/api/foodEncyclopedia.js @@ -0,0 +1,93 @@ +// +---------------------------------------------------------------------- +// | 食物百科管理 API +// +---------------------------------------------------------------------- + +import request from '@/utils/request'; + +/** + * 食物列表 + * @param {Object} params - { keyword, category, status, page, limit } + */ +export function ListFood(params) { + return request({ + url: '/admin/tool/food/list', + method: 'GET', + params: { + keyword: params.keyword, + category: params.category, + status: params.status, + page: params.page, + limit: params.limit, + }, + }); +} + +/** + * 食物详情 + * @param {Number} id - 食物ID + */ +export function InfoFood(id) { + return request({ + url: `/admin/tool/food/info/${id}`, + method: 'GET', + }); +} + +/** + * 新增食物 + * @param {Object} data - 食物数据 + */ +export function AddFood(data) { + return request({ + url: '/admin/tool/food/save', + method: 'POST', + data, + }); +} + +/** + * 修改食物 + * @param {Number} id - 食物ID + * @param {Object} data - 食物数据 + */ +export function UpdateFood(id, data) { + return request({ + url: `/admin/tool/food/update/${id}`, + method: 'POST', + data, + }); +} + +/** + * 删除食物 + * @param {Number} id - 食物ID + */ +export function DeleteFood(id) { + return request({ + url: `/admin/tool/food/delete/${id}`, + method: 'GET', + }); +} + +/** + * 修改上下架状态 + * @param {Number} id - 食物ID + * @param {String} status - active/inactive + */ +export function ChangeStatusFood(id, status) { + return request({ + url: `/admin/tool/food/changeStatus/${id}`, + method: 'POST', + params: { status }, + }); +} + +/** + * 获取食物分类列表 + */ +export function GetFoodCategories() { + return request({ + url: '/admin/tool/food/categories', + method: 'GET', + }); +} diff --git a/msh_single_admin/src/router/modules/content.js b/msh_single_admin/src/router/modules/content.js index f0be6b0..d43eb6f 100755 --- a/msh_single_admin/src/router/modules/content.js +++ b/msh_single_admin/src/router/modules/content.js @@ -48,6 +48,25 @@ const contentRouter = { icon: 'clipboard', }, }, + { + path: 'foodManager', + name: 'foodManager', + component: () => import('@/views/content/food/list'), + meta: { + title: '食物百科', + icon: 'clipboard', + }, + }, + { + path: 'foodEdit/:id?', + name: 'foodEdit', + component: () => import('@/views/content/food/edit'), + meta: { + title: '编辑食物', + noCache: true, + activeMenu: '/content/foodManager', + }, + }, ], }; diff --git a/msh_single_admin/src/views/content/food/edit.vue b/msh_single_admin/src/views/content/food/edit.vue new file mode 100644 index 0000000..dbb9f19 --- /dev/null +++ b/msh_single_admin/src/views/content/food/edit.vue @@ -0,0 +1,365 @@ + + + + + diff --git a/msh_single_admin/src/views/content/food/list.vue b/msh_single_admin/src/views/content/food/list.vue new file mode 100644 index 0000000..c263ef5 --- /dev/null +++ b/msh_single_admin/src/views/content/food/list.vue @@ -0,0 +1,237 @@ + + + + +