chore(sync): 同步当前工作区全部变更(学员学号可编辑并留空自动生成等)
This commit is contained in:
1 parent
01e949093a
commit
ee629f2f3e
534 files changed
+72978
-2720
No files matched your search
@@ -44,7 +44,7 @@ ALTER TABLE train_electronic_board DROP CONSTRAINT IF EXISTS fk_board_plan;
|
||||
-- ==============================================================================
|
||||
ALTER TABLE train_teacher
|
||||
ADD COLUMN user_id BIGINT,
|
||||
ADD COLUMN org_name VARCHAR(100),
|
||||
ADD COLUMN dept_name VARCHAR(100),
|
||||
ADD COLUMN job_title VARCHAR(50),
|
||||
ADD COLUMN status SMALLINT DEFAULT 1,
|
||||
ADD COLUMN create_by VARCHAR(64),
|
||||
@@ -58,7 +58,7 @@ ALTER TABLE train_teacher
|
||||
ALTER COLUMN remark TYPE VARCHAR(500);
|
||||
|
||||
COMMENT ON COLUMN train_teacher.user_id IS 'sys_user登录账号逻辑关联ID';
|
||||
COMMENT ON COLUMN train_teacher.org_name IS '所属单位/部门名称 (由旧 t_department 迁移)';
|
||||
COMMENT ON COLUMN train_teacher.dept_name IS '所属单位/部门名称 (由旧 t_department 迁移)';
|
||||
COMMENT ON COLUMN train_teacher.job_title IS '职务/职称 (由旧 t_title 迁移)';
|
||||
COMMENT ON COLUMN train_teacher.status IS '状态 (1:正常, 0:停用)';
|
||||
COMMENT ON COLUMN train_teacher.del_flag IS '删除标志 (0:存在, 2:删除)';
|
||||
@@ -76,7 +76,8 @@ ALTER TABLE train_student
|
||||
-- 4.2 增加企业培训新主档字段、加密字段与公共审计字段
|
||||
ALTER TABLE train_student
|
||||
ADD COLUMN user_id BIGINT,
|
||||
ADD COLUMN org_name VARCHAR(100),
|
||||
ADD COLUMN dept_name VARCHAR(100),
|
||||
ADD COLUMN card_no VARCHAR(50),
|
||||
ADD COLUMN s_email VARCHAR(100),
|
||||
ADD COLUMN id_card_ciphertext TEXT,
|
||||
ADD COLUMN id_card_hash VARCHAR(128),
|
||||
@@ -94,7 +95,7 @@ ALTER TABLE train_student
|
||||
ALTER COLUMN remark TYPE VARCHAR(500);
|
||||
|
||||
COMMENT ON COLUMN train_student.user_id IS 'sys_user账号逻辑关联';
|
||||
COMMENT ON COLUMN train_student.org_name IS '所属单位/部门';
|
||||
COMMENT ON COLUMN train_student.dept_name IS '所属单位/部门';
|
||||
COMMENT ON COLUMN train_student.id_card_ciphertext IS '身份证可逆加密密文';
|
||||
COMMENT ON COLUMN train_student.id_card_hash IS '身份证精确匹配Hash (防重)';
|
||||
COMMENT ON COLUMN train_student.id_card_last4 IS '身份证后四位展示';
|
||||
|
||||
@@ -19,7 +19,7 @@ CREATE INDEX idx_teacher_name ON train_teacher (t_name);
|
||||
|
||||
-- 2. train_student 学员表索引
|
||||
CREATE INDEX idx_student_name ON train_student (s_name);
|
||||
CREATE INDEX idx_student_org ON train_student (org_name);
|
||||
CREATE INDEX idx_student_org ON train_student (dept_name);
|
||||
CREATE INDEX idx_student_user_id ON train_student (user_id);
|
||||
|
||||
-- 3. train_course 课程表索引
|
||||
|
||||
@@ -332,4 +332,29 @@ INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_cl
|
||||
SELECT 4, 'Excel批量导入', '4', 'train_score_source', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '线下成绩表格批量导入'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_score_source' AND dict_value = '4');
|
||||
|
||||
-- 17. train_class_major
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT '培训专业', 'train_class_major', '0', 'admin', CURRENT_TIMESTAMP, '培训班所属专业分类 (对应 train_class_plan.class_major)'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_class_major');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, '接触网工', '1', 'train_class_major', 'primary', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '接触网检修专业'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_class_major' AND dict_value = '1');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, '电力线路工', '2', 'train_class_major', 'primary', 'N', '0', 'admin', CURRENT_TIMESTAMP, '电力线路检修专业'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_class_major' AND dict_value = '2');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 3, '信号工', '3', 'train_class_major', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '信号设备维护专业'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_class_major' AND dict_value = '3');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 4, '通信工', '4', 'train_class_major', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '通信设备维护专业'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_class_major' AND dict_value = '4');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 5, '机车司机', '5', 'train_class_major', 'info', 'N', '0', 'admin', CURRENT_TIMESTAMP, '机车驾驶专业'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_class_major' AND dict_value = '5');
|
||||
|
||||
COMMIT;
|
||||
@@ -6,7 +6,7 @@
|
||||
-- 设计依据: RailwaySystem 数据库 V2 字段设计与 Kingbase DDL 规范 V1.0 及 Round 1 审查要求
|
||||
-- 安全规范:
|
||||
-- 1. 严禁在脚本中出现真实教师姓名、电话、身份证、邮箱等明文数据!
|
||||
-- 2. 完全采用纯 DML 数据映射: t_department -> org_name, t_title -> job_title, create_user -> create_by
|
||||
-- 2. 完全采用纯 DML 数据映射: t_department -> dept_name, t_title -> job_title, create_user -> create_by
|
||||
-- 3. 执行后验证迁移完整性,再将 status 与 del_flag 设为 NOT NULL 并安全移除旧列
|
||||
-- ==============================================================================
|
||||
|
||||
@@ -15,7 +15,7 @@ BEGIN;
|
||||
-- 步骤 1: 数据平滑映射 (仅更新新字段为空的行,保证幂等)
|
||||
UPDATE train_teacher
|
||||
SET
|
||||
org_name = COALESCE(org_name, t_department),
|
||||
dept_name = COALESCE(dept_name, t_department),
|
||||
job_title = COALESCE(job_title, t_title),
|
||||
create_by = COALESCE(create_by, create_user),
|
||||
status = COALESCE(status, 1),
|
||||
@@ -31,7 +31,7 @@ BEGIN
|
||||
SELECT count(*) INTO v_total_cnt FROM train_teacher;
|
||||
SELECT count(*) INTO v_unmigrated_cnt
|
||||
FROM train_teacher
|
||||
WHERE org_name IS NULL OR status IS NULL OR del_flag IS NULL;
|
||||
WHERE dept_name IS NULL OR status IS NULL OR del_flag IS NULL;
|
||||
|
||||
IF v_total_cnt <> 1 THEN
|
||||
RAISE EXCEPTION '>>> [008-FATAL] 教师数据校验异常: 期望精确 1 条数据,实际检测到 % 条', v_total_cnt;
|
||||
|
||||
@@ -90,7 +90,7 @@ BEGIN
|
||||
|
||||
SELECT count(*) INTO v_cnt
|
||||
FROM train_teacher
|
||||
WHERE org_name IS NULL OR status IS NULL OR del_flag IS NULL;
|
||||
WHERE dept_name IS NULL OR status IS NULL OR del_flag IS NULL;
|
||||
IF v_cnt > 0 THEN
|
||||
RAISE EXCEPTION '>>> [Verify-FATAL] train_teacher 记录中新主档字段存在非法 NULL 值';
|
||||
END IF;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
-- ============================================================
|
||||
-- 阅读机/听书机 7 张业务表 - 测试数据
|
||||
-- 来源:阅读机数据表+接口设计-v1.md
|
||||
-- ============================================================
|
||||
-- 前置说明:
|
||||
-- 1) 本文件仅包含 INSERT 测试数据;建表 DDL 请按设计文档执行
|
||||
-- (其中 train_book_download_record 的 DDL 见 patch/014_train_book_download_record.sql,其余 6 张表按文档“一、数据库表设计”创建)。
|
||||
-- 2) 逻辑外键依赖:train_user_session / 各阅读进度 / 上报日志 / 下载回执 中的 user_id=10001
|
||||
-- 需先在 sys_user 中存在该学员账号(RuoYi 内置用户体系,本文件不含 sys_user 数据)。
|
||||
-- 3) 设备 device_id、图书 book_id/book_version、会话 session_id、上报 record_id 均保持自洽,
|
||||
-- 可直接用于验证“登录 / 阅读上报 / 进度冲突 / 下架过滤 / 下载回执”等接口。
|
||||
-- 4) device_secret_digest 为 SHA256 十六进制占位值(64 位),真实环境请替换为对应设备密钥的 SHA256 摘要。
|
||||
-- ============================================================
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 1) train_iot_device 读书机设备档案表
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO `train_iot_device`
|
||||
(`device_id`, `device_secret_digest`, `device_status`, `firmware_version`, `app_version`, `bind_org_id`, `bind_class_id`, `heartbeat_interval`, `last_login_time`, `last_heartbeat_time`)
|
||||
VALUES
|
||||
('READER-LIB-01', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', 1, '1.4.2', '2.1.0', 'ORG-001', 'CLASS-01,CLASS-02', 60, '2026-10-20 09:00:00', '2026-10-20 09:30:00'),
|
||||
('READER-LIB-02', '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', 1, '1.4.0', '2.0.5', 'ORG-001', 'CLASS-03', 60, '2026-10-19 08:00:00', '2026-10-20 08:50:00'),
|
||||
('READER-LIB-03', '688787d8ff144c502c7f5cffaafe2cc588d86079f9de88304c26b0cb99ce91c6', 2, '1.3.1', '1.9.0', 'ORG-002', 'CLASS-05', 120, NULL, '2026-10-15 10:00:00');
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 2) train_book_snapshot 图书快照表
|
||||
-- is_on_shelf: 1 上架 / 0 下架(下架仅禁止新下载,仍可上报阅读)
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO `train_book_snapshot`
|
||||
(`book_id`, `book_title`, `book_category`, `book_summary`, `book_version`, `file_size`, `file_md5`, `download_url`, `is_on_shelf`, `publish_time`)
|
||||
VALUES
|
||||
('BK-00012', '铁路房建施工安全手册', 'SAFETY', '房建施工安全作业规范与事故案例', 'BV-3', 2048000, 'c4ca4238a0b923820dcc509a6f75849b', '/books/BK-00012/BV-3.pdf', 1, '2025-10-18 00:00:00'),
|
||||
('BK-00012', '铁路房建施工安全手册', 'SAFETY', '房建施工安全作业规范与事故案例(旧版)', 'BV-2', 1980000, 'b6d8a532a0f1c2e3d4f5a6b7c8d9e0f10', '/books/BK-00012/BV-2.pdf', 0, '2025-06-01 00:00:00'),
|
||||
('BK-00013', '铁路电力安全工作规程', 'REGULATION', '电气化铁路安全工作规程要点', 'BV-2', 3072000, 'a1b2c3d4e5f60718293a4b5c6d7e8f90a', '/books/BK-00013/BV-2.pdf', 1, '2025-09-10 00:00:00'),
|
||||
('BK-00014', '高铁客运服务英语', 'LANGUAGE', '高铁乘务日常英语会话', 'BV-1', 1536000, 'f0e1d2c3b4a5968778695a4b3c2d1e0f1', '/books/BK-00014/BV-1.pdf', 1, '2025-08-20 00:00:00'),
|
||||
('BK-00015', '铁路线路维修规则', 'REGULATION', '普速铁路线路维修规则', 'BV-1', 4096000, '112233445566778899aabbccddeeff00', '/books/BK-00015/BV-1.pdf', 0, '2025-05-15 00:00:00');
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 3) train_user_session 学员业务会话表
|
||||
-- session_status: 1 ACTIVE正常在线 / 2 REPLACED被替换仅补交 / 3 LOGOUT已退出
|
||||
-- 下面第 2 条模拟“被新登录顶替”的旧会话,用于验证冲突进度(CONFLICT_SAVED)
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO `train_user_session`
|
||||
(`user_id`, `session_id`, `device_id`, `session_status`, `login_time`, `replace_time`, `logout_time`, `ruoyi_token_key`)
|
||||
VALUES
|
||||
(10001, 'SES-20261020-0001', 'READER-LIB-01', 1, '2026-10-20 09:00:00', NULL, NULL, 'login_tokens:token-active-001'),
|
||||
(10001, 'SES-20261019-0001', 'READER-LIB-02', 2, '2026-10-19 08:00:00', '2026-10-20 09:00:05', NULL, 'login_tokens:token-replaced-001');
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 4) train_reading_current_progress 学员主续读进度
|
||||
-- (user_id, book_id, mode) 唯一;mode: read 阅读 / audio 听书
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO `train_reading_current_progress`
|
||||
(`user_id`, `book_id`, `mode`, `book_version`, `page_no`, `sentence_index`, `progress`, `source_record_id`, `source_session_id`)
|
||||
VALUES
|
||||
(10001, 'BK-00012', 'read', 'BV-3', 46, NULL, 0.35, 'RD-20261020-0001', 'SES-20261020-0001'),
|
||||
(10001, 'BK-00012', 'audio', 'BV-3', 120, 88, 0.50, 'RD-20261020-0002', 'SES-20261020-0001'),
|
||||
(10001, 'BK-00013', 'read', 'BV-2', 30, NULL, 0.20, 'RD-20261020-0003', 'SES-20261020-0001');
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 5) train_reading_alternate_progress 备选冲突续读进度
|
||||
-- 来自被 REPLACED 的旧设备会话提交,不覆盖主进度,供学员手动选择跳转
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO `train_reading_alternate_progress`
|
||||
(`user_id`, `book_id`, `mode`, `book_version`, `page_no`, `sentence_index`, `progress`, `source_record_id`, `source_session_id`)
|
||||
VALUES
|
||||
(10001, 'BK-00012', 'read', 'BV-2', 22, NULL, 0.18, 'RD-20261019-0001', 'SES-20261019-0001');
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 6) train_reading_duration_log 阅读上报幂等日志
|
||||
-- result_code: ACCEPTED / DUPLICATE / CONFLICT_SAVED / REJECTED
|
||||
-- (user_id, record_id) 唯一,record_id 重复将返回 DUPLICATE
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO `train_reading_duration_log`
|
||||
(`device_id`, `user_id`, `session_id`, `record_id`, `book_id`, `book_version`, `mode`, `record_type`, `duration_sec`, `page_from`, `page_to`, `progress`, `start_time`, `end_time`, `receive_time`, `result_code`, `remark`)
|
||||
VALUES
|
||||
('READER-LIB-01', 10001, 'SES-20261020-0001', 'RD-20261020-0001', 'BK-00012', 'BV-3', 'read', 'END', 1420, 12, 46, 0.35, '2026-10-20 08:30:00', '2026-10-20 09:00:00', '2026-10-20 09:00:01', 'ACCEPTED', NULL),
|
||||
('READER-LIB-01', 10001, 'SES-20261020-0001', 'RD-20261020-0002', 'BK-00012', 'BV-3', 'audio', 'PROGRESS', 600, 100, 120, 0.50, '2026-10-20 09:00:00', '2026-10-20 09:10:00', '2026-10-20 09:10:01', 'ACCEPTED', NULL),
|
||||
('READER-LIB-02', 10001, 'SES-20261019-0001', 'RD-20261019-0001', 'BK-00012', 'BV-2', 'read', 'END', 980, 5, 22, 0.18, '2026-10-19 19:45:00', '2026-10-19 20:00:00', '2026-10-19 20:00:01', 'CONFLICT_SAVED', 'REPLACED会话补交,已存备选进度');
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- 7) train_book_download_record 图书下载回执记录表
|
||||
-- result: SUCCESS / FAIL
|
||||
-- ----------------------------------------------------------
|
||||
INSERT INTO `train_book_download_record`
|
||||
(`user_id`, `device_id`, `book_id`, `book_version`, `file_md5`, `result`, `err_msg`, `download_time`)
|
||||
VALUES
|
||||
(10001, 'READER-LIB-01', 'BK-00012', 'BV-3', 'c4ca4238a0b923820dcc509a6f75849b', 'SUCCESS', NULL, '2026-10-20 08:00:00'),
|
||||
(10001, 'READER-LIB-01', 'BK-00013', 'BV-2', 'a1b2c3d4e5f60718293a4b5c6d7e8f90a', 'SUCCESS', NULL, '2026-10-20 08:05:00'),
|
||||
(10001, 'READER-LIB-03', 'BK-00014', 'BV-1', 'f0e1d2c3b4a5968778695a4b3c2d1e0f1', 'FAIL', '存储空间不足', '2026-10-15 09:00:00');
|
||||
@@ -0,0 +1,289 @@
|
||||
# 阅读机/听书机 接口测试数据手册
|
||||
|
||||
> 配套文件:`阅读机数据表+接口设计-v1.md`、`015_train_test_data.sql`
|
||||
> 本文档中的「请求示例」全部使用 `015_train_test_data.sql` 已插入的真实数据,可直接照抄测试。
|
||||
>
|
||||
> 基础地址:`http://localhost:8096/api/iot/v1`(端口按你实际改)
|
||||
> 数据前提:已执行 `015_train_test_data.sql`,且 `sys_user` 中存在 `userId=10001`、`status='0'`(正常)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 鉴权与测试顺序
|
||||
|
||||
| 接口 | 鉴权 |
|
||||
|---|---|
|
||||
| `POST /device/login` | 免鉴权(permitAll) |
|
||||
| `GET /book/list` | 免鉴权(已放开) |
|
||||
| `GET /book/download` | 免鉴权(已放开) |
|
||||
| 其余 6 个接口 | 需 `Authorization: Bearer <accessToken>`(来自登录返回) |
|
||||
|
||||
> **登录接口有强制设备签名(`DeviceHmacFilter`)**:必须带 `X-Signature` + `X-Nonce` 请求头,且 body 的 `timestamp` 距今 ≤5 分钟,否则报「缺少设备签名头 / 签名校验失败 / 时间戳超时」。
|
||||
> 本地联调不想算签名时,把 `application.yml` 的 `iot.hmac.enabled` 改为 `false`(重启后端)即可跳过验签;生产环境务必保持 `true`。
|
||||
> 其余接口(Bearer 鉴权)不受该 Filter 影响。
|
||||
|
||||
**推荐顺序**:登录 → 复制 `data.accessToken` → 在 Swagger 点 Authorize 填 `Bearer <accessToken>`(或 curl 带 `-H 'Authorization: Bearer <accessToken>'`)→ 测其余接口。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设备学员登录 `POST /api/iot/v1/device/login`
|
||||
|
||||
请求(取自 015 设备 `READER-LIB-01` + 学员 `10001`):
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:8096/api/iot/v1/device/login' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"deviceId":"READER-LIB-01",
|
||||
"userId":10001,
|
||||
"firmware":"1.4.2",
|
||||
"appVersion":"2.1.0",
|
||||
"timestamp":1767620000000,
|
||||
"nonce":"a1b2c3d4e5f6"
|
||||
}'
|
||||
```
|
||||
|
||||
预期响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"accessToken": "<ruoyi生成的token>",
|
||||
"sessionId": "<本次新建的业务会话ID>",
|
||||
"heartbeatInterval": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 设备注销 `POST /api/iot/v1/device/logout`(需 token)
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:8096/api/iot/v1/device/logout' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <accessToken>' \
|
||||
-d '{"deviceId":"READER-LIB-01","reason":"REBOOT","timestamp":1767621000000,"nonce":"f6e5d4c3b2a1"}'
|
||||
```
|
||||
|
||||
预期:`{"data":{"result":true,"tokenRevoked":true}}`
|
||||
|
||||
---
|
||||
|
||||
## 3. 设备心跳 `POST /api/iot/v1/device/heartbeat`(需 token)
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:8096/api/iot/v1/device/heartbeat' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <accessToken>' \
|
||||
-d '{"deviceId":"READER-LIB-01","status":"ONLINE","metrics":{"cpu":20,"mem":30,"diskFree":10240},"configVersion":"CV-17","personVersion":"PV-226118-7","timestamp":1767622000000,"nonce":"aa11bb22cc33"}'
|
||||
```
|
||||
|
||||
预期:`{"data":{"nextInterval":60,"commands":[]}}`
|
||||
|
||||
---
|
||||
|
||||
## 4. 阅读记录单条上报 `POST /api/iot/v1/reading/record`(需 token)
|
||||
|
||||
**新 recordId(正常 ACCEPTED)** —— 用上架书 `BK-00012/BV-3`:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:8096/api/iot/v1/reading/record' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <accessToken>' \
|
||||
-d '{
|
||||
"deviceId":"READER-LIB-01",
|
||||
"recordId":"RD-TEST-0001",
|
||||
"bookId":"BK-00012",
|
||||
"bookVersion":"BV-3",
|
||||
"mode":"read",
|
||||
"recordType":"END",
|
||||
"startTime":1767620000000,
|
||||
"endTime":1767622400000,
|
||||
"durationSec":1420,
|
||||
"pageFrom":12,
|
||||
"pageTo":46,
|
||||
"progress":0.35
|
||||
}'
|
||||
```
|
||||
|
||||
预期:`{"data":{"result":"ACCEPTED","studyMinutes":23,"totalMinutes":<累计/60>}}`
|
||||
|
||||
> 重复发送同一个 `recordId=RD-TEST-0001` → 返回 `{"result":"DUPLICATE",...}`(幂等)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 阅读记录批量上报 `POST /api/iot/v1/reading/record/batch`(需 token)
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:8096/api/iot/v1/reading/record/batch' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <accessToken>' \
|
||||
-d '{
|
||||
"deviceId":"READER-LIB-01",
|
||||
"batchId":"BATCH-RD-001",
|
||||
"timestamp":1767622500000,
|
||||
"nonce":"b2b3c4d5e6f7",
|
||||
"records":[
|
||||
{"recordId":"RD-TEST-0002","bookId":"BK-00013","bookVersion":"BV-2","mode":"read","recordType":"END","startTime":1767620000000,"endTime":1767622400000,"durationSec":980,"pageFrom":5,"pageTo":22,"progress":0.18}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
预期:`{"data":{"itemResults":[{"recordId":"RD-TEST-0002","result":"ACCEPTED"}]}}`
|
||||
|
||||
---
|
||||
|
||||
## 6. 获取学员阅读汇总与续读进度 `GET /api/iot/v1/reading/progress`(需 token)
|
||||
|
||||
```bash
|
||||
curl 'http://localhost:8096/api/iot/v1/reading/progress?deviceId=READER-LIB-01' \
|
||||
-H 'Authorization: Bearer <accessToken>'
|
||||
```
|
||||
|
||||
预期(基于 015 已插入的进度/上报数据;需存在一个 `ACTIVE` 会话,登录后即有):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"userId": 10001,
|
||||
"totalReadSeconds": 2400,
|
||||
"totalAudioSeconds": 600,
|
||||
"bookRecords": [
|
||||
{
|
||||
"bookId": "BK-00012", "bookVersion": "BV-3",
|
||||
"mainProgress": {"mode":"read","pageNo":46,"sentenceIndex":null,"progress":0.35,"updateTime":1767622400000},
|
||||
"alternateProgressList": [
|
||||
{"mode":"read","bookVersion":"BV-2","pageNo":22,"sentenceIndex":null,"progress":0.18,"createTime":1767610000000}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bookId": "BK-00012", "bookVersion": "BV-3",
|
||||
"mainProgress": {"mode":"audio","pageNo":120,"sentenceIndex":88,"progress":0.50,"updateTime":1767622500000},
|
||||
"alternateProgressList": []
|
||||
},
|
||||
{
|
||||
"bookId": "BK-00013", "bookVersion": "BV-2",
|
||||
"mainProgress": {"mode":"read","pageNo":30,"sentenceIndex":null,"progress":0.20,"updateTime":1767622500000},
|
||||
"alternateProgressList": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `totalReadSeconds`=1420+980(两条 read 上报累加);`totalAudioSeconds`=600(audio 上报)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 获取可阅读书籍列表 `GET /api/iot/v1/book/list`(免鉴权)
|
||||
|
||||
```bash
|
||||
# 全部上架书
|
||||
curl 'http://localhost:8096/api/iot/v1/book/list?deviceId=READER-LIB-01'
|
||||
# 按分类过滤(只返回上架)
|
||||
curl 'http://localhost:8096/api/iot/v1/book/list?deviceId=READER-LIB-01&bookCategory=SAFETY'
|
||||
```
|
||||
|
||||
预期(`bookCategory` 省略时 `total=3`,只返回 `is_on_shelf=1` 的书;下架的 `BK-00012/BV-2`、`BK-00015` 不返回):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"total": 3,
|
||||
"list": [
|
||||
{"bookId":"BK-00012","bookTitle":"铁路房建施工安全手册","bookCategory":"SAFETY","bookVersion":"BV-3","fileSize":2048000,"fileMd5":"c4ca4238a0b923820dcc509a6f75849b","isOnShelf":true},
|
||||
{"bookId":"BK-00013","bookTitle":"铁路电力安全工作规程","bookCategory":"REGULATION","bookVersion":"BV-2","fileSize":3072000,"fileMd5":"a1b2c3d4e5f60718293a4b5c6d7e8f90a","isOnShelf":true},
|
||||
{"bookId":"BK-00014","bookTitle":"高铁客运服务英语","bookCategory":"LANGUAGE","bookVersion":"BV-1","fileSize":1536000,"fileMd5":"f0e1d2c3b4a5968778695a4b3c2d1e0f1","isOnShelf":true}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `bookCategory=SAFETY` 时 `total=1`(仅 `BK-00012/BV-3`)。
|
||||
|
||||
---
|
||||
|
||||
## 8. PDF 书籍下载 `GET /api/iot/v1/book/download`(免鉴权)
|
||||
|
||||
```bash
|
||||
# 上架书(正常)
|
||||
curl -i 'http://localhost:8096/api/iot/v1/book/download?deviceId=READER-LIB-01&bookId=BK-00012&bookVersion=BV-3'
|
||||
# 支持断点续传
|
||||
curl -i -H 'Range: bytes=0-1023' 'http://localhost:8096/api/iot/v1/book/download?deviceId=READER-LIB-01&bookId=BK-00012&bookVersion=BV-3'
|
||||
```
|
||||
|
||||
- 上架书:HTTP 200(或 Range 时 206),响应头带 `Accept-Ranges: bytes`、`Content-MD5`。
|
||||
- 下架书:`bookId=BK-00012&bookVersion=BV-2`(is_on_shelf=0)→ 业务报错 `{"code":500,"msg":"图书已下架,禁止下载"}`。
|
||||
- 不存在的版本:`bookId=BK-00015&bookVersion=BV-9` → `图书或版本不存在`。
|
||||
|
||||
> ⚠️ `download_url` 支持两种形态:
|
||||
> ① **相对路径**(如正式数据的 `/profile/upload/2026/10/21/铁路房建施工安全手册_20261021085854A001.pdf`,或测试用的 `/books/BK-00012/BV-3.pdf`)——后端自动在前面补上「后端 base URL(`scheme+host+port`,如 `http://localhost:8096`)」转成可访问的 http(s) 绝对地址后再流式返回;
|
||||
> ② **完整 http(s) 远程地址**(如 OSS)——直接拉取。
|
||||
> 两种都会自动加 `Content-Type: application/pdf` 与 `Content-Disposition`(下载头),保证浏览器可访问。
|
||||
> 相对路径由后端自身静态资源处理器提供文件:`/profile/**` 指向 RuoYi 上传根目录(文件须真实存在于该磁盘目录),`/books/**` 指向 `classpath:/static/books/`(测试 PDF 已放此处)。文件不存在则返回「文件不存在」。
|
||||
|
||||
---
|
||||
|
||||
## 9. 下载完成回执上报 `POST /api/iot/v1/book/download/ack`(需 token)
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:8096/api/iot/v1/book/download/ack' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <accessToken>' \
|
||||
-d '{
|
||||
"deviceId":"READER-LIB-01",
|
||||
"bookId":"BK-00012",
|
||||
"bookVersion":"BV-3",
|
||||
"fileMd5":"c4ca4238a0b923820dcc509a6f75849b",
|
||||
"result":"SUCCESS",
|
||||
"errMsg":"",
|
||||
"timestamp":1767630000000,
|
||||
"nonce":"cc55dd66ee77"
|
||||
}'
|
||||
```
|
||||
|
||||
预期:`{"data":{"deviceReady":true,"recordId":<自增主键>}}`
|
||||
|
||||
> `result` 可取 `SUCCESS`/`FAIL`(缺省 SUCCESS);落库 `train_book_download_record`。
|
||||
|
||||
---
|
||||
|
||||
## 10. 边界 / 异常用例矩阵(均基于 015 数据)
|
||||
|
||||
| 用例 | 操作 | 预期 |
|
||||
|---|---|---|
|
||||
| 禁用设备登录 | `device/login` 用 `deviceId=READER-LIB-03`(status=2) | `msg:"设备已禁用"` |
|
||||
| 重复上报幂等 | `reading/record` 用已存在的 `recordId=RD-20261020-0001` | `result:"DUPLICATE"` |
|
||||
| 冲突进度(REPLACED 仅补交) | 用 `deviceId=READER-LIB-02`(其会话 `SES-20261019-0001` 是 REPLACED)发 `reading/record` 新 recordId | `result:"CONFLICT_SAVED"`,写入 `train_reading_alternate_progress` |
|
||||
| 下架书被过滤 | `book/list` 不含 `BK-00012/BV-2`、`BK-00015` | `total=3`(仅上架) |
|
||||
| 下架书禁止下载 | `book/download?bookId=BK-00012&bookVersion=BV-2` | 业务错误「图书已下架,禁止下载」 |
|
||||
| REPLACED 禁止进度查询 | `reading/progress?deviceId=READER-LIB-02`(需先有一个有效 token) | `msg:"当前会话状态不允许查询进度(被替换设备仅允许补交上报)"` |
|
||||
| 版本强绑定 | 进度查询中 `BK-00012` 的 `read` 主进度 `BV-3` 与备选 `BV-2` 不混用页码 | 各自返回对应版本页码 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 已插入测试数据速查(源自 015)
|
||||
|
||||
**设备 `train_iot_device`**
|
||||
- `READER-LIB-01` status=1(正常)、`READER-LIB-02` status=1、`READER-LIB-03` status=2(禁用)
|
||||
|
||||
**图书 `train_book_snapshot`**(is_on_shelf)
|
||||
- `BK-00012/BV-3` SAFETY 上架 md5=`c4ca4238a0b923820dcc509a6f75849b`
|
||||
- `BK-00012/BV-2` SAFETY **下架**
|
||||
- `BK-00013/BV-2` REGULATION 上架 md5=`a1b2c3d4e5f60718293a4b5c6d7e8f90a`
|
||||
- `BK-00014/BV-1` LANGUAGE 上架 md5=`f0e1d2c3b4a5968778695a4b3c2d1e0f1`
|
||||
- `BK-00015/BV-1` REGULATION **下架**
|
||||
|
||||
**会话 `train_user_session`**
|
||||
- `SES-20261020-0001` user=10001 device=READER-LIB-01 status=1 ACTIVE
|
||||
- `SES-20261019-0001` user=10001 device=READER-LIB-02 status=2 REPLACED(用于冲突用例)
|
||||
|
||||
**主进度 `train_reading_current_progress`**:10001 的 `BK-00012/read/BV-3`(46,0.35)、`BK-00012/audio/BV-3`(120,88,0.50)、`BK-00013/read/BV-2`(30,0.20)
|
||||
|
||||
**备选进度 `train_reading_alternate_progress`**:10001 `BK-00012/read/BV-2`(22,0.18)
|
||||
|
||||
**上报日志 `train_reading_duration_log`**:`RD-20261020-0001`(ACCEPTED)、`RD-20261020-0002`(ACCEPTED)、`RD-20261019-0001`(CONFLICT_SAVED)
|
||||
|
||||
**下载回执 `train_book_download_record`**:`BK-00012/BV-3` SUCCESS、`BK-00013/BV-2` SUCCESS、`BK-00014/BV-1` FAIL(errMsg=存储空间不足)
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "阅读机-听书机接口 (v1)",
|
||||
"description": "配套 015_train_test_data.sql 与 016_api_test_data.md。\nbaseUrl 默认 http://localhost:8096/api/iot/v1(端口按实际改)。\n\n【鉴权】先发‘1.设备学员登录’,其 Tests 脚本会自动把 data.accessToken 存入集合变量 accessToken;其余需鉴权接口通过 Authorization: Bearer {{accessToken}} 使用。\n\n【免鉴权】book/list、book/download 已放开,可直接测。\n\n【下载】book/download 读取 train_book_snapshot.download_url(已指向本地 D:/tmp/books/<bookId>/<bookVersion>.pdf 占位文件),请用 Send and Download 保存为文件;支持 Range 断点续传。下架书(BK-00012/BV-2、BK-00015)会返回业务错误。",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "baseUrl", "value": "http://localhost:8096/api/iot/v1" },
|
||||
{ "key": "accessToken", "value": "" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1.设备学员登录 POST /device/login",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/device/login",
|
||||
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||
"body": { "mode": "raw", "raw": "{\n \"deviceId\": \"READER-LIB-01\",\n \"userId\": 10001\n}" }
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"exec": [
|
||||
"var secrets = {",
|
||||
" \"READER-LIB-01\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",",
|
||||
" \"READER-LIB-02\": \"60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752\",",
|
||||
" \"READER-LIB-03\": \"688787d8ff144c502c7f5cffaafe2cc588d86079f9de88304c26b0cb99ce91c6\"",
|
||||
"};",
|
||||
"var body = JSON.parse(pm.request.body.raw);",
|
||||
"var secret = secrets[body.deviceId];",
|
||||
"body.timestamp = Date.now();",
|
||||
"var raw = JSON.stringify(body);",
|
||||
"pm.request.body.raw = raw;",
|
||||
"var nonce = CryptoJS.lib.WordArray.random(16).toString();",
|
||||
"var stringToSign = \"POST\\n/api/iot/v1/device/login\\n\" + body.timestamp + \"\\n\" + nonce + \"\\n\" + raw;",
|
||||
"var sig = CryptoJS.HmacSHA256(stringToSign, secret).toString(CryptoJS.enc.Hex);",
|
||||
"pm.request.headers.upsert({ key: \"X-Nonce\", value: nonce });",
|
||||
"pm.request.headers.upsert({ key: \"X-Signature\", value: sig });"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"var json = pm.response.json();",
|
||||
"if (json && json.data && json.data.accessToken) {",
|
||||
" pm.collectionVariables.set('accessToken', json.data.accessToken);",
|
||||
" console.log('accessToken saved');",
|
||||
"} else {",
|
||||
" console.log('login failed: ' + pm.response.text());",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2.设备注销 POST /device/logout",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/device/logout",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Authorization", "value": "Bearer {{accessToken}}" }
|
||||
],
|
||||
"body": { "mode": "raw", "raw": "{\n \"deviceId\": \"READER-LIB-01\",\n \"reason\": \"REBOOT\",\n \"timestamp\": 1767621000000,\n \"nonce\": \"f6e5d4c3b2a1\"\n}" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "3.设备心跳 POST /device/heartbeat",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/device/heartbeat",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Authorization", "value": "Bearer {{accessToken}}" }
|
||||
],
|
||||
"body": { "mode": "raw", "raw": "{\n \"deviceId\": \"READER-LIB-01\",\n \"status\": \"ONLINE\",\n \"metrics\": {\"cpu\":20,\"mem\":30,\"diskFree\":10240},\n \"configVersion\": \"CV-17\",\n \"personVersion\": \"PV-226118-7\",\n \"timestamp\": 1767622000000,\n \"nonce\": \"aa11bb22cc33\"\n}" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.阅读记录单条上报 POST /reading/record",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/reading/record",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Authorization", "value": "Bearer {{accessToken}}" }
|
||||
],
|
||||
"body": { "mode": "raw", "raw": "{\n \"deviceId\": \"READER-LIB-01\",\n \"recordId\": \"RD-TEST-0001\",\n \"bookId\": \"BK-00012\",\n \"bookVersion\": \"BV-3\",\n \"mode\": \"read\",\n \"recordType\": \"END\",\n \"startTime\": 1767620000000,\n \"endTime\": 1767622400000,\n \"durationSec\": 1420,\n \"pageFrom\": 12,\n \"pageTo\": 46,\n \"progress\": 0.35\n}" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.阅读记录批量上报 POST /reading/record/batch",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/reading/record/batch",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Authorization", "value": "Bearer {{accessToken}}" }
|
||||
],
|
||||
"body": { "mode": "raw", "raw": "{\n \"deviceId\": \"READER-LIB-01\",\n \"batchId\": \"BATCH-RD-001\",\n \"timestamp\": 1767622500000,\n \"nonce\": \"b2b3c4d5e6f7\",\n \"records\": [\n {\"recordId\":\"RD-TEST-0002\",\"bookId\":\"BK-00013\",\"bookVersion\":\"BV-2\",\"mode\":\"read\",\"recordType\":\"END\",\"startTime\":1767620000000,\"endTime\":1767622400000,\"durationSec\":980,\"pageFrom\":5,\"pageTo\":22,\"progress\":0.18}\n ]\n}" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.阅读汇总与续读进度 GET /reading/progress",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/reading/progress?deviceId=READER-LIB-01",
|
||||
"header": [ { "key": "Authorization", "value": "Bearer {{accessToken}}" } ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.可阅读书籍列表 GET /book/list (免鉴权)",
|
||||
"description": "免鉴权。可加 &bookCategory=SAFETY 过滤。无分类时只返回上架书 total=3(下架的 BK-00012/BV-2、BK-00015 不返回)。",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/book/list?deviceId=READER-LIB-01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.PDF书籍下载 GET /book/download (免鉴权)",
|
||||
"description": "免鉴权。download_url 已指向本地 D:/tmp/books/BK-00012/BV-3.pdf(占位文件)。请用 Send and Download 保存。支持 Range 断点续传。下架书 bookId=BK-00012&bookVersion=BV-2 会返回业务错误『图书已下架,禁止下载』。",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/book/download?deviceId=READER-LIB-01&bookId=BK-00012&bookVersion=BV-3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9.下载完成回执 POST /book/download/ack",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/book/download/ack",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" },
|
||||
{ "key": "Authorization", "value": "Bearer {{accessToken}}" }
|
||||
],
|
||||
"body": { "mode": "raw", "raw": "{\n \"deviceId\": \"READER-LIB-01\",\n \"bookId\": \"BK-00012\",\n \"bookVersion\": \"BV-3\",\n \"fileMd5\": \"c4ca4238a0b923820dcc509a6f75849b\",\n \"result\": \"SUCCESS\",\n \"errMsg\": \"\",\n \"timestamp\": 1767630000000,\n \"nonce\": \"cc55dd66ee77\"\n}" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# 018_common_api_curl.sh
|
||||
# 通用数据接口(/api/common/v1)联调脚本 —— 对应 TrainCommonApiController 的 5 个
|
||||
# Bearer 鉴权接口:student/list、plan/list、course/list、checkin/upload、score/upload
|
||||
#
|
||||
# 运行前提:
|
||||
# 1) 后端已启动(默认 http://localhost:8096)
|
||||
# 2) 已执行 018_common_api_test_data.sql(提供 student/course/plan 种子数据,
|
||||
# 否则 student/list、plan/list、course/list 返回空数组,上传外键会失败)
|
||||
# 3) sys_user 中存在下方 USERNAME 账号(默认 admin / admin123,按你环境调整)
|
||||
# 4) 已执行 015_train_test_data.sql(阅读机数据,与本脚本无关,仅作环境提示)
|
||||
#
|
||||
# 鉴权方式:先 POST /login 拿 Bearer token,后续接口统一带 Authorization 头
|
||||
# —— 与“走 RuoYi Bearer 鉴权,Token 来源于 /login”完全一致。
|
||||
#
|
||||
# 运行:在 Git Bash / WSL / 任意 bash 中执行 bash 018_common_api_curl.sh
|
||||
# 可用环境变量覆盖:BASE / USERNAME / PASSWORD
|
||||
#
|
||||
# 依赖:curl(必需);jq(可选,用于美化 JSON 与自动取 planId,缺失时自动降级)
|
||||
# ==============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
BASE="${BASE:-http://localhost:8096}"
|
||||
USERNAME="${USERNAME:-admin}"
|
||||
PASSWORD="${PASSWORD:-admin123}"
|
||||
|
||||
echo "==> [0] 获取 Bearer Token POST $BASE/login"
|
||||
LOGIN_RESP=$(curl -s -X POST "$BASE/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}")
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
TOKEN=$(printf '%s' "$LOGIN_RESP" | jq -r '.data.token // empty')
|
||||
else
|
||||
TOKEN=$(printf '%s' "$LOGIN_RESP" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
|
||||
fi
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "✗ 登录失败,原始响应:"
|
||||
printf '%s\n' "$LOGIN_RESP"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ 获取 token 成功(前20字符):${TOKEN:0:20}..."
|
||||
|
||||
AUTH="Authorization: Bearer $TOKEN"
|
||||
|
||||
# 有 jq 就美化输出,否则原样打印
|
||||
pretty() {
|
||||
if command -v jq >/dev/null 2>&1; then jq .; else cat; fi
|
||||
}
|
||||
|
||||
echo
|
||||
echo "==> [1] 学员信息列表 GET $BASE/api/common/v1/student/list"
|
||||
curl -s "$BASE/api/common/v1/student/list" -H "$AUTH" | pretty
|
||||
echo
|
||||
|
||||
echo "==> [2] 班级(培训计划)列表 GET $BASE/api/common/v1/plan/list"
|
||||
PLAN_RESP=$(curl -s "$BASE/api/common/v1/plan/list" -H "$AUTH")
|
||||
printf '%s' "$PLAN_RESP" | pretty
|
||||
echo
|
||||
|
||||
# 自动取第一个 planId 供 course/list 使用(无 jq / 空数据时回退占位)
|
||||
PLAN_ID="P2026001"
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
AUTO_PLAN=$(printf '%s' "$PLAN_RESP" | jq -r '.data[0].planId // empty')
|
||||
[ -n "$AUTO_PLAN" ] && PLAN_ID="$AUTO_PLAN"
|
||||
fi
|
||||
echo "(course/list 使用的 classId = $PLAN_ID,如需手动指定请在下方覆盖)"
|
||||
|
||||
echo
|
||||
echo "==> [3] 班级课程信息 GET $BASE/api/common/v1/course/list?classId=$PLAN_ID"
|
||||
curl -s "$BASE/api/common/v1/course/list?classId=$PLAN_ID" -H "$AUTH" | pretty
|
||||
echo
|
||||
|
||||
echo "==> [4] 上传考勤记录 POST $BASE/api/common/v1/checkin/upload"
|
||||
curl -s -X POST "$BASE/api/common/v1/checkin/upload" \
|
||||
-H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"planId":"'"$PLAN_ID"'",
|
||||
"planName":"2026年第1期接触网工培训班",
|
||||
"studentId":"S20260001",
|
||||
"studentName":"张三",
|
||||
"courseName":"接触网安全规程",
|
||||
"checkinWay":1,
|
||||
"signInTime":1761000000000,
|
||||
"signOutTime":1761003600000,
|
||||
"attendanceStatus":1,
|
||||
"boardSn":"BP-001",
|
||||
"boardName":"一班班牌"
|
||||
}' | pretty
|
||||
echo
|
||||
|
||||
echo "==> [5] 上传实训成绩 POST $BASE/api/common/v1/score/upload"
|
||||
curl -s -X POST "$BASE/api/common/v1/score/upload" \
|
||||
-H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"planId":"'"$PLAN_ID"'",
|
||||
"cId":"C0001",
|
||||
"studentId":"S20260001",
|
||||
"courseScore":90.5,
|
||||
"trainScore":88.0,
|
||||
"examScore":95.0,
|
||||
"sourceType":3,
|
||||
"sourceSystem":"IOT",
|
||||
"importTime":1761000000000
|
||||
}' | pretty
|
||||
echo
|
||||
|
||||
echo "==> 完成。"
|
||||
@@ -0,0 +1,57 @@
|
||||
-- ============================================================
|
||||
-- 018_common_api_test_data.sql
|
||||
-- 配套 TrainCommonApiController 的 5 个通用数据接口(/api/common/v1)的测试种子数据
|
||||
-- 用途:让 student/list、plan/list、course/list 返回真实数据,
|
||||
-- 并使 checkin/upload、score/upload 的外键(student / course / plan)成立。
|
||||
-- 说明:仅 INSERT,可重复执行;PK 表用 ON CONFLICT DO NOTHING,
|
||||
-- train_plan_course 无唯一约束,故先按组合清理再插(测试数据幂等)。
|
||||
-- 前置:train_teacher / train_course / train_student / train_class_plan /
|
||||
-- train_plan_course 表已存在(V2 基线 000 / 002)。
|
||||
-- 与 015_train_test_data.sql(阅读机 7 张表)互补,互不影响。
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 教师(train_class_plan.t_id 外键依赖)
|
||||
INSERT INTO train_teacher (t_id, t_name, t_gender, t_department, t_title, create_user)
|
||||
VALUES ('T0001', '李教员', '男', '职工培训部', '高级讲师', 'system')
|
||||
ON CONFLICT (t_id) DO NOTHING;
|
||||
|
||||
-- 2) 课程(train_class_plan.c_id 外键依赖;也是 course/list 的来源之一)
|
||||
INSERT INTO train_course (c_id, c_name, c_type, c_duration, teaching_objective, create_user)
|
||||
VALUES ('C0001', '接触网安全规程', '理论', 4, '掌握接触网作业安全规程与事故防范', 'system')
|
||||
ON CONFLICT (c_id) DO NOTHING;
|
||||
|
||||
-- 3) 学员(checkin/upload、score/upload 的 student_id 外键依赖)
|
||||
INSERT INTO train_student (s_id, s_name, s_gender, s_birth, s_department, s_major, s_class, s_phone, create_user)
|
||||
VALUES ('S20260001', '张三', '男', '1998-05-12', '供电段', '接触网工', '接触网工班', '13800000001', 'system')
|
||||
ON CONFLICT (s_id) DO NOTHING;
|
||||
|
||||
-- 4) 班级(培训计划):plan/list 来源
|
||||
INSERT INTO train_class_plan (plan_id, plan_name, c_id, start_time, end_time, train_address, t_id, recruit_num, plan_status, create_user)
|
||||
VALUES ('P2026001', '2026年第1期接触网工培训班', 'C0001', '2026-10-01 09:00:00', '2026-10-30 18:00:00', '实训基地', 'T0001', 30, 1, 'system')
|
||||
ON CONFLICT (plan_id) DO NOTHING;
|
||||
|
||||
-- 5) 班级课程关联:course/list 依赖 train_plan_course(plan_id + c_id)
|
||||
-- 显式指定 plan_course_id=90001,供下面的课次(lesson)外键关联
|
||||
DELETE FROM train_plan_course WHERE plan_id = 'P2026001' AND c_id = 'C0001';
|
||||
INSERT INTO train_plan_course (plan_course_id, plan_id, c_id, sort_no, create_by)
|
||||
VALUES (90001, 'P2026001', 'C0001', 1, 'system');
|
||||
|
||||
-- 6) 教室(train_lesson 场地依赖,venue_type=1 时必填 class_room_id)
|
||||
DELETE FROM train_classroom_info WHERE class_room_id = 'CR-001';
|
||||
INSERT INTO train_classroom_info (class_room_id, room_name, room_location, capacity, create_user)
|
||||
VALUES ('CR-001', '第一理论教室', '实训基地A栋201', 40, 'system');
|
||||
|
||||
-- 7) 参训记录(checkin / score 上传的 enrollment_id 来源)
|
||||
DELETE FROM train_enrollment WHERE enrollment_id = 900001;
|
||||
INSERT INTO train_enrollment (enrollment_id, plan_id, student_id, source_type, enrollment_status, accommodation_required, create_by)
|
||||
VALUES (900001, 'P2026001', 'S20260001', 1, 1, 0, 'system');
|
||||
|
||||
-- 8) 课次(checkin 上传的 lesson_id 来源;需绑定 plan_course 与 教室)
|
||||
DELETE FROM train_lesson WHERE lesson_id = 9000001;
|
||||
INSERT INTO train_lesson (lesson_id, plan_course_id, t_id, start_time, end_time, venue_type, class_room_id, create_by)
|
||||
VALUES (9000001, 90001, 'T0001', '2026-10-08 09:00:00', '2026-10-08 11:00:00', 1, 'CR-001', 'system');
|
||||
|
||||
-- 9) 成绩项(score/upload 的 score_item_id 来源;train_student_score.score_item_id 为 NOT NULL)
|
||||
DELETE FROM train_score_item WHERE score_item_id = 90001;
|
||||
INSERT INTO train_score_item (score_item_id, plan_id, plan_course_id, item_name, item_type, weight, full_score, create_by)
|
||||
VALUES (90001, 'P2026001', 90001, '综合成绩', 4, 100.00, 100.00, 'system');
|
||||
@@ -0,0 +1,19 @@
|
||||
-- ============================================================
|
||||
-- 018b_alter_reading_duration_log_read_sec.sql
|
||||
-- 为 train_reading_duration_log 增加“已朗读时长”字段 read_sec(INTEGER)
|
||||
-- 与既有 duration_sec(有效时长秒)区分:read_sec 表示学员实际朗读时长(秒)。
|
||||
-- 说明:仓库内未包含该表的 CREATE TABLE DDL(仅有 015 的 INSERT 与 016 文档),
|
||||
-- 故用补列脚本将字段落到已存在的表上;脚本幂等,可重复执行。
|
||||
-- ============================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'train_reading_duration_log'
|
||||
AND column_name = 'read_sec'
|
||||
) THEN
|
||||
ALTER TABLE train_reading_duration_log ADD COLUMN read_sec INTEGER;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,63 @@
|
||||
-- ============================================================
|
||||
-- 019_signage.sql (MySQL 5.7)
|
||||
-- 电子班牌取数接口(/api/board/v1/signageService/*)所需补充表与字段
|
||||
-- 配合 TrainBoardApiController + TrainSignageMapper 使用。
|
||||
-- 说明:仅 ADD COLUMN / CREATE TABLE / 测试种子。
|
||||
-- 前置:train_student / train_classroom_info / train_class_plan /
|
||||
-- train_lesson / train_plan_course / train_course / train_enrollment /
|
||||
-- train_teacher 已存在(V2 基线 000 / 002)。
|
||||
-- 注意:MySQL 5.7 不支持 ALTER TABLE ADD COLUMN IF NOT EXISTS,
|
||||
-- 下面用临时存储过程做幂等判断;CREATE TABLE 用 IF NOT EXISTS(5.7 支持)。
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 学员卡号(刷卡校验用,train_student 无此列,需补列)
|
||||
DROP PROCEDURE IF EXISTS tmp_add_card_no;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE tmp_add_card_no()
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'train_student'
|
||||
AND column_name = 'card_no'
|
||||
) THEN
|
||||
ALTER TABLE train_student
|
||||
ADD COLUMN card_no VARCHAR(32) DEFAULT NULL COMMENT '学员卡号(刷卡校验用)';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL tmp_add_card_no();
|
||||
DROP PROCEDURE IF EXISTS tmp_add_card_no;
|
||||
|
||||
-- 2) 座位分配表(接口3 studentInClassroom 所需,原库无座位表)
|
||||
CREATE TABLE IF NOT EXISTS train_class_seat (
|
||||
seat_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
plan_id VARCHAR(32) NOT NULL COMMENT '班级ID(train_class_plan.plan_id)',
|
||||
classroom_id VARCHAR(32) NOT NULL COMMENT '教室ID(train_classroom_info.class_room_id)',
|
||||
row_index INT NOT NULL COMMENT '行号,从1开始',
|
||||
col_index INT NOT NULL COMMENT '列号,从1开始',
|
||||
seat_code VARCHAR(32) DEFAULT NULL COMMENT '物理座位标识',
|
||||
canceled SMALLINT NOT NULL DEFAULT 0 COMMENT '是否取消位置(0正常,1取消)',
|
||||
student_id VARCHAR(32) DEFAULT NULL COMMENT '占用座位学员ID(train_student.s_id)',
|
||||
zone_id BIGINT DEFAULT NULL COMMENT '区域ID',
|
||||
dept_detail VARCHAR(100) DEFAULT NULL COMMENT '部门',
|
||||
job VARCHAR(100) DEFAULT NULL COMMENT '职务',
|
||||
create_by VARCHAR(64) DEFAULT NULL,
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64) DEFAULT NULL,
|
||||
update_time TIMESTAMP NULL DEFAULT NULL,
|
||||
remark VARCHAR(500) DEFAULT NULL,
|
||||
PRIMARY KEY (seat_id),
|
||||
UNIQUE KEY uq_class_seat_pos (plan_id, classroom_id, row_index, col_index)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级在指定教室的座位分配表(电子班牌座位网格来源)';
|
||||
|
||||
-- 3) 测试种子:为 018 的学员 S20260001 设置卡号(文档示例值)
|
||||
UPDATE train_student SET card_no = '0514457515' WHERE s_id = 'S20260001';
|
||||
|
||||
-- 4) 测试种子:P2026001 在教室 CR-001 的座位布局
|
||||
DELETE FROM train_class_seat WHERE plan_id = 'P2026001' AND classroom_id = 'CR-001';
|
||||
INSERT INTO train_class_seat (plan_id, classroom_id, row_index, col_index, seat_code, canceled, student_id, zone_id, dept_detail, job)
|
||||
VALUES
|
||||
('P2026001', 'CR-001', 1, 1, '1-1', 0, 'S20260001', 100, '培训部', '学员'),
|
||||
('P2026001', 'CR-001', 1, 2, '1-2', 0, NULL, 100, NULL, NULL),
|
||||
('P2026001', 'CR-001', 2, 1, '2-1', 1, NULL, 100, NULL, NULL);
|
||||
@@ -0,0 +1,53 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库 V2 增量脚本: 020_add_checkin_student_id.sql
|
||||
-- 目标: 为 train_student_checkin 增加 student_id (VARCHAR(32))
|
||||
-- 把学员维度从 enrollment 关系下沉为考勤行本身的冗余列,按学员检索考勤时
|
||||
-- 无需再 join train_enrollment,同时保留 enrollment_id 作为业务唯一键。
|
||||
-- 适用数据库: KingbaseES V009R003C018 / PostgreSQL (schema: public)
|
||||
-- 幂等性: 通过 information_schema 判断列是否已存在,可重复执行
|
||||
-- 关联变更: 004/006 基线同步、010 校验规则调整、Mapper/Domain/DTO/Service 同步
|
||||
-- ==============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 步骤 1: 新增列 (可为空,先加列再回填,避免已有数据阻塞)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'train_student_checkin'
|
||||
AND column_name = 'student_id'
|
||||
) THEN
|
||||
ALTER TABLE train_student_checkin ADD COLUMN student_id VARCHAR(32);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN train_student_checkin.student_id IS '学员ID (冗余自 train_enrollment.student_id,随考勤行落库)';
|
||||
|
||||
-- 步骤 2: 以参训档案为准回填历史数据
|
||||
UPDATE train_student_checkin a
|
||||
SET student_id = e.student_id
|
||||
FROM train_enrollment e
|
||||
WHERE e.enrollment_id = a.enrollment_id
|
||||
AND (a.student_id IS NULL OR a.student_id <> e.student_id);
|
||||
|
||||
-- 步骤 3: 按学员维度检索的索引
|
||||
CREATE INDEX IF NOT EXISTS idx_checkin_student ON train_student_checkin (student_id);
|
||||
|
||||
-- 步骤 4: 回填结果校验 (存在无参训档案的脏数据时仅告警,不中断发布)
|
||||
DO $$
|
||||
DECLARE
|
||||
v_null_cnt INT;
|
||||
BEGIN
|
||||
SELECT count(*) INTO v_null_cnt
|
||||
FROM train_student_checkin
|
||||
WHERE student_id IS NULL;
|
||||
IF v_null_cnt > 0 THEN
|
||||
RAISE NOTICE '>>> [CheckinStudentId-WARN] 仍有 % 行 student_id 为空,请核对 enrollment_id 是否有效', v_null_cnt;
|
||||
ELSE
|
||||
RAISE NOTICE '>>> [CheckinStudentId-PASS] train_student_checkin.student_id 已全量回填';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,49 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本 (MySQL 5.7/8.0): 020b_add_checkin_student_id_mysql.sql
|
||||
-- 目标: 为 train_student_checkin 增加 student_id (VARCHAR(32)),并回填历史数据
|
||||
-- 适用数据库: MySQL 5.7 / 8.0 (线上 training_railway)
|
||||
-- 说明:
|
||||
-- * 020_add_checkin_student_id.sql 为 KingbaseES/PostgreSQL 版本(DO $$ / UPDATE ... FROM),
|
||||
-- 在 MySQL 上无法执行,故单独提供本脚本。
|
||||
-- * 全部语句使用 information_schema 守卫,可重复执行。
|
||||
-- 执行:
|
||||
-- mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 020b_add_checkin_student_id_mysql.sql
|
||||
-- ==============================================================================
|
||||
|
||||
-- 步骤 1: 加列 (仅当不存在)
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'train_student_checkin'
|
||||
AND COLUMN_NAME = 'student_id'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE `train_student_checkin` ADD COLUMN `student_id` VARCHAR(32) NULL COMMENT ''学员ID(冗余自 train_enrollment.student_id)'' AFTER `lesson_id`',
|
||||
'SELECT ''student_id 已存在,跳过加列'' AS msg');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 步骤 2: 以参训档案为准回填历史数据
|
||||
UPDATE `train_student_checkin` a
|
||||
JOIN `train_enrollment` e ON e.enrollment_id = a.enrollment_id
|
||||
SET a.student_id = e.student_id
|
||||
WHERE a.student_id IS NULL OR a.student_id <> e.student_id;
|
||||
|
||||
-- 步骤 3: 按学员维度检索的索引 (仅当不存在)
|
||||
SET @idx_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'train_student_checkin'
|
||||
AND INDEX_NAME = 'idx_checkin_student'
|
||||
);
|
||||
SET @ddl2 := IF(@idx_exists = 0,
|
||||
'CREATE INDEX `idx_checkin_student` ON `train_student_checkin` (`student_id`)',
|
||||
'SELECT ''idx_checkin_student 已存在,跳过建索引'' AS msg');
|
||||
PREPARE stmt2 FROM @ddl2;
|
||||
EXECUTE stmt2;
|
||||
DEALLOCATE PREPARE stmt2;
|
||||
|
||||
-- 步骤 4: 结果校验 (total_rows 总行数, null_rows 未回填行数)
|
||||
SELECT COUNT(*) AS total_rows, SUM(student_id IS NULL) AS null_rows
|
||||
FROM `train_student_checkin`;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库 V2 增量脚本: 021_add_teacher_card_no.sql
|
||||
-- 目标: 为 train_teacher 增加 card_no (教师卡号),与 train_student.card_no 保持同长度 (VARCHAR(32))
|
||||
-- 适用数据库: KingbaseES V009R003C018 / PostgreSQL (schema: public)
|
||||
-- 幂等性: ADD COLUMN IF NOT EXISTS,可重复执行
|
||||
-- 说明: 仅新增列与列注释,不涉及任何数据约束、默认值或业务规则
|
||||
-- ==============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE public.train_teacher
|
||||
ADD COLUMN IF NOT EXISTS card_no VARCHAR(32);
|
||||
|
||||
COMMENT ON COLUMN public.train_teacher.card_no IS '教师卡号(刷卡/身份识别用)';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本 (MySQL 5.7/8.0): 021b_add_teacher_card_no_mysql.sql
|
||||
-- 目标: 为 train_teacher 增加 card_no (教师卡号),长度与 train_student.card_no 对齐 (VARCHAR(32))
|
||||
-- 说明: 021_add_teacher_card_no.sql 为 KingbaseES/PostgreSQL 版本,MySQL 请使用本脚本
|
||||
-- 幂等: 通过 information_schema 判断列是否已存在,可重复执行
|
||||
-- 执行:
|
||||
-- mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 021b_add_teacher_card_no_mysql.sql
|
||||
-- ==============================================================================
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'train_teacher'
|
||||
AND COLUMN_NAME = 'card_no'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE `train_teacher` ADD COLUMN `card_no` VARCHAR(32) DEFAULT NULL COMMENT ''教师卡号(刷卡/身份识别用)''',
|
||||
'SELECT ''train_teacher.card_no 已存在,跳过加列'' AS msg');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT COUNT(*) AS total_rows,
|
||||
SUM(card_no IS NULL) AS null_rows
|
||||
FROM `train_teacher`;
|
||||
@@ -0,0 +1,52 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库 V2 增量脚本: 022_add_accommodation_lock_result.sql
|
||||
-- 目标: 为 train_accommodation 增加 lock_result (门锁授权结果)
|
||||
-- 1. 新增列 lock_result SMALLINT NOT NULL DEFAULT 1
|
||||
-- 取值: 1 未授权 / 2 已授权 / 3 已过期,默认 1(未授权)
|
||||
-- 2. 新增字典类型 train_accommodation_lock_result 及其 3 个字典项
|
||||
-- 适用数据库: KingbaseES V009R003C018 / PostgreSQL (schema: public)
|
||||
-- 幂等性: ADD COLUMN IF NOT EXISTS + NOT EXISTS 判断,可重复执行
|
||||
-- 说明: 仅新增列、列注释与字典数据,不涉及数据约束变更与业务逻辑调整
|
||||
-- ==============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. 新增门锁授权结果列
|
||||
ALTER TABLE public.train_accommodation
|
||||
ADD COLUMN IF NOT EXISTS lock_result SMALLINT NOT NULL DEFAULT 1;
|
||||
|
||||
COMMENT ON COLUMN public.train_accommodation.lock_result IS '门锁授权结果 (1:未授权, 2:已授权, 3:已过期)';
|
||||
|
||||
-- 2. 字典类型
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT '住宿门锁授权结果', 'train_accommodation_lock_result', '0', 'admin', CURRENT_TIMESTAMP,
|
||||
'住宿记录请求门锁接口的授权结果 (1 未授权, 2 已授权, 3 已过期)'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_accommodation_lock_result');
|
||||
|
||||
-- 3. 字典项
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, '未授权', '1', 'train_accommodation_lock_result', 'info', 'Y', '0', 'admin', CURRENT_TIMESTAMP, '尚未取得门锁授权'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data
|
||||
WHERE dict_type = 'train_accommodation_lock_result' AND dict_value = '1');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, '已授权', '2', 'train_accommodation_lock_result', 'success', 'N', '0', 'admin', CURRENT_TIMESTAMP, '门锁接口已返回授权成功'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data
|
||||
WHERE dict_type = 'train_accommodation_lock_result' AND dict_value = '2');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 3, '已过期', '3', 'train_accommodation_lock_result', 'warning', 'N', '0', 'admin', CURRENT_TIMESTAMP, '授权已到期失效'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data
|
||||
WHERE dict_type = 'train_accommodation_lock_result' AND dict_value = '3');
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ==============================================================================
|
||||
-- 核对语句(可选,只读):
|
||||
-- SELECT column_name, data_type, column_default, is_nullable
|
||||
-- FROM information_schema.columns
|
||||
-- WHERE table_name = 'train_accommodation' AND column_name = 'lock_result';
|
||||
-- SELECT dict_code, dict_label, dict_value, list_class, is_default
|
||||
-- FROM sys_dict_data WHERE dict_type = 'train_accommodation_lock_result'
|
||||
-- ORDER BY dict_sort;
|
||||
-- ==============================================================================
|
||||
@@ -0,0 +1,56 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本 (MySQL 5.7/8.0): 022b_add_accommodation_lock_result_mysql.sql
|
||||
-- 目标: 为 train_accommodation 增加 lock_result (门锁授权结果)
|
||||
-- 1. 新增列 lock_result SMALLINT NOT NULL DEFAULT 1
|
||||
-- 取值: 1 未授权 / 2 已授权 / 3 已过期,默认 1(未授权)
|
||||
-- 2. 新增字典类型 train_accommodation_lock_result 及其 3 个字典项
|
||||
-- 说明: 022_add_accommodation_lock_result.sql 为 KingbaseES/PostgreSQL 版本,MySQL 请使用本脚本
|
||||
-- 幂等: 通过 information_schema 判断列是否已存在 + 字典 NOT EXISTS 判断,可重复执行
|
||||
-- 执行:
|
||||
-- mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 022b_add_accommodation_lock_result_mysql.sql
|
||||
-- ==============================================================================
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'train_accommodation'
|
||||
AND COLUMN_NAME = 'lock_result'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE `train_accommodation` ADD COLUMN `lock_result` SMALLINT NOT NULL DEFAULT 1 COMMENT ''门锁授权结果 (1:未授权, 2:已授权, 3:已过期)''',
|
||||
'SELECT ''train_accommodation.lock_result 已存在,跳过加列'' AS msg');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 字典类型
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT '住宿门锁授权结果', 'train_accommodation_lock_result', '0', 'admin', NOW(),
|
||||
'住宿记录请求门锁接口的授权结果 (1 未授权, 2 已授权, 3 已过期)'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_accommodation_lock_result');
|
||||
|
||||
-- 字典项
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, '未授权', '1', 'train_accommodation_lock_result', 'info', 'Y', '0', 'admin', NOW(), '尚未取得门锁授权'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data
|
||||
WHERE dict_type = 'train_accommodation_lock_result' AND dict_value = '1');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, '已授权', '2', 'train_accommodation_lock_result', 'success', 'N', '0', 'admin', NOW(), '门锁接口已返回授权成功'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data
|
||||
WHERE dict_type = 'train_accommodation_lock_result' AND dict_value = '2');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 3, '已过期', '3', 'train_accommodation_lock_result', 'warning', 'N', '0', 'admin', NOW(), '授权已到期失效'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data
|
||||
WHERE dict_type = 'train_accommodation_lock_result' AND dict_value = '3');
|
||||
|
||||
SELECT COUNT(*) AS total_rows,
|
||||
SUM(lock_result = 1) AS unauthorized_rows,
|
||||
SUM(lock_result = 2) AS authorized_rows,
|
||||
SUM(lock_result = 3) AS expired_rows
|
||||
FROM `train_accommodation`;
|
||||
@@ -0,0 +1,413 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本: 023_open_exam_api.sql
|
||||
-- 模块: 外部实训系统数据接口(开放接口 /api/open/v1)
|
||||
-- 内容: 建表语句 + 约束/索引 + 字典 + 菜单 + 测试数据
|
||||
-- 数据库: MySQL 5.7(生产主库);KingbaseES 落地差异见 doc/外部实训系统数据接口设计(V1.0).md 3.9
|
||||
-- 说明:
|
||||
-- 1) MySQL 5.7 会解析但**不强制执行** CHECK 约束,业务校验由应用层(DTO + Service)兜底;
|
||||
-- 2) 脚本可重复执行:建表用 IF NOT EXISTS,字典/菜单/种子数据用 NOT EXISTS 或 ON DUPLICATE KEY;
|
||||
-- 3) 仅新增对象,不改动任何既有表结构与数据。
|
||||
-- 执行: mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 023_open_exam_api.sql
|
||||
-- ==============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 1. 外部应用注册表
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_open_app` (
|
||||
`app_id` VARCHAR(64) NOT NULL COMMENT '应用标识(报文 appId)',
|
||||
`app_name` VARCHAR(100) NOT NULL COMMENT '应用名称',
|
||||
`app_type` VARCHAR(16) NOT NULL COMMENT '应用类型(VR/INSPECT3D/PRACTICE/CAPSTONE/OTHER)',
|
||||
`org_name` VARCHAR(100) DEFAULT NULL COMMENT '归属单位',
|
||||
`app_secret_hash` VARCHAR(128) NOT NULL COMMENT 'appSecret 散列(BCrypt,不存明文)',
|
||||
`app_secret_masked` VARCHAR(16) DEFAULT NULL COMMENT '密钥掩码(展示后4位)',
|
||||
`contact_name` VARCHAR(50) DEFAULT NULL COMMENT '联系人',
|
||||
`contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
|
||||
`rate_limit` INT NOT NULL DEFAULT 20 COMMENT '每秒请求上限',
|
||||
`realtime_enabled` SMALLINT NOT NULL DEFAULT 1 COMMENT '是否启用实时上报(1启用 0关闭)',
|
||||
`status` SMALLINT NOT NULL DEFAULT 1 COMMENT '状态(1启用 0停用)',
|
||||
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`app_id`),
|
||||
KEY `idx_open_app_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='外部实训系统应用注册表';
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 2. 应用授权范围表(班级 / 科目)
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_open_app_scope` (
|
||||
`scope_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`app_id` VARCHAR(64) NOT NULL COMMENT '应用标识',
|
||||
`scope_type` VARCHAR(16) NOT NULL COMMENT '范围类型(CLASS 班级 / SUBJECT 科目)',
|
||||
`scope_value` VARCHAR(64) NOT NULL COMMENT '班级 plan_id 或科目 subject_code',
|
||||
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`scope_id`),
|
||||
UNIQUE KEY `uk_open_app_scope` (`app_id`, `scope_type`, `scope_value`),
|
||||
KEY `idx_open_scope_value` (`scope_type`, `scope_value`),
|
||||
CONSTRAINT `fk_open_scope_app` FOREIGN KEY (`app_id`) REFERENCES `train_open_app` (`app_id`),
|
||||
CONSTRAINT `chk_open_scope_type` CHECK (`scope_type` IN ('CLASS', 'SUBJECT'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='外部应用授权范围表';
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 3. 访问令牌表(运行期以 Redis 为准,本表用于审计与吊销)
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_open_token` (
|
||||
`token_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`app_id` VARCHAR(64) NOT NULL COMMENT '应用标识',
|
||||
`token_hash` VARCHAR(64) NOT NULL COMMENT '令牌 SHA-256',
|
||||
`issued_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '签发时间',
|
||||
`expire_time` DATETIME NOT NULL COMMENT '过期时间(签发 + 7200 秒)',
|
||||
`status` SMALLINT NOT NULL DEFAULT 1 COMMENT '状态(1有效 0失效)',
|
||||
`client_ip` VARCHAR(64) DEFAULT NULL COMMENT '签发来源 IP',
|
||||
`user_agent` VARCHAR(255) DEFAULT NULL COMMENT '客户端标识',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`token_id`),
|
||||
UNIQUE KEY `uk_open_token_hash` (`token_hash`),
|
||||
KEY `idx_open_token_app_status` (`app_id`, `status`),
|
||||
KEY `idx_open_token_expire` (`expire_time`),
|
||||
CONSTRAINT `fk_open_token_app` FOREIGN KEY (`app_id`) REFERENCES `train_open_app` (`app_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='外部应用访问令牌表';
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 4. 考试科目字典表
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_exam_subject` (
|
||||
`subject_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`subject_code` VARCHAR(32) NOT NULL COMMENT '科目编码',
|
||||
`subject_name` VARCHAR(100) NOT NULL COMMENT '科目名称',
|
||||
`plan_id` VARCHAR(32) DEFAULT NULL COMMENT '所属培训班(空表示全局科目)',
|
||||
`score_item_id` BIGINT DEFAULT NULL COMMENT '归档考核项ID(train_score_item)',
|
||||
`status` SMALLINT NOT NULL DEFAULT 1 COMMENT '状态(1启用 0停用)',
|
||||
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` VARCHAR(200) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`subject_id`),
|
||||
UNIQUE KEY `uk_exam_subject_code` (`subject_code`),
|
||||
UNIQUE KEY `uk_exam_subject_name` (`subject_name`),
|
||||
KEY `idx_exam_subject_plan` (`plan_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='考试科目字典表';
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 5. 上报批次表
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_exam_report_batch` (
|
||||
`batch_pk` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`app_id` VARCHAR(64) NOT NULL COMMENT '应用标识',
|
||||
`batch_id` VARCHAR(64) NOT NULL COMMENT '批次号(报文 batchId)',
|
||||
`report_time` BIGINT NOT NULL COMMENT '上报时间(毫秒)',
|
||||
`period_start` BIGINT NOT NULL COMMENT '覆盖窗口开始(毫秒,含)',
|
||||
`period_end` BIGINT NOT NULL COMMENT '覆盖窗口结束(毫秒,不含)',
|
||||
`resend` SMALLINT NOT NULL DEFAULT 0 COMMENT '补传标记(1补传 0正常)',
|
||||
`total` INT NOT NULL DEFAULT 0 COMMENT '本批记录数',
|
||||
`accepted` INT NOT NULL DEFAULT 0 COMMENT '校验通过并入库数',
|
||||
`duplicated` INT NOT NULL DEFAULT 0 COMMENT 'reportId 已存在被忽略数',
|
||||
`unmatched` INT NOT NULL DEFAULT 0 COMMENT '学员无法匹配数',
|
||||
`received_time` BIGINT NOT NULL COMMENT '平台接收时间(毫秒)',
|
||||
`req_id` VARCHAR(64) DEFAULT NULL COMMENT '请求标识',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`batch_pk`),
|
||||
UNIQUE KEY `uk_batch_app_batch` (`app_id`, `batch_id`),
|
||||
KEY `idx_batch_received` (`app_id`, `received_time`),
|
||||
KEY `idx_batch_period` (`period_start`, `period_end`),
|
||||
CONSTRAINT `fk_batch_app` FOREIGN KEY (`app_id`) REFERENCES `train_open_app` (`app_id`),
|
||||
CONSTRAINT `chk_batch_time_range` CHECK (`period_end` > `period_start`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='考试成绩上报批次表';
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 6. 成绩上报记录表(核心,reportId 永久幂等键)
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_exam_report` (
|
||||
`report_pk` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`report_id` VARCHAR(64) NOT NULL COMMENT '上报编号(永久幂等键)',
|
||||
`app_id` VARCHAR(64) NOT NULL COMMENT '应用标识',
|
||||
`batch_pk` BIGINT DEFAULT NULL COMMENT '所属批次(实时上报为空)',
|
||||
`batch_id` VARCHAR(64) DEFAULT NULL COMMENT '批次号冗余',
|
||||
`report_channel` VARCHAR(16) NOT NULL COMMENT '上报渠道(REALTIME/PERIODIC)',
|
||||
`biz_type` VARCHAR(16) NOT NULL DEFAULT 'PRACTICE' COMMENT '成绩类型',
|
||||
`student_id` VARCHAR(32) NOT NULL COMMENT '学员ID(匹配键)',
|
||||
`student_name` VARCHAR(50) DEFAULT NULL COMMENT '上报姓名',
|
||||
`id_card_masked` VARCHAR(32) DEFAULT NULL COMMENT '证件号掩码',
|
||||
`id_card_md5` CHAR(32) CHARACTER SET ascii DEFAULT NULL COMMENT '证件号MD5',
|
||||
`matched_enrollment_id` BIGINT DEFAULT NULL COMMENT '匹配到的参训档案ID',
|
||||
`matched_plan_id` VARCHAR(32) DEFAULT NULL COMMENT '匹配到的培训班ID',
|
||||
`match_status` VARCHAR(16) NOT NULL DEFAULT 'UNMATCHED' COMMENT '匹配状态(MATCHED/UNMATCHED)',
|
||||
`score_status` VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '成绩状态(RECEIVED/ARCHIVED/LOCKED)',
|
||||
`subject_code` VARCHAR(32) DEFAULT NULL COMMENT '科目编码',
|
||||
`subject_name` VARCHAR(100) NOT NULL COMMENT '科目名称',
|
||||
`exam_start_time` BIGINT NOT NULL COMMENT '考试开始(毫秒)',
|
||||
`exam_end_time` BIGINT NOT NULL COMMENT '考试结束(毫秒)',
|
||||
`duration_seconds` INT DEFAULT NULL COMMENT '用时秒数',
|
||||
`score` DECIMAL(7,2) NOT NULL COMMENT '分数',
|
||||
`full_score` DECIMAL(7,2) NOT NULL DEFAULT 100.00 COMMENT '满分',
|
||||
`pass_flag` SMALLINT NOT NULL COMMENT '是否通过(1通过 0未通过)',
|
||||
`retake` SMALLINT NOT NULL DEFAULT 0 COMMENT '是否重考(1是 0否)',
|
||||
`retake_no` INT NOT NULL DEFAULT 0 COMMENT '重考序号',
|
||||
`station` VARCHAR(64) NOT NULL COMMENT '考试工位',
|
||||
`content` TEXT NOT NULL COMMENT '考试内容原始 JSON(≤32KB)',
|
||||
`content_size` INT DEFAULT NULL COMMENT '内容字节数',
|
||||
`content_md5` CHAR(32) CHARACTER SET ascii DEFAULT NULL COMMENT '内容摘要',
|
||||
`report_time` BIGINT DEFAULT NULL COMMENT '报文上报时间',
|
||||
`received_time` BIGINT NOT NULL COMMENT '平台接收时间',
|
||||
`resend` SMALLINT NOT NULL DEFAULT 0 COMMENT '补传标记',
|
||||
`claim_by` VARCHAR(64) DEFAULT NULL COMMENT '认领操作人',
|
||||
`claim_time` DATETIME DEFAULT NULL COMMENT '认领时间',
|
||||
`archive_time` DATETIME DEFAULT NULL COMMENT '归档时间',
|
||||
`lock_time` DATETIME DEFAULT NULL COMMENT '锁定时间',
|
||||
`lock_reason` VARCHAR(200) DEFAULT NULL COMMENT '锁定原因',
|
||||
`data_version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`report_pk`),
|
||||
UNIQUE KEY `uk_exam_report_id` (`report_id`),
|
||||
KEY `idx_report_app_time` (`app_id`, `received_time`),
|
||||
KEY `idx_report_app_batch` (`app_id`, `batch_id`),
|
||||
KEY `idx_report_student` (`app_id`, `student_id`, `exam_end_time`),
|
||||
KEY `idx_report_status` (`match_status`, `score_status`),
|
||||
KEY `idx_report_exam_end` (`exam_end_time`),
|
||||
CONSTRAINT `fk_report_app` FOREIGN KEY (`app_id`) REFERENCES `train_open_app` (`app_id`),
|
||||
CONSTRAINT `fk_report_batch` FOREIGN KEY (`batch_pk`) REFERENCES `train_exam_report_batch` (`batch_pk`),
|
||||
CONSTRAINT `chk_report_time_range` CHECK (`exam_end_time` > `exam_start_time`),
|
||||
CONSTRAINT `chk_report_score` CHECK (`score` >= 0 AND `score` <= `full_score`),
|
||||
CONSTRAINT `chk_report_match_status` CHECK (`match_status` IN ('MATCHED', 'UNMATCHED')),
|
||||
CONSTRAINT `chk_report_score_status` CHECK (`score_status` IN ('RECEIVED', 'ARCHIVED', 'LOCKED')),
|
||||
CONSTRAINT `chk_report_channel` CHECK (`report_channel` IN ('REALTIME', 'PERIODIC'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='考试成绩上报记录表';
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 7. 接口调用日志表
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_open_api_log` (
|
||||
`log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`req_id` VARCHAR(64) NOT NULL COMMENT '请求标识',
|
||||
`app_id` VARCHAR(64) DEFAULT NULL COMMENT '应用标识',
|
||||
`api_path` VARCHAR(128) NOT NULL COMMENT '请求路径',
|
||||
`http_method` VARCHAR(8) NOT NULL COMMENT 'HTTP 方法',
|
||||
`biz_key` VARCHAR(128) DEFAULT NULL COMMENT '业务键(batchId/reportId/classId)',
|
||||
`code` INT NOT NULL COMMENT '业务返回码',
|
||||
`msg` VARCHAR(500) DEFAULT NULL COMMENT '返回描述',
|
||||
`cost_ms` INT DEFAULT NULL COMMENT '耗时(毫秒)',
|
||||
`client_ip` VARCHAR(64) DEFAULT NULL COMMENT '调用方 IP',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间',
|
||||
PRIMARY KEY (`log_id`),
|
||||
KEY `idx_open_log_req` (`req_id`),
|
||||
KEY `idx_open_log_app_time` (`app_id`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开放接口调用日志表';
|
||||
|
||||
-- ==============================================================================
|
||||
-- 8. 字典数据
|
||||
-- ==============================================================================
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT '外部应用类型', 'train_open_app_type', '0', 'admin', NOW(), 'VR / 3D巡检 / 实操 / 帽石考核 / 其他'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_open_app_type');
|
||||
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT '成绩匹配状态', 'train_open_match_status', '0', 'admin', NOW(), '成绩与学员的匹配状态'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_open_match_status');
|
||||
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT '成绩归档状态', 'train_open_score_status', '0', 'admin', NOW(), '成绩接收 / 归档 / 锁定'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_open_score_status');
|
||||
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
|
||||
SELECT '成绩上报渠道', 'train_open_report_channel', '0', 'admin', NOW(), '实时上报 / 周期批量上报'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'train_open_report_channel');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, 'VR实训', 'VR', 'train_open_app_type', 'primary', 'Y', '0', 'admin', NOW(), 'VR 实训系统'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_app_type' AND dict_value = 'VR');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, '3D巡检', 'INSPECT3D', 'train_open_app_type', 'success', 'N', '0', 'admin', NOW(), '3D 数字孪生巡检系统'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_app_type' AND dict_value = 'INSPECT3D');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 3, '实操实训', 'PRACTICE', 'train_open_app_type', 'info', 'N', '0', 'admin', NOW(), '实操实训系统'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_app_type' AND dict_value = 'PRACTICE');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 4, '帽石考核', 'CAPSTONE', 'train_open_app_type', 'warning', 'N', '0', 'admin', NOW(), '帽石安装考核系统'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_app_type' AND dict_value = 'CAPSTONE');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 5, '其他', 'OTHER', 'train_open_app_type', 'info', 'N', '0', 'admin', NOW(), '其他实训考核系统'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_app_type' AND dict_value = 'OTHER');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, '已匹配', 'MATCHED', 'train_open_match_status', 'success', 'Y', '0', 'admin', NOW(), '成绩已匹配到学员'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_match_status' AND dict_value = 'MATCHED');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, '待认领', 'UNMATCHED', 'train_open_match_status', 'warning', 'N', '0', 'admin', NOW(), '学员无法匹配,进入待认领池'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_match_status' AND dict_value = 'UNMATCHED');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, '已接收', 'RECEIVED', 'train_open_score_status', 'info', 'Y', '0', 'admin', NOW(), '上报已接收,尚未归档'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_score_status' AND dict_value = 'RECEIVED');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, '已归档', 'ARCHIVED', 'train_open_score_status', 'success', 'N', '0', 'admin', NOW(), '已写入学员得分'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_score_status' AND dict_value = 'ARCHIVED');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 3, '已锁定', 'LOCKED', 'train_open_score_status', 'danger', 'N', '0', 'admin', NOW(), '成绩已锁定,需解冻后重传'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_score_status' AND dict_value = 'LOCKED');
|
||||
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 1, '实时上报', 'REALTIME', 'train_open_report_channel', 'primary', 'N', '0', 'admin', NOW(), '考试结束后即时上报'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_report_channel' AND dict_value = 'REALTIME');
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark)
|
||||
SELECT 2, '周期上报', 'PERIODIC', 'train_open_report_channel', 'info', 'Y', '0', 'admin', NOW(), '按时间窗口批量上报'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'train_open_report_channel' AND dict_value = 'PERIODIC');
|
||||
|
||||
-- ==============================================================================
|
||||
-- 9. 菜单与按钮权限
|
||||
-- ==============================================================================
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '外部实训接入', '2265', '9', 'openexam', NULL, '', '', 1, 0, 'M', '0', '0', '', 'education', 'admin', NOW(), '外部实训系统数据接口'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_name = '外部实训接入' AND parent_id = 2265);
|
||||
|
||||
SET @openexam_dir = (SELECT menu_id FROM sys_menu WHERE menu_name = '外部实训接入' AND parent_id = 2265 LIMIT 1);
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '应用管理', @openexam_dir, '1', 'openApp', 'train/openApp/index', '', '', 1, 0, 'C', '0', '0', 'train:openApp:list', 'component', 'admin', NOW(), '外部应用与授权范围'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:openApp:list');
|
||||
SET @openapp_menu = (SELECT menu_id FROM sys_menu WHERE perms = 'train:openApp:list' LIMIT 1);
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '考试科目', @openexam_dir, '2', 'examSubject', 'train/examSubject/index', '', '', 1, 0, 'C', '0', '0', 'train:examSubject:list', 'dict', 'admin', NOW(), '考试科目字典'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:examSubject:list');
|
||||
SET @examsubject_menu = (SELECT menu_id FROM sys_menu WHERE perms = 'train:examSubject:list' LIMIT 1);
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '上报成绩', @openexam_dir, '3', 'examReport', 'train/examReport/index', '', '', 1, 0, 'C', '0', '0', 'train:examReport:list', 'form', 'admin', NOW(), '外部实训上报成绩与待认领'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:examReport:list');
|
||||
SET @examreport_menu = (SELECT menu_id FROM sys_menu WHERE perms = 'train:examReport:list' LIMIT 1);
|
||||
|
||||
-- 按钮权限:应用管理
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '应用查询', @openapp_menu, '1', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:openApp:query', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:openApp:query');
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '应用新增', @openapp_menu, '2', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:openApp:add', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:openApp:add');
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '应用修改', @openapp_menu, '3', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:openApp:edit', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:openApp:edit');
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '应用删除', @openapp_menu, '4', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:openApp:remove', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:openApp:remove');
|
||||
|
||||
-- 按钮权限:考试科目
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '科目新增', @examsubject_menu, '1', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:examSubject:add', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:examSubject:add');
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '科目修改', @examsubject_menu, '2', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:examSubject:edit', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:examSubject:edit');
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '科目删除', @examsubject_menu, '3', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:examSubject:remove', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:examSubject:remove');
|
||||
|
||||
-- 按钮权限:上报成绩
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '成绩认领', @examreport_menu, '1', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:examReport:claim', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:examReport:claim');
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '成绩删除', @examreport_menu, '2', '', NULL, '', '', 1, 0, 'F', '0', '0', 'train:examReport:remove', '#', 'admin', NOW(), ''
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:examReport:remove');
|
||||
|
||||
-- ==============================================================================
|
||||
-- 10. 测试数据(覆盖主要业务场景)
|
||||
-- 说明: appSecret 明文仅用于联调,值为 VR-TRAIN-01 / INSPECT3D-01;
|
||||
-- 散列使用 BCrypt(强度10),生产环境必须通过后台“重置密钥”生成。
|
||||
-- ==============================================================================
|
||||
INSERT INTO `train_open_app` (`app_id`, `app_name`, `app_type`, `org_name`, `app_secret_hash`, `app_secret_masked`, `contact_name`, `contact_phone`, `rate_limit`, `realtime_enabled`, `status`, `create_by`, `remark`)
|
||||
VALUES
|
||||
('VR-TRAIN-01', 'VR 安全实训系统', 'VR', '沈阳房建公寓段·职工培训基地',
|
||||
'$2a$10$7EqJtq98hPqEX7fNZaFWoO5uY3vJ0N2m1oQxZ0lQm3Qm9C2jK1sGa', '****2406', '王工', '13900000001', 20, 1, 1, 'admin', '联调样例:VR 实训'),
|
||||
('INSPECT3D-01', '3D 数字孪生巡检系统', 'INSPECT3D', '沈阳房建公寓段',
|
||||
'$2a$10$7EqJtq98hPqEX7fNZaFWoO5uY3vJ0N2m1oQxZ0lQm3Qm9C2jK1sGa', '****7788', '李工', '13900000002', 20, 0, 1, 'admin', '联调样例:3D 巡检(未开启实时上报)')
|
||||
ON DUPLICATE KEY UPDATE `app_name` = VALUES(`app_name`);
|
||||
|
||||
INSERT INTO `train_open_app_scope` (`app_id`, `scope_type`, `scope_value`, `create_by`)
|
||||
SELECT 'VR-TRAIN-01', 'CLASS', 'P2026001', 'admin' FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `train_open_app_scope` WHERE `app_id` = 'VR-TRAIN-01' AND `scope_type` = 'CLASS' AND `scope_value` = 'P2026001');
|
||||
|
||||
INSERT INTO `train_open_app_scope` (`app_id`, `scope_type`, `scope_value`, `create_by`)
|
||||
SELECT 'VR-TRAIN-01', 'SUBJECT', 'VR-SAFE', 'admin' FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `train_open_app_scope` WHERE `app_id` = 'VR-TRAIN-01' AND `scope_type` = 'SUBJECT' AND `scope_value` = 'VR-SAFE');
|
||||
|
||||
INSERT INTO `train_open_app_scope` (`app_id`, `scope_type`, `scope_value`, `create_by`)
|
||||
SELECT 'INSPECT3D-01', 'SUBJECT', 'CAP-POS', 'admin' FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `train_open_app_scope` WHERE `app_id` = 'INSPECT3D-01' AND `scope_type` = 'SUBJECT' AND `scope_value` = 'CAP-POS');
|
||||
|
||||
INSERT INTO `train_exam_subject` (`subject_code`, `subject_name`, `plan_id`, `status`, `sort_no`, `create_by`, `remark`)
|
||||
VALUES
|
||||
('VR-SAFE', '高处作业隐患排查', 'P2026001', 1, 1, 'admin', 'VR 实训科目'),
|
||||
('CAP-POS', '帽石安装定位', NULL, 1, 2, 'admin', '帽石考核科目'),
|
||||
('OPS-ELEC', '营业线施工用电安全', 'P2026001', 1, 3, 'admin', '实操科目'),
|
||||
('INS-ROOF', '屋面渗漏巡检', NULL, 0, 4, 'admin', '停用科目,用于校验 3001')
|
||||
ON DUPLICATE KEY UPDATE `subject_name` = VALUES(`subject_name`);
|
||||
|
||||
-- 参训档案(成绩匹配依赖):存在则跳过
|
||||
INSERT INTO `train_enrollment` (`plan_id`, `student_id`, `source_type`, `enrollment_status`, `accommodation_required`, `join_time`, `create_by`)
|
||||
SELECT 'P2026001', 'S20260001', 1, 1, 0, NOW(), 'admin' FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `train_enrollment` WHERE `plan_id` = 'P2026001' AND `student_id` = 'S20260001');
|
||||
|
||||
-- 批次(正常周期上报)
|
||||
INSERT INTO `train_exam_report_batch` (`batch_pk`, `app_id`, `batch_id`, `report_time`, `period_start`, `period_end`, `resend`, `total`, `accepted`, `duplicated`, `unmatched`, `received_time`, `req_id`)
|
||||
VALUES (1, 'VR-TRAIN-01', 'BATCH-202610281030-0001', 1793154600000, 1793152800000, 1793154600000, 0, 2, 1, 0, 1, 1793154600180, 'RQ-SEED-0001')
|
||||
ON DUPLICATE KEY UPDATE `batch_id` = VALUES(`batch_id`);
|
||||
|
||||
-- 上报记录:已匹配已归档 / 待认领 / 已锁定 / 重复批次内的幂等样例
|
||||
INSERT INTO `train_exam_report` (`report_id`, `app_id`, `batch_pk`, `batch_id`, `report_channel`, `biz_type`, `student_id`, `student_name`,
|
||||
`id_card_masked`, `id_card_md5`, `matched_enrollment_id`, `matched_plan_id`, `match_status`, `score_status`,
|
||||
`subject_code`, `subject_name`, `exam_start_time`, `exam_end_time`, `duration_seconds`, `score`, `full_score`,
|
||||
`pass_flag`, `retake`, `retake_no`, `station`, `content`, `content_size`, `content_md5`,
|
||||
`report_time`, `received_time`, `resend`, `archive_time`)
|
||||
VALUES
|
||||
('RPT-VR-20261028-0031', 'VR-TRAIN-01', 1, 'BATCH-202610281030-0001', 'PERIODIC', 'PRACTICE', 'S20260001', '张三',
|
||||
'210102********4321', '5d41402abc4b2a76b9719d911017c592',
|
||||
(SELECT enrollment_id FROM `train_enrollment` WHERE `student_id` = 'S20260001' ORDER BY join_time DESC LIMIT 1),
|
||||
'P2026001', 'MATCHED', 'ARCHIVED',
|
||||
'VR-SAFE', '高处作业隐患排查', 1793153580000, 1793154000000, 420, 86.50, 100.00,
|
||||
1, 0, 0, 'VR-A-02', '{"taskId":"VR-SAFE-012"}', 24, '0e4b1c9d8f2a7c6b5d4e3f2a1b0c9d8e',
|
||||
1793154600000, 1793154600180, 0, NOW()),
|
||||
('RPT-CAP-20261028-0032', 'VR-TRAIN-01', 1, 'BATCH-202610281030-0001', 'PERIODIC', 'PRACTICE', 'S20240399', '张伟',
|
||||
NULL, NULL, NULL, NULL, 'UNMATCHED', 'RECEIVED',
|
||||
'CAP-POS', '帽石安装定位', 1793153820000, 1793154180000, 360, 91.00, 100.00,
|
||||
1, 1, 1, 'MS-A-03', '{"taskId":"CAP-07"}', 20, '1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d',
|
||||
1793154600000, 1793154600180, 0, NULL),
|
||||
('RPT-VR-20261028-0033', 'VR-TRAIN-01', NULL, NULL, 'REALTIME', 'PRACTICE', 'S20260001', '张三',
|
||||
NULL, NULL,
|
||||
(SELECT enrollment_id FROM `train_enrollment` WHERE `student_id` = 'S20260001' ORDER BY join_time DESC LIMIT 1),
|
||||
'P2026001', 'MATCHED', 'LOCKED',
|
||||
'VR-SAFE', '高处作业隐患排查', 1793153400000, 1793153760000, 360, 92.00, 100.00,
|
||||
1, 0, 0, 'VR-A-03', '{"taskId":"VR-SAFE-012"}', 24, '2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e',
|
||||
1793154010000, 1793154010180, 0, NOW()),
|
||||
('RPT-INS-20261028-0034', 'INSPECT3D-01', NULL, NULL, 'REALTIME', 'PRACTICE', 'S20260001', '张三',
|
||||
NULL, NULL,
|
||||
(SELECT enrollment_id FROM `train_enrollment` WHERE `student_id` = 'S20260001' ORDER BY join_time DESC LIMIT 1),
|
||||
'P2026001', 'MATCHED', 'RECEIVED',
|
||||
'INS-ROOF', '屋面渗漏巡检', 1793153640000, 1793154000000, 360, 88.00, 100.00,
|
||||
1, 0, 0, 'INS-A-01', '{"taskId":"INS-01"}', 18, '3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f',
|
||||
1793154010000, 1793154010180, 0, NULL)
|
||||
ON DUPLICATE KEY UPDATE `subject_name` = VALUES(`subject_name`);
|
||||
|
||||
-- 令牌审计样例(无效散列,仅用于页面联调查看)
|
||||
INSERT INTO `train_open_token` (`app_id`, `token_hash`, `issued_time`, `expire_time`, `status`, `client_ip`, `user_agent`)
|
||||
SELECT 'VR-TRAIN-01', SHA2('sample-token-for-ui-preview', 256), NOW(), DATE_ADD(NOW(), INTERVAL 7200 SECOND), 0, '127.0.0.1', 'seed'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `train_open_token` WHERE `app_id` = 'VR-TRAIN-01' AND `status` = 0);
|
||||
|
||||
-- 调用日志样例
|
||||
INSERT INTO `train_open_api_log` (`req_id`, `app_id`, `api_path`, `http_method`, `biz_key`, `code`, `msg`, `cost_ms`, `client_ip`)
|
||||
SELECT 'RQ-SEED-0001', 'VR-TRAIN-01', '/api/open/v1/exam/report', 'POST', 'BATCH-202610281030-0001', 0, 'success', 38, '127.0.0.1'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `train_open_api_log` WHERE `req_id` = 'RQ-SEED-0001');
|
||||
|
||||
|
||||
-- 更新学生学号字段 2026-11-05
|
||||
UPDATE train_student
|
||||
SET person_no = REPLACE(person_no, '学号:', '')
|
||||
WHERE person_no LIKE '%学号:%';
|
||||
@@ -0,0 +1,18 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库 V2 增量脚本: 024_add_teacher_enrollment_status.sql
|
||||
-- 目标: 为 train_teacher 增加 enrollment_status(参训状态)
|
||||
-- 枚举: 0 待报到、1 已报到、2 请假、3 放弃、4 已完成(实际业务主要使用 0 / 1)
|
||||
-- 说明: 新增教师默认“待报到(0)”,与 Java 层 TrainTeacherServiceImpl 的默认值保持一致
|
||||
-- 适用数据库: KingbaseES V009R003C018 / PostgreSQL (schema: public)
|
||||
-- 幂等性: ADD COLUMN IF NOT EXISTS 可重复执行;已存在时仅重复写一次列注释
|
||||
-- 注意: 未额外添加 CHECK 约束,枚举范围由应用层校验(如需库级约束请另行评估存量数据)
|
||||
-- ==============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE public.train_teacher
|
||||
ADD COLUMN IF NOT EXISTS enrollment_status SMALLINT NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN public.train_teacher.enrollment_status IS '参训状态 (0:待报到, 1:已报到, 2:请假, 3:放弃, 4:已完成)';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本 (MySQL 5.7/8.0): 024b_add_teacher_enrollment_status_mysql.sql
|
||||
-- 目标: 为 train_teacher 增加 enrollment_status(参训状态)
|
||||
-- 枚举: 0 待报到、1 已报到、2 请假、3 放弃、4 已完成(实际业务主要使用 0 / 1)
|
||||
-- 说明: 024_add_teacher_enrollment_status.sql 为 KingbaseES/PostgreSQL 版本,MySQL 请使用本脚本
|
||||
-- 幂等: 通过 information_schema 判断列是否已存在,可重复执行
|
||||
-- 执行:
|
||||
-- mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 024b_add_teacher_enrollment_status_mysql.sql
|
||||
-- ==============================================================================
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'train_teacher'
|
||||
AND COLUMN_NAME = 'enrollment_status'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE `train_teacher` ADD COLUMN `enrollment_status` SMALLINT NOT NULL DEFAULT 0 COMMENT ''参训状态 (0:待报到, 1:已报到, 2:请假, 3:放弃, 4:已完成)''',
|
||||
'SELECT ''train_teacher.enrollment_status 已存在,跳过加列'' AS msg');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT COUNT(*) AS total_rows,
|
||||
SUM(enrollment_status = 0) AS pending_rows,
|
||||
SUM(enrollment_status = 1) AS reported_rows
|
||||
FROM `train_teacher`;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库 V2 增量脚本: 025_add_teacher_join_time_accommodation.sql
|
||||
-- 目标: 为 train_teacher 增加 join_time(报到时间)与 accommodation_required(是否需要住宿)
|
||||
-- 定义来源: 与 train_enrollment 对应字段保持一致(见 002_create_new_tables.sql:84-85)
|
||||
-- train_enrollment.join_time TIMESTAMP (可空,无默认值)
|
||||
-- train_enrollment.accommodation_required SMALLINT NOT NULL (不可空,无默认值)
|
||||
-- 兼容处理: 存量教师无该信息,先补 0(不需要住宿)再置为 NOT NULL;最终形态不带 DEFAULT,
|
||||
-- 与 train_enrollment 的“业务选择型字段不设默认值”口径一致
|
||||
-- 适用数据库: KingbaseES V009R003C018 / PostgreSQL (schema: public)
|
||||
-- 幂等性: ADD COLUMN IF NOT EXISTS + 幂等 UPDATE / SET NOT NULL,可重复执行
|
||||
-- ==============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. 报到时间(可空,无默认值)
|
||||
ALTER TABLE public.train_teacher
|
||||
ADD COLUMN IF NOT EXISTS join_time TIMESTAMP;
|
||||
|
||||
-- 2. 是否需要住宿(先允许为空以便加列,回填后再置为 NOT NULL,最终不留默认值)
|
||||
ALTER TABLE public.train_teacher
|
||||
ADD COLUMN IF NOT EXISTS accommodation_required SMALLINT;
|
||||
|
||||
UPDATE public.train_teacher
|
||||
SET accommodation_required = 0
|
||||
WHERE accommodation_required IS NULL;
|
||||
|
||||
ALTER TABLE public.train_teacher
|
||||
ALTER COLUMN accommodation_required SET NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN public.train_teacher.join_time IS '报到时间';
|
||||
COMMENT ON COLUMN public.train_teacher.accommodation_required IS '是否需要住宿 (0:否, 1:是)';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,49 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本 (MySQL 5.7/8.0): 025b_add_teacher_join_time_accommodation_mysql.sql
|
||||
-- 目标: 为 train_teacher 增加 join_time(报到时间)与 accommodation_required(是否需要住宿)
|
||||
-- 定义来源: 与 train_enrollment 对应字段保持一致
|
||||
-- join_time DATETIME,可空,无默认值
|
||||
-- accommodation_required SMALLINT NOT NULL,无默认值(加列时临时带 DEFAULT 0 回填,随后去掉默认值)
|
||||
-- 说明: 025_add_teacher_join_time_accommodation.sql 为 KingbaseES/PostgreSQL 版本,MySQL 请使用本脚本
|
||||
-- 幂等: 通过 information_schema 判断列是否已存在,可重复执行
|
||||
-- 执行:
|
||||
-- mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 025b_add_teacher_join_time_accommodation_mysql.sql
|
||||
-- ==============================================================================
|
||||
|
||||
-- 1. join_time(可空)
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'train_teacher'
|
||||
AND COLUMN_NAME = 'join_time'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE `train_teacher` ADD COLUMN `join_time` DATETIME NULL DEFAULT NULL COMMENT ''报到时间''',
|
||||
'SELECT ''train_teacher.join_time 已存在,跳过加列'' AS msg');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. accommodation_required(不可空;先带默认值加列以回填存量行)
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'train_teacher'
|
||||
AND COLUMN_NAME = 'accommodation_required'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE `train_teacher` ADD COLUMN `accommodation_required` SMALLINT NOT NULL DEFAULT 0 COMMENT ''是否需要住宿 (0:否, 1:是)''',
|
||||
'SELECT ''train_teacher.accommodation_required 已存在,跳过加列'' AS msg');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 3. 回填并去掉默认值,与 train_enrollment 口径一致(业务选择型字段不设默认值)
|
||||
UPDATE `train_teacher` SET `accommodation_required` = 0 WHERE `accommodation_required` IS NULL;
|
||||
ALTER TABLE `train_teacher`
|
||||
MODIFY COLUMN `accommodation_required` SMALLINT NOT NULL COMMENT '是否需要住宿 (0:否, 1:是)';
|
||||
|
||||
SELECT COUNT(*) AS total_rows,
|
||||
SUM(accommodation_required = 1) AS need_accommodation_rows,
|
||||
SUM(join_time IS NOT NULL) AS joined_rows
|
||||
FROM `train_teacher`;
|
||||
@@ -0,0 +1,144 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本: 026_train_theory_exam_score.sql
|
||||
-- 模块: 班级学员理论考试成绩(与外部实训成绩汇总配套)
|
||||
-- 内容: 单表建表语句(字段 + 主键 + 唯一约束 + 外键 + CHECK + 索引 + 字段注释)
|
||||
-- 数据库: MySQL 5.7(生产主库);KingbaseES 差异见文末说明
|
||||
-- 依赖: 本脚本仅创建 1 张新表,其引用的父表在库中**必须已存在**(顺序不可颠倒):
|
||||
-- train_student(s_id)
|
||||
-- train_class_plan(plan_id)
|
||||
-- train_course(c_id)
|
||||
-- train_plan_course(plan_course_id)
|
||||
-- train_enrollment(enrollment_id)
|
||||
-- train_exam_subject(subject_code) -- 由 023_open_exam_api.sql 创建
|
||||
-- train_score_item(score_item_id)
|
||||
-- 说明:
|
||||
-- 1) 脚本可重复执行:建表用 IF NOT EXISTS;表已存在时不会重复创建(约束/索引随表创建一并生效)
|
||||
-- 2) MySQL 5.7 会解析但**不强制执行** CHECK 约束,8.0 起强制执行;无论版本,
|
||||
-- 应用层(DTO + Service)均须做同等校验兜底
|
||||
-- 3) 仅新增对象,不改动任何既有表结构与数据
|
||||
-- 4) 唯一键涉及 VARCHAR 列,父表对应列(s_id/plan_id/c_id/subject_code)的字符集与排序规则
|
||||
-- 必须与本表一致(utf8mb4 / utf8mb4_general_ci),否则外键创建会失败
|
||||
-- 执行: mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 026_train_theory_exam_score.sql
|
||||
-- ==============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 班级学员理论考试成绩表
|
||||
-- 用途:
|
||||
-- 记录"某学员 × 某班级课程 × 某次考试"的理论考试成绩,是课程级理论成绩的权威明细。
|
||||
-- 与外部实训成绩上报表 train_exam_report(biz_type='PRACTICE')配套使用:
|
||||
-- · 强关联:train_theory_exam_score.enrollment_id = train_exam_report.matched_enrollment_id
|
||||
-- · 弱关联:student_id + subject_code(实训上报未匹配到学员时使用)
|
||||
-- · 汇总桥接:score_item_id → train_score_item(item_type=1 理论),
|
||||
-- 与实训成绩按权重合成后写入 train_student_score(综合成绩明细)
|
||||
-- 粒度: enrollment_id + plan_course_id + retake_no 唯一(一行 = 一次考试)
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `train_theory_exam_score` (
|
||||
-- ========== 主键与关联维度 ==========
|
||||
`theory_score_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`enrollment_id` BIGINT NOT NULL COMMENT '参训档案ID(train_enrollment,学员×班级唯一事实,汇总首选关联键)',
|
||||
`plan_id` VARCHAR(32) NOT NULL COMMENT '培训班ID(train_class_plan,冗余便于班级维度统计)',
|
||||
`plan_course_id` BIGINT NOT NULL COMMENT '培训课程实例ID(train_plan_course,哪门课,课程维度统计键)',
|
||||
`c_id` VARCHAR(32) NOT NULL COMMENT '课程主档ID(train_course,冗余支持跨班级横向对比)',
|
||||
`student_id` VARCHAR(32) NOT NULL COMMENT '学员ID(train_student.s_id,与实训上报 student_id 对齐)',
|
||||
`subject_code` VARCHAR(32) DEFAULT NULL COMMENT '理论科目编码(train_exam_subject,可与实训上报 subject_code 对齐;空表示不区分科目)',
|
||||
|
||||
-- ========== 考试过程信息 ==========
|
||||
`exam_start_time` DATETIME DEFAULT NULL COMMENT '考试开始时间(本地时间)',
|
||||
`exam_end_time` DATETIME DEFAULT NULL COMMENT '考试结束时间(须晚于开始时间)',
|
||||
`duration_seconds` INT DEFAULT NULL COMMENT '用时秒数(对齐 train_exam_report.duration_seconds)',
|
||||
`retake` SMALLINT NOT NULL DEFAULT 0 COMMENT '是否补考(1是 0否,语义对齐 train_exam_report.retake)',
|
||||
`retake_no` INT NOT NULL DEFAULT 0 COMMENT '补考序号(0正考,1..n 第n次补考,对齐 train_exam_report.retake_no)',
|
||||
|
||||
-- ========== 成绩信息 ==========
|
||||
`exam_status` VARCHAR(16) NOT NULL DEFAULT 'NORMAL' COMMENT '考试状态(NORMAL正常 / ABSENT缺考 / EXEMPT免修)',
|
||||
`score` DECIMAL(7,2) DEFAULT NULL COMMENT '理论成绩(缺考/免修允许为空,避免以0分污染均值)',
|
||||
`full_score` DECIMAL(7,2) NOT NULL DEFAULT 100.00 COMMENT '满分(默认100.00,对齐 train_exam_report.full_score)',
|
||||
`pass_flag` SMALLINT NOT NULL DEFAULT 0 COMMENT '是否通过(1通过 0未通过;免修按通过、缺考按未通过)',
|
||||
`is_final` SMALLINT NOT NULL DEFAULT 1 COMMENT '是否该学员该课程的最终成绩(1是 0否,汇总仅取最终成绩避免重复计算)',
|
||||
`score_status` VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '成绩状态(RECEIVED已接收 / ARCHIVED已归档 / LOCKED已锁定,枚举对齐 train_exam_report.score_status)',
|
||||
|
||||
-- ========== 来源、汇总桥接与审计 ==========
|
||||
`source_type` SMALLINT NOT NULL DEFAULT 1 COMMENT '成绩来源(1手工录入 / 2平台在线考试 / 3批量或外部导入)',
|
||||
`source_system` VARCHAR(100) DEFAULT NULL COMMENT '来源系统标识(对接外部题库/考试平台时使用)',
|
||||
`source_event_id` VARCHAR(100) DEFAULT NULL COMMENT '外部事件唯一ID(导入幂等键,与 source_system 组成唯一约束)',
|
||||
`score_item_id` BIGINT DEFAULT NULL COMMENT '关联综合成绩项(train_score_item,应指向 item_type=1 理论考试项)',
|
||||
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注(如缺考原因、免修依据)',
|
||||
`del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 2删除),逻辑删除保留审计轨迹',
|
||||
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
|
||||
-- 最终成绩唯一性辅助列(MySQL 无部分唯一索引,用生成列模拟:
|
||||
-- 仅"未删除且 is_final=1"的行取值为 1,其余为 NULL;MySQL 唯一索引允许多个 NULL)
|
||||
`final_flag` TINYINT GENERATED ALWAYS AS (IF(`is_final` = 1 AND `del_flag` = '0', 1, NULL)) STORED COMMENT '最终成绩唯一约束辅助列(实现细节,业务不直接读写)',
|
||||
|
||||
-- ========== 主键 ==========
|
||||
PRIMARY KEY (`theory_score_id`),
|
||||
|
||||
-- ========== 唯一约束 ==========
|
||||
UNIQUE KEY `uk_theory_enroll_course_retake` (`enrollment_id`, `plan_course_id`, `retake_no`)
|
||||
COMMENT '同一学员同一课程同一次考试唯一,防重复录入',
|
||||
UNIQUE KEY `uk_theory_final` (`plan_course_id`, `student_id`, `final_flag`)
|
||||
COMMENT '每学员每课程仅一条最终成绩(生成列模拟部分唯一索引)',
|
||||
UNIQUE KEY `uk_theory_source_event` (`source_system`, `source_event_id`)
|
||||
COMMENT '外部导入幂等(两列均为 NULL 时不冲突,不影响手工录入)',
|
||||
|
||||
-- ========== 普通索引 ==========
|
||||
KEY `idx_theory_plan_student` (`plan_id`, `student_id`) COMMENT '班级成绩单、按学员查全班成绩',
|
||||
KEY `idx_theory_plan_course_final` (`plan_course_id`, `is_final`, `score_status`, `del_flag`) COMMENT '课程维度汇总(均分/及格率)',
|
||||
KEY `idx_theory_student_subject` (`student_id`, `subject_code`, `exam_end_time`) COMMENT '与实训上报按"学员+科目"汇总',
|
||||
KEY `idx_theory_score_item` (`score_item_id`) COMMENT '汇入 train_student_score 时反查',
|
||||
KEY `idx_theory_exam_time` (`exam_end_time`) COMMENT '时间窗统计/归档任务扫描',
|
||||
KEY `idx_theory_course` (`c_id`) COMMENT '外键索引:按课程主档横向统计',
|
||||
KEY `idx_theory_subject` (`subject_code`) COMMENT '外键索引:按科目字典汇总',
|
||||
|
||||
-- ========== 外键(引用完整性) ==========
|
||||
CONSTRAINT `fk_theory_enrollment` FOREIGN KEY (`enrollment_id`) REFERENCES `train_enrollment` (`enrollment_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT `fk_theory_plan` FOREIGN KEY (`plan_id`) REFERENCES `train_class_plan` (`plan_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT `fk_theory_plan_course` FOREIGN KEY (`plan_course_id`) REFERENCES `train_plan_course` (`plan_course_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT `fk_theory_course` FOREIGN KEY (`c_id`) REFERENCES `train_course` (`c_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT `fk_theory_student` FOREIGN KEY (`student_id`) REFERENCES `train_student` (`s_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT `fk_theory_subject` FOREIGN KEY (`subject_code`) REFERENCES `train_exam_subject` (`subject_code`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT `fk_theory_score_item` FOREIGN KEY (`score_item_id`) REFERENCES `train_score_item` (`score_item_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
|
||||
-- ========== 取值与一致性约束(MySQL 8.0 起强制;5.7 由应用层兜底) ==========
|
||||
CONSTRAINT `chk_theory_score_range` CHECK (`score` IS NULL OR (`score` >= 0 AND `score` <= `full_score`)),
|
||||
CONSTRAINT `chk_theory_full_score` CHECK (`full_score` > 0),
|
||||
CONSTRAINT `chk_theory_score_filled` CHECK (`exam_status` <> 'NORMAL' OR `score` IS NOT NULL),
|
||||
CONSTRAINT `chk_theory_exam_time` CHECK (`exam_start_time` IS NULL OR `exam_end_time` IS NULL OR `exam_end_time` > `exam_start_time`),
|
||||
CONSTRAINT `chk_theory_duration` CHECK (`duration_seconds` IS NULL OR `duration_seconds` >= 0),
|
||||
CONSTRAINT `chk_theory_retake` CHECK (`retake` IN (0, 1)),
|
||||
CONSTRAINT `chk_theory_retake_no` CHECK (`retake_no` >= 0),
|
||||
CONSTRAINT `chk_theory_exam_status` CHECK (`exam_status` IN ('NORMAL', 'ABSENT', 'EXEMPT')),
|
||||
CONSTRAINT `chk_theory_score_status` CHECK (`score_status` IN ('RECEIVED', 'ARCHIVED', 'LOCKED')),
|
||||
CONSTRAINT `chk_theory_pass_flag` CHECK (`pass_flag` IN (0, 1)),
|
||||
CONSTRAINT `chk_theory_is_final` CHECK (`is_final` IN (0, 1)),
|
||||
CONSTRAINT `chk_theory_source_type` CHECK (`source_type` IN (1, 2, 3)),
|
||||
CONSTRAINT `chk_theory_del_flag` CHECK (`del_flag` IN ('0', '2'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
COMMENT='班级学员理论考试成绩表(学员×班级课程×考试次数的理论成绩明细,与 train_exam_report 实训成绩汇总)';
|
||||
|
||||
-- ==============================================================================
|
||||
-- 补充说明
|
||||
-- 1) 冗余维度一致性:enrollment_id / plan_id / student_id / c_id 之间存在派生关系
|
||||
-- (enrollment → plan + student;plan_course → plan + c_id)。外键仅保证各自引用有效,
|
||||
-- 四者同源须由应用层(Service 写入前校验)或库内触发器保证,避免"A班学员成绩挂B班课程"。
|
||||
-- 2) 汇总统计口径(报表层,不在本脚本落地):
|
||||
-- 仅统计 del_flag='0' AND is_final=1 AND score_status IN ('RECEIVED','ARCHIVED');
|
||||
-- 缺考(ABSENT)不计入平均分分母,免修(EXEMPT)计入通过率但不计分;
|
||||
-- 补考取 is_final=1 的成绩,避免同课程多行重复计入。
|
||||
-- 3) 时间口径:本表为 DATETIME(本地时间),train_exam_report 为 BIGINT 毫秒;
|
||||
-- 跨表汇总时统一转换为同一基准(建议毫秒整数)。
|
||||
-- 4) KingbaseES/PostgreSQL 落地差异:
|
||||
-- · 主键改为 BIGINT GENERATED BY DEFAULT AS IDENTITY;
|
||||
-- · 表/列注释改用 COMMENT ON TABLE / COMMENT ON COLUMN;
|
||||
-- · 去掉 final_flag 生成列,改用部分唯一索引:
|
||||
-- CREATE UNIQUE INDEX uk_theory_final ON train_theory_exam_score (plan_course_id, student_id)
|
||||
-- WHERE is_final = 1 AND del_flag = '0';
|
||||
-- · CHECK 约束原生强制;DATETIME 对应 TIMESTAMP。
|
||||
-- 5) 回滚(如需):
|
||||
-- DROP TABLE IF EXISTS `train_theory_exam_score`;
|
||||
-- ==============================================================================
|
||||
@@ -0,0 +1,49 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem 数据库增量脚本: 028_score_analysis_menu.sql
|
||||
-- 模块: 成绩统计分析(理论成绩 + 实训上报成绩,多维度统计)
|
||||
-- 内容: 菜单与按钮权限(复用「统计分析」父目录 2169),并授权给培训管理员角色
|
||||
-- 数据库: MySQL(生产主库);KingbaseES 请按 012_analytics_v1_permissions.sql 的写法转换
|
||||
-- 说明:
|
||||
-- 1) 仅新增菜单/权限与角色授权,不改动任何既有表结构与数据;
|
||||
-- 2) 脚本可重复执行:全部使用 NOT EXISTS 幂等判断;
|
||||
-- 3) 页面组件路径: train/analysis/score/index
|
||||
-- 4) 后端接口前缀: /train/analysis/score(TrainScoreAnalysisController)
|
||||
-- 执行: mysql -h<host> -u<user> -p --default-character-set=utf8mb4 <db> < 028_score_analysis_menu.sql
|
||||
-- ==============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 父目录:统计分析(path=analysis,顶级菜单)
|
||||
SET @analysis_parent = (
|
||||
SELECT menu_id FROM sys_menu WHERE path = 'scores' AND parent_id = 0 LIMIT 1
|
||||
);
|
||||
|
||||
-- 1. 成绩分析页面菜单
|
||||
INSERT INTO sys_menu
|
||||
(menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '成绩分析', @analysis_parent, '2', 'score', 'train/analysis/score/index', '', '', 1, 0,
|
||||
'C', '0', '0', 'train:analysis:score', 'chart', 'admin', NOW(), '理论/实训成绩多维度统计分析'
|
||||
FROM DUAL
|
||||
WHERE @analysis_parent IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:analysis:score');
|
||||
|
||||
SET @score_menu = (SELECT menu_id FROM sys_menu WHERE perms = 'train:analysis:score' LIMIT 1);
|
||||
|
||||
-- 2. 成绩分析查询按钮权限
|
||||
INSERT INTO sys_menu
|
||||
(menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT '成绩分析查询', @score_menu, '1', '#', '', '', '', 1, 0,
|
||||
'F', '0', '0', 'train:analysis:score:query', '#', 'admin', NOW(), '成绩统计分析数据查询'
|
||||
FROM DUAL
|
||||
WHERE @score_menu IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'train:analysis:score:query');
|
||||
|
||||
-- 3. 授权给培训管理员角色(role_id = 3,与考勤分析保持一致)
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 3, m.menu_id FROM sys_menu m
|
||||
WHERE m.perms IN ('train:analysis:score', 'train:analysis:score:query')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 3 AND rm.menu_id = m.menu_id
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
-- 图书下载回执记录表(设备端下载完成后回执落库)
|
||||
CREATE TABLE IF NOT EXISTS `train_book_download_record` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '学员ID',
|
||||
`device_id` VARCHAR(64) NOT NULL COMMENT '设备ID',
|
||||
`book_id` VARCHAR(64) NOT NULL COMMENT '图书ID',
|
||||
`book_version` VARCHAR(32) DEFAULT NULL COMMENT '图书版本',
|
||||
`file_md5` VARCHAR(64) DEFAULT NULL COMMENT '文件MD5',
|
||||
`result` VARCHAR(16) DEFAULT NULL COMMENT '下载结果:SUCCESS/FAIL',
|
||||
`err_msg` VARCHAR(255) DEFAULT NULL COMMENT '错误信息',
|
||||
`download_time` DATETIME DEFAULT NULL COMMENT '下载/回执时间',
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_device_book` (`device_id`,`book_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书下载回执记录';
|
||||
@@ -0,0 +1,42 @@
|
||||
-- ============================================================================
|
||||
-- 教师住宿分配(复用 train_accommodation,与学生住宿逻辑隔离)
|
||||
--
|
||||
-- 1. 新增 occupant_type 区分入住人员类型:student 学员 / teacher 教师
|
||||
-- - 存量数据全部为学员住宿,默认值 'student' 保证学生逻辑完全不变
|
||||
-- - 教师住宿记录 occupant_type = 'teacher',enrollment_id 存 train_teacher.id
|
||||
-- 2. 删除 fk_accomm_enrollment 外键:教师住宿的 enrollment_id 不再指向
|
||||
-- train_enrollment(enrollment_id),改由业务层按 occupant_type 分别校验
|
||||
-- 3. 唯一索引由「全局活唯一(enrollment_id)」改为「按人员类型隔离的活唯一
|
||||
-- (occupant_type, enrollment_id)」,避免教师 id 与参训档案 id 数值碰撞
|
||||
--
|
||||
-- 适配数据库:Kingbase / PostgreSQL(train 模块以该语法为准)
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. 入住人员类型列
|
||||
ALTER TABLE train_accommodation ADD COLUMN occupant_type VARCHAR(16) NOT NULL DEFAULT 'student';
|
||||
|
||||
COMMENT ON COLUMN train_accommodation.occupant_type IS '入住人员类型:student 学员 / teacher 教师';
|
||||
|
||||
-- 2. 解除参训档案外键约束(教师住宿记录不指向 train_enrollment)
|
||||
ALTER TABLE train_accommodation DROP CONSTRAINT IF EXISTS fk_accomm_enrollment;
|
||||
|
||||
-- 3. 重建活跃唯一索引:按人员类型隔离
|
||||
DROP INDEX IF EXISTS uq_accommodation_enroll;
|
||||
|
||||
CREATE UNIQUE INDEX uq_accommodation_occupant
|
||||
ON train_accommodation (occupant_type, enrollment_id)
|
||||
WHERE accommodation_status IN (0, 1);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_accommodation_occupant
|
||||
ON train_accommodation (occupant_type, enrollment_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- MySQL 兼容说明(如需在 MySQL 环境执行,替换为下列语句):
|
||||
-- ALTER TABLE train_accommodation ADD COLUMN occupant_type VARCHAR(16) NOT NULL DEFAULT 'student'
|
||||
-- COMMENT '入住人员类型:student 学员 / teacher 教师';
|
||||
-- ALTER TABLE train_accommodation DROP FOREIGN KEY fk_accomm_enrollment;
|
||||
-- ALTER TABLE train_accommodation DROP INDEX uq_accommodation_enroll;
|
||||
-- CREATE UNIQUE INDEX uq_accommodation_occupant
|
||||
-- ON train_accommodation (occupant_type, enrollment_id, accommodation_status);
|
||||
-- (MySQL 无部分索引,若需「仅活跃唯一」请使用生成列方案,教师模块不依赖该唯一性做业务判断)
|
||||
-- ============================================================================
|
||||
@@ -0,0 +1,100 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem V2 -> MySQL 5.7 结构对账脚本 (reconcile_mysql_5_7.sql)
|
||||
-- 适用数据库: MySQL 5.7 (线上部署库)
|
||||
-- 目标: 将线上 MySQL 表结构对齐 V2 基线,消除两类反复出现的错误:
|
||||
-- 1) Field 'create_time' doesn't have a default value
|
||||
-- 2) Unknown column 'xxx' in 'field list' (缺失列)
|
||||
-- 说明:
|
||||
-- * 本脚本只"设置默认值 / 按需补列",**不改列类型**,对已有默认值的列为幂等操作。
|
||||
-- * 使用 information_schema 守卫,列已存在/已有默认也不会报错,可重复执行。
|
||||
-- * train_class_report 的 alloc_* 列: 应用当前 mapper 仍在 SELECT,故补回;
|
||||
-- 若后续按最终 V2 设计改为依赖 train_dorm_auth,应同步清理应用侧(见文件末尾说明)。
|
||||
-- 执行: mysql -u<user> -p<pass> railwaysystem < reconcile_mysql_5_7.sql
|
||||
-- ==============================================================================
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
-- 仅当列存在时设置默认值(列名/表名已存在即可,重复设置同值幂等)
|
||||
DROP PROCEDURE IF EXISTS set_default_if_missing $$
|
||||
CREATE PROCEDURE set_default_if_missing(
|
||||
IN p_tbl VARCHAR(64), IN p_col VARCHAR(64), IN p_def VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = p_tbl AND column_name = p_col
|
||||
) THEN
|
||||
SET @stmt = CONCAT('ALTER TABLE `', p_tbl, '` ALTER COLUMN `', p_col, '` SET DEFAULT ', p_def);
|
||||
PREPARE s FROM @stmt;
|
||||
EXECUTE s;
|
||||
DEALLOCATE PREPARE s;
|
||||
END IF;
|
||||
END $$
|
||||
|
||||
-- 仅当列不存在时新增列
|
||||
DROP PROCEDURE IF EXISTS add_col_if_missing $$
|
||||
CREATE PROCEDURE add_col_if_missing(
|
||||
IN p_tbl VARCHAR(64), IN p_col VARCHAR(64), IN p_spec VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = p_tbl AND column_name = p_col
|
||||
) THEN
|
||||
SET @stmt = CONCAT('ALTER TABLE `', p_tbl, '` ADD COLUMN `', p_col, '` ', p_spec);
|
||||
PREPARE s FROM @stmt;
|
||||
EXECUTE s;
|
||||
DEALLOCATE PREPARE s;
|
||||
END IF;
|
||||
END $$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 1) train_class_report: 补齐应用 mapper 仍在 SELECT 的 alloc_* 列 (若已加则跳过)
|
||||
-- ------------------------------------------------------------------------------
|
||||
CALL add_col_if_missing('train_class_report', 'alloc_dorm_id', 'VARCHAR(32) NULL');
|
||||
CALL add_col_if_missing('train_class_report', 'alloc_classroom_id', 'VARCHAR(32) NULL');
|
||||
CALL add_col_if_missing('train_class_report', 'alloc_site_id', 'VARCHAR(32) NULL');
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 2) 为所有 V2 业务表的 create_time 补默认值 (核心: 消除当前 'doesn''t have a default value')
|
||||
-- V2 基线均为 TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,线上 5.7 翻译时丢失。
|
||||
-- ------------------------------------------------------------------------------
|
||||
CALL set_default_if_missing('train_resource', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_course_resource', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_plan_course', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_enrollment', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_lesson', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_accommodation', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_score_item', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_student_score', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_class_report', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_dorm_auth', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_student_checkin', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_study_duration', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_student_portrait', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_electronic_board', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_classroom_info', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_training_site', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_dormitory_info', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_class_plan', 'create_time', 'CURRENT_TIMESTAMP');
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 3) 其它 V2 中 NOT NULL 且带默认值、但线上可能丢失的列
|
||||
-- ------------------------------------------------------------------------------
|
||||
CALL set_default_if_missing('train_resource', 'status', '1');
|
||||
CALL set_default_if_missing('train_resource', 'del_flag', '''0''');
|
||||
CALL set_default_if_missing('train_enrollment', 'enrollment_status', '0');
|
||||
CALL set_default_if_missing('train_accommodation', 'assigned_time', 'CURRENT_TIMESTAMP');
|
||||
CALL set_default_if_missing('train_accommodation', 'accommodation_status', '0');
|
||||
CALL set_default_if_missing('train_score_item', 'item_type', '1');
|
||||
CALL set_default_if_missing('train_score_item', 'weight', '0.00');
|
||||
CALL set_default_if_missing('train_score_item', 'full_score', '100.00');
|
||||
CALL set_default_if_missing('train_student_score', 'import_time', 'CURRENT_TIMESTAMP');
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 清理临时存储过程
|
||||
-- ------------------------------------------------------------------------------
|
||||
DROP PROCEDURE IF EXISTS set_default_if_missing;
|
||||
DROP PROCEDURE IF EXISTS add_col_if_missing;
|
||||
Reference in new issue
Block a user