|
|
@@ -3221,6 +3221,31 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
|
|
|
} catch (Exception e) {
|
|
|
log.warn("迁移393b: 创建 org_members 表失败(可能已存在): {}", e.getMessage());
|
|
|
}
|
|
|
+
|
|
|
+ // 迁移394: 创建 planner_cert_categories 表 + 初始化首批学习种类(planner/nutritionist/assessor)
|
|
|
+ // 见 docs/superpowers/specs/2026-10-22-planner-learning-assessment-gate-design.md
|
|
|
+ createPlannerCertCategoriesTable();
|
|
|
+
|
|
|
+ // 迁移395: 创建 planner_courses 表(线上课程目录,映射小鹅通课程)
|
|
|
+ createPlannerCoursesTable();
|
|
|
+
|
|
|
+ // 迁移396: 创建 planner_course_progress 表(每用户每课程进度,小鹅通回传/手动同步写入)
|
|
|
+ createPlannerCourseProgressTable();
|
|
|
+
|
|
|
+ // 迁移397: 创建 planner_offline_course_map 表(线下培训活动↔认证类别桥接,复用 activities 模块)
|
|
|
+ createPlannerOfflineCourseMapTable();
|
|
|
+
|
|
|
+ // 迁移398: 创建 activity_checkins 表(活动签到记录,供培训课时桥接,修复签到审核形同虚设旧患)
|
|
|
+ createActivityCheckinsTable();
|
|
|
+
|
|
|
+ // 迁移399: 创建 planner_certifications 表(规划师每类别培训认证主表,认证闸门依据)
|
|
|
+ createPlannerCertificationsTable();
|
|
|
+
|
|
|
+ // 迁移400: 创建 planner_exam_papers / planner_exam_questions / planner_exam_attempts 表(考核自建)
|
|
|
+ createPlannerExamTables();
|
|
|
+
|
|
|
+ // 迁移401: 存量规划师补登认证(D6:active guide 按已持有基础类别 planner certified 补登,避免误封存量)
|
|
|
+ backfillExistingPlannerCertifications();
|
|
|
}
|
|
|
|
|
|
private void migrateHealthDimensionsTable() {
|
|
|
@@ -12738,4 +12763,226 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
log.warn("迁移387: 下线服务角色申请菜单失败: {}", e.getMessage());
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 迁移391: 创建 planner_cert_categories 表(学习种类/认证类别,管理端可配置)+ 首批种子数据
|
|
|
+ private void createPlannerCertCategoriesTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_cert_categories (" +
|
|
|
+ "id INT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "code VARCHAR(32) NOT NULL COMMENT '类别编码: planner/nutritionist/assessor...'," +
|
|
|
+ "name VARCHAR(64) NOT NULL COMMENT '类别名称'," +
|
|
|
+ "description VARCHAR(255) DEFAULT '' COMMENT '类别说明'," +
|
|
|
+ "required_hours DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '该类别要求总课时(线上+线下)'," +
|
|
|
+ "is_base TINYINT(1) DEFAULT 0 COMMENT '1=基础类别(获得即 Guide.status=active)'," +
|
|
|
+ "order_index INT DEFAULT 0 COMMENT '排序(越小越前)'," +
|
|
|
+ "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "UNIQUE KEY uk_code (code)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师学习种类/认证类别'");
|
|
|
+ log.info("迁移394: 已创建/确认 planner_cert_categories 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移394: 创建 planner_cert_categories 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ // 首批种子类别(幂等)
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("INSERT IGNORE INTO planner_cert_categories (code, name, description, required_hours, is_base, order_index, status) VALUES " +
|
|
|
+ "('planner', '成长规划师', '规划师基础身份认证,默认必学必考;获得后即可接基础服务', 10, 1, 0, 1), " +
|
|
|
+ "('nutritionist', '营养师', '营养方案/饮食建议类服务前置认证', 8, 0, 1, 1), " +
|
|
|
+ "('assessor', '测评师', '测评录入/解读类服务前置认证', 8, 0, 2, 1)");
|
|
|
+ log.info("迁移394: 首批学习种类种子数据已确认");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移394: 初始化首批学习种类种子数据失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移395: 创建 planner_courses 表(线上课程目录,映射小鹅通课程/专栏)
|
|
|
+ private void createPlannerCoursesTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_courses (" +
|
|
|
+ "id INT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "category_id INT NOT NULL COMMENT '所属学习种类(planner_cert_categories.id)'," +
|
|
|
+ "xiaoe_resource_id VARCHAR(64) DEFAULT '' COMMENT '小鹅通课程/商品ID'," +
|
|
|
+ "title VARCHAR(120) NOT NULL COMMENT '课程标题'," +
|
|
|
+ "required_hours DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '该课程要求课时'," +
|
|
|
+ "order_index INT DEFAULT 0 COMMENT '排序'," +
|
|
|
+ "is_required TINYINT(1) DEFAULT 1 COMMENT '是否必修'," +
|
|
|
+ "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "KEY idx_category (category_id)," +
|
|
|
+ "KEY idx_status (status)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程目录(小鹅通)'");
|
|
|
+ log.info("迁移395: 已创建/确认 planner_courses 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移395: 创建 planner_courses 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移396: 创建 planner_course_progress 表(每用户每线上课程学习进度,小鹅通回传/手动同步写入)
|
|
|
+ private void createPlannerCourseProgressTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_course_progress (" +
|
|
|
+ "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '规划师用户ID'," +
|
|
|
+ "course_id INT NOT NULL COMMENT 'planner_courses.id'," +
|
|
|
+ "category_id INT NOT NULL COMMENT '所属学习种类'," +
|
|
|
+ "xiaoe_resource_id VARCHAR(64) DEFAULT '' COMMENT '小鹅通资源ID'," +
|
|
|
+ "progress_percent INT DEFAULT 0 COMMENT '学习进度百分比'," +
|
|
|
+ "completed_hours DECIMAL(6,2) DEFAULT 0 COMMENT '已完成课时'," +
|
|
|
+ "completed TINYINT(1) DEFAULT 0 COMMENT '是否完成'," +
|
|
|
+ "last_synced_at DATETIME DEFAULT NULL COMMENT '最近同步时间'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "UNIQUE KEY uk_user_course (user_id, course_id)," +
|
|
|
+ "KEY idx_user (user_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程学习进度'");
|
|
|
+ log.info("迁移396: 已创建/确认 planner_course_progress 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移396: 创建 planner_course_progress 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移397: 创建 planner_offline_course_map 表(线下培训活动↔认证类别桥接,复用 activities 模块)
|
|
|
+ private void createPlannerOfflineCourseMapTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_offline_course_map (" +
|
|
|
+ "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "activity_id BIGINT NOT NULL COMMENT '关联 activities.id(线下培训活动)'," +
|
|
|
+ "category_id INT NOT NULL COMMENT '关联 planner_cert_categories.id'," +
|
|
|
+ "duration_hours DECIMAL(6,2) DEFAULT 0 COMMENT '该活动签到后计入的课时'," +
|
|
|
+ "is_required TINYINT(1) DEFAULT 0 COMMENT '是否必修环节'," +
|
|
|
+ "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "UNIQUE KEY uk_activity (activity_id)," +
|
|
|
+ "KEY idx_category (category_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线下培训活动↔认证类别映射(桥接)'");
|
|
|
+ log.info("迁移397: 已创建/确认 planner_offline_course_map 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移397: 创建 planner_offline_course_map 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移398: 创建 activity_checkins 表(活动签到记录,供培训课时桥接;修复"签到审核形同虚设"旧患)
|
|
|
+ private void createActivityCheckinsTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_checkins (" +
|
|
|
+ "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "activity_id BIGINT NOT NULL COMMENT '活动ID(activities.id)'," +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '签到人用户ID(规划师=userId)'," +
|
|
|
+ "registration_id BIGINT DEFAULT NULL COMMENT '关联报名记录(activity_registrations.id)'," +
|
|
|
+ "check_in_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间'," +
|
|
|
+ "check_in_type VARCHAR(16) DEFAULT 'qr' COMMENT 'qr扫码/admin手动/gps'," +
|
|
|
+ "check_in_by BIGINT DEFAULT NULL COMMENT 'admin手动补录人ID'," +
|
|
|
+ "review_status VARCHAR(16) DEFAULT 'approved' COMMENT 'approved/pending/rejected(对应原requireCheckinReview)'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "UNIQUE KEY uk_activity_user (activity_id, user_id)," +
|
|
|
+ "KEY idx_user (user_id)," +
|
|
|
+ "KEY idx_activity (activity_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动签到记录(通用,供培训课时桥接)'");
|
|
|
+ log.info("迁移398: 已创建/确认 activity_checkins 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移398: 创建 activity_checkins 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移399: 创建 planner_certifications 表(规划师每类别培训认证主表,认证闸门依据)
|
|
|
+ private void createPlannerCertificationsTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_certifications (" +
|
|
|
+ "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '规划师用户ID'," +
|
|
|
+ "guide_id BIGINT DEFAULT NULL COMMENT '关联 guides.id'," +
|
|
|
+ "category_id INT NOT NULL COMMENT '所属学习种类(planner_cert_categories.id)'," +
|
|
|
+ "category_code VARCHAR(32) DEFAULT '' COMMENT '冗余类别编码便于查询'," +
|
|
|
+ "required_hours DECIMAL(6,2) DEFAULT 0 COMMENT '该类别总要求课时'," +
|
|
|
+ "completed_hours DECIMAL(6,2) DEFAULT 0 COMMENT '已完成课时(线上+线下汇总)'," +
|
|
|
+ "learning_status VARCHAR(20) DEFAULT 'pending_learning' COMMENT 'pending_learning/learning_completed'," +
|
|
|
+ "exam_status VARCHAR(20) DEFAULT 'none' COMMENT 'none/exam_failed/exam_passed'," +
|
|
|
+ "exam_score INT DEFAULT NULL COMMENT '考核分数'," +
|
|
|
+ "exam_attempts INT DEFAULT 0 COMMENT '考核尝试次数'," +
|
|
|
+ "cert_no VARCHAR(64) DEFAULT '' COMMENT '证书编号'," +
|
|
|
+ "certified_at DATETIME DEFAULT NULL COMMENT '认证通过时间'," +
|
|
|
+ "expire_at DATETIME DEFAULT NULL COMMENT '有效期(NULL=不过期)'," +
|
|
|
+ "status VARCHAR(20) DEFAULT 'pending_cert' COMMENT 'pending_cert/learning_completed/certified/suspended'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "UNIQUE KEY uk_user_category (user_id, category_id)," +
|
|
|
+ "KEY idx_status (status)," +
|
|
|
+ "KEY idx_user (user_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师每类别培训认证主表'");
|
|
|
+ log.info("迁移399: 已创建/确认 planner_certifications 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移399: 创建 planner_certifications 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移400: 创建考核三表(试卷/题库/作答记录,考核本系统自建)
|
|
|
+ private void createPlannerExamTables() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_exam_papers (" +
|
|
|
+ "id INT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "category_id INT NOT NULL COMMENT '所属学习种类'," +
|
|
|
+ "title VARCHAR(120) NOT NULL COMMENT '试卷标题'," +
|
|
|
+ "pass_score INT NOT NULL DEFAULT 60 COMMENT '及格分'," +
|
|
|
+ "duration_minutes INT DEFAULT 30 COMMENT '限时(分钟)'," +
|
|
|
+ "question_count INT DEFAULT 0 COMMENT '题目数'," +
|
|
|
+ "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "KEY idx_category (category_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核试卷'");
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_exam_questions (" +
|
|
|
+ "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "paper_id INT NOT NULL COMMENT '试卷ID(planner_exam_papers.id)'," +
|
|
|
+ "question TEXT NOT NULL COMMENT '题干'," +
|
|
|
+ "options TEXT COMMENT '选项JSON数组'," +
|
|
|
+ "answer VARCHAR(10) COMMENT '正确选项'," +
|
|
|
+ "score INT DEFAULT 1 COMMENT '分值'," +
|
|
|
+ "order_index INT DEFAULT 0 COMMENT '排序'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "KEY idx_paper (paper_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考核题库'");
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_exam_attempts (" +
|
|
|
+ "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '规划师用户ID'," +
|
|
|
+ "category_id INT NOT NULL COMMENT '所属学习种类'," +
|
|
|
+ "paper_id INT NOT NULL COMMENT '试卷ID'," +
|
|
|
+ "score INT DEFAULT 0 COMMENT '得分'," +
|
|
|
+ "passed TINYINT(1) DEFAULT 0 COMMENT '是否通过'," +
|
|
|
+ "answers TEXT COMMENT 'JSON作答明细'," +
|
|
|
+ "started_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间'," +
|
|
|
+ "submitted_at DATETIME DEFAULT NULL COMMENT '提交时间'," +
|
|
|
+ "PRIMARY KEY (id)," +
|
|
|
+ "KEY idx_user (user_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核作答记录'");
|
|
|
+ log.info("迁移400: 已创建/确认考核三表(试卷/题库/作答记录)");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移400: 创建考核三表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移401: 存量规划师补登认证(D6:active guide 按已持有基础类别 planner certified 补登,其余类别 pending_learning)
|
|
|
+ private void backfillExistingPlannerCertifications() {
|
|
|
+ try {
|
|
|
+ int updated = jdbcTemplate.update(
|
|
|
+ "INSERT IGNORE INTO planner_certifications (user_id, guide_id, category_id, category_code, required_hours, completed_hours, learning_status, exam_status, exam_score, cert_no, certified_at, status) " +
|
|
|
+ "SELECT g.user_id, g.id, c.id, c.code, c.required_hours, c.required_hours, 'learning_completed', 'exam_passed', 100, CONCAT('CERT-', DATE_FORMAT(NOW(), '%Y%m%d'), '-', g.user_id), NOW(), 'certified' " +
|
|
|
+ "FROM guides g " +
|
|
|
+ "JOIN planner_cert_categories c ON c.code = 'planner' AND c.status = 1 " +
|
|
|
+ "WHERE g.status = 'active'");
|
|
|
+ log.info("迁移401: 存量规划师基础类别认证补登 {} 行", updated);
|
|
|
+ // 同步存量 active guide 状态为 active(保持不变),并确保新增认证不影响接单
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移401: 存量规划师认证补登失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|