# 报到机接口文档(TrainRegisterApiController) > **服务模块**:`ruoyi-admin` > **控制器**:`com.ruoyi.web.controller.api.TrainRegisterApiController` > **接口前缀**:`/api/register/v1` > **文档依据**:控制器源码(截至 2026-11-09),字段口径以对应 VO / 数据库字段为准 > **适用对象**:报到机(自助终端)厂商、联调与测试人员 --- ## 目录 - [1. 通用说明](#1-通用说明) - [1.1 请求地址](#11-请求地址) - [1.2 鉴权与请求头](#12-鉴权与请求头) - [1.3 统一响应结构](#13-统一响应结构) - [1.4 状态码与错误约定](#14-状态码与错误约定) - [1.5 时间与命名约定](#15-时间与命名约定) - [2. 接口总览](#2-接口总览) - [3. 学员信息模块](#3-学员信息模块) - [3.1 获取学员信息列表](#31-获取学员信息列表) - [3.2 获取单个学员详情](#32-获取单个学员详情) - [3.3 获取学员基础信息](#33-获取学员基础信息) - [3.4 获取学员关联资源](#34-获取学员关联资源) - [4. 教师信息模块](#4-教师信息模块) - [4.1 获取教师信息列表](#41-获取教师信息列表) - [4.2 获取教师基础信息](#42-获取教师基础信息) - [5. 班级模块](#5-班级模块) - [5.1 获取进行中班级的未报到学员名单](#51-获取进行中班级的未报到学员名单) - [6. 宿舍模块](#6-宿舍模块) - [6.1 获取宿舍布局信息](#61-获取宿舍布局信息) - [7. 课表模块](#7-课表模块) - [7.1 获取课程安排(按日期 + 课程类型)](#71-获取课程安排按日期--课程类型) - [8. 人脸模块](#8-人脸模块) - [8.1 上传学员人脸照片](#81-上传学员人脸照片) - [8.2 上传教师人脸照片](#82-上传教师人脸照片) - [8.3 上传人脸照片(按员工标识自动分发)](#83-上传人脸照片按员工标识自动分发) - [9. 报到管理模块](#9-报到管理模块) - [9.1 报到重置](#91-报到重置) - [10. 附录](#10-附录) - [10.1 枚举值速查](#101-枚举值速查) - [10.2 关联数据表](#102-关联数据表) --- ## 1. 通用说明 ### 1.1 请求地址 | 项 | 值 | |---|---| | 协议 | HTTP(生产建议由网关承载 HTTPS) | | Host | 部署服务器地址与端口,例如 `http://192.168.1.202:8080` | | Base Path | `/api/register/v1` | | 完整示例 | `http://192.168.1.202:8080/api/register/v1/student/list` | ### 1.2 鉴权与请求头 - `SecurityConfig` 中已将 `/api/register/v1/**` 配置为 `permitAll`,**当前所有接口均无需登录令牌**(控制器类注释中提到的 Bearer 鉴权已不再生效)。 - 若生产环境在网关/边缘设备层增加了设备签名(如 HMAC)校验,以网关约定为准,不影响本文档的接口定义。 - 请求头: | 场景 | Header | 说明 | |---|---|---| | 所有请求 | `Accept: application/json` | 可选 | | POST 请求 | `Content-Type: application/json` | **必填**,请求体为 JSON | | 文件/人脸 | 无 | 人脸图片以 base64 字符串放在 JSON 请求体内,不使用 multipart | ### 1.3 统一响应结构 除个别特殊说明外,全部接口返回 `AjaxResult`: ```json { "code": 200, "msg": "操作成功", "data": {} } ``` | 字段 | 类型 | 说明 | |---|---|---| | code | int | 业务状态码,见 1.4 | | msg | string | 提示信息,成功时为“操作成功”,失败时为错误描述 | | data | any | 业务数据;失败时可能不存在 | > 注意:`data` 为 `null` 时部分序列化配置下该字段可能被省略,客户端应做空值兜底。 ### 1.4 状态码与错误约定 | code | 含义 | 典型场景 | |---|---|---| | 200 | 成功 | 正常返回 | | 500 | 通用业务/参数错误 | 参数缺失、图片解析失败、文件写入失败、业务校验不通过 | | 601 | 警告 | 预留,本文档接口未使用 | | 40402 | 学员不存在 | `GET /student/{sId}` 及依赖学员主档的接口(`StudentErrorCode.STUDENT_NOT_FOUND`) | | 401 | 未认证 | 当前 `/api/register/v1/**` 已放行,正常不会出现 | **重要约定**:业务异常由全局异常处理器统一捕获,**HTTP 状态码固定为 200**,调用方必须依据响应体中的 `code` 判断成败,不能只看 HTTP 状态。 ### 1.5 时间与命名约定 - 服务端 Jackson 时区为 `GMT+8`,日期时间统一格式 `yyyy-MM-dd HH:mm:ss`,纯日期格式 `yyyy-MM-dd`。 - JSON 字段以接口定义为准,部分沿用数据库下划线风格(如 `dorm_room`),属于既有契约,请勿自行转换。 - 学员标识为 `sId`,教师标识为 `tId`;人脸库中教师人员 ID 统一为 `tId + "_teacher"`。 --- ## 2. 接口总览 | 序号 | 模块 | 方法 | 路径 | 用途 | |---|---|---|---|---| | 1 | 学员信息 | GET | `/student/list` | 学员主档列表(含报到状态) | | 2 | 学员信息 | GET | `/student/{sId}` | 单个学员详情(含账号绑定信息) | | 3 | 学员信息 | GET | `/student/{sId}/base` | 学员基础信息(宿舍/教室/开班/场地) | | 4 | 学员信息 | GET | `/student/{sId}/resources` | 学员关联资源(在基础信息上增加课程表与课次) | | 5 | 教师信息 | GET | `/teacher/list` | 教师主档列表(字段结构同学员列表) | | 6 | 教师信息 | GET | `/teacher/{tId}/base` | 教师基础信息(信息/宿舍/授课课表) | | 7 | 班级 | GET | `/class/student` | 进行中班级及其未报到学员名单 | | 8 | 宿舍 | GET | `/dormitory/layout` | 宿舍布局(楼栋→楼层→房间) | | 9 | 课表 | GET | `/lesson/schedule` | 按日期(可选课程类型)查询课程安排 | | 10 | 人脸 | POST | `/student/face` | 上传学员人脸并更新头像、补办报到 | | 11 | 人脸 | POST | `/teacher/face` | 上传教师人脸并更新头像 | | 12 | 人脸 | POST | `/face` | 按 `employeeId` 后缀自动分发到学员/教师 | | 13 | 报到管理 | POST | `/report/reset` | 报到重置(解绑人脸 + 清门锁授权 + 复位参训状态) | --- ## 3. 学员信息模块 ### 3.1 获取学员信息列表 **用途**:报到机启动或刷新时拉取全部学员主档,用于本地人员检索、人脸比对名单准备;每条记录额外补入该学员最新一条报到记录的参训状态与报到时间。 - **方法 / 路径**:`GET /api/register/v1/student/list` - **请求参数**:无 - **请求体**:不适用 **响应示例** ```json { "code": 200, "msg": "操作成功", "data": [ { "sId": "S20260001", "sName": "张三", "personNo": "P0001", "sGender": "男", "sBirth": "1998-05-12", "deptName": "供电段", "cardNo": "C0001", "sPhone": "13800000001", "sEmail": "zhangsan@example.com", "idCardMasked": "210***********1234", "idCardLast4": "1234", "sourceType": 1, "status": 1, "avatar": "/profile/upload/2026/11/09/xxx.png", "dutyLevel": null, "id": 1, "deptId": 103, "enrollmentStatus": 0, "joinTime": null } ] } ``` **字段说明(`StudentItemVO`)** | 字段 | 类型 | 说明 | |---|---|---| | sId | string | 学员唯一标识(主键引用口径) | | sName | string | 姓名 | | personNo | string | 人员编号 | | sGender | string | 性别 | | sBirth | string | 出生日期 `yyyy-MM-dd` | | deptName | string | 所属部门/车间名称 | | cardNo | string | 卡号 | | sPhone / sEmail | string | 手机号 / 邮箱 | | idCardMasked / idCardLast4 | string | 脱敏身份证 / 后四位(明文身份证不下发) | | sourceType | int | 学员来源类型 | | status | int | 档案状态 | | avatar | string | 头像可访问路径(相对资源前缀) | | id / deptId | long | 自增编号 / 部门ID | | enrollmentStatus | int | 参训状态:0待报到、1已报到、2请假、3放弃、4已完成(取最新一条报名记录) | | joinTime | datetime | 报到时间(取最新一条报名记录,未报到为 null) | | extractStatus / extractTime / extractMsg / photoMd5 / featureData / featureMd5 / featureAlgoVersion / featureSize | — | 人脸特征提取的内部扩展字段,报到机通常无需使用,多为 null | **错误场景** | 场景 | code | msg | |---|---|---| | 数据库异常 | 500 | 系统异常提示 | | 查询结果为空 | 200 | `data: []`(空数组,非错误) | --- ### 3.2 获取单个学员详情 **用途**:按 `sId` 查询单个学员的完整档案,并附带其系统账号绑定状态,供报到机核对身份或人工处理。 - **方法 / 路径**:`GET /api/register/v1/student/{sId}` - **请求参数(Path)** | 参数名 | 类型 | 必填 | 说明 | |---|---|---|---| | sId | string | 是 | 学员唯一标识 | - **请求体**:不适用 **响应示例** ```json { "code": 200, "msg": "操作成功", "data": { "sId": "S20260001", "sName": "张三", "personNo": "P0001", "sGender": "男", "sBirth": "1998-05-12", "deptName": "供电段", "cardNo": "C0001", "sPhone": "13800000001", "sEmail": "zhangsan@example.com", "avatar": "/profile/upload/2026/11/09/xxx.png", "faceRef": null, "remark": null, "createBy": "admin", "createTime": "2026-09-01 09:00:00", "userId": 105, "userName": "student01", "nickName": "张三", "accountBindingStatus": 1 } } ``` **字段说明**:`StudentAdminVO = StudentItemVO + StudentDetailVO + 账号绑定字段` | 字段 | 类型 | 说明 | |---|---|---| | (基础字段) | — | 同 3.1 学员列表字段 | | faceRef | string | 人脸参考标识 | | remark | string | 备注 | | createBy / createTime | string / datetime | 建档人 / 建档时间 | | userId | long | 绑定的系统用户ID(未绑定为 null) | | userName | string | 绑定的系统登录账号 | | nickName | string | 绑定的用户昵称 | | accountBindingStatus | int | 账号关联状态:0未关联、1已关联 | **错误场景** | 场景 | code | msg | |---|---|---| | 学员不存在 | 40402 | 学员不存在 | | `sId` 为空 | 40402 / 500 | 学员不存在(按未命中处理) | > 说明:源码中 `detail == null` 分支返回 `data: null`,但底层 `selectStudentDetail` 在未命中时会抛业务异常,因此实际表现为 `code=40402`。 --- ### 3.3 获取学员基础信息 **用途**:报到机刷脸/刷卡识别学员后,展示该学员的**个人基础信息、所属开班、当前宿舍床位、上课教室、实训场地**,用于报到确认页。 - **方法 / 路径**:`GET /api/register/v1/student/{sId}/base` - **请求参数(Path)** | 参数名 | 类型 | 必填 | 说明 | |---|---|---|---| | sId | string | 是 | 学员唯一标识 | - **请求体**:不适用 - **数据来源**:班级优先取该学员最新报到记录(`train_class_report`)中的 `plan_id`,缺失时回落到报名关系(`train_enrollment`)中最新的班级;教室取该班排课课次(`train_lesson`)中**首个带教室**的课次,实训场地取**首个带场地**的课次(两者可能来自不同课次)。 **响应示例** ```json { "code": 200, "msg": "操作成功", "data": { "studInfo": { "sId": "S20260001", "sName": "张三", "sPhone": "13800000001", "sGender": "男", "deptName": "供电段", "personNo": "P0001" }, "classPlan": { "planId": "PLAN-2026-01", "planName": "2026年秋季高铁房建设施高级研修班", "startTime": "2026-09-15 08:30:00", "endTime": "2026-09-30 17:30:00" }, "dormitory": { "dormId": "DORM-0101", "bedNo": "2", "dormBuilding": "1号楼", "dormRoom": "101" }, "classroom": { "classRoomId": "CR-001", "roomLocation": "实训基地A栋201", "roomName": "第一理论教室" }, "trainingSite": { "siteId": "TS-001", "siteLocation": "实训基地B栋", "siteName": "接触网实训场", "major": "接触网工" } } } ``` **字段说明** | 字段 | 类型 | 说明 | |---|---|---| | studInfo | object | 学员精简信息:`sId`、`sName`、`sPhone`、`sGender`、`deptName`、`personNo` | | classPlan | object \| null | 所属开班:`planId`、`planName`、`startTime`、`endTime`;未分配班级时为 null | | dormitory | object | 宿舍:`dormId`、`bedNo`(有住宿记录时)、`dormBuilding`、`dormRoom`;未分配宿舍时字段可能为 null | | classroom | object \| null | 上课教室:`classRoomId`、`roomLocation`、`roomName`;该班无教室课次时为 null | | trainingSite | object \| null | 实训场地:`siteId`、`siteLocation`、`siteName`、`major`;该班无场地课次时为 null | > 本接口**不返回** `courses` 与 `lesson` 明细(源码中已注释关闭),需要课表请调用 3.4 或 7.1。 **错误场景** | 场景 | code | msg | |---|---|---| | 学员不存在 | 40402 | 学员不存在 | | 学员未分配班级/宿舍 | 200 | 对应字段为 `null`(不是错误) | --- ### 3.4 获取学员关联资源 **用途**:在 3.3 的基础上**额外返回该学员所在班级的课程清单与课次明细**,供报到机展示“我的课程 / 我的课表”。 - **方法 / 路径**:`GET /api/register/v1/student/{sId}/resources` - **请求参数(Path)** | 参数名 | 类型 | 必填 | 说明 | |---|---|---|---| | sId | string | 是 | 学员唯一标识 | - **请求体**:不适用 **响应示例**(在 3.3 的 `data` 基础上增加两个字段) ```json { "code": 200, "msg": "操作成功", "data": { "studInfo": { "sId": "S20260001", "sName": "张三", "sPhone": "13800000001", "sGender": "男", "deptName": "供电段", "personNo": "P0001" }, "classPlan": { "planId": "PLAN-2026-01", "planName": "2026年秋季高铁房建设施高级研修班", "startTime": "2026-09-15 08:30:00", "endTime": "2026-09-30 17:30:00" }, "courses": [ { "cId": "C0001", "cName": "高铁站房巡检" } ], "lesson": [ { "startTime": "2026-09-16 09:00:00", "endTime": "2026-09-16 11:30:00", "planCourseId": 1, "cName": "高铁站房巡检" } ], "dormitory": { "dormId": "DORM-0101", "bedNo": "2", "dormBuilding": "1号楼", "dormRoom": "101" }, "classroom": { "classRoomId": "CR-001", "roomLocation": "实训基地A栋201", "roomName": "第一理论教室" }, "trainingSite": { "siteId": "TS-001", "siteLocation": "实训基地B栋", "siteName": "接触网实训场", "major": "接触网工" } } } ``` **新增字段说明** | 字段 | 类型 | 说明 | |---|---|---| | courses | array | 该班已配置课程:`cId`(课程ID)、`cName`(课程名称);无班级时为 null | | lesson | array | 该班全部课次:`startTime`、`endTime`、`planCourseId`、`cName`;无课次时为 null 或空数组 | > `lesson` 元素**不含**教师与场地字段;如需教师/场地/班牌信息,请使用 7.1 `GET /lesson/schedule`。 **错误场景** | 场景 | code | msg | |---|---|---| | 学员不存在 | 40402 | 学员不存在 | | 无班级 | 200 | `classPlan`、`courses`、`lesson` 为 null | --- ## 4. 教师信息模块 ### 4.1 获取教师信息列表 **用途**:拉取全部教师主档,字段结构与学员列表**完全一致**,便于报到机用同一套解析逻辑处理两类人员。 - **方法 / 路径**:`GET /api/register/v1/teacher/list` - **请求参数**:无 - **请求体**:不适用 **字段映射规则** | 输出字段 | 取值来源 | |---|---| | sId | `tId + "_teacher"`(与人脸库教师 employee_id 口径一致) | | sName | `train_teacher.t_name` | | personNo / sGender / sBirth / deptName / cardNo / sPhone / sEmail / status / avatar / id | `train_teacher` 对应字段 | | enrollmentStatus | `train_teacher.enrollment_status`(0待报到/1已报到/2请假/3放弃/4已完成) | | joinTime | `train_teacher.join_time`(未报到为 null) | **响应示例** ```json { "code": 200, "msg": "操作成功", "data": [ { "sId": "T101_teacher", "sName": "李老师", "personNo": "T0001", "sGender": "男", "deptName": "供电教研室", "sPhone": "13900000001", "status": 1, "avatar": "/profile/upload/2026/11/09/teacher.png", "id": 3, "enrollmentStatus": 1, "joinTime": "2026-09-14 09:10:00" } ] } ``` **错误场景** | 场景 | code | msg | |---|---|---| | 无教师数据 | 200 | `data: []` | --- ### 4.2 获取教师基础信息 **用途**:教师刷脸后展示**教师基本信息、当前宿舍、授课课表**。 - **方法 / 路径**:`GET /api/register/v1/teacher/{tId}/base` - **请求参数(Path)** | 参数名 | 类型 | 必填 | 说明 | |---|---|---|---| | tId | string | 是 | 教师工号;**支持传入 `tId + "_teacher"`**,服务端会自动截去后缀 | - **请求体**:不适用 **响应示例** ```json { "code": 200, "msg": "操作成功", "data": { "teacherInfo": { "tId": "T101", "tName": "李老师", "tPhone": "13900000001", "tGender": "男", "deptName": "供电教研室", "personNo": "T0001", "avatar": "/profile/upload/2026/11/09/teacher.png" }, "dormitory": { "dormId": "DORM-0203", "bedNo": "1", "dormBuilding": "2号楼", "dormRoom": "203" }, "lesson": [ { "lessonId": 101, "planCourseId": 1, "courseName": "高铁站房巡检", "tId": "T101", "teacherName": "李老师", "startTime": "2026-09-16 09:00:00", "endTime": "2026-09-16 11:30:00", "venueType": 1, "classRoomId": "CR-001", "classRoomName": "第一理论教室", "siteId": null, "siteName": null, "venueName": "第一理论教室", "boardSn": "BOARD-SN-001", "remark": null } ] } } ``` **字段说明** | 字段 | 类型 | 说明 | |---|---|---| | teacherInfo | object \| null | 教师不存在时为 null;字段:`tId`、`tName`、`tPhone`、`tGender`、`deptName`、`personNo`、`avatar` | | dormitory | object | `dormId`、`bedNo`(有住宿时)、`dormBuilding`、`dormRoom` | | lesson | array | 该教师全部授课课次(按开始时间升序),元素为 `TimetableLessonItemVO`,字段见下表 | **`lesson` 元素字段(TimetableLessonItemVO)** | 字段 | 类型 | 说明 | |---|---|---| | lessonId | long | 课次ID | | planCourseId | long | 计划课程实例ID | | courseName | string | 课程名称 | | tId | string | 教师工号 | | teacherName | string | 教师姓名 | | startTime / endTime | datetime | 课次起止时间 | | venueType | int | 场地类型:1普通教室、2实训场地 | | classRoomId / classRoomName | string | 教室ID / 名称(venueType=1 时有效) | | siteId / siteName | string | 实训场地ID / 名称(venueType=2 时有效) | | venueName | string | 场地聚合展示名(教室名或场地名) | | boardSn | string | 电子班牌 SN;未唯一绑定该教室时为 null | | remark | string | 课次备注 | > 该接口不按日期过滤,返回教师全部课次。 **错误场景** | 场景 | code | msg | |---|---|---| | tId 不存在 | 200 | `teacherInfo: null`、`lesson: []` | | 教师无住宿/无课次 | 200 | 对应字段为空对象/空数组 | --- ## 5. 班级模块 ### 5.1 获取进行中班级的未报到学员名单 **用途**:报到机按班级展示**待报到学员**,用于现场点名/呼叫报到。 - **方法 / 路径**:`GET /api/register/v1/class/student` - **请求参数**:无 - **请求体**:不适用 - **筛选口径**:班级 `plan_status = 1`(进行中);学员 `enrollment_status = 0`(待报到) **响应示例** ```json { "code": 200, "msg": "操作成功", "data": [ { "planId": "PLAN-2026-01", "planName": "2026年秋季高铁房建设施高级研修班", "classId": "CLASS-01", "className": "房建一班", "students": [ { "sId": "S20260001", "sName": "张三", "deptId": 103, "deptName": "供电段" } ] } ] } ``` **字段说明** | 字段 | 类型 | 说明 | |---|---|---| | planId / planName | string | 培训班ID / 名称 | | classId / className | string | 班级ID / 名称 | | students | array | 未报到学员:`sId`、`sName`、`deptId`、`deptName` | **容错说明**:单个学员主档缺失时不会中断整体返回,改用报名记录(`train_enrollment`)中的姓名/部门冗余字段兜底,此时 `deptId` 为 null。 **错误场景** | 场景 | code | msg | |---|---|---| | 无进行中班级 | 200 | `data: []` | --- ## 6. 宿舍模块 ### 6.1 获取宿舍布局信息 **用途**:报到机展示宿舍楼栋—楼层—房间的床位占用情况(如“已分配数/总床位数”)。 - **方法 / 路径**:`GET /api/register/v1/dormitory/layout` - **请求参数**:无 - **请求体**:不适用 - **统计口径**:已分配数 = `train_accommodation` 中住宿状态为 0(预分配)、1(已入住)的记录数;总床位 = 房间核定容量 `capacity` **响应示例** ```json { "code": 200, "msg": "操作成功", "data": { "1号楼": { "1层": { "101": { "id": 10, "dorm_room": "101", "accommodation": "2/4" } }, "未标注楼层": { "102": { "id": 11, "dorm_room": "102", "accommodation": "0/2" } } } } } ``` **字段说明** | 层级 | 键 | 说明 | |---|---|---| | 一级 | 楼栋名称 | `dorm_building`,为空的数据会被跳过 | | 二级 | 楼层 | `dorm_floor`,为空时固定为 `未标注楼层` | | 三级 | 房间号 | `dorm_room` | | 叶子对象 | id | 房间自增编号 | | | dorm_room | 房间号(下划线命名,保持既有契约) | | | accommodation | `已分配床位数/总床位数` 字符串,如 `2/4` | > 返回使用 `LinkedHashMap`,键顺序与 SQL 排序(楼栋→楼层→房间号)一致;无宿舍数据时 `data: {}`。 **错误场景** | 场景 | code | msg | |---|---|---| | 无宿舍数据 | 200 | `data: {}` | --- ## 7. 课表模块 ### 7.1 获取课程安排(按日期 + 课程类型) **用途**:查询指定自然日的课程安排,按**日期为键**分组返回课程名称、课程类型、上课地点、上课教师等信息,供报到机/班牌展示当日课表。 - **方法 / 路径**:`GET /api/register/v1/lesson/schedule` - **请求参数(Query)** | 参数名 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | startTime | string | 否 | 当天(北京时间) | 课程开始时间,对应 `train_lesson.start_time`。支持 `yyyy-MM-dd`、`yyyy-MM-dd HH:mm:ss`、`yyyy/MM/dd`、ISO-8601(`T` 分隔、`Z` 结尾、`+08:00` 偏移)、10 位秒级 / 13 位毫秒级时间戳;服务端取该时间所在自然日 | | courseType | string | 否 | 不筛选 | 课程类型,对应 `train_course.c_type`:**`1` = 理论**、**`2` = 实训**。缺省或空串返回当日全部类型;传其它值返回参数错误 | - **请求体**:不适用 - **查询范围**:`train_lesson` 为主表,关联 `train_plan_course`、`train_course`、`train_teacher`、`train_classroom_info`、`train_training_site`、`train_class_plan`;日期区间为 `[当日 00:00:00, 次日 00:00:00)`,由 SQL `STR_TO_DATE` 在库内生成(不受应用 JVM 时区影响);培训班为逻辑删除时不参与过滤(课表属于已发生的教学事实)。 **请求示例** ``` GET /api/register/v1/lesson/schedule GET /api/register/v1/lesson/schedule?startTime=2026-11-09 GET /api/register/v1/lesson/schedule?startTime=2026-11-09&courseType=1 GET /api/register/v1/lesson/schedule?startTime=2026-11-09T09:00:00&courseType=2 ``` **响应示例** ```json { "code": 200, "msg": "操作成功", "data": { "2026-11-09": [ { "id": 101, "name": "高铁站房巡检", "courseType": "1", "venueType": 1, "startTime": "2026-11-09 09:00:00", "endTime": "2026-11-09 11:30:00", "teacher": "李老师", "classroom": "第一理论教室", "planName": "2026年秋季高铁房建设施高级研修班" }, { "id": 102, "name": "接触网实操", "courseType": "2", "venueType": 2, "startTime": "2026-11-09 14:00:00", "endTime": "2026-11-09 16:30:00", "teacher": "王老师", "classroom": "接触网实训场", "planName": "2026年秋季高铁房建设施高级研修班" } ] } } ``` **`data` 结构说明** | 层级 | 说明 | |---|---| | 键 | 日期字符串 `yyyy-MM-dd`(按课次开始时间的北京时间自然日分组,升序) | | 值 | 该日课程数组,按 `startTime`、`id` 升序 | **课程元素字段(`CourseScheduleItemVO`)** | 字段 | 类型 | 说明 | |---|---|---| | id | long | 课次ID(`train_lesson.lesson_id`) | | name | string | 课程名称(`train_course.c_name`) | | courseType | string | 课程类型,与 `train_course.c_type` 一致:`1` 理论、`2` 实训;课程缺失或为空时为 null | | venueType | int | 课次场地类型:1 普通教室、2 实训场地 | | startTime / endTime | datetime | 课次起止时间 `yyyy-MM-dd HH:mm:ss` | | teacher | string | 上课教师姓名(`train_teacher.t_name`) | | classroom | string | 上课地点:`venueType=1` 取教室名称,`venueType=2` 取实训场地名称 | | planName | string | 所属培训班名称(`train_class_plan.plan_name`) | **错误场景** | 场景 | code | msg | 说明 | |---|---|---|---| | startTime 格式无法识别 | 500 | `startTime 格式不正确,支持 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss / ISO-8601,示例:2026-11-09` | 例如传入 `2026年11月9日` | | courseType 非 1/2 | 500 | `courseType 仅支持 1(理论)或 2(实训)` | 例如传入 `理论`、`3` | | 当日无课程 | 200 | `data: {}` | 空对象,非错误 | | 该类型当日无课程 | 200 | `data: {}` | 加了 `courseType` 后无匹配 | > 服务端会输出日志 `lessonSchedule 课程安排查询 day=..., courseType=..., rows=...`,排障时可直接用 `rows` 区分“SQL 未命中”与“命中但未返回”。 --- ## 8. 人脸模块 ### 8.1 上传学员人脸照片 **用途**:报到机采集学员人脸 → 转 PNG 落盘 → 更新 `train_student.avatar` → 同步到人脸识别设备 → 回写报到状态并办理门锁入住(若已分配宿舍)。 - **方法 / 路径**:`POST /api/register/v1/student/face` - **请求头**:`Content-Type: application/json` **请求体** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | sId | string | 是 | 学员唯一标识,同时作为人脸库 `employee_id` | | faceImage | string | 是 | base64 人脸图片,可带 `data:image/png;base64,` 前缀;服务端统一转码为 PNG | ```json { "sId": "S20260001", "faceImage": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." } ``` **业务副作用**(按顺序) 1. 图片解码并保存到 `profile/upload/yyyy/MM/dd/{uuid}.png`,路径写入 `train_student.avatar` 2. 调用人脸设备新增/更新人员(`employee_id = sId`) 3. 回查该学员报名记录,将**第一条**记录的参训状态置为 1(已报到)并记录报到时间 4. 若存在有效住宿记录,调用门禁入住接口(`/openapi/iot/stay/checkin`)下发入住授权 **响应示例** ```json { "code": 200, "msg": "操作成功", "data": { "sId": "S20260001", "avatar": "/profile/upload/2026/11/09/9f1c....png", "faceSync": "success", "lockCheckin": { "code": 200, "msg": "入住成功" }, "lockAuthId": "", "joinTime": "2026-11-09 09:12:33" } } ``` **字段说明** | 字段 | 类型 | 说明 | |---|---|---| | sId | string | 学员标识 | | avatar | string | 头像可访问路径 | | faceSync | string | 人脸设备同步结果:`success`、`failed`、`skipped` | | faceMsg | string | 仅 `faceSync=failed` 时出现,失败原因(设备无响应/异常信息) | | lockCheckin | object | 门锁入住接口返回结果;无有效住宿时不返回 | | lockAuthId | string | 预留字段,固定空串 | | joinTime | datetime | 实际报到时间;无住宿时可能不返回 | | (设备扩展字段) | any | 设备成功时其返回的 `data` 会被并入本层 | **错误场景** | 场景 | code | msg | |---|---|---| | 缺少 sId 或 faceImage | 500 | 参数缺失:sId 与 faceImage 均为必填 | | sId 为空串 | 500 | sId 不能为空 | | 图片非法/无法解码 | 500 | 图片解析失败:{具体原因} | | 目录创建失败 | 500 | 创建存储目录失败 | | 文件写入失败 | 500 | 图片写入失败:{具体原因} | | 学员不存在或更新 0 行 | 500 | 学员不存在或更新失败 | | 设备同步失败 | 200 | 仍返回成功,`faceSync=failed` + `faceMsg`(头像已保存) | --- ### 8.2 上传教师人脸照片 **用途**:教师人脸采集与入库,更新 `train_teacher.avatar`,并同步人脸设备(`employee_id = tId + "_teacher"`)。 - **方法 / 路径**:`POST /api/register/v1/teacher/face` - **请求头**:`Content-Type: application/json` **请求体** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | teacherId | string | 是 | 教师工号(`train_teacher.t_id`),**不要**带 `_teacher` 后缀 | | faceImage | string | 是 | base64 人脸图片,可带 data URI 前缀 | ```json { "teacherId": "T101", "faceImage": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." } ``` **业务差异**:教师无开班报名记录,**不做报到状态更新**;仅当教师已有有效住宿且已生成门锁流水号时,补发一次入住授权(有效期 15 天)。 **响应示例** ```json { "code": 200, "msg": "操作成功", "data": { "tId": "T101", "avatar": "/profile/upload/2026/11/09/ab34....png", "faceSync": "success", "lockCheckin": { "code": 200, "msg": "入住成功" }, "lockAuthId": "" } } ``` **字段说明**:`tId`、`avatar`、`faceSync`、`faceMsg`(失败时)、`lockCheckin`(有住宿且门锁可用时)、`lockAuthId`(固定空串)、设备扩展字段合并规则同上。 **错误场景** | 场景 | code | msg | |---|---|---| | 缺少 teacherId 或 faceImage | 500 | 参数缺失:teacherId 与 faceImage 均为必填 | | teacherId 为空串 | 500 | teacherId 不能为空 | | 图片非法/无法解码 | 500 | 图片解析失败:{具体原因} | | 目录/文件写入失败 | 500 | 创建存储目录失败 / 图片写入失败:{具体原因} | | 教师不存在或更新 0 行 | 500 | 教师不存在或更新失败 | | 设备同步失败 | 200 | `faceSync=failed` + `faceMsg`(头像已保存) | --- ### 8.3 上传人脸照片(按员工标识自动分发) **用途**:统一入口。按 `employeeId` 后缀自动路由,设备侧无需区分学员/教师接口。 - **方法 / 路径**:`POST /api/register/v1/face` - **请求头**:`Content-Type: application/json` **请求体** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | employeeId | string | 是 | 人脸库人员ID。以 `_teacher` 结尾 → 视为教师(去掉后缀即 `tId`);其它 → 视为学员 `sId` | | faceImage | string | 是 | base64 人脸图片 | ```json { "employeeId": "T101_teacher", "faceImage": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." } ``` **分发规则** | 判定 | 路由 | 说明 | |---|---|---| | `employeeId` 以 `_teacher` 结尾 | → 8.2 教师人脸上传 | `tId = employeeId` 去掉后缀;后缀匹配区分大小写、不做 trim | | 其它 | → 8.1 学员人脸上传 | `sId = employeeId` | **响应**:直接透传被调用接口(8.1 / 8.2)的响应体,结构与之一致。 **错误场景** | 场景 | code | msg | |---|---|---| | 缺少 employeeId 或 faceImage | 500 | 参数缺失:employeeId 与 faceImage 均为必填 | | employeeId 为空串 | 500 | employeeId 不能为空 | | employeeId 仅为 `_teacher`(工号为空) | 500 | employeeId 非法:教师工号(tId)不能为空 | | 后续流程失败 | 500 / 200 | 同 8.1 / 8.2 | --- ## 9. 报到管理模块 ### 9.1 报到重置 **用途**:误点他人信息导致人脸错绑时的补救接口。按顺序执行:**解绑人脸 → 清除门锁入住授权 → 参训状态复位为 0(待报到) → 清空学员头像**。宿舍分配记录保留,不做退宿。 - **方法 / 路径**:`POST /api/register/v1/report/reset` - **请求头**:`Content-Type: application/x-www-form-urlencoded`(参数通过 Query 传递,无请求体) **请求参数(Query)** | 参数名 | 类型 | 必填 | 说明 | |---|---|---|---| | planId | string | 是 | 培训班ID(`train_enrollment.plan_id`) | | studentId | string | 是 | 学员ID(`train_student.s_id`) | **请求示例** ``` POST /api/register/v1/report/reset?planId=PLAN-2026-01&studentId=S20260001 ``` **响应示例(成功)** ```json { "code": 200, "msg": "报到重置成功", "data": { "planId": "PLAN-2026-01", "studentId": "S20260001", "enrollmentId": 900001, "previousStatus": 1, "faceUnbind": "success", "lockCheckout": "success", "lockApiBizNo": "LOCK20261109001", "currentStatus": 0, "alreadyReset": false, "accommodationKept": true, "avatarCleared": true, "avatarSId": "S20260001" } } ``` **字段说明** | 字段 | 类型 | 说明 | |---|---|---| | planId / studentId | string | 入参回显 | | enrollmentId | long | 参训记录ID | | previousStatus | int | 重置前参训状态 | | faceUnbind | string | `success`(人脸解绑成功) | | lockCheckout | string | `success`,或 `skipped: 无有效住宿记录` / `skipped: 缺少门锁业务流水号与宿舍信息` | | lockApiBizNo | string | 门锁业务流水号(仅实际操作门锁时返回) | | currentStatus | int | 重置后状态,固定 0 | | alreadyReset | boolean | 重置前是否已是 0(重复调用标识) | | accommodationKept | boolean | 是否保留了住宿分配 | | avatarCleared | boolean | 学员头像是否已清空 | | avatarSId | string | 被清空头像的学员标识 | **分步失败场景(返回 code=500,`msg` 指明失败步骤)** | 场景 | msg | |---|---| | 缺少 planId / studentId 参数 | Spring 参数缺失提示(`Required request parameter 'planId' ...`) | | planId 或 studentId 为空串 | `planId 和 studentId 不能为空` | | 报名记录不存在 | `未找到学员[{studentId}]在班级[{planId}]的参训记录` | | 学员已放弃培训(状态3) | `学员已放弃培训,不允许重置报到状态` | | 学员已结训(状态4) | `学员已结训,不允许重置报到状态` | | 步骤1 人脸解绑失败 | `步骤1 人脸解绑失败:{原因};参训状态未变更` | | 步骤2 门锁响应解析失败 | `步骤2 门锁授权清除响应解析失败:{原因};人脸已解绑,参训状态未变更` | | 步骤2 门锁清除失败 | `步骤2 门锁授权清除失败:{原因};人脸已解绑,参训状态未变更` | | 步骤3 状态复位失败 | `步骤3 参训状态重置失败;人脸与门锁授权已解除,请人工核对后重试` | | 学员档案缺失(前 3 步已完成) | `报到重置已完成,但未找到有效学员档案[{sId}],avatar 未清空`(失败响应中仍带 `data`) | | 清空头像异常(前 3 步已完成) | `报到重置已完成,但清空学员头像失败:{原因}`(失败响应中仍带 `data`) | > 该接口为**非幂等敏感操作**,重复调用会对已复位的数据再次执行解绑/清库动作,建议设备端加二次确认。 --- ## 10. 附录 ### 10.1 枚举值速查 | 字段 | 取值 | |---|---| | 参训/报到状态 `enrollmentStatus` | 0 待报到、1 已报到、2 请假、3 放弃、4 已完成 | | 场地类型 `venueType` | 1 普通教室、2 实训场地 | | 课程类型 `courseType`(`train_course.c_type`) | 1 理论、2 实训 | | 账号关联状态 `accountBindingStatus` | 0 未关联、1 已关联 | | 人脸同步状态 `faceSync` | success / failed / skipped | | 房间占用 `accommodation` | 字符串 `已分配数/总床位数`,如 `2/4` | ### 10.2 关联数据表 | 模块 | 主要数据表 | |---|---| | 学员 | `train_student`、`train_enrollment` | | 教师 | `train_teacher` | | 开班 / 课程 | `train_class_plan`、`train_plan_course`、`train_course` | | 排课 | `train_lesson`、`train_classroom_info`、`train_training_site`、`train_electronic_board` | | 宿舍 | `train_dormitory_info`、`train_accommodation` | | 报到 | `train_class_report` | | 人脸/门锁 | 人脸识别服务、`/openapi/iot/stay/checkin` 门禁开放接口 | --- *文档结束。接口变更请同步更新本文件,并在提交说明中标注版本。*