- 修复油脂类食物推荐量系数 (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>
32 KiB
功能开发详细设计文档
版本: v1.0 日期: 2026-03-25 依据: 《测试问题分析报告_2026-03-22》 项目: 慢生活- 慢性肾病营养管理小程序
目录
- 公共前置工作(数据库变更 + 数据初始化)
- 页面一:食谱计算器结果页
- 页面二:AI 营养师对话页
- 页面三:食物百科列表页
- 页面四:食物百科详情页
- 页面五:健康知识营养素列表页
- 页面六:营养素详情页
- 联调与测试检查清单
一、公共前置工作
以下数据库变更和数据初始化需在所有页面开发之前完成。
1.1 数据库表结构变更
1.1.1 v2_food 表新增字段
-- 新增嘌呤含量字段
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
// 新增字段
/** 嘌呤含量(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:
-- 以大米为例
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 。
二、页面一:食谱计算器结果页
页面信息
| 项目 | 说明 |
|---|---|
| 前端路径 | 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()
修改内容:
// ❌ 修改前(第 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 调用。
// 注入 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");
}
新增辅助方法:
/**
* 从 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 流式端点
// ✅ 统一的消息发送方法
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 缓存表
操作步骤:
# 多次调用,每次处理 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 兜底。
<!-- 食物卡片图片,增加 @error 处理 -->
<image
class="food-img"
:src="item.image || '/static/images/food-placeholder.png'"
mode="aspectFill"
@error="onImageError($event, item)"
/>
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 |
问题概述
- 营养成分表只显示 7 项(缺少钙、铁、维生素C、嘌呤)
- 图标使用了 Figma 临时 URL,过期后不显示
- "每100g" 标注是硬编码,不根据实际数据变化
开发任务
任务 5-1:后端接口补充返回字段(后端)
文件: ToolFoodServiceImpl.java → getDetail() 方法
在现有 map.put(...) 代码块之后,追加以下字段返回:
// ====== 新增返回字段 ======
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 字段:
map.put("servingSize", food.getServingSize());
预计工时: 0.5 小时
任务 5-2:前端成分表解析增加新字段(前端)
文件: msh_single_uniapp/pages/tool/food-detail.vue
修改 parseNutritionTable() 或 computed 中的成分表构建逻辑,确保包含以下完整字段:
// 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
修改前(硬编码):
<!-- 第 32 行 -->
<view class="unit-badge">每100g</view>
<!-- 第 54 行 -->
<text class="unit-text">每100g</text>
修改后(动态):
<view class="unit-badge">{{ foodData.servingSize || '每100g' }}</view>
<text class="unit-text">{{ foodData.servingSize || '每100g' }}</text>
在数据解析方法中,从 API 返回值提取 servingSize:
// 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 |
操作方式:
- 从 Figma 设计稿导出对应图标为 PNG(建议 64x64px,2x 为 128x128px)
- 放置到
msh_single_uniapp/static/icons/目录 - 或上传到 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 行):
<view
class="nutrient-card"
v-for="(item, index) in nutrientList"
:key="index"
@click="goToNutrientDetail" :data-nutrient-index="index"
>
修改后:
<view
class="nutrient-card"
v-for="(item, index) in nutrientList"
:key="index"
@click="goToNutrientDetail(index)"
>
修改 methods 中的 goToNutrientDetail 方法:
// ❌ 修改前
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 |
问题概述
- 详情页数据完全来自前端硬编码的
nutrientMap,未调用后端接口 data()默认值为"钠"的内容,参数丢失时所有页面都显示"钠"- 内容质量参差不齐,需要用 AI 生成专业科普内容
开发任务
步骤 1:后端 — AI 生成营养素内容并落库
实现方式 A(推荐):编写后端管理接口
新建文件或在 ToolController.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 插入:
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 条
;
插入后,调用封面图生成接口:
POST /api/front/tool/knowledge/fill-cover-images?limit=6
预计工时: 2 小时
步骤 2:前端 — 详情页改为从后端 API 获取数据
文件: msh_single_uniapp/pages/tool/nutrient-detail.vue
修改 1:data() 默认值改为空状态
// ❌ 修改前:默认是"钠"的完整数据
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() 中增加参数解码
onLoad(options) {
if (options.name) {
const name = decodeURIComponent(options.name);
uni.setNavigationBarTitle({ title: name });
this.loadNutrientData(name);
} else {
this.loadError = true;
}
}
修改 3:loadNutrientData() 改为优先调用 API,兜底使用本地数据
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:增加加载态和错误态的模板
<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,替换 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 人天 |