Просмотр исходного кода

feat: update backend config, schema, entities and services for new modules

DatabaseInitializer schema migration, WebConfig JWT paths + resource handler, JwtInterceptor API path updates, schema.sql additions, entity/service updates for article/energy/commission/badge support

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 3 месяцев назад
Родитель
Сommit
68b487925c

+ 351 - 8
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -447,10 +447,10 @@ public class DatabaseInitializer implements CommandLineRunner {
             "status TINYINT DEFAULT 1 COMMENT '状态:0禁用1启用', " +
             "status TINYINT DEFAULT 1 COMMENT '状态:0禁用1启用', " +
             "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
             "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
             "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
             "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-            "INDEX idx_game_code (game_code), " +
-            "INDEX idx_status (status)" +
-            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
-);
+"INDEX idx_game_code (game_code), " +
+"INDEX idx_status (status)" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
+        );
 
 
         // 执行创建表SQL
         // 执行创建表SQL
         for (String sql : createTableSQLs) {
         for (String sql : createTableSQLs) {
@@ -1199,7 +1199,7 @@ log.info("已添加template_id列到tasks表");
             log.warn("检查/添加个人信息补充字段失败: {}", e.getMessage());
             log.warn("检查/添加个人信息补充字段失败: {}", e.getMessage());
         }
         }
 
 
-        // Phase 0: 福俱乐部 — 服务商字段
+        // Phase 0: 浠艾福俱乐部 — 服务商字段
         try {
         try {
             Integer exists = jdbcTemplate.queryForObject(
             Integer exists = jdbcTemplate.queryForObject(
                 "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'vendor_type'",
                 "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'vendor_type'",
@@ -1210,9 +1210,9 @@ log.info("已添加template_id列到tasks表");
                 jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_reject_reason VARCHAR(500) DEFAULT NULL COMMENT '审核拒绝原因'");
                 jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_reject_reason VARCHAR(500) DEFAULT NULL COMMENT '审核拒绝原因'");
                 jdbcTemplate.execute("ALTER TABLE users ADD COLUMN is_family_admin TINYINT DEFAULT 0 COMMENT '是否家庭管理员'");
                 jdbcTemplate.execute("ALTER TABLE users ADD COLUMN is_family_admin TINYINT DEFAULT 0 COMMENT '是否家庭管理员'");
                 jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_info VARCHAR(2000) DEFAULT NULL COMMENT '服务商资质信息(JSON)'");
                 jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_info VARCHAR(2000) DEFAULT NULL COMMENT '服务商资质信息(JSON)'");
-                log.info("已添加福俱乐部服务商字段到users表");
+                log.info("已添加浠艾福俱乐部服务商字段到users表");
             } else {
             } else {
-                log.info("福俱乐部服务商字段已存在,跳过");
+                log.info("浠艾福俱乐部服务商字段已存在,跳过");
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
             log.warn("检查/添加服务商字段失败: {}", e.getMessage());
             log.warn("检查/添加服务商字段失败: {}", e.getMessage());
@@ -1230,7 +1230,7 @@ log.info("已添加template_id列到tasks表");
             log.warn("迁移规划师到服务商体系失败: {}", e.getMessage());
             log.warn("迁移规划师到服务商体系失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== Phase 1: 福俱乐部商品系统 ====================
+        // ==================== Phase 1: 浠艾福俱乐部商品系统 ====================
 
 
         // 商品表
         // 商品表
         try {
         try {
@@ -1332,6 +1332,336 @@ log.info("已添加template_id列到tasks表");
             log.warn("Product 种子数据初始化失败: {}", e.getMessage());
             log.warn("Product 种子数据初始化失败: {}", e.getMessage());
         }
         }
 
 
+        // ==================== Phase 2: 内容区块可见性配置 ====================
+
+        // 内容区块配置表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS content_sections (" +
+                "  id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                "  page_key VARCHAR(50) NOT NULL," +
+                "  section_key VARCHAR(50) NOT NULL," +
+                "  title VARCHAR(100)," +
+                "  allowed_roles VARCHAR(255) DEFAULT '[\"anonymous\"]'," +
+                "  sort_order INT DEFAULT 0," +
+                "  status VARCHAR(20) DEFAULT 'active'," +
+                "  created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+                "  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")"
+            );
+            log.info("content_sections 表已创建");
+        } catch (Exception e) {
+            log.warn("创建 content_sections 表失败: {}", e.getMessage());
+        }
+
+        // ContentSection 种子数据
+        try {
+            insertContentSectionSeed("mind", "func_entries", "功能入口", "[\"anonymous\"]", 1);
+            insertContentSectionSeed("mind", "daily_tip", "每日心理", "[\"anonymous\"]", 2);
+            insertContentSectionSeed("mind", "recommended_reading", "推荐阅读", "[\"anonymous\"]", 3);
+            insertContentSectionSeed("mind", "premium_content", "会员专属内容", "[\"parent\",\"teacher\"]", 4);
+            insertContentSectionSeed("body", "func_entries", "功能入口", "[\"anonymous\"]", 1);
+            insertContentSectionSeed("body", "daily_stats", "今日数据", "[\"anonymous\"]", 2);
+            insertContentSectionSeed("body", "health_tips", "健康小贴士", "[\"anonymous\"]", 3);
+            insertContentSectionSeed("body", "teacher_tips", "规划师推荐", "[\"teacher\"]", 4);
+            log.info("ContentSection 种子数据已加载");
+        } catch (Exception e) {
+            log.warn("ContentSection 种子数据初始化失败: {}", e.getMessage());
+        }
+
+        // ==================== 文章内容系统种子数据 ====================
+
+        // 文章分类种子数据
+        try {
+            Integer catCount = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM article_categories WHERE id IN (1,2,3,4,5)", Integer.class);
+            if (catCount == null || catCount == 0) {
+                jdbcTemplate.execute("INSERT IGNORE INTO article_categories (id, name, icon, color, sort_order, status) VALUES " +
+                    "(1, '心理健康', '🧠', '#5B9BD5', 1, 1), " +
+                    "(2, '情绪管理', '💖', '#FF6B35', 2, 1), " +
+                    "(3, '亲子教育', '👨‍👩‍👧‍👦', '#4CAF50', 3, 1), " +
+                    "(4, '学习力', '📚', '#FFD700', 4, 1), " +
+                    "(5, '专注力', '🎯', '#8D6E63', 5, 1)");
+                log.info("文章分类种子数据已加载");
+            }
+        } catch (Exception e) {
+            log.warn("文章分类种子数据初始化失败: {}", e.getMessage());
+        }
+
+        // ==================== 分佣裂变系统 ====================
+        
+        // 迁移: users表添加推荐人字段
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'referrer_id'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE users ADD COLUMN referrer_id BIGINT DEFAULT NULL COMMENT '推荐人ID'");
+                jdbcTemplate.execute("ALTER TABLE users ADD COLUMN referral_code VARCHAR(32) DEFAULT NULL COMMENT '个人邀请码'");
+                jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_referral_code (referral_code)");
+                log.info("已添加分佣字段到users表");
+            } else {
+                log.info("分佣金字段已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加分佣金字段失败: {}", e.getMessage());
+        }
+
+        // 迁移: products表添加利润率字段
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'products' AND COLUMN_NAME = 'profit_rate'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE products ADD COLUMN profit_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '平台利润率(%)'");
+                log.info("已添加profit_rate列到products表");
+            } else {
+                log.info("profit_rate列已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加profit_rate列失败: {}", e.getMessage());
+        }
+
+        // 新表: commission_records (佣金记录)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS commission_records (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "order_id BIGINT NOT NULL COMMENT '订单ID', " +
+                "order_type VARCHAR(32) NOT NULL COMMENT '订单类型: membership/product/assessment/package', " +
+                "referrer_id BIGINT NOT NULL COMMENT '推荐人ID', " +
+                "buyer_id BIGINT NOT NULL COMMENT '购买人ID', " +
+                "commission_type VARCHAR(16) NOT NULL COMMENT '佣金类型: member/profit', " +
+                "order_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额', " +
+                "profit_rate DECIMAL(5,2) DEFAULT 0.00 COMMENT '利润率(%)', " +
+                "commission_amount DECIMAL(10,2) NOT NULL COMMENT '佣金金额', " +
+                "status VARCHAR(16) NOT NULL DEFAULT 'settled' COMMENT '状态: pending/settled', " +
+                "remark VARCHAR(255) DEFAULT NULL, " +
+                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_referrer (referrer_id), " +
+                "INDEX idx_buyer (buyer_id), " +
+                "INDEX idx_order (order_id, order_type)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+            log.info("已创建commission_records表");
+        } catch (Exception e) {
+            log.warn("创建commission_records表失败: {}", e.getMessage());
+        }
+
+        // 新表: withdrawal_requests (提现申请)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS withdrawal_requests (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                "amount DECIMAL(10,2) NOT NULL COMMENT '提现金额', " +
+                "account_info TEXT COMMENT '账户信息(JSON)', " +
+                "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/approved/rejected', " +
+                "audit_by BIGINT DEFAULT NULL COMMENT '审核人ID', " +
+                "audit_at DATETIME DEFAULT NULL COMMENT '审核时间', " +
+                "remark VARCHAR(255) DEFAULT NULL COMMENT '审核备注', " +
+                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_user (user_id), " +
+                "INDEX idx_status (status)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+            log.info("已创建withdrawal_requests表");
+        } catch (Exception e) {
+            log.warn("创建withdrawal_requests表失败: {}", e.getMessage());
+        }
+
+        // ==================== 文章内容系统 ====================
+
+        // 文章分类表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_categories (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "name VARCHAR(50) NOT NULL COMMENT '分类名', " +
+                "icon VARCHAR(20) DEFAULT '' COMMENT '图标emoji', " +
+                "color VARCHAR(20) DEFAULT '#5B9BD5' COMMENT '标识色', " +
+                "sort_order INT DEFAULT 0 COMMENT '排序', " +
+                "status TINYINT DEFAULT 1 COMMENT '1启用/0禁用'" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类'");
+            log.info("已创建article_categories表");
+        } catch (Exception e) {
+            log.warn("创建article_categories表失败: {}", e.getMessage());
+        }
+
+        // 文章表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS articles (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "category_id BIGINT DEFAULT 0 COMMENT '所属分类ID', " +
+                "title VARCHAR(200) NOT NULL COMMENT '标题', " +
+                "summary VARCHAR(500) DEFAULT '' COMMENT '摘要', " +
+                "cover_image VARCHAR(500) DEFAULT '' COMMENT '封面图URL', " +
+                "content LONGTEXT COMMENT '富文本内容', " +
+                "tags VARCHAR(200) DEFAULT '' COMMENT '标签JSON数组', " +
+                "author VARCHAR(100) DEFAULT '浠艾福' COMMENT '作者', " +
+                "read_time INT DEFAULT 0 COMMENT '预计阅读分钟数', " +
+                "related_dimensions VARCHAR(100) DEFAULT '' COMMENT '关联五维JSON数组', " +
+                "visibility VARCHAR(20) DEFAULT 'public' COMMENT '浏览权限:public/login/private', " +
+                "visible_to TEXT COMMENT '私密指定人群JSON', " +
+                "status VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/published/archived', " +
+                "is_featured TINYINT DEFAULT 0 COMMENT '1精选/0普通', " +
+                "published_at DATETIME DEFAULT NULL COMMENT '发布时间', " +
+                "view_count INT DEFAULT 0 COMMENT '浏览次数', " +
+                "created_by BIGINT DEFAULT 0 COMMENT '发布人adminID', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章'");
+            log.info("已创建articles表");
+        } catch (Exception e) {
+            log.warn("创建articles表失败: {}", e.getMessage());
+        }
+
+        // 迁移: articles表补充新字段(兼容旧表)
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'visibility'",
+                Integer.class);
+            if (colExists == null || colExists == 0) {
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN visibility VARCHAR(20) DEFAULT 'public' COMMENT '浏览权限:public/login/private' AFTER related_dimensions");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN visible_to TEXT COMMENT '私密指定人群JSON' AFTER visibility");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN tags VARCHAR(200) DEFAULT '' COMMENT '标签JSON数组' AFTER content");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN related_dimensions VARCHAR(100) DEFAULT '' COMMENT '关联五维JSON数组' AFTER read_time");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN is_featured TINYINT DEFAULT 0 COMMENT '1精选/0普通' AFTER status");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN published_at DATETIME DEFAULT NULL COMMENT '发布时间' AFTER is_featured");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN view_count INT DEFAULT 0 COMMENT '浏览次数' AFTER published_at");
+                jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN created_by BIGINT DEFAULT 0 COMMENT '发布人adminID' AFTER view_count");
+                log.info("已迁移articles表补充字段");
+            } else {
+                log.info("articles表字段已完整,跳过迁移");
+            }
+        } catch (Exception e) {
+            log.warn("articles表字段迁移失败: {}", e.getMessage());
+        }
+
+        // ==================== 五维能量系统(账本模式) ====================
+
+        // 维度定义表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS energy_dimension (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "code VARCHAR(16) NOT NULL UNIQUE COMMENT '维度代码: body/mind/wisdom/action/wealth', " +
+                "name VARCHAR(20) NOT NULL COMMENT '维度名称: 身/心/智/行/富', " +
+                "icon VARCHAR(16) COMMENT '图标emoji', " +
+                "element VARCHAR(10) COMMENT '五行元素: 土/火/金/木/水', " +
+                "sort_order INT DEFAULT 0 COMMENT '显示顺序', " +
+                "status TINYINT DEFAULT 1 COMMENT '1启用/0禁用', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量维度定义表'"
+            );
+            log.info("已创建energy_dimension表");
+        } catch (Exception e) {
+            log.warn("创建energy_dimension表失败: {}", e.getMessage());
+        }
+
+        // 服务-维度比例配置表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS energy_source_config (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "source_type VARCHAR(32) NOT NULL COMMENT '来源类型: task/activity/course/product/consultation', " +
+                "source_id BIGINT NOT NULL COMMENT '来源业务表ID', " +
+                "source_name VARCHAR(200) COMMENT '冗余名称', " +
+                "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
+                "ratio DECIMAL(5,4) NOT NULL COMMENT '占比如0.6000=60%', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_source (source_type, source_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表'"
+            );
+            log.info("已创建energy_source_config表");
+        } catch (Exception e) {
+            log.warn("创建energy_source_config表失败: {}", e.getMessage());
+        }
+
+        // 能量流水表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS energy_log (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+                "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
+                "amount INT NOT NULL COMMENT '变动量(正=获得,负=消耗)', " +
+                "balance_after INT NOT NULL COMMENT '变动后余额', " +
+                "source_type VARCHAR(32) COMMENT '来源类型: task/product/medical_report/daily_record', " +
+                "source_id BIGINT COMMENT '来源ID', " +
+                "ref_id BIGINT COMMENT '关联旧流水ID(覆盖时标记)', " +
+                "expires_at DATETIME COMMENT '过期时间(空=永不过期)', " +
+                "description VARCHAR(500) COMMENT '描述', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_child_dim (child_id, dimension_id), " +
+                "INDEX idx_created (created_at), " +
+                "INDEX idx_source (source_type, source_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量流水表'"
+            );
+            log.info("已创建energy_log表");
+        } catch (Exception e) {
+            log.warn("创建energy_log表失败: {}", e.getMessage());
+        }
+
+        // 维度余额表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS energy_balance (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+                "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
+                "balance INT DEFAULT 0 COMMENT '当前能量值(可为负)', " +
+                "total_earned INT DEFAULT 0 COMMENT '累计获得', " +
+                "total_spent INT DEFAULT 0 COMMENT '累计消耗', " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "UNIQUE KEY uk_child_dim (child_id, dimension_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度余额表'"
+            );
+            log.info("已创建energy_balance表");
+        } catch (Exception e) {
+            log.warn("创建energy_balance表失败: {}", e.getMessage());
+        }
+
+        // 维度种子数据(按相生链: 智→富→行→心→身)
+        try {
+            Integer dimCount = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM energy_dimension", Integer.class);
+            if (dimCount == null || dimCount == 0) {
+                jdbcTemplate.execute("INSERT INTO energy_dimension (code, name, icon, element, sort_order, status) VALUES " +
+                    "('mind', '心', '🔥', '火', 4, 1), " +
+                    "('body', '身', '🌏', '土', 5, 1), " +
+                    "('wisdom', '智', '⚔️', '金', 1, 1), " +
+                    "('action', '行', '🌿', '木', 3, 1), " +
+                    "('wealth', '富', '💧', '水', 2, 1)");
+                log.info("能量维度种子数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化能量维度种子数据失败: {}", e.getMessage());
+        }
+
+        // 为已有商品插入默认比例配置(与Product.domain一致)
+        try {
+            Integer productConfigCount = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM energy_source_config WHERE source_type = 'product'", Integer.class);
+            if (productConfigCount == null || productConfigCount == 0) {
+                String[][] domainMapping = {
+                    {"action", "action"}, {"mind", "mind"}, {"body", "body"},
+                    {"wisdom", "wisdom"}, {"wealth", "wealth"}
+                };
+                for (String[] mapping : domainMapping) {
+                    jdbcTemplate.execute(
+                        "INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio) " +
+                        "SELECT 'product', p.id, p.name, d.id, 1.0000 " +
+                        "FROM products p " +
+                        "JOIN energy_dimension d ON d.code = '" + mapping[1] + "' " +
+                        "WHERE p.domain = '" + mapping[0] + "' " +
+                        "AND NOT EXISTS (" +
+                        "  SELECT 1 FROM energy_source_config esc " +
+                        "  WHERE esc.source_type = 'product' AND esc.source_id = p.id" +
+                        ")"
+                    );
+                }
+                log.info("已有商品能量比例配置已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化商品能量比例配置失败: {}", e.getMessage());
+        }
+
         log.info("数据库迁移完成");
         log.info("数据库迁移完成");
     }
     }
 
 
@@ -1816,4 +2146,17 @@ try {
                 name, desc, price, type, domain, memberEligible);
                 name, desc, price, type, domain, memberEligible);
         }
         }
     }
     }
+
+    private void insertContentSectionSeed(String pageKey, String sectionKey, String title,
+                                        String allowedRoles, int sortOrder) {
+        Integer count = jdbcTemplate.queryForObject(
+            "SELECT COUNT(*) FROM content_sections WHERE page_key = ? AND section_key = ?",
+            Integer.class, pageKey, sectionKey);
+        if (count == null || count == 0) {
+            jdbcTemplate.update(
+                "INSERT INTO content_sections (page_key, section_key, title, allowed_roles, sort_order, status, created_at, updated_at) " +
+                "VALUES (?, ?, ?, ?, ?, 'active', NOW(), NOW())",
+                pageKey, sectionKey, title, allowedRoles, sortOrder);
+        }
+    }
 }
 }

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

@@ -17,6 +17,21 @@ public class JwtInterceptor implements HandlerInterceptor {
     @Resource
     @Resource
     private JwtConfig jwtConfig;
     private JwtConfig jwtConfig;
 
 
+    /** 无需登录的公开路径前缀(兜底,与 WebConfig.excludePathPatterns 保持同步) */
+    private static final String[] PUBLIC_PATHS = {
+        "/api/auth/",
+        "/api/product/list",
+        "/api/product/detail",
+        "/api/product/type-list",
+        "/api/mini-game/list",
+        "/api/admin-auth/",
+        "/api/articles/list",
+        "/api/articles/detail",
+        "/api/articles/categories",
+        "/api/articles/featured",
+        "/api/articles/record-read"
+    };
+
     @Override
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         // 放 OPTIONS 请求
         // 放 OPTIONS 请求
@@ -24,6 +39,13 @@ public class JwtInterceptor implements HandlerInterceptor {
             return true;
             return true;
         }
         }
 
 
+        String path = request.getRequestURI();
+        for (String publicPath : PUBLIC_PATHS) {
+            if (path.equals(publicPath) || path.startsWith(publicPath)) {
+                return true;
+            }
+        }
+
         // 获取token
         // 获取token
         String token = request.getHeader("Authorization");
         String token = request.getHeader("Authorization");
         
         

+ 29 - 14
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -4,6 +4,7 @@ import javax.annotation.Resource;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.CorsRegistry;
 import org.springframework.web.servlet.config.annotation.CorsRegistry;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 
 @Configuration
 @Configuration
@@ -30,20 +31,34 @@ public class WebConfig implements WebMvcConfigurer {
         registry.addInterceptor(jwtInterceptor)
         registry.addInterceptor(jwtInterceptor)
                 .addPathPatterns("/api/**")
                 .addPathPatterns("/api/**")
                 .excludePathPatterns(
                 .excludePathPatterns(
-                        "/api/auth/send-code",
-                        "/api/auth/phone-login",
-                        "/api/auth/wechat-login",
-                        "/api/auth/wechat-phone-login",
-                        "/api/auth/check-phone",
-                        "/api/auth/direct-register",
-                        "/api/auth/register-with-idcard",
-                        "/api/admin-auth/send-code",
-                        "/api/admin-auth/login",
-                        "/api/admin-auth/login-by-password"
-                );
-
-        // 操作日志拦截器(JWT之后)
+                "/api/auth/send-code",
+                "/api/auth/phone-login",
+                "/api/auth/wechat-login",
+                "/api/auth/wechat-phone-login",
+                "/api/auth/check-phone",
+                "/api/auth/direct-register",
+                "/api/auth/register-with-idcard",
+                "/api/admin-auth/send-code",
+                "/api/admin-auth/login",
+                "/api/admin-auth/login-by-password",
+                "/api/product/list",
+                "/api/product/detail",
+                "/api/product/type-list",
+                "/api/mini-game/list",
+                "/api/articles/list",
+                "/api/articles/detail",
+                "/api/articles/categories",
+                "/api/articles/featured",
+                "/api/articles/record-read"
+        );
+
         registry.addInterceptor(operationLogInterceptor)
         registry.addInterceptor(operationLogInterceptor)
                 .addPathPatterns("/api/**");
                 .addPathPatterns("/api/**");
     }
     }
-}
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler("/uploads/**")
+                .addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");
+    }
+}

+ 8 - 24
cfc-backend/src/main/java/com/etotem/cfc/controller/MiniGameController.java

@@ -2,16 +2,14 @@ package com.etotem.cfc.controller;
 
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.common.Result;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import com.etotem.cfc.dto.CompleteGameDTO;
 import com.etotem.cfc.entity.MiniGame;
 import com.etotem.cfc.entity.MiniGame;
-import com.etotem.cfc.entity.User;
-import com.etotem.cfc.entity.Child;
-import com.etotem.cfc.mapper.UserMapper;
-import com.etotem.cfc.mapper.ChildMapper;
 import com.etotem.cfc.service.MiniGameService;
 import com.etotem.cfc.service.MiniGameService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+
+import javax.validation.Valid;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 @Tag(name = "小游戏管理", description = "小游戏相关接口")
 @Tag(name = "小游戏管理", description = "小游戏相关接口")
@@ -20,8 +18,6 @@ import java.util.Map;
 public class MiniGameController {
 public class MiniGameController {
     @Resource
     @Resource
     private MiniGameService miniGameService;
     private MiniGameService miniGameService;
-    private UserMapper userMapper;
-    private ChildMapper childMapper;
     /**
     /**
      * 获取启用的小游戏列表
      * 获取启用的小游戏列表
      */
      */
@@ -56,22 +52,10 @@ public class MiniGameController {
     @PostMapping("/complete")
     @PostMapping("/complete")
     public Result<Map<String, Object>> completeGame(
     public Result<Map<String, Object>> completeGame(
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
-            @RequestBody Map<String, Object> body) {
-        Long childId = Long.valueOf(body.get("childId").toString());
-        String gameCode = (String) body.get("gameCode");
-        Integer completionTime = body.get("completionTime") != null ?
-                Integer.valueOf(body.get("completionTime").toString()) : null;
-        Integer score = body.get("score") != null ?
-                Integer.valueOf(body.get("score").toString()) : null;
-        // 权限校验:验证用户是否有权限操作该孩子
-        User user = userMapper.selectById(userId);
-        if (user != null) {
-            Child child = childMapper.selectById(childId);
-            if (child == null) {
-                return Result.error("孩子不存在");
-                // 验证孩子是否属于用户的家庭
-            }
-        }
-        return Result.error("处理不成功");
+            @RequestBody @Valid CompleteGameDTO dto) {
+        Map<String, Object> result = miniGameService.completeGame(
+                dto.getChildId(), dto.getGameCode(),
+                dto.getCompletionTime(), dto.getScore());
+        return Result.success(result);
     }
     }
 }
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java

@@ -35,6 +35,10 @@ public class Product implements Serializable {
     private String externalSource;
     private String externalSource;
     private String externalId;
     private String externalId;
     private String externalData;
     private String externalData;
+
+    // 分佣系统 - 平台利润率(%)
+    private BigDecimal profitRate;
+
     private Date createdAt;
     private Date createdAt;
     private Date updatedAt;
     private Date updatedAt;
 }
 }

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java

@@ -91,6 +91,12 @@ public class User implements Serializable {
     // 服务商资质信息 (JSON)
     // 服务商资质信息 (JSON)
     private String vendorInfo;
     private String vendorInfo;
 
 
+    // 分佣系统 - 推荐人ID
+    private Long referrerId;
+
+    // 分佣系统 - 个人邀请码(6位字母数字)
+    private String referralCode;
+
     private Date createdAt;
     private Date createdAt;
 
 
     private Date updatedAt;
     private Date updatedAt;

+ 10 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.etotem.cfc.entity.AssessmentOrder;
 import com.etotem.cfc.entity.AssessmentOrder;
 import com.etotem.cfc.mapper.AssessmentOrderMapper;
 import com.etotem.cfc.mapper.AssessmentOrderMapper;
+import com.etotem.cfc.service.CommissionService;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
@@ -20,6 +21,9 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
     @Resource
     @Resource
     private AssessmentOrderMapper assessmentOrderMapper;
     private AssessmentOrderMapper assessmentOrderMapper;
 
 
+    @Resource
+    private CommissionService commissionService;
+
     public AssessmentOrder createOrder(Long familyId, Long userId, Long childId,
     public AssessmentOrder createOrder(Long familyId, Long userId, Long childId,
                                        Long guideId, Long packageId,
                                        Long guideId, Long packageId,
                                        String guideName, String packageName,
                                        String guideName, String packageName,
@@ -57,7 +61,12 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
         order.setPayType(payType);
         order.setPayType(payType);
         order.setTransactionId(transactionId);
         order.setTransactionId(transactionId);
         order.setUpdatedAt(new Date());
         order.setUpdatedAt(new Date());
-        return this.updateById(order);
+        boolean updated = this.updateById(order);
+        if (updated) {
+            commissionService.settle(order.getId(), "assessment", order.getUserId(),
+                    new java.math.BigDecimal(order.getActualPrice()), null);
+        }
+        return updated;
     }
     }
 
 
     public boolean cancelOrder(String orderNo) {
     public boolean cancelOrder(String orderNo) {

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MiniGameService.java

@@ -32,6 +32,9 @@ public class MiniGameService implements MiniGameServiceInterface {
     @Resource
     @Resource
     private PointsLogMapper pointsLogMapper;
     private PointsLogMapper pointsLogMapper;
 
 
+    @Resource
+    private GameRecordService gameRecordService;
+
     /**
     /**
      * 获取所有启用的小游戏
      * 获取所有启用的小游戏
      */
      */
@@ -112,6 +115,13 @@ public class MiniGameService implements MiniGameServiceInterface {
         log.setCreatedAt(new Date());
         log.setCreatedAt(new Date());
         pointsLogMapper.insert(log);
         pointsLogMapper.insert(log);
 
 
+        // 持久化游戏记录
+        try {
+            gameRecordService.saveRecord(childId, gameCode, completionTime, score, "medium", earnedPoints);
+        } catch (Exception e) {
+            MiniGameService.log.warn("保存游戏记录失败: {}", e.getMessage());
+        }
+
         // 返回结果
         // 返回结果
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
         result.put("pointsEarned", earnedPoints);
         result.put("pointsEarned", earnedPoints);

+ 8 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java

@@ -8,6 +8,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.entity.TaskTemplatePackage;
 import com.etotem.cfc.entity.TaskTemplatePackage;
 import com.etotem.cfc.mapper.PackageOrderMapper;
 import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.service.CommissionService;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.annotation.Value;
@@ -31,6 +32,9 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
     @Resource
     @Resource
     private TaskTemplatePackageService packageService;
     private TaskTemplatePackageService packageService;
 
 
+    @Resource
+    private CommissionService commissionService;
+
     @Value("${wechat.appid}")
     @Value("${wechat.appid}")
     private String appid;
     private String appid;
 
 
@@ -186,7 +190,10 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
                 order.setNotifyData(xmlData);
                 order.setNotifyData(xmlData);
                 order.setUpdatedAt(new Date());
                 order.setUpdatedAt(new Date());
                 orderMapper.updateById(order);
                 orderMapper.updateById(order);
-                
+
+                commissionService.settle(order.getId(), "package", order.getUserId(),
+                        order.getPrice(), null);
+
                 log.info("订单支付成功: {}", orderNo);
                 log.info("订单支付成功: {}", orderNo);
                 return true;
                 return true;
             }
             }

+ 11 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java

@@ -6,6 +6,7 @@ import javax.annotation.Resource;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.mapper.PackageOrderMapper;
 import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.service.CommissionService;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.util.Date;
 import java.util.Date;
@@ -20,6 +21,9 @@ public class PaymentService {
     @Resource
     @Resource
     private PackageOrderMapper packageOrderMapper;
     private PackageOrderMapper packageOrderMapper;
 
 
+    @Resource
+    private CommissionService commissionService;
+
     /**
     /**
      * 创建微信支付订单
      * 创建微信支付订单
      */
      */
@@ -71,8 +75,13 @@ public class PaymentService {
         order.setTransactionId(transactionId);
         order.setTransactionId(transactionId);
         order.setPaidAt(new Date());
         order.setPaidAt(new Date());
         order.setUpdatedAt(new Date());
         order.setUpdatedAt(new Date());
-        
-        return packageOrderMapper.updateById(order) > 0;
+
+        boolean updated = packageOrderMapper.updateById(order) > 0;
+        if (updated) {
+            commissionService.settle(order.getId(), "package", order.getUserId(),
+                    order.getPrice(), null);
+        }
+        return updated;
     }
     }
 
 
     /**
     /**

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -10,6 +10,7 @@ import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.CommissionService;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
@@ -33,6 +34,9 @@ public class ProductOrderService {
     @Resource
     @Resource
     private ProductService productService;
     private ProductService productService;
 
 
+    @Resource
+    private CommissionService commissionService;
+
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
         if (buyerId == null) {
         if (buyerId == null) {
             return Result.error("请先登录");
             return Result.error("请先登录");
@@ -88,6 +92,8 @@ public class ProductOrderService {
         order.setPaidAt(new Date());
         order.setPaidAt(new Date());
         order.setUpdatedAt(new Date());
         order.setUpdatedAt(new Date());
         orderMapper.updateById(order);
         orderMapper.updateById(order);
+        commissionService.settle(order.getId(), "product", order.getBuyerId(),
+                order.getTotalAmount(), order.getProductId());
         return Result.success(ProductOrderDTO.from(order));
         return Result.success(ProductOrderDTO.from(order));
     }
     }
 
 

+ 18 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -42,6 +42,9 @@ public class TaskService implements TaskServiceInterface {
     @Resource
     @Resource
     private MiniGameMapper miniGameMapper;
     private MiniGameMapper miniGameMapper;
 
 
+    @Resource
+    private EnergyService energyService;
+
     private static final int EARLY_BONUS = 1; // 提前完成奖励
     private static final int EARLY_BONUS = 1; // 提前完成奖励
     private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
     private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
     private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
     private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
@@ -364,10 +367,22 @@ expireCal.add(Calendar.DAY_OF_MONTH, task.getPointsExpireDays());
 pointsLog.setExpireAt(expireCal.getTime());
 pointsLog.setExpireAt(expireCal.getTime());
 }
 }
 
 
-pointsLogMapper.insert(pointsLog);
+        pointsLogMapper.insert(pointsLog);
+
+        // 五维能量发放(与积分并行,独立系统,失败不影响积分)
+        try {
+            int energyAmount = Math.abs(pointsEarned);
+            if (energyAmount > 0) {
+                energyService.awardEnergy(childId, "task", taskId, energyAmount,
+                        "完成任务: " + task.getTitle(), null);
+            }
+        } catch (Exception e) {
+            log.warn("能量发放失败(不影响积分): childId={}, taskId={}, error={}",
+                    childId, taskId, e.getMessage());
+        }
 
 
-Map<String, Object> result = new HashMap<>();
-result.put("pointsEarned", pointsEarned);
+        Map<String, Object> result = new HashMap<>();
+        result.put("pointsEarned", pointsEarned);
 result.put("newBalance", child.getTotalPoints());
 result.put("newBalance", child.getTotalPoints());
 result.put("needReview", task.getNeedReview() == 1);
 result.put("needReview", task.getNeedReview() == 1);
 
 

+ 1 - 1
cfc-backend/src/main/resources/application.yml

@@ -1,5 +1,5 @@
 server:
 server:
-  port: 8080
+  port: 9082
   tomcat:
   tomcat:
     headers:
     headers:
       X-Content-Type-Options: nosniff
       X-Content-Type-Options: nosniff

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

@@ -630,3 +630,59 @@ CREATE TABLE IF NOT EXISTS teacher_messages (
     INDEX idx_parent_id (parent_id),
     INDEX idx_parent_id (parent_id),
     INDEX idx_status (status)
     INDEX idx_status (status)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 游戏记录表(专注训练模块)
+CREATE TABLE IF NOT EXISTS game_records (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    child_id BIGINT NOT NULL COMMENT '孩子ID',
+    game_code VARCHAR(20) NOT NULL COMMENT '游戏代码',
+    score INT DEFAULT 0 COMMENT '得分(0-100)',
+    completion_time INT COMMENT '完成用时(秒)',
+    difficulty VARCHAR(20) DEFAULT 'medium' COMMENT '难度(easy/medium/hard)',
+    points_earned INT DEFAULT 0 COMMENT '获得积分',
+    played_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '游玩时间',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_child_id (child_id),
+    INDEX idx_game_code (game_code),
+    INDEX idx_played_at (played_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 勋章定义表
+CREATE TABLE IF NOT EXISTS badge (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    badge_id VARCHAR(50) UNIQUE NOT NULL COMMENT '勋章唯一标识',
+    name VARCHAR(100) NOT NULL COMMENT '勋章名称',
+    description TEXT COMMENT '勋章描述',
+    icon VARCHAR(255) COMMENT '勋章图标',
+    category VARCHAR(20) COMMENT '分类(学习/运动/家务/阅读/打卡/综合)',
+    level VARCHAR(20) COMMENT '等级(铜牌/银牌/金牌/钻石)',
+    rarity VARCHAR(20) COMMENT '稀有度(普通/稀有/史诗/传说)',
+    trigger_type VARCHAR(20) COMMENT '触发类型(task_count/streak_days/points_total/reward_count/custom)',
+    threshold INT DEFAULT 0 COMMENT '阈值',
+    need_approval TINYINT DEFAULT 0 COMMENT '是否需要审批',
+    expire_days INT DEFAULT -1 COMMENT '有效期天数(-1永久)',
+    sort_order INT DEFAULT 0 COMMENT '排序',
+    is_active TINYINT DEFAULT 1 COMMENT '是否启用',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_category (category),
+    INDEX idx_is_active (is_active)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 儿童勋章关联表
+CREATE TABLE IF NOT EXISTS child_badge (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    child_id BIGINT NOT NULL COMMENT '孩子ID',
+    badge_id BIGINT NOT NULL COMMENT '勋章ID',
+    earned_at DATETIME COMMENT '获得时间',
+    expire_at DATETIME COMMENT '过期时间',
+    status TINYINT DEFAULT 1 COMMENT '状态:1有效 0过期 -1撤销',
+    is_favorite TINYINT DEFAULT 0 COMMENT '是否收藏',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_child_badge (child_id, badge_id),
+    INDEX idx_child_id (child_id),
+    INDEX idx_badge_id (badge_id),
+    INDEX idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;