Bläddra i källkod

fix(db): 迁移修正 - finance_checkins.child_id允许NULL+补齐type列(251c) + 维度权重迁移表名修正去重(233) + 移除已稳定的profile/report_template/social_circle迁移

Xiaogang Liao 4 veckor sedan
förälder
incheckning
45b4dd37fb

+ 65 - 158
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2993,17 +2993,18 @@ log.info("已添加template_id列到tasks表");
 		} catch (Exception ex) {
 		} catch (Exception ex) {
 			log.warn("health_plans.updated_at 添加失败(可能已存在): " + ex.getMessage());
 			log.warn("health_plans.updated_at 添加失败(可能已存在): " + ex.getMessage());
 		}
 		}
-		// 迁移253: foods表添加diet_type列(清真/素食标识
+		// 迁移251c: finance_checkins.child_id 允许NULL(家长本人打卡不选孩子,Service已支持childId为空;原NOT NULL无默认值导致INSERT报错
 		try {
 		try {
-			ensureColumn("foods", "diet_type", "VARCHAR(20) DEFAULT NULL COMMENT '饮食类型: none/清真/halal/素食/vegan'");
-		} catch (Exception ex) {
-			log.warn("foods.diet_type 添加失败(可能已存在): " + ex.getMessage());
+			jdbcTemplate.execute("ALTER TABLE finance_checkins MODIFY COLUMN child_id BIGINT NULL COMMENT '孩子ID,家长本人打卡时为NULL'");
+			log.info("已修改finance_checkins.child_id字段允许NULL");
+		} catch (Exception e) {
+			log.warn("finance_checkins.child_id 修改失败: " + e.getMessage());
 		}
 		}
-		// 迁移254: diet_preferences表添加must_eat列(必须要吃的食物
+		// 迁移251c: finance_checkins 补齐 type 列(initGuideModuleTables建表版本缺失,实体FinanceCheckin含type字段且收支统计依赖
 		try {
 		try {
-			ensureColumn("diet_preferences", "must_eat", "JSON DEFAULT NULL COMMENT '必须要吃的食物列表'");
-		} catch (Exception ex) {
-			log.warn("diet_preferences.must_eat 添加失败(可能已存在): " + ex.getMessage());
+			ensureColumn("finance_checkins", "type", "VARCHAR(10) DEFAULT 'expense' COMMENT '类型: income(收入)/expense(支出)'");
+		} catch (Exception e) {
+			log.warn("finance_checkins.type 添加失败(可能已存在): " + e.getMessage());
 		}
 		}
         ensureColumn("file_record", "file_type", "VARCHAR(50) DEFAULT NULL COMMENT '文件类型: image/pdf/video/audio/other'");
         ensureColumn("file_record", "file_type", "VARCHAR(50) DEFAULT NULL COMMENT '文件类型: image/pdf/video/audio/other'");
         ensureColumn("file_record", "description", "TEXT COMMENT '文件描述'");
         ensureColumn("file_record", "description", "TEXT COMMENT '文件描述'");
@@ -3012,7 +3013,7 @@ log.info("已添加template_id列到tasks表");
 
 
         // 迁移252: 体素/心理/认知指标定义种子数据
         // 迁移252: 体素/心理/认知指标定义种子数据
         try {
         try {
-            String[][] rows = {
+            Object[][] rows = {
                 {"body","nutrition","body_fat","体脂率(%)","18-28","1"},
                 {"body","nutrition","body_fat","体脂率(%)","18-28","1"},
                 {"body","nutrition","bmi","BMI指数","18.5-24","2"},
                 {"body","nutrition","bmi","BMI指数","18.5-24","2"},
                 {"body","nutrition","protein","蛋白质摄入(g/日)","50-80","3"},
                 {"body","nutrition","protein","蛋白质摄入(g/日)","50-80","3"},
@@ -3041,10 +3042,10 @@ log.info("已添加template_id列到tasks表");
                 {"wisdom","cognitive","processing_speed","加工速度","0-100","6"},
                 {"wisdom","cognitive","processing_speed","加工速度","0-100","6"},
                 {"wisdom","cognitive","growth_mindset","成长型思维","0-100","7"}
                 {"wisdom","cognitive","growth_mindset","成长型思维","0-100","7"}
             };
             };
-            for (String[] r : rows) {
+            for (Object[] r : rows) {
                 jdbcTemplate.update(
                 jdbcTemplate.update(
                     "INSERT IGNORE INTO indicator_definitions (domain, category, code, name, data_type, ref_range, sort_order, status) VALUES (?,?,?,?,?,?,?,1)",
                     "INSERT IGNORE INTO indicator_definitions (domain, category, code, name, data_type, ref_range, sort_order, status) VALUES (?,?,?,?,?,?,?,1)",
-                    r[0], r[1], r[2], r[3], "number", r[4], String.valueOf(String.valueOf(Integer.parseInt(r[5])))
+                    r[0], r[1], r[2], r[3], "number", r[4], Integer.parseInt((String) r[5])
                 );
                 );
             }
             }
             log.info("迁移252: indicator_definitions 种子数据已加载 ({}条)", rows.length);
             log.info("迁移252: indicator_definitions 种子数据已加载 ({}条)", rows.length);
@@ -3937,8 +3938,9 @@ try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS finance_checkins (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS finance_checkins (" +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                 "user_id BIGINT NOT NULL COMMENT '用户ID', " +
                 "user_id BIGINT NOT NULL COMMENT '用户ID', " +
-                "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+                "child_id BIGINT NULL COMMENT '孩子ID,家长本人打卡时为NULL', " +
                 "checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review', " +
                 "checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review', " +
+                "type VARCHAR(10) DEFAULT 'expense' COMMENT '类型: income(收入)/expense(支出)', " +
                 "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
                 "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
                 "amount INT COMMENT '金额(分,收支记录)', " +
                 "amount INT COMMENT '金额(分,收支记录)', " +
                 "category VARCHAR(50) COMMENT '收支分类', " +
                 "category VARCHAR(50) COMMENT '收支分类', " +
@@ -8707,37 +8709,59 @@ private void runMigration100() {
 		log.info("已添加recommendation_logs.recommend_type列");
 		log.info("已添加recommendation_logs.recommend_type列");
 
 
 		// 迁移233: 将 Article/Activity 现有 dimension_weights JSON 同步到 dimension_weights 表
 		// 迁移233: 将 Article/Activity 现有 dimension_weights JSON 同步到 dimension_weights 表
+		// (修正:源表名 article→articles、activity→activities;activity与article同为五维JSON对象按维度键展开;
+		//   dimension_weights表无唯一键,INSERT IGNORE不去重,用sys_config一次性标记+自连接去重兜底)
 		try {
 		try {
-			jdbcTemplate.execute(
-				"INSERT IGNORE INTO dimension_weights (target_type, target_id, dimension, weight, enabled) " +
-				"SELECT 'article', id, 'body', COALESCE(CAST(JSON_EXTRACT(dimension_weights, '$.body') AS SIGNED), 0), 1 " +
-				"FROM article WHERE dimension_weights IS NOT NULL AND dimension_weights != '' AND JSON_EXTRACT(dimension_weights, '$.body') IS NOT NULL " +
-				"UNION ALL " +
-				"SELECT 'article', id, 'mind', COALESCE(CAST(JSON_EXTRACT(dimension_weights, '$.mind') AS SIGNED), 0), 1 " +
-				"FROM article WHERE dimension_weights IS NOT NULL AND dimension_weights != '' AND JSON_EXTRACT(dimension_weights, '$.mind') IS NOT NULL " +
-				"UNION ALL " +
-				"SELECT 'article', id, 'wisdom', COALESCE(CAST(JSON_EXTRACT(dimension_weights, '$.wisdom') AS SIGNED), 0), 1 " +
-				"FROM article WHERE dimension_weights IS NOT NULL AND dimension_weights != '' AND JSON_EXTRACT(dimension_weights, '$.wisdom') IS NOT NULL " +
-				"UNION ALL " +
-				"SELECT 'article', id, 'action', COALESCE(CAST(JSON_EXTRACT(dimension_weights, '$.action') AS SIGNED), 0), 1 " +
-				"FROM article WHERE dimension_weights IS NOT NULL AND dimension_weights != '' AND JSON_EXTRACT(dimension_weights, '$.action') IS NOT NULL " +
-				"UNION ALL " +
-				"SELECT 'article', id, 'wealth', COALESCE(CAST(JSON_EXTRACT(dimension_weights, '$.wealth') AS SIGNED), 0), 1 " +
-				"FROM article WHERE dimension_weights IS NOT NULL AND dimension_weights != '' AND JSON_EXTRACT(dimension_weights, '$.wealth') IS NOT NULL"
-			);
-			log.info("已完成article维度权重到dimension_weights表的一次性迁移");
-		} catch (Exception ex) {
-			log.warn("article维度迁移失败: {}", ex.getMessage());
-		}
-		try {
-			jdbcTemplate.execute(
-				"INSERT IGNORE INTO dimension_weights (target_type, target_id, dimension, weight, enabled) " +
-				"SELECT 'activity', id, dimension_code, COALESCE(CAST(dimension_weights AS SIGNED), 50), 1 " +
-				"FROM activity WHERE dimension_code IS NOT NULL AND dimension_code != '' AND dimension_weights IS NOT NULL"
-			);
-			log.info("已完成activity维度权重到dimension_weights表的一次性迁移");
+			Integer synced = jdbcTemplate.queryForObject(
+					"SELECT COUNT(*) FROM sys_config WHERE config_key = 'dimension_weights_legacy_sync_done'",
+					Integer.class);
+			if (synced == null || synced == 0) {
+				jdbcTemplate.execute(
+					"INSERT IGNORE INTO dimension_weights (target_type, target_id, dimension, weight, enabled) " +
+					"SELECT 'article', a.id, 'body', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.body') AS SIGNED), 0), 1 " +
+					"FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.body') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'article', a.id, 'mind', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.mind') AS SIGNED), 0), 1 " +
+					"FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.mind') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'article', a.id, 'wisdom', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.wisdom') AS SIGNED), 0), 1 " +
+					"FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.wisdom') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'article', a.id, 'action', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.action') AS SIGNED), 0), 1 " +
+					"FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.action') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'article', a.id, 'wealth', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.wealth') AS SIGNED), 0), 1 " +
+					"FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.wealth') IS NOT NULL"
+				);
+				jdbcTemplate.execute(
+					"INSERT IGNORE INTO dimension_weights (target_type, target_id, dimension, weight, enabled) " +
+					"SELECT 'activity', act.id, 'body', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.body') AS SIGNED), 0), 1 " +
+					"FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.body') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'activity', act.id, 'mind', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.mind') AS SIGNED), 0), 1 " +
+					"FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.mind') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'activity', act.id, 'wisdom', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.wisdom') AS SIGNED), 0), 1 " +
+					"FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.wisdom') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'activity', act.id, 'action', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.action') AS SIGNED), 0), 1 " +
+					"FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.action') IS NOT NULL " +
+					"UNION ALL " +
+					"SELECT 'activity', act.id, 'wealth', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.wealth') AS SIGNED), 0), 1 " +
+					"FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.wealth') IS NOT NULL"
+				);
+				jdbcTemplate.execute(
+					"DELETE dw1 FROM dimension_weights dw1 INNER JOIN dimension_weights dw2 ON " +
+					"dw1.target_type = dw2.target_type AND dw1.target_id = dw2.target_id AND dw1.dimension = dw2.dimension " +
+					"AND dw1.id > dw2.id"
+				);
+				jdbcTemplate.update("INSERT INTO sys_config (config_key, config_value, description) VALUES ('dimension_weights_legacy_sync_done', '1', 'Article/Activity五维权重已同步到dimension_weights表')");
+				log.info("已完成Article/Activity维度权重到dimension_weights表的一次性迁移");
+			} else {
+				log.info("迁移233: 维度权重已同步过,跳过");
+			}
 		} catch (Exception ex) {
 		} catch (Exception ex) {
-			log.warn("activity维度迁移失败: {}", ex.getMessage());
+			log.warn("Article/Activity维度权重同步失败: {}", ex.getMessage());
 		}
 		}
 
 
 		// 迁移234: 创建 problem_domains 表(问题域定义,改版v1:需求驱动·逐步解锁)
 		// 迁移234: 创建 problem_domains 表(问题域定义,改版v1:需求驱动·逐步解锁)
@@ -9021,123 +9045,6 @@ private void runMigration100() {
 		// 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(《角色权限与菜单归类整合方案》第二部分,幂等)
 		// 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(《角色权限与菜单归类整合方案》第二部分,幂等)
 		migrateHealthMenuMount();
 		migrateHealthMenuMount();
 
 
-		// 迁移252: 创建用户画像表
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS profile_snapshot (" +
-					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-					"member_id BIGINT NOT NULL UNIQUE, " +
-					"dimension_scores JSON DEFAULT NULL, " +
-					"body_metrics JSON DEFAULT NULL, " +
-					"mind_metrics JSON DEFAULT NULL, " +
-					"wisdom_metrics JSON DEFAULT NULL, " +
-					"action_metrics JSON DEFAULT NULL, " +
-					"wealth_metrics JSON DEFAULT NULL, " +
-					"problem_domains VARCHAR(500) DEFAULT NULL, " +
-					"computed_at DATETIME DEFAULT NULL, " +
-					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-					"INDEX idx_member_id (member_id)" +
-					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
-			log.info("已创建profile_snapshot表");
-		} catch (Exception e) {
-			log.warn("创建profile_snapshot表可能已存在: {}", e.getMessage());
-		}
-
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS profile_history (" +
-					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-					"member_id BIGINT NOT NULL, " +
-					"snapshot_date DATE NOT NULL, " +
-					"all_metrics JSON NOT NULL, " +
-					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-					"INDEX idx_member_date (member_id, snapshot_date)" +
-					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
-			log.info("已创建profile_history表");
-		} catch (Exception e) {
-			log.warn("创建profile_history表可能已存在: {}", e.getMessage());
-		}
-
-		// 迁移254: 创建 report_template 表(报告展示模板配置)
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_template (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"name VARCHAR(100) NOT NULL COMMENT '模板名称', " +
-				"report_type VARCHAR(50) NOT NULL COMMENT '报告类型: gut_flora/dan/physical_exam/tongue', " +
-				"config JSON NOT NULL COMMENT '模板配置JSON: fixedBlocks+freeDisplay', " +
-				"is_active TINYINT DEFAULT 1 COMMENT '0=停用 1=启用', " +
-				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-				"UNIQUE KEY uk_report_type (report_type)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告展示模板'");
-			log.info("已创建report_template表");
-		} catch (Exception e) {
-			log.warn("report_template表可能已存在: {}", e.getMessage());
-		}
-
-		// 迁移255: report_unknown_upload 添加 annotation 列(管理员标注训练数据)
-		ensureColumn("report_unknown_upload", "annotation", "TEXT COMMENT '管理员标注: 手动指定类型+确认提取结果JSON'");
-
-		// 迁移256: social_circle 添加圈子管理字段
-		ensureColumn("social_circle", "description", "VARCHAR(500) NULL COMMENT '圈子描述'");
-		ensureColumn("social_circle", "cover_image", "VARCHAR(500) NULL COMMENT '封面图URL'");
-		ensureColumn("social_circle", "join_condition", "TINYINT(1) DEFAULT 0 COMMENT '加入条件: 0=公开/1=审核/2=邀请'");
-		ensureColumn("social_circle", "review_required", "TINYINT(1) DEFAULT 0 COMMENT '是否需要审核: 0=否/1=是'");
-		ensureColumn("social_circle", "is_public", "TINYINT(1) DEFAULT 1 COMMENT '是否公开可见: 0=否/1=是'");
-
-		// 迁移257: social_circle_member 添加审核状态和角色字段
-		ensureColumn("social_circle_member", "status", "VARCHAR(20) DEFAULT 'approved' COMMENT '申请状态: pending/approved/rejected'");
-		ensureColumn("social_circle_member", "role", "VARCHAR(20) DEFAULT 'member' COMMENT '角色: member/admin'");
-
-		// 迁移258: 创建圈子动态帖子表
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_post (" +
-					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-					"circle_id BIGINT NOT NULL COMMENT '圈子ID', " +
-					"member_id BIGINT NOT NULL COMMENT '发布者成员ID', " +
-					"member_type VARCHAR(20) NOT NULL COMMENT 'parent/child', " +
-					"type VARCHAR(50) NOT NULL COMMENT '帖子类型: checkin/diet/exercise/sleep/water/mood/text', " +
-					"content TEXT COMMENT '文字内容', " +
-					"checkin_id BIGINT COMMENT '关联打卡记录ID', " +
-					"image_urls TEXT COMMENT '图片URL列表JSON', " +
-					"like_count INT DEFAULT 0 COMMENT '点赞数', " +
-					"comment_count INT DEFAULT 0 COMMENT '评论数', " +
-					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-					"deleted TINYINT(1) DEFAULT 0, " +
-					"INDEX idx_circle_time (circle_id, created_at), " +
-					"INDEX idx_member_time (member_id, created_at)" +
-					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子动态帖子'");
-			log.info("已创建social_circle_post表");
-		} catch (Exception e) { log.warn("social_circle_post表可能已存在"); }
-
-		// 迁移259: 创建圈子点赞表
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_like (" +
-					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-					"circle_id BIGINT NOT NULL, " +
-					"post_id BIGINT NOT NULL COMMENT '被点赞的帖子ID', " +
-					"member_id BIGINT NOT NULL COMMENT '点赞成员ID', " +
-					"member_type VARCHAR(20) NOT NULL, " +
-					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-					"UNIQUE KEY uk_like (circle_id, post_id, member_id)" +
-					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子点赞记录'");
-			log.info("已创建social_circle_like表");
-		} catch (Exception e) { log.warn("social_circle_like表可能已存在"); }
-
-		// 迁移260: 创建圈子评论表
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_comment (" +
-					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-					"circle_id BIGINT NOT NULL, " +
-					"post_id BIGINT NOT NULL COMMENT '所属帖子ID', " +
-					"parent_id BIGINT DEFAULT NULL COMMENT '回复的评论ID(NULL=一级评论)', " +
-					"member_id BIGINT NOT NULL COMMENT '评论者成员ID', " +
-					"member_type VARCHAR(20) NOT NULL, " +
-					"content TEXT NOT NULL COMMENT '评论内容', " +
-					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-					"INDEX idx_post_time (post_id, created_at)" +
-					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子评论记录'");
-			log.info("已创建social_circle_comment表");
-		} catch (Exception e) { log.warn("social_circle_comment表可能已存在"); }
-
 	}
 	}
 
 
 	/**
 	/**

+ 1 - 1
cfc-backend/src/main/resources/schema.sql

@@ -1686,7 +1686,7 @@ CREATE TABLE IF NOT EXISTS guide_invite_codes (
 CREATE TABLE IF NOT EXISTS finance_checkins (
 CREATE TABLE IF NOT EXISTS finance_checkins (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     user_id BIGINT NOT NULL COMMENT '用户ID',
     user_id BIGINT NOT NULL COMMENT '用户ID',
-    child_id BIGINT NOT NULL COMMENT '孩子ID',
+    child_id BIGINT NULL COMMENT '孩子ID,家长本人打卡时为NULL',
 checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review',
 checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review',
     checkin_date DATE NOT NULL COMMENT '记账日期',
     checkin_date DATE NOT NULL COMMENT '记账日期',
     type VARCHAR(10) DEFAULT 'expense' COMMENT '类型: income(收入)/expense(支出)',
     type VARCHAR(10) DEFAULT 'expense' COMMENT '类型: income(收入)/expense(支出)',