Files
railwaysystem/RuoYi-Vue/doc/电子班牌接口API文档.md
T

613 lines
24 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.TrainBoardApiController` |
| 源文件 | `ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TrainBoardApiController.java` |
| BasePath | `/api/board/v1` |
| 接口数量 | 7(4 个查询类 + 1 个签到上报 + 2 个新增能力) |
| 鉴权 | RuoYi Bearer Token(`Authorization: Bearer {token}`) |
| 文档更新 | 2026-11-03 |
| 编写依据 | 控制器源码逐行核对,与当前实现一致 |
---
## 目录
- [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-查询班级学员卡号)
- [3.5 上报签到数据](#35-上报签到数据)
- [3.6 查询本节课签到记录](#36-查询本节课签到记录)
- [3.7 查询座位布局](#37-查询座位布局)
- [4. 异常与错误汇总](#4-异常与错误汇总)
- [5. 调用示例](#5-调用示例)
- [6. 附录](#6-附录)
---
## 1. 通用约定
### 1.1 鉴权
- 所有接口均需携带 `Authorization: Bearer {token}`,Token 由平台登录接口签发。
- 未携带或 Token 失效时,返回 **HTTP 200** + 响应体 `code=401`:
```json
{ "code": 401, "msg": "请求访问:/api/board/v1/signageService/selectClassroom,认证失败,无法访问系统资源" }
```
> 本控制器未使用 `@PreAuthorize` 数据权限注解,因此不存在 `403` 业务场景。
### 1.2 统一响应结构
所有接口返回 `AjaxResult`:
```json
{ "code": 200, "msg": "操作成功", "data": {} }
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | number | 业务状态码,见 1.3 |
| `msg` | string | 提示信息;成功固定为 `操作成功` |
| `data` | object / array | 业务数据,无数据时字段不出现 |
### 1.3 状态码
| code | 含义 | HTTP 状态 | 触发来源 |
|---|---|---|---|
| 200 | 成功 | 200 | 正常返回 |
| 401 | 未认证 / Token 无效 | **200** | `AuthenticationEntryPointImpl` |
| 404 | 接口路径不存在 | 404 | `NoResourceFoundException` |
| 500 | 参数缺失、参数格式错误、数据校验失败、控制器内业务拦截、系统异常 | 200 | `GlobalExceptionHandler` / 控制器返回值 |
| 40080 / 40081 / 40085 / 40086 / 40087 | 考勤业务码(计划不一致 / 课次不存在 / 未报到 / 已放弃 / 已结训) | 200 | `ServiceException`(`AttendanceErrorCode`) |
> **调用方必须判断响应体 `code`**:除 404 外,失败场景的 HTTP 状态码仍是 200。
### 1.4 列表数据包装
除 `3.1 查询教室信息`(`data` 直接是数组)外,其余查询接口的 `data` 为:
```json
{ "total": 2, "rows": [ ... ] }
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | number | 本次返回的记录数(非分页总数) |
| `rows` | array | 数据列表,无数据时为 `[]` |
### 1.5 字段类型说明
类注释约定 `id / classId / classroomId / courseId / teacherId / stuId / studentId` 为 Long,
但本系统相关表以业务编码为主键(字符串),因此这些字段**返回字符串**;
仅 `lessonId(id)`、`enrollmentId`、`zoneId`、`seatId`、`capacity`、`classHour` 等为数值。
---
## 2. 接口总览
| # | 接口名称 | 方法 | 路径 | 用途 |
|---|---|---|---|---|
| 3.1 | 查询教室信息 | GET | `/api/board/v1/signageService/selectClassroom` | 返回全部教学教室与实训场地 |
| 3.2 | 查询指定教室当天课程 | GET | `/api/board/v1/signageService/coursesInClassroom` | 按教室与日期范围返回排课 |
| 3.3 | 查询指定班级在指定教室的座位 | GET | `/api/board/v1/signageService/studentInClassroom` | 返回座位网格(含学员姓名) |
| 3.4 | 查询班级学员卡号 | GET | `/api/board/v1/signageService/studentCard` | 刷卡校验用卡号与班级信息 |
| 3.5 | 上报签到数据 | POST | `/api/board/v1/signageService/checkin/upload` | JSON 上报签到并落库 |
| 3.6 | 查询本节课签到记录 | GET | `/api/board/v1/signageService/checkin/list` | 按课次返回签到流水与学员姓名 |
| 3.7 | 查询座位布局 | GET | `/api/board/v1/signageService/seatLayout` | 仅读 `train_class_seat` 的座位布局 |
---
## 3. 接口详情
### 3.1 查询教室信息
**用途**:返回全部教学教室 + 实训场地(`train_classroom_info` UNION `train_training_site`),供班牌后台定位终端绑定教室。
| 项 | 内容 |
|---|---|
| HTTP 方法 | `GET` |
| 请求路径 | `/api/board/v1/signageService/selectClassroom` |
| 鉴权 | 需要 |
**请求参数**:无。
**请求体**:无。
**响应结构**:`data` 为数组(**不是** `{total, rows}` 包装)。
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 教室ID(`class_room_id`)或场地ID(`site_id`) |
| `name` | string | 教室/场地名称 |
| `campus` | string | 校区/位置 |
| `capacity` | number | 容量 |
| `outside` | number | `0`=教室,`1`=实训场地 |
| `category` | string | `教室` / `实训场地` |
**成功示例**
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{ "id": "CR01", "name": "多媒体教室01", "campus": "一号教学楼3层", "capacity": 60, "outside": 0, "category": "教室" },
{ "id": "SITE01", "name": "信号实训场", "campus": "实训楼1层", "capacity": 40, "outside": 1, "category": "实训场地" }
]
}
```
**异常说明**:未认证 → `code=401`;无数据 → `data: []`。
---
### 3.2 查询指定教室当天课程
**用途**:按教室/场地与日期范围(含首尾日期)返回排课列表。文档约定 `beginDate=endDate=当天`,接口本身不限制两者相等。
| 项 | 内容 |
|---|---|
| HTTP 方法 | `GET` |
| 请求路径 | `/api/board/v1/signageService/coursesInClassroom` |
| 鉴权 | 需要 |
**请求参数(Query String)**
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `classroomId` | string | 是 | 无 | 教室ID(`class_room_id`)或场地ID(`site_id`),SQL 同时匹配两列 |
| `beginDate` | string | 是 | 无 | 开始日期 `yyyy-MM-dd`,条件 `start_time >= beginDate` |
| `endDate` | string | 是 | 无 | 结束日期 `yyyy-MM-dd`,条件 `start_time < endDate + 1 天`(含当天) |
**请求体**:无。
**响应结构**:`data = { total, rows[] }`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | number | 排课ID(`train_lesson.lesson_id`),**即 3.5 的 `lessonId`** |
| `courseId` | string | 课程主数据ID(`train_course.c_id`) |
| `classId` | string | 班级ID(`train_class_plan.plan_id`) |
| `classroomId` | string | 教室ID或场地ID(`COALESCE(class_room_id, site_id)`) |
| `courseName` | string | 课程名称 |
| `couresName` | string | 兼容旧拼写的课程名称(与 `courseName` 同值) |
| `courseTime` | string | 上课时间 `yyyy-MM-dd HH:mm:ss` |
| `classHour` | number | 时长(分钟) |
| `className` | string | 班级名称 |
| `teacherId` | string | 教师ID(`train_lesson.t_id`) |
| `teacherName` | string | 教师姓名,未排教师为 `null` |
| `courseIntro` | string | 课程简介(`train_course.teaching_objective`) |
| `isPublic` | number | 固定 `0` |
**成功示例**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"total": 1,
"rows": [
{
"id": 1001, "courseId": "CR01", "classId": "PLAN_A", "classroomId": "CR01",
"courseName": "电子班牌数字化实务", "couresName": "电子班牌数字化实务",
"courseTime": "2026-11-02 09:00:00", "classHour": 90,
"className": "2026年第一期培训班", "teacherId": "T101", "teacherName": "张三",
"courseIntro": "班牌设备安装与配置", "isPublic": 0
}
]
}
}
```
**异常说明**
| 场景 | 响应 |
|---|---|
| 缺少 `classroomId` | `{"code":500,"msg":"Required request parameter 'classroomId' for method parameter type String is not present"}` |
| 无排课 | `{"code":200,"msg":"操作成功","data":{"total":0,"rows":[]}}` |
---
### 3.3 查询班级学员卡号
**用途**:按班级返回学员卡号与班级、住宿信息,用于班牌刷卡校验。仅返回参训状态非“已放弃(3)”的学员。
| 项 | 内容 |
|---|---|
| HTTP 方法 | `GET` |
| 请求路径 | `/api/board/v1/signageService/studentCard` |
| 鉴权 | 需要 |
**请求参数(Query String)**
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `classId` | string | 是 | 无 | 班级ID(`train_enrollment.plan_id`) |
**请求体**:无。
**响应结构**:`data = { total, rows[] }`
| 字段 | 类型 | 说明 |
|---|---|---|
| `cardNo` | string | 卡号(`train_student.card_no`,保留前导零) |
| `studentId` | string | 学员ID(`s_id`) |
| `classId` | string | 班级ID |
| `studentName` | string | 学员姓名 |
| `className` | string | 班级名称 |
| `id` | number | 参训档案ID(`enrollment_id`,**不是学员ID**) |
| `roomId` | string | 宿舍ID,未分配为 `null` |
| `bedType` | string | 床位类型(预留,固定 `null`) |
| `seatNumber` | string | 床位号,未分配为 `null` |
**成功示例**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"total": 1,
"rows": [
{ "cardNo": "00012345", "studentId": "STU_1", "classId": "PLAN_A", "studentName": "王五", "className": "2026年第一期培训班", "id": 201, "roomId": "DORM-101", "bedType": null, "seatNumber": "1" }
]
}
}
```
**异常说明**:缺少 `classId` → `code=500`;无数据 → `{total:0, rows:[]}`。
---
### 3.4 上报签到数据
**用途**:班牌上报学员签到。服务端按 `studentId` 在课次所属培训班内查询 `train_enrollment` 得到 `enrollmentId`,写入 `train_student_checkin`(`student_id`、`board_id`、`lesson_id`、`sign_in_time` 一并落库)。
| 项 | 内容 |
|---|---|
| HTTP 方法 | `POST` |
| 请求路径 | `/api/board/v1/signageService/checkin/upload` |
| 鉴权 | 需要 |
| Content-Type | **必须 `application/json`** |
| 文件上传 | 无 |
**请求参数**:无 Query 参数,全部字段在请求体中。
**请求体结构**(`BoardCheckinUploadRequest`)
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `studentId` | string | 是 | 无 | 学员ID(`train_student.s_id`),首尾空格会被去除 |
| `boardId` | string | 否 | `null` | 门牌(班牌)ID,写入 `train_student_checkin.board_id`;空串按 `null` 处理 |
| `lessonId` | number(long) | 是 | 无 | 课程(课次)ID,对应 `train_student_checkin.lesson_id`,取值来自 3.2 的 `id` |
| `signInTime` | string | 否 | 服务器当前时间 | 签到时间,格式 `yyyy-MM-dd HH:mm:ss`;格式非法直接报错,不静默改写 |
```json
{
"studentId": "STU_1",
"boardId": "BP001",
"lessonId": 1001,
"signInTime": "2026-11-02 08:30:00"
}
```
**响应结构**(`data` 为单对象)
| 字段 | 类型 | 说明 |
|---|---|---|
| `checkinId` | number | 考勤记录ID |
| `studentId` | string | 学员ID(库中 `student_id`,为空时回填请求值) |
| `enrollmentId` | number | 参训档案ID(由 `studentId` + 班次推导) |
| `lessonId` | number | 课次ID |
| `boardId` | string | 班牌ID,未传为 `null` |
| `signInTime` | string | 实际落库的签到时间 `yyyy-MM-dd HH:mm:ss` |
**成功示例**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"checkinId": 9001, "studentId": "STU_1", "enrollmentId": 201,
"lessonId": 1001, "boardId": "BP001", "signInTime": "2026-11-02 08:30:00"
}
}
```
**异常与错误说明**
| 场景 | code | 响应 msg |
|---|---|---|
| `studentId` 为空 | 500 | `studentId 不能为空` |
| `lessonId` 为空 | 500 | `lessonId 不能为空` |
| `signInTime` 格式错误 | 500 | `signInTime 格式不正确,应为 yyyy-MM-dd HH:mm:ss` |
| 课次不存在 | 500 | `课程(课次)不存在: 1001` |
| 课次未关联有效培训计划 | 500 | `课程(课次)[1001]未关联有效的培训计划` |
| 学员无该班参训记录 | 500 | `未找到学员[STU_1]在班级[PLAN_A]的参训记录` |
| 非 JSON / 空请求体 | 500 | Spring 反序列化异常信息(如 `Content-Type 'text/plain' is not supported`) |
| 未认证 | 401 | `请求访问:/api/board/v1/signageService/checkin/upload,认证失败,无法访问系统资源` |
| 学员未报到 / 已放弃 / 已结训 | 40085 / 40086 / 40087 | 对应中文提示(见 `AttendanceErrorCode`) |
| 课次所属计划与参训计划不一致 | 40080 | `业务不变量校验失败:考勤学员与课次所属培训班不一致` |
**业务规则**
1. **幂等**:同一 `enrollmentId + lessonId` 已有签到记录(`sign_in_time` 非空)时返回已有记录,不覆盖首次签到时间。
2. **生命周期**:仅“已报到(1)”“请假(2)”状态允许签到。
3. **计划一致性**:学员参训档案所属培训班必须与课次所属培训班一致。
4. **打卡方式**:固定按设备打卡 `checkinWay=1` 落库。
---
### 3.5 查询本节课签到记录
**用途**:按 `lessonId` 返回 `train_student_checkin` 签到流水,并按 `studentId` 关联 `train_student` 返回学员姓名(`s_name`)。
| 项 | 内容 |
|---|---|
| HTTP 方法 | `GET` |
| 请求路径 | `/api/board/v1/signageService/checkin/list` |
| 鉴权 | 需要 |
**请求参数(Query String)**
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `lessonId` | number(long) | 是 | 无 | 课程(课次)ID;非数字时返回参数类型不匹配错误 |
**请求体**:无。
**响应结构**:`data = { total, rows[] }`,`rows[]` 为 `TrainStudentCheckin`:
| 字段 | 类型 | 说明 |
|---|---|---|
| `checkinId` | number | 考勤记录ID |
| `enrollmentId` | number | 参训档案ID |
| `lessonId` | number | 课次ID |
| `studentId` | string | 学员ID(优先取 `train_student_checkin.student_id`,为空取参训档案的 `student_id`) |
| `studentName` | string | 学员姓名(`train_student.s_name`) |
| `planId` | string | 培训班ID |
| `planName` | string | 培训班名称 |
| `className` | string | 班级名称 |
| `courseName` | string | 课程名称 |
| `boardId` | string | 打卡班牌ID |
| `boardSn` | string | 班牌序列号 |
| `boardName` | string | 班牌名称 |
| `checkinWay` | number | 打卡方式:1 人脸/设备,2 刷卡,3 扫码,4 教师代签 |
| `signInTime` | string | 签到时间 `yyyy-MM-dd HH:mm:ss`,未签到为 `null` |
| `signOutTime` | string | 签退时间 `yyyy-MM-dd HH:mm:ss`,未签退为 `null` |
| `attendanceStatus` | number | 考勤结论:0 缺勤,1 正常,2 迟到,3 早退,4 请假 |
| `manualFlag` | number | 是否人工修正:0 否,1 是 |
| `createBy` / `createTime` / `updateBy` / `updateTime` / `remark` | string | 审计字段(时间格式 `yyyy-MM-dd HH:mm:ss`) |
> `params` 为空时不输出;`searchValue` 始终不输出。
**成功示例**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"total": 1,
"rows": [
{
"checkinId": 9001, "enrollmentId": 201, "lessonId": 1001,
"studentId": "STU_1", "studentName": "张三",
"planId": "PLAN_A", "planName": "2026年第一期培训班", "className": "一班",
"courseName": "电子班牌数字化实务",
"boardId": "BP001", "boardSn": "BP001", "boardName": "1号班牌",
"checkinWay": 1, "signInTime": "2026-11-02 08:30:00", "signOutTime": null,
"attendanceStatus": 1, "manualFlag": 0,
"createBy": "board_device", "createTime": "2026-11-02 08:30:01",
"updateBy": null, "updateTime": null, "remark": null
}
]
}
}
```
**异常与错误说明**
| 场景 | 响应 |
|---|---|
| 缺少 `lessonId` | `{"code":500,"msg":"Required request parameter 'lessonId' for method parameter type Long is not present"}` |
| `lessonId` 非数字 | `{"code":500,"msg":"请求参数类型不匹配,参数[lessonId]要求类型为:'java.lang.Long',但输入值为:'abc'"}` |
| 课次无签到记录 | `{"code":200,"msg":"操作成功","data":{"total":0,"rows":[]}}` |
---
### 3.6 查询座位布局
**用途**:按班级与教室查询座位布局,**仅读取 `train_class_seat` 单表**,不关联学员、教室、班级表;不校验班级是否删除、学员是否存在。
| 项 | 内容 |
|---|---|
| HTTP 方法 | `GET` |
| 请求路径 | `/api/board/v1/signageService/seatLayout` |
| 鉴权 | 需要 |
**请求参数(Query String)**
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `planId` | string | 是 | 无 | 班级ID,对应 `train_class_seat.plan_id` |
| `classroomId` | string | 是 | 无 | 教室ID,对应 `train_class_seat.classroom_id` |
**请求体**:无。
**响应结构**:`data = { total, rows[] }`
| 字段 | 类型 | 说明 |
|---|---|---|
| `seatId` | number | 座位主键(`seat_id`) |
| `planId` | string | 班级ID(`plan_id`) |
| `classroomId` | string | 教室ID(`classroom_id`) |
| `rowIndex` | number | 行号,从 1 开始 |
| `colIndex` | number | 列号,从 1 开始 |
| `seatCode` | string | 物理座位标识(`seat_code`) |
| `canceled` | number | `0`=正常,`1`=取消位置(**不做过滤,原样返回**) |
| `studentId` | string | 占用座位学员ID,空座为 `null` |
| `zoneId` | number | 区域ID |
| `deptDetail` | string | 部门 |
| `job` | string | 职务 |
排序:`row_index, col_index` 升序。
**成功示例**
```json
http://localhost:8096/api/board/v1/signageService/seatLayout?classroomId=afcf21a8bd99488cb19638d285f157a8&planId=f70ec13a651942e8a704ee4cc010c95e
{
"msg": "操作成功",
"code": 200,
"data": {
"total": 2,
"rows": [
{
"canceled": 0,
"classroomId": "afcf21a8bd99488cb19638d285f157a8",
"colIndex": 5,
"deptDetail": null,
"job": null,
"planId": "f70ec13a651942e8a704ee4cc010c95e",
"rowIndex": 2,
"seatCode": "2-5",
"seatId": 29,
"studentId": "c78c425e66384104bf37d150519fc5d3",
"zoneId": null
},
{
"canceled": 0,
"classroomId": "afcf21a8bd99488cb19638d285f157a8",
"colIndex": 9,
"deptDetail": null,
"job": null,
"planId": "f70ec13a651942e8a704ee4cc010c95e",
"rowIndex": 3,
"seatCode": "3-9",
"seatId": 30,
"studentId": "63d889e8be2c4d61900d09b51b2108a5",
"zoneId": null
}
]
}
}
```
**异常与错误说明**
| 场景 | 响应 |
|---|---|
| 缺少 `planId` 或 `classroomId` | `code=500`,`msg` 为 Spring 缺参提示 |
| 该班该教室无座位数据 | `{"code":200,"msg":"操作成功","data":{"total":0,"rows":[]}}` |
| 表 `train_class_seat` 不存在 | `code=500`,`msg` 含 `Table 'xxx.train_class_seat' doesn't exist`(需先执行 `sql/train/v2/019_signage.sql`) |
---
## 4. 异常与错误汇总
| 类型 | 返回形式 | 说明 |
|---|---|---|
| 未认证 | HTTP 200,`code=401` | Token 缺失、过期、伪造 |
| 路径不存在 | HTTP 404,`code=404`,`msg=接口不存在或未启用` | 路由未匹配 |
| 缺少必填 Query 参数 | HTTP 200,`code=500` | `Required request parameter 'xxx' ... is not present` |
| 参数类型不匹配 | HTTP 200,`code=500` | `请求参数类型不匹配,参数[lessonId]要求类型为:'java.lang.Long',但输入值为:'abc'` |
| 请求方法不支持 | HTTP 200,`code=500` | `Request method 'GET' is not supported` |
| 请求体非 JSON | HTTP 200,`code=500` | `Content-Type 'xxx' is not supported` |
| 控制器内业务拦截 | HTTP 200,`code=500` | 见 3.5 异常表 |
| 考勤业务异常 | HTTP 200,`code=40080/40081/40085/40086/40087` | `AttendanceErrorCode` |
`AttendanceErrorCode` 常用码:
| code | 含义 |
|---|---|
| 40080 | 考勤学员与课次所属培训班不一致 |
| 40081 | 课次不存在 |
| 40085 | 学员尚未完成开班报到,禁止录入课次考勤 |
| 40086 | 学员已放弃培训 |
| 40087 | 学员已结训 |
---
## 5. 调用示例
```bash
# 登录获取 token(按平台实际登录接口调整)
TOKEN=$(curl -s -X POST 'http://192.168.106.24:8096/login' \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}' | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
# 3.1 教室信息
curl -s 'http://192.168.106.24:8096/api/board/v1/signageService/selectClassroom' \
-H "Authorization: Bearer $TOKEN"
# 3.2 指定教室当天课程
curl -s 'http://192.168.106.24:8096/api/board/v1/signageService/coursesInClassroom?classroomId=CR01&beginDate=2026-11-02&endDate=2026-11-02' \
-H "Authorization: Bearer $TOKEN"
# 3.3 指定班级在指定教室的座位
curl -s 'http://192.168.106.24:8096/api/board/v1/signageService/studentInClassroom?classroomId=CR01&classId=PLAN_A' \
-H "Authorization: Bearer $TOKEN"
# 3.4 班级学员卡号
curl -s 'http://192.168.106.24:8096/api/board/v1/signageService/studentCard?classId=PLAN_A' \
-H "Authorization: Bearer $TOKEN"
# 3.5 上报签到(JSON 请求体)
curl -s -X POST 'http://192.168.106.24:8096/api/board/v1/signageService/checkin/upload' \
-H 'Content-Type: application/json' -H "Authorization: Bearer $TOKEN" \
-d '{"studentId":"STU_1","boardId":"BP001","lessonId":1001,"signInTime":"2026-11-02 08:30:00"}'
# 3.6 本节课签到记录
curl -s 'http://192.168.106.24:8096/api/board/v1/signageService/checkin/list?lessonId=1001' \
-H "Authorization: Bearer $TOKEN"
# 3.7 座位布局
curl -s 'http://192.168.106.24:8096/api/board/v1/signageService/seatLayout?planId=P2026001&classroomId=CR-001' \
-H "Authorization: Bearer $TOKEN"
```
---
## 6. 附录
### 6.1 数据来源
| 接口 | 主要数据表 |
|---|---|
| 3.1 | `train_classroom_info`、`train_training_site` |
| 3.2 | `train_lesson`、`train_plan_course`、`train_course`、`train_class_plan`、`train_teacher` |
| 3.3 | `train_class_seat`、`train_class_plan`、`train_student` |
| 3.4 | `train_enrollment`、`train_student`、`train_class_plan`、`train_accommodation`、`train_dormitory_info` |
| 3.5 | `train_lesson`、`train_plan_course`、`train_enrollment`、`train_student_checkin` |
| 3.6 | `train_student_checkin`、`train_enrollment`、`train_student`、`train_class_plan`、`train_plan_course`、`train_course`、`train_electronic_board` |
| 3.7 | `train_class_seat`(单表) |
### 6.2 Swagger
Swagger UI:`/swagger-ui/index.html`,分组 `电子班牌接口`。
若 3.5 的 Request body 显示为 query 参数,说明部署的是旧包(当前实现为 `@RequestBody`)。
### 6.3 已知约束
1. `lessonId` 是**课次ID(`train_lesson.lesson_id`,数值)**,不是课程编码(`c_id`,如 `CR01`);班牌须先调用 3.2 获取 `id`。
2. 3.5 只接受 JSON 请求体,query/form 写法无效。
3. 3.5 依赖 `train_student_checkin.student_id` 列;报 `Unknown column 'a.student_id'` 时需执行 `sql/train/v2/020b_add_checkin_student_id_mysql.sql`(MySQL)或 `sql/train/v2/020_add_checkin_student_id.sql`(KingbaseES/PostgreSQL)。
4. 3.3 与 3.7 的区别:3.3 关联 `train_student` 返回姓名并过滤已删除班级;3.7 严格单表返回,含 `canceled=1` 的座位,不做班级有效性校验。
5. 所有查询接口的 `total` 为本次返回条数,不是分页总数。