diff --git a/docs/course-domain-jsonb-migration-runbook.md b/docs/course-domain-jsonb-migration-runbook.md new file mode 100644 index 0000000..2e7e3c0 --- /dev/null +++ b/docs/course-domain-jsonb-migration-runbook.md @@ -0,0 +1,206 @@ +# 教学域命名重构:JSONB 数据迁移手册 + +## 1. 结论先看 + +- JSONB 结构字段已在 Rust 代码中切换为 `course_*` 命名。 +- 已新增并提交 JSONB 存量迁移脚本(`up/down.sql`)。 +- 对于线上已有数据,仍需要按本手册执行 migration + 校验,确保旧键 `qumu_*` 全量迁移到 `course_*`。 + +## 2. 代码与迁移现状 + +### 2.1 Rust 结构已改为 `course_*` + +核心结构体(`htyws_models`): +- `CourseSectionJsonData` 使用: + - `course_id` + - `course_type` + - `course_name` + - `course_text` + - `course_category_key` + - `course_category_name` + +并且 `daka/jihua/kecheng` 相关请求与模型字段已统一到 `course_sections`。 + +### 2.2 JSONB 存量迁移脚本 + +已存在以下 migration: + +- `htyws_models/migrations/2026-04-23-100003_migrate_qumu_jsonb_keys_to_course` + - 迁移对象:`daka.course_sections`、`jihua.course_sections` + - 逻辑:使用 `jsonb_array_elements + jsonb_agg` 重组 `vals` 数组元素,将 `qumu_*` 改为 `course_*` + +- `htykc_models/migrations/2026-04-23-100004_migrate_qumu_jsonb_keys_to_course` + - 迁移对象:`kecheng.course_sections` + - 逻辑:同上 + +## 3. 迁移前检查 + +### 3.1 数据库与权限 + +确认目标库可连接并有执行 migration 权限: + +```sql +\c htyws_local +SELECT current_user, current_database(); +``` + +`htykc_local` 同样检查。 + +### 3.2 迁移前数据基线(建议保存结果) + +在 `htyws_local`: + +```sql +-- daka 中仍含 qumu_* 键的记录数 +SELECT count(*) AS daka_rows_with_qumu_keys +FROM daka +WHERE course_sections IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM jsonb_array_elements(course_sections->'vals') AS item + WHERE item ? 'qumu_id' + OR item ? 'qumu_type' + OR item ? 'qumu_name' + OR item ? 'qumu_text' + OR item ? 'qumu_category_key' + OR item ? 'qumu_category_name' + ); + +-- jihua 中仍含 qumu_* 键的记录数 +SELECT count(*) AS jihua_rows_with_qumu_keys +FROM jihua +WHERE course_sections IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM jsonb_array_elements(course_sections->'vals') AS item + WHERE item ? 'qumu_id' + OR item ? 'qumu_type' + OR item ? 'qumu_name' + OR item ? 'qumu_text' + OR item ? 'qumu_category_key' + OR item ? 'qumu_category_name' + ); +``` + +在 `htykc_local`: + +```sql +SELECT count(*) AS kecheng_rows_with_qumu_keys +FROM kecheng +WHERE course_sections IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM jsonb_array_elements(course_sections->'vals') AS item + WHERE item ? 'qumu_id' + OR item ? 'qumu_type' + OR item ? 'qumu_name' + OR item ? 'qumu_text' + OR item ? 'qumu_category_key' + OR item ? 'qumu_category_name' + ); +``` + +## 4. 执行步骤 + +### 4.1 执行 `htyws_models` migration + +```bash +cd huike-back/htyws_models +DATABASE_URL=postgres://htyws@localhost/htyws_local diesel migration run +``` + +### 4.2 执行 `htykc_models` migration + +```bash +cd huike-back/htykc_models +DATABASE_URL=postgres://htykc@localhost/htykc_local diesel migration run +``` + +> 若本地无 `htykc` DB 角色,先创建角色或使用可用 DB 用户连接串再执行。 + +## 5. 迁移后校验 + +### 5.1 校验旧键已清理 + +在 `htyws_local` 与 `htykc_local` 分别执行: + +```sql +-- 应返回 0:仍包含 qumu_* 的元素数量 +SELECT count(*) AS remaining_qumu_key_rows +FROM ( + SELECT item + FROM daka, jsonb_array_elements(daka.course_sections->'vals') AS item + WHERE daka.course_sections IS NOT NULL + UNION ALL + SELECT item + FROM jihua, jsonb_array_elements(jihua.course_sections->'vals') AS item + WHERE jihua.course_sections IS NOT NULL +) t +WHERE item ? 'qumu_id' + OR item ? 'qumu_type' + OR item ? 'qumu_name' + OR item ? 'qumu_text' + OR item ? 'qumu_category_key' + OR item ? 'qumu_category_name'; +``` + +`htykc_local` 可单独查: + +```sql +SELECT count(*) AS remaining_qumu_key_rows +FROM ( + SELECT item + FROM kecheng, jsonb_array_elements(kecheng.course_sections->'vals') AS item + WHERE kecheng.course_sections IS NOT NULL +) t +WHERE item ? 'qumu_id' + OR item ? 'qumu_type' + OR item ? 'qumu_name' + OR item ? 'qumu_text' + OR item ? 'qumu_category_key' + OR item ? 'qumu_category_name'; +``` + +### 5.2 校验新键可读 + +```sql +SELECT count(*) AS rows_with_course_id +FROM ( + SELECT item + FROM daka, jsonb_array_elements(daka.course_sections->'vals') AS item + WHERE daka.course_sections IS NOT NULL + UNION ALL + SELECT item + FROM jihua, jsonb_array_elements(jihua.course_sections->'vals') AS item + WHERE jihua.course_sections IS NOT NULL +) t +WHERE item ? 'course_id'; +``` + +## 6. 回滚方案 + +若需要回滚 JSONB 键名,可执行: + +```bash +cd huike-back/htyws_models +DATABASE_URL=postgres://htyws@localhost/htyws_local diesel migration revert + +cd huike-back/htykc_models +DATABASE_URL=postgres://htykc@localhost/htykc_local diesel migration revert +``` + +对应 `down.sql` 会将 `course_*` 键改回 `qumu_*`。 + +## 7. 上线建议顺序 + +1. 先部署支持新字段命名的后端代码。 +2. 执行 DB migration(表/列 + JSONB 键)。 +3. 执行上述 SQL 校验。 +4. 再部署前端。 +5. 观察日志与关键功能(课程、课程分组、计划、打卡、排课)至少一个业务周期。 + +## 8. 风险提示 + +- JSONB 迁移依赖 `course_sections->'vals'` 是数组结构;若历史脏数据不满足该结构,可能迁移失败。 +- 执行前建议做 DB 备份或快照。 +- 若线上写流量较高,建议在低峰期执行并观察慢查询。 diff --git a/htyws_models/migrations/2026-04-23-100005_rename_qumu_category_to_course_category/down.sql b/htyws_models/migrations/2026-04-23-100005_rename_qumu_category_to_course_category/down.sql new file mode 100644 index 0000000..fb6f19c --- /dev/null +++ b/htyws_models/migrations/2026-04-23-100005_rename_qumu_category_to_course_category/down.sql @@ -0,0 +1 @@ +ALTER TABLE course_category RENAME TO qumu_category; diff --git a/htyws_models/migrations/2026-04-23-100005_rename_qumu_category_to_course_category/up.sql b/htyws_models/migrations/2026-04-23-100005_rename_qumu_category_to_course_category/up.sql new file mode 100644 index 0000000..06e16a3 --- /dev/null +++ b/htyws_models/migrations/2026-04-23-100005_rename_qumu_category_to_course_category/up.sql @@ -0,0 +1 @@ +ALTER TABLE qumu_category RENAME TO course_category; diff --git a/htyws_models/src/models.rs b/htyws_models/src/models.rs index e42e9f6..06d4c9d 100644 --- a/htyws_models/src/models.rs +++ b/htyws_models/src/models.rs @@ -61,7 +61,7 @@ pub struct Course { Insertable, Clone, )] -#[diesel(table_name = qumu_category)] +#[diesel(table_name = course_category)] pub struct QumuCategory { pub category_key: String, pub category_name: String, @@ -380,8 +380,8 @@ impl QumuCategory { in_course_category: &QumuCategory, conn: &mut PgConnection, ) -> anyhow::Result { - use crate::schema::qumu_category::dsl::*; - match insert_into(qumu_category) + use crate::schema::course_category::dsl::*; + match insert_into(course_category) .values(in_course_category) .get_result(conn) { @@ -397,8 +397,8 @@ impl QumuCategory { in_course_category: &QumuCategory, conn: &mut PgConnection, ) -> anyhow::Result { - let result = update(qumu_category::table) - .filter(qumu_category::id.eq(in_course_category.clone().id)) + let result = update(course_category::table) + .filter(course_category::id.eq(in_course_category.clone().id)) .set(in_course_category) .get_result(conn) .map_err(|e| { @@ -414,8 +414,8 @@ impl QumuCategory { id_course_category: &String, conn: &mut PgConnection, ) -> anyhow::Result { - match qumu_category::table - .filter(qumu_category::id.eq(id_course_category)) + match course_category::table + .filter(course_category::id.eq(id_course_category)) .first::(conn) { Ok(q) => Ok(q), @@ -427,8 +427,8 @@ impl QumuCategory { } pub fn find_all_active(conn: &mut PgConnection) -> anyhow::Result> { - use crate::schema::qumu_category::dsl::*; - match qumu_category + use crate::schema::course_category::dsl::*; + match course_category .filter(is_delete.eq(false)) .load::(conn) { diff --git a/htyws_models/src/schema.rs b/htyws_models/src/schema.rs index daef932..88e34a0 100644 --- a/htyws_models/src/schema.rs +++ b/htyws_models/src/schema.rs @@ -45,6 +45,19 @@ diesel::table! { } } +diesel::table! { + course_category (id) { + category_key -> Varchar, + category_name -> Varchar, + category_desc -> Nullable, + created_at -> Timestamp, + created_by -> Varchar, + id -> Varchar, + is_delete -> Bool, + is_default -> Nullable, + } +} + diesel::table! { course_group (id) { id -> Varchar, @@ -199,19 +212,6 @@ diesel::table! { } } -diesel::table! { - qumu_category (id) { - category_key -> Varchar, - category_name -> Varchar, - category_desc -> Nullable, - created_at -> Timestamp, - created_by -> Varchar, - id -> Varchar, - is_delete -> Bool, - is_default -> Nullable, - } -} - diesel::table! { ref_resources (id) { id -> Varchar, @@ -313,6 +313,7 @@ diesel::allow_tables_to_appear_in_same_query!( acl, comments, course, + course_category, course_group, course_info, course_section, @@ -323,7 +324,6 @@ diesel::allow_tables_to_appear_in_same_query!( lianxi, piyue, piyue_info, - qumu_category, ref_resources, resource_note, resource_note_group,