Files
railwaysystem/RuoYi-Vue/doc/报到机接口API文档_20261105.md
T

688 lines
28 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.
# 报到机接口 API 文档
| 项目 | 内容 |
|---|---|
| 控制器 | `com.ruoyi.web.controller.api.TrainRegisterApiController` |
| 源文件 | `ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainRegisterApiController.java` |
| BasePath | `/api/register/v1` |
| 接口数量 | 12(8 个 GET + 4 个 POST) |
| 鉴权 | Spring Security 已放行 `/api/register/v1/**`(`permitAll`);仅 `/report/reset` 内部取登录用户,需携带 RuoYi Bearer Token |
| 统一响应 | `AjaxResult`(`code` / `msg` / `data`) |
| 日期格式 | `yyyy-MM-dd HH:mm:ss`(`spring.jackson.date-format`,时区 GMT+8) |
| 文档更新 | 2026-11-09(新增宿舍布局接口) |
| 编写依据 | 控制器源码逐行核对,与当前实现一致 |
---
## 目录
- [1. 通用约定](#1-通用约定)
- [1.1 鉴权说明](#11-鉴权说明)
- [1.2 统一响应结构](#12-统一响应结构)
- [1.3 状态码与错误提示](#13-状态码与错误提示)
- [1.4 公共数据结构](#14-公共数据结构)
- [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-上传学员人脸照片)
- [6.2 上传教师人脸照片](#62-上传教师人脸照片)
- [6.3 上传人脸照片(统一入口)](#63-上传人脸照片统一入口)
- [7. 报到业务](#7-报到业务)
- [7.1 报到重置](#71-报到重置)
- [8. 宿舍布局](#8-宿舍布局)
- [8.1 获取宿舍布局信息](#81-获取宿舍布局信息)
- [9. 附录](#9-附录)
- [9.1 典型调用流程](#91-典型调用流程)
- [9.2 调用示例](#92-调用示例)
---
## 1. 通用约定
### 1.1 鉴权说明
- 源代码类注释声明“其余接口复用 RuoYi Bearer 鉴权”,但当前 `SecurityConfig` 中已配置:
```110:110:ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
.requestMatchers("/api/register/v1/**").permitAll();
```
即 **整个 `/api/register/v1/**` 路径实际为匿名可访问**,查询类与人脸上传接口不强制 Token。
- 例外:`POST /report/reset` 内部通过 `SecurityUtils.getUsername()` 写入操作人,无登录态时该方法会抛 `ServiceException("获取用户账户异常", 401)`。因此该接口**必须携带** `Authorization: Bearer {token}`。
- 控制器注释提到的 `X-Signature` / `X-Nonce`(HMAC)验签目前**未在本控制器内实现**,`DeviceHmacFilter` 仅对 `/api/iot/v1/device/login` 生效。
### 1.2 统一响应结构
所有接口返回 `AjaxResult`:
```json
{ "code": 200, "msg": "操作成功", "data": {} }
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | number | 业务状态码,见 1.3 |
| `msg` | string | 提示信息;成功时通常为 `操作成功` |
| `data` | object / array | 业务数据,无数据时该字段不出现 |
### 1.3 状态码与错误提示
| code | 含义 | 触发来源 |
|---|---|---|
| 200 | 成功 | 正常返回 |
| 401 | 未认证 / 无登录上下文 | `SecurityUtils` 抛出的 `ServiceException` |
| 500 | 参数缺失、图片解析失败、目标记录不存在等业务失败 | 控制器内 `AjaxResult.error(...)` |
> 除框架级 401/404 外,业务失败统一为 `code=500`,调用方需读取 `msg` 区分原因。
### 1.4 公共数据结构
#### StudentItemVO(学员列表项,`/student/list`)
| 字段 | 类型 | 说明 |
|---|---|---|
| `sId` | string | 学员ID(业务主键) |
| `sName` | string | 学员姓名 |
| `personNo` | string | 人员编号 |
| `sGender` | string | 性别 |
| `sBirth` | string | 出生日期,`yyyy-MM-dd` |
| `deptName` | string | 所属部门名称 |
| `cardNo` | string | 卡号 |
| `sPhone` | string | 手机号 |
| `sEmail` | string | 邮箱 |
| `idCardMasked` | string | 证件号掩码 |
| `idCardLast4` | string | 证件号后 4 位 |
| `sourceType` | number | 数据来源类型 |
| `status` | number | 状态 |
| `avatar` | string | 头像访问路径(如 `/profile/upload/...png`) |
| `dutyLevel` | number | 职务级别 |
| `extractStatus` / `extractTime` / `extractMsg` | string / string / string | 人脸特征提取状态、时间、描述 |
| `photoMd5` | string | 人脸照片 MD5 |
| `featureData` / `featureMd5` / `featureAlgoVersion` / `featureSize` | string / string / string / number | 人脸特征数据及元信息 |
| `id` | number | 主键 |
| `deptId` | number | 部门ID |
| `enrollmentStatus` | number | 报到状态:0 待报到、1 已报到、2 请假、3 放弃、4 已完成 |
| `joinTime` | string | 报到/入学时间 |
#### 门锁入住结果 lockCheckin(`LockStayCheckinResult`)
| 字段 | 类型 | 说明 |
|---|---|---|
| `success` | boolean | 是否成功 |
| `status` | string | 处理状态 |
| `bizNo` | string | 门锁业务流水号 |
| `httpStatus` | number | 下游 HTTP 状态 |
| `code` | string | 下游业务码 |
| `message` | string | 下游提示信息 |
| `rawBody` | string | 下游原始响应体 |
---
## 2. 接口总览
| 序号 | 分组 | 方法 | 路径 | 功能说明 |
|---|---|---|---|---|
| 1 | 学员信息 | GET | `/api/register/v1/student/list` | 获取全部学员列表(含报到状态) |
| 2 | 学员信息 | GET | `/api/register/v1/student/{sId}` | 获取单个学员详情(含账号绑定信息) |
| 3 | 学员信息 | GET | `/api/register/v1/student/{sId}/base` | 获取学员基础信息(个人信息/开班/宿舍/教室/实训场地) |
| 4 | 学员信息 | GET | `/api/register/v1/student/{sId}/resources` | 获取学员关联资源(基础信息 + 课程 + 课表) |
| 5 | 教师信息 | GET | `/api/register/v1/teacher/list` | 获取全部教师列表(结构同学员列表) |
| 6 | 教师信息 | GET | `/api/register/v1/teacher/{tId}/base` | 获取教师基础信息(个人信息/宿舍/授课课表) |
| 7 | 班级名单 | GET | `/api/register/v1/class/student` | 获取进行中班级及其未报到学员名单 |
| 8 | 人脸照片 | POST | `/api/register/v1/student/face` | 上传学员人脸照片并同步人脸设备、办理报到 |
| 9 | 人脸照片 | POST | `/api/register/v1/teacher/face` | 上传教师人脸照片并同步人脸设备 |
| 10 | 人脸照片 | POST | `/api/register/v1/face` | 按 `employeeId` 后缀统一分发到学员/教师上传 |
| 11 | 报到业务 | POST | `/api/register/v1/report/reset` | 报到重置(解绑人脸、清门锁授权、状态回退) |
| 12 | 宿舍布局 | GET | `/api/register/v1/dormitory/layout` | 按楼栋/楼层/房间返回宿舍布局及已分配床位数 |
---
## 3. 学员信息
### 3.1 获取学员信息列表
- **接口路径**:`/api/register/v1/student/list`
- **请求方法**:`GET`
- **功能说明**:读取学员主档列表(`train_student`),并按 `sId` 反查 `train_enrollment` 报名记录,用第一条记录的报到状态与报到时间补齐 `enrollmentStatus`、`joinTime`。
**请求参数**:无(无分页、无过滤条件,返回全量)
**返回结果说明**:
- `data`:`StudentItemVO` 数组,字段见 [1.4](#14-公共数据结构)。
- 学员无报名记录时,`enrollmentStatus` 与 `joinTime` 为 `null`。
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"sId": "S0001",
"sName": "张三",
"personNo": "P1001",
"sGender": "1",
"sBirth": "2000-01-01",
"deptName": "供电段",
"sPhone": "13800000000",
"status": 1,
"avatar": "/profile/upload/2026/11/05/xxxx.png",
"enrollmentStatus": 1,
"joinTime": "2026-11-05 09:12:33"
}
]
}
```
---
### 3.2 获取学员详情
- **接口路径**:`/api/register/v1/student/{sId}`
- **请求方法**:`GET`
- **功能说明**:按学员ID查询详情,并在基础字段上补充账号绑定信息(`userId`、`userName`、`nickName`、`accountBindingStatus`)。
**请求参数**
| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `sId` | path | string | 是 | 学员ID,如 `S0001` |
**返回结果说明**:
- `data` 为 `StudentAdminVO`,继承 `StudentDetailVO` ← `StudentItemVO`。
- 在 [1.4 StudentItemVO](#14-公共数据结构) 字段基础上,追加:
| 字段 | 类型 | 说明 |
|---|---|---|
| `faceRef` | string | 人脸引用 |
| `remark` | string | 备注 |
| `createBy` / `createTime` | string / string | 创建人 / 创建时间 |
| `userId` | number | 绑定的系统用户ID |
| `userName` | string | 登录账号 |
| `nickName` | string | 用户昵称 |
| `accountBindingStatus` | number | 账号绑定状态:1 已绑定,0 未绑定 |
- 学员不存在时返回 `{"code":200,"msg":"操作成功","data":null}`。
---
### 3.3 获取学员基础信息
- **接口路径**:`/api/register/v1/student/{sId}/base`
- **请求方法**:`GET`
- **功能说明**:按 `sId` 聚合该学员的个人信息、所属开班计划、宿舍、教室、实训场地,供报到机首屏展示。开班 planId 取值优先取最新报到记录,缺失时从报名关系兜底。
**请求参数**
| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `sId` | path | string | 是 | 学员ID |
**返回结果说明**:`data` 为对象(字段可能为 `null`):
| 字段 | 类型 | 说明 |
|---|---|---|
| `studInfo` | object | 学员基础信息:`sId`、`sName`、`sPhone`、`sGender`、`deptName`、`personNo` |
| `classPlan` | object | 开班信息:`planId`、`planName`、`startTime`、`endTime` |
| `dormitory` | object | 宿舍信息:`dormId`、`bedNo`、`dormBuilding`、`dormRoom` |
| `classroom` | object | 教室信息(取自本计划首个带教室的课次):`classRoomId`、`roomLocation`、`roomName` |
| `trainingSite` | object | 实训场地信息(取自本计划首个带场地的课次):`siteId`、`siteLocation`、`siteName`、`major` |
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"studInfo": { "sId": "S0001", "sName": "张三", "sPhone": "13800000000", "sGender": "1", "deptName": "供电段", "personNo": "P1001" },
"classPlan": { "planId": "P2026001", "planName": "2026年第一期", "startTime": "2026-11-01 08:00:00", "endTime": "2026-11-30 18:00:00" },
"dormitory": { "dormId": "D01", "bedNo": "3", "dormBuilding": "1号楼", "dormRoom": "101" },
"classroom": { "classRoomId": "R01", "roomLocation": "教学楼2层", "roomName": "201" },
"trainingSite": { "siteId": "T01", "siteLocation": "实训楼1层", "siteName": "接触网实训场", "major": "接触网" }
}
}
```
> 注意:本接口**不返回** `courses` 与 `lesson`(源码中已注释),需要课表请使用 3.4。
---
### 3.4 获取学员关联资源
- **接口路径**:`/api/register/v1/student/{sId}/resources`
- **请求方法**:`GET`
- **功能说明**:在 3.3 的基础上额外返回本计划课程列表与课次(含课程名),供报到机展示课表。
**请求参数**
| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `sId` | path | string | 是 | 学员ID |
**返回结果说明**:`data` 在 3.3 全部字段之外,追加:
| 字段 | 类型 | 说明 |
|---|---|---|
| `courses` | array | 本计划课程:`cId`(计划课程主键)、`cName`(课程名称) |
| `lesson` | array | 课次列表:`startTime`、`endTime`、`planCourseId`、`cName` |
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"studInfo": { "sId": "S0001", "sName": "张三" },
"classPlan": { "planId": "P2026001", "planName": "2026年第一期" },
"courses": [ { "cId": 1001, "cName": "接触网巡视" } ],
"lesson": [ { "startTime": "2026-11-06 08:00:00", "endTime": "2026-11-06 10:00:00", "planCourseId": 1001, "cName": "接触网巡视" } ],
"dormitory": { "dormId": "D01", "bedNo": "3" },
"classroom": { "classRoomId": "R01", "roomName": "201" },
"trainingSite": { "siteId": "T01", "siteName": "接触网实训场" }
}
}
```
---
## 4. 教师信息
### 4.1 获取教师信息列表
- **接口路径**:`/api/register/v1/teacher/list`
- **请求方法**:`GET`
- **功能说明**:读取教师主档(`train_teacher`),按**与学员列表完全一致的结构**返回,便于报到机统一处理。转换规则:
- `sId` = `tId + "_teacher"`(与人脸库教师 `employee_id` 口径一致)
- `sName` = `tName`,其余字段一一映射
- 教师无报名记录,`enrollmentStatus`、`joinTime` 恒为 `null`
**请求参数**:无
**返回结果说明**:`data` 为 `StudentItemVO` 数组,实际填充字段:`sId`、`sName`、`personNo`、`sGender`、`sBirth`、`deptName`、`cardNo`、`sPhone`、`sEmail`、`status`、`avatar`、`id`。
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"sId": "T0001_teacher",
"sName": "李老师",
"personNo": "P2001",
"sGender": "1",
"sPhone": "13900000000",
"deptName": "供电段",
"status": 1,
"avatar": null,
"enrollmentStatus": null,
"joinTime": null
}
]
}
```
---
### 4.2 获取教师基础信息
- **接口路径**:`/api/register/v1/teacher/{tId}/base`
- **请求方法**:`GET`
- **功能说明**:按教师工号聚合个人信息、宿舍信息与授课课表。兼容传入 `tId + "_teacher"` 口径:路径参数以 `_teacher` 结尾时会自动去除后缀再查询。
**请求参数**
| 名称 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `tId` | path | string | 是 | 教师工号;可传 `T0001` 或 `T0001_teacher` |
**返回结果说明**:
| 字段 | 类型 | 说明 |
|---|---|---|
| `teacherInfo` | object | 教师信息:`tId`、`tName`、`tPhone`、`tGender`、`deptName`、`personNo`、`avatar`;教师不存在时为 `null` |
| `dormitory` | object | `dormId`、`bedNo`、`dormBuilding`、`dormRoom`(无有效住宿时 `dormId` 为 `null`) |
| `lesson` | array | 授课课次(按开始时间升序,不传日期范围即全部);字段:`lessonId`、`planCourseId`、`courseName`、`tId`、`teacherName`、`startTime`、`endTime`、`venueType`、`classRoomId`、`classRoomName`、`siteId`、`siteName`、`venueName`、`boardSn`、`remark` |
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"teacherInfo": { "tId": "T0001", "tName": "李老师", "tPhone": "13900000000", "tGender": "1", "deptName": "供电段", "personNo": "P2001", "avatar": null },
"dormitory": { "dormId": "D02", "bedNo": "1", "dormBuilding": "2号楼", "dormRoom": "205" },
"lesson": [
{ "lessonId": 9001, "courseName": "接触网巡视", "tId": "T0001", "teacherName": "李老师", "startTime": "2026-11-06 08:00:00", "endTime": "2026-11-06 10:00:00", "classRoomId": "R01", "classRoomName": "201", "venueName": "教学楼201" }
]
}
}
```
---
## 5. 班级名单
### 5.1 获取进行中班级的学员名单
- **接口路径**:`/api/register/v1/class/student`
- **请求方法**:`GET`
- **功能说明**:查询所有**进行中**(`planStatus = 1`)的开班计划,并按计划取报名记录中**未报到**(`enrollmentStatus = 0`)的学员,学员列表作为班级属性 `students` 一并返回。单个学员主档缺失时用报名记录的冗余字段兜底,不会中断整体返回。
**请求参数**:无
**返回结果说明**:`data` 为班级数组:
| 字段 | 类型 | 说明 |
|---|---|---|
| `planId` | string | 开班计划ID |
| `planName` | string | 开班计划名称 |
| `classId` | string | 班级ID |
| `className` | string | 班级名称 |
| `students` | array | 未报到学员:`sId`、`sName`、`deptId`、`deptName` |
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"planId": "P2026001",
"planName": "2026年第一期",
"classId": "C01",
"className": "接触网一班",
"students": [
{ "sId": "S0001", "sName": "张三", "deptId": 103, "deptName": "供电段" }
]
}
]
}
```
---
## 6. 人脸照片上传
> 三个接口均为 `POST` + `application/json`,`faceImage` 支持**纯 base64** 或 **data URI**(`data:image/png;base64,....`)两种形式;服务端统一解码并转码为 PNG 落盘,路径形如 `/profile/upload/yyyy/MM/dd/{uuid}.png`,随后写回对应主档的 `avatar` 字段。
### 6.1 上传学员人脸照片
- **接口路径**:`/api/register/v1/student/face`
- **请求方法**:`POST`
- **功能说明**:保存学员人脸照片到 `train_student.avatar`,同步人脸识别设备(`employee_id = sId`),并按住宿记录办理报到:
1. 更新报名记录 `enrollmentStatus = 1`、`joinTime = 当前时间`(存在报名记录时);
2. 若学员有有效住宿记录(按 `create_time` 降序首条且 `dormId` 非空),生成门锁业务流水号并调用入住接口 `/openapi/iot/stay/checkin`。
**请求参数**(JSON Body)
| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `sId` | string | 是 | 学员ID |
| `faceImage` | string | 是 | 人脸图片 base64,可带 `data:image/xxx;base64,` 前缀 |
**返回结果说明**:`data` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `sId` | string | 学员ID |
| `avatar` | string | 头像访问路径 |
| `faceSync` | string | 设备同步状态:`success` / `failed` / `skipped` |
| `faceMsg` | string | 同步失败原因(仅失败时存在) |
| `faceData` | any | 设备返回的非 Map 数据(成功时可能追加) |
| `lockCheckin` | object | 门锁入住结果,结构见 [1.4 lockCheckin](#14-公共数据结构)(有住宿时返回) |
| `lockAuthId` | string | 门锁授权ID,当前固定为空串(有住宿时返回) |
| `joinTime` | string | 报到时间(有住宿时返回) |
**失败场景**(`code=500`):
| msg | 触发条件 |
|---|---|
| `参数缺失:sId 与 faceImage 均为必填` | 任一参数为 `null` |
| `sId 不能为空` | `sId` 为空串 |
| `图片解析失败:...` | base64 非法 / 非图片 / 无法编码为 PNG |
| `创建存储目录失败` | 落盘目录创建失败 |
| `图片写入失败:...` | 文件写入 IO 异常 |
| `学员不存在或更新失败` | 更新 `train_student` 影响行数为 0 |
> 人脸设备同步失败不影响接口成功返回,仅通过 `faceSync=failed` + `faceMsg` 体现。
---
### 6.2 上传教师人脸照片
- **接口路径**:`/api/register/v1/teacher/face`
- **请求方法**:`POST`
- **功能说明**:保存教师人脸照片到 `train_teacher.avatar`,以 `employee_id = tId + "_teacher"` 同步人脸识别设备。教师无报名记录,**不处理报到状态**;若教师已有有效住宿且已生成门锁流水号,则补发一次入住授权(有效期 15 天)。
**请求参数**(JSON Body)
| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `teacherId` | string | 是 | 教师工号(`train_teacher.t_id`),不含 `_teacher` 后缀 |
| `faceImage` | string | 是 | 人脸图片 base64,可带 data URI 前缀 |
**返回结果说明**:`data` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `tId` | string | 教师工号 |
| `avatar` | string | 头像访问路径 |
| `faceSync` | string | 设备同步状态:`success` / `failed` / `skipped` |
| `faceMsg` | string | 同步失败原因(仅失败时存在) |
| `lockCheckin` | object | 补发入住授权结果(教师有住宿且已生成 `lockApiBizNo` 时返回) |
| `lockAuthId` | string | 当前固定为空串(补发授权时返回) |
**失败场景**(`code=500`):
| msg | 触发条件 |
|---|---|
| `参数缺失:teacherId 与 faceImage 均为必填` | 任一参数为 `null` |
| `teacherId 不能为空` | `teacherId` 为空串 |
| `图片解析失败:...` / `创建存储目录失败` / `图片写入失败:...` | 同 6.1 |
| `教师不存在或更新失败` | 更新 `train_teacher` 影响行数为 0 |
---
### 6.3 上传人脸照片(统一入口)
- **接口路径**:`/api/register/v1/face`
- **请求方法**:`POST`
- **功能说明**:按 `employeeId` 后缀分发到 6.1 / 6.2,返回被调用方法的原样结果:
- `employeeId` 以 `_teacher` 结尾 → 去掉后缀作为 `teacherId`,转 6.2;
- 其他情况 → `employeeId` 即 `sId`,转 6.1。
- 后缀匹配**区分大小写**且不做 `trim`(`_TEACHER` 会被当作学员ID)。
**请求参数**(JSON Body)
| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `employeeId` | string | 是 | 人脸库人员ID:学员 `sId` 或教师 `tId + "_teacher"` |
| `faceImage` | string | 是 | 人脸图片 base64,可带 data URI 前缀 |
**返回结果说明**:与分发目标接口完全一致(见 6.1 / 6.2 的 `data` 说明)。
**失败场景**(`code=500`):
| msg | 触发条件 |
|---|---|
| `参数缺失:employeeId 与 faceImage 均为必填` | 任一参数为 `null` |
| `employeeId 不能为空` | `employeeId` 为空串 |
| `employeeId 非法:教师工号(tId)不能为空` | `employeeId` 仅由 `_teacher` 组成 |
---
## 7. 报到业务
### 7.1 报到重置
- **接口路径**:`/api/register/v1/report/reset`
- **请求方法**:`POST`
- **功能说明**:用于“误点他人信息导致人脸错绑”的补救。按顺序执行 4 步,任一关键步骤失败即中止并返回原因:
1. 解绑人脸(复用 `TrainFaceApiController.deleteEmployee`,`employee_id = studentId`);
2. 清除门锁入住授权(复用 `TrainLockApiController.checkout`),**住宿分配记录保留**;
3. 参训状态重置为未报到 `0`(备注保持不变);
4. 清空学员头像(`train_student.avatar` 置 NULL)。
**请求参数**(Query / Form,`@RequestParam`,非 JSON Body)
| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `planId` | string | 是 | 开班计划ID(`train_enrollment.plan_id`) |
| `studentId` | string | 是 | 学员ID(`train_enrollment.student_id`) |
**返回结果说明**:`data` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `planId` | string | 开班计划ID |
| `studentId` | string | 学员ID |
| `enrollmentId` | number | 参训记录主键 |
| `previousStatus` | number | 重置前的报到状态 |
| `faceUnbind` | string | 人脸解绑结果,成功为 `success` |
| `lockCheckout` | string | 门锁授权清除结果:`success` / `skipped: 无有效住宿记录` / `skipped: 缺少门锁业务流水号与宿舍信息` |
| `lockApiBizNo` | string | 门锁业务流水号(清除成功时返回) |
| `currentStatus` | number | 重置后状态,固定 `0` |
| `alreadyReset` | boolean | 重置前是否已是未报到 |
| `accommodationKept` | boolean | 是否保留了住宿记录 |
| `avatarCleared` | boolean | 头像是否已清空(成功路径) |
| `avatarSId` | string | 清空头像的学员ID(成功路径) |
**失败场景**(`code=500`):
| msg | 触发条件 |
|---|---|
| `planId 和 studentId 不能为空` | 任一参数为空 |
| `未找到学员[xxx]在班级[yyy]的参训记录` | 报名记录不存在 |
| `学员已放弃培训,不允许重置报到状态` | `previousStatus = 3` |
| `学员已结训,不允许重置报到状态` | `previousStatus = 4` |
| `步骤1 人脸解绑失败:...;参训状态未变更` | 人脸解绑未返回 200 |
| `步骤2 门锁授权清除响应解析失败:...;人脸已解绑,参训状态未变更` | 下游响应非法 JSON |
| `步骤2 门锁授权清除失败:...;人脸已解绑,参训状态未变更` | 下游返回非 200 |
| `步骤3 参训状态重置失败;人脸与门锁授权已解除,请人工核对后重试` | 状态更新影响行数为 0 |
| `报到重置已完成,但未找到有效学员档案[xxx],avatar 未清空` | 主档缺失(`data` 仍返回已完成信息) |
| `报到重置已完成,但清空学员头像失败:...` | 头像更新异常(`data` 仍返回已完成信息) |
```json
{
"code": 200,
"msg": "报到重置成功",
"data": {
"planId": "P2026001",
"studentId": "S0001",
"enrollmentId": 5001,
"previousStatus": 1,
"faceUnbind": "success",
"lockCheckout": "success",
"lockApiBizNo": "STAY20261105001",
"currentStatus": 0,
"alreadyReset": false,
"accommodationKept": true,
"avatarCleared": true,
"avatarSId": "S0001"
}
}
```
---
## 8. 宿舍布局
### 8.1 获取宿舍布局信息
- **接口路径**:`/api/register/v1/dormitory/layout`
- **请求方法**:`GET`
- **功能说明**:查询 `train_dormitory_info`(仅未删除房间),依次按 `dorm_building`(楼栋)→ `dorm_floor`(楼层)→ `dorm_room`(房间号)分组;并左关联 `train_accommodation` 统计每个房间的已分配床位数(住宿状态 `0 预分配` / `1 已入住` 计入,`2 已退宿` 不计),总床位数取房间核定容量 `capacity`。
**请求参数**:无
**返回结果说明**:
- `data` 为三层嵌套对象,键依次为「楼栋名称 → 楼层 → 房间号」,每间房的值包含:
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | number | 宿舍自增编号(`train_dormitory_info.id`) |
| `dorm_room` | string | 房间号 |
| `accommodation` | string | 已分配床位数/总床位数,如 `"2/4"` |
- 楼栋、楼层、房间均按数据库升序排列(`dorm_building, dorm_floor, dorm_room`),嵌套对象使用有序 Map 输出,顺序稳定。
- 无房间数据时 `data` 为空对象 `{}`。
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"1号楼": {
"1": {
"101": { "id": 1, "dorm_room": "101", "accommodation": "2/4" },
"102": { "id": 2, "dorm_room": "102", "accommodation": "0/4" }
},
"2": {
"201": { "id": 5, "dorm_room": "201", "accommodation": "4/6" }
}
},
"2号楼": {
"1": {
"101": { "id": 9, "dorm_room": "101", "accommodation": "1/2" }
}
}
}
}
```
> 说明:同一楼栋内房间号唯一(约束 `uq_dorm_building_room`),因此以房间号作为楼层下的键不会冲突;楼层为空时键为 `未标注楼层`。
---
## 9. 附录
### 9.1 典型调用流程
1. `GET /student/list` 或 `GET /class/student` —— 报到机展示待报到人员。
2. `GET /teacher/list` —— 展示教师名单(`sId` 形如 `T0001_teacher`)。
3. 人员点击后:`GET /student/{sId}/resources`(或 `/base`)—— 展示开班、宿舍、教室、课表。
4. 采集人脸后:`POST /face`(统一入口,传 `employeeId` + `faceImage`)—— 完成照片入库、设备下发、报到登记、门锁入住。
5. 宿舍看板:`GET /dormitory/layout` —— 按楼栋/楼层/房间展示床位占用情况。
6. 误绑时:`POST /report/reset?planId=...&studentId=...` —— 回退报到与人脸/门锁授权。
### 9.2 调用示例
```bash
# 学员列表
curl "http://localhost:8096/api/register/v1/student/list"
# 学员关联资源
curl "http://localhost:8096/api/register/v1/student/S0001/resources"
# 教师基础信息(tId 可带 _teacher 后缀)
curl "http://localhost:8096/api/register/v1/teacher/T0001_teacher/base"
# 统一人脸上传
curl -X POST "http://localhost:8096/api/register/v1/face" \
-H "Content-Type: application/json" \
-d '{"employeeId":"S0001","faceImage":"data:image/jpeg;base64,/9j/4AAQSkZJRg..."}'
# 报到重置(需登录态)
curl -X POST "http://localhost:8096/api/register/v1/report/reset?planId=P2026001&studentId=S0001" \
-H "Authorization: Bearer {token}"
# 宿舍布局
curl "http://localhost:8096/api/register/v1/dormitory/layout"
```
### 9.3 与源码注释的差异说明
| 位置 | 注释/声明 | 实际实现 |
|---|---|---|
| 类注释 | 登录接口需 HMAC 签名(`X-Signature`/`X-Nonce`) | 控制器内未实现;`DeviceHmacFilter` 仅覆盖 `/api/iot/v1/device/login` |
| `SecurityConfig` 注释 | `/api/register/v1/**` 走 Bearer 鉴权 | 已配置 `permitAll`,除 `/report/reset` 外不校验 Token |
| `/student/{sId}/base` | 注释称返回“课表、实训场地” | 实际不返回 `courses`、`lesson`,需调用 `/student/{sId}/resources` |