diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java index c0a2448..81095de 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java @@ -23,6 +23,9 @@ import com.zbkj.service.service.UserService; import com.zbkj.service.service.UserSignService; import com.zbkj.service.service.tool.ToolCommunityService; import com.zbkj.service.service.tool.ToolNutritionFillService; +import com.zbkj.service.service.tool.ToolCozeService; +import com.zbkj.common.request.coze.CozeWorkflowRequest; +import com.zbkj.common.response.CozeBaseResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -71,6 +74,9 @@ public class ToolCommunityServiceImpl implements ToolCommunityService { @Resource private ToolNutritionFillService toolNutritionFillService; + @Resource + private ToolCozeService toolCozeService; + /** * 获取社区内容列表 * @param pageParamRequest 分页参数 @@ -593,6 +599,7 @@ public class ToolCommunityServiceImpl implements ToolCommunityService { /** * 根据帖子内容用 AI 填充营养数据并更新帖子 + * 使用 Coze 工作流 7613879935811354687 进行营养成分分析 * @param postId 帖子ID * @return 填充后的营养数据 */ @@ -611,14 +618,100 @@ public class ToolCommunityServiceImpl implements ToolCommunityService { if (StrUtil.isNotBlank(post.getContent())) text.append(post.getContent()); if (text.length() == 0) throw new CrmebException("帖子标题和内容为空,无法估算营养"); - Map nutrition = toolNutritionFillService.fillFromText(text.toString()); - if (nutrition.isEmpty()) throw new CrmebException("AI 未能估算出营养数据"); - - // 存库使用嵌套格式(value/unit),与 v2_community_posts.nutrition_data_json 约定一致 - Map nested = toolNutritionFillService.flatToNestedForDb(nutrition); - post.setNutritionDataJson(JSON.toJSONString(nested)); + // 使用 Coze 工作流进行营养成分分析 + CozeWorkflowRequest workflowRequest = new CozeWorkflowRequest(); + workflowRequest.setWorkflowId("7613879935811354687"); + Map params = new HashMap<>(); + params.put("text", text.toString()); + workflowRequest.setParameters(params); + + CozeBaseResponse response = toolCozeService.workflow(workflowRequest); + if (response == null || response.getData() == null) { + throw new CrmebException("Coze 工作流调用失败"); + } + + // 解析工作流返回结果 + Map workflowResult = parseWorkflowResponse(response.getData()); + if (workflowResult == null || workflowResult.isEmpty()) { + throw new CrmebException("AI 未能估算出营养数据"); + } + + // 提取 output 中的营养数据(嵌套格式) + @SuppressWarnings("unchecked") + Map output = (Map) workflowResult.get("output"); + if (output == null || output.isEmpty()) { + throw new CrmebException("AI 返回数据格式错误"); + } + + // 将嵌套格式转为扁平格式返回给前端 + Map flatNutrition = nestedToFlat(output); + + // 存库使用嵌套格式(value/unit) + post.setNutritionDataJson(JSON.toJSONString(output)); post.setUpdatedAt(new Date()); v2CommunityPostDao.updateById(post); - return nutrition; + + return flatNutrition; + } + + /** + * 解析 Coze 工作流响应 + */ + @SuppressWarnings("unchecked") + private Map parseWorkflowResponse(Object data) { + if (data == null) return null; + if (data instanceof Map) { + return (Map) data; + } + // 如果是 JSON 字符串,尝试解析 + if (data instanceof String) { + try { + return JSON.parseObject((String) data, Map.class); + } catch (Exception e) { + log.warn("Failed to parse workflow response as JSON: {}", e.getMessage()); + return null; + } + } + return null; + } + + /** + * 将嵌套格式转为扁平格式 + * 输入: {"calories":{"value":103,"unit":"kcal"},...} + * 输出: {"energyKcal":103,"proteinG":17.5,...} + */ + @SuppressWarnings("unchecked") + private Map nestedToFlat(Map nested) { + Map flat = new HashMap<>(); + if (nested == null) return flat; + + // 映射关系:嵌套 key -> 扁平 key + copyFlat(flat, nested, "calories", "energyKcal"); + copyFlat(flat, nested, "protein", "proteinG"); + copyFlat(flat, nested, "potassium", "potassiumMg"); + copyFlat(flat, nested, "phosphorus", "phosphorusMg"); + copyFlat(flat, nested, "fat", "fatG"); + copyFlat(flat, nested, "carbohydrate", "carbohydratesG"); + copyFlat(flat, nested, "sodium", "sodiumMg"); + + return flat; + } + + @SuppressWarnings("unchecked") + private void copyFlat(Map flat, Map nested, + String nestedKey, String flatKey) { + Object entry = nested.get(nestedKey); + if (!(entry instanceof Map)) return; + Object value = ((Map) entry).get("value"); + if (value instanceof Number) { + flat.put(flatKey, ((Number) value).doubleValue()); + } else if (value != null) { + // 尝试转换为数字 + try { + flat.put(flatKey, Double.parseDouble(value.toString())); + } catch (NumberFormatException e) { + flat.put(flatKey, value); + } + } } } diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java index 3a4e0bd..14c41b7 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java @@ -161,7 +161,7 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService { for (String type : new String[]{"guide", "article", "nutrients", "recipe"}) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); lqw.eq(V2Knowledge::getType, type); - byType.put(type, v2KnowledgeDao.selectCount(lqw)); + byType.put(type, Long.valueOf(v2KnowledgeDao.selectCount(lqw))); } stats.put("byType", byType); @@ -169,7 +169,7 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService { for (String status : new String[]{"published", "draft", "deleted"}) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); lqw.eq(V2Knowledge::getStatus, status); - byStatus.put(status, v2KnowledgeDao.selectCount(lqw)); + byStatus.put(status, Long.valueOf(v2KnowledgeDao.selectCount(lqw))); } stats.put("byStatus", byStatus); return stats; diff --git a/msh_single_uniapp/pages/tool/post-detail.vue b/msh_single_uniapp/pages/tool/post-detail.vue index 89cd2d0..2ec9cf6 100644 --- a/msh_single_uniapp/pages/tool/post-detail.vue +++ b/msh_single_uniapp/pages/tool/post-detail.vue @@ -385,7 +385,7 @@ export default { buildNutritionStatsFromDetailData(data) { if (!data) return [] - // 1) 后端直接返回的 stat 数组(兼容不同命名) + // 1) 后端直接返回的 stat 数组或对象(兼容不同命名) const rawStats = data.nutritionStats || data.nutrition_stats if (Array.isArray(rawStats) && rawStats.length > 0) { const mapped = rawStats.map(s => ({ @@ -398,6 +398,11 @@ export default { } return mapped } + // 1b) 后端返回 nutritionStats 为对象(如 { protein: 56, calories: 200 })时按营养对象解析 + if (rawStats && typeof rawStats === 'object' && !Array.isArray(rawStats) && Object.keys(rawStats).length > 0) { + const statsFromObj = this.buildNutritionStatsFromNutritionObject(rawStats) + if (statsFromObj.length > 0 && !statsFromObj.every(s => s.value === '-')) return statsFromObj + } // 2) nutritionDataJson / nutrition_data_json(兼容后端驼峰与下划线;含 fill-nutrition 的 energyKcal/proteinG/potassiumMg/phosphorusMg、打卡字段 actualEnergy/actualProtein) const jsonRaw = data.nutritionDataJson || data.nutrition_data_json @@ -454,13 +459,42 @@ export default { return [] }, - /** 从单一营养对象构建 [{label, value}, ...],统一兼容 energyKcal/proteinG/potassiumMg/phosphorusMg 及 calories/protein、下划线命名 */ + /** + * 从对象中取数值,兼容后端嵌套格式 { calories: { value, unit } } 与扁平格式 energyKcal/proteinG + * 嵌套 value 支持 number 或 string(后端 BigDecimal 等可能序列化为字符串) + */ + readNutritionNumber(obj, ...flatKeys) { + if (!obj || typeof obj !== 'object') return null + for (const key of flatKeys) { + const v = obj[key] + if (v == null) continue + if (typeof v === 'number' && !Number.isNaN(v)) return v + // 嵌套格式 { value, unit }:value 可能为 number 或 string + if (typeof v === 'object' && v !== null && 'value' in v) { + const val = v.value + if (typeof val === 'number' && !Number.isNaN(val)) return val + const str = typeof val === 'string' ? val.trim() : (val != null ? String(val) : '') + if (str !== '' && str !== 'undefined' && str !== 'null') { + const n = parseFloat(str) + if (!Number.isNaN(n)) return n + } + } + const s = typeof v === 'string' ? v.trim() : String(v) + if (s !== '' && s !== 'undefined' && s !== 'null') { + const n = parseFloat(s) + if (!Number.isNaN(n)) return n + } + } + return null + }, + + /** 从单一营养对象构建 [{label, value}, ...],统一兼容 energyKcal/proteinG、嵌套 calories: { value, unit }、下划线命名 */ buildNutritionStatsFromNutritionObject(obj) { if (!obj || typeof obj !== 'object') return [] - const cal = obj.energyKcal ?? obj.energy_kcal ?? obj.calories ?? obj.energy ?? obj.calorie ?? obj.actualEnergy - const pro = obj.proteinG ?? obj.protein_g ?? obj.protein ?? obj.proteins ?? obj.actualProtein - const pot = obj.potassiumMg ?? obj.potassium_mg ?? obj.potassium ?? obj.k - const pho = obj.phosphorusMg ?? obj.phosphorus_mg ?? obj.phosphorus ?? obj.p + const cal = this.readNutritionNumber(obj, 'energyKcal', 'energy_kcal', 'calories', 'energy', 'calorie', 'actualEnergy') + const pro = this.readNutritionNumber(obj, 'proteinG', 'protein_g', 'protein', 'proteins', 'actualProtein') + const pot = this.readNutritionNumber(obj, 'potassiumMg', 'potassium_mg', 'potassium', 'k') + const pho = this.readNutritionNumber(obj, 'phosphorusMg', 'phosphorus_mg', 'phosphorus', 'p') return [ { label: '热量(kcal)', value: cal != null ? String(cal) : '-' }, { label: '蛋白质', value: this.formatNutritionValue(pro, 'g') }, @@ -548,8 +582,9 @@ export default { const data = (res && res.data !== undefined && res.data !== null) ? res.data : res if (!data) return const stats = this.buildNutritionStatsFromDetailData(data) - if (stats.length > 0) { + if (stats.length > 0 && !stats.every(s => s.value === '-' || s.value === '')) { this.$set(this.postData, 'nutritionStats', stats) + console.log('[post-detail] 营养数据已补齐:', stats) } } catch (e) { console.warn('服务端填充帖子营养失败:', e)