-- ============================================================================== -- 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 -u -p --default-character-set=utf8mb4 < 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 '%学号:%';