--- name: 食谱百科食物图AI生成与OSS更新 overview: 对 v2_foods 表中 image 非阿里云 OSS 的记录,根据 name 调用 KieAI 生成图片(压缩至不超过 100KB),上传到 OSS 并回写 v2_foods.image;复用现有 DishImageServiceImpl 的 KieAI + 压缩 + OSS 能力,扩展“食物”场景并增加更新数据库的入口与触发方式。 todos: [] isProject: false --- # 食谱百科食物图 AI 生成并更新 v2_foods.image ## 现状 - **食谱百科前端**:`[msh_single_uniapp/pages/tool/food-encyclopedia.vue](msh_single_uniapp/pages/tool/food-encyclopedia.vue)` 通过 `getFoodList` / `searchFood`(`[api/tool.js](msh_single_uniapp/api/tool.js)`)请求 `tool/food/list`、`tool/food/search`,列表项中的 `image` 直接来自后端。 - **后端数据**:`[ToolFoodServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodServiceImpl.java)` 从 `v2_foods` 查数据,返回的 map 包含 `image`(`[V2Food](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java)` 的 `image` 字段)。当前部分记录的 `image` 为 Figma/非 OSS 等非阿里云地址。 - **可复用能力**:`[DishImageServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/DishImageServiceImpl.java)` 已实现:KieAI 文生图 → 下载图片 → **压缩至 100KB**(`MAX_IMAGE_BYTES = 100*1024`、`compressImageToMaxBytes`)→ 上传 OSS(`recipes/`)→ 写 `V2DishImageCache`。菜品 prompt 为“一道精美的中式菜品照片:{name}…”。 ## 目标 - 对 **v2_foods** 中 **image 非阿里云 OSS** 的记录:根据 **name** 调用 AI 生成图片,生成图 **不超过 100KB**,上传到 OSS 后 **更新 v2_foods.image** 为 OSS 地址。 - 判断“非 OSS”:`image` 为空或 `image` 不包含当前项目使用的 OSS 域名(如配置中的 `alUploadUrl` 或固定包含 `aliyuncs.com`)。 ## 实现方案 ### 1. 判断“是否为 OSS 地址” - 在公共工具或 Service 内封装:若 `image` 为空/blank,视为非 OSS;若不为空,则判断是否包含 OSS 域名(可从 `SystemConfigService.getValueByKey(SysConfigConstants.CONFIG_AL_UPLOAD_URL)` 取域名,或简单用 `image.contains("aliyuncs.com")`)。满足则为 OSS,否则需要补图。 ### 2. 后端:扩展“食物图片”能力并写回 v2_foods **方案 A(推荐):在现有 DishImageServiceImpl 上扩展** - 在 `[DishImageServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/DishImageServiceImpl.java)` 中: - 增加 **食物** 用 prompt 方法,例如:`buildFoodImagePrompt(String foodName)`,内容为“一份新鲜的食物/食材照片:{name},高清静物摄影,白色背景或餐盘,自然光,细节清晰”(与菜品区分开)。 - 增加 **食物** 的 OSS 路径常量,如 `OSS_FOODS_PATH = "foods/"`,上传时使用 `foods/food-{sanitizedName}-{timestamp}.jpg`,与 `recipes/` 区分。 - 新增 **公共流程**:`generateImageAndUploadToOss(String name, String prompt, String ossPathPrefix)`(或拆成:用现有 KieAI + 下载 + `compressImageToMaxBytes(imageBytes, 100*1024)` + 上传,仅参数为 name/prompt/路径前缀),返回 OSS 完整 URL。 - 注入 `V2FoodDao`,新增方法:`ensureFoodImageAndUpdateDb(Long foodId)`: - 根据 foodId 查 `V2Food`; - 若 `image` 已是 OSS(用上述判断),直接返回当前 image; - 否则用 `buildFoodImagePrompt(food.getName())` 调用上述“生成并上传”流程,得到 ossUrl; - 更新 `food.setImage(ossUrl)` 并 `v2FoodDao.updateById(food)`; - 失败时可按现有逻辑降级为占位图并同样写回 DB,或只打日志不更新,视产品要求而定。 - 在 `[DishImageService](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/DishImageService.java)` 接口中声明 `ensureFoodImageAndUpdateDb(Long foodId)`(或返回 String 的 `ensureFoodImageUrl(Long foodId)` 由调用方更新 DB,二选一即可)。 **方案 B:新建 FoodImageService** - 新建 `FoodImageService` + `FoodImageServiceImpl`,内部注入 `DishImageService` 或直接复用 KieAI、Oss、压缩逻辑(若将 DishImageServiceImpl 中下载/压缩/上传抽成 package 内可复用方法,或抽到公共工具类)。 - `FoodImageServiceImpl.ensureFoodImageAndUpdateDb(Long foodId)` 查 v2_foods → 判断 image 非 OSS → 调 KieAI 生图 → 压缩 ≤100KB → 上传 OSS(路径 `foods/`)→ 更新 `v2_foods.image`。 - 与方案 A 二选一即可;方案 A 复用更集中,改动面小。 ### 3. 触发方式(可选其一或组合) - **按需(列表/详情)**:在 `[ToolFoodServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodServiceImpl.java)` 的 `getList`/`search` 返回列表前,对每条记录的 `image` 判断;若非 OSS,可**异步**调用 `ensureFoodImageAndUpdateDb(food.getFoodId())`,本次仍返回原 image,下次请求得到 OSS 地址;或**同步**调用(会拉长接口耗时,需权衡)。详情 `getDetail` 同理,对当前条若 image 非 OSS 可同步/异步补图并更新。 - **批量/管理端**:在 `[ToolController](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java)` 或管理端增加接口,例如 `POST /api/front/tool/food/refresh-images` 或后台 `POST /api/admin/tool/food/refresh-images`:查询 `v2_foods` 中 `image` 为空或非 OSS 的记录,循环调用 `ensureFoodImageAndUpdateDb(foodId)`,可限制单次条数(如 20)并返回处理数量,避免一次性跑全表。 ### 4. 技术细节摘要 - **图片大小**:沿用 `DishImageServiceImpl` 的 `compressImageToMaxBytes(bytes, 100*1024L)`,保证上传前 ≤100KB。 - **OSS 路径**:使用 `foods/food-{sanitizedName}-{timestamp}.jpg`,与菜品 `recipes/dish-...` 区分,便于运维与排查。 - **KieAI 配置**:与现有菜品一致,使用 `KieAIConfig`、`ToolKieAIService`;未配置 Token 时可按现有逻辑降级(占位图或跳过更新)。 - **前端**:无需改;列表/详情接口返回的 `image` 更新为 OSS 后,`[food-encyclopedia.vue](msh_single_uniapp/pages/tool/food-encyclopedia.vue)` 和 `[food-detail.vue](msh_single_uniapp/pages/tool/food-detail.vue)` 会自然展示新图。 ### 5. 可选说明(不阻塞本次需求) - 食谱百科列表跳详情当前为 `id=${item.name}`,而后端 `getFoodDetail` 为 `Long id`;若实际数据以 id 为主,建议前端改为传 `id=${item.id}` 并在详情用 id 请求,避免按 name 查的兼容逻辑。 ## 涉及文件(建议) | 类型 | 路径 | | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | 接口 | `msh_crmeb_22/crmeb-service/.../tool/DishImageService.java` | | 实现 | `msh_crmeb_22/crmeb-service/.../tool/DishImageServiceImpl.java` | | 食物服务/控制器 | `msh_crmeb_22/crmeb-service/.../tool/ToolFoodServiceImpl.java`、`msh_crmeb_22/crmeb-front/.../ToolController.java`(若做按需或批量触发) | ## 流程概览 ```mermaid flowchart LR A[v2_foods 记录] --> B{image 是否 OSS?} B -->|是| C[直接使用] B -->|否| D[buildFoodImagePrompt] D --> E[KieAI 文生图] E --> F[下载图片] F --> G[压缩至 100KB] G --> H[上传 OSS foods/] H --> I[更新 v2_foods.image] I --> C ```