Browse Source

feat: 五维沙盘个人/全家视角切换 + 食物表字段扩展 + 忽略报告解析临时文件

Xiaogang Liao 2 months ago
parent
commit
197a9fc98c

+ 3 - 0
.gitignore

@@ -37,6 +37,9 @@ coverage/
 *~
 ~$*
 
+# 健康报告解析临时结果(含用户健康数据,不入库)
+**/uploads/json-results/
+
 # Sisyphus work directory
 .sisyphus/
 

+ 44 - 31
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -4584,30 +4584,36 @@ public class DatabaseInitializer implements CommandLineRunner {
 
         // 4张新表 (CREATE TABLE IF NOT EXISTS)
         String[] ddl = {
-                "CREATE TABLE IF NOT EXISTS foods ("
-                        + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
-                        + "name VARCHAR(100) NOT NULL COMMENT '食材名称', "
-                        + "category VARCHAR(50) COMMENT '分类: 蔬菜/水果/肉禽/水产/蛋奶/豆类/谷物/调味/其他', "
-                        + "unit VARCHAR(20) DEFAULT 'g' COMMENT '计量单位', "
-                        + "calories DECIMAL(8,2) COMMENT '热量(kcal/100g)', "
-                        + "protein DECIMAL(8,2) COMMENT '蛋白质(g/100g)', "
-                        + "fat DECIMAL(8,2) COMMENT '脂肪(g/100g)', "
-                        + "carbs DECIMAL(8,2) COMMENT '碳水(g/100g)', "
-                        + "fiber DECIMAL(8,2) COMMENT '膳食纤维(g/100g)', "
-                        + "price_level TINYINT COMMENT '价格等级 1-5', "
-                        + "nutrition_tags VARCHAR(500) COMMENT '营养标签JSON', "
-                        + "suitable_for VARCHAR(200) COMMENT '适宜人群JSON', "
-                        + "allergens VARCHAR(200) COMMENT '过敏原JSON', "
-                        + "external_source VARCHAR(100) COMMENT '数据来源', "
-                        + "external_id VARCHAR(100) COMMENT '外部ID', "
-                        + "cover_image VARCHAR(500) COMMENT '食材图片', "
-                        + "status VARCHAR(20) DEFAULT 'active', "
-                        + "sort_order INT DEFAULT 0, "
-                        + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
-                        + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
-                        + "INDEX idx_category (category), "
-                        + "INDEX idx_price (price_level)"
-                        + ");",
+"CREATE TABLE IF NOT EXISTS foods ("
+            + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
+            + "name VARCHAR(100) NOT NULL COMMENT '食材名称', "
+            + "category VARCHAR(50) COMMENT '分类: 蔬菜/水果/肉禽/水产/蛋奶/豆类/谷物/调味/其他', "
+            + "unit VARCHAR(20) DEFAULT 'g' COMMENT '计量单位', "
+            + "calories DECIMAL(8,2) COMMENT '热量(kcal/100g)', "
+            + "protein DECIMAL(8,2) COMMENT '蛋白质(g/100g)', "
+            + "fat DECIMAL(8,2) COMMENT '脂肪(g/100g)', "
+            + "carbs DECIMAL(8,2) COMMENT '碳水(g/100g)', "
+            + "fiber DECIMAL(8,2) COMMENT '膳食纤维(g/100g)', "
+            + "energy_kj DECIMAL(8,2) COMMENT '能量(kJ/100g)', "
+            + "starch DECIMAL(8,2) COMMENT '淀粉(g/100g)', "
+            + "cholesterol DECIMAL(8,2) COMMENT '胆固醇(mg/100g)', "
+            + "price_level TINYINT COMMENT '价格等级 1-5', "
+            + "nutrition_tags VARCHAR(500) COMMENT '营养标签JSON', "
+            + "suitable_for VARCHAR(200) COMMENT '适宜人群JSON', "
+            + "allergens VARCHAR(200) COMMENT '过敏原JSON', "
+            + "external_source VARCHAR(100) COMMENT '数据来源', "
+            + "external_id VARCHAR(100) COMMENT '外部ID', "
+            + "cover_image VARCHAR(500) COMMENT '食材图片', "
+            + "report_id BIGINT COMMENT '关联报告ID(从PDF解析的食物推荐)', "
+            + "score INT COMMENT '食物评分', "
+            + "diet_type VARCHAR(50) COMMENT '饮食类型', "
+            + "status VARCHAR(20) DEFAULT 'active', "
+            + "sort_order INT DEFAULT 0, "
+            + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
+            + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+            + "INDEX idx_category (category), "
+            + "INDEX idx_price (price_level)"
+            + ");",
 
                 "CREATE TABLE IF NOT EXISTS recipes ("
                         + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
@@ -8456,13 +8462,20 @@ public class DatabaseInitializer implements CommandLineRunner {
         ensureColumn("foods", "report_id", "BIGINT DEFAULT NULL COMMENT '关联报告ID(从PDF解析的食物推荐)'");
         ensureColumn("foods", "score", "INT DEFAULT NULL COMMENT '食物评分'");
 
-        // 迁移192: verification_codes.type 添加默认值(旧版插入未携带 type 字段,DB 列 NOT NULL 无默认值导致报错)
-        try {
-            jdbcTemplate.execute("ALTER TABLE verification_codes MODIFY COLUMN `type` VARCHAR(20) NOT NULL DEFAULT 'login' COMMENT '验证码类型: login/register/reset'");
-            log.info("已为verification_codes.type添加默认值");
-        } catch (Exception e) {
-            log.warn("verification_codes.type 默认值修改失败(可能已修改): " + e.getMessage());
-        }
+		// 迁移263: foods 补齐 energy_kj/starch/cholesterol/diet_type 列(前三列原迁移在文件重组时丢失、生产靠历史执行残留;
+		//   dietType 为实体孤立字段从未建列,MyBatis-Plus 全字段 SELECT 报 Unknown column)
+		ensureColumn("foods", "energy_kj", "DECIMAL(8,2) DEFAULT NULL COMMENT '能量(kJ/100g)'");
+		ensureColumn("foods", "starch", "DECIMAL(8,2) DEFAULT NULL COMMENT '淀粉(g/100g)'");
+		ensureColumn("foods", "cholesterol", "DECIMAL(8,2) DEFAULT NULL COMMENT '胆固醇(mg/100g)'");
+		ensureColumn("foods", "diet_type", "VARCHAR(50) DEFAULT NULL COMMENT '饮食类型'");
+
+		// 迁移192: verification_codes.type 添加默认值(旧版插入未携带 type 字段,DB 列 NOT NULL 无默认值导致报错)
+		try {
+			jdbcTemplate.execute("ALTER TABLE verification_codes MODIFY COLUMN `type` VARCHAR(20) NOT NULL DEFAULT 'login' COMMENT '验证码类型: login/register/reset'");
+			log.info("已为verification_codes.type添加默认值");
+		} catch (Exception e) {
+			log.warn("verification_codes.type 默认值修改失败(可能已修改): " + e.getMessage());
+		}
 
         // 迁移193: family_relationships表添加关系质量评分三列(关系条展示增强)
         ensureColumn("family_relationships", "trust_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)'");

+ 32 - 0
cfc-backend/src/main/resources/schema.sql

@@ -4409,6 +4409,38 @@ CREATE TABLE IF NOT EXISTS user_data_permissions (
 
 -- ===== 饮食模块 =====
 
+-- 食材库表
+CREATE TABLE IF NOT EXISTS foods (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(100) NOT NULL COMMENT '食材名称',
+    category VARCHAR(50) COMMENT '分类: 蔬菜/水果/肉禽/水产/蛋奶/豆类/谷物/调味/其他',
+    unit VARCHAR(20) DEFAULT 'g' COMMENT '计量单位',
+    calories DECIMAL(8,2) COMMENT '热量(kcal/100g)',
+    protein DECIMAL(8,2) COMMENT '蛋白质(g/100g)',
+    fat DECIMAL(8,2) COMMENT '脂肪(g/100g)',
+    carbs DECIMAL(8,2) COMMENT '碳水(g/100g)',
+    fiber DECIMAL(8,2) COMMENT '膳食纤维(g/100g)',
+    energy_kj DECIMAL(8,2) COMMENT '能量(kJ/100g)',
+    starch DECIMAL(8,2) COMMENT '淀粉(g/100g)',
+    cholesterol DECIMAL(8,2) COMMENT '胆固醇(mg/100g)',
+    price_level TINYINT COMMENT '价格等级 1-5',
+    nutrition_tags VARCHAR(500) COMMENT '营养标签JSON',
+    suitable_for VARCHAR(200) COMMENT '适宜人群JSON',
+    allergens VARCHAR(200) COMMENT '过敏原JSON',
+    external_source VARCHAR(100) COMMENT '数据来源',
+    external_id VARCHAR(100) COMMENT '外部ID',
+    cover_image VARCHAR(500) COMMENT '食材图片',
+    report_id BIGINT COMMENT '关联报告ID(从PDF解析的食物推荐)',
+    score INT COMMENT '食物评分',
+    status VARCHAR(20) DEFAULT 'active',
+    sort_order INT DEFAULT 0,
+    diet_type VARCHAR(50) COMMENT '饮食类型',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_category (category),
+    INDEX idx_price (price_level)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='食材库表';
+
 -- 饮食偏好调研表(每成员一份)
 CREATE TABLE IF NOT EXISTS diet_preferences (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,

+ 13 - 2
cfc-frontend/components/wuxing-sandbox.vue

@@ -10,7 +10,7 @@
   </slot>
 
   <!-- 个人/全家切换开关(仅 familyUnlocked 时显示) -->
-  <view class="sandbox-toggle" v-if="familyUnlocked && mode !== 'preview'">
+  <view class="sandbox-toggle" v-if="familyUnlocked && hasMultipleMembers && mode !== 'preview'">
     <view class="toggle-item" :class="activeClassName" @click="onToggle('personal')">
       <text class="toggle-icon">👤</text>
       <text class="toggle-text">个人</text>
@@ -169,7 +169,7 @@ export default {
   },
   data: function() {
     return {
-      viewMode: 'family',
+      viewMode: 'personal',
       selectedMemberIndex: 0,
       previewScores: [0, 0, 0, 0, 0],
       canvasReady: false,
@@ -317,6 +317,16 @@ export default {
         var dimOrder = newVal.map(function(d) { return d.code })
         this._flowAnim.refreshPaths({ dimOrder: dimOrder })
       }
+    },
+    currentView: function(newVal) {
+      if (newVal && newVal !== this.viewMode) {
+        this.viewMode = newVal
+      }
+    },
+    hasMultipleMembers: function(newVal) {
+      if (!newVal && this.viewMode === 'family') {
+        this.viewMode = 'personal'
+      }
     }
   },
   mounted: function() {
@@ -400,6 +410,7 @@ export default {
 
     /** 个人/全家切换(由父组件控制数据源) */
     onToggle: function(view) {
+      this.viewMode = view
       if (view === this.currentView) return
       this.$emit('view-change', view)
     },

+ 19 - 3
cfc-frontend/pages/index-home/index.vue

@@ -71,6 +71,9 @@
       :dimensions="energyDimensions"
       :totalEnergy="totalEnergy"
       :totalHealthIndex="totalHealthIndex"
+      :currentView="currentSandboxView"
+      :familyUnlocked="familyUnlocked"
+      @view-change="onSandboxViewChange"
       showDimensionBar
       starHeight="480rpx" />
 
@@ -363,7 +366,7 @@ import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 import DailyTaskCard from '../../components/daily-task-card.vue'
 import ReportUploadCard from '../../components/report-upload-card.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getEnergyOverview, getFamilyMemberList, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus } from '../../utils/api.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus } from '../../utils/api.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
 import { parseDate } from '../../utils/format.js'
@@ -430,6 +433,8 @@ export default {
       sandboxData: null,
       sandboxLocked: false,
       sandboxLockGate: null,
+      currentSandboxView: 'personal',
+      familyUnlocked: false,
       energyDimensions: [],
       totalEnergy: 0,
       totalHealthIndex: 0,
@@ -608,7 +613,7 @@ export default {
     },
     loadSandboxData: function() {
       var self = this
-      getFamilyEnergySandbox().then(function(res) {
+      getEnergySandbox(this.currentSandboxView).then(function(res) {
         if (res && res.data) {
           var d = res.data
           if (d.locked) {
@@ -619,13 +624,24 @@ export default {
           } else {
             self.sandboxLocked = false
             self.sandboxLockGate = null
-            self.sandboxData = d
+            if (d.view === 'personal') {
+              self.sandboxData = d.personalData || null
+              self.familyUnlocked = d.familyUnlocked || false
+            } else {
+              self.sandboxData = d.familyData || null
+            }
           }
           self._splashReady = true
           self._tryHideSplash()
         }
       }).catch(function() {})
     },
+    /** 切换沙盘个人/全家视角 */
+    onSandboxViewChange: function(view) {
+      if (view === this.currentSandboxView) return
+      this.currentSandboxView = view
+      this.loadSandboxData()
+    },
     loadActionArticles: function() {
       var self = this
       getFeaturedArticles({ size: 5 }).then(function(res) {