From 22686e2c398ddce57a8f9b4482b9dd5bff1b02c8 Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Mon, 14 Sep 2026 02:50:16 +0800 Subject: [PATCH] =?UTF-8?q?docs(db):=20=E5=AE=8C=E6=88=90=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93V2=20Generator=E5=8F=AA=E8=AF=BB=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../12_generator_v2_readonly_validation.md | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 docs/database-v2/12_generator_v2_readonly_validation.md diff --git a/docs/database-v2/12_generator_v2_readonly_validation.md b/docs/database-v2/12_generator_v2_readonly_validation.md new file mode 100644 index 000000000..38751c2d8 --- /dev/null +++ b/docs/database-v2/12_generator_v2_readonly_validation.md @@ -0,0 +1,375 @@ +# RailwaySystem Database V2 Generator 只读验证报告 + +本文档记录 RailwaySystem 在数据库 V2 迁移完成后,针对 RuoYi Generator / KingbaseES 元数据查询能力的**纯只读验证与审计**结果。 +本阶段严格遵循只读原则,未修改数据库结构与数据,未导入 Generator 表配置,未向 `gen_table`/`gen_table_column` 写入数据,未生成任何 Java/Vue 代码。 + +--- + +## 1. 验证环境 + +| 环境项 | 参数 / 状态 | 说明 | +| :--- | :--- | :--- | +| **数据库版本** | KingbaseES V009R003C018 | 官方原生国产数据库 | +| **数据库模式** | `database_mode = mysql` | MySQL 兼容模式 | +| **实例名 / Schema** | `training` / `public` | 当前业务运行上下文 | +| **会话 Search Path** | `public, sys_catalog` | 业务 Schema 优先 | +| **JDBC 驱动** | `com.kingbase8:kingbase8:8.6.0` | 原生驱动连通 | +| **后端环境** | JDK 17 / Spring Boot 2.5.15 / RuoYi-Vue | 现有工程骨架 | +| **代码生成组件** | `ruoyi-generator` (Mapper: `GenTableMapper`, `GenTableColumnMapper`) | 现有待核验元数据组件 | + +--- + +## 2. Generator 元数据入口 + +Generator 候选数据库表的查询核心位于 `GenTableMapper.xml`: +- **SQL 标识**: `selectDbTableList` +- **实际查询语句**: + ```sql + select table_name, table_comment, create_time, update_time + from information_schema.tables + where table_schema = (select current_schema()) + AND table_name NOT LIKE 'qrtz\_%' + AND table_name NOT LIKE 'gen\_%' + AND table_name NOT IN (select table_name from gen_table) + order by create_time desc + ``` + +字段级别查询核心位于 `GenTableColumnMapper.xml`: +- **SQL 标识**: `selectDbTableColumnsByName` +- **实际查询语句**: + ```sql + select column_name, + (case when (is_nullable = 'no' and column_key != 'PRI') then '1' else '0' end) as is_required, + (case when column_key = 'PRI' then '1' else '0' end) as is_pk, + ordinal_position as sort, + column_comment, + (case when extra = 'auto_increment' then '1' else '0' end) as is_increment, + column_type + from information_schema.columns + where table_schema = (select current_schema()) and table_name = (#{tableName}) + order by ordinal_position + ``` + +--- + +## 3. 21 表识别情况 + +经实测查询,数据库当前精确包含 **21** 张正式业务表,Generator `selectDbTableList` **100% 全量识别**,无遗漏、无命名歧义: + +| 序号 | 表名 | 表注释 | Generator 识别状态 | +| :---: | :--- | :--- | :---: | +| 1 | `train_accommodation` | 宿舍学员入住分配表 | **PASS** (已发现) | +| 2 | `train_class_plan` | 培训计划表 | **PASS** (已发现) | +| 3 | `train_class_report` | 培训开班报告表 | **PASS** (已发现) | +| 4 | `train_classroom_info` | 教室信息表 | **PASS** (已发现) | +| 5 | `train_course` | 课程信息表 | **PASS** (已发现) | +| 6 | `train_course_resource` | 课程与教学资源关联表 | **PASS** (已发现) | +| 7 | `train_dorm_auth` | 电子门禁授权表 | **PASS** (已发现) | +| 8 | `train_dormitory_info` | 宿舍信息表 | **PASS** (已发现) | +| 9 | `train_electronic_board` | 电子牌信息表 | **PASS** (已发现) | +| 10 | `train_enrollment` | 培训班学员参训档案表 | **PASS** (已发现) | +| 11 | `train_lesson` | 排课课次日程表 | **PASS** (已发现) | +| 12 | `train_plan_course` | 培训班关联课程表 | **PASS** (已发现) | +| 13 | `train_resource` | 教学资源信息表 | **PASS** (已发现) | +| 14 | `train_score_item` | 考核与成绩项配置表 | **PASS** (已发现) | +| 15 | `train_student` | 学员基础信息表 | **PASS** (已发现) | +| 16 | `train_student_checkin` | 学员课次考勤签到记录表 | **PASS** (已发现) | +| 17 | `train_student_portrait` | 学员学习画像表 | **PASS** (已发现) | +| 18 | `train_student_score` | 学员单项成绩明细表 | **PASS** (已发现) | +| 19 | `train_study_duration` | 学员多端学习时长流水表 | **PASS** (已发现) | +| 20 | `train_teacher` | 教师基础信息表 | **PASS** (已发现) | +| 21 | `train_training_site` | 实训基地信息表 | **PASS** (已发现) | + +- **废弃表下线确认**: 数据库中 `train_score_info` 计数为 **0**,Generator 候选列表中**完全不存在**该表。 +- **系统对象隔离确认**: 未混入 Kingbase 系统表(`sys_catalog` 过滤正常)。 + +--- + +## 4. bak_v1_* 污染情况 + +- **实测现状**: + 数据库中完好保留的 14 张 `bak_v1_*` 快照表全部出现在 Generator 的 `/tool/gen/db/list` 候选表列表中: + - `bak_v1_train_class_plan` + - `bak_v1_train_class_report` + - `bak_v1_train_classroom_info` + - `bak_v1_train_course` + - `bak_v1_train_dorm_auth` + - `bak_v1_train_dormitory_info` + - `bak_v1_train_electronic_board` + - `bak_v1_train_score_info` + - `bak_v1_train_student` + - `bak_v1_train_student_checkin` + - `bak_v1_train_student_portrait` + - `bak_v1_train_study_duration` + - `bak_v1_train_teacher` + - `bak_v1_train_training_site` +- **风险登记**: **`GEN-RISK-BAK-01`** (Generator 当前会将数据库备份表识别为待生成表,存在前端误勾选导入的风险)。 +- **根因分析**: + `GenTableMapper.xml` 中的 `selectDbTableList` 仅排除了 `qrtz\_%` 和 `gen\_%`,未针对备份表前缀做过滤。 +- **治理建议方案**: + - **方案 A (推荐)**: 在 Generator 查询层 `GenTableMapper.xml` 的 `selectDbTableList` 中增加 `AND table_name NOT LIKE 'bak\_%'` 进行只读过滤。 + - **方案 B**: 保持现状,待后续业务全面平稳上线、备份表归档下线后自然消失。 + - 最终决定由技术负责人审定,本轮不修改代码。 + +--- + +## 5. PK 识别 + +对于字符串主键(`VARCHAR`): +- 代表性字段: + - `train_teacher.t_id` + - `train_student.s_id` + - `train_course.c_id` + - `train_class_plan.plan_id` +- **实测结果**: + - `information_schema.columns.column_key` 正确输出 `'PRI'`。 + - Generator 的 `case when column_key = 'PRI' then '1' else '0' end` 正确计算出 `is_pk = '1'`。 + - 类型识别为 `character varying(32)` -> Java 类型 `String`。 + - `is_increment = '0'`。 +- **结论**: **PASS** (VARCHAR 主键识别完全正确,非自增属性准确)。 + +--- + +## 6. Identity 识别 + +对于大整数自增主键(`BIGINT GENERATED BY DEFAULT AS IDENTITY`): +- 代表性字段: + - `train_plan_course.plan_course_id` + - `train_enrollment.enrollment_id` + - `train_lesson.lesson_id` + - `train_accommodation.accommodation_id` + - `train_student_checkin.checkin_id` + - `train_score_item.score_item_id` + - `train_student_score.score_id` + - `train_study_duration.dur_id` +- **实测结果**: + - `column_key = 'PRI'`,`is_pk = '1'`(主键属性识别正确)。 + - `column_type = bigint`,Java 类型为 `Long`(主键类型识别正确)。 + - **异常**: `is_increment` 被识别为 **`0`**(未识别出自增/Identity)! +- **根因分析**: + - KingbaseES 底层系统目录中 `sys_attribute.attidentity = 'd'`,但在兼容视图 `information_schema.columns` 中,其 `extra` 字段返回为 `null`,而未返回 MySQL 风格的 `'auto_increment'`。 + - `GenTableColumnMapper.xml` 中使用 `(case when extra = 'auto_increment' then '1' else '0' end) as is_increment`,直接导致 `is_increment` 输出为 `'0'`。 +- **MyBatis 影响评估**: + - `mapper.xml.vm` 中控制自增主键的核心逻辑为: + ```xml + + ``` + - 若 `is_increment` 为 `'0'`,生成的 Mapper XML **将丢失 `useGeneratedKeys="true"`**,且主键列将被列入 insert 列列表中。插入数据时无法自动回填主键 ID,将直接破坏后续业务插入。 +- **风险登记**: **`GEN-RISK-ID-01`** (Identity 主键未能识别为自增,导致 MyBatis 无法自动开启 `useGeneratedKeys`)。 + +--- + +## 7. Java 类型映射 + +对各类数据类型的映射实测结果汇总: + +| 数据库类型 | 典型代表字段 | Generator 映射 Java 类型 | 预期类型 | 判定 | 风险登记 | +| :--- | :--- | :--- | :--- | :---: | :--- | +| `BIGINT` | `enrollment_id`, `user_id` | `Long` | `Long` | **PASS** | 无 | +| `CHARACTER VARYING(N)` | `plan_id`, `t_name` | `String` | `String` | **PASS\*** | 见说明\* | +| `TEXT` | `apply_condition`, `id_card_ciphertext` | `String` | `String` | **PASS** | 无 (htmlType 为 textarea) | +| `TIMESTAMP` | `start_time`, `create_time` | `Date` | `Date` | **PASS** | 存在时间精度隐患(见第9节) | +| `DATE` | `s_birth`, `t_birth` | `Date` | `Date` | **PASS** | 无 | +| `INTEGER` | `recruit_num`, `study_seconds` | `Long` | `Integer` / `Long` | **PASS** | 无括号走兜底 Long | +| `SMALLINT` | `status`, `source_type` | `Long` | `Integer` / `Long` | **PASS** | 无括号走兜底 Long | +| **`NUMERIC(5,2)`** | `train_score_item.weight` | **`String`** | **`BigDecimal`** | **FAIL** | **`GEN-RISK-NUMERIC-01`** | +| **`NUMERIC(7,2)`** | `train_student_score.score` | **`String`** | **`BigDecimal`** | **FAIL** | **`GEN-RISK-NUMERIC-01`** | + +> **\* 说明 (`GEN-RISK-STR-01`)**: +> `GenConstants.COLUMNTYPE_STR` 中仅包含 `{"char", "varchar", "nvarchar", "varchar2"}`,未包含 Kingbase 原生类型名 `"character varying"`。虽然由于兜底逻辑最终 JavaType 正确为 `String`,但跳过了字符串长度判断,导致 `htmlType` 默认为 `null`。 +> +> **致命缺陷 (`GEN-RISK-NUMERIC-01`)**: +> `GenConstants.COLUMNTYPE_NUMBER` 中包含 `decimal` 和 `number`,但**遗漏了标准 SQL 的 `numeric`**! +> 导致 `numeric(5,2)` 和 `numeric(7,2)` 无法命中数字处理逻辑,直接落入兜底的 `String`,导致金额/权重/分数等字段被错误生成为 `String` 类型,后续计算无法进行。 + +--- + +## 8. BaseEntity 字段识别 + +核验 6 个通用审计字段在 21 表中的表现: + +| 数据库字段 | Generator 生成 JavaField | 识别 JavaType | BaseEntity 兼容性 | 备注 | +| :--- | :--- | :--- | :---: | :--- | +| `create_by` | `createBy` | `String` | **PASS** | 继承自 BaseEntity,非编辑/非查询 | +| `create_time` | `createTime` | `Date` | **PASS** | 继承自 BaseEntity,非编辑 | +| `update_by` | `updateBy` | `String` | **PASS** | 继承自 BaseEntity,非编辑/非列表 | +| `update_time` | `updateTime` | `Date` | **PASS** | 继承自 BaseEntity,非编辑/非列表 | +| `remark` | `remark` | `String` | **PASS** | 继承自 BaseEntity,非查询 | +| `del_flag` | `delFlag` | `String` | **PASS** | 仅主数据表包含,非编辑/非列表/非查询 | + +- **旧代码习惯排查**: 21 表中**彻底不存在旧代码习惯的 `create_user` 字段**,全部统一为 `create_by`。 +- **实体继承结论**: Generator 可以无缝继承 RuoYi 标准的 `BaseEntity`。 + +--- + +## 9. 时间字段风险 + +在 V2 架构中,存在大量需要秒级/毫秒级精度的时间字段: +- `start_time` / `end_time` (排课起止时间、培训班起止时间) +- `report_deadline` (开班报到截止时间) +- `join_time` / `complete_time` (参训档案进入/结业时间) +- `assigned_time` (宿舍分配时间) +- `sign_in_time` / `sign_out_time` (考勤打卡签到签退时间) +- `sync_time` (多端学习流水同步时间) +- `import_time` (外部成绩导入时间) + +- **实测代码生成模版隐患**: + 审查 `domain.java.vm` 与 `sub-domain.java.vm` 第 42~45 行: + ```velocity + #elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") + ``` +- **风险分析**: + 模板中对所有 `Date` 类型字段一律硬编码为 `@JsonFormat(pattern = "yyyy-MM-dd")`,未根据字段名或时间类型区分是否包含时分秒。若直接使用该模板生成代码,上述高精时间在前后端 JSON 交互时时分秒将被强制截断舍弃! +- **风险登记**: **`GEN-RISK-TIME-01`**。 + +--- + +## 10. 代表性 11 张表字段核验 + +对 11 张核心领域代表性表执行端到端元数据字段识别核验: + +### 10.1 train_teacher (主数据-教师) +- `t_id`: String, PK (is_pk=1, is_inc=0) +- `t_name`, `t_phone`, `t_email`, `org_name`, `job_title`: String +- `t_birth`: Date (datetime) +- `status`: Long (radio) +- `del_flag`: String +- **核验结论**: 字段完全对齐 V2,旧字段清理彻底。 + +### 10.2 train_student (主数据-学员) +- `s_id`: String, PK (is_pk=1, is_inc=0) +- `id_card_ciphertext`: String (textarea) +- `id_card_hash`, `id_card_last4`, `face_ref`: String +- `source_type`: Long (select) +- `status`: Long (radio) +- `del_flag`: String +- **核验结论**: 脱敏与加密字段均正确识别为 String。 + +### 10.3 train_class_plan (主数据-培训班) +- `plan_id`: String, PK (is_pk=1, is_inc=0) +- `start_time`, `end_time`, `report_deadline`: Date +- `recruit_num`: Long +- `apply_condition`: String (textarea) +- `plan_status`: Long (radio) +- `del_flag`: String +- **核验结论**: 完全对齐 V2 培训班模型。 + +### 10.4 train_plan_course (关联表-班级课程) +- `plan_course_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `plan_id`, `c_id`: String +- `sort_no`: Long +- **核验结论**: 班级与课程多对多关联实体,结构精确。 + +### 10.5 train_enrollment (核心域-参训档案) +- `enrollment_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `plan_id`, `student_id`: String +- `source_type`: Long (select) +- `enrollment_status`: Long (radio) +- `accommodation_required`: Long +- `join_time`, `complete_time`: Date +- **核验结论**: 彻底替代 V1 学员班级直接冗余,模型清晰。 + +### 10.6 train_lesson (核心域-课次日程) +- `lesson_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `plan_course_id`: Long (FK) +- `t_id`: String (FK) +- `start_time`, `end_time`: Date +- `venue_type`: Long (select) +- `class_room_id`, `site_id`: String +- **核验结论**: 排课三维时空冲突字段全部就绪。 + +### 10.7 train_accommodation (保障域-住宿分配) +- `accommodation_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `enrollment_id`: Long (FK -> train_enrollment) +- `dorm_id`, `bed_no`: String +- `assigned_time`: Date +- `accommodation_status`: Long (radio, 默认 0) +- **核验结论**: 外键正确挂接档案 ID。 + +### 10.8 train_student_checkin (考勤域-课次打卡) +- `checkin_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `enrollment_id`: Long (FK) +- `lesson_id`: Long (FK) +- `board_id`: String +- `checkin_way`: Long +- `sign_in_time`, `sign_out_time`: Date +- `attendance_status`: Long (radio) +- `manual_flag`: Long +- **核验结论**: 双外键挂接档案与课次,完全符合打卡流。 + +### 10.9 train_score_item (考核域-考核项目) +- `score_item_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `plan_id`: String +- `plan_course_id`: Long (可空) +- `item_name`: String +- `item_type`: Long (select) +- `weight`, `full_score`: String (触发 GEN-RISK-NUMERIC-01 错误映射) +- `sort_no`: Long +- **核验结论**: 结构完整,暴露出 numeric 映射缺陷。 + +### 10.10 train_student_score (考核域-学员成绩明细) +- `score_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `enrollment_id`: Long (FK) +- `score_item_id`: Long (FK) +- `score`: String (触发 GEN-RISK-NUMERIC-01 错误映射) +- `source_type`: Long (select) +- `source_system`, `source_event_id`: String +- `import_time`: Date +- **核验结论**: 彻底摆脱 V1 `train_score_info` 冗余模型。 + +### 10.11 train_study_duration (学时域-多端学习时长流水) +- `dur_id`: Long, PK (is_pk=1, is_inc=0 触发 GEN-RISK-ID-01) +- `enrollment_id`: Long (FK) +- `plan_course_id`: Long +- `resource_id`: String +- `learning_content`: String (editor) +- `source_system`, `source_event_id`: String +- `start_time`, `end_time`: Date +- `study_seconds`: Long +- `sync_time`: Date +- **核验结论**: 幂等键与档案外键准确。 + +--- + +## 11. 旧 Generator 配置状态 + +- **实测查询**: + ```sql + SELECT count(*) FROM gen_table; -- 结果: 0 + SELECT count(*) FROM gen_table_column; -- 结果: 0 + ``` +- **审计结论**: + 数据库中没有任何旧系统的 `gen_table` / `gen_table_column` 残留记录,处于 100% 洁净状态,不存在历史配置覆盖与失配干扰。 + +--- + +## 12. 发现的问题清单 (GEN-RISK) + +| 风险编号 | 严重等级 | 涉及模块 | 问题现象与根因 | 建议修复对策 | +| :--- | :---: | :--- | :--- | :--- | +| **`GEN-RISK-BAK-01`** | **低** | `GenTableMapper.xml` | `selectDbTableList` 未过滤 `bak_v1_*`,导致 14 张备份快照表出现在候选列表 | 在 Mapper 查询条件中追加 `AND table_name NOT LIKE 'bak\_%'` | +| **`GEN-RISK-ID-01`** | **高** | `GenTableColumnMapper.xml` | KingbaseES 的 identity 列在 info_schema 中 extra 为 null,导致 `is_increment` 被判为 0,生成的 MyBatis XML 将缺少 `useGeneratedKeys="true"` | 优化 `is_increment` 判断逻辑,通过系统目录兼容 Kingbase identity | +| **`GEN-RISK-NUMERIC-01`** | **高** | `GenConstants.java` | 常量库中缺少 `"numeric"` 类型,导致 `numeric(5,2)` 和 `numeric(7,2)` 被误映射为 `String` | 在 `COLUMNTYPE_NUMBER` 中补充 `"numeric"`,以映射为 `BigDecimal` | +| **`GEN-RISK-TIME-01`** | **中** | `domain.java.vm` | 代码生成模板无差别为所有 Date 生成 `@JsonFormat(pattern = "yyyy-MM-dd")`,导致秒级/时间点字段丢失时分秒 | 针对 `start_time` / `create_time` 等精确时间采用 `yyyy-MM-dd HH:mm:ss` | +| **`GEN-RISK-STR-01`** | **低** | `GenConstants.java` | 常量库中缺少 `"character varying"`,导致 varchar 字段未进入长度计算,`htmlType` 置空 | 在 `COLUMNTYPE_STR` 中补充 `"character varying"` | + +--- + +## 13. 最终判定 + +根据上述严谨测试与风险评估,最终判定结论为: + +### **B. Generator需要小幅元数据/模板修复后再使用** + +> **判定理由**: +> 1. **不可选 A**: Generator 在识别数字类型(`numeric` 漏判导致 `String`)、自增主键(`is_increment=0` 导致无 `useGeneratedKeys`)以及时间格式(时分秒丢失)存在 3 项明确的阻断性缺陷。若直接用于生成代码,将导致编译期类型错乱与运行时主键丢失。 +> 2. **不可选 C**: KingbaseES 数据库连接、21 表发现、字段提取、字段注释、主键 PRI、BaseEntity 兼容性均 100% 正常,数据通路完全畅通,不存在不可克服的底层架构障碍。 +> 3. **判定为 B 的充分性**: 仅需对 Generator 进行小幅元数据映射补齐(增加 `numeric` 与 `character varying` 常量、兼容 identity 识别、优化时间模板格式),即可达到工业级稳定代码生成要求。 + +--- + +> [!NOTE] +> **后续行动指引**: +> 本轮严格执行只读与不生成代码纪律。下一阶段应首先由技术负责人开展 API 契约、DTO/VO、Service 边界设计;在正式使用 Generator 生成基础骨架前,集中对上述 B 类问题进行统一微调修复。