diff --git a/docs/sql/migration_2026-03-25_add_food_fields.sql b/docs/sql/migration_2026-03-25_add_food_fields.sql new file mode 100644 index 0000000..8a978ac --- /dev/null +++ b/docs/sql/migration_2026-03-25_add_food_fields.sql @@ -0,0 +1,49 @@ +-- ============================================================ +-- 数据库迁移脚本 +-- 版本: v1.0 +-- 日期: 2026-03-25 +-- 描述: 为 v2_food 表新增嘌呤含量和重量基准字段 +-- ============================================================ + +-- 1. 新增 purine 字段(嘌呤含量,单位 mg) +ALTER TABLE v2_food + ADD COLUMN purine DECIMAL(10, 2) DEFAULT NULL COMMENT '嘌呤含量(mg)'; + +-- 2. 新增 serving_size 字段(营养成分对应重量基准) +ALTER TABLE v2_food + ADD COLUMN serving_size VARCHAR(50) DEFAULT '每100g' COMMENT '营养成分对应的食物重量基准,如"每100g"、"每份(50g)"'; + +-- 验证 +DESC v2_food; + +-- ============================================================ +-- 参考数据初始化示例(按需执行) +-- 数据来源: https://www.ishen365.com/article/cereal +-- ============================================================ + +-- 谷薯类 +UPDATE v2_food SET calcium = 13, iron = 2.3, vitamin_c = 0, purine = 18.4, serving_size = '每100g' WHERE name = '大米' AND category_name LIKE '%谷%'; +UPDATE v2_food SET calcium = 34, iron = 5.1, vitamin_c = 0, purine = 25.0, serving_size = '每100g' WHERE name = '小米' AND category_name LIKE '%谷%'; +UPDATE v2_food SET calcium = 38, iron = 5.9, vitamin_c = 0, purine = 22.4, serving_size = '每100g' WHERE name = '玉米' AND category_name LIKE '%谷%'; +UPDATE v2_food SET calcium = 31, iron = 3.5, vitamin_c = 0, purine = 17.1, serving_size = '每100g' WHERE name = '面粉' AND category_name LIKE '%谷%'; + +-- 蔬菜类 +UPDATE v2_food SET calcium = 48, iron = 1.2, vitamin_c = 14, purine = 10.1, serving_size = '每100g' WHERE name = '菠菜' AND category_name LIKE '%蔬%'; +UPDATE v2_food SET calcium = 36, iron = 0.5, vitamin_c = 4, purine = 5.5, serving_size = '每100g' WHERE name = '西红柿' AND category_name LIKE '%蔬%'; +UPDATE v2_food SET calcium = 21, iron = 0.3, vitamin_c = 2, purine = 3.4, serving_size = '每100g' WHERE name = '土豆' AND category_name LIKE '%蔬%'; + +-- 水果类 +UPDATE v2_food SET calcium = 4, iron = 0.6, vitamin_c = 8, purine = 0.9, serving_size = '每100g' WHERE name = '苹果' AND category_name LIKE '%水果%'; +UPDATE v2_food SET calcium = 7, iron = 0.2, vitamin_c = 8, purine = 1.9, serving_size = '每100g' WHERE name = '香蕉' AND category_name LIKE '%水果%'; + +-- 肉蛋类 +UPDATE v2_food SET calcium = 11, iron = 2.5, vitamin_c = 0, purine = 122.5, serving_size = '每100g' WHERE name = '猪肉' AND category_name LIKE '%肉%'; +UPDATE v2_food SET calcium = 9, iron = 2.3, vitamin_c = 0, purine = 107.6, serving_size = '每100g' WHERE name = '牛肉' AND category_name LIKE '%肉%'; +UPDATE v2_food SET calcium = 56, iron = 2.0, vitamin_c = 0, purine = 2.6, serving_size = '每100g' WHERE name = '鸡蛋' AND category_name LIKE '%蛋%'; + +-- 豆类 +UPDATE v2_food SET calcium = 191, iron = 8.2, vitamin_c = 0, purine = 166.5, serving_size = '每100g' WHERE name = '黄豆' AND category_name LIKE '%豆%'; +UPDATE v2_food SET calcium = 164, iron = 6.4, vitamin_c = 0, purine = 116.9, serving_size = '每100g' WHERE name = '豆腐' AND category_name LIKE '%豆%'; + +-- 将所有未设置 serving_size 的记录默认填充 +UPDATE v2_food SET serving_size = '每100g' WHERE serving_size IS NULL OR serving_size = ''; diff --git a/docs/功能开发详细设计_2026-03-25.md b/docs/功能开发详细设计_2026-03-25.md index 49a8e1a..fbfaedb 100644 --- a/docs/功能开发详细设计_2026-03-25.md +++ b/docs/功能开发详细设计_2026-03-25.md @@ -3,7 +3,7 @@ > **版本:** v1.0 > **日期:** 2026-03-25 > **依据:** 《测试问题分析报告_2026-03-22》 -> **项目:** 民生汇 - 慢性肾病营养管理小程序 +> **项目:** 慢生活- 慢性肾病营养管理小程序 --- @@ -320,7 +320,7 @@ async sendToCoze(content, images = []) { **建议系统提示词:** ``` -你是民生汇小程序的 AI 营养师,专注于慢性肾脏病(CKD)患者的饮食营养指导。 +你是慢生活小程序的 AI 营养师,专注于慢性肾脏病(CKD)患者的饮食营养指导。 回复规范: 1. 【一句话建议】用一句话直接回答用户问题 diff --git a/docs/开发任务完成报告_2026-03-25.md b/docs/开发任务完成报告_2026-03-25.md new file mode 100644 index 0000000..379cea1 --- /dev/null +++ b/docs/开发任务完成报告_2026-03-25.md @@ -0,0 +1,94 @@ +# 开发任务完成报告 2026-03-25 + +## 任务总览 + +| 任务 | 状态 | 说明 | +|------|------|------| +| 任务 0:数据库表结构变更 | ✅ 完成 | V2Food.java 新增 purine、servingSize 字段 | +| 任务 1:油脂系数 Bug 修复 | ✅ 完成 | 油脂类系数 5.7 → 2.5 | +| 任务 2:AI 营养师接入 Coze API | ✅ 完成 | 替换 Mock 回复为 Coze API 调用 | +| 任务 3:食物百科详情字段补全 | ✅ 完成 | 新增钙、铁、维生素C、嘌呤、重量基准 | +| 任务 4:食物百科详情页前端修复 | ✅ 完成 | 动态重量标注 + Figma URL 替换 + 营养成分扩展 | +| 任务 5:营养素列表传参 Bug 修复 | ✅ 完成 | dataset 传参改为直接传参 | +| 任务 6:营养素详情页接入后端 API | ✅ 完成 | 优先 API、本地 map 兜底 + Figma URL 替换 | + +--- + +## 详细修改记录 + +### 任务 0:数据库表结构变更 + +**文件:** `msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java` + +- 在 vitaminC 字段后(约第 70 行)新增: + - `purine` (BigDecimal):嘌呤含量(mg),对应数据库字段 `purine` + - `servingSize` (String):营养成分重量基准,对应数据库字段 `serving_size` +- 类已使用 `@Data` 注解(Lombok),无需手动添加 getter/setter + +### 任务 1:油脂系数 Bug 修复 + +**文件:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java` + +- 第 516 行:`round(5.7 * energyRatio)` → `round(2.5 * energyRatio)` +- 验证:文件中仅第 510 行谷薯类保留 `5.7` 系数,油脂类已改为 `2.5` + +### 任务 2:AI 营养师接入 Coze API + +**文件:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolAiNutritionistServiceImpl.java` + +- 新增注入:`ToolCozeService toolCozeService` +- 新增 import:`ToolCozeService`、`CozeChatRequest`、`CozeBaseResponse` +- `sendMessage()` 方法中: + - 移除 Mock 回复 `"这是一个模拟的AI回复。"` + - 替换为 `toolCozeService.chat(cozeRequest)` 调用 + - Bot ID: `7591133240535449654` + - 添加 try-catch 异常处理,失败时设置 `aiResponseStatus("failed")` + +### 任务 3:食物百科详情字段补全 + +**文件:** `msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolFoodServiceImpl.java` + +- `getDetail()` 方法中(约第 121 行后)新增 5 个字段返回: + - `calcium`、`iron`、`vitaminC`、`purine`、`servingSize` + +### 任务 4:食物百科详情页前端修复 + +**文件:** `msh_single_uniapp/pages/tool/food-detail.vue` + +- **修复点 A:** 两处 `"每100g"` 硬编码替换为 `{{ foodData.servingSize || '每100g' }}` +- **修复点 B:** `parseNutritionTable` 中新增铁 (iron) 和维生素C (vitaminC) 营养项;`parseKeyNutrients` 和 `parseNutritionTable` 的 nutrients 来源改为兼容平铺字段 (`data.nutrients || data.nutritionData || data`) +- **修复点 C:** 3 处 Figma 临时 URL 替换为本地占位图路径(`/static/images/icon-share.png`、`/static/images/icon-search.png`、空字符串) + +### 任务 5:营养素列表传参 Bug 修复 + +**文件:** `msh_single_uniapp/pages/tool/nutrition-knowledge.vue` + +- 第 39 行:`@click="goToNutrientDetail" :data-nutrient-index="index"` → `@click="goToNutrientDetail(index)"` +- `goToNutrientDetail` 方法:参数从 `event` 改为直接接收 `index`,移除 `event.currentTarget.dataset.nutrientIndex` 取值逻辑 + +### 任务 6:营养素详情页接入后端 API + +**文件:** `msh_single_uniapp/pages/tool/nutrient-detail.vue` + +- **修复点 A:** `nutrientData` 默认值从硬编码的"钠"数据改为空对象 `{}` +- **修复点 B:** `loadNutrientData()` 方法改为 async,优先调用 `getNutrientDetail(name)` API,失败时使用本地 `nutrientMap` 兜底 +- **修复点 C:** 3 处 Figma 临时图片 URL 替换为本地占位图路径 + +--- + +## 遇到的问题及处理方式 + +1. **ToolCozeService 接口设计**:`chat()` 方法接收 `CozeChatRequest` 对象而非简单的 `(botId, message, userId)` 参数。处理方式:构造完整的 `CozeChatRequest` 对象,设置 `botId`、`userId`、`additionalMessages` 等字段。 + +2. **food-detail.vue 营养数据解析**:后端返回的是平铺字段(如 `data.calcium`),而前端解析方法原本从 `data.nutrients` 子对象取值。处理方式:将 nutrients 来源改为 `data.nutrients || data.nutritionData || data`,兼容两种数据结构。 + +3. **Figma 临时图片 URL**:多个 Vue 文件中使用了 Figma API 的临时资源 URL。处理方式:统一替换为本地静态资源路径(`/static/images/xxx.png`),需后续补充实际图标文件。 + +--- + +## 后续待办 + +- [ ] 数据库执行 ALTER TABLE 添加 `purine` 和 `serving_size` 字段 +- [ ] 补充 `/static/images/` 下的图标文件(icon-share.png、icon-search.png、icon-why-important.png、icon-recommendation.png、icon-suggestions.png) +- [ ] 后端实现 `getNutrientDetail` 接口(若尚未实现) +- [ ] 用户手动执行 `git push origin main` diff --git a/docs/测试问题分析报告_2026-03-22.md b/docs/测试问题分析报告_2026-03-22.md index e8c10d4..0350527 100644 --- a/docs/测试问题分析报告_2026-03-22.md +++ b/docs/测试问题分析报告_2026-03-22.md @@ -2,7 +2,7 @@ > **日期:** 2026-03-22 > **分析人:** Claude AI -> **项目:** 民生汇 - 慢性肾病营养管理小程序 +> **项目:** 慢生活 - 慢性肾病营养管理小程序 --- diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java index 0a7d685..7f2205c 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Food.java @@ -68,6 +68,16 @@ public class V2Food implements Serializable { @ApiModelProperty(value = "维生素C(mg)") private BigDecimal vitaminC; + /** 嘌呤含量(mg) */ + @ApiModelProperty(value = "嘌呤含量(mg)") + @com.baomidou.mybatisplus.annotation.TableField("purine") + private BigDecimal purine; + + /** 营养成分重量基准 */ + @ApiModelProperty(value = "营养成分重量基准") + @com.baomidou.mybatisplus.annotation.TableField("serving_size") + private String servingSize; + @ApiModelProperty(value = "其他营养成分,JSON格式") private String nutrientsJson; diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolAiNutritionistServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolAiNutritionistServiceImpl.java index f3a01d6..b9f47c1 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolAiNutritionistServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolAiNutritionistServiceImpl.java @@ -12,6 +12,9 @@ import com.zbkj.common.token.FrontTokenComponent; import com.zbkj.service.dao.tool.V2AiConversationDao; import com.zbkj.service.dao.tool.V2AiMessageDao; import com.zbkj.service.service.tool.ToolAiNutritionistService; +import com.zbkj.service.service.tool.ToolCozeService; +import com.zbkj.common.request.coze.CozeChatRequest; +import com.zbkj.common.response.CozeBaseResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -39,6 +42,9 @@ public class ToolAiNutritionistServiceImpl implements ToolAiNutritionistService @Autowired private FrontTokenComponent frontTokenComponent; + @Autowired + private ToolCozeService toolCozeService; + /** * 发送消息给AI营养师 * @param data 消息数据 @@ -85,15 +91,45 @@ public class ToolAiNutritionistServiceImpl implements ToolAiNutritionistService message.setAiResponseStatus("pending"); v2AiMessageDao.insert(message); - - // Mock AI response - // In real system, this would call AI service - // Here we just set a mock response for now - message.setAiResponse("这是一个模拟的AI回复。"); - message.setAiResponseStatus("success"); + + // 调用 Coze API 获取 AI 回复 + try { + String botId = "7591133240535449654"; + String userMessage = (String) data.get("content"); + + CozeChatRequest cozeRequest = new CozeChatRequest(); + cozeRequest.setBotId(botId); + cozeRequest.setUserId(String.valueOf(userId)); + cozeRequest.setStream(false); + + // 构建消息 + CozeChatRequest.ChatMessage chatMessage = new CozeChatRequest.ChatMessage(); + chatMessage.setRole("user"); + chatMessage.setContent(userMessage); + chatMessage.setContentType("text"); + cozeRequest.setAdditionalMessages(java.util.Collections.singletonList( + new java.util.HashMap() {{ + put("role", "user"); + put("content", userMessage); + put("content_type", "text"); + }} + )); + + CozeBaseResponse cozeResponse = toolCozeService.chat(cozeRequest); + if (cozeResponse != null && cozeResponse.getData() != null) { + message.setAiResponse(String.valueOf(cozeResponse.getData())); + message.setAiResponseStatus("success"); + } else { + message.setAiResponse("AI服务暂时无法响应,请稍后再试。"); + message.setAiResponseStatus("failed"); + } + } catch (Exception e) { + log.error("Coze API调用失败, userId={}, conversationId={}", userId, conversationId, e); + message.setAiResponse("AI服务调用失败,请稍后再试。"); + message.setAiResponseStatus("failed"); + } message.setAiResponseTime(new Date()); - // message.setSender("ai"); // Keep sender as user, response is in aiResponse field - + v2AiMessageDao.updateById(message); // Update conversation last message time diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java index a7503bd..a925eef 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java @@ -513,7 +513,7 @@ public class ToolCalculatorServiceImpl extends ServiceImpl 关键营养成分 - 每100g + {{ foodData.servingSize || '每100g' }} 营养成分表 - 每100g + {{ foodData.servingSize || '每100g' }} { const val = nutrients[item.key] - if (val !== undefined) { - const level = val <= item.thresholds[0] ? 'low' : (val <= item.thresholds[1] ? 'medium' : 'high') + if (val !== undefined && val !== null) { + const numVal = Number(val) + const level = numVal <= item.thresholds[0] ? 'low' : (numVal <= item.thresholds[1] ? 'medium' : 'high') const levelText = level === 'low' ? '低' : (level === 'medium' ? '中' : '高') table.push({ name: item.name, value: String(val), unit: item.unit, level, levelText }) } diff --git a/msh_single_uniapp/pages/tool/nutrient-detail.vue b/msh_single_uniapp/pages/tool/nutrient-detail.vue index 004d52a..b9a3c49 100644 --- a/msh_single_uniapp/pages/tool/nutrient-detail.vue +++ b/msh_single_uniapp/pages/tool/nutrient-detail.vue @@ -108,40 +108,34 @@ export default { data() { return { // 图标资源 - iconWhyImportant: 'https://www.figma.com/api/mcp/asset/51e00c9b-5719-4391-9dff-68b70d24aece', - iconRecommendation: 'https://www.figma.com/api/mcp/asset/29e03ae9-a3ba-4416-8c6e-962bf3a8c8bb', - iconSuggestions: 'https://www.figma.com/api/mcp/asset/9027fced-f029-4f65-bba1-a0b5883b8d2b', - nutrientData: { - name: '钠', - english: 'Sodium (Na)', - icon: '🧂', - description: '调节体液平衡的电解质', - status: '适量控制', - statusDesc: '可适量补充,保持均衡', - importance: '钠参与调节体液平衡和血压,过多摄入会导致水肿和高血压,增加心血管负担。', - recommendation: 'CKD患者:2-3g食盐/天(相当于800-1200mg钠)', - foodSources: ['食盐', '酱油', '腌制食品', '加工肉类', '咸菜', '味精'], - riskWarning: '摄入过多会导致水肿、高血压、心力衰竭等问题。', - suggestions: [ - '每日食盐控制在3-5g(约一啤酒瓶盖)', - '避免腌制、熏制食品', - '少用酱油、味精等调味品', - '可用葱姜蒜、柠檬汁调味', - '查看食品标签,选择低钠产品', - '透析患者控制饮水量' - ], - disclaimer: '以上建议仅供参考,\n\r 具体方案请咨询您的医生或营养师' - } + iconWhyImportant: '/static/images/icon-why-important.png', + iconRecommendation: '/static/images/icon-recommendation.png', + iconSuggestions: '/static/images/icon-suggestions.png', + nutrientName: '', + nutrientData: {} } }, onLoad(options) { // 根据传入的营养素名称加载对应数据 if (options.name) { - this.loadNutrientData(options.name) + this.nutrientName = decodeURIComponent(options.name) + this.loadNutrientData(this.nutrientName) } }, methods: { - loadNutrientData(name) { + async loadNutrientData(name) { + // 优先调用后端接口获取营养素详情 + try { + const { getNutrientDetail } = await import('@/api/tool.js') + const res = await getNutrientDetail(name) + if (res && res.data) { + this.nutrientData = res.data + return + } + } catch (e) { + console.warn('API获取营养素详情失败,使用本地数据', e) + } + // 兜底:使用本地 nutrientMap const nutrientMap = { '钠': { name: '钠', @@ -271,9 +265,7 @@ export default { } } - if (nutrientMap[name]) { - this.nutrientData = nutrientMap[name] - } + this.nutrientData = nutrientMap[name] || {} } } } diff --git a/msh_single_uniapp/pages/tool/nutrition-knowledge.vue b/msh_single_uniapp/pages/tool/nutrition-knowledge.vue index 141fb1f..92114aa 100644 --- a/msh_single_uniapp/pages/tool/nutrition-knowledge.vue +++ b/msh_single_uniapp/pages/tool/nutrition-knowledge.vue @@ -36,7 +36,7 @@ class="nutrient-card" v-for="(item, index) in nutrientList" :key="index" - @click="goToNutrientDetail" :data-nutrient-index="index" + @click="goToNutrientDetail(index)" > {{ item.icon }} @@ -290,8 +290,7 @@ export default { this.currentTab = tab; await this.loadKnowledgeList(); }, - goToNutrientDetail(event) { - const index = event.currentTarget.dataset.nutrientIndex; + goToNutrientDetail(index) { const item = this.nutrientList[index]; if (!item) return; uni.navigateTo({ diff --git a/msh_single_uniapp/static/images/icon-recommendation.png b/msh_single_uniapp/static/images/icon-recommendation.png new file mode 100644 index 0000000..0f8fb46 Binary files /dev/null and b/msh_single_uniapp/static/images/icon-recommendation.png differ diff --git a/msh_single_uniapp/static/images/icon-search.png b/msh_single_uniapp/static/images/icon-search.png new file mode 100644 index 0000000..8e0f062 Binary files /dev/null and b/msh_single_uniapp/static/images/icon-search.png differ diff --git a/msh_single_uniapp/static/images/icon-share.png b/msh_single_uniapp/static/images/icon-share.png new file mode 100644 index 0000000..8e0f062 Binary files /dev/null and b/msh_single_uniapp/static/images/icon-share.png differ diff --git a/msh_single_uniapp/static/images/icon-suggestions.png b/msh_single_uniapp/static/images/icon-suggestions.png new file mode 100644 index 0000000..0f8fb46 Binary files /dev/null and b/msh_single_uniapp/static/images/icon-suggestions.png differ diff --git a/msh_single_uniapp/static/images/icon-why-important.png b/msh_single_uniapp/static/images/icon-why-important.png new file mode 100644 index 0000000..0f8fb46 Binary files /dev/null and b/msh_single_uniapp/static/images/icon-why-important.png differ