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

32 KiB
Raw Blame History

功能开发详细设计文档

版本: v1.0 日期: 2026-03-25 依据: 《测试问题分析报告_2026-03-22》 项目: 慢生活- 慢性肾病营养管理小程序


目录

  1. 公共前置工作(数据库变更 + 数据初始化)
  2. 页面一:食谱计算器结果页
  3. 页面二AI 营养师对话页
  4. 页面三:食物百科列表页
  5. 页面四:食物百科详情页
  6. 页面五:健康知识营养素列表页
  7. 页面六:营养素详情页
  8. 联调与测试检查清单

一、公共前置工作

以下数据库变更和数据初始化需在所有页面开发之前完成。

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.javagetDetail()
API 接口 GET /api/front/tool/food/detail/{id}
数据模型 V2Food.java

问题概述

  1. 营养成分表只显示 7 项缺少钙、铁、维生素C、嘌呤
  2. 图标使用了 Figma 临时 URL过期后不显示
  3. "每100g" 标注是硬编码,不根据实际数据变化

开发任务

任务 5-1后端接口补充返回字段后端

文件: ToolFoodServiceImpl.javagetDetail() 方法

在现有 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

操作方式:

  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 行):

<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.javagetNutrientDetail(name)
API 接口 GET /api/front/tool/knowledge/nutrient/{name}
数据表 v2_knowledgetype='nutrients'
封面图生成 POST /api/front/tool/knowledge/fill-cover-images?limit=6

问题概述

  1. 详情页数据完全来自前端硬编码的 nutrientMap,未调用后端接口
  2. data() 默认值为"钠"的内容,参数丢失时所有页面都显示"钠"
  3. 内容质量参差不齐,需要用 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

修改 1data() 默认值改为空状态

// ❌ 修改前:默认是"钠"的完整数据
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: ''
        }
    }
}

修改 2onLoad() 中增加参数解码

onLoad(options) {
    if (options.name) {
        const name = decodeURIComponent(options.name);
        uni.setNavigationBarTitle({ title: name });
        this.loadNutrientData(name);
    } else {
        this.loadError = true;
    }
}

修改 3loadNutrientData() 改为优先调用 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 人天