Files
railwaysystem/docs/database-v2/12_generator_v2_readonly_validation.md
T

375 lines
19 KiB
Markdown
Raw Normal View History

# 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
<insert id="insert${ClassName}" parameterType="${ClassName}"#if($pkColumn.increment) useGeneratedKeys="true" keyProperty="$pkColumn.javaField"#end>
```
- 若 `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 类问题进行统一微调修复。