Files
railwaysystem/RuoYi-Vue/docs/train_register_api.md
T

1015 lines
36 KiB
Markdown
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.
# 报到机接口文档(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` 门禁开放接口 |
---
*文档结束。接口变更请同步更新本文件,并在提交说明中标注版本。*