|
|
@@ -2983,6 +2983,21 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
|
|
|
|
|
|
runSystemMigrations();
|
|
|
registerCollectExpectedIndicators();
|
|
|
+
|
|
|
+ // 减脂专题表创建(远程 fatloss 功能)
|
|
|
+ createFatWeightRecordTable();
|
|
|
+ createFatDietRecordTable();
|
|
|
+ createFatExerciseRecordTable();
|
|
|
+ createFatGoalTable();
|
|
|
+
|
|
|
+ // 指标统一层迁移(356~362)
|
|
|
+ migrateIndicatorDefinitionsColumns();
|
|
|
+ migrateIndicatorValuesColumns();
|
|
|
+ addIndicatorValuesKeys();
|
|
|
+ createIndicatorMappingRulesTable();
|
|
|
+ createIndicatorCorrelationsTable();
|
|
|
+ ensureIndicatorCollectionDate();
|
|
|
+ seedDimensionDefinitions();
|
|
|
}
|
|
|
|
|
|
private void migrateHealthDimensionsTable() {
|
|
|
@@ -6038,7 +6053,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
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_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/PROFESSIONAL/ENTERPRISE', " +
|
|
|
"level_name VARCHAR(100) NOT NULL COMMENT '等级名称', " +
|
|
|
"level_desc TEXT COMMENT '等级描述', " +
|
|
|
"price_monthly INT COMMENT '月费(分)', " +
|
|
|
@@ -8120,7 +8135,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
try {
|
|
|
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS membership_benefits (" +
|
|
|
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
- "level_code VARCHAR(50) NOT NULL COMMENT '会员等级: FREE/BASIC/PROFESSIONAL/ENTERPRISE', " +
|
|
|
+ "level_code VARCHAR(50) NOT NULL COMMENT '会员等级: FREE/PROFESSIONAL/ENTERPRISE', " +
|
|
|
"benefit_code VARCHAR(50) NOT NULL COMMENT '权益编码: AI_CHAT,FAMILY_MEMBERS等', " +
|
|
|
"benefit_name VARCHAR(100) NOT NULL COMMENT '权益名称', " +
|
|
|
"benefit_desc VARCHAR(500) COMMENT '权益描述', " +
|
|
|
@@ -10935,6 +10950,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
backfillTemplateInstanceFamilyMemberId();
|
|
|
createReportTransferRequestsTable();
|
|
|
addProductMinDiscountLevel();
|
|
|
+ restoreBasicMembershipLevel();
|
|
|
backfillMembershipLevelOrder();
|
|
|
removeReportUploadQuota();
|
|
|
|
|
|
@@ -10945,15 +10961,8 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
addAbTestColumnsToRecommendationLog();
|
|
|
createTaskReminderConfigTable();
|
|
|
addMissingColumnsToRecommendationLog();
|
|
|
-
|
|
|
- // 迁移341: BASIC 体验会员 features JSON 补 referral_commission(迁移315 未包含,前端对比表已显示可参与)
|
|
|
- try {
|
|
|
- jdbcTemplate.update("UPDATE membership_levels SET features = ? WHERE level_code = 'BASIC' AND features NOT LIKE '%referral_commission%'",
|
|
|
- "[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\",\"referral_commission\"]");
|
|
|
- log.info("迁移341: 已为 BASIC 体验会员补充 referral_commission 功能");
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("迁移341: 更新 BASIC features 失败: {}", e.getMessage());
|
|
|
- }
|
|
|
+ createDietFamilySelectedIngredientsTable();
|
|
|
+ createSsoTicketTable();
|
|
|
|
|
|
// 迁移327: 创建 report_feedback 表(报告反馈+AI解答)
|
|
|
try {
|
|
|
@@ -11020,55 +11029,6 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
log.warn("迁移314: meal_configs 添加索引 idx_config_date 失败(可能已存在): {}", e.getMessage());
|
|
|
}
|
|
|
|
|
|
-// 迁移315: membership_levels 新增 BASIC 体验会员等级(9.9元/月)
|
|
|
- try {
|
|
|
- jdbcTemplate.execute(
|
|
|
- "INSERT IGNORE INTO membership_levels " +
|
|
|
- "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
|
|
|
- "VALUES ('BASIC', '体验会员', " +
|
|
|
- "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
|
|
|
- "990, " +
|
|
|
- "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\"]', " +
|
|
|
- "3, 3)");
|
|
|
- log.info("迁移315: 已初始化BASIC体验会员等级");
|
|
|
- } catch (Exception e) {
|
|
|
- // 等级已存在,忽略
|
|
|
- }
|
|
|
-
|
|
|
- // 迁移316: membership_benefits 新增 BASIC 会员权益
|
|
|
- try {
|
|
|
- jdbcTemplate.execute(
|
|
|
- "INSERT IGNORE INTO membership_benefits " +
|
|
|
- "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
|
|
|
- "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
|
|
|
- "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
|
|
|
- "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
|
|
|
- "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8)");
|
|
|
- log.info("迁移316: 已初始化BASIC会员权益列表");
|
|
|
- } catch (Exception e) {
|
|
|
- // 已存在,忽略
|
|
|
- }
|
|
|
-
|
|
|
- // 迁移317: 创建 basic_quota_usage 表(BASIC会员月度配额使用记录)
|
|
|
- try {
|
|
|
- jdbcTemplate.execute(
|
|
|
- "CREATE TABLE IF NOT EXISTS basic_quota_usage (" +
|
|
|
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
- "user_id BIGINT NOT NULL COMMENT '家长用户ID', " +
|
|
|
- "quota_type VARCHAR(30) NOT NULL COMMENT '配额类型', " +
|
|
|
- "used_count INT DEFAULT 0 COMMENT '已用次数', " +
|
|
|
- "total_count INT NOT NULL COMMENT '月度总配额', " +
|
|
|
- "period_start DATE NOT NULL COMMENT '周期开始日期', " +
|
|
|
- "period_end DATE NOT NULL COMMENT '周期结束日期', " +
|
|
|
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
|
|
|
- "UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start), " +
|
|
|
- "INDEX idx_user_period (user_id, period_start) " +
|
|
|
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录'");
|
|
|
- log.info("迁移317: 已创建basic_quota_usage表");
|
|
|
- } catch (Exception e) {
|
|
|
- // 表已存在,忽略
|
|
|
- }
|
|
|
|
|
|
try {
|
|
|
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_stage (" +
|
|
|
@@ -11194,15 +11154,15 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
}
|
|
|
|
|
|
// 迁移321: 商品/分类可见性由「角色」改为「按会员级别」控制
|
|
|
- // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6
|
|
|
+ // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5
|
|
|
// - products.min_visible_level: 最低可见会员级别编码(NULL=所有用户含未登录可见)
|
|
|
// - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=所有用户含未登录可见)
|
|
|
ensureColumn("membership_levels", "level_order",
|
|
|
- "INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6'");
|
|
|
+ "INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5'");
|
|
|
try {
|
|
|
jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
|
|
|
- "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 WHEN 'PROVIDER' THEN 4 " +
|
|
|
- "WHEN 'PREMIUM' THEN 5 WHEN 'LIFETIME' THEN 6 ELSE level_order END");
|
|
|
+ "WHEN 'FREE' THEN 1 WHEN 'FAMILY' THEN 2 WHEN 'PROVIDER' THEN 3 " +
|
|
|
+ "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
|
|
|
log.info("迁移321: 已回填 membership_levels.level_order");
|
|
|
} catch (Exception e) {
|
|
|
log.warn("迁移321: 回填 membership_levels.level_order 失败: {}", e.getMessage());
|
|
|
@@ -11793,7 +11753,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
private void addProductMinDiscountLevel() {
|
|
|
try {
|
|
|
jdbcTemplate.execute("ALTER TABLE products ADD COLUMN min_discount_level VARCHAR(50) DEFAULT NULL " +
|
|
|
- "COMMENT '享受会员价所需的最低会员级别编码(FREE/BASIC/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)' AFTER min_visible_level");
|
|
|
+ "COMMENT '享受会员价所需的最低会员级别编码(FREE/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)' AFTER min_visible_level");
|
|
|
log.info("迁移350: 已添加 products.min_discount_level 列");
|
|
|
} catch (Exception e) {
|
|
|
// 列已存在,忽略
|
|
|
@@ -11808,18 +11768,57 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
|
|
|
"WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 WHEN 'PROVIDER' THEN 4 " +
|
|
|
"WHEN 'PREMIUM' THEN 5 WHEN 'LIFETIME' THEN 6 ELSE level_order END");
|
|
|
- log.info("迁移351: 已顺延 membership_levels.level_order({} 行),BASIC=2", updated);
|
|
|
+ log.info("迁移351: 已回填 membership_levels.level_order({} 行):FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6", updated);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移351: 回填 membership_levels.level_order 失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 迁移360: 恢复 9.9 元/月 BASIC 体验会员等级与权益(幂等)。
|
|
|
+ * 反向还原提交 f86dd164b 删除的内容;level_order 由 backfillMembershipLevelOrder 统一回填。
|
|
|
+ */
|
|
|
+ private void restoreBasicMembershipLevel() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute(
|
|
|
+ "INSERT IGNORE INTO membership_levels " +
|
|
|
+ "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
|
|
|
+ "VALUES ('BASIC', '体验会员', " +
|
|
|
+ "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
|
|
|
+ "990, " +
|
|
|
+ "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\",\"referral_commission\"]', " +
|
|
|
+ "3, 3)");
|
|
|
+ log.info("迁移360: 已恢复 BASIC 体验会员等级");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移360: 恢复 BASIC 会员等级失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ // 兼容历史数据:早期 BASIC features 缺少 referral_commission,补齐
|
|
|
+ try {
|
|
|
+ jdbcTemplate.update("UPDATE membership_levels SET features = ? WHERE level_code = 'BASIC' AND features NOT LIKE '%referral_commission%'",
|
|
|
+ "[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\",\"referral_commission\"]");
|
|
|
} catch (Exception e) {
|
|
|
- log.warn("迁移351: 顺延 membership_levels.level_order 失败: {}", e.getMessage());
|
|
|
+ log.warn("迁移360: 补齐 BASIC features 失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute(
|
|
|
+ "INSERT IGNORE INTO membership_benefits " +
|
|
|
+ "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
|
|
|
+ "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
|
|
|
+ "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
|
|
|
+ "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
|
|
|
+ "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测试', '📅', '#E0E7FF', 8)");
|
|
|
+ log.info("迁移360: 已恢复 BASIC 会员权益");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移360: 恢复 BASIC 会员权益失败: {}", e.getMessage());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 迁移352: 健康报告上传改为对所有登录用户免费无限,移除 REPORT_UPLOAD 配额与 BASIC 权益文案
|
|
|
+ // 迁移352: 健康报告上传改为对所有登录用户免费无限
|
|
|
private void removeReportUploadQuota() {
|
|
|
try {
|
|
|
int deletedBenefits = jdbcTemplate.update(
|
|
|
"DELETE FROM membership_benefits WHERE level_code = 'BASIC' AND benefit_code = 'REPORT_UPLOAD_LITE'");
|
|
|
- log.info("迁移352: 已移除 BASIC 权益 REPORT_UPLOAD_LITE({} 行)", deletedBenefits);
|
|
|
+ log.info("迁移352: 已清理遗留权益记录({} 行)", deletedBenefits);
|
|
|
} catch (Exception e) {
|
|
|
log.warn("迁移352: 移除 REPORT_UPLOAD_LITE 权益失败: {}", e.getMessage());
|
|
|
}
|
|
|
@@ -11852,6 +11851,29 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 迁移359: 跨小程序单点登录一次性票据表(cfc ↔ train 双向免登)
|
|
|
+ // 详见 schema.sql。此处自愈建表,避免 schema.sql 建表语句因故静默失败导致登录报错。
|
|
|
+ private void createSsoTicketTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS sso_ticket (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
|
|
|
+ "ticket VARCHAR(64) NOT NULL COMMENT '一次性票据(SecureRandom 生成)'," +
|
|
|
+ "source_system VARCHAR(16) NOT NULL COMMENT '签发方系统: cfc / train'," +
|
|
|
+ "target_system VARCHAR(16) NOT NULL COMMENT '目标系统: cfc / train'," +
|
|
|
+ "source_user_id BIGINT NOT NULL COMMENT '签发方用户ID(cfc=users.id / train=train_user.id)'," +
|
|
|
+ "cfc_user_id BIGINT NOT NULL COMMENT 'cfc 统一会员ID(users.id),跨系统换票依据'," +
|
|
|
+ "used TINYINT NOT NULL DEFAULT 0 COMMENT '是否已使用: 0=未用 1=已用'," +
|
|
|
+ "expire_at DATETIME NOT NULL COMMENT '过期时间'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "UNIQUE KEY uk_ticket (ticket)," +
|
|
|
+ "INDEX idx_expire (expire_at)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跨小程序单点登录一次性票据'");
|
|
|
+ log.info("迁移359: 已创建 sso_ticket 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移359: 创建 sso_ticket 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 迁移355: product_recommendation_log 补齐缺失列
|
|
|
// 旧库该表由更早版本的建表语句创建(CREATE TABLE IF NOT EXISTS 不会补列),
|
|
|
// 缺 family_id/was_clicked/was_purchased/clicked_at/purchased_at/updated_at,
|
|
|
@@ -11993,14 +12015,15 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 写入类型「应采集指标项」定义(每次启动覆盖,保证与代码一致) */
|
|
|
- private void seedExpectedIndicator(String typeId, String expectedJson) {
|
|
|
+ /** 写入类型「应采集指标项」定义(仅当为空时写入,保留管理员编辑) */
|
|
|
+ void seedExpectedIndicator(String typeId, String expectedJson) {
|
|
|
try {
|
|
|
int updated = jdbcTemplate.update(
|
|
|
- "UPDATE report_type_registry SET expected_indicators = ?, updated_at = NOW() WHERE type_id = ?",
|
|
|
+ "UPDATE report_type_registry SET expected_indicators = ?, updated_at = NOW() " +
|
|
|
+ "WHERE type_id = ? AND (expected_indicators IS NULL OR expected_indicators = '')",
|
|
|
expectedJson, typeId);
|
|
|
if (updated == 0) {
|
|
|
- log.warn("迁移354: 类型 {} 不存在,跳过指标项定义", typeId);
|
|
|
+ log.info("迁移354: 类型 {} 已有指标项定义,跳过种子", typeId);
|
|
|
} else {
|
|
|
log.info("迁移354: 已为类型 {} 写入指标项定义", typeId);
|
|
|
}
|
|
|
@@ -12020,4 +12043,265 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
}
|
|
|
return sb.append(']').toString();
|
|
|
}
|
|
|
+
|
|
|
+ // 迁移359: 家庭共餐选中食材表(持久化共餐场景食材选择)
|
|
|
+ private void createDietFamilySelectedIngredientsTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_family_selected_ingredients (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
|
|
|
+ "family_id BIGINT NOT NULL COMMENT '家庭ID'," +
|
|
|
+ "food_id BIGINT NOT NULL COMMENT '食材ID'," +
|
|
|
+ "food_name VARCHAR(100) COMMENT '食材名称'," +
|
|
|
+ "score INT COMMENT '推荐分'," +
|
|
|
+ "reason VARCHAR(255) COMMENT '推荐理由'," +
|
|
|
+ "category VARCHAR(50) COMMENT '分类'," +
|
|
|
+ "calories_per_100g INT COMMENT '每100克卡路里'," +
|
|
|
+ "cooking_level TINYINT COMMENT '烹饪难度(1=快手,2=中等,3=耗时)'," +
|
|
|
+ "nutrition_score INT COMMENT '营养密度分(0-100)'," +
|
|
|
+ "balance_reason VARCHAR(255) COMMENT '均衡化推荐理由'," +
|
|
|
+ "sort_order INT DEFAULT 0 COMMENT '排序'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "UNIQUE KEY uk_family_food (family_id, food_id)," +
|
|
|
+ "INDEX idx_family (family_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭共餐选中食材'");
|
|
|
+ log.info("迁移359: 已创建 diet_family_selected_ingredients 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移359: 创建 diet_family_selected_ingredients 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移356: indicator_definitions 升级为统一指标层(指标变化曲线/速查/关联 需求)
|
|
|
+ private void migrateIndicatorDefinitionsColumns() {
|
|
|
+ ensureColumn("indicator_definitions", "source_kind",
|
|
|
+ "VARCHAR(32) DEFAULT NULL COMMENT '指标来源: report_indicator/bacteria/food_index/dimension_score'");
|
|
|
+ ensureColumn("indicator_definitions", "report_type_scope",
|
|
|
+ "VARCHAR(32) DEFAULT NULL COMMENT '适用报告类型(type_id,空=通用)'");
|
|
|
+ ensureColumn("indicator_definitions", "value_type",
|
|
|
+ "VARCHAR(16) DEFAULT NULL COMMENT '语义类型: numeric/categorical/level'");
|
|
|
+ ensureColumn("indicator_definitions", "good_direction",
|
|
|
+ "VARCHAR(16) DEFAULT 'neutral' COMMENT '变化优劣方向: higher_is_better/lower_is_better/in_range_is_better/neutral'");
|
|
|
+ ensureColumn("indicator_definitions", "dimension_code",
|
|
|
+ "VARCHAR(16) DEFAULT NULL COMMENT '关联五维(body/mind/wisdom/action/wealth,可空)'");
|
|
|
+ log.info("迁移356: indicator_definitions 已补齐统一指标层 5 列");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移357: indicator_values 升级为统一观测时序(subject_id/family_id/report_id/report_date/unit/status)
|
|
|
+ private void migrateIndicatorValuesColumns() {
|
|
|
+ ensureColumn("indicator_values", "subject_id", "BIGINT DEFAULT NULL COMMENT '观测对象(family_members.id)'");
|
|
|
+ ensureColumn("indicator_values", "family_id", "BIGINT DEFAULT NULL COMMENT '所属家庭(family_members.family_id)'");
|
|
|
+ ensureColumn("indicator_values", "report_id", "BIGINT DEFAULT NULL COMMENT '来源报告(health_reports.id,食物指数可空)'");
|
|
|
+ ensureColumn("indicator_values", "report_date", "DATE DEFAULT NULL COMMENT '冗余真实报告日期(时序查询键)'");
|
|
|
+ ensureColumn("indicator_values", "unit", "VARCHAR(32) DEFAULT NULL COMMENT '观测单位'");
|
|
|
+ ensureColumn("indicator_values", "status", "VARCHAR(32) DEFAULT NULL COMMENT '观测状态快照(正常/偏高/偏低等)'");
|
|
|
+ log.info("迁移357: indicator_values 已补齐统一观测时序 6 列");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移358: indicator_values 幂等唯一键(source_type+source_id+definition_id)+ 时序查询索引
|
|
|
+ private void addIndicatorValuesKeys() {
|
|
|
+ try {
|
|
|
+ // 先清理极端重复(保留最小 id),避免建唯一键失败;正常数据本无重复
|
|
|
+ jdbcTemplate.execute(
|
|
|
+ "DELETE v FROM indicator_values v " +
|
|
|
+ "JOIN (SELECT MIN(id) keep_id, source_type, source_id, definition_id " +
|
|
|
+ " FROM indicator_values " +
|
|
|
+ " GROUP BY source_type, source_id, definition_id " +
|
|
|
+ " HAVING COUNT(*) > 1) d " +
|
|
|
+ " ON v.source_type = d.source_type AND v.source_id = d.source_id " +
|
|
|
+ " AND v.definition_id = d.definition_id AND v.id <> d.keep_id");
|
|
|
+ log.info("迁移358: 已清理 indicator_values 重复行(如有)");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移358: 清理 indicator_values 重复行失败(表可能为空): {}", e.getMessage());
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE indicator_values ADD UNIQUE KEY uk_source_definition (source_type, source_id, definition_id)");
|
|
|
+ log.info("迁移358: 已添加 uk_source_definition 唯一键");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.info("迁移358: uk_source_definition 已存在,跳过");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE indicator_values ADD INDEX idx_subject_def_date (subject_id, definition_id, report_date)");
|
|
|
+ log.info("迁移358: 已添加 idx_subject_def_date 索引");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.info("迁移358: idx_subject_def_date 已存在,跳过");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移359: 新建 indicator_mapping_rules(指标归一化映射规则)
|
|
|
+ private void createIndicatorMappingRulesTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS indicator_mapping_rules (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
|
|
+ "source_type VARCHAR(32) NOT NULL COMMENT '来源: gut_flora|physical_exam|dan_a2|food_recommend|five_dimension|health_dimension'," +
|
|
|
+ "raw_name VARCHAR(200) NOT NULL COMMENT '报告中的原始名称(指标名/菌名/食材名)'," +
|
|
|
+ "alias_text TEXT COMMENT '别名 JSON 数组,用于模糊匹配'," +
|
|
|
+ "definition_id BIGINT NOT NULL COMMENT 'FK → indicator_definitions.id'," +
|
|
|
+ "match_type VARCHAR(20) DEFAULT 'exact' COMMENT 'exact|fuzzy|ai_fallback|auto_created'," +
|
|
|
+ "priority INT DEFAULT 0 COMMENT '多规则冲突时取最高'," +
|
|
|
+ "status TINYINT DEFAULT 1," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
|
|
|
+ "UNIQUE KEY uk_source_raw (source_type, raw_name)," +
|
|
|
+ "INDEX idx_def (definition_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='指标归一化映射规则'");
|
|
|
+ log.info("迁移359: 已创建 indicator_mapping_rules 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移359: 创建 indicator_mapping_rules 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移360: 新建 indicator_correlations(指标关联关系:人工定义 + 数据校准)
|
|
|
+ private void createIndicatorCorrelationsTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS indicator_correlations (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
|
|
|
+ "definition_id_a BIGINT NOT NULL COMMENT 'code 字典序较小的指标'," +
|
|
|
+ "definition_id_b BIGINT NOT NULL COMMENT 'code 字典序较大的指标'," +
|
|
|
+ "relation_type VARCHAR(20) DEFAULT 'correlation' COMMENT 'correlation|influence|same_source'," +
|
|
|
+ "direction VARCHAR(10) NOT NULL COMMENT 'positive|negative'," +
|
|
|
+ "manual_strength DECIMAL(3,2) NOT NULL COMMENT '人工标定强度 0~1'," +
|
|
|
+ "computed_strength DECIMAL(4,3) NULL COMMENT '统计计算相关系数(Spearman rho)'," +
|
|
|
+ "method VARCHAR(20) DEFAULT 'spearman' COMMENT 'pearson|spearman'," +
|
|
|
+ "sample_size INT DEFAULT 0 COMMENT '参与计算的聚合有效样本数'," +
|
|
|
+ "confidence VARCHAR(10) COMMENT 'high(>=30)|medium(10~29)|low(<10)'," +
|
|
|
+ "effective_strength DECIMAL(4,3) NOT NULL COMMENT '展示值: 样本>=30 时取 computed 否则取 manual'," +
|
|
|
+ "manual_locked TINYINT DEFAULT 0 COMMENT '1=人工锁定,校准不覆盖'," +
|
|
|
+ "cohort_scope VARCHAR(20) DEFAULT 'subject' COMMENT 'subject|family|population'," +
|
|
|
+ "status VARCHAR(20) DEFAULT 'active' COMMENT 'active|inactive|pending'," +
|
|
|
+ "computed_at DATETIME NULL COMMENT '最后校准时间'," +
|
|
|
+ "created_by BIGINT NULL COMMENT '创建者'," +
|
|
|
+ "description VARCHAR(500) NULL COMMENT '给用户看的解释'," +
|
|
|
+ "source_note VARCHAR(255) NULL COMMENT '依据/出处'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
|
|
|
+ "UNIQUE KEY uk_pair (definition_id_a, definition_id_b)," +
|
|
|
+ "INDEX idx_def_a (definition_id_a)," +
|
|
|
+ "INDEX idx_def_b (definition_id_b)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='指标关联关系(人工定义+数据校准)'");
|
|
|
+ log.info("迁移360: 已创建 indicator_correlations 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移360: 创建 indicator_correlations 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移361: health_indicators.collection_date 生产库兜底(schema.sql 已有该列,仅确保老库补齐)
|
|
|
+ private void ensureIndicatorCollectionDate() {
|
|
|
+ ensureColumn("health_indicators", "collection_date",
|
|
|
+ "DATETIME COMMENT '指标采集日期(报告检测日期)'");
|
|
|
+ log.info("迁移361: health_indicators.collection_date 已确认存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移362: 预置五维展示定义(dim.body 等,仅作展示标签,不写 indicator_values)
|
|
|
+ private void seedDimensionDefinitions() {
|
|
|
+ Object[][] dims = {
|
|
|
+ {"dim.body", "身", "身体", "higher_is_better", "higher_is_better"},
|
|
|
+ {"dim.mind", "心", "心理", "higher_is_better", "higher_is_better"},
|
|
|
+ {"dim.wisdom", "智", "智慧", "higher_is_better", "higher_is_better"},
|
|
|
+ {"dim.action", "行", "行动", "higher_is_better", "higher_is_better"},
|
|
|
+ {"dim.wealth", "富", "财富", "higher_is_better", "higher_is_better"},
|
|
|
+ };
|
|
|
+ for (Object[] d : dims) {
|
|
|
+ try {
|
|
|
+ Integer exists = jdbcTemplate.queryForObject(
|
|
|
+ "SELECT COUNT(*) FROM indicator_definitions WHERE code = ?", Integer.class, (String) d[0]);
|
|
|
+ if (exists == null || exists == 0) {
|
|
|
+ jdbcTemplate.update(
|
|
|
+ "INSERT INTO indicator_definitions (domain, category, code, name, data_type, unit, " +
|
|
|
+ "source_kind, value_type, good_direction, dimension_code, sort_order, status) " +
|
|
|
+ "VALUES ('physical', ?, ?, ?, 'number', '分', 'dimension_score', 'numeric', ?, ?, 0, 1)",
|
|
|
+ d[1], d[0], d[2], d[4], d[3]);
|
|
|
+ log.info("迁移362: 已预置维度定义 {}", d[0]);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移362: 预置维度定义 {} 失败: {}", d[0], e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移355: 创建减脂专题体重记录表
|
|
|
+ private void createFatWeightRecordTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_weight_record (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "member_id BIGINT NOT NULL, " +
|
|
|
+ "weight DECIMAL(5,2) NOT NULL, " +
|
|
|
+ "body_fat_percent DECIMAL(4,2) DEFAULT NULL, " +
|
|
|
+ "waist_circumference DECIMAL(4,2) DEFAULT NULL, " +
|
|
|
+ "measure_date DATE NOT NULL, " +
|
|
|
+ "remark VARCHAR(255) DEFAULT NULL, " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_fat_weight_member (member_id, measure_date)) " +
|
|
|
+ "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-体重体脂记录'");
|
|
|
+ log.info("迁移355: 已创建fat_weight_record表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移355: 创建fat_weight_record表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移356: 创建减脂专题饮食记录表
|
|
|
+ private void createFatDietRecordTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_diet_record (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "member_id BIGINT NOT NULL, " +
|
|
|
+ "meal_type VARCHAR(20) NOT NULL, " +
|
|
|
+ "food_name VARCHAR(255) NOT NULL, " +
|
|
|
+ "food_weight DECIMAL(6,2) DEFAULT NULL, " +
|
|
|
+ "calories INT DEFAULT 0, " +
|
|
|
+ "protein DECIMAL(6,2) DEFAULT 0, " +
|
|
|
+ "fat DECIMAL(6,2) DEFAULT 0, " +
|
|
|
+ "carbs DECIMAL(6,2) DEFAULT 0, " +
|
|
|
+ "eat_date DATE NOT NULL, " +
|
|
|
+ "remark VARCHAR(255) DEFAULT NULL, " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_fat_diet_member (member_id, eat_date)) " +
|
|
|
+ "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-饮食记录'");
|
|
|
+ log.info("迁移356: 已创建fat_diet_record表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移356: 创建fat_diet_record表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移357: 创建减脂专题运动记录表
|
|
|
+ private void createFatExerciseRecordTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_exercise_record (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "member_id BIGINT NOT NULL, " +
|
|
|
+ "exercise_type VARCHAR(50) NOT NULL, " +
|
|
|
+ "duration_min INT DEFAULT 0, " +
|
|
|
+ "distance_km DECIMAL(5,2) DEFAULT NULL, " +
|
|
|
+ "calories_burned INT DEFAULT 0, " +
|
|
|
+ "exercise_date DATE NOT NULL, " +
|
|
|
+ "remark VARCHAR(255) DEFAULT NULL, " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_fat_exercise_member (member_id, exercise_date)) " +
|
|
|
+ "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-运动记录'");
|
|
|
+ log.info("迁移357: 已创建fat_exercise_record表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移357: 创建fat_exercise_record表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移358: 创建减脂专题目标表
|
|
|
+ private void createFatGoalTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_goal (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "member_id BIGINT NOT NULL, " +
|
|
|
+ "target_weight DECIMAL(5,2) NOT NULL, " +
|
|
|
+ "start_weight DECIMAL(5,2) NOT NULL, " +
|
|
|
+ "target_body_fat DECIMAL(4,2) DEFAULT NULL, " +
|
|
|
+ "target_date DATE DEFAULT NULL, " +
|
|
|
+ "status VARCHAR(20) DEFAULT 'active', " +
|
|
|
+ "start_date DATE NOT NULL, " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_fat_goal_member (member_id)) " +
|
|
|
+ "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-减脂目标'");
|
|
|
+ log.info("迁移358: 已创建fat_goal表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移358: 创建fat_goal表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|