Files
msh-system/docs/功能开发详细设计_2026-03-25.md
Developer ba08abd374 fix: 修复6项测试问题并补全配套资源
- 修复油脂类食物推荐量系数 (5.7→2.5) [ToolCalculatorServiceImpl]
- AI营养师接入真实Coze API,替换Mock回复 [ToolAiNutritionistServiceImpl]
- 食物百科详情新增钙/铁/维C/嘌呤/重量基准字段返回 [ToolFoodServiceImpl]
- V2Food模型新增purine、servingSize字段 [V2Food.java]
- 食物百科详情页动态重量标注+新增4项营养展示+替换Figma URL [food-detail.vue]
- 修复营养素列表dataset传参Bug(WeChat camelCase) [nutrition-knowledge.vue]
- 营养素详情页接入后端API+兜底本地数据+替换Figma URL [nutrient-detail.vue]
- 新增数据库迁移脚本及参考初始化数据 [docs/sql/]
- 新增前端占位图标5个 [static/images/]
- 新增开发任务完成报告 [docs/]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:18:00 +08:00

926 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 功能开发详细设计文档
> **版本:** v1.0
> **日期:** 2026-03-25
> **依据:** 《测试问题分析报告_2026-03-22》
> **项目:** 慢生活- 慢性肾病营养管理小程序
---
## 目录
1. [公共前置工作(数据库变更 + 数据初始化)](#一公共前置工作)
2. [页面一:食谱计算器结果页](#二页面一食谱计算器结果页)
3. [页面二AI 营养师对话页](#三页面二ai-营养师对话页)
4. [页面三:食物百科列表页](#四页面三食物百科列表页)
5. [页面四:食物百科详情页](#五页面四食物百科详情页)
6. [页面五:健康知识营养素列表页](#六页面五健康知识营养素列表页)
7. [页面六:营养素详情页](#七页面六营养素详情页)
8. [联调与测试检查清单](#八联调与测试检查清单)
---
## 一、公共前置工作
> 以下数据库变更和数据初始化需在所有页面开发之前完成。
### 1.1 数据库表结构变更
#### 1.1.1 `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 '营养成分对应的食物重量基准,如"每100g"、"每份(50g)"';
```
**验证方式:** 执行后 `DESC v2_food;` 确认新增列存在。
#### 1.1.2 `v2_food` 数据模型变更
**文件:** `msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java`
```java
// 新增字段
/** 嘌呤含量(mg) */
@TableField("purine")
private BigDecimal purine;
/** 营养成分重量基准 */
@TableField("serving_size")
private String servingSize;
```
### 1.2 食物营养数据补全
**参考来源:** https://www.ishen365.com/article/cereal (爱肾网-食物营养成分表)
**执行方式:** 编写数据迁移脚本SQL 或 Python将 ishen365 上各分类(谷薯类、蔬菜类、水果类、肉蛋类、水产类、奶类、豆类、坚果类)的食物营养数据批量更新到 `v2_food` 表。
**需要补全的字段:**
| 字段 | 说明 | 数据来源 |
|------|------|----------|
| `calcium` | 钙含量(mg) | ishen365 / 《中国食物成分表》 |
| `iron` | 铁含量(mg) | ishen365 / 《中国食物成分表》 |
| `vitamin_c` | 维生素C含量(mg) | ishen365 / 《中国食物成分表》 |
| `purine` | 嘌呤含量(mg) | ishen365 / 专业嘌呤数据库 |
| `serving_size` | 重量基准 | 统一填写 `"每100g"` |
**示例 SQL**
```sql
-- 以大米为例
UPDATE v2_food SET
calcium = 13,
iron = 2.3,
vitamin_c = 0,
purine = 18.4,
serving_size = '每100g'
WHERE name = '大米' AND category = '谷薯类';
```
**负责人:** 后端开发
**预计工时:** 1 天(含数据核对)
### 1.3 营养素知识内容 AI 生成并落库
**目标:**`v2_knowledge` 表插入 6 条营养素科普记录(蛋白质、钾、磷、钠、钙、水分),内容由 Coze AI 生成。
**执行方式:** 编写后端管理脚本或一次性接口。
**详见 [页面六:营养素详情页 - 步骤 1](#步骤-1后端---ai-生成营养素内容并落库) 。**
---
## 二、页面一:食谱计算器结果页
### 页面信息
| 项目 | 说明 |
|------|------|
| 前端路径 | `msh_single_uniapp/pages/tool/calculator-result.vue` |
| 后端服务 | `ToolCalculatorServiceImpl.java` |
| API 接口 | `POST /api/front/tool/calculator/calculate` |
| 关联接口 | `GET /api/front/tool/calculator/result/{id}` |
### 问题概述
食谱计算器中油脂类份数计算系数错误(与谷薯类使用了相同系数 5.7),导致计算出的每日用油量约 60g远超膳食指南推荐的 25-30g。
### 开发任务
#### 任务 2-1修改油脂类计算系数后端
**文件:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java`
**方法:** `generateFoodPortions()`
**修改内容:**
```java
// ❌ 修改前(第 516 行)
list.add(createFoodPortion(7, "油脂类10g", round(5.7 * energyRatio)));
// ✅ 修改后
list.add(createFoodPortion(7, "油脂类10g", round(2.5 * energyRatio)));
```
**验证计算:**
| 患者 | 标准体重 | 每日能量 | energyRatio | 修改前油脂份数 | 修改后油脂份数 | 修改后每日用油 |
|------|----------|----------|-------------|----------------|----------------|----------------|
| 男性170cm | 63kg | 2205 kcal | 1.10 | 6.3份(63g) | 2.8份(28g) | 28g |
| 女性160cm | 54kg | 1890 kcal | 0.95 | 5.4份(54g) | 2.4份(24g) | 24g |
| 男性175cm | 66.5kg | 2328 kcal | 1.16 | 6.6份(66g) | 2.9份(29g) | 29g |
**预计工时:** 0.5 小时
**自测要点:** 输入不同体型参数,验证结果页中"油脂类"份数是否在 2-3 份范围内。
---
## 三、页面二AI 营养师对话页
### 页面信息
| 项目 | 说明 |
|------|------|
| 前端路径 | `msh_single_uniapp/pages/tool/ai-nutritionist.vue` |
| 后端服务 | `ToolAiNutritionistServiceImpl.java` |
| Coze 服务 | `ToolCozeServiceImpl.java` |
| Coze 控制器 | `CozeController.java` |
| 前端 API | `msh_single_uniapp/api/tool.js` + `api/models-api.js` |
### 问题概述
后端 `sendMessage()` 使用 Mock 回复;前端文本消息走 KieAI Gemini、图片消息走 Coze双路径导致体验混乱且响应慢。
### 开发任务
#### 任务 3-1后端 sendMessage() 对接 Coze API后端
**文件:** `ToolAiNutritionistServiceImpl.java`
**修改内容:**`sendMessage()` 方法中的 Mock 逻辑替换为 Coze API 调用。
```java
// 注入 Coze 服务
@Autowired
private ToolCozeServiceImpl cozeService;
// sendMessage() 方法内,替换第 89-97 行的 Mock 逻辑:
// ❌ 删除
message.setAiResponse("这是一个模拟的AI回复。");
message.setAiResponseStatus("success");
// ✅ 替换为
try {
CozeChatRequest chatRequest = new CozeChatRequest();
chatRequest.setBotId("7591133240535449654");
chatRequest.setUserId(String.valueOf(userId));
chatRequest.setContent(message.getContent());
if (conversationId != null) {
chatRequest.setConversationId(conversationId.toString());
}
CreateChatResp resp = cozeService.chat(chatRequest);
// 从 Coze 响应中提取 AI 回复内容
String aiContent = extractAiContent(resp);
message.setAiResponse(aiContent);
message.setAiResponseStatus("success");
message.setAiResponseTime(new Date());
} catch (Exception e) {
log.error("Coze AI 回复失败, userId={}", userId, e);
message.setAiResponse("抱歉AI 营养师暂时无法回答,请稍后再试。");
message.setAiResponseStatus("failed");
}
```
**新增辅助方法:**
```java
/**
* 从 Coze 对话响应中提取 AI 回复文本
*/
private String extractAiContent(CreateChatResp resp) {
if (resp == null) return "暂无回复";
// 根据 Coze SDK 实际返回结构提取 answer 类型消息
// 需调用 listMessages() 获取 role=assistant, type=answer 的消息
String conversationId = resp.getConversationId();
String chatId = resp.getId();
List<Message> messages = cozeService.listMessages(conversationId, chatId);
return messages.stream()
.filter(m -> "assistant".equals(m.getRole()) && "answer".equals(m.getType()))
.map(Message::getContent)
.findFirst()
.orElse("暂无回复");
}
```
**预计工时:** 2 小时
#### 任务 3-2前端统一对话路径为 Coze前端
**文件:** `msh_single_uniapp/pages/tool/ai-nutritionist.vue`
**修改范围:** `sendMessage()` / `handleSend()` 方法
**当前逻辑(需重构):**
- 文本消息 → `api.kieaiGeminiChat()` KieAI Gemini
- 图片消息 → `api.cozeChat()` + 轮询 `api.cozeRetrieveChat()`
**修改为:** 统一走 Coze SSE 流式端点
```javascript
// ✅ 统一的消息发送方法
async sendToCoze(content, images = []) {
// 1. 如果有图片,先上传到 Coze 获取 file_id
let messageContent = content;
if (images.length > 0) {
const fileIds = [];
for (const img of images) {
const res = await api.cozeUploadFile(img.path);
fileIds.push({ type: 'image', file_id: res.id });
}
// 构建多模态消息
messageContent = JSON.stringify([
{ type: 'text', text: content },
...fileIds
]);
}
// 2. 添加用户消息到列表
this.messageList.push({ role: 'user', content, images });
// 3. 添加 AI 占位消息(用于流式填充)
const aiMsg = { role: 'ai', content: '', loading: true };
this.messageList.push(aiMsg);
this.scrollToBottom();
// 4. 调用 Coze 非流式接口(小程序不支持 SSE改用轮询
try {
const chatRes = await api.cozeChat({
botId: this.botId,
userId: this.userId,
additionalMessages: [{
role: 'user',
content: messageContent,
content_type: images.length > 0 ? 'object_string' : 'text'
}],
conversationId: this.conversationId || undefined
});
this.conversationId = chatRes.conversation_id;
const chatId = chatRes.id;
// 5. 轮询等待完成(每 1.5 秒,最多 40 次 = 60 秒)
let status = '';
let attempts = 0;
while (status !== 'completed' && attempts < 40) {
await this.sleep(1500);
const statusRes = await api.cozeRetrieveChat(this.conversationId, chatId);
status = statusRes.status;
attempts++;
if (status === 'failed') throw new Error('AI 回复失败');
}
// 6. 获取 AI 回复
const msgRes = await api.cozeMessageList(this.conversationId, chatId);
const answer = msgRes.find(m => m.role === 'assistant' && m.type === 'answer');
aiMsg.content = answer ? answer.content : '暂无回复';
} catch (e) {
aiMsg.content = '抱歉AI 营养师暂时无法回答,请稍后再试。';
console.error('Coze 对话失败:', e);
} finally {
aiMsg.loading = false;
this.isLoading = false;
this.scrollToBottom();
}
}
```
**需要删除/注释的代码:**
- `api.kieaiGeminiChat()` 调用逻辑(文本消息路径 A
- 图片消息的独立处理分支(合并到统一方法中)
**保留的能力:**
- 图片选择 + 预览(最多 3 张)
- 语音输入ASR 转文本后作为文本发送)
- 快捷问题按钮
- 清空对话历史
**预计工时:** 4 小时
#### 任务 3-3优化 Coze Bot Prompt运营配置
**操作位置:** Coze 平台 → Bot `7591133240535449654` → 系统提示词
**建议系统提示词:**
```
你是慢生活小程序的 AI 营养师专注于慢性肾脏病CKD患者的饮食营养指导。
回复规范:
1. 【一句话建议】用一句话直接回答用户问题
2. 【营养分析】针对用户的具体情况,从蛋白质、钾、磷、钠、能量等维度分析
3. 【推荐方案】给出 2-3 个具体可操作的饮食建议
4. 【注意事项】提醒需要警惕的风险,如高钾、高磷食物
约束:
- 不提供药物建议,仅限饮食营养指导
- 涉及具体用药、透析方案时,提醒用户咨询主治医生
- 语言通俗易懂,避免过多专业术语
- 每条建议附带具体食材示例
```
**预计工时:** 0.5 小时
#### 自测检查清单
- [ ] 发送文本消息 → AI 在 5 秒内开始回复
- [ ] 发送图片+文本 → AI 能识别图片并给出营养建议
- [ ] 多轮对话 → AI 能基于上下文连续回答
- [ ] 清空对话 → 重新开始新会话
- [ ] 网络异常 → 显示友好错误提示,不崩溃
- [ ] AI 回复内容具有结构性(有摘要、分析、建议)
---
## 四、页面三:食物百科列表页
### 页面信息
| 项目 | 说明 |
|------|------|
| 前端路径 | `msh_single_uniapp/pages/tool/food-encyclopedia.vue` |
| 后端服务 | `ToolFoodServiceImpl.java` |
| API 接口 | `GET /api/front/tool/food/search` / `GET /api/front/tool/food/list` |
### 问题概述
食物列表中部分食物图片显示异常(空白或破图)。
### 开发任务
#### 任务 4-1批量刷新食物图片后端/运维)
**已有接口:** `POST /api/front/tool/food/refresh-images?limit=20`
**执行流程(无需额外开发):**
```
调用接口 → ToolFoodServiceImpl.refreshFoodImages(limit)
→ 查询 v2_food 中 image 为空或非 OSS 链接的记录
→ 遍历调用 DishImageService.ensureFoodImageAndUpdateDb(foodId)
→ KieAI 生成食物照片
→ 下载 → 压缩至 ≤100KB
→ 上传阿里云 OSS路径: foods/xxx.jpg
→ 更新 v2_food.image 字段
→ 写入 v2_dish_image_cache 缓存表
```
**操作步骤:**
```bash
# 多次调用,每次处理 20 条,直到所有食物都有有效图片
curl -X POST "https://your-domain/api/front/tool/food/refresh-images?limit=20" \
-H "Authori-zation: {管理员token}"
# 查询还有多少条无图记录
SELECT COUNT(*) FROM v2_food WHERE status='active' AND (image IS NULL OR image = '' OR image NOT LIKE '%aliyuncs.com%');
```
**预计工时:** 0.5 天含执行等待时间KieAI 生图每张约 20-60 秒)
#### 任务 4-2前端图片加载容错前端
**文件:** `msh_single_uniapp/pages/tool/food-encyclopedia.vue`
**修改内容:** 为食物列表图片增加 `@error` 兜底。
```html
<!-- 食物卡片图片,增加 @error 处理 -->
<image
class="food-img"
:src="item.image || '/static/images/food-placeholder.png'"
mode="aspectFill"
@error="onImageError($event, item)"
/>
```
```javascript
methods: {
onImageError(e, item) {
// 图片加载失败时替换为占位图
item.image = '/static/images/food-placeholder.png';
}
}
```
**需要新增的占位图:** `/static/images/food-placeholder.png`(一个通用的食物灰色占位图,建议 200x200px
**预计工时:** 1 小时
---
## 五、页面四:食物百科详情页
### 页面信息
| 项目 | 说明 |
|------|------|
| 前端路径 | `msh_single_uniapp/pages/tool/food-detail.vue` |
| 后端服务 | `ToolFoodServiceImpl.java``getDetail()` |
| API 接口 | `GET /api/front/tool/food/detail/{id}` |
| 数据模型 | `V2Food.java` |
### 问题概述
1. 营养成分表只显示 7 项缺少钙、铁、维生素C、嘌呤
2. 图标使用了 Figma 临时 URL过期后不显示
3. "每100g" 标注是硬编码,不根据实际数据变化
### 开发任务
#### 任务 5-1后端接口补充返回字段后端
**文件:** `ToolFoodServiceImpl.java``getDetail()` 方法
**在现有 `map.put(...)` 代码块之后,追加以下字段返回:**
```java
// ====== 新增返回字段 ======
map.put("calcium", food.getCalcium()); // 钙(mg)
map.put("iron", food.getIron()); // 铁(mg)
map.put("vitaminC", food.getVitaminC()); // 维生素C(mg)
map.put("purine", food.getPurine()); // 嘌呤(mg) —— 新增字段
map.put("servingSize", food.getServingSize()); // 重量基准 —— 新增字段
map.put("nutrientsJson", food.getNutrientsJson());// 扩展营养素JSON
map.put("recommendedAmount", food.getRecommendedAmount()); // 推荐摄入量
```
**同时在 `search()` 方法的列表返回中,也补充 `servingSize` 字段:**
```java
map.put("servingSize", food.getServingSize());
```
**预计工时:** 0.5 小时
#### 任务 5-2前端成分表解析增加新字段前端
**文件:** `msh_single_uniapp/pages/tool/food-detail.vue`
**修改 `parseNutritionTable()` 或 `computed` 中的成分表构建逻辑,确保包含以下完整字段:**
```javascript
// displayNutritionTable 的构建逻辑
buildNutritionTable(data) {
const table = [
{ name: '能量', value: data.energy, unit: 'kcal', level: this.getLevel('energy', data.energy) },
{ name: '蛋白质', value: data.protein, unit: 'g', level: this.getLevel('protein', data.protein) },
{ name: '脂肪', value: data.fat, unit: 'g', level: 'normal' },
{ name: '碳水化合物', value: data.carbohydrate, unit: 'g', level: 'normal' },
{ name: '钾', value: data.potassium, unit: 'mg', level: this.getLevel('potassium', data.potassium) },
{ name: '磷', value: data.phosphorus, unit: 'mg', level: this.getLevel('phosphorus', data.phosphorus) },
{ name: '钠', value: data.sodium, unit: 'mg', level: this.getLevel('sodium', data.sodium) },
// ====== 以下为新增 ======
{ name: '钙', value: data.calcium, unit: 'mg', level: 'normal' },
{ name: '铁', value: data.iron, unit: 'mg', level: 'normal' },
{ name: '维生素C', value: data.vitaminC, unit: 'mg', level: 'normal' },
{ name: '嘌呤', value: data.purine, unit: 'mg', level: this.getLevel('purine', data.purine) },
];
// 过滤掉值为 null/undefined 的条目(后端可能部分食物没有数据)
return table.filter(item => item.value != null && item.value !== '');
}
```
**预计工时:** 1 小时
#### 任务 5-3动态显示重量标注前端
**文件:** `msh_single_uniapp/pages/tool/food-detail.vue`
**修改前(硬编码):**
```html
<!-- 第 32 行 -->
<view class="unit-badge">每100g</view>
<!-- 第 54 行 -->
<text class="unit-text">每100g</text>
```
**修改后(动态):**
```html
<view class="unit-badge">{{ foodData.servingSize || '每100g' }}</view>
<text class="unit-text">{{ foodData.servingSize || '每100g' }}</text>
```
**在数据解析方法中,从 API 返回值提取 `servingSize`**
```javascript
// API 返回数据解析时
this.foodData.servingSize = res.data.servingSize || '每100g';
```
**预计工时:** 0.5 小时
#### 任务 5-4替换 Figma 临时 URL前端
**文件:** `msh_single_uniapp/pages/tool/food-detail.vue`
**需替换的 URL第 95-96 行 `data()` 中):**
| 变量名 | 当前值Figma 临时 URL | 替换方案 |
|--------|--------------------------|----------|
| `iconShare` | `https://www.figma.com/api/mcp/asset/f9f0d7b9-...` | 替换为 OSS 图片或本地 `/static/icons/share.png` |
| `iconSearch` | `https://www.figma.com/api/mcp/asset/aa6bb75b-...` | 替换为 OSS 图片或本地 `/static/icons/search.png` |
| `defaultFoodData.image` | `https://www.figma.com/api/mcp/asset/bf4ff04c-...` | 替换为 `/static/images/food-placeholder.png` |
**同时检查 `nutrient-detail.vue` 中的 Figma URL第 111-113 行):**
| 变量名 | 替换方案 |
|--------|----------|
| `iconWhyImportant` | 本地 `/static/icons/why-important.png` |
| `iconRecommendation` | 本地 `/static/icons/recommendation.png` |
| `iconSuggestions` | 本地 `/static/icons/suggestions.png` |
**操作方式:**
1. 从 Figma 设计稿导出对应图标为 PNG建议 64x64px2x 为 128x128px
2. 放置到 `msh_single_uniapp/static/icons/` 目录
3. 或上传到 OSS 获取稳定 URL 后替换
**预计工时:** 1 小时
#### 自测检查清单
- [ ] 食物详情页显示完整的 11 项营养成分能量、蛋白质、脂肪、碳水、钾、磷、钠、钙、铁、维C、嘌呤
- [ ] 如果某食物缺少部分营养数据(如嘌呤为 null该项不显示而非显示 "null"
- [ ] 重量标注动态显示(如"每100g"),而非硬编码
- [ ] 分享图标、搜索图标正常显示
- [ ] 默认占位图在食物图片加载失败时正常显示
- [ ] 与 ishen365 上同一食物的数据进行交叉对比,确保数值一致
---
## 六、页面五:健康知识营养素列表页
### 页面信息
| 项目 | 说明 |
|------|------|
| 前端路径 | `msh_single_uniapp/pages/tool/nutrition-knowledge.vue` |
| 后端服务 | `ToolKnowledgeServiceImpl.java` |
| API 接口 | 营养素列表本地静态、饮食指南/科普文章从 `GET /api/front/tool/knowledge/list` 获取 |
### 问题概述
营养素卡片的点击事件使用 `dataset` 传参,在微信小程序中因属性名大小写转换导致取值失败,用户点击后无法跳转到对应的详情页。
### 开发任务
#### 任务 6-1修复营养素卡片点击传参前端
**文件:** `msh_single_uniapp/pages/tool/nutrition-knowledge.vue`
**修改前(第 39 行):**
```html
<view
class="nutrient-card"
v-for="(item, index) in nutrientList"
:key="index"
@click="goToNutrientDetail" :data-nutrient-index="index"
>
```
**修改后:**
```html
<view
class="nutrient-card"
v-for="(item, index) in nutrientList"
:key="index"
@click="goToNutrientDetail(index)"
>
```
**修改 `methods` 中的 `goToNutrientDetail` 方法:**
```javascript
// ❌ 修改前
goToNutrientDetail(event) {
const index = event.currentTarget.dataset.nutrientIndex;
const item = this.nutrientList[index];
if (!item) return;
uni.navigateTo({
url: `/pages/tool/nutrient-detail?name=${encodeURIComponent(item.name)}`
});
}
// ✅ 修改后
goToNutrientDetail(index) {
const item = this.nutrientList[index];
if (!item) return;
uni.navigateTo({
url: `/pages/tool/nutrient-detail?name=${encodeURIComponent(item.name)}`
});
}
```
**预计工时:** 0.5 小时
#### 自测检查清单
- [ ] 依次点击 6 个营养素卡片(蛋白质、钾、磷、钠、钙、水分),均能正常跳转
- [ ] 在微信开发者工具 + 真机预览中均测试通过
- [ ] 跳转后详情页标题与点击的营养素名称一致
---
## 七、页面六:营养素详情页
### 页面信息
| 项目 | 说明 |
|------|------|
| 前端路径 | `msh_single_uniapp/pages/tool/nutrient-detail.vue` |
| 后端服务 | `ToolKnowledgeServiceImpl.java``getNutrientDetail(name)` |
| API 接口 | `GET /api/front/tool/knowledge/nutrient/{name}` |
| 数据表 | `v2_knowledge`type='nutrients' |
| 封面图生成 | `POST /api/front/tool/knowledge/fill-cover-images?limit=6` |
### 问题概述
1. 详情页数据完全来自前端硬编码的 `nutrientMap`,未调用后端接口
2. `data()` 默认值为"钠"的内容,参数丢失时所有页面都显示"钠"
3. 内容质量参差不齐,需要用 AI 生成专业科普内容
### 开发任务
#### 步骤 1后端 — AI 生成营养素内容并落库
**实现方式 A推荐编写后端管理接口**
**新建文件或在 `ToolController.java` 中增加管理端接口:**
```java
/**
* 批量生成营养素科普内容(管理端一次性调用)
*/
@PostMapping("/admin/tool/knowledge/generate-nutrients")
public CommonResult<String> generateNutrientContent() {
String[] nutrients = {"蛋白质", "", "", "", "", "水分"};
int success = 0;
for (String nutrient : nutrients) {
try {
// 1. 检查是否已存在
V2Knowledge existing = knowledgeDao.selectOne(
new LambdaQueryWrapper<V2Knowledge>()
.eq(V2Knowledge::getType, "nutrients")
.eq(V2Knowledge::getNutrientName, nutrient)
);
if (existing != null) {
log.info("营养素 {} 已存在,跳过", nutrient);
continue;
}
// 2. 调用 Coze AI 生成内容
CozeChatRequest req = new CozeChatRequest();
req.setBotId("7591133240535449654");
req.setContent(buildNutrientPrompt(nutrient));
CreateChatResp resp = cozeService.chat(req);
String content = extractAiContent(resp);
// 3. 写入 v2_knowledge 表
V2Knowledge knowledge = new V2Knowledge();
knowledge.setType("nutrients");
knowledge.setNutrientName(nutrient);
knowledge.setTitle(nutrient + " — CKD患者膳食管理");
knowledge.setContent(content); // JSON 格式
knowledge.setSummary("了解" + nutrient + "在慢性肾病饮食中的重要性");
knowledge.setStatus("published");
knowledge.setSortOrder(success + 1);
knowledge.setCreatedAt(new Date());
knowledgeDao.insert(knowledge);
success++;
} catch (Exception e) {
log.error("生成营养素内容失败: {}", nutrient, e);
}
}
// 4. 自动补充封面图
knowledgeService.fillMissingCoverImages(6);
return CommonResult.success("成功生成 " + success + " 条营养素内容");
}
private String buildNutrientPrompt(String nutrient) {
return "请为慢性肾脏病CKD患者生成关于「" + nutrient + "」的科普内容。\n"
+ "要求严格按以下 JSON 格式返回(不要包含 markdown 标记):\n"
+ "{\n"
+ " \"name\": \"营养素名称\",\n"
+ " \"english\": \"英文名\",\n"
+ " \"icon\": \"一个合适的emoji\",\n"
+ " \"description\": \"一句话描述\",\n"
+ " \"status\": \"控制建议(如:需控制/严格控制/适量补充)\",\n"
+ " \"statusDesc\": \"状态补充说明\",\n"
+ " \"importance\": \"为什么重要2-3句话\",\n"
+ " \"recommendation\": \"推荐摄入量按CKD分期分别说明\",\n"
+ " \"foodSources\": [\"食物来源1\", \"食物来源2\", ...最多6个],\n"
+ " \"riskWarning\": \"风险提示2-3句话\",\n"
+ " \"suggestions\": [\"建议1\", \"建议2\", ...共6条实用建议],\n"
+ " \"disclaimer\": \"以上建议仅供参考,具体方案请咨询您的主治医生或营养师\"\n"
+ "}";
}
```
**实现方式 B备选直接执行 SQL 手动插入**
如果 AI 生成效果不理想,可手动整理内容后直接 SQL 插入:
```sql
INSERT INTO v2_knowledge (type, nutrient_name, title, content, summary, status, sort_order, created_at) VALUES
('nutrients', '蛋白质', '蛋白质 — CKD患者膳食管理', '{"name":"蛋白质","english":"Protein","icon":"🥩",...}', '了解蛋白质在CKD饮食中的重要性', 'published', 1, NOW()),
('nutrients', '', '钾 — CKD患者膳食管理', '{"name":"钾","english":"Potassium (K)","icon":"🍌",...}', '高钾血症的预防与饮食管理', 'published', 2, NOW()),
-- ... 其余 4 条
;
```
**插入后,调用封面图生成接口:**
```bash
POST /api/front/tool/knowledge/fill-cover-images?limit=6
```
**预计工时:** 2 小时
#### 步骤 2前端 — 详情页改为从后端 API 获取数据
**文件:** `msh_single_uniapp/pages/tool/nutrient-detail.vue`
**修改 1`data()` 默认值改为空状态**
```javascript
// ❌ 修改前:默认是"钠"的完整数据
data() {
return {
nutrientData: { name: '钠', english: 'Sodium (Na)', icon: '🧂', ... }
}
}
// ✅ 修改后:默认为空,显示加载状态
data() {
return {
loading: true,
loadError: false,
nutrientData: {
name: '', english: '', icon: '', description: '',
status: '', statusDesc: '', importance: '', recommendation: '',
foodSources: [], riskWarning: '', suggestions: [], disclaimer: ''
}
}
}
```
**修改 2`onLoad()` 中增加参数解码**
```javascript
onLoad(options) {
if (options.name) {
const name = decodeURIComponent(options.name);
uni.setNavigationBarTitle({ title: name });
this.loadNutrientData(name);
} else {
this.loadError = true;
}
}
```
**修改 3`loadNutrientData()` 改为优先调用 API兜底使用本地数据**
```javascript
async loadNutrientData(name) {
this.loading = true;
try {
// 优先从后端获取v2_knowledge 表, type='nutrients'
const { getNutrientDetail } = await import('@/api/tool.js');
const result = await getNutrientDetail(encodeURIComponent(name));
if (result && result.data && result.data.content) {
const detail = result.data;
// content 字段存储的是 JSON 字符串
const parsed = typeof detail.content === 'string'
? JSON.parse(detail.content)
: detail.content;
this.nutrientData = parsed;
this.loading = false;
return;
}
} catch (error) {
console.warn('从后端获取营养素详情失败,降级使用本地数据:', error);
}
// 降级:使用本地硬编码数据(保留原有 nutrientMap 作为兜底)
const localData = this.getLocalNutrientData(name);
if (localData) {
this.nutrientData = localData;
} else {
this.loadError = true;
}
this.loading = false;
},
getLocalNutrientData(name) {
const nutrientMap = {
'蛋白质': { name: '蛋白质', english: 'Protein', icon: '🥩', ... },
'钾': { ... },
// ... 保留原有 6 项数据作为兜底
};
return nutrientMap[name] || null;
}
```
**修改 4增加加载态和错误态的模板**
```html
<template>
<view class="nutrient-detail-page">
<!-- 加载中 -->
<view v-if="loading" class="loading-wrap">
<text>加载中...</text>
</view>
<!-- 加载失败 -->
<view v-else-if="loadError" class="error-wrap">
<text>暂无该营养素的详细信息</text>
</view>
<!-- 正常内容(保持原有结构不变) -->
<scroll-view v-else class="content-scroll" scroll-y>
<!-- ... 原有内容区域 ... -->
</scroll-view>
</view>
</template>
```
**预计工时:** 2 小时
#### 步骤 3替换 Figma 图标 URL
同 [任务 5-4](#任务-5-4替换-figma-临时-url前端),替换 `nutrient-detail.vue` 中第 111-113 行的 3 个 Figma URL。
**预计工时:** 0.5 小时
#### 自测检查清单
- [ ] 从列表页点击每个营养素 → 跳转到对应的详情页
- [ ] 详情页标题/图标/内容与营养素名称一致(不再全部显示"钠"
- [ ] 后端 `v2_knowledge` 有数据时 → 显示后端数据
- [ ] 后端数据不存在时 → 降级显示本地硬编码数据
- [ ] 图标正常显示(不是 Figma 的破图)
- [ ] 6 种营养素的内容专业准确,格式统一
---
## 八、联调与测试检查清单
### 全量回归测试
| 序号 | 测试场景 | 涉及页面 | 预期结果 |
|------|----------|----------|----------|
| 1 | 食谱计算器 → 输入标准体型参数 → 查看结果 | calculator-result | 油脂类份数在 2-3 份之间(每日 20-30g |
| 2 | AI 营养师 → 发送文字问题 | ai-nutritionist | 5 秒内收到结构化回复 |
| 3 | AI 营养师 → 发送图片+文字 | ai-nutritionist | AI 识别图片并给出营养分析 |
| 4 | AI 营养师 → 连续多轮对话 | ai-nutritionist | 上下文连贯,不丢失历史 |
| 5 | 食物百科 → 浏览列表 | food-encyclopedia | 所有食物有图片,无破图 |
| 6 | 食物百科 → 点击查看详情 | food-detail | 成分表完整11 项),有重量标注 |
| 7 | 食物详情 → 对比 ishen365 | food-detail | 营养数值与参考站点一致 |
| 8 | 健康知识 → 点击"蛋白质" | nutrition-knowledge → nutrient-detail | 跳转正常,显示蛋白质内容 |
| 9 | 健康知识 → 依次点击全部 6 项 | nutrient-detail | 每项内容不同,不再全部显示"钠" |
| 10 | 健康知识 → 弱网/断网测试 | nutrient-detail | 降级显示本地数据,不崩溃 |
### 工时汇总
| 任务 | 页面 | 开发人员 | 预计工时 |
|------|------|----------|----------|
| 数据库变更 + 数据补全 | 公共 | 后端 | 1 天 |
| 任务 2-1 油脂系数修改 | 计算器结果页 | 后端 | 0.5h |
| 任务 3-1 后端对接 Coze | AI 营养师 | 后端 | 2h |
| 任务 3-2 前端统一 Coze | AI 营养师 | 前端 | 4h |
| 任务 3-3 Coze Bot Prompt | AI 营养师 | 运营 | 0.5h |
| 任务 4-1 批量刷新图片 | 食物列表 | 后端/运维 | 0.5 天 |
| 任务 4-2 图片容错 | 食物列表 | 前端 | 1h |
| 任务 5-1 接口补字段 | 食物详情 | 后端 | 0.5h |
| 任务 5-2 成分表新字段 | 食物详情 | 前端 | 1h |
| 任务 5-3 动态重量标注 | 食物详情 | 前端 | 0.5h |
| 任务 5-4 替换 Figma URL | 食物详情+营养素详情 | 前端 | 1h |
| 任务 6-1 修复传参 Bug | 营养素列表 | 前端 | 0.5h |
| 步骤 1 AI 生成内容落库 | 营养素详情 | 后端 | 2h |
| 步骤 2 前端改 API 获取 | 营养素详情 | 前端 | 2h |
| 联调 + 回归测试 | 全部 | 全员 | 1 天 |
| **合计** | | | **约 4.5 人天** |