2026-03-22 10:18:18 +08:00
|
|
|
|
# 测试问题分析报告
|
|
|
|
|
|
|
|
|
|
|
|
> **日期:** 2026-03-22
|
|
|
|
|
|
> **分析人:** Claude AI
|
|
|
|
|
|
> **项目:** 民生汇 - 慢性肾病营养管理小程序
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 一、食谱计算器 —— 用油量计算逻辑错误
|
|
|
|
|
|
|
|
|
|
|
|
### 问题描述
|
|
|
|
|
|
|
|
|
|
|
|
食谱计算器计算出的用油量偏多,不符合实际膳食指导标准。
|
|
|
|
|
|
|
|
|
|
|
|
### 问题定位
|
|
|
|
|
|
|
|
|
|
|
|
**文件:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java`
|
|
|
|
|
|
**位置:** `generateFoodPortions()` 方法,第 516 行
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 当前代码(有误)
|
|
|
|
|
|
list.add(createFoodPortion(7, "油脂类10g", round(5.7 * energyRatio)));
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 根因分析
|
|
|
|
|
|
|
|
|
|
|
|
油脂类的份数计算使用了系数 `5.7`,与第 510 行谷薯类的系数完全相同:
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
list.add(createFoodPortion(1, "谷薯50g", round(5.7 * energyRatio))); // 谷薯类
|
|
|
|
|
|
list.add(createFoodPortion(7, "油脂类10g", round(5.7 * energyRatio))); // 油脂类(错误地使用了同一系数)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
其中 `energyRatio = energy / 2000.0`。以标准体重 60kg 患者为例:
|
|
|
|
|
|
|
|
|
|
|
|
- 每日能量目标 = 60 × 35 = 2100 kcal
|
|
|
|
|
|
- energyRatio = 2100 / 2000 = 1.05
|
|
|
|
|
|
- 油脂份数 = 5.7 × 1.05 ≈ **6.0 份**
|
|
|
|
|
|
- 每份 10g,即每天需要 **60g 油**
|
|
|
|
|
|
|
|
|
|
|
|
根据《中国居民膳食指南》和《慢性肾脏病患者膳食指导(2017)》,CKD 患者每日食用油推荐量为 **25-30g**,当前计算结果严重偏高。
|
|
|
|
|
|
|
|
|
|
|
|
### 修复建议
|
|
|
|
|
|
|
|
|
|
|
|
将油脂类的计算系数从 `5.7` 调整为 `2.5`:
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 修复后
|
|
|
|
|
|
list.add(createFoodPortion(7, "油脂类10g", round(2.5 * energyRatio)));
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
修复后以同一患者为例:2.5 × 1.05 ≈ 2.6 份,即每日约 26g 油,符合膳食指南要求。
|
|
|
|
|
|
|
|
|
|
|
|
### 优先级:高
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 二、AI 营养师 —— 反应慢、内容凌乱、体验差
|
|
|
|
|
|
|
|
|
|
|
|
### 问题描述
|
|
|
|
|
|
|
|
|
|
|
|
AI 营养师功能反应比较慢,提供的内容比较凌乱,逻辑性差,整体体验感不好。
|
|
|
|
|
|
|
|
|
|
|
|
### 问题定位
|
|
|
|
|
|
|
|
|
|
|
|
**后端文件:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolAiNutritionistServiceImpl.java`
|
|
|
|
|
|
**前端文件:** `msh_single_uniapp/pages/tool/ai-nutritionist.vue`
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**Coze服务:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCozeServiceImpl.java`
|
|
|
|
|
|
**Coze控制器:** `msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/CozeController.java`
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
### 根因分析
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 1. 后端 AI 服务使用 Mock,未调用 Coze API(核心原因)
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
后端 `ToolAiNutritionistServiceImpl.sendMessage()` 方法(第 89-97 行)使用的是 **Mock 模拟回复**:
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// Mock AI response
|
|
|
|
|
|
message.setAiResponse("这是一个模拟的AI回复。");
|
|
|
|
|
|
message.setAiResponseStatus("success");
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
项目中已存在完整可用的 Coze API 对接服务 `ToolCozeServiceImpl`,提供了以下现成能力:
|
|
|
|
|
|
- `chat()` — 非流式对话(适用于简单问答)
|
|
|
|
|
|
- `chatStream()` — SSE 流式对话(适用于长回复的实时输出)
|
|
|
|
|
|
- `retrieveChat()` / `listMessages()` — 查询对话状态和消息列表
|
|
|
|
|
|
- `uploadFile()` — 上传图片/文件到 Coze
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
后端配置已就绪:Bot ID `7591133240535449654`(AI 营养师机器人),API 地址 `https://api.coze.cn`,支持 PAT 和 JWT 两种认证方式。**但 `sendMessage()` 中并未调用这些服务。**
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 2. 前端已对接 Coze,但走了两条冗余路径
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
前端 `ai-nutritionist.vue` 实际上已编写了 Coze 调用逻辑,但存在两条并行路径导致混乱:
|
|
|
|
|
|
- **路径 A(文本消息)**:调用 KieAI Gemini (`api.kieaiGeminiChat()`) 进行多模态对话
|
|
|
|
|
|
- **路径 B(图片消息)**:调用 Coze API (`api.cozeChat()`) 并通过轮询 `api.cozeRetrieveChat()` 获取结果(每 1 秒轮询,最多 60 次)
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
两条路径的响应格式、错误处理、消息展示逻辑各不相同,导致用户体验不一致。文本走 KieAI 时响应速度取决于 Gemini 接口,且非流式返回;图片走 Coze 时需要轮询等待,延迟更大。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 3. 缺乏统一的流式响应体验
|
|
|
|
|
|
|
|
|
|
|
|
后端 `CozeController` 已提供 SSE 流式端点 `POST /api/front/coze/chat/stream`,`ToolCozeServiceImpl.chatStream()` 通过 RxJava Observable 实现了 60 秒超时的 SSE 推送。但前端并未使用此端点,而是采用非流式调用 + 轮询的方式,无法实现"逐字输出"效果。
|
|
|
|
|
|
|
|
|
|
|
|
### 修复方案
|
|
|
|
|
|
|
|
|
|
|
|
**核心策略:统一使用 Coze API 作为 AI 营养师的唯一对话服务,利用已有的 SSE 流式通道提升响应速度。**
|
|
|
|
|
|
|
|
|
|
|
|
#### 步骤 1:后端 — 将 `sendMessage()` 对接 Coze 服务
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// ToolAiNutritionistServiceImpl.java — 修改 sendMessage() 方法
|
|
|
|
|
|
@Autowired
|
|
|
|
|
|
private ToolCozeServiceImpl cozeService;
|
|
|
|
|
|
|
|
|
|
|
|
// 替换 Mock 逻辑,调用 Coze chat
|
|
|
|
|
|
CozeChatRequest chatRequest = new CozeChatRequest();
|
|
|
|
|
|
chatRequest.setBotId("7591133240535449654");
|
|
|
|
|
|
chatRequest.setUserId(String.valueOf(userId));
|
|
|
|
|
|
chatRequest.setContent(message.getContent());
|
|
|
|
|
|
chatRequest.setConversationId(conversationId.toString());
|
|
|
|
|
|
|
|
|
|
|
|
CreateChatResp resp = cozeService.chat(chatRequest);
|
|
|
|
|
|
message.setAiResponse(resp.getContent());
|
|
|
|
|
|
message.setAiResponseStatus("success");
|
|
|
|
|
|
```
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 步骤 2:前端 — 统一调用 Coze 流式端点
|
|
|
|
|
|
|
|
|
|
|
|
将 `ai-nutritionist.vue` 中的文本消息路径从 KieAI Gemini 切换为 Coze SSE 流式调用:
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// 统一使用 Coze SSE 流式对话
|
|
|
|
|
|
const eventSource = new EventSourcePolyfill('/api/front/coze/chat/stream', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
body: JSON.stringify({ botId: this.botId, content: message, conversationId: this.conversationId }),
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json', 'Authori-zation': token }
|
|
|
|
|
|
});
|
|
|
|
|
|
eventSource.onmessage = (event) => {
|
|
|
|
|
|
// 实时追加文字到消息气泡
|
|
|
|
|
|
this.currentAiMessage.content += event.data;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 步骤 3:Coze Bot 侧 — 优化 Prompt 提升内容质量
|
|
|
|
|
|
|
|
|
|
|
|
在 Coze 平台编辑 Bot `7591133240535449654` 的系统提示词,增加结构化输出要求:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
你是一位专业的肾病营养师。请按以下格式回复:
|
|
|
|
|
|
【摘要】一句话概括建议
|
|
|
|
|
|
【营养分析】针对用户问题的营养学分析
|
|
|
|
|
|
【饮食建议】具体可操作的建议(分点列出)
|
|
|
|
|
|
【注意事项】需要特别关注的风险提示
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 预期效果
|
|
|
|
|
|
|
|
|
|
|
|
| 指标 | 修复前 | 修复后 |
|
|
|
|
|
|
|------|--------|--------|
|
|
|
|
|
|
| 首字响应时间 | 3-5 秒(轮询等待) | <1 秒(SSE 流式) |
|
|
|
|
|
|
| 内容逻辑性 | 无结构 | 四段式结构化输出 |
|
|
|
|
|
|
| 对话路径 | KieAI + Coze 双路径混乱 | Coze 单一路径统一 |
|
|
|
|
|
|
| 多模态支持 | 图片走 Coze,文本走 KieAI | 统一走 Coze(支持文本+图片) |
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
### 优先级:高
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 三、食物百科 —— 成分表不全、图片错误、缺少重量标注
|
|
|
|
|
|
|
|
|
|
|
|
### 问题描述
|
|
|
|
|
|
|
|
|
|
|
|
食物百科中食物成分表内容不全,图片显示有错误,而且没有标注具体是多少重量的食物所提供的营养成分。
|
|
|
|
|
|
|
|
|
|
|
|
### 问题定位
|
|
|
|
|
|
|
|
|
|
|
|
**后端文件:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodServiceImpl.java`
|
|
|
|
|
|
**前端文件:** `msh_single_uniapp/pages/tool/food-detail.vue`
|
|
|
|
|
|
**数据模型:** `msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java`
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**图片服务:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/DishImageServiceImpl.java`
|
|
|
|
|
|
**参考站点:** https://www.ishen365.com/article/cereal (爱肾网食物营养成分表)
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
### 根因分析
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 3.1 食物成分表内容不全 + 缺少重量标注
|
|
|
|
|
|
|
|
|
|
|
|
**参考标准:** 爱肾网(ishen365.com)的食物成分表采用的展示规范是:每种食物标注 **"每份重量"(如"谷类-大米 每100g")**,并列出完整的营养成分包括:能量(kcal)、蛋白质(g)、脂肪(g)、碳水化合物(g)、钾(mg)、磷(mg)、钠(mg)、钙(mg)、嘌呤(mg) 等。这是 CKD 患者膳食管理的行业标准展示方式。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**当前问题:** 后端 `getDetail()` 方法返回的营养字段不完整:
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
```java
|
2026-03-24 07:06:37 +08:00
|
|
|
|
// 当前返回的字段(仅 7 项)
|
2026-03-22 10:18:18 +08:00
|
|
|
|
map.put("energy", food.getEnergy());
|
|
|
|
|
|
map.put("protein", food.getProtein());
|
|
|
|
|
|
map.put("fat", food.getFat());
|
|
|
|
|
|
map.put("carbohydrate", food.getCarbohydrate());
|
|
|
|
|
|
map.put("potassium", food.getPotassium());
|
|
|
|
|
|
map.put("phosphorus", food.getPhosphorus());
|
|
|
|
|
|
map.put("sodium", food.getSodium());
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**对比 ishen365 标准缺失的字段:**
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
| 营养素 | ishen365 有 | 当前系统 | 状态 |
|
|
|
|
|
|
|--------|------------|----------|------|
|
|
|
|
|
|
| 能量(kcal) | 有 | 有 | 正常 |
|
|
|
|
|
|
| 蛋白质(g) | 有 | 有 | 正常 |
|
|
|
|
|
|
| 脂肪(g) | 有 | 有 | 正常 |
|
|
|
|
|
|
| 碳水化合物(g) | 有 | 有 | 正常 |
|
|
|
|
|
|
| 钾(mg) | 有 | 有 | 正常 |
|
|
|
|
|
|
| 磷(mg) | 有 | 有 | 正常 |
|
|
|
|
|
|
| 钠(mg) | 有 | 有 | 正常 |
|
|
|
|
|
|
| **钙(mg)** | **有** | **模型有字段,接口未返回** | **缺失** |
|
|
|
|
|
|
| **铁(mg)** | 有 | 模型有字段,接口未返回 | **缺失** |
|
|
|
|
|
|
| **维生素C(mg)** | 有 | 模型有字段,接口未返回 | **缺失** |
|
|
|
|
|
|
| **嘌呤(mg)** | **有** | **模型无此字段** | **缺失** |
|
|
|
|
|
|
| **每份重量标注** | **有(如"每100g")** | **前端硬编码,数据库无字段** | **缺失** |
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
`V2Food` 模型中定义了 `calcium`、`iron`、`vitaminC`、`nutrientsJson` 字段但后端未返回;同时缺少 `purine`(嘌呤)字段和 `servingSize`(每份重量基准)字段。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
前端 `food-detail.vue` 的 `parseNutritionTable()` 尝试解析 `calcium`、`purine` 等,但后端没有返回。前端"每100g"标签是硬编码的静态文本,无法反映实际数据的重量基准。
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.2 图片显示错误
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
**a) 前端硬编码了 Figma 临时 URL**
|
|
|
|
|
|
|
|
|
|
|
|
`food-detail.vue` 和 `nutrient-detail.vue` 中的默认图片使用了 Figma API 的临时资源 URL:
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
iconWhyImportant: 'https://www.figma.com/api/mcp/asset/51e00c9b-5719-4391-9dff-68b70d24aece'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这些 URL 是开发阶段从 Figma 设计稿导出的临时链接,过期后图片无法加载。
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**b) 部分食物图片为空或无效**
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
数据库中部分食物的 `image` 字段为空或为旧的无效 URL。项目中已有 `DishImageServiceImpl` 可自动生成并落库,但未对存量数据做全量刷新。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
### 修复方案
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 步骤 1:补全数据库字段和成分数据(参考 ishen365 标准)
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**a) 数据库表 `v2_food` 增加字段:**
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
ALTER TABLE v2_food ADD COLUMN purine DECIMAL(10,2) DEFAULT NULL COMMENT '嘌呤含量(mg)';
|
|
|
|
|
|
ALTER TABLE v2_food ADD COLUMN serving_size VARCHAR(50) DEFAULT '每100g' COMMENT '营养成分对应的食物重量基准';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**b) 参考 ishen365.com 补全营养数据:**
|
|
|
|
|
|
|
|
|
|
|
|
以谷薯类为例,按 ishen365 的数据格式补充 `calcium`、`iron`、`vitaminC`、`purine`、`serving_size` 等字段值。数据可通过爬虫脚本从 ishen365 获取后批量导入,或手动录入。
|
|
|
|
|
|
|
|
|
|
|
|
**c) 后端 `getDetail()` 补充返回字段:**
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// ToolFoodServiceImpl.java — getDetail() 补充以下字段
|
|
|
|
|
|
map.put("calcium", food.getCalcium());
|
|
|
|
|
|
map.put("iron", food.getIron());
|
|
|
|
|
|
map.put("vitaminC", food.getVitaminC());
|
|
|
|
|
|
map.put("purine", food.getPurine());
|
|
|
|
|
|
map.put("servingSize", food.getServingSize()); // "每100g" / "每份(50g)"
|
|
|
|
|
|
map.put("nutrientsJson", food.getNutrientsJson());
|
|
|
|
|
|
map.put("recommendedAmount", food.getRecommendedAmount());
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**d) 前端动态显示重量标注:**
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// food-detail.vue — 替换硬编码的 "每100g"
|
|
|
|
|
|
<text class="serving-label">{{ foodData.servingSize || '每100g' }}</text>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 步骤 2:利用已有 DishImageService 批量修复食物图片
|
|
|
|
|
|
|
|
|
|
|
|
项目中 `DishImageServiceImpl` 已实现完整的 **AI 生图 → 压缩 → 上传 OSS → 缓存到 `v2_dish_image_cache` 表** 的流程,`ToolFoodServiceImpl.refreshFoodImages()` 和 `ToolController` 中已暴露批量刷新接口。
|
|
|
|
|
|
|
|
|
|
|
|
**直接调用已有接口修复:**
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 调用已有的批量刷新接口,每次处理 20 条缺失/无效图片的食物记录
|
|
|
|
|
|
POST /api/front/tool/food/refresh-images?limit=20
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**内部执行流程(已实现,无需额外开发):**
|
|
|
|
|
|
1. 查询 `v2_food` 表中 `image` 为空或不含 `aliyuncs.com`(非 OSS)的记录
|
|
|
|
|
|
2. 对每条记录调用 `DishImageService.ensureFoodImageAndUpdateDb(foodId)`
|
|
|
|
|
|
3. KieAI 生成食物照片(Prompt:`"新鲜食物/食材照片:{食物名},白色背景,自然光照"`)
|
|
|
|
|
|
4. 下载 → 压缩至 ≤100KB → 上传阿里云 OSS(路径 `foods/`)
|
|
|
|
|
|
5. 更新 `v2_food.image` 字段 + 写入 `v2_dish_image_cache` 缓存表
|
|
|
|
|
|
|
|
|
|
|
|
**需多次调用直至所有食物图片修复完成。** 建议写一个定时任务或脚本循环调用。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 步骤 3:替换前端 Figma 临时 URL
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
将 `food-detail.vue`、`nutrient-detail.vue` 中的 Figma API 图标链接替换为 OSS 稳定资源,或使用本地 icon 图片。
|
|
|
|
|
|
|
|
|
|
|
|
### 优先级:高
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 四、健康知识营养素板块 —— 所有选项显示同一内容
|
|
|
|
|
|
|
|
|
|
|
|
### 问题描述
|
|
|
|
|
|
|
|
|
|
|
|
健康知识中营养素板块,所有的营养素选项(蛋白质、钾、磷、钠、钙、水分)点击后显示的都是同一个内容。
|
|
|
|
|
|
|
|
|
|
|
|
### 问题定位
|
|
|
|
|
|
|
|
|
|
|
|
**列表页:** `msh_single_uniapp/pages/tool/nutrition-knowledge.vue` 第 39 行
|
|
|
|
|
|
**详情页:** `msh_single_uniapp/pages/tool/nutrient-detail.vue` 第 114 行、第 137-277 行
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**后端服务:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java`
|
|
|
|
|
|
**后端接口:** `GET /api/front/tool/knowledge/nutrient/{name}` — 按营养素名查询详情
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
### 根因分析
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
这是一个**前端事件传参 Bug + 数据来源设计缺陷**联合导致的问题。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### Bug 1:列表页事件传参问题(前端 Bug)
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
`nutrition-knowledge.vue` 中营养素卡片的点击事件:
|
|
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
|
@click="goToNutrientDetail" :data-nutrient-index="index"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`goToNutrientDetail` 方法通过 `event.currentTarget.dataset.nutrientIndex` 获取索引值。但在**微信小程序**环境中,`dataset` 的属性名会被自动转换为全小写(`nutrientindex` 而非 `nutrientIndex`),导致取值为 `undefined`。
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
当 index 为 `undefined` 时,方法直接 `return`,不跳转;即使跳转成功,中文参数编解码也可能导致 `nutrientMap[name]` 匹配失败。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### Bug 2:详情页数据来源设计缺陷
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
`nutrient-detail.vue` 的数据完全来自**前端硬编码的 `nutrientMap`**(6 种营养素的静态内容),且 `data()` 默认值是"钠"的内容。当参数传递失败时,所有页面都显示"钠"。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**更深层问题:** 后端 `ToolKnowledgeServiceImpl` 已提供 `getNutrientDetail(name)` 方法,可从 `v2_knowledge` 表中查询 `type='nutrients'` 的记录,但前端根本没有调用这个接口,而是使用了硬编码数据。这导致内容无法动态更新,且 6 种营养素的详细内容质量参差不齐。
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
### 修复方案
|
|
|
|
|
|
|
|
|
|
|
|
**核心策略:通过 AI 生成 6 种营养素的专业内容后写入 `v2_knowledge` 表,前端改为从后端接口获取数据。**
|
|
|
|
|
|
|
|
|
|
|
|
#### 步骤 1:AI 生成营养素内容并落库
|
|
|
|
|
|
|
|
|
|
|
|
利用已有的 Coze API 服务,批量生成 6 种营养素(蛋白质、钾、磷、钠、钙、水分)的专业科普内容,写入 `v2_knowledge` 表。
|
|
|
|
|
|
|
|
|
|
|
|
**a) 数据表结构(已存在 `v2_knowledge` 表):**
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 每种营养素插入一条记录,type='nutrients'
|
|
|
|
|
|
INSERT INTO v2_knowledge (type, nutrient_name, title, content, summary, cover_image, status, sort_order, created_at) VALUES
|
|
|
|
|
|
('nutrients', '蛋白质', '蛋白质 — CKD患者的关键营养素', '{AI生成的详细内容JSON}', '了解蛋白质摄入与肾功能的关系', '', 'published', 1, NOW()),
|
|
|
|
|
|
('nutrients', '钾', '钾 — 维持生命的双刃剑', '{AI生成的详细内容JSON}', '高钾血症的预防与饮食管理', '', 'published', 2, NOW()),
|
|
|
|
|
|
('nutrients', '磷', '磷 — 骨骼健康的隐形杀手', '{AI生成的详细内容JSON}', '控磷饮食与磷结合剂的正确使用', '', 'published', 3, NOW()),
|
|
|
|
|
|
('nutrients', '钠', '钠 — 水盐平衡的调节器', '{AI生成的详细内容JSON}', '低盐饮食的科学方法', '', 'published', 4, NOW()),
|
|
|
|
|
|
('nutrients', '钙', '钙 — 骨骼和心脏的守护者', '{AI生成的详细内容JSON}', '钙磷平衡与维生素D的补充', '', 'published', 5, NOW()),
|
|
|
|
|
|
('nutrients', '水分', '水分 — 透析患者的生命线', '{AI生成的详细内容JSON}', '量出为入的饮水管理原则', '', 'published', 6, NOW());
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**b) AI 生成内容的 JSON 格式(与前端 `nutrientData` 结构对齐):**
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "蛋白质",
|
|
|
|
|
|
"english": "Protein",
|
|
|
|
|
|
"icon": "🥩",
|
|
|
|
|
|
"description": "构成人体组织的重要营养素",
|
|
|
|
|
|
"status": "需控制",
|
|
|
|
|
|
"statusDesc": "根据CKD分期调整摄入量",
|
|
|
|
|
|
"importance": "蛋白质是人体细胞的基本组成成分...",
|
|
|
|
|
|
"recommendation": "CKD 1-2期:0.8-1.0g/kg/天\nCKD 3-5期:0.6-0.8g/kg/天\n透析患者:1.0-1.2g/kg/天",
|
|
|
|
|
|
"foodSources": ["鸡蛋", "鱼类", "瘦肉", "牛奶", "豆腐", "鸡胸肉"],
|
|
|
|
|
|
"riskWarning": "过多蛋白质摄入会产生大量含氮废物,加重肾脏负担...",
|
|
|
|
|
|
"suggestions": ["优先选择优质蛋白", "控制植物蛋白摄入", "每餐均匀分配", "..."],
|
|
|
|
|
|
"disclaimer": "以上建议仅供参考,具体方案请咨询您的主治医生或营养师"
|
2026-03-22 10:18:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
**c) 通过 Coze API 批量生成内容的实现思路:**
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
可编写一个后端管理接口或脚本,循环调用 `ToolCozeServiceImpl.chat()` 为每种营养素生成内容:
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
```java
|
|
|
|
|
|
// 管理端一次性脚本
|
|
|
|
|
|
String[] nutrients = {"蛋白质", "钾", "磷", "钠", "钙", "水分"};
|
|
|
|
|
|
for (String nutrient : nutrients) {
|
|
|
|
|
|
CozeChatRequest req = new CozeChatRequest();
|
|
|
|
|
|
req.setBotId("7591133240535449654");
|
|
|
|
|
|
req.setContent("请为慢性肾病患者生成关于'" + nutrient + "'的科普内容,"
|
|
|
|
|
|
+ "包含:重要性说明、推荐摄入量、主要食物来源、风险提示、实用建议(6条)。"
|
|
|
|
|
|
+ "请用JSON格式返回。");
|
|
|
|
|
|
CreateChatResp resp = cozeService.chat(req);
|
|
|
|
|
|
// 解析响应 → 存入 v2_knowledge 表
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**d) 利用已有的 `fillMissingCoverImages()` 自动补充封面图:**
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
`ToolKnowledgeServiceImpl.fillMissingCoverImages()` 会自动为缺少封面图的知识记录生成图片(通过 `DishImageService.generateImageAndUploadToOssForKnowledge()`),生成后自动更新到数据库。调用:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
POST /api/front/tool/knowledge/fill-cover-images?limit=6
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 步骤 2:修复前端传参 Bug
|
|
|
|
|
|
|
|
|
|
|
|
**a) 列表页改用直接传参:**
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
```html
|
2026-03-24 07:06:37 +08:00
|
|
|
|
<!-- nutrition-knowledge.vue — 修改前 -->
|
2026-03-22 10:18:18 +08:00
|
|
|
|
@click="goToNutrientDetail" :data-nutrient-index="index"
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 修改后 -->
|
|
|
|
|
|
@click="goToNutrientDetail(index)"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
goToNutrientDetail(index) {
|
|
|
|
|
|
const item = this.nutrientList[index];
|
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: `/pages/tool/nutrient-detail?name=${encodeURIComponent(item.name)}`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 步骤 3:详情页改为从后端接口获取数据
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
```javascript
|
2026-03-24 07:06:37 +08:00
|
|
|
|
// nutrient-detail.vue — 将硬编码的 nutrientMap 替换为 API 调用
|
|
|
|
|
|
async loadNutrientData(name) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { getNutrientDetail } = await import('@/api/tool.js');
|
|
|
|
|
|
const result = await getNutrientDetail(name);
|
|
|
|
|
|
if (result && result.data) {
|
|
|
|
|
|
// 后端返回 v2_knowledge 记录,content 字段为 JSON
|
|
|
|
|
|
const detail = result.data;
|
|
|
|
|
|
this.nutrientData = typeof detail.content === 'string'
|
|
|
|
|
|
? JSON.parse(detail.content)
|
|
|
|
|
|
: detail.content;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载营养素详情失败:', error);
|
|
|
|
|
|
// 降级使用本地 nutrientMap(保留作为兜底)
|
|
|
|
|
|
if (this.nutrientMap[name]) {
|
|
|
|
|
|
this.nutrientData = this.nutrientMap[name];
|
|
|
|
|
|
}
|
2026-03-22 10:18:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
#### 预期效果
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
| 方面 | 修复前 | 修复后 |
|
|
|
|
|
|
|------|--------|--------|
|
|
|
|
|
|
| 数据来源 | 前端硬编码 6 份静态数据 | 后端 `v2_knowledge` 表,AI 生成专业内容 |
|
|
|
|
|
|
| 内容质量 | 手写内容,质量不均 | Coze AI 生成,专业、结构统一 |
|
|
|
|
|
|
| 点击跳转 | dataset 传参失败,不跳转 | 直接传参,兼容微信小程序 |
|
|
|
|
|
|
| 封面图片 | 无 | AI 生成后上传 OSS,自动落库 |
|
|
|
|
|
|
| 内容维护 | 需改代码重新发版 | 后台修改数据库即可,无需发版 |
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
### 优先级:高
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 问题优先级汇总
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
| 序号 | 问题 | 类型 | 优先级 | 修复策略 |
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|------|------|------|--------|----------|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
| 一 | 食谱计算器用油量偏多 | 算法缺陷 | 高 | 修改油脂系数 5.7→2.5 |
|
|
|
|
|
|
| 二 | AI 营养师体验差 | 架构优化 | 高 | 统一走 Coze API + SSE 流式 |
|
|
|
|
|
|
| 三 | 食物百科成分表/图片/重量 | 数据补全 | 高 | 参考 ishen365 补全数据 + 调用已有 DishImageService 批量生图落库 |
|
|
|
|
|
|
| 四 | 营养素板块显示重复 | Bug + 重构 | 高 | 修复传参 Bug + AI 生成内容落库到 v2_knowledge |
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 涉及文件清单
|
|
|
|
|
|
|
|
|
|
|
|
### 后端(Java)
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
| 文件 | 问题 | 修改内容 |
|
|
|
|
|
|
|------|------|----------|
|
|
|
|
|
|
| `ToolCalculatorServiceImpl.java` | 问题一 | 修改油脂系数 |
|
|
|
|
|
|
| `ToolAiNutritionistServiceImpl.java` | 问题二 | `sendMessage()` 对接 `ToolCozeServiceImpl.chat()` |
|
|
|
|
|
|
| `ToolCozeServiceImpl.java` | 问题二 | 已有完整 Coze API 实现,直接调用 |
|
|
|
|
|
|
| `CozeController.java` | 问题二 | SSE 流式端点已就绪 |
|
|
|
|
|
|
| `ToolFoodServiceImpl.java` | 问题三 | `getDetail()` 补充 calcium/iron/vitaminC/purine/servingSize 返回 |
|
|
|
|
|
|
| `V2Food.java` | 问题三 | 新增 purine、serving_size 字段 |
|
|
|
|
|
|
| `DishImageServiceImpl.java` | 问题三 | 已有 AI 生图+OSS 上传+落库功能,调用 `refreshFoodImages()` 即可 |
|
|
|
|
|
|
| `ToolKnowledgeServiceImpl.java` | 问题四 | 已有 `getNutrientDetail(name)` + `fillMissingCoverImages()`,配合 AI 内容生成后落库 |
|
2026-03-22 10:18:18 +08:00
|
|
|
|
|
|
|
|
|
|
### 前端(Vue)
|
|
|
|
|
|
|
2026-03-24 07:06:37 +08:00
|
|
|
|
| 文件 | 问题 | 修改内容 |
|
|
|
|
|
|
|------|------|----------|
|
|
|
|
|
|
| `pages/tool/ai-nutritionist.vue` | 问题二 | 统一 Coze SSE 调用,移除 KieAI 双路径 |
|
|
|
|
|
|
| `pages/tool/food-detail.vue` | 问题三 | 替换 Figma URL,动态显示 servingSize |
|
|
|
|
|
|
| `pages/tool/nutrient-detail.vue` | 问题四 | 改为调用后端 `getNutrientDetail()` API,保留硬编码作兜底 |
|
|
|
|
|
|
| `pages/tool/nutrition-knowledge.vue` | 问题四 | 修复 `goToNutrientDetail` 传参方式 |
|
|
|
|
|
|
|
|
|
|
|
|
### 数据库(SQL)
|
|
|
|
|
|
|
|
|
|
|
|
| 操作 | 问题 | 说明 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| `v2_food` 新增字段 | 问题三 | 添加 `purine`、`serving_size` 列 |
|
|
|
|
|
|
| `v2_food` 数据补全 | 问题三 | 参考 ishen365 补充 calcium/iron/vitaminC/purine/serving_size 数据 |
|
|
|
|
|
|
| `v2_knowledge` 插入数据 | 问题四 | AI 生成 6 种营养素内容 + 封面图,type='nutrients' |
|