|
|
@@ -2984,11 +2984,20 @@ 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() {
|
|
|
@@ -12080,6 +12089,153 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 迁移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 {
|