Jelajahi Sumber

fix: add 9 missing tables (migrations 117-125) + fix JwtInterceptor familyId + fix test emotion childId

E2E Test Bot 1 bulan lalu
induk
melakukan
e139d807e3

+ 207 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -5782,6 +5782,213 @@ try {
     // 迁移116: growth_records表添加family_member_id和family_member_age列(修复/record/list 500错误)
     ensureColumn("growth_records", "family_member_id", "BIGINT COMMENT '关联家庭成员ID'");
     ensureColumn("growth_records", "family_member_age", "INT COMMENT '成员年龄快照'");
+
+    // 迁移117: 创建 emotion_checkin 表情绪打卡表(Fix 心理与情绪模块 500 错误)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_checkin (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
+            "mood_weather VARCHAR(50) COMMENT '心情晴雨表: sunny/cloudy/rainy/stormy', " +
+            "emotion_tags VARCHAR(200) COMMENT '情绪标签(逗号分隔)', " +
+            "photo_url VARCHAR(500) COMMENT '配图URL', " +
+            "note TEXT COMMENT '文字备注', " +
+            "energy_awarded INT DEFAULT 0 COMMENT '奖励能量值', " +
+            "checkin_date DATE COMMENT '打卡日期', " +
+            "mood_score INT COMMENT '心情分数(1-10)', " +
+            "emotion_type VARCHAR(50) COMMENT '情绪类型', " +
+            "stress_level INT COMMENT '压力等级(1-10)', " +
+            "arousal_level INT COMMENT '激活水平(1-10)', " +
+            "energy_level INT COMMENT '精力水平(1-10)', " +
+            "sleep_hours DECIMAL(4,1) COMMENT '睡眠时长', " +
+            "sleep_quality INT COMMENT '睡眠质量(1-10)', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_child_date (child_id, checkin_date)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪打卡记录'");
+        log.info("已创建emotion_checkin表");
+    } catch (Exception e) {
+        log.warn("创建emotion_checkin表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移118: 创建 emotion_alert 表情绪告警表
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_alert (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
+            "alert_type VARCHAR(50) NOT NULL COMMENT '告警类型: consecutive_low_mood/crisis_keyword/sharp_decline', " +
+            "severity VARCHAR(20) NOT NULL DEFAULT 'low' COMMENT '严重级别: low/medium/high', " +
+            "title VARCHAR(200) COMMENT '告警标题', " +
+            "message TEXT COMMENT '告警具体描述', " +
+            "related_checkin_id BIGINT COMMENT '关联情绪打卡ID', " +
+            "resolved TINYINT(1) DEFAULT 0 COMMENT '是否已处理', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "resolved_at DATETIME COMMENT '处理时间', " +
+            "INDEX idx_child (child_id), " +
+            "INDEX idx_alert_type (alert_type)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪告警'");
+        log.info("已创建emotion_alert表");
+    } catch (Exception e) {
+        log.warn("创建emotion_alert表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移119: 创建 membership_levels 表(会员等级配置)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS membership_levels (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "level_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/BASIC/PROFESSIONAL/ENTERPRISE', " +
+            "level_name VARCHAR(100) NOT NULL COMMENT '等级名称', " +
+            "level_desc TEXT COMMENT '等级描述', " +
+            "price_monthly INT COMMENT '月费(分)', " +
+            "price_quarterly INT COMMENT '季费(分)', " +
+            "price_yearly INT COMMENT '年费(分)', " +
+            "features TEXT COMMENT '功能权限列表(JSON)', " +
+            "max_children INT DEFAULT 0 COMMENT '可添加孩子数量', " +
+            "max_tasks_per_day INT DEFAULT 0 COMMENT '每日任务上限', " +
+            "ai_review_enabled TINYINT(1) DEFAULT 0 COMMENT 'AI审核功能', " +
+            "priority_support TINYINT(1) DEFAULT 0 COMMENT '优先客服', " +
+            "teacher_consultation TINYINT(1) DEFAULT 0 COMMENT '老师咨询', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "UNIQUE KEY uk_level_code (level_code)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级配置'");
+        log.info("已创建membership_levels表");
+    } catch (Exception e) {
+        log.warn("创建membership_levels表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移120: 创建 family_memberships 表(家庭会员订阅记录)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_memberships (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+            "level_code VARCHAR(50) NOT NULL DEFAULT 'FREE' COMMENT '会员等级编码', " +
+            "start_date DATETIME COMMENT '开始时间', " +
+            "end_date DATETIME COMMENT '结束时间', " +
+            "payment_method VARCHAR(50) COMMENT '支付方式: wechat/alipay/card', " +
+            "payment_status VARCHAR(20) DEFAULT 'pending' COMMENT '支付状态: pending/paid/expired/refunded', " +
+            "order_no VARCHAR(100) COMMENT '订单号', " +
+            "transaction_id VARCHAR(200) COMMENT '交易流水号', " +
+            "amount INT COMMENT '支付金额(分)', " +
+            "auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_family_id (family_id), " +
+            "INDEX idx_end_date (end_date)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭会员订阅'");
+        log.info("已创建family_memberships表");
+    } catch (Exception e) {
+        log.warn("创建family_memberships表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移121: 创建 share_events 表(分享事件记录)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS share_events (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "user_id BIGINT NOT NULL COMMENT '分享用户ID', " +
+            "target_type VARCHAR(50) NOT NULL COMMENT '分享目标类型: article/growth/product', " +
+            "target_id BIGINT NOT NULL COMMENT '分享目标ID', " +
+            "share_channel VARCHAR(50) COMMENT '分享渠道: wechat/timeline/qq/url', " +
+            "share_title VARCHAR(200) COMMENT '分享标题', " +
+            "share_url VARCHAR(500) COMMENT '分享URL', " +
+            "click_count INT DEFAULT 0 COMMENT '点击次数', " +
+            "read_count INT DEFAULT 0 COMMENT '阅读次数', " +
+            "reward_points INT DEFAULT 0 COMMENT '奖励积分', " +
+            "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/expired', " +
+            "expires_at DATETIME COMMENT '过期时间', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_user (user_id), " +
+            "INDEX idx_target (target_type, target_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分享事件'");
+        log.info("已创建share_events表");
+    } catch (Exception e) {
+        log.warn("创建share_events表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移122: 创建 share_reads 表(分享阅读记录)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS share_reads (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "share_event_id BIGINT NOT NULL COMMENT '分享事件ID', " +
+            "reader_id BIGINT COMMENT '阅读者用户ID', " +
+            "reader_ip VARCHAR(50) COMMENT '阅读者IP', " +
+            "read_duration INT COMMENT '阅读时长(秒)', " +
+            "read_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间', " +
+            "INDEX idx_share_event (share_event_id), " +
+            "INDEX idx_reader (reader_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分享阅读记录'");
+        log.info("已创建share_reads表");
+    } catch (Exception e) {
+        log.warn("创建share_reads表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移123: 创建 food_recommend_idx 表(饮食推荐指数)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS food_recommend_idx (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+            "food_id BIGINT NOT NULL COMMENT '食材ID', " +
+            "food_name VARCHAR(100) COMMENT '食材名称', " +
+            "index_score INT COMMENT '推荐指数(0-100)', " +
+            "health_report_id BIGINT COMMENT '关联健康报告ID', " +
+            "calculated_at DATETIME COMMENT '计算时间', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_user (user_id), " +
+            "INDEX idx_food (food_id), " +
+            "INDEX idx_report (health_report_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食推荐指数'");
+        log.info("已创建food_recommend_idx表");
+    } catch (Exception e) {
+        log.warn("创建food_recommend_idx表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移124: 创建 points_exchange_products 表(积分兑换商品)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS points_exchange_products (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "name VARCHAR(200) NOT NULL COMMENT '商品名称', " +
+            "description TEXT COMMENT '商品描述', " +
+            "cover_image VARCHAR(500) COMMENT '封面图片', " +
+            "points_price INT NOT NULL COMMENT '积分价格', " +
+            "stock INT DEFAULT 0 COMMENT '库存', " +
+            "product_type VARCHAR(50) COMMENT '商品类型', " +
+            "category VARCHAR(50) COMMENT '分类', " +
+            "expiry_days INT COMMENT '有效期(天)', " +
+            "sort_order INT DEFAULT 0 COMMENT '排序', " +
+            "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/disabled', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_status (status), " +
+            "INDEX idx_category (category)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分兑换商品'");
+        log.info("已创建points_exchange_products表");
+    } catch (Exception e) {
+        log.warn("创建points_exchange_products表可能已存在: {}", e.getMessage());
+    }
+
+    // 迁移125: 创建 points_exchange_records 表(积分兑换记录)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS points_exchange_records (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "user_id BIGINT COMMENT '用户ID', " +
+            "child_id BIGINT COMMENT '孩子ID', " +
+            "family_member_id BIGINT COMMENT '家庭成员ID', " +
+            "product_id BIGINT NOT NULL COMMENT '商品ID', " +
+            "product_name VARCHAR(200) COMMENT '商品名称快照', " +
+            "points_cost INT NOT NULL COMMENT '消耗积分', " +
+            "quantity INT DEFAULT 1 COMMENT '数量', " +
+            "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/completed/cancelled', " +
+            "redeem_code VARCHAR(100) COMMENT '兑换码', " +
+            "expired_at DATETIME COMMENT '过期时间', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_user (user_id), " +
+            "INDEX idx_product (product_id), " +
+            "INDEX idx_status (status)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分兑换记录'");
+        log.info("已创建points_exchange_records表");
+    } catch (Exception e) {
+        log.warn("创建points_exchange_records表可能已存在: {}", e.getMessage());
     }
 
     private void runMigration82() {

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -1,5 +1,7 @@
 package com.etotem.cfc.config;
 
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import io.jsonwebtoken.Claims;
 import lombok.extern.slf4j.Slf4j;
 import javax.annotation.Resource;
@@ -19,6 +21,9 @@ public class JwtInterceptor implements HandlerInterceptor {
     @Resource
     private JwtConfig jwtConfig;
 
+    @Resource
+    private UserMapper userMapper;
+
     private static final String[] PUBLIC_PATHS = {
         "/api/auth/",
         "/api/auth/verify",
@@ -93,6 +98,16 @@ public class JwtInterceptor implements HandlerInterceptor {
             request.setAttribute("role", role);
             request.setAttribute("roles", roles);
 
+            // 设置familyId(查询用户所属家庭)
+            try {
+                User user = userMapper.selectById(userId);
+                if (user != null && user.getFamilyId() != null) {
+                    request.setAttribute("familyId", user.getFamilyId());
+                }
+            } catch (Exception e) {
+                log.warn("查询用户familyId失败: userId={}, error={}", userId, e.getMessage());
+            }
+
             return true;
         } catch (Exception e) {
         log.warn("Token解析失败: {}", e.getMessage());

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

@@ -3280,3 +3280,184 @@ CREATE TABLE IF NOT EXISTS activity_feedback (
     INDEX idx_user_id (user_id),
     INDEX idx_child_id (child_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动评价表';
+
+-- ============================================================
+-- 情绪打卡记录表(迁移117)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS emotion_checkin (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    child_id BIGINT NOT NULL COMMENT '家庭成员ID',
+    mood_weather VARCHAR(50) COMMENT '心情晴雨表: sunny/cloudy/rainy/stormy',
+    emotion_tags VARCHAR(200) COMMENT '情绪标签(逗号分隔)',
+    photo_url VARCHAR(500) COMMENT '配图URL',
+    note TEXT COMMENT '文字备注',
+    energy_awarded INT DEFAULT 0 COMMENT '奖励能量值',
+    checkin_date DATE COMMENT '打卡日期',
+    mood_score INT COMMENT '心情分数(1-10)',
+    emotion_type VARCHAR(50) COMMENT '情绪类型',
+    stress_level INT COMMENT '压力等级(1-10)',
+    arousal_level INT COMMENT '激活水平(1-10)',
+    energy_level INT COMMENT '精力水平(1-10)',
+    sleep_hours DECIMAL(4,1) COMMENT '睡眠时长',
+    sleep_quality INT COMMENT '睡眠质量(1-10)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_child_date (child_id, checkin_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪打卡记录';
+
+-- ============================================================
+-- 情绪告警表(迁移118)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS emotion_alert (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    child_id BIGINT NOT NULL COMMENT '家庭成员ID',
+    alert_type VARCHAR(50) NOT NULL COMMENT '告警类型: consecutive_low_mood/crisis_keyword/sharp_decline',
+    severity VARCHAR(20) NOT NULL DEFAULT 'low' COMMENT '严重级别: low/medium/high',
+    title VARCHAR(200) COMMENT '告警标题',
+    message TEXT COMMENT '告警具体描述',
+    related_checkin_id BIGINT COMMENT '关联情绪打卡ID',
+    resolved TINYINT(1) DEFAULT 0 COMMENT '是否已处理',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    resolved_at DATETIME COMMENT '处理时间',
+    INDEX idx_child (child_id),
+    INDEX idx_alert_type (alert_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪告警';
+
+-- ============================================================
+-- 会员等级配置表(迁移119)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS membership_levels (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    level_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/BASIC/PROFESSIONAL/ENTERPRISE',
+    level_name VARCHAR(100) NOT NULL COMMENT '等级名称',
+    level_desc TEXT COMMENT '等级描述',
+    price_monthly INT COMMENT '月费(分)',
+    price_quarterly INT COMMENT '季费(分)',
+    price_yearly INT COMMENT '年费(分)',
+    features TEXT COMMENT '功能权限列表(JSON)',
+    max_children INT DEFAULT 0 COMMENT '可添加孩子数量',
+    max_tasks_per_day INT DEFAULT 0 COMMENT '每日任务上限',
+    ai_review_enabled TINYINT(1) DEFAULT 0 COMMENT 'AI审核功能',
+    priority_support TINYINT(1) DEFAULT 0 COMMENT '优先客服',
+    teacher_consultation TINYINT(1) DEFAULT 0 COMMENT '老师咨询',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_level_code (level_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级配置';
+
+-- ============================================================
+-- 家庭会员订阅表(迁移120)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS family_memberships (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    level_code VARCHAR(50) NOT NULL DEFAULT 'FREE' COMMENT '会员等级编码',
+    start_date DATETIME COMMENT '开始时间',
+    end_date DATETIME COMMENT '结束时间',
+    payment_method VARCHAR(50) COMMENT '支付方式: wechat/alipay/card',
+    payment_status VARCHAR(20) DEFAULT 'pending' COMMENT '支付状态: pending/paid/expired/refunded',
+    order_no VARCHAR(100) COMMENT '订单号',
+    transaction_id VARCHAR(200) COMMENT '交易流水号',
+    amount INT COMMENT '支付金额(分)',
+    auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_family_id (family_id),
+    INDEX idx_end_date (end_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭会员订阅';
+
+-- ============================================================
+-- 分享事件表(迁移121)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS share_events (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '分享用户ID',
+    target_type VARCHAR(50) NOT NULL COMMENT '分享目标类型: article/growth/product',
+    target_id BIGINT NOT NULL COMMENT '分享目标ID',
+    share_channel VARCHAR(50) COMMENT '分享渠道: wechat/timeline/qq/url',
+    share_title VARCHAR(200) COMMENT '分享标题',
+    share_url VARCHAR(500) COMMENT '分享URL',
+    click_count INT DEFAULT 0 COMMENT '点击次数',
+    read_count INT DEFAULT 0 COMMENT '阅读次数',
+    reward_points INT DEFAULT 0 COMMENT '奖励积分',
+    status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/expired',
+    expires_at DATETIME COMMENT '过期时间',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_user (user_id),
+    INDEX idx_target (target_type, target_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分享事件';
+
+-- ============================================================
+-- 分享阅读记录表(迁移122)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS share_reads (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    share_event_id BIGINT NOT NULL COMMENT '分享事件ID',
+    reader_id BIGINT COMMENT '阅读者用户ID',
+    reader_ip VARCHAR(50) COMMENT '阅读者IP',
+    read_duration INT COMMENT '阅读时长(秒)',
+    read_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间',
+    INDEX idx_share_event (share_event_id),
+    INDEX idx_reader (reader_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分享阅读记录';
+
+-- ============================================================
+-- 饮食推荐指数表(迁移123)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS food_recommend_idx (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    food_id BIGINT NOT NULL COMMENT '食材ID',
+    food_name VARCHAR(100) COMMENT '食材名称',
+    index_score INT COMMENT '推荐指数(0-100)',
+    health_report_id BIGINT COMMENT '关联健康报告ID',
+    calculated_at DATETIME COMMENT '计算时间',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_user (user_id),
+    INDEX idx_food (food_id),
+    INDEX idx_report (health_report_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食推荐指数';
+
+-- ============================================================
+-- 积分兑换商品表(迁移124)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS points_exchange_products (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(200) NOT NULL COMMENT '商品名称',
+    description TEXT COMMENT '商品描述',
+    cover_image VARCHAR(500) COMMENT '封面图片',
+    points_price INT NOT NULL COMMENT '积分价格',
+    stock INT DEFAULT 0 COMMENT '库存',
+    product_type VARCHAR(50) COMMENT '商品类型',
+    category VARCHAR(50) COMMENT '分类',
+    expiry_days INT COMMENT '有效期(天)',
+    sort_order INT DEFAULT 0 COMMENT '排序',
+    status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/disabled',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_status (status),
+    INDEX idx_category (category)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分兑换商品';
+
+-- ============================================================
+-- 积分兑换记录表(迁移125)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS points_exchange_records (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT COMMENT '用户ID',
+    child_id BIGINT COMMENT '孩子ID',
+    family_member_id BIGINT COMMENT '家庭成员ID',
+    product_id BIGINT NOT NULL COMMENT '商品ID',
+    product_name VARCHAR(200) COMMENT '商品名称快照',
+    points_cost INT NOT NULL COMMENT '消耗积分',
+    quantity INT DEFAULT 1 COMMENT '数量',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/completed/cancelled',
+    redeem_code VARCHAR(100) COMMENT '兑换码',
+    expired_at DATETIME COMMENT '过期时间',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_user (user_id),
+    INDEX idx_product (product_id),
+    INDEX idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分兑换记录';

+ 6 - 3
tests/e2e/flow-l1-tests.js

@@ -920,10 +920,13 @@ async function testMindFlow() {
   var t = tokens.child;
   if (!t) { skip(F, 'child', '所有测试', '未登录'); return; }
 
-  // 情绪打卡(EmotionCheckinDTO - 服务端用userId=child的userId找孩子
+  // 情绪打卡(childId=1001 为 child user_id=1003 对应的 family_member_id
   var r = await apiCall('POST', '/api/mind/checkin/create', {
-    emotion: 'happy',
-    intensity: 8,
+    childId: 1001,
+    moodWeather: 'sunny',
+    moodScore: 8,
+    emotionType: 'joy',
+    stressLevel: 2,
     note: 'Feeling good today'
   }, t);
   record(F, 'child', '情绪打卡', r.status === 200 && r.body && r.body.code === 200, r.body ? 'emotion checkin OK' : r.body ? r.body.message : r.raw);