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:
12
.cursor/debug.log
Normal file
12
.cursor/debug.log
Normal 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}
|
||||
111
.cursor/plans/egfr_fix_and_video_gen_9021ff8f.plan.md
Normal file
111
.cursor/plans/egfr_fix_and_video_gen_9021ff8f.plan.md
Normal 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.
|
||||
|
||||
475
.cursor/plans/mealplan_image_ai_generation_e8e123a0.plan.md
Normal file
475
.cursor/plans/mealplan_image_ai_generation_e8e123a0.plan.md
Normal 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 相关 DTO(TextToImageInput, CreateTaskRequest, CreateTaskResponse, QueryTaskResponse, NanoBananaRequest, NanoBananaResponse 等)
|
||||
status: completed
|
||||
- id: replicate-kieai-helper
|
||||
content: 在 crmeb-service 中复刻 NanoBananaHelper(API 调用工具类)
|
||||
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: 实现 DishImageService:URL 检测 + 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 检查原始 URL(3秒超时)
|
||||
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/**` |
|
||||
|
||||
|
||||
193
.cursor/plans/migrate_apis_to_crmeb-front_e6251166.plan.md
Normal file
193
.cursor/plans/migrate_apis_to_crmeb-front_e6251166.plan.md
Normal 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
|
||||
|
||||
559
.cursor/plans/tool_module_test_plan_4894ed78.plan.md
Normal file
559
.cursor/plans/tool_module_test_plan_4894ed78.plan.md
Normal 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`: 邀请统计为静态占位数据
|
||||
|
||||
107
.cursor/plans/修复kieai回调反序列化问题_85d41db5.plan.md
Normal file
107
.cursor/plans/修复kieai回调反序列化问题_85d41db5.plan.md
Normal 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`
|
||||
- 日志记录完善
|
||||
|
||||
问题仅在于回调数据无法被正确反序列化,导致方法未被调用。
|
||||
459
.cursor/plans/打卡视频生成与展示_12778af3.plan.md
Normal file
459
.cursor/plans/打卡视频生成与展示_12778af3.plan.md
Normal 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表正确更新 → 前端轮询立即获取到结果
|
||||
|
||||
166
.cursor/plans/计算器结果保存食谱_f149b3dc.plan.md
Normal file
166
.cursor/plans/计算器结果保存食谱_f149b3dc.plan.md
Normal 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` | 食谱卡片展示适配 |
|
||||
|
||||
|
||||
256
.cursor/plans/诊断article_status_task异常问题_6bd03fd4.plan.md
Normal file
256
.cursor/plans/诊断article_status_task异常问题_6bd03fd4.plan.md
Normal 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创建
|
||||
|
||||
91
.cursor/plans/食谱百科食物图ai生成与oss更新_b5228ab9.plan.md
Normal file
91
.cursor/plans/食谱百科食物图ai生成与oss更新_b5228ab9.plan.md
Normal 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
|
||||
```
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user