Initial commit: MSH System\n\n- msh_single_uniapp: Vue 2 + UniApp 前端(微信小程序/H5/App/支付宝小程序)\n- msh_crmeb_22: Spring Boot 2.2 后端(C端API/管理端/业务逻辑)\n- models-integration: AI服务集成(Coze/KieAI/腾讯ASR)\n- docs: 产品文档与设计稿

This commit is contained in:
2026-02-28 05:40:21 +08:00
commit 14d29d51c0
2182 changed files with 482509 additions and 0 deletions

12
.cursor/debug.log Normal file
View File

@@ -0,0 +1,12 @@
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/63e7b8e86e0147abafd057a1dd520732f69kuixvgh.jpg"},"timestamp":1771054614504,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/92e75695b6934eef879a8067835bdf130gl7q8lus2.mp3"},"timestamp":1771054622634,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":true,"setToUserSign":1,"parsedBoolean":true},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771054634346_b","message":"enableAIVideo parsed","timestamp":1771054634346}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/dd52bbd508044ac699acf0e1be8ab7dd77ns0z9rf1.jpg"},"timestamp":1771054891217,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/a87b47ddf8fc4172963747c9b8a1e4105kshwyiubg.mp3"},"timestamp":1771054902191,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":false,"setToUserSign":0,"parsedBoolean":false},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771054906349_b","message":"enableAIVideo parsed","timestamp":1771054906349}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/2728953d15ba4e26a7c1a799a4c15b49elt7yjci5x.png"},"timestamp":1771054919713,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/73e142306c5b4f5ea5b93171b8ce54f370chfc8lz6.mp3"},"timestamp":1771054927494,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":true,"setToUserSign":1,"parsedBoolean":true},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771054935503_b","message":"enableAIVideo parsed","timestamp":1771054935503}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/checkin/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/checkin/2026/02/14/db082b45736e42e08c8cecd15798090eexut37l8lp.jpg"},"timestamp":1771055166731,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"location":"UploadServiceImpl.commonUpload","message":"before createFile","data":{"rootPath":"/Users/apple/.crmeb_upload/","webPath":"crmebimage/public/audio/2026/02/14/","destPath":"/Users/apple/.crmeb_upload/crmebimage/public/audio/2026/02/14/e75ddab2e25d40b492e2036e2f44301fcuri32duxq.mp3"},"timestamp":1771055173015,"hypothesisId":"H1,H2,H4","runId":"post-fix"}
{"data":{"rawValue":true,"setToUserSign":1,"parsedBoolean":true},"hypothesisId":"E","location":"ToolCheckinServiceImpl.java:135","id":"log_1771055180778_b","message":"enableAIVideo parsed","timestamp":1771055180778}

View File

@@ -0,0 +1,111 @@
---
name: eGFR fix and video gen
overview: Fix the eGFR field not appearing in calculator results due to a Lombok/Jackson serialization mismatch, and ensure the check-in page correctly calls the KieAI API to generate sora videos when the toggle is enabled.
todos:
- id: fix-egfr-serialization
content: Add @JsonProperty("eGFR") to HealthData.eGFR field in NutritionCalculateResponse.java to fix JSON serialization mismatch
status: completed
- id: fix-nframes-hardcode
content: Fix n_frames hardcoded value in ToolSora2ServiceImpl.java to use the request input value instead of always "15"
status: completed
- id: verify-checkin-video-flow
content: Verify and fix the end-to-end video generation flow from checkin-publish.vue through KieAI API, ensuring taskId is properly stored in UserSign
status: completed
- id: improve-video-error-handling
content: Improve error handling and user feedback in checkin-publish.vue for video generation failures
status: completed
isProject: false
---
# Fix eGFR Display and Check-in Video Generation
## Issue 1: eGFR Not Showing in Calculator Results
### Root Cause
The field `private String eGFR` in `HealthData` uses Lombok `@Data`, which generates the getter `getEGFR()`. Jackson follows Java Beans naming convention: when the first two characters after stripping "get" are both uppercase ("EG"), the property name stays as-is: `"EGFR"`. The frontend expects `data.healthData.eGFR` but receives `data.healthData.EGFR`, resulting in `undefined` and displaying `--`.
```mermaid
sequenceDiagram
participant FE as Frontend
participant BE as Backend
FE->>BE: GET /tool/calculator/result/{id}
BE->>BE: buildResponse() sets healthData.setEGFR(...)
BE-->>FE: JSON: {"healthData":{"EGFR":"7.9",...}}
FE->>FE: healthData.eGFR is undefined
FE->>FE: Displays "--"
```
### Fix
Add `@JsonProperty("eGFR")` annotation to the `eGFR` field in [NutritionCalculateResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/NutritionCalculateResponse.java) (line 55):
```java
@JsonProperty("eGFR")
@ApiModelProperty(value = "eGFR数值ml/min/1.73m²)", example = "7.9")
private String eGFR;
```
This forces Jackson to use the exact field name `"eGFR"` in JSON output, matching the frontend's expectation.
---
## Issue 2: Check-in Page Video Generation
### Current State
The implementation already exists in [checkin-publish.vue](msh_single_uniapp/pages/tool/checkin-publish.vue) (lines 590-627). It calls `api.createImageToVideoTask()` / `api.createTextToVideoTask()` from [models-api.js](msh_single_uniapp/api/models-api.js) which hit the backend [KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java) endpoints at `/api/front/kieai/image-to-video` and `/api/front/kieai/text-to-video`.
Auth is not required (kieai endpoints are excluded from the interceptor in [WebConfig.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/config/WebConfig.java) line 98).
```mermaid
sequenceDiagram
participant User
participant CheckinPage as checkin-publish.vue
participant ModelsAPI as models-api.js
participant Backend as KieAIController
participant Sora2 as ToolSora2ServiceImpl
participant KieAI as KieAI API
User->>CheckinPage: Toggle "生成打卡视频" ON, click 发布
CheckinPage->>ModelsAPI: createImageToVideoTask({imageUrl, prompt, uid})
ModelsAPI->>Backend: POST /api/front/kieai/image-to-video
Backend->>Sora2: createImageToVideoTask(...)
Sora2->>KieAI: Upload image to KieAI
Sora2->>KieAI: POST /api/v1/jobs/createTask (sora-2-image-to-video)
KieAI-->>Sora2: {taskId: "..."}
Sora2->>Sora2: Save Article record with taskId
Sora2-->>Backend: taskId
Backend-->>ModelsAPI: CommonResult{code:200, data: taskId}
ModelsAPI-->>CheckinPage: response
CheckinPage->>CheckinPage: submitCheckin({...taskId...})
```
### Issues to Fix
**a) Request parameter format bug in `models-api.js**`
The `createImageToVideoTask` function sends `image_urls` as `[params.imageUrl]` (single URL in array). However, the `Sora2Request.Input.image_urls` field is `String[]` -- this should work for deserialization, but the request body also sets `uid` as a potential number (from vuex). The `Sora2Request.uid` is `String` type. Jackson auto-coerces `number -> String`, so this is fine. No code change needed here.
**b) `n_frames` hardcoded in backend**
The backend [ToolSora2ServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java) hardcodes `input.put("n_frames", "15")` at lines 44 and 109, ignoring the value sent by the frontend (`5`). For check-in videos, a shorter duration (5 frames ~ 5 seconds) is more appropriate. Change to use the value from the request input.
**c) Video task result not linked to check-in in backend**
Currently the video task creates an `Article` record but has no direct link to the `UserSign` (check-in) record. The frontend passes `taskId` in the `submitCheckin` request, but this linkage should be verified in the backend `ToolCheckinServiceImpl` to properly store and retrieve the video for the check-in.
**d) Error handling improvement in frontend**
The current error handling silently fails. Improve feedback to the user when video generation fails.
### Changes
- **[ToolSora2ServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java)**: Use the `n_frames` value from the request input instead of hardcoding `"15"`. Both `createTextToVideoTask` (line 44) and `createImageToVideoTask` (line 109) need this fix.
- **[checkin-publish.vue](msh_single_uniapp/pages/tool/checkin-publish.vue)**: Improve video generation error handling and ensure proper prompt construction for nutritional video content.
- **Verify backend checkin service** stores the `taskId` from the video task in the `UserSign` record correctly.

View File

@@ -0,0 +1,475 @@
---
name: MealPlan Image AI Generation
overview: 将 models-integration 项目中 KieAI 图像/视频生成接口复刻到 crmeb-front并在 mealPlan 菜品图片 URL 无效时,通过 KieAI NanoBanana API 生成图片、上传 OSS、返回有效 URL。
todos:
- id: replicate-kieai-config
content: 在 crmeb-common 中新增 KieAIConfig 配置类,在 application-sophia.yml 中添加 kie-ai 配置
status: completed
- id: replicate-kieai-dtos
content: 在 crmeb-common 中复刻 KieAI 相关 DTOTextToImageInput, CreateTaskRequest, CreateTaskResponse, QueryTaskResponse, NanoBananaRequest, NanoBananaResponse 等)
status: completed
- id: replicate-kieai-helper
content: 在 crmeb-service 中复刻 NanoBananaHelperAPI 调用工具类)
status: completed
- id: replicate-kieai-service
content: 在 crmeb-service 中复刻 KieAI 图像生成 Service 接口及实现ToolKieAIService / ToolKieAIServiceImpl
status: completed
- id: replicate-kieai-controller
content: 在 crmeb-front 中新增 KieAIController暴露文生图、图编辑、任务查询等 REST 接口
status: completed
- id: create-cache-table
content: 创建 v2_dish_image_cache 数据库表及对应 MyBatis Entity/Dao/XML
status: completed
- id: extend-oss-service
content: 扩展 OssService 支持 InputStream 上传AI 生成的图片无需先落盘)
status: completed
- id: implement-dish-image-service
content: 实现 DishImageServiceURL 检测 + KieAI 文生图 + 轮询等待 + 下载图片 + OSS 上传 + 缓存写入
status: completed
- id: modify-calculator-service
content: 修改 ToolCalculatorServiceImpl.generateMealPlan() 集成 DishImageService
status: completed
- id: error-handling
content: 实现降级策略KieAI 生成失败时使用默认占位图,不阻断主流程
status: completed
isProject: false
---
# MealPlan 菜品图片 KieAI 生成 + OSS 上传方案
## 一、现状分析
### 1.1 问题
`[ToolCalculatorServiceImpl.generateMealPlan()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)`11 道菜品的图片 URL 是硬编码的,指向 `https://uthink2025.oss-cn-shanghai.aliyuncs.com/recipes/xxx.jpg`。如果这些图片在 OSS 上不存在,前端会显示裂图。
### 1.2 已有基础设施
- **crmeb 项目**: Coze API SDK (v0.2.3)、阿里云 OSS 上传 (`[OssServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/OssServiceImpl.java)`)、OSS 配置存储在 `system_config` 表中
- **models-integration 项目**: 已有完整的 KieAI 集成,包括:
- `NanoBanana` 文生图 API`POST {baseUrl}/predictions`
- `Sora2` 文生视频 / 图生视频 API`POST {baseUrl}/api/v1/jobs/createTask`
- 任务状态轮询、回调通知机制
- 文件上传接口(`POST {uploadBaseUrl}/api/file-url-upload`
### 1.3 KieAI API 核心信息
| 项目 | 值 |
| --------------- | ---------------------------------------------------- |
| API Base URL | `https://api.kie.ai` |
| Upload Base URL | `https://kieai.redpandaai.co` |
| 文生图端点 | `POST /predictions` |
| 查询任务端点 | `GET /predictions/{taskId}` |
| 认证方式 | `Authorization: Bearer {apiToken}` |
| 文生图模型 | `google/nano-banana` |
| 图片编辑模型 | `google/nano-banana-edit` / `google/nano-banana-pro` |
| 任务状态值 | `queuing` / `processing` / `completed` / `failed` |
---
## 二、整体架构
### 2.1 KieAI 接口复刻架构
```mermaid
graph TD
subgraph crmebFront [crmeb-front]
KieAICtrl[KieAIController]
end
subgraph crmebService [crmeb-service]
ToolKieAISvc[ToolKieAIService]
ToolKieAISvcImpl[ToolKieAIServiceImpl]
KieAIHelper[KieAIHelper]
end
subgraph crmebCommon [crmeb-common]
KieAICfg[KieAIConfig]
DTOs[DTOs: TextToImageInput etc.]
end
subgraph external [External]
KieAIAPI["KieAI API (api.kie.ai)"]
end
KieAICtrl --> ToolKieAISvc
ToolKieAISvc --> ToolKieAISvcImpl
ToolKieAISvcImpl --> KieAIHelper
KieAIHelper --> KieAICfg
KieAIHelper --> DTOs
KieAIHelper --> KieAIAPI
```
### 2.2 菜品图片生成流程
```mermaid
sequenceDiagram
participant Client
participant CalculatorService
participant DishImageService
participant DB as v2_dish_image_cache
participant KieAI as KieAI NanoBanana API
participant OSS as 阿里云OSS
Client->>CalculatorService: POST /calculator/calculate
CalculatorService->>CalculatorService: generateMealPlan()
loop 每道菜品
CalculatorService->>DishImageService: ensureValidImage(dishName, originalUrl)
DishImageService->>DB: 查询缓存(dishName)
alt 缓存命中
DB-->>DishImageService: 返回缓存的 oss_url
else 缓存未命中
DishImageService->>DishImageService: HTTP HEAD 检查 originalUrl
alt 原始URL有效
DishImageService->>DB: 写入缓存
else 原始URL无效(404)
DishImageService->>KieAI: createTextToImageTask(prompt)
KieAI-->>DishImageService: 返回 taskId
DishImageService->>KieAI: waitForTaskCompletion(taskId)
KieAI-->>DishImageService: 返回 output URLs
DishImageService->>DishImageService: 下载图片 byte[]
DishImageService->>OSS: 上传到 recipes/ 目录
OSS-->>DishImageService: 返回 OSS URL
DishImageService->>DB: 写入缓存
end
end
DishImageService-->>CalculatorService: 返回有效 URL
end
CalculatorService-->>Client: NutritionCalculateResponse
```
---
## 三、实现步骤
### 步骤 1复刻 KieAI 配置到 crmeb-common
**新增文件:** `crmeb-common/src/main/java/com/zbkj/common/config/KieAIConfig.java`
`[models-integration KieAIConfig](models-integration/src/main/java/com/integration/api/config/KieAIConfig.java)` 复刻,保留核心字段:
```java
@Configuration
@ConfigurationProperties(prefix = "kie-ai")
public class KieAIConfig {
private String baseUrl = "https://api.kie.ai";
private String apiToken; // API Token
private String apiUploadBaseUrl = "https://kieai.redpandaai.co";
private String apiCallbackUrl; // 回调 URL
private Integer connectTimeout = 30000;
private Integer readTimeout = 60000;
private Integer pollInterval = 2000; // 轮询间隔
private Integer maxWaitTime = 300000; // 最大等待时间
private String defaultOutputFormat = "png";
private String defaultImageSize = "1:1"; // 菜品图默认正方形
}
```
**修改:** `application-sophia.yml` 新增配置段:
```yaml
kie-ai:
base-url: https://api.kie.ai
api-token: 484661585fe62c5bcb77e6d392ba8ee8
api-callback-url: https://sophia-shop.uj345.cc/api/front/kieai/callback
api-upload-base-url: https://kieai.redpandaai.co
connect-timeout: 30000
read-timeout: 60000
poll-interval: 2000
max-wait-time: 300000
default-output-format: png
default-image-size: "1:1"
```
### 步骤 2复刻 KieAI DTOs 到 crmeb-common
**新增文件位置:** `crmeb-common/src/main/java/com/zbkj/common/request/kieai/``crmeb-common/src/main/java/com/zbkj/common/response/kieai/`
`[models-integration DTOs](models-integration/src/main/java/com/integration/api/dto/)` 复刻以下类:
| 源文件 (models-integration) | 目标位置 (crmeb-common) | 说明 |
| ------------------------- | --------------------------------------------- | --------- |
| `TextToImageInput.java` | `request/kieai/KieAITextToImageInput.java` | 文生图输入参数 |
| `ImageEditInput.java` | `request/kieai/KieAIImageEditInput.java` | 图编辑输入参数 |
| `CreateTaskRequest.java` | `request/kieai/KieAICreateTaskRequest.java` | 创建任务请求 |
| `NanoBananaRequest.java` | `request/kieai/KieAINanoBananaRequest.java` | 前端传入的完整请求 |
| `CreateTaskResponse.java` | `response/kieai/KieAICreateTaskResponse.java` | 创建任务响应 |
| `QueryTaskResponse.java` | `response/kieai/KieAIQueryTaskResponse.java` | 查询任务响应 |
| `NanoBananaResponse.java` | `response/kieai/KieAINanoBananaResponse.java` | 通用响应包装 |
核心 DTO 结构(`KieAIQueryTaskResponse`
```java
public class KieAIQueryTaskResponse {
private String id; // 任务ID
private String status; // queuing/processing/completed/failed
private String created_at;
private String finished_at;
private String model;
private Object input;
private List<String> output; // 生成的图片URL列表
private String error;
}
```
### 步骤 3复刻 KieAI Helper 到 crmeb-service
**新增文件:** `crmeb-service/src/main/java/com/zbkj/service/helper/KieAIHelper.java`
`[NanoBananaHelper](models-integration/src/main/java/com/integration/api/helper/NanoBananaHelper.java)` 复刻,核心方法:
- `createHeaders()` -- 构建 `Authorization: Bearer {token}` 请求头
- `createTask(KieAICreateTaskRequest)` -- POST `/predictions` 创建文生图任务
- `queryTask(String taskId)` -- GET `/predictions/{taskId}` 查询任务
- `buildTextToImageRequest(KieAITextToImageInput, callbackUrl)` -- 构建请求
- `isApiTokenConfigured()` -- 验证 Token 配置
使用项目中已有的 `RestTemplate`(如果 crmeb 中没有 RestTemplate Bean需要注册一个
### 步骤 4复刻 KieAI Service 到 crmeb-service
**新增文件:**
- `crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolKieAIService.java`(接口)
- `crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKieAIServiceImpl.java`(实现)
`[NanoBananaServiceImpl](models-integration/src/main/java/com/integration/api/service/impl/NanoBananaServiceImpl.java)` 复刻核心方法:
```java
public interface ToolKieAIService {
/** 文生图 - 创建任务 */
KieAICreateTaskResponse createTextToImageTask(KieAINanoBananaRequest request);
/** 查询任务状态 */
KieAIQueryTaskResponse queryTask(String taskId);
/** 同步等待任务完成(轮询) */
KieAIQueryTaskResponse waitForTaskCompletion(String taskId, long maxWaitTime);
/** 图编辑 - 创建任务 */
KieAICreateTaskResponse createImageEditTask(KieAINanoBananaRequest request);
}
```
**注意:** 复刻时简化设计 -- 不需要 `NanoBananaTask` 数据库表和 `Article` 表关联逻辑,因为 crmeb 中的主要使用场景是内部调用(菜品图片生成),任务记录可选。
### 步骤 5复刻 KieAI Controller 到 crmeb-front
**新增文件:** `crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java`
`[KieAI2ImageController](models-integration/src/main/java/com/integration/api/controller/KieAI2ImageController.java)` 复刻,暴露以下接口:
| 接口 | 方法 | 说明 |
| ------------------------------------- | ---- | ------ |
| `/api/front/kieai/text-to-image` | POST | 文生图 |
| `/api/front/kieai/image-edit` | POST | 图编辑 |
| `/api/front/kieai/task/{taskId}` | GET | 查询任务 |
| `/api/front/kieai/task/{taskId}/wait` | GET | 同步等待任务 |
| `/api/front/kieai/callback` | POST | 回调通知 |
同时需要在安全白名单中添加 `/api/front/kieai/**` 路径(参考 Coze 接口的白名单配置)。
### 步骤 6新建菜品图片缓存表
```sql
CREATE TABLE `v2_dish_image_cache` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`dish_name` VARCHAR(100) NOT NULL COMMENT '菜品名称',
`original_url` VARCHAR(500) DEFAULT NULL COMMENT '原始图片URL',
`oss_url` VARCHAR(500) NOT NULL COMMENT 'OSS有效图片URL',
`ai_provider` VARCHAR(50) DEFAULT 'kieai' COMMENT 'AI生成来源',
`task_id` VARCHAR(100) DEFAULT NULL COMMENT 'KieAI任务ID',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dish_name` (`dish_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品图片缓存表';
```
对应新增:
- `crmeb-common/.../model/tool/V2DishImageCache.java`(实体)
- `crmeb-service/.../dao/tool/V2DishImageCacheDao.java`Mapper
- `resources/mapper/tool/V2DishImageCacheDao.xml`MyBatis XML
### 步骤 7扩展 OssService 支持 InputStream 上传
当前 `[OssServiceImpl.upload()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/OssServiceImpl.java)` 只支持 `File` 参数。新增 InputStream 重载:
```java
// OssService 接口新增
void upload(CloudVo cloudVo, String objectKey, InputStream inputStream, long contentLength);
// OssServiceImpl 实现
@Override
public void upload(CloudVo cloudVo, String objectKey, InputStream inputStream, long contentLength) {
OSS ossClient = new OSSClientBuilder().build(cloudVo.getRegion(), cloudVo.getAccessKey(), cloudVo.getSecretKey());
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(contentLength);
PutObjectRequest putRequest = new PutObjectRequest(cloudVo.getBucketName(), objectKey, inputStream, metadata);
ossClient.putObject(putRequest);
} finally {
ossClient.shutdown();
}
}
```
### 步骤 8实现 DishImageService
**新增文件:**
- `crmeb-service/.../service/tool/DishImageService.java`
- `crmeb-service/.../service/impl/tool/DishImageServiceImpl.java`
核心流程:
```java
@Service
public class DishImageServiceImpl implements DishImageService {
@Autowired private V2DishImageCacheDao cacheDao;
@Autowired private ToolKieAIService kieAIService;
@Autowired private OssService ossService;
@Autowired private SystemConfigService systemConfigService;
@Override
public String ensureValidImageUrl(String dishName, String originalUrl) {
// 1. 查缓存
V2DishImageCache cache = cacheDao.selectByDishName(dishName);
if (cache != null) return cache.getOssUrl();
// 2. HTTP HEAD 检查原始 URL3秒超时
if (checkUrlAccessible(originalUrl)) {
saveCache(dishName, originalUrl, originalUrl, null);
return originalUrl;
}
// 3. KieAI 文生图
try {
String prompt = "一道精美的中式菜品照片:" + dishName
+ ",高清美食摄影风格,白色餐盘,俯拍角度,自然光照";
KieAICreateTaskResponse createResp = kieAIService.createTextToImageTask(buildRequest(prompt));
KieAIQueryTaskResponse result = kieAIService.waitForTaskCompletion(createResp.getId(), 120000);
if ("completed".equals(result.getStatus()) && result.getOutput() != null) {
String imageUrl = result.getOutput().get(0);
// 4. 下载图片并上传 OSS
byte[] imageBytes = downloadImage(imageUrl);
String ossUrl = uploadToOss(imageBytes, dishName);
// 5. 写缓存
saveCache(dishName, originalUrl, ossUrl, createResp.getId());
return ossUrl;
}
} catch (Exception e) {
log.error("菜品图片AI生成失败: {}", dishName, e);
}
// 6. 降级:返回默认占位图
return getDefaultPlaceholderUrl();
}
}
```
### 步骤 9修改 ToolCalculatorServiceImpl
`[generateMealPlan()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)` 方法末尾集成 DishImageService
```java
@Autowired
private DishImageService dishImageService;
private MealPlan generateMealPlan(String ckdStage, Boolean dialysis) {
MealPlan plan = new MealPlan();
// ... 现有代码不变(创建早/午/晚餐 Dish 列表)...
// 确保所有菜品图片URL有效
ensureMealPlanImages(plan);
return plan;
}
private void ensureMealPlanImages(MealPlan plan) {
Stream.of(plan.getBreakfast(), plan.getLunch(), plan.getDinner())
.filter(Objects::nonNull)
.flatMap(List::stream)
.forEach(dish -> {
String validUrl = dishImageService.ensureValidImageUrl(dish.getName(), dish.getImage());
dish.setImage(validUrl);
});
}
```
---
## 四、异常处理与降级策略
- KieAI 生成失败 / 超时 -> 返回**默认占位图** URL预先上传到 OSS 的通用食物图片)
- HTTP URL 检测设置 **3 秒超时**,避免阻塞主流程
- 整个图片处理过程用 try-catch 包裹,**失败不影响营养计算结果的返回**
- 日志记录每次 AI 生成和上传操作,便于排查
- KieAI API Token 未配置时跳过 AI 生成,直接使用原始 URL 或占位图
---
## 五、性能优化
- **缓存优先**: DB 缓存命中后直接返回,零网络开销
- **仅 11 道固定菜品**: 缓存全部填充后,后续请求无 AI 调用
- **首次预热**: 可提供管理后台接口或启动任务,手动触发所有菜品图片预生成
- **并行处理**: 可用 `CompletableFuture` 并行检查/生成多道菜品图片(可选优化)
---
## 六、涉及文件清单
### 6.1 新增文件KieAI 接口复刻)
| 模块 | 文件路径 | 说明 |
| ------------- | --------------------------------------------- | ------------- |
| crmeb-common | `config/KieAIConfig.java` | KieAI 配置类 |
| crmeb-common | `request/kieai/KieAITextToImageInput.java` | 文生图输入 DTO |
| crmeb-common | `request/kieai/KieAIImageEditInput.java` | 图编辑输入 DTO |
| crmeb-common | `request/kieai/KieAICreateTaskRequest.java` | 创建任务请求 DTO |
| crmeb-common | `request/kieai/KieAINanoBananaRequest.java` | 前端完整请求 DTO |
| crmeb-common | `response/kieai/KieAICreateTaskResponse.java` | 创建任务响应 DTO |
| crmeb-common | `response/kieai/KieAIQueryTaskResponse.java` | 查询任务响应 DTO |
| crmeb-common | `response/kieai/KieAINanoBananaResponse.java` | 通用响应包装 |
| crmeb-service | `helper/KieAIHelper.java` | API 调用工具类 |
| crmeb-service | `service/tool/ToolKieAIService.java` | KieAI 服务接口 |
| crmeb-service | `service/impl/tool/ToolKieAIServiceImpl.java` | KieAI 服务实现 |
| crmeb-front | `controller/KieAIController.java` | KieAI REST 接口 |
### 6.2 新增文件(菜品图片缓存)
| 模块 | 文件路径 | 说明 |
| ------------- | ----------------------------------------------- | ------------------------- |
| crmeb-common | `model/tool/V2DishImageCache.java` | 缓存实体 |
| crmeb-service | `dao/tool/V2DishImageCacheDao.java` | MyBatis Mapper |
| crmeb-service | `resources/mapper/tool/V2DishImageCacheDao.xml` | MyBatis XML |
| crmeb-service | `service/tool/DishImageService.java` | 菜品图片服务接口 |
| crmeb-service | `service/impl/tool/DishImageServiceImpl.java` | 菜品图片服务实现 |
| - | SQL 迁移脚本 | 建 `v2_dish_image_cache` 表 |
### 6.3 修改文件
| 模块 | 文件路径 | 改动内容 |
| ------------- | -------------------------------------------------- | ------------------------ |
| crmeb-service | `service/OssService.java` | 新增 InputStream 上传重载方法 |
| crmeb-service | `service/impl/OssServiceImpl.java` | 实现 InputStream 上传 |
| crmeb-service | `service/impl/tool/ToolCalculatorServiceImpl.java` | 集成 DishImageService |
| crmeb-front | `resources/application-sophia.yml` | 新增 `kie-ai` 配置段 |
| crmeb-front | 安全白名单配置 | 添加 `/api/front/kieai/**` |

View File

@@ -0,0 +1,193 @@
---
name: Migrate APIs to crmeb-front
overview: Migrate all backend API implementations that models-api.js currently calls from models-integration to crmeb-front, and update the frontend to route all requests through crmeb-front's /api/front/ prefix.
todos:
- id: module-1-video
content: "Module 1: Copy KieAI Video (Sora2) -- DTO, Service, ServiceImpl, Controller endpoints (text-to-video, image-to-video, pro variants, watermark, file-upload, task query)"
status: completed
- id: module-2-asr
content: "Module 2: Copy Tencent ASR -- add tencentcloud-sdk-java dependency, Config, DTO, Service, ServiceImpl, Controller (create-task, query-status, sentence-recognition)"
status: completed
- id: module-3-articles
content: "Module 3: Copy Article Models -- Model/VO/Mapper, Service, Controller at /api/front/article-models (getById, list, search)"
status: completed
- id: coze-resume
content: Add Coze workflow resume endpoint to existing CozeController and ToolCozeService
status: completed
- id: interceptor-config
content: Update WebConfig/application.yml to exclude new routes from token interceptor
status: completed
- id: frontend-update
content: "Update models-api.js: change API_BASE_URL, update all paths to /api/front/ prefix, move secrets to server-side config"
status: completed
isProject: false
---
# Migrate models-api.js Backend to crmeb-front
## Current Architecture
The frontend `models-api.js` calls `API_BASE_URL = 'https://sophia-shop.uj345.cc/models'` (models-integration, port 5081). The goal is to migrate all these endpoints to crmeb-front (port 8081) so the frontend only needs to call `/api/front/` endpoints.
## Endpoint Mapping: models-api.js -> models-integration -> crmeb-front
### Already Ported (only need frontend URL change)
These endpoints already exist in crmeb-front and have working service implementations:
- **Coze Chat**: `/api/coze/chat` -> `/api/front/coze/chat` (CozeController)
- **Coze Retrieve**: `/api/coze/chat/retrieve` -> `/api/front/coze/chat/retrieve`
- **Coze Messages**: `/api/coze/chat/messages/list` -> `/api/front/coze/chat/messages/list`
- **Coze Workflow Run**: `/api/coze/workflow/run` -> `/api/front/coze/workflow/run`
- **Coze Workflow Stream**: `/api/coze/workflow/stream` -> `/api/front/coze/workflow/stream`
- **Coze File Upload**: `/api/coze/file/upload` -> `/api/front/coze/file/upload`
- **KieAI Image Edit**: `/api/kieai/image/image-edit` -> `/api/front/kieai/image-edit` (KieAIController)
- **Upload File**: already calls `sophia-shop.uj345.cc/api/front/upload/imageOuter` (crmeb-front)
### Need to Add to crmeb-front (3 modules)
#### Module 1: KieAI Video (Sora2) -- 6 endpoints
Source: [KieAI2VideoController.java](models-integration/src/main/java/com/integration/api/controller/KieAI2VideoController.java) + [Sora2ServiceImpl.java](models-integration/src/main/java/com/integration/api/service/impl/Sora2ServiceImpl.java)
New endpoints to add to `KieAIController` or a new `KieAIVideoController`:
- `POST /api/front/kieai/text-to-video` (createTextToVideoTask)
- `POST /api/front/kieai/image-to-video` (createImageToVideoTask)
- `POST /api/front/kieai/pro/text-to-video` (createProTextToVideoTask)
- `POST /api/front/kieai/pro/image-to-video` (createProImageToVideoTask)
- `POST /api/front/kieai/remove-watermark` (removeWatermark)
- `POST /api/front/kieai/file-url-upload` (uploadFileByUrl)
- `GET /api/front/kieai/video/task/{taskId}` (getTaskStatus - video task query)
Files to create/copy:
- **DTO**: Copy `Sora2Request` -> `com.zbkj.common.request.kieai.Sora2Request`
- **DTO**: Copy `TaskStatus` -> `com.zbkj.common.response.kieai.KieAIVideoTaskStatus`
- **DTO**: Copy `CreateProTextToVideoRequest` -> `com.zbkj.common.request.kieai.CreateProTextToVideoRequest`
- **Service**: Create `ToolSora2Service` interface in `crmeb-service`
- **ServiceImpl**: Copy `Sora2ServiceImpl` logic -> `ToolSora2ServiceImpl` in `crmeb-service`
- **Controller**: Add video endpoints to [KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java) or new controller
- **Helper**: Copy `HttpRequestUtils` -> `com.zbkj.common.utils.HttpRequestUtils` (or reuse existing crmeb HTTP utils)
Dependencies already available:
- Apache HttpClient (in crmeb-common pom.xml)
- FastJSON (in crmeb parent pom.xml)
- `KieAIConfig` already exists with `baseUrl`, `apiCallbackUrl` etc.
Config to add to `KieAIConfig`:
- `apiUploadBaseUrl` property (used by Sora2ServiceImpl for file uploads)
#### Module 2: Tencent ASR -- 3 endpoints
Source: [TencentAsrController.java](models-integration/src/main/java/com/integration/api/controller/TencentAsrController.java) + [TencentAsrServiceImpl.java](models-integration/src/main/java/com/integration/api/service/impl/TencentAsrServiceImpl.java)
New endpoints:
- `POST /api/front/tencent/asr/create-task`
- `GET /api/front/tencent/asr/query-status/{taskId}`
- `POST /api/front/tencent/asr/sentence-recognition`
Files to create/copy:
- **DTO**: Copy `TencentAsrRequest` -> `com.zbkj.common.request.tencent.TencentAsrRequest`
- **DTO**: Copy `TencentAsrResponse` -> `com.zbkj.common.response.tencent.TencentAsrResponse`
- **DTO**: Copy `TencentAsrTaskStatus` -> `com.zbkj.common.response.tencent.TencentAsrTaskStatus`
- **Config**: Create `TencentAsrConfig` in `com.zbkj.common.config` (prefix `tencent-asr`, fields: secretId, secretKey, region, connectTimeout, readTimeout, defaultEngineModel, defaultResTextFormat, defaultChannelNum, enabled)
- **Service**: Create `ToolTencentAsrService` interface
- **ServiceImpl**: Copy `TencentAsrServiceImpl` logic -> `ToolTencentAsrServiceImpl`
- **Controller**: Create `TencentAsrController` in crmeb-front at `/api/front/tencent/asr`
**New dependency needed** in `crmeb-common/pom.xml`:
```xml
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.880</version>
</dependency>
```
**Config to add** to `application-sophia.yml`:
```yaml
tencent-asr:
secret-id: ${TENCENT_SECRET_ID:xxx}
secret-key: ${TENCENT_SECRET_KEY:xxx}
region: ap-shanghai
enabled: true
```
#### Module 3: Articles (models-integration style) -- 3 endpoints
Source: [ArticleController.java](models-integration/src/main/java/com/integration/api/controller/ArticleController.java)
Note: crmeb-front already has an `ArticleController` at `/api/front/article` for CMS articles, but that is a different data model (CRMEB's own articles). The models-integration `ArticleController` manages AI-generated content articles in a separate `eb_article` table with fields like `taskId`, `prompt`, `videoUrl`, `statusTask`.
Approach: Add these as new endpoints in a **new controller** `ArticleModelsController` at `/api/front/article-models` to avoid conflict with the existing crmeb ArticleController.
New endpoints:
- `GET /api/front/article-models/{id}` (getArticleById)
- `GET /api/front/article-models` (getArticleList, paginated)
- `GET /api/front/article-models/search` (searchArticles)
Files needed:
- The `Article` model, `ArticleVO`, `ArticleMapper`, `UserMapper` already exist in models-integration's DB tables. Need to ensure the crmeb service can access the same `eb_article` table or create equivalent mapper/model in crmeb-common.
- **Service**: Create `ToolArticleModelsService` interface + impl
- **Controller**: Create `ArticleModelsController`
### Callback Endpoints (remain in models-integration)
The following callbacks should **stay in models-integration** since they are called by external services (KieAI, Coze) and need a stable URL:
- `POST /api/kieai/callback`
- `POST /api/kieai/callback20994`
- `POST /api/diet-checkin/callback/video`
- `POST /api/diet-checkin/callback/analysis`
These use `callBackUrl` that is configured in the task creation requests, so they can continue pointing to models-integration.
### Coze Workflow Resume
The frontend `cozeWorkflowResume` function calls `/api/coze/workflow/resume`, but this endpoint does **not exist** in either models-integration or crmeb-front. The Coze SDK does support workflow resume. Need to:
- Add `/api/front/coze/workflow/resume` endpoint to crmeb-front CozeController
- Add `workflowResume()` method to `ToolCozeService` / `ToolCozeServiceImpl`
## Frontend Changes (models-api.js)
After backend is ready, update [models-api.js](msh_single_uniapp/utils/models-api.js):
1. Change `API_BASE_URL` from `'https://sophia-shop.uj345.cc/models'` to `'https://sophia-shop.uj345.cc'` (crmeb-front domain)
2. Update all API paths to use `/api/front/` prefix:
- `/api/articles/{id}` -> `/api/front/article-models/{id}`
- `/api/articles` -> `/api/front/article-models`
- `/api/articles/search` -> `/api/front/article-models/search`
- `/api/kieai/text-to-video` -> `/api/front/kieai/text-to-video`
- `/api/kieai/image-to-video` -> `/api/front/kieai/image-to-video`
- `/api/kieai/image/image-edit` -> `/api/front/kieai/image-edit`
- `/api/kieai/task/{taskId}` -> `/api/front/kieai/video/task/{taskId}`
- `/api/kieai/file-url-upload` -> `/api/front/kieai/file-url-upload`
- `/api/tencent/asr/create-task` -> `/api/front/tencent/asr/create-task`
- `/api/tencent/asr/query-status/{id}` -> `/api/front/tencent/asr/query-status/{id}`
- `/api/coze/chat` -> `/api/front/coze/chat`
- `/api/coze/chat/retrieve` -> `/api/front/coze/chat/retrieve`
- `/api/coze/chat/messages/list` -> `/api/front/coze/chat/messages/list`
- `/api/coze/workflow/run` -> `/api/front/coze/workflow/run`
- `/api/coze/workflow/stream` -> `/api/front/coze/workflow/stream`
- `/api/coze/workflow/resume` -> `/api/front/coze/workflow/resume`
- `/api/coze/file/upload` -> `/api/front/coze/file/upload` (in `cozeUploadFile`)
3. Remove hardcoded `tenant_id`, `api_key`, callback URLs from frontend -- move these to server-side config
## Interceptor Config
Ensure the new routes are excluded from token interceptor in WebConfig (or application.yml `excludePathPatterns`):
- `/api/front/tencent/asr/**`
- `/api/front/article-models/**`
- KieAI video endpoints are under `/api/front/kieai/**` which is likely already excluded

View File

@@ -0,0 +1,559 @@
---
name: Tool Module Test Plan
overview: Create a comprehensive frontend functional test plan for the WeChat mini-program tool module, covering the hub page (tool_main/index.vue) and all 18 sub-pages under pages/tool/.
todos:
- id: test-hub
content: "测试 Tool 首页 (tool_main/index.vue): 数据加载、四宫格导航、推荐内容、登录态"
status: completed
- id: test-calculator
content: "测试食谱计算器流程: calculator.vue 表单校验提交 -> calculator-result.vue 结果展示与采纳"
status: completed
- id: test-ai
content: "测试 AI 营养师: 文字对话、语音输入(ASR)、图片发送、Coze API 集成"
status: completed
- id: test-checkin
content: "测试打卡流程: checkin.vue 积分/连续打卡 -> checkin-publish.vue 发布 -> checkin-detail.vue 详情 -> dietary-records.vue 列表 -> checkin-copy.vue 借鉴"
status: completed
- id: test-food
content: "测试食物百科: food-encyclopedia.vue 搜索/分类/列表 -> food-detail.vue 详情"
status: completed
- id: test-knowledge
content: "测试营养知识: nutrition-knowledge.vue Tab切换/列表 -> nutrient-detail.vue 详情"
status: completed
- id: test-community
content: "测试社区帖子: post-detail.vue 展示/点赞/收藏/评论/关注"
status: completed
- id: test-profile
content: "测试用户与积分: my-profile.vue / points-rules.vue / invite-rewards.vue / welcome-gift.vue"
status: completed
- id: test-common
content: "通用测试: 登录态、网络异常、UI多机型适配、性能、页面标题与返回"
status: completed
isProject: false
---
# Tool 模块前端功能测试计划
## 一、测试范围
本测试计划覆盖微信小程序 Tool 模块的全部 19 个页面:
- **入口首页**: [pages/tool_main/index.vue](msh_single_uniapp/pages/tool_main/index.vue)
- **子页面 (18个)**: [pages/tool/](msh_single_uniapp/pages/tool/) 目录下全部 `.vue` 文件
### 页面与路由清单
| 页面文件 | 路由路径 | 页面标题 | 分类 |
| ---- | ---- | ---- | --- |
> 注意Markdown 表格不便渲染,以列表替代:
**食谱计算器**
- `calculator.vue` -> `/pages/tool/calculator` (食谱计算器)
- `calculator-result.vue` -> `/pages/tool/calculator-result` (营养计算结果)
**AI 营养师**
- `ai-nutritionist.vue` -> `/pages/tool/ai-nutritionist` (AI营养师)
**食物百科**
- `food-encyclopedia.vue` -> `/pages/tool/food-encyclopedia` (食物百科)
- `food-detail.vue` -> `/pages/tool/food-detail` (食物详情)
**营养知识**
- `nutrition-knowledge.vue` -> `/pages/tool/nutrition-knowledge` (营养知识)
- `nutrient-detail.vue` -> `/pages/tool/nutrient-detail` (营养素详情)
**饮食打卡**
- `checkin.vue` -> `/pages/tool/checkin` (打卡)
- `checkin-publish.vue` -> `/pages/tool/checkin-publish` (饮食打卡)
- `checkin-detail.vue` -> `/pages/tool/checkin-detail` (打卡详情)
- `checkin-copy.vue` -> `/pages/tool/checkin-copy` (一键借鉴打卡)
- `dietary-records.vue` -> `/pages/tool/dietary-records` (饮食记录)
**社区帖子**
- `post-detail.vue` -> `/pages/tool/post-detail` (帖子详情)
**食谱**
- `recipe-detail.vue` -> `/pages/tool/recipe-detail` (食谱详情)
**用户与积分**
- `my-profile.vue` -> `/pages/tool/my-profile` (我的)
- `points-rules.vue` -> `/pages/tool/points-rules` (积分规则)
- `invite-rewards.vue` -> `/pages/tool/invite-rewards` (邀请有礼)
- `welcome-gift.vue` -> `/pages/tool/welcome-gift` (会员福利)
---
## 二、测试环境与前置条件
- 测试平台:微信开发者工具 + 真机iOS / Android
- 网络环境WiFi 正常网络 + 弱网 (3G) + 断网
- 账号准备:已登录账号 / 未登录访客
- 后端接口:已部署测试环境 API`tool/*` 全部接口可用)
- 测试数据:至少 1 条已有计算结果、1 条打卡记录、1 条社区帖子
---
## 三、功能测试用例
### 3.1 Tool 首页 (tool_main/index.vue)
**页面加载**
- 页面打开后,并发调用 `getRecommendedRecipes``getRecommendedKnowledge``getUserHealthStatus` 三个接口,数据正常展示
- 接口返回失败时(单个/全部),页面不白屏,其他模块正常展示
- 弱网环境下 loading 状态正常
**用户信息区域**
- 已登录:显示用户头像、昵称、健康档案状态
- 未登录:显示"请点击登录",点击触发登录弹窗
- 点击头像跳转到 `/pages/tool/my-profile`
- 点击"打卡"按钮跳转到 `/pages/tool/checkin`
**四宫格工具入口**
- "食谱计算器" 跳转到 `/pages/tool/calculator`
- "AI营养师" 跳转到 `/pages/tool/ai-nutritionist`
- "食物百科" 跳转到 `/pages/tool/food-encyclopedia`
- "营养知识" 跳转到 `/pages/tool/nutrition-knowledge`
**推荐食谱区域**
- 展示 2 条推荐食谱(封面图、标题)
- 点击食谱卡片跳转 `/pages/tool/recipe-detail?id=xxx`
- 点击"更多"显示 toast "食谱列表功能开发中"
- 无数据时不展示该区域 / 展示空态
**健康知识区域**
- 展示 2 条推荐知识(标题、摘要)
- 点击知识条目跳转 `/pages/tool/nutrition-knowledge?id=xxx`
- 点击"更多"跳转 `/pages/tool/nutrition-knowledge`
**推广卡片**
- "慢生活营养专家" 卡片正常展示
- 点击跳转 `/pages/tool/welcome-gift`
---
### 3.2 食谱计算器 (calculator.vue)
**表单输入**
- 性别选择(男/女)单选正常
- 年龄、身高、干体重、血肌酐输入框:只允许数字,限制合理范围
- 透析状态切换(是/否),选"是"后透析类型选项出现
- 透析类型(血液透析/腹膜透析)单选正常
- 所有必填项为空时,点击"开始计算"给出校验提示
**计算提交**
- 填写完整后点击"开始计算",调用 `calculateNutrition` 接口(`POST tool/calculator/calculate`
- 接口返回成功后跳转到 `/pages/tool/calculator-result?id=xxx`
- 接口返回失败时 toast 提示错误信息
- 重复点击防抖(按钮 loading 状态)
---
### 3.3 营养计算结果 (calculator-result.vue)
**页面加载**
- 携带 `id` 参数进入,调用 `getCalculatorResult(id)` 获取数据
- Loading 状态展示正常
- 无结果 / id 无效时展示 error 状态
**健康概览 Tab**
- 展示 eGFR 值、体重、BMI、CKD 分期数据卡片
- 营养目标(蛋白质、热量、钠、钾、磷等)正确展示
- 食物份量列表正确展示
**膳食计划 Tab**
- Tab 切换正常(健康概览 / 膳食计划)
- 早餐、午餐、晚餐 三餐方案展示正确
- 每餐菜品名称、份量、图片展示
- 菜品图片加载(含 AI 生成图片、OSS 压缩图片)正常
- 温馨提示区域展示
**操作按钮**
- "采纳方案" 按钮调用 `adoptNutritionPlan(resultId)``POST tool/calculator/adopt`
- 采纳成功后提示并更新状态
- "联系营养师" 按钮跳转 AI 营养师或显示联系方式
---
### 3.4 AI 营养师 (ai-nutritionist.vue)
**对话界面**
- 页面打开后展示推广 banner
- 快捷问题列表展示,点击快捷问题自动发送
- 消息列表scroll-view正常滚动新消息自动滚到底部
**文字对话**
- 输入文字后点击发送按钮,消息气泡出现(用户侧)
- AI 回复消息气泡出现AI 侧),打字指示器展示
- 使用 Coze API 流程:`cozeChat` -> `cozeRetrieveChat` -> `cozeMessageList`
- 空消息不允许发送
**语音输入**
- 点击麦克风按钮开始录音
- 录音完成后上传语音 (`uploadFile`) 并调用 ASR (`createAsrTask` -> `queryAsrStatus`)
- 语音识别结果转为文字并发送
- 录音授权拒绝时提示用户
**图片发送**
- 点击相机按钮选择/拍照图片
- 图片上传后通过 Coze 接口发送给 AI
- 图片预览功能正常
**异常处理**
- 网络断开时发送消息提示网络错误
- AI 回复超时处理
- 会话 ID 管理正常(多次对话保持上下文)
---
### 3.5 食物百科 (food-encyclopedia.vue)
- 页面加载调用 `getFoodList` 接口,展示食物列表
- 顶部搜索框输入关键词,调用 `searchFood` 接口
- 搜索防抖正常(不会每个字符触发请求)
- 横向分类滑动切换(全部/谷物/蔬菜/水果/肉类/海鲜)
- 切换分类后列表刷新
- 食物卡片展示:图片、名称、安全标签、营养信息
- 点击食物卡片跳转 `/pages/tool/food-detail`
- 列表分页加载(上拉加载更多)
- 空搜索结果展示空态
---
### 3.6 食物详情 (food-detail.vue)
- 页面加载展示食物图片、分类、安全标签
- 关键营养素宫格展示
- 营养成分表完整展示
- "相似食物" 按钮功能
- 分享按钮功能正常
---
### 3.7 营养知识 (nutrition-knowledge.vue)
- Tab 切换:营养素 / 饮食指导 / 文章
- 各 Tab 调用 `getKnowledgeList` 接口type 参数不同)
- 营养素列表展示,点击跳转 `/pages/tool/nutrient-detail`
- 饮食指导列表展示
- 文章列表展示
- 列表分页(上拉加载更多)
- 携带 `id` 参数进入时自动定位到对应内容
---
### 3.8 营养素详情 (nutrient-detail.vue)
- 页面通过本地营养素数据 map 渲染
- 展示:营养素头部信息、状态卡片、重要性、摄入建议、食物来源、风险提示、建议
- 底部免责声明展示
- 不存在的营养素名称进入时展示空态或提示
---
### 3.9 打卡首页 (checkin.vue)
- 积分展示区域,调用 `getUserPoints` 接口
- 7 日连续打卡圈展示,调用 `getCheckinStreak` 接口
- "积分规则"按钮跳转 `/pages/tool/points-rules`
- "立即打卡"按钮跳转 `/pages/tool/checkin-publish`
- 任务列表展示,调用 `getCheckinTasks` 接口
- 兑换列表展示
- 底部提示 banner 展示
---
### 3.10 饮食打卡发布 (checkin-publish.vue)
**图片上传**
- 点击添加照片,打开相册/相机
- 最多上传 6 张图片
- 图片预览和删除功能
- 图片上传调用 `uploadFile` 接口
**餐次选择**
- 早餐/午餐/晚餐/加餐 选择器正常
**语音备注**
- 录音功能正常(同 AI 营养师)
- 语音识别结果填入备注
**AI 功能开关**
- AI 视频生成开关切换
- 开启后提交时调用 `createImageToVideoTask``createTextToVideoTask`
**提交**
- 点击"发布"调用 `submitCheckin` 接口(`POST tool/checkin/submit`
- 提交成功后跳转打卡详情或返回打卡首页
- 无图片时提示必须上传
- 防止重复提交
---
### 3.11 打卡详情 (checkin-detail.vue)
- 携带 `id` 参数进入,调用 `getCheckinDetail(id)`
- 图片轮播swiper正常底部图片计数器
- 餐次标签展示
- 作者信息、积分徽章展示
- 描述文本展示
- AI 营养分析卡片展示
- "分享到社区"按钮功能
---
### 3.12 一键借鉴打卡 (checkin-copy.vue)
- 原始记录卡片展示
- 照片网格选择(可勾选/取消勾选)
- 备注文本编辑
- AI 视频开关
- 取消/确认按钮
- 注意:当前 `loadOriginalRecord` 为 TODO需确认数据来源是否已接通
---
### 3.13 饮食记录 (dietary-records.vue)
- 自定义导航栏展示(`navigationStyle: "custom"`
- Tab 切换:全部 / 早餐 / 午餐 / 晚餐
- 调用 `getCheckinList` 接口,切换 Tab 传不同 `mealType`
- 列表项展示图片、餐次、备注、积分、AI 分析
- 上拉加载更多
- 空态展示(无记录时)
- 下拉刷新
---
### 3.14 社区帖子详情 (post-detail.vue)
**内容展示**
- 调用 `getCommunityDetail(id)` 获取帖子
- 图片轮播、餐次标签、标题、评分、描述、标签展示
- 作者信息区、关注按钮
- 营养统计数据展示
- AI 评论展示
**互动功能**
- 点赞:调用 `toggleLike`,状态切换正常
- 收藏:调用 `toggleCollect`,状态切换正常
- 关注:调用 `toggleFollow`,状态切换正常
- "一键借鉴打卡"按钮跳转 `/pages/tool/checkin-copy`
**评论功能**
- 调用 `getCommentList` 加载评论列表
- 底部输入框发表评论,调用 `addComment`
- 评论成功后列表刷新
**相关帖子**
- 调用 `getCommunityList` 展示相关帖子
- 点击相关帖子跳转对应详情
---
### 3.15 食谱详情 (recipe-detail.vue)
- 封面图展示
- 食谱信息(名称、难度徽章)展示
- 营养团队卡片展示
- 简介文本展示
- 营养数据宫格展示
- 膳食方案(早/午/晚)展示
- 注意事项列表展示
- 温馨提示展示
- 底部操作栏:点赞、收藏、打卡按钮
- 注意:当前 `loadRecipeData` 为 TODO需确认数据加载是否已接通
---
### 3.16 我的 (my-profile.vue)
- 用户头像、昵称展示
- 统计数据(打卡次数、积分、关注数)展示
- "我的健康" 区域展示
- "我的内容" 区域展示
- "工具与服务" 区域展示
- "设置" 区域展示
- 退出登录按钮功能
- 各导航项跳转正确
---
### 3.17 积分规则 (points-rules.vue)
- 页面为纯静态内容scroll-view 滚动正常
- 展示区域:赚取规则、使用方式、温馨提示、注意事项
- 文字排版在不同机型上无溢出/截断
---
### 3.18 邀请有礼 (invite-rewards.vue)
- 邀请卡片展示
- 奖励盒子列表展示
- 邀请按钮功能(触发分享或复制链接)
- 海报按钮功能
- 权益说明列表展示
- 邀请步骤展示
- 注意:当前为静态占位数据,确认是否有接口接通
---
### 3.19 会员福利 (welcome-gift.vue)
- 礼品 banner 展示
- 礼品项列表展示
- 二维码展示
- 领取步骤说明
- "添加微信" 按钮功能(复制微信号或打开二维码)
- 免责声明展示
---
## 四、通用测试项(每页必测)
### 4.1 页面基础
- 页面标题正确(与 pages.json 配置一致)
- 返回按钮功能正常
- 页面下拉不出现橡皮筋效果(或符合预期)
### 4.2 登录态
- 未登录访问需登录页面时,弹出登录引导
- 登录后自动返回原页面并刷新数据
### 4.3 网络异常
- 断网时接口请求给出友好提示
- 弱网环境下 loading 态不丢失
- 恢复网络后可重试/刷新
### 4.4 UI 适配
- iPhone SE / iPhone 14 Pro Max / 小屏安卓 / 平板 屏幕适配
- 安全区域(底部 Home Indicator适配
- 暗色模式适配(如支持)
- 长文本不溢出、不截断
### 4.5 性能
- 页面首屏加载时间 < 2s
- 列表页滚动流畅,无卡顿
- 图片懒加载正常
---
## 五、关键 API 接口清单
以下接口在前端测试中需确认可用:
**首页**
- `GET tool/home/recipes`
- `GET tool/home/knowledge`
- `GET tool/home/health-status`
**食谱计算器**
- `POST tool/calculator/calculate`
- `GET tool/calculator/result/{id}`
- `POST tool/calculator/adopt`
**AI 营养师** (通过 models-api.js)
- Coze 系列接口 (cozeChat / cozeRetrieveChat / cozeMessageList)
- ASR 接口 (createAsrTask / queryAsrStatus)
- 文件上传 (uploadFile)
**打卡**
- `POST tool/checkin/submit`
- `GET tool/checkin/list`
- `GET tool/checkin/detail/{id}`
- `GET tool/checkin/streak`
- `GET tool/checkin/tasks`
- `POST tool/checkin/copy`
- `POST tool/checkin/learn`
**食物百科**
- `GET tool/food/search`
- `GET tool/food/list`
- `GET tool/food/detail/{id}`
- `GET tool/food/similar/{id}`
**营养知识**
- `GET tool/knowledge/list`
- `GET tool/knowledge/detail/{id}`
- `GET tool/knowledge/nutrient/{name}`
**社区**
- `GET tool/community/detail/{id}`
- `GET tool/community/list`
- `POST tool/community/like`
- `POST tool/community/collect`
- `POST tool/community/follow`
- `POST tool/community/comment`
- `GET tool/community/comment/list/{postId}`
**积分**
- `GET tool/points/info`
- `GET tool/points/rules`
**文件上传**
- `POST tool/upload/image`
- `POST tool/upload/voice`
---
## 六、已知待完善项
以下页面有 TODO/未接通功能,测试时需标注为"受限测试"
- `checkin-copy.vue`: `loadOriginalRecord` 为 TODO数据加载待实现
- `food-detail.vue`: `loadFoodData` 为空实现,静态数据
- `recipe-detail.vue`: `loadRecipeData` 为 TODO静态数据
- `tool_main/index.vue`: "食谱列表" 入口仅 toast 提示开发中
- `invite-rewards.vue`: 邀请统计为静态占位数据

View File

@@ -0,0 +1,107 @@
---
name: 修复KieAI回调反序列化问题
overview: 修复 KieAI 回调接口的 JSON 反序列化错误,确保视频生成完成后能正确更新 article 表中的 video_url 和 status_task 字段
todos: []
isProject: false
---
# 修复 KieAI 回调反序列化问题
## 问题分析
通过日志分析发现:
```
Could not resolve parameter [0]... Problem deserializing 'setterless' property 'resultUrls':
get method returned null
```
KieAI 的回调请求已经到达后端 `/api/front/kieai/callback`,但是 Jackson 在反序列化 `KieAIQueryTaskResponse` 时失败,导致:
1. 回调处理方法 `handleCallback` 无法正常执行
2. `handleTaskCallback` 中更新 article 表的逻辑从未运行
3. 数据库中的 video_url 和 status_task 字段保持未更新状态
## 技术细节
**反序列化失败原因:**
- `KieAIQueryTaskResponse.TaskData` 类使用了 `@Data` 注解Lombok
- `resultUrls` 字段虽然有 getter但 Jackson 在反序列化嵌套的 List 类型时需要显式的 setter
- 当前的便捷方法 `getResultUrls()` 定义在父类,不满足 Jackson 对 `TaskData.resultUrls` 字段的反序列化要求
## 修复方案
### 文件:`[KieAIQueryTaskResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/kieai/KieAIQueryTaskResponse.java)`
**当前问题代码(第 73-113 行):**
```java
@Data
public static class TaskData implements Serializable {
// ... other fields ...
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls; // Lombok @Data 应该生成 setter但可能被优化掉了
// ... other fields ...
}
```
**修复方法:**
添加显式的 setter 方法到 `TaskData` 类中:
```java
@Data
public static class TaskData implements Serializable {
// ... existing fields ...
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls;
// 显式添加 setter 方法以确保 Jackson 反序列化正常工作
public void setResultUrls(java.util.List<String> resultUrls) {
this.resultUrls = resultUrls;
}
// ... other fields ...
}
```
**为什么这样修复:**
1. Lombok 的 `@Data` 注解理论上会自动生成 getter/setter但在某些情况下如嵌套泛型、继承关系等Jackson 可能无法正确识别
2. 显式声明 setter 方法可以确保 Jackson 在反序列化时能够正确填充 `resultUrls` 字段
3. 这不会与 Lombok 冲突,显式方法优先级更高
## 验证方法
修复后,当 KieAI 发送回调时:
1. 查看日志,应该看到:
```
接收KieAI任务回调任务ID: xxx
处理KieAI任务回调任务ID: xxx, 状态: success
找到article记录articleId: xxx, videoUrl: xxx, statusTask: 1
更新article视频信息成功
```
2. 数据库验证:
```sql
SELECT id, task_id, video_url, status_task FROM eb_article WHERE task_id = 'xxx';
```
应该看到 `video_url` 被正确填充,`status_task` 为 1成功或 2失败
3. 前端验证:
- 访问 `http://127.0.0.1:20822/api/front/tool/community/detail/302`
- 响应中应该包含 `videoUrl` 和 `hasVideo: true`
## 其他说明
**现有的回调处理逻辑是正确的:**
- `ToolKieAIServiceImpl.handleTaskCallback()` 方法(第 148-187 行)逻辑完整
- 通过 `taskId` 查询 article 记录
- 解析 `resultUrls` 并更新 `video_url` 和 `status_task`
- 日志记录完善
问题仅在于回调数据无法被正确反序列化,导致方法未被调用。

View File

@@ -0,0 +1,459 @@
---
name: 打卡视频生成与展示
overview: 实现打卡勾选生成视频后,在饮食记录页面定时查询视频任务状态并显示,在打卡详情支持视频播放,勾选生成视频的打卡帖子在社区最新tab显示
todos:
- id: backend-checkin-article
content: 修改打卡提交逻辑,勾选视频时创建eb_article记录存储taskId
status: completed
- id: backend-list-api
content: 更新打卡列表/详情接口,关联article表返回videoUrl和videoStatus
status: completed
- id: backend-callback
content: 完善KieAI回调处理,更新article的video_url和status_task
status: completed
- id: backend-community
content: 更新社区列表接口,关联打卡记录的视频信息
status: completed
- id: frontend-dietary-records
content: 饮食记录页面增加视频状态显示和定时轮询逻辑
status: completed
- id: frontend-detail-video
content: 打卡详情页增加视频播放器组件
status: completed
- id: frontend-community-badge
content: 社区页面卡片增加视频标记
status: completed
- id: frontend-api
content: 新增getVideoTaskStatus API方法
status: completed
isProject: false
---
# 打卡视频生成与展示功能实现计划
## 背景分析
根据代码探索,当前系统已具备:
- **打卡发布**: `[checkin-publish.vue](msh_single_uniapp/pages/tool/checkin-publish.vue)` 支持勾选"生成打卡视频分享到社区",会调用KieAI创建视频任务并返回 `taskId`
- **数据存储**: `eb_user_sign` 表存储 `task_id``enable_ai_video` 字段,`eb_article` 表存储视频URL和任务状态
- **视频生成**: 通过 `[ToolSora2Service](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java)` 调用KieAI API
- **状态查询接口**: `GET /api/front/kieai/video/task/{taskId}` 已存在
**缺失功能**:
1. 打卡时未创建 `eb_article` 记录来存储视频
2. 饮食记录页面不显示视频任务状态
3. 打卡详情页不支持视频播放
4. 社区帖子未关联视频信息
## 实现方案
### 数据库层面
#### 1. 修改表结构
**eb_user_sign表**: 已有 `task_id`, `enable_ai_video` 字段,无需修改
**v2_community_posts表**: 已有 `video_url` 字段(从SQL看到),需确认Java实体类是否包含
**eb_article表**: 已有 `video_url`, `task_id`, `status_task`, `check_in_record_id` 字段
#### 2. 数据流设计
```mermaid
flowchart TD
A[用户勾选生成视频并发布打卡] --> B[前端:调用KieAI创建视频任务]
B --> C[前端:获得taskId]
C --> D[前端:提交打卡 带taskId+enableAIVideo]
D --> E[后端:保存eb_user_sign记录]
E --> F[后端:创建eb_article记录]
F --> G[eb_article: taskId, statusTask=0, checkInRecordId]
H[饮食记录页面] --> I[加载打卡列表API]
I --> J[返回 taskId, enableAIVideo, videoUrl]
J --> K[前端:检测有taskId且无videoUrl]
K --> L[启动定时轮询 /kieai/video/task/taskId]
L --> M{任务状态?}
M -->|进行中| N[显示生成中状态]
M -->|完成| O[更新videoUrl到article]
M -->|失败| P[显示失败状态]
O --> Q[页面刷新后显示视频]
R[打卡详情页] --> S[查询详情API]
S --> T[返回taskId+videoUrl]
T --> U{有videoUrl?}
U -->|是| V[显示video标签播放]
U -->|否+有taskId| W[显示生成中状态]
X[社区最新tab] --> Y[查询v2_community_posts]
Y --> Z[关联eb_user_sign.enableAIVideo]
Z --> AA[显示有视频标记的帖子]
```
### 后端改动
#### 1. 更新打卡提交逻辑
`[ToolCheckinServiceImpl.submit()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)`
- 在打卡成功后,如果 `enableAIVideo=1` 且有 `taskId`,创建 `eb_article` 记录:
- `type = 2` (视频类型)
- `task_id = taskId`
- `status_task = 0` (已创建)
- `check_in_record_id = userSign.id`
- `title` = 打卡备注或默认标题
- 其他字段按现有逻辑填充
#### 2. 更新打卡列表接口
`[ToolCheckinServiceImpl.getList()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)`
- 查询时关联 `eb_article` 表(通过 `eb_user_sign.task_id = eb_article.task_id`)
- 返回字段增加:
- `videoUrl`: 从 `eb_article.video_url` 获取
- `videoStatus`: 从 `eb_article.status_task` 获取 (0=生成中, 1=完成, 2=失败)
- `taskId`: 已有
- `enableAIVideo`: 已有
#### 3. 更新打卡详情接口
`[ToolCheckinServiceImpl.getDetail()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)`
- 增加返回 `videoUrl``videoStatus` 字段
#### 4. 完善KieAI回调处理
`[KieAIController.handleCallback()](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java)`
- 当前只记录日志,需改为:
- 解析回调参数获取 `taskId`, `state`, `result_urls`
- 根据 `taskId` 更新 `eb_article` 表:
- `status_task = 1` (成功) 或 `2` (失败)
- `video_url = result_urls[0]`
参考 `models-integration` 项目的实现:
```java
articleMapper.updateVideoUrlAndTaskStatusByTaskId(taskId, videoUrl, statusTask);
```
#### 5. 更新社区列表接口
`[ToolCommunityServiceImpl.getList()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java)`
- 当前已关联 `eb_user_sign` 获取 `mealType`
- 同样方式关联获取 `enable_ai_video``task_id`
- 通过 `task_id` 关联 `eb_article` 获取 `video_url`
- 返回字段增加:
- `hasVideo`: boolean (是否有视频)
- `videoUrl`: 视频地址
- `enableAIVideo`: 是否启用了视频生成
#### 6. 更新V2CommunityPost实体类
`[V2CommunityPost.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2CommunityPost.java)`
- 确认是否已有 `videoUrl` 字段,若无则添加:
```java
@ApiModelProperty(value = "视频地址")
private String videoUrl;
```
### 前端改动
#### 1. 饮食记录页面增加视频状态显示
`[dietary-records.vue](msh_single_uniapp/pages/tool/dietary-records.vue)`
**显示逻辑**:
- 列表项增加视频状态角标:
-`videoUrl`: 显示"📹视频"标记
-`taskId` 但无 `videoUrl`: 显示"⏳生成中"或进度条
- `videoStatus = 2`: 显示"❌生成失败"
**轮询逻辑**:
```javascript
data() {
return {
pollingTasks: [], // 需要轮询的任务列表
pollingTimer: null
}
},
methods: {
// 启动轮询
startPolling() {
if (this.pollingTimer) return;
this.pollingTimer = setInterval(() => {
this.pollVideoTasks();
}, 5000); // 每5秒查询一次
},
// 轮询视频任务状态
async pollVideoTasks() {
const tasksToCheck = this.recordList.filter(item =>
item.enableAIVideo && item.taskId && !item.videoUrl
);
if (tasksToCheck.length === 0) {
this.stopPolling();
return;
}
for (const task of tasksToCheck) {
try {
const res = await getVideoTaskStatus(task.taskId);
if (res.code === 200 && res.data) {
const status = res.data.state;
if (status === 'success') {
// 刷新列表以获取最新视频URL
this.loadRecordList();
break;
} else if (status === 'failed') {
// 标记失败
task.videoStatus = 2;
}
}
} catch (error) {
console.error('查询视频任务失败:', error);
}
}
},
// 停止轮询
stopPolling() {
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
}
}
```
**生命周期**:
```javascript
onLoad() {
this.loadRecordList();
this.startPolling();
},
onUnload() {
this.stopPolling();
},
onShow() {
// 从其他页面返回时重新检查
this.loadRecordList();
this.startPolling();
}
```
**模板增加视频状态标记**:
```vue
<view class="video-status-badge" v-if="item.enableAIVideo">
<text v-if="item.videoUrl" class="status-success">📹 有视频</text>
<text v-else-if="item.videoStatus === 2" class="status-failed"> 生成失败</text>
<text v-else class="status-pending"> 生成中</text>
</view>
```
#### 2. 打卡详情页增加视频播放
`[checkin-detail.vue](msh_single_uniapp/pages/tool/checkin-detail.vue)`
**数据结构更新**:
```javascript
data() {
return {
checkinData: {
// ...现有字段
videoUrl: '',
taskId: '',
enableAIVideo: false,
videoStatus: 0
}
}
}
```
**模板增加视频播放器**:
```vue
<!-- 在图片轮播区域下方或上方增加 -->
<view class="video-section" v-if="checkinData.videoUrl">
<view class="video-header">
<text class="video-icon">🎬</text>
<text class="video-title">打卡视频</text>
</view>
<video
class="checkin-video"
:src="checkinData.videoUrl"
controls
:show-center-play-btn="true"
:enable-progress-gesture="true"
object-fit="contain"
></video>
</view>
<!-- 视频生成中状态 -->
<view class="video-generating" v-else-if="checkinData.enableAIVideo && checkinData.taskId">
<text class="generating-icon"></text>
<text class="generating-text">视频生成中,请稍后...</text>
</view>
```
**formatCheckinData更新**:
```javascript
formatCheckinData(item) {
// ...现有逻辑
this.checkinData = {
// ...现有字段
videoUrl: item.videoUrl || '',
taskId: item.taskId || '',
enableAIVideo: item.enableAIVideo || false,
videoStatus: item.videoStatus || 0
};
}
```
#### 3. 社区页面显示视频标记
`[community.vue](msh_single_uniapp/pages/tool_main/community.vue)`
**formatPostList更新**:
```javascript
formatPostList(list) {
return list.map(item => {
// ...现有逻辑
return {
// ...现有字段
hasVideo: item.hasVideo || false,
videoUrl: item.videoUrl || '',
enableAIVideo: item.enableAIVideo || false
};
});
}
```
**卡片增加视频标记**:
```vue
<view class="post-card">
<!-- 现有内容 -->
<!-- 视频标记 -->
<view class="video-badge" v-if="item.hasVideo || item.videoUrl">
<text class="badge-icon">🎬</text>
<text class="badge-text">视频</text>
</view>
</view>
```
**样式**:
```scss
.video-badge {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.6);
border-radius: 12px;
padding: 4px 10px;
display: flex;
align-items: center;
gap: 4px;
.badge-icon {
font-size: 14px;
}
.badge-text {
font-size: 12px;
color: #fff;
}
}
```
#### 4. 新增API方法
`[tool.js](msh_single_uniapp/api/tool.js)`
```javascript
/**
* 查询视频任务状态
* @param {String} taskId - 任务ID
*/
export function getVideoTaskStatus(taskId) {
return request.get(`kieai/video/task/${taskId}`, {
apiKey: 'your-api-key' // 从配置获取
});
}
```
### 关键技术细节
#### 1. 轮询策略
- **触发条件**: 列表中存在 `enableAIVideo=true``taskId` 不为空但 `videoUrl` 为空的记录
- **轮询间隔**: 5秒
- **停止条件**:
- 所有任务都有 `videoUrl``videoStatus=2`
- 页面销毁(onUnload)
- 超过最大轮询次数(如60次,即5分钟)
- **优化**: 只轮询当前可见列表中的任务,避免全量查询
#### 2. 视频播放兼容性
- 使用uni-app的 `<video>` 组件,支持小程序、H5、APP
- 视频格式要求: mp4 (KieAI返回格式)
- 封面图: 可使用打卡的第一张照片作为视频封面
#### 3. 错误处理
- **视频生成失败**: 显示失败状态,允许用户重新生成或查看原因
- **网络错误**: 轮询失败时静默处理,不影响用户浏览
- **回调失败**: 前端轮询作为兜底方案
## 文件清单
### 后端Java文件
| 文件 | 改动类型 | 说明 |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -------------------------------------------- |
| `[ToolCheckinServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)` | 修改 | submit增加article创建,getList/getDetail增加video字段 |
| `[KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java)` | 修改 | 完善回调处理更新article表 |
| `[ToolKieAIServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKieAIServiceImpl.java)` | 修改 | handleTaskCallback增加更新article逻辑 |
| `[ToolCommunityServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java)` | 修改 | getList关联video信息 |
| `[V2CommunityPost.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2CommunityPost.java)` | 检查/修改 | 确认有videoUrl字段 |
| `[ArticleDao.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/dao/ArticleDao.java)` | 新增方法 | 增加updateVideoUrlByTaskId方法 |
### 前端Vue文件
| 文件 | 改动类型 | 说明 |
| ------------------------------------------------------------------------- | ---- | ------------------------ |
| `[dietary-records.vue](msh_single_uniapp/pages/tool/dietary-records.vue)` | 修改 | 增加视频状态显示和轮询逻辑 |
| `[checkin-detail.vue](msh_single_uniapp/pages/tool/checkin-detail.vue)` | 修改 | 增加视频播放器 |
| `[community.vue](msh_single_uniapp/pages/tool_main/community.vue)` | 修改 | 增加视频标记 |
| `[tool.js](msh_single_uniapp/api/tool.js)` | 新增方法 | 增加getVideoTaskStatus API |
## 测试要点
1. **打卡流程**: 勾选生成视频 → 发布 → 验证taskId保存 → 验证article记录创建
2. **状态轮询**: 饮食记录页面显示"生成中" → 等待KieAI完成 → 自动更新显示"有视频"
3. **视频播放**: 打卡详情查看视频 → 播放正常 → 控制条功能正常
4. **社区显示**: 最新tab显示有视频标记的帖子 → 点击查看带视频
5. **异常处理**: 视频生成失败 → 显示失败状态 → 不影响打卡记录查看
6. **回调机制**: KieAI回调成功 → article表正确更新 → 前端轮询立即获取到结果

View File

@@ -0,0 +1,166 @@
---
name: 计算器结果保存食谱
overview: 在用户采纳营养计划时,将食谱计算器的配餐方案提取并保存到 v2_recipes 食谱表,并在首页"精选食谱"中混合展示用户自己的食谱(排在前面)。
todos:
- id: db-migration
content: 新建 SQL 变更文件v2_recipes 表新增 source 和 source_id 字段
status: completed
- id: entity-update
content: V2Recipe.java 实体类新增 source 和 sourceId 属性
status: completed
- id: adopt-save-recipe
content: ToolCalculatorServiceImpl.adopt() 中新增保存食谱到 v2_recipes 的逻辑(含幂等检查)
status: completed
- id: home-mix-recipes
content: ToolHomeServiceImpl.getRecommendedRecipes() 混入当前用户的计算器食谱
status: completed
- id: home-return-fields
content: 推荐食谱接口返回字段补充 description、source、totalProtein
status: completed
- id: frontend-recipe-card
content: 首页 index.vue 食谱卡片展示适配tag、desc 等字段映射)
status: completed
isProject: false
---
# 食谱计算器结果保存到食谱表
## 整体流程
```mermaid
sequenceDiagram
participant User as 用户
participant FE as 前端
participant Ctrl as ToolController
participant CalcSvc as ToolCalculatorServiceImpl
participant RecipeDao as V2RecipeDao
participant HomeSvc as ToolHomeServiceImpl
User->>FE: 点击"采纳计划"
FE->>Ctrl: POST /calculator/adopt
Ctrl->>CalcSvc: adopt(resultId)
CalcSvc->>CalcSvc: 创建营养计划 (已有逻辑)
CalcSvc->>RecipeDao: 保存一条食谱到 v2_recipes
CalcSvc-->>FE: 返回 adoptResponse
Note over FE: 首页刷新时
FE->>Ctrl: GET /home/recipes
Ctrl->>HomeSvc: getRecommendedRecipes()
HomeSvc->>RecipeDao: 查询推荐食谱 + 用户自己的食谱
HomeSvc-->>FE: 返回混合列表
```
## 1. 数据库变更 - v2_recipes 新增字段
需要新增两个字段以追踪食谱来源:
```sql
ALTER TABLE v2_recipes
ADD COLUMN source VARCHAR(20) DEFAULT 'manual' COMMENT '来源manual(手动)/calculator(计算器)/ai(AI生成)' AFTER sort_order,
ADD COLUMN source_id BIGINT DEFAULT NULL COMMENT '来源ID如计算器结果ID' AFTER source;
```
同时新增一个变更 SQL 文件记录此次 DDL。
## 2. 后端 - 实体类更新
**文件**: [V2Recipe.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Recipe.java)
新增两个字段:
```java
@ApiModelProperty(value = "来源manual/calculator/ai")
private String source;
@ApiModelProperty(value = "来源ID计算器结果ID等")
private Long sourceId;
```
## 3. 后端 - 采纳时保存食谱
**文件**: [ToolCalculatorServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)
`adopt()` 方法中,步骤 5创建营养计划之后新增步骤"保存食谱到 v2_recipes"
- 注入 `V2RecipeDao`
-`V2CalculatorResult` 中解析 `mealPlanJson` 得到 MealPlan
- 构建 `V2Recipe` 对象:
- `userId` = 当前用户ID
- `name` = "每日营养配餐 - {ckdStage}",如 "每日营养配餐 - CKD 5期"
- `description` = "蛋白质 {proteinIntake}g/天 | 能量 {energyIntake}kcal/天"
- `coverImage` = 取午餐第一道菜的图片 URL最具代表性
- `mealType` = null整日配餐非单餐
- `category` = "营养配餐"
- `tagsJson` = `["AI配餐", "{ckdStage}"]`
- `ingredientsJson` = 聚合三餐所有食材
- `stepsJson` = 存放完整 mealPlanJson早/午/晚餐详情),复用此字段存储配餐详情
- `totalProtein` = result.getProteinIntake()
- `totalEnergy` = result.getEnergyIntake()
- `suitableStagesJson` = `["{ckdStage}"]`
- `suitableDialysis` = result.getHasDialysis()
- `status` = "published"
- `isRecommend` = 0不进入官方推荐通过 source + userId 查询)
- `isOfficial` = 0
- `source` = "calculator"
- `sourceId` = resultId
- 幂等处理:先检查是否已存在 `source='calculator' AND source_id=resultId` 的记录,避免重复
## 4. 后端 - 首页推荐食谱混入用户食谱
**文件**: [ToolHomeServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolHomeServiceImpl.java)
修改 `getRecommendedRecipes()` 方法:
1. 如果用户已登录,先查询该用户的食谱(`user_id = currentUserId AND status = 'published' AND source = 'calculator'`,按 `created_at DESC`,取最新 1 条)
2. 再查询官方推荐食谱(现有逻辑,`is_recommend = 1`
3. 将用户食谱排在前面,官方推荐排在后面,合并后返回
4. 总数仍控制在 `limit` 范围内(如用户有 1 条自己的食谱,则官方推荐取 limit-1 条)
5. 缓存 key 需要区分用户(登录用户的缓存 key 加上 userId
## 5. 后端 - 推荐食谱接口返回字段补充
**文件**: [ToolHomeServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolHomeServiceImpl.java)
在返回数据 map 中增加字段,使前端能够区分并展示更多信息:
```java
map.put("id", recipe.getRecipeId());
map.put("name", recipe.getName());
map.put("coverImage", recipe.getCoverImage());
map.put("totalEnergy", recipe.getTotalEnergy());
map.put("description", recipe.getDescription()); // 新增
map.put("source", recipe.getSource()); // 新增:标记来源
map.put("totalProtein", recipe.getTotalProtein()); // 新增
```
## 6. 前端 - 首页食谱展示适配
**文件**: [index.vue (首页)](msh_single_uniapp/pages/tool_main/index.vue)
调整食谱卡片展示逻辑:
- `item.tag`:如果 `source === 'calculator'` 显示 "我的配餐",否则显示 "推荐"
- `item.tagClass`:根据来源使用不同样式(如用户食谱用不同颜色)
- `item.desc`:使用 `description` 字段(如 "蛋白质 75.6g/天 | 能量 2205kcal/天"
- `item.time`:可不显示或显示 "每日配餐"
- `item.views`:使用 `viewCount` 或隐藏
修改 `loadData` 中对 `recipeList` 的处理逻辑,将后端返回的数据映射为前端需要的格式。
## 7. 前端 - 食谱详情页适配
点击首页食谱卡片跳转到 `recipe-detail` 页面,需要确认该页面能正确显示来自计算器的食谱内容(特别是 `stepsJson` 中存储的 mealPlan 数据)。如果需要特殊处理,在详情页根据 `source` 字段做渲染分支。
## 涉及文件清单
| 层级 | 文件 | 改动 |
| -------- | --------------------------------------------- | ------------------------------ |
| SQL | `docs/sql/v2_recipes_add_source.sql`(新建) | 新增 source、source_id 字段 |
| Entity | `V2Recipe.java` | 新增 source、sourceId 字段 |
| Service | `ToolCalculatorServiceImpl.java` | adopt() 中新增保存食谱逻辑 |
| Service | `ToolHomeServiceImpl.java` | getRecommendedRecipes() 混入用户食谱 |
| Frontend | `msh_single_uniapp/pages/tool_main/index.vue` | 食谱卡片展示适配 |

View File

@@ -0,0 +1,256 @@
---
name: ""
overview: ""
todos: []
isProject: false
---
# 诊断article.status_task异常和video_url未更新问题
## 问题描述
**问题1**提交饮食打卡记录时article表中的status_task字段保持为0或变成2而不是预期的1完成
**问题2**article表中的video_url字段始终为NULL即使视频已经生成完成
**关键信息**
- KieAI回调一般需要10秒以上才到达不会立即回调
- 两个问题有关联,都指向**KieAI回调没有正常执行或执行失败**
## 根因分析
### 核心问题KieAI回调未正常执行
根据分析,最可能的原因是:
**原因1回调反序列化失败**(最可能)
根据[修复kieai回调反序列化问题计划](修复kieai回调反序列化问题_85d41db5.plan.md)KieAI回调到达后端但Jackson反序列化失败
```
Could not resolve parameter [0]... Problem deserializing 'setterless' property 'resultUrls':
get method returned null
```
这导致:
1. `handleCallback` 方法无法执行
2. `handleTaskCallback` 中更新article的逻辑从未运行
3. `video_url``status_task` 字段保持未更新状态
**原因2回调URL配置错误**
- `kieAIConfig.getApiCallbackUrl()` 可能未配置或配置错误
- KieAI服务无法访问回调地址网络问题、防火墙等
- 回调地址格式不正确
**原因3回调数据格式问题**
- KieAI返回的数据结构与`KieAIQueryTaskResponse`不匹配
- `resultUrls`字段名称或格式异常
## 执行流程分析
### 预期流程(正常情况)
```mermaid
sequenceDiagram
participant Frontend as 前端
participant Sora2 as ToolSora2ServiceImpl
participant KieAI as KieAI服务
participant Callback as KieAIController
participant Service as ToolKieAIServiceImpl
participant DB as 数据库
Frontend->>Sora2: createImageToVideoTask
Sora2->>KieAI: POST createTask含callBackUrl
KieAI-->>Sora2: 返回taskId
Sora2->>DB: INSERT article status_task=0
Note over DB: status_task = 0
Note over KieAI: 10秒以上视频生成中
KieAI->>Callback: POST callback含resultUrls
Callback->>Callback: 反序列化KieAIQueryTaskResponse
Callback->>Service: handleTaskCallback
Service->>DB: SELECT article WHERE task_id
Service->>Service: extractVideoUrl
Service->>DB: UPDATE video_url status_task=1
Note over DB: video_url有值 status_task=1
```
### 实际流程(异常情况)
```mermaid
sequenceDiagram
participant Frontend as 前端
participant Sora2 as ToolSora2ServiceImpl
participant KieAI as KieAI服务
participant Callback as KieAIController
participant DB as 数据库
Frontend->>Sora2: createImageToVideoTask
Sora2->>KieAI: POST createTask
KieAI-->>Sora2: 返回taskId
Sora2->>DB: INSERT article status_task=0
Note over DB: status_task = 0
Note over KieAI: 10秒以上...
KieAI->>Callback: POST callback
Callback--xCallback: 反序列化失败
Note over Callback: Jackson报错setterless property resultUrls
Note over DB: video_url仍为NULL status_task仍为0
Note over Frontend,DB: 回调处理逻辑从未执行数据库未更新
```
## 解决方案
### 方案1修复回调反序列化问题主要修复
**文件**[KieAIQueryTaskResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/kieai/KieAIQueryTaskResponse.java)
**问题代码**TaskData类
```java
@Data
public static class TaskData implements Serializable {
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls; // Lombok生成的setter可能被Jackson忽略
}
```
**修复方法**显式添加setter方法
```java
@Data
public static class TaskData implements Serializable {
@ApiModelProperty(value = "结果URL数组视频/图片地址)")
private java.util.List<String> resultUrls;
// 显式添加setter以确保Jackson反序列化正常
public void setResultUrls(java.util.List<String> resultUrls) {
this.resultUrls = resultUrls;
}
}
```
### 方案2验证回调URL配置
**检查配置文件**application.yml或application.properties
```yaml
kie-ai:
api-callback-url: http://your-domain.com/api/front/kieai/callback
```
**验证要点**
1. URL必须是KieAI服务可访问的公网地址
2. 确保没有防火墙阻止
3. 路径正确:`/api/front/kieai/callback`
### 方案3添加详细日志辅助调试
在[KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java)的回调方法开头添加:
```java
@PostMapping("/callback")
public KieAINanoBananaResponse<String> handleCallback(
@RequestBody KieAIQueryTaskResponse callbackData) {
try {
logger.info("======== KieAI回调原始数据 ========");
logger.info("完整对象: {}", JSON.toJSONString(callbackData));
logger.info("taskId: {}", callbackData.getTaskId());
logger.info("state: {}", callbackData.getState());
logger.info("resultUrls: {}", callbackData.getResultUrls());
String taskId = callbackData.getTaskId();
logger.info("接收KieAI任务回调任务ID: {}", taskId);
toolKieAIService.handleTaskCallback(taskId, callbackData);
return KieAINanoBananaResponse.success("回调处理成功");
} catch (Exception e) {
logger.error("处理任务回调失败", e);
return KieAINanoBananaResponse.fail("回调处理失败");
}
}
```
## 诊断和修复步骤
### 步骤1修复反序列化问题优先级最高
1. 修改`KieAIQueryTaskResponse.java`中的`TaskData`显式添加setter
2. 重启后端服务
3. 提交一次打卡测试
### 步骤2检查后端日志
**如果回调仍未执行**,检查日志:
1. 搜索:`接收KieAI任务回调`
- 找到:回调到达了,检查反序列化错误
- 没找到回调未到达检查回调URL配置
2. 搜索:`deserializing``Jackson``Could not resolve`
- 找到:确认是反序列化问题
3. 查看article创建日志
```
文章新增成功, taskId: xxx
```
### 步骤3验证回调URL配置
1. 检查`application.yml`中的`kie-ai.api-callback-url`
2. 确认URL可从外网访问
3. 测试回调接口是否正常响应
### 步骤4验证修复效果
提交打卡后,检查数据库:
```sql
SELECT id, task_id, video_url, status_task, create_time, update_time
FROM eb_article
WHERE task_id = 'your_task_id'
ORDER BY create_time DESC
LIMIT 1;
```
**预期结果**
- `status_task = 1`(任务完成)
- `video_url` 有值
- `update_time` 在create_time的10秒以上后更新
## 问题总结
### 核心原因
KieAI回调无法被正确处理导致
1. `video_url`未更新始终为NULL
2. `status_task`未更新保持为0
### 最可能的原因
根据[修复kieai回调反序列化问题计划](修复kieai回调反序列化问题_85d41db5.plan.md)Jackson反序列化失败导致回调处理方法无法执行。
### 修复优先级
1. **优先级1**:修复`KieAIQueryTaskResponse.TaskData`反序列化添加显式setter
2. **优先级2**验证回调URL配置
3. **优先级3**:添加详细日志
## 涉及文件
- [KieAIQueryTaskResponse.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/response/kieai/KieAIQueryTaskResponse.java) - 主要修复
- [KieAIController.java](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java) - 回调入口
- [ToolKieAIServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKieAIServiceImpl.java) - 回调处理
- [ToolSora2ServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java) - article创建

View File

@@ -0,0 +1,91 @@
---
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
```