|
@@ -21,7 +21,6 @@ import java.util.ArrayList;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
-import java.util.regex.Matcher;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
@Component
|
|
@Component
|
|
@@ -95,18 +94,37 @@ public class DatabaseInitializer implements CommandLineRunner {
|
|
|
// Remove SQL single-line comments
|
|
// Remove SQL single-line comments
|
|
|
content = content.replaceAll("(?m)^\\s*--.*$", "");
|
|
content = content.replaceAll("(?m)^\\s*--.*$", "");
|
|
|
|
|
|
|
|
- // Extract all CREATE TABLE IF NOT EXISTS statements
|
|
|
|
|
- Pattern pattern = Pattern.compile(
|
|
|
|
|
- "CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+.*?;",
|
|
|
|
|
- Pattern.CASE_INSENSITIVE | Pattern.DOTALL
|
|
|
|
|
- );
|
|
|
|
|
- Matcher matcher = pattern.matcher(content);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // 提取所有 CREATE TABLE IF NOT EXISTS 语句。
|
|
|
|
|
+ // 不能用非贪婪 ".*?;" 匹配:COMMENT 字符串字面量中可能含分号(如
|
|
|
|
|
+ // task_reminder_config.offset_minutes 的注释 "截止前N分钟; after_timeout..."),
|
|
|
|
|
+ // 会被提前截断导致语句不完整而建表失败。此处按字符扫描,跳过引号内部的分号。
|
|
|
List<String> createTableSQLs = new ArrayList<>();
|
|
List<String> createTableSQLs = new ArrayList<>();
|
|
|
- while (matcher.find()) {
|
|
|
|
|
- String sql = matcher.group().trim();
|
|
|
|
|
- if (!sql.isEmpty()) {
|
|
|
|
|
- createTableSQLs.add(sql);
|
|
|
|
|
|
|
+ Pattern createTableHead = Pattern.compile(
|
|
|
|
|
+ "^\\s*CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\b", Pattern.CASE_INSENSITIVE);
|
|
|
|
|
+ StringBuilder stmt = new StringBuilder();
|
|
|
|
|
+ boolean inSingleQuote = false;
|
|
|
|
|
+ boolean inBacktick = false;
|
|
|
|
|
+ for (int i = 0; i < content.length(); i++) {
|
|
|
|
|
+ char c = content.charAt(i);
|
|
|
|
|
+ if (c == '\'' && !inBacktick) {
|
|
|
|
|
+ if (inSingleQuote && i + 1 < content.length() && content.charAt(i + 1) == '\'') {
|
|
|
|
|
+ stmt.append("''");
|
|
|
|
|
+ i++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ inSingleQuote = !inSingleQuote;
|
|
|
|
|
+ stmt.append(c);
|
|
|
|
|
+ } else if (c == '`' && !inSingleQuote) {
|
|
|
|
|
+ inBacktick = !inBacktick;
|
|
|
|
|
+ stmt.append(c);
|
|
|
|
|
+ } else if (c == ';' && !inSingleQuote && !inBacktick) {
|
|
|
|
|
+ String sql = stmt.toString().trim();
|
|
|
|
|
+ if (!sql.isEmpty() && createTableHead.matcher(sql).find()) {
|
|
|
|
|
+ createTableSQLs.add(sql + ";");
|
|
|
|
|
+ }
|
|
|
|
|
+ stmt.setLength(0);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stmt.append(c);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -3014,6 +3032,15 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
|
|
|
createIndicatorCorrelationsTable();
|
|
createIndicatorCorrelationsTable();
|
|
|
ensureIndicatorCollectionDate();
|
|
ensureIndicatorCollectionDate();
|
|
|
seedDimensionDefinitions();
|
|
seedDimensionDefinitions();
|
|
|
|
|
+
|
|
|
|
|
+ // 商品组合推荐与采购任务迁移(363)
|
|
|
|
|
+ createProcurementTaskTable();
|
|
|
|
|
+
|
|
|
|
|
+ // 迁移364: health_report_drafts 添加解析进度/重试字段(known-type 解析失败退避重试,不再静默回退 opencode)
|
|
|
|
|
+ addDraftParseProgressColumns();
|
|
|
|
|
+
|
|
|
|
|
+ // 迁移365: diet_preferences 添加无特殊偏好标记(问卷第3步「无特殊偏好」一键跳过落库)
|
|
|
|
|
+ ensureColumn("diet_preferences", "no_preference", "TINYINT(1) DEFAULT 0 COMMENT '无特殊偏好标记(1=用户明确选择无特殊偏好)'");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void migrateHealthDimensionsTable() {
|
|
private void migrateHealthDimensionsTable() {
|
|
@@ -3279,6 +3306,12 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
|
|
|
// blood_type_config.description
|
|
// blood_type_config.description
|
|
|
modifyColumnToText("blood_type_config", "description", "性格描述");
|
|
modifyColumnToText("blood_type_config", "description", "性格描述");
|
|
|
|
|
|
|
|
|
|
+ // 迁移20240709-02: 北京菌群报告采集增强(P0 疾病关联菌 / P1 菌群状态)
|
|
|
|
|
+ // health_disease_risk.related_bacteria: 疾病关联菌(异常菌)明细 JSON
|
|
|
|
|
+ ensureColumn("health_disease_risk", "related_bacteria", "TEXT NULL COMMENT '关联菌明细JSON:[{bacteria,correlation,value,status}]'");
|
|
|
|
|
+ // health_reports.gut_status_level: 肠道菌群状态(0度~4度)
|
|
|
|
|
+ ensureColumn("health_reports", "gut_status_level", "VARCHAR(20) NULL COMMENT '肠道菌群状态(0度~4度)'");
|
|
|
|
|
+
|
|
|
// 迁移248: 任务系统升级(行为调度中心)——tasks 新增字段
|
|
// 迁移248: 任务系统升级(行为调度中心)——tasks 新增字段
|
|
|
ensureColumn("tasks", "prerequisite_task_id", "BIGINT NULL COMMENT '前置任务ID:完成后才解锁本任务'");
|
|
ensureColumn("tasks", "prerequisite_task_id", "BIGINT NULL COMMENT '前置任务ID:完成后才解锁本任务'");
|
|
|
ensureColumn("tasks", "action_type", "VARCHAR(32) NULL COMMENT '联动动作: minigame/focus/survey/article/activity/cart/order'");
|
|
ensureColumn("tasks", "action_type", "VARCHAR(32) NULL COMMENT '联动动作: minigame/focus/survey/article/activity/cart/order'");
|
|
@@ -6299,6 +6332,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
// 迁移134: products表添加sort_order列(商品按排序字段排序展示)
|
|
// 迁移134: products表添加sort_order列(商品按排序字段排序展示)
|
|
|
ensureColumn("products", "sort_order", "INT DEFAULT 0 COMMENT '排序(值越小越靠前)'");
|
|
ensureColumn("products", "sort_order", "INT DEFAULT 0 COMMENT '排序(值越小越靠前)'");
|
|
|
ensureColumn("products", "recommendation_tags", "VARCHAR(500) DEFAULT NULL COMMENT '推荐标签'");
|
|
ensureColumn("products", "recommendation_tags", "VARCHAR(500) DEFAULT NULL COMMENT '推荐标签'");
|
|
|
|
|
+ ensureColumn("products", "is_recommended", "TINYINT(1) DEFAULT 0 COMMENT '是否推荐商品 0否 1是'");
|
|
|
ensureColumn("products", "repurchase_interval_days", "INT DEFAULT 30 COMMENT '复购间隔天数'");
|
|
ensureColumn("products", "repurchase_interval_days", "INT DEFAULT 30 COMMENT '复购间隔天数'");
|
|
|
|
|
|
|
|
// 迁移134: 创建 category_ppoint 表(品类P点比例配置)
|
|
// 迁移134: 创建 category_ppoint 表(品类P点比例配置)
|
|
@@ -9206,7 +9240,8 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
"('system', '系统通知', '您有一条新的系统通知,请前往查看。', 'in_app'), " +
|
|
"('system', '系统通知', '您有一条新的系统通知,请前往查看。', 'in_app'), " +
|
|
|
"('energy_change', '能量变动', '{description}获得{amount}能量', 'in_app'), " +
|
|
"('energy_change', '能量变动', '{description}获得{amount}能量', 'in_app'), " +
|
|
|
"('points_change', '积分变动', '{description}变动{amount}积分', 'in_app'), " +
|
|
"('points_change', '积分变动', '{description}变动{amount}积分', 'in_app'), " +
|
|
|
- "('cf_change', 'CF值变动', '{remark}获得{amount} CF值', 'in_app')");
|
|
|
|
|
|
|
+ "('cf_change', 'CF值变动', '{remark}获得{amount} CF值', 'in_app'), " +
|
|
|
|
|
+ "('procurement_completed', '采购任务已完成', '您的【{comboTier}】套餐采购任务已完成,共{productCount}件商品,下单时间:{createTime},产品即将送达,请及时查收。', 'in_app,wechat')");
|
|
|
log.info("notification_templates 种子数据已插入");
|
|
log.info("notification_templates 种子数据已插入");
|
|
|
} else {
|
|
} else {
|
|
|
log.info("notification_templates 已有 {} 条数据,跳过种子插入", tplCount);
|
|
log.info("notification_templates 已有 {} 条数据,跳过种子插入", tplCount);
|
|
@@ -10989,6 +11024,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
backfillTemplateInstanceFamilyMemberId();
|
|
backfillTemplateInstanceFamilyMemberId();
|
|
|
createReportTransferRequestsTable();
|
|
createReportTransferRequestsTable();
|
|
|
addProductMinDiscountLevel();
|
|
addProductMinDiscountLevel();
|
|
|
|
|
+ restoreBasicMembershipLevel();
|
|
|
backfillMembershipLevelOrder();
|
|
backfillMembershipLevelOrder();
|
|
|
removeReportUploadQuota();
|
|
removeReportUploadQuota();
|
|
|
|
|
|
|
@@ -11818,15 +11854,53 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
private void backfillMembershipLevelOrder() {
|
|
private void backfillMembershipLevelOrder() {
|
|
|
try {
|
|
try {
|
|
|
int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
|
|
int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
|
|
|
- "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("迁移351: 已顺延 membership_levels.level_order({} 行),BASIC=2", updated);
|
|
|
|
|
|
|
+ "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({} 行):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("迁移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) {
|
|
} catch (Exception e) {
|
|
|
- log.warn("迁移351: 顺延 membership_levels.level_order 失败: {}", e.getMessage());
|
|
|
|
|
|
|
+ log.warn("迁移360: 恢复 BASIC 会员权益失败: {}", e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 迁移352: 健康报告上传改为对所有登录用户免费无限
|
|
|
|
|
private void removeReportUploadQuota() {
|
|
private void removeReportUploadQuota() {
|
|
|
try {
|
|
try {
|
|
|
int deletedBenefits = jdbcTemplate.update(
|
|
int deletedBenefits = jdbcTemplate.update(
|
|
@@ -12319,7 +12393,6 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 迁移359: products 表添加 vendor_visible 列(供应商仅可见功能)
|
|
|
|
|
private void addProductVendorVisibleColumn() {
|
|
private void addProductVendorVisibleColumn() {
|
|
|
ensureColumn("products", "vendor_visible", "TINYINT(1) DEFAULT NULL COMMENT '是否仅供应商体系内用户可见(列表接口生效,直接分享可bypass)'");
|
|
ensureColumn("products", "vendor_visible", "TINYINT(1) DEFAULT NULL COMMENT '是否仅供应商体系内用户可见(列表接口生效,直接分享可bypass)'");
|
|
|
}
|
|
}
|
|
@@ -12442,4 +12515,55 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
}
|
|
}
|
|
|
log.info("迁移364: disease_intake_limits种子数据初始化完成");
|
|
log.info("迁移364: disease_intake_limits种子数据初始化完成");
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 迁移363: 创建采购任务表(商品组合推荐与采购任务生命周期管理)
|
|
|
|
|
+ private void createProcurementTaskTable() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS procurement_tasks (" +
|
|
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
|
|
+ "task_id VARCHAR(64) NOT NULL COMMENT '任务唯一标识', " +
|
|
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '所属用户ID', " +
|
|
|
|
|
+ "family_member_id BIGINT NOT NULL COMMENT '所属家庭成员ID', " +
|
|
|
|
|
+ "combo_tier VARCHAR(20) NOT NULL COMMENT '选择的套餐等级: high|medium|low', " +
|
|
|
|
|
+ "original_combo_tier VARCHAR(20) DEFAULT NULL COMMENT '原始套餐等级(用于自定义追踪)', " +
|
|
|
|
|
+ "selected_product_sku_ids TEXT COMMENT '最终选中的产品SKU列表(JSON数组)', " +
|
|
|
|
|
+ "custom_addition_sku_ids TEXT COMMENT '用户自新增的产品SKU列表(JSON数组)', " +
|
|
|
|
|
+ "custom_removal_sku_ids TEXT COMMENT '用户移除的产品SKU列表(JSON数组)', " +
|
|
|
|
|
+ "status VARCHAR(32) NOT NULL COMMENT '任务状态: SELECTED|PROCURING|PROCURED|EXECUTING|COMPLETED|CANCELLED', " +
|
|
|
|
|
+ "create_time DATETIME NOT NULL COMMENT '创建时间', " +
|
|
|
|
|
+ "complete_time DATETIME DEFAULT NULL COMMENT '完成时间', " +
|
|
|
|
|
+ "message_reminder_sent TINYINT DEFAULT 0 COMMENT '是否已发送消息提醒', " +
|
|
|
|
|
+ "execution_task_id VARCHAR(64) DEFAULT NULL COMMENT '关联的执行任务ID', " +
|
|
|
|
|
+ "update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
|
|
|
|
|
+ "UNIQUE KEY uk_task_id (task_id), " +
|
|
|
|
|
+ "INDEX idx_user_id (user_id), " +
|
|
|
|
|
+ "INDEX idx_family_member_id (family_member_id), " +
|
|
|
|
|
+ "INDEX idx_status (status), " +
|
|
|
|
|
+ "INDEX idx_create_time (create_time)" +
|
|
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购任务表(商品组合推荐与采购任务生命周期管理)'");
|
|
|
|
|
+ log.info("已创建procurement_tasks表");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("创建procurement_tasks表失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 迁移364: health_report_drafts 添加解析进度/重试字段(INFORMATION_SCHEMA 检查后再 ALTER,幂等)
|
|
|
|
|
+ private void addDraftParseProgressColumns() {
|
|
|
|
|
+ if (!columnExists("health_report_drafts", "parse_attempt")) {
|
|
|
|
|
+ jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN parse_attempt INT DEFAULT 0 COMMENT '已知类型解析重试次数'");
|
|
|
|
|
+ log.info("已添加 parse_attempt 列到 health_report_drafts 表");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!columnExists("health_report_drafts", "next_retry_at")) {
|
|
|
|
|
+ jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN next_retry_at DATETIME NULL COMMENT '下次自动重试时间(指数退避,上限5分钟)'");
|
|
|
|
|
+ log.info("已添加 next_retry_at 列到 health_report_drafts 表");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!columnExists("health_report_drafts", "parse_started_at")) {
|
|
|
|
|
+ jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN parse_started_at DATETIME NULL COMMENT '最近一次解析开始时间'");
|
|
|
|
|
+ log.info("已添加 parse_started_at 列到 health_report_drafts 表");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!columnExists("health_report_drafts", "parse_finished_at")) {
|
|
|
|
|
+ jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN parse_finished_at DATETIME NULL COMMENT '最近一次解析结束时间(成功/失败)'");
|
|
|
|
|
+ log.info("已添加 parse_finished_at 列到 health_report_drafts 表");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|