Files
railwaysystem/RuoYi-Vue/sql/train/v2/023_open_exam_api.sql
T

413 lines
34 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ==============================================================================
-- 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 '%学号:%';