feat: 使用Coze工作流自动补齐帖子营养数据

- 后端: fillNutrition()改用Coze工作流7613879935811354687分析营养成分
- 后端: 新增parseWorkflowResponse/nestedToFlat解析工作流响应
- 前端: 页面加载时自动检测并补齐不完整的营养统计
- 修复: ToolKnowledgeServiceImpl编译错误(Integer转Long)

工作流输出格式: output.{calories,protein,potassium,phosphorus}.{value,unit}
This commit is contained in:
2026-03-07 12:36:56 +08:00
parent d62f934d7f
commit 8f94035703
3 changed files with 144 additions and 16 deletions

View File

@@ -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)