docs: 新增功能开发详细设计文档(以前端页面为维度)

基于测试问题分析报告,按6个前端页面拆分开发任务,含代码示例、自测清单和工时估算

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-25 11:23:21 +08:00
parent 45ac22d725
commit a5de6fb46d

View File

@@ -0,0 +1,925 @@
# 功能开发详细设计文档
> **版本:** 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 人天** |