Files
BirdsProject/APP端API接口文档.md
2026-10-09 10:51:39 +08:00

1629 lines
45 KiB
Markdown
Raw Permalink 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.
# 智能飞禽物联平台 APP 端 API 接口文档
> 文档版本:v2.0
> 生成日期:2026-09-20
> 面向对象:Android、iOS、跨端 APP 前端开发人员
> 代码基线:`bird_monitoring` 当前 `HEAD`(`bc1074d`)
> 接口范围:APP 认证、个人中心、设备、直播、版本检查、消息中心
> 接口数量:35 个 APP 业务接口,其中 34 个可用、1 个旧接口已停用;另说明动态返回的 MJPEG 播放地址
本文档以当前后端 Controller、BO、VO、Service、统一异常处理和 Jackson 配置为事实来源。它不包含后台管理端、工作流、代码生成器、演示模块和设备 MQTT 上下行协议。
---
## 1. 接入约定
### 1.1 Base URL
本地后端默认地址:
```text
http://localhost:8081
```
当前 Servlet Context Path 为 `/`,因此完整示例为:
```text
POST http://localhost:8081/app/auth/login
GET http://localhost:8081/app/device/list
```
生产环境只替换协议、域名和端口,本文中的接口路径不变。建议 APP 只在一个环境配置项中维护 Base URL,不要在页面中拼接域名。
### 1.2 请求头与登录态
建议所有 APP JSON 请求统一发送:
```http
Content-Type: application/json;charset=utf-8
clientid: app
```
登录后的受保护接口还必须发送:
```http
Authorization: Bearer <token>
```
| 请求头 | 是否必填 | 使用范围 | 说明 |
|---|---:|---|---|
| `Content-Type` | 有 JSON Body 时是 | `POST`、`PUT` | 固定为 `application/json;charset=utf-8` |
| `Authorization` | 受保护接口是 | 登录后接口 | 必须是 `Bearer`、一个空格、再接登录返回的 token |
| `clientid` | 受保护接口是 | 登录后接口 | 当前固定值 `app`,必须与 token 内的客户端标识一致 |
| `Content-Language` | 否 | 全部 | 可传 `zh_CN`;部分校验文案支持国际化 |
公共接口:
- `/app/auth/**` 全部使用 `@SaIgnore`,不要求登录。
- `GET /app/version/latest` 使用 `@SaIgnore`,不要求登录。
- 公共接口当前不会校验 token 与 `clientid`,但建议请求封装仍统一携带 `clientid: app`。
- `POST /app/auth/logout` 本身允许未登录调用;若要注销当前会话,必须携带当前 token。
受保护接口:
- `/app/user/**`
- `/app/device/**`
- `/app/stream/**`
- `/app/message/**`
APP token 只允许访问 `/app/**`。拿 APP token 调后台 `/system/**`、`/device/**` 等路径会被拒绝。
当前默认 token 固定有效期和活跃超时均为 30 天;允许同一账号多端同时登录,每次登录生成独立 token。普通退出只注销当前 token,注销账号会清理该账号的全部会话。
### 1.3 统一响应结构
除 MJPEG 图片/视频流外,所有接口都返回:
```ts
export interface ApiResponse<T> {
code: number;
msg: string;
data: T | null;
}
```
带业务数据的成功响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"token": "eyJ..."
}
}
```
只返回操作结果的成功响应:
```json
{
"code": 200,
"msg": "修改成功",
"data": null
}
```
业务失败响应:
```json
{
"code": 500,
"msg": "验证码错误",
"data": null
}
```
| `code` | 含义 | APP 处理建议 |
|---:|---|---|
| `200` | 成功 | 使用 `data`;操作类接口可展示 `msg` |
| `400` | JSON 或参数格式错误 | 展示 `msg`,保留当前表单 |
| `401` | 未登录、token 失效、`clientid` 不匹配 | 清 token,停止重试,跳转登录页 |
| `403` | token 无权访问当前路径 | 展示 `msg`,不要循环重试 |
| `405` | HTTP Method 错误 | 作为客户端版本缺陷上报 |
| `500` | Bean Validation 或业务校验失败 | `msg` 通常可直接展示给用户 |
| `601` | 警告 | 使用警告样式展示 `msg` |
重要:业务异常和多数鉴权异常会包装在响应体中,HTTP 状态不一定同步为失败状态。APP 必须判断响应体 `code`,不能只依赖 HTTP 2xx。
### 1.4 JSON 类型与空值
当前全局 Jackson 配置对前端类型有以下影响:
| Java 类型 | JSON 表现 | APP 建议类型 |
|---|---|---|
| `Long` | 安全整数范围内为 number,超出后为 string | `string \| number` |
| 雪花 ID | 通常超出 JS 安全整数,实际通常为 string | `string \| number`,业务中按 string 使用 |
| `BigDecimal` | 固定序列化为 string | `string \| null` |
| `Integer` | number | `number` |
| `Boolean` | boolean | `boolean` |
| `LocalDateTime` | `yyyy-MM-dd HH:mm:ss` | `string` |
因此:
- `lng`、`lat` 是十进制字符串,例如 `"116.397128"`,不是 JSON number。
- `messageId`、`alertId` 等雪花 ID 不得转成 JavaScript `number` 后再回传,否则可能丢精度。
- `fileSize`、`expireAt`、未读数等较小 `Long` 当前通常是 number,但客户端仍建议兼容 `string | number`。
- 可空字段会返回 `null`;不要把 `null`、空字符串和 `0` 混为一类。
- 日期时间不含时区偏移,按服务端本地时区解释;若 APP 需要跨时区展示,应在客户端明确约定时区。
### 1.5 分页结构
消息列表使用分页响应:
```ts
export interface PageResult<T> {
total: number;
rows: T[];
}
```
示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"total": 26,
"rows": []
}
}
```
设备列表不分页,直接返回数组。
### 1.6 通用输入规则
| 数据 | 当前规则 |
|---|---|
| 中国大陆手机号 | `13800138000`、`+8613800138000`、`008613800138000` 均可;服务端统一保存为 11 位号码 |
| 国际手机号 | 必须带 `+` 或 `00` 国家码,总数字长度 7-15 位,如 `+14155552671` |
| 手机号字符 | 请求 DTO 校验不接受空格、括号或连字符,提交前必须清理 |
| 邮箱 | 最长 50 字符;服务端去首尾空白并转小写;可通过配置限制允许域名 |
| 验证码 | 6 位数字、5 分钟有效、验证成功后立即失效 |
| 密码 | 8-20 位,同时包含大写字母、小写字母和数字,不允许空白字符 |
| IMEI | 15 位数字 |
| 设备名称 | 1-20 个字符,中文按一个字符计算 |
| `deviceNo` | 路径参数;使用时应做 URL 编码 |
短信和邮箱验证码均按“通道 + 场景 + 账号”隔离:不同场景的验证码不能混用。同一目标、同一场景 60 秒内只能发送一次。错误验证码在 60 秒窗口内超过 5 次后会短暂限速;正确验证码仍可通过,不会被错误尝试主动销毁。
### 1.7 前端错误处理原则
1. `code === 200` 才进入成功分支。
2. `code === 401` 时清理本地登录态,不自动无限重放请求。
3. `code === 500` 时优先展示后端 `msg`,不要替换成笼统的“请求失败”。
4. 网络超时、DNS、TLS、网关错误与业务错误分开处理。
5. 对眼罩控制、直播控制等物理动作,不做成功态的乐观更新,以服务端确认结果为准。
6. 所有 ID 在 URL 中按原始字符串传递,不执行 `Number(id)`。
---
## 2. 认证接口
基础路径:`/app/auth`。本组接口免登录。
> APP 只能使用本章的 `/app/auth/sms/code` 和 `/app/auth/email/code` 获取验证码。仓库中的
> `/resource/sms/code`、`/resource/email/code`、`/resource/gmail/code`、`/sendInternational`
> 属于框架或后台兼容入口,验证码位数、场景和 Redis 缓存键都与 APP 认证链路不同。
> 混用后即使验证码发送成功,APP 注册、登录或找回密码仍会校验失败。
### 2.1 发送短信验证码
```http
POST /app/auth/sms/code
```
请求体:
```json
{
"phoneNumber": "+8613800138000",
"scene": "login"
}
```
| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `phoneNumber` | string | 是 | 见 1.6 手机号规则 |
| `scene` | string | 是 | `register`、`login`、`resetPwd`、`bind`、`rebind` |
| `scene` | 后续接口 | 使用场景 |
|---|---|---|
| `register` | `POST /app/auth/register` | 手机号注册 |
| `login` | `POST /app/auth/sms-login` | 短信验证码登录 |
| `resetPwd` | `POST /app/auth/reset-pwd` | 手机号找回密码 |
| `bind` | `PUT /app/user/phone` | 邮箱注册账号首次绑定手机号 |
| `rebind` | `PUT /app/user/phone` | 已有手机号的账号更换手机号 |
真实短信模式成功:
```json
{
"code": 200,
"msg": "验证码已发送",
"data": null
}
```
开发环境 `device.app.sms.mock=true` 时,验证码会直接放在 `msg` 中,例如 `"123456"`,仅用于联调。生产环境不会在响应中返回验证码。
常见失败:`验证码场景不合法`、`验证码发送过于频繁,请稍后再试`、`验证码短信发送失败`。
### 2.2 手机号注册
```http
POST /app/auth/register
```
请求体:
```json
{
"phoneNumber": "13800138000",
"code": "123456",
"password": "Password1",
"nickName": "小明"
}
```
| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `phoneNumber` | string | 是 | 手机号 |
| `code` | string | 是 | `scene=register` 获取的 6 位验证码 |
| `password` | string | 是 | 8-20 位,含大小写字母和数字 |
| `nickName` | string | 否 | 最长 30 字符,不允许脚本内容;空时默认使用手机号 |
成功:`msg = "注册成功"`,`data = null`。注册接口不自动登录,前端注册成功后仍需调用登录接口。
协议勾选状态由 APP 本地校验,当前服务端不接收也不保存协议勾选字段。
常见失败:`手机号已注册`、`验证码错误`、`验证码已过期`、密码或昵称格式提示。
### 2.3 手机号密码登录
```http
POST /app/auth/login
```
```json
{
"phoneNumber": "13800138000",
"password": "Password1"
}
```
登录成功:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"token": "eyJ..."
}
}
```
密码登录只要求密码非空,不在登录阶段用“新密码规则”拦截历史密码。
常见失败:`账号不存在`、`手机号或密码错误`、`账号已被停用`。
### 2.4 短信验证码登录
```http
POST /app/auth/sms-login
```
```json
{
"phoneNumber": "13800138000",
"code": "123456"
}
```
验证码必须由 `scene=login` 获取。成功响应与 2.3 相同。
服务端先判断账号存在且可用,再验证验证码。短信登录不会自动注册新账号。
### 2.5 手机号找回密码
```http
POST /app/auth/reset-pwd
```
```json
{
"phoneNumber": "13800138000",
"code": "123456",
"newPassword": "NewPassword1"
}
```
验证码必须由 `scene=resetPwd` 获取。成功:`msg = "密码重置成功"`。
### 2.6 退出登录
```http
POST /app/auth/logout
Authorization: Bearer <token>
clientid: app
```
无请求体。成功:`msg = "退出成功"`。
该接口幂等:未登录或 token 已失效时调用仍按成功处理。它只退出当前 token,不影响同一账号的其他设备会话。APP 收到成功后应立即删除本地 token。
### 2.7 发送邮箱验证码
```http
POST /app/auth/email/code
```
```json
{
"email": "User@gmail.com",
"scene": "register"
}
```
| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `email` | string | 是 | 合法邮箱,最长 50 字符;服务端统一转为小写 |
| `scene` | string | 是 | 仅支持 `register`、`login`、`resetPwd` |
成功:`msg = "验证码已发送至邮箱"`。
邮箱 mock 模式也不会把验证码返回给 APP,只会写开发日志。若配置了邮箱域名白名单,不在白名单内会返回 `该邮箱域名暂不支持注册`。
### 2.8 邮箱注册
```http
POST /app/auth/email/register
```
```json
{
"email": "user@gmail.com",
"code": "123456",
"password": "Password1",
"nickName": "小明"
}
```
| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `email` | string | 是 | 合法邮箱,最长 50 字符 |
| `code` | string | 是 | `scene=register` 获取的邮箱验证码 |
| `password` | string | 是 | 8-20 位,含大小写字母和数字 |
| `nickName` | string | 否 | 最长 30 字符;空时默认使用邮箱 |
成功:`msg = "注册成功,请绑定手机号"`。
邮箱注册不会自动登录,也不会同时创建手机号。登录后 `profile.phoneBound` 为 `false`,此时服务端会禁止绑定设备,APP 必须先走 3.3 首次绑定手机号。
### 2.9 邮箱密码登录
```http
POST /app/auth/email/login
```
```json
{
"email": "user@gmail.com",
"password": "Password1"
}
```
成功响应与 2.3 相同。常见失败:`账号不存在`、`邮箱或密码错误`、`账号已被停用`。
### 2.10 邮箱验证码登录
```http
POST /app/auth/email/code-login
```
```json
{
"email": "user@gmail.com",
"code": "123456"
}
```
验证码必须由 `scene=login` 获取。成功响应与 2.3 相同,不会自动注册账号。
### 2.11 邮箱找回密码
```http
POST /app/auth/email/reset-pwd
```
```json
{
"email": "user@gmail.com",
"code": "123456",
"newPassword": "NewPassword1"
}
```
验证码必须由 `scene=resetPwd` 获取。成功:`msg = "密码重置成功"`。
---
## 3. 个人中心接口
基础路径:`/app/user`。本组全部需要登录,只能操作当前账号。
### 3.1 获取当前用户资料
```http
GET /app/user/profile
```
响应:`ApiResponse<AppUserProfile>`。
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"userName": "app_1900000000000000000",
"nickName": "小明",
"phoneNumber": "138****8000",
"email": "u***@gmail.com",
"phoneBound": true,
"userType": "app_user",
"createTime": "2026-09-20 10:30:00"
}
}
```
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `userName` | string | 否 | 服务端账号标识,不是手机号或邮箱 |
| `nickName` | string | 否 | 用户昵称 |
| `phoneNumber` | string | 是 | 脱敏手机号;尚未绑定时为 `null` |
| `email` | string | 是 | 脱敏邮箱;手机号注册账号通常为 `null` |
| `phoneBound` | boolean | 否 | 是否已绑定手机号 |
| `userType` | string | 否 | 固定为 `app_user` |
| `createTime` | string | 否 | 注册时间 |
登录成功后建议立即拉取本接口。`phoneBound=false` 时应引导绑定手机号,并禁止进入设备绑定流程。
### 3.2 修改密码
```http
PUT /app/user/password
```
```json
{
"oldPassword": "Password1",
"newPassword": "NewPassword1"
}
```
| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `oldPassword` | string | 是 | 当前密码 |
| `newPassword` | string | 是 | 8-20 位,含大小写字母和数字;不能与旧密码相同 |
成功:`msg = "密码修改成功"`。当前实现不会在改密成功后自动退出本设备,APP 可按产品要求主动重新登录。
### 3.3 首次绑定或更换手机号
```http
PUT /app/user/phone
```
```json
{
"newPhoneNumber": "13900139000",
"code": "123456"
}
```
发送验证码前必须根据 `profile.phoneBound` 选择场景:
| `phoneBound` | 短信 `scene` | 含义 |
|---:|---|---|
| `false` | `bind` | 邮箱账号首次绑定手机号 |
| `true` | `rebind` | 更换已有手机号 |
验证码发送到新手机号。成功:`msg = "手机号更换成功"`,首次绑定也使用该文案。
常见失败:`新手机号与当前手机号相同`、`手机号已注册`、`验证码错误`、`验证码已过期`。
### 3.4 注销账号
```http
DELETE /app/user/account
```
无请求体。服务端依次执行:解绑全部设备、停用账号、注销该账号全部会话。成功:`msg = "注销成功"`。
这是不可在 APP 内直接撤销的账号操作。前端应二次确认;成功后清除 token、用户缓存、设备缓存并返回登录页。
### 3.5 查询用户协议或隐私政策
```http
GET /app/user/agreements?type=user
```
| Query 参数 | 类型 | 必填 | 取值 |
|---|---|---:|---|
| `type` | string | 是 | `user` 或 `privacy`,区分大小写 |
响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"type": "user",
"url": "https://example.com/agreement/user",
"version": "v1.0",
"updateTime": "2026-09-20"
}
}
```
注意:当前接口需要登录。如果登录页也要展示协议,应由产品/后端另行提供公开入口,前端不要假定本接口可匿名访问。
协议内容来自服务端配置,默认 `url` 和 `updateTime` 可以是空字符串;APP 应为未配置状态准备占位提示,不要直接打开空 URL。
---
## 4. 设备接口
基础路径:`/app/device`。全部需要登录。按设备编号查询或操作时,服务端同时校验设备属于当前用户;设备不存在、未绑定或属于他人统一返回 `设备不存在`,防止枚举设备归属。
### 4.1 我的设备列表
```http
GET /app/device/list
```
无 Query 参数,不分页,按绑定时间倒序。没有设备时 `data = []`。
响应项字段:
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `deviceNo` | string | 否 | 设备业务编号 |
| `name` | string | 否 | 用户设置的名称 |
| `imei` | string | 否 | 脱敏值,如 `1234****5678` |
| `model` | string | 是 | 设备型号 |
| `status` | string | 否 | `0` 未激活、`1` 在线、`2` 离线 |
| `signalLevel` | number | 是 | `0` 离线、`1` 弱、`2` 一般、`3` 强 |
| `signalText` | string | 是 | 服务端派生的信号文案 |
| `hoodConnected` | string | 是 | `0` 已断开、`1` 已连接 |
| `hoodConnectedText` | string | 是 | 服务端派生的连接文案 |
| `hoodBattery` | number | 是 | 头套电量,0-100 |
| `ringBattery` | number | 是 | 脚环电量,0-100 |
| `lng` | string | 是 | 最新经度;`BigDecimal` 以字符串返回 |
| `lat` | string | 是 | 最新纬度;`BigDecimal` 以字符串返回 |
| `address` | string | 是 | 最新地址 |
示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"deviceNo": "BM202609200001",
"name": "客厅设备",
"imei": "1234****2345",
"model": "BM-A1",
"status": "1",
"signalLevel": 3,
"signalText": "强",
"hoodConnected": "1",
"hoodConnectedText": "已连接",
"hoodBattery": 82,
"ringBattery": 76,
"lng": "116.397128",
"lat": "39.916527",
"address": "北京市东城区"
}
]
}
```
### 4.2 设备详情
```http
GET /app/device/{deviceNo}
```
响应字段与列表项相同,额外返回:
| 字段 | 类型 | 说明 |
|---|---|---|
| `hoodCoverOn` | string | `0` 打开、`1` 遮盖;可能因设备尚未上报而为 `null` |
### 4.3 绑定设备
```http
POST /app/device/bind
```
```json
{
"imei": "123456789012345",
"name": "客厅设备"
}
```
| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `imei` | string | 是 | 15 位数字,通常来自扫码 |
| `name` | string | 是 | 1-20 个字符 |
成功:`msg = "添加成功"`。
前置条件:当前账号必须已绑定手机号。常见失败:`请先绑定手机号`、`设备不存在`、`重复绑定`、`设备已被绑定`。
### 4.4 重命名设备
```http
PUT /app/device/{deviceNo}/name
```
```json
{
"name": "新的设备名称"
}
```
Body 只传 `name`,不要重复传 `deviceNo`。成功:`msg = "修改成功"`。
### 4.5 解绑设备
```http
DELETE /app/device/{deviceNo}
```
无请求体。成功:`msg = "解绑成功"`。
解绑后设备回到未激活状态,但设备建档与历史告警仍保留。历史消息归告警发生时的用户,不会转给下一位绑定用户。
### 4.6 控制眼罩
```http
PUT /app/device/{deviceNo}/hood
```
```json
{
"coverOn": true
}
```
| `coverOn` | 物理动作 |
|---:|---|
| `true` | 遮盖眼罩 |
| `false` | 打开眼罩 |
`coverOn` 必须显式传入,不能省略。
成功响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"deviceNo": "BM202609200001",
"hoodCoverOn": "1",
"hoodCoverOnText": "遮盖"
}
}
```
接口会等待设备回执,默认最长 5 秒,另有很短的服务端宽限时间。前端建议:
- 单次请求超时设置为至少 8-10 秒。
- 请求期间展示 loading 并禁用重复点击。
- 只有收到 `code=200` 后才用 `data.hoodCoverOn` 更新页面。
- 设备离线时两个方向都拒绝;低电只拒绝“遮盖”,不阻止“打开”。
常见失败:`设备不存在`、`设备已离线`、`电量过低`、`指令下发失败`。
### 4.7 获取设备定位
```http
GET /app/device/{deviceNo}/location
```
响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"deviceNo": "BM202609200001",
"lng": "116.397128",
"lat": "39.916527",
"address": "北京市东城区",
"lastOnlineTime": "2026-09-20 11:30:00"
}
}
```
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `deviceNo` | string | 否 | 设备编号 |
| `lng` | string | 是 | 最新经度 |
| `lat` | string | 是 | 最新纬度 |
| `address` | string | 是 | 设备上报地址 |
| `lastOnlineTime` | string | 是 | 最后在线时间 |
从未上报定位时接口仍成功,`lng`、`lat` 为 `null`。APP 应展示“暂无定位”空态,不要把空坐标传给地图 SDK。
### 4.8 获取直播播放信息
```http
GET /app/device/{deviceNo}/live
```
该接口等价于获取播放地址,不会通知设备开始推流。响应结构与 5.1 的 `AppStreamPlay` 相同。正常开播流程应先调用 `POST /app/stream/start`。
---
## 5. 直播接口
基础路径:`/app/stream`。全部需要登录,只能操作当前用户绑定的设备。
当前默认 `mjpeg.enabled=true`,直播走 HTTP MJPEG;关闭该配置后切换为 ZLMediaKit。APP 不应硬编码播放协议,应从 `playUrls` 中选择非空地址。
### 5.1 开始推流
```http
POST /app/stream/start
```
```json
{
"deviceNo": "BM202609200001"
}
```
前置条件:设备属于当前用户且 `status=1`。接口向设备发送开始推流指令,然后立即返回播放信息;它不等待第一帧,因此刚开播时 `online` 可能为 `false`。
当前 MJPEG 模式响应示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"deviceNo": "BM202609200001",
"streamId": "BM202609200001",
"app": "live",
"online": false,
"pushUrl": "https://stream.example.com/mjpeg/ingest/BM202609200001",
"playToken": "7f2d...",
"expireAt": 1789875123456,
"expireSeconds": 120,
"playUrls": {
"flv": null,
"hls": null,
"rtmp": null,
"rtsp": null,
"webrtc": null,
"mjpeg": "https://stream.example.com/mjpeg/play/BM202609200001?token=..."
}
}
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `deviceNo` | string | 设备编号 |
| `streamId` | string | 当前与 `deviceNo` 相同 |
| `app` | string | 流媒体应用名,当前默认 `live` |
| `online` | boolean | 当前是否已经存在有效流/最近帧 |
| `pushUrl` | string | 设备侧推流地址,APP 播放器不要使用 |
| `playToken` | string | 播放凭证;一般无需单独拼接 |
| `expireAt` | number \| string | 播放凭证过期时间,epoch 毫秒 |
| `expireSeconds` | number | 有效期秒数,当前默认 120 |
| `playUrls` | object | 各协议播放地址,token 已附在 URL 中 |
限流:同一设备控制维度、同一 IP 60 秒内最多 30 次。
播放 token 同时绑定 `streamId` 和签发时客户端 IP,不能分享给其他设备或网络出口。网络切换、token 临近过期或收到播放端 `401` 时,应重新调用播放信息接口获取新 URL。
### 5.2 停止推流
```http
POST /app/stream/stop/{deviceNo}
```
通知设备停止推流,并关闭服务端当前流。响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"deviceNo": "BM202609200001",
"streamId": "BM202609200001",
"online": false
}
}
```
限流:同一设备控制维度、同一 IP 60 秒内最多 20 次。离开直播页时建议调用一次,但不要在组件反复渲染时重复调用。
### 5.3 获取播放地址
```http
GET /app/stream/play-url/{deviceNo}
```
只签发新的播放 token 和地址,不通知设备开播。响应结构与 5.1 相同。限流:同一设备、同一 IP 60 秒内最多 60 次。
适用场景:
- 播放 URL 即将过期时刷新。
- APP 从后台回到前台后重新获取凭证。
- 已开播但播放器重连。
### 5.4 仅关闭服务端流
```http
POST /app/stream/close/{deviceNo}
```
只关闭当前直播后端的流或 MJPEG 会话,不通知设备停止推流。正常用户流程优先使用 5.2 `stop`。
成功:`data = null`。限流:同一设备、同一 IP 60 秒内最多 20 次。
### 5.5 查询流状态
```http
GET /app/stream/status/{deviceNo}
```
响应:`ApiResponse<AppStreamStatus>`,结构见 5.2。限流:同一设备、同一 IP 60 秒内最多 60 次。
若需要等待第一帧,可每 2-3 秒轮询一次,成功在线或达到客户端等待上限后停止轮询。
### 5.6 已停用的旧接口
```http
POST /app/stream/open
```
此接口固定失败:`msg = "客户端自定义视频源已停用"`。新 APP 不得调用,也不要允许用户输入自定义拉流地址。
### 5.7 MJPEG 播放地址说明
`playUrls.mjpeg` 指向:
```http
GET /mjpeg/play/{deviceNo}?token=<playToken>
```
它返回 `multipart/x-mixed-replace` 长连接,不返回统一 JSON。APP 可使用支持 MJPEG 的图片/流组件直接打开完整 URL。
调试单帧地址:
```http
GET /mjpeg/play/{deviceNo}/snapshot?token=<playToken>
```
| HTTP 状态 | 含义 |
|---:|---|
| `200` | 有流或有最新帧 |
| `401` | token 无效、过期、IP 不匹配或 MJPEG 未启用 |
| `404` | 当前没有活动流或尚无帧 |
不要自行构造或长期缓存播放 URL,始终使用业务接口返回的完整地址。
---
## 6. APP 版本检查
基础路径:`/app/version`。本组免登录,供冷启动最早阶段调用。
### 6.1 检查最新版本
```http
GET /app/version/latest?platform=android&versionCode=100&versionName=1.0.0&deviceModel=Pixel%208&osVersion=15
```
| Query 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `platform` | string | 是 | 严格取 `android` 或 `ios`,区分大小写 |
| `versionCode` | number | 是 | 当前构建号,必须为正整数;唯一比较依据 |
| `versionName` | string | 否 | 当前实现接收但不参与比较、不落库、也不在响应中回显 |
| `deviceModel` | string | 否 | 当前实现接收但不参与判定、不落库 |
| `osVersion` | string | 否 | 当前实现接收但不参与判定、不落库 |
比较规则只使用整数 `versionCode`。不要使用字符串 `versionName` 比较大小。
无更新响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"platform": "android",
"hasUpdate": false,
"forceUpdate": false,
"latestVersionCode": null,
"latestVersionName": null,
"changelog": null,
"fileSize": null,
"md5": null,
"downloadUrl": null,
"storeUrl": null,
"currentVersionCode": 100
}
}
```
有更新响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"platform": "android",
"hasUpdate": true,
"forceUpdate": true,
"latestVersionCode": 101,
"latestVersionName": "1.0.1",
"changelog": "修复已知问题并提升直播稳定性",
"fileSize": 52428800,
"md5": null,
"downloadUrl": "https://oss.example.com/app.apk?...",
"storeUrl": null,
"currentVersionCode": 100
}
}
```
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `platform` | string | 否 | 回显平台 |
| `hasUpdate` | boolean | 否 | 更新弹窗主开关 |
| `forceUpdate` | boolean | 否 | 是否强制更新;无更新时恒为 `false` |
| `latestVersionCode` | number | 是 | 最新构建号 |
| `latestVersionName` | string | 是 | 最新展示版本号 |
| `changelog` | string | 是 | 更新说明 |
| `fileSize` | number \| string | 是 | 安装包字节数;使用前执行 `Number(fileSize)` |
| `md5` | string | 是 | 预留字段,本期通常为 `null` |
| `downloadUrl` | string | 是 | 动态解析出的安装包 URL |
| `storeUrl` | string | 是 | App Store 等引导地址,后台未填时为 `null` |
| `currentVersionCode` | number | 否 | 回显当前构建号 |
三态处理:
| 条件 | APP 行为 |
|---|---|
| `hasUpdate=false` | 不弹更新框 |
| `hasUpdate=true && forceUpdate=false` | 展示可关闭的可选更新弹窗 |
| `hasUpdate=true && forceUpdate=true` | 展示不可跳过的强更弹窗 |
补充规则:
- 没有启用版本、当前构建号大于等于最新构建号、或安装包 URL 无法解析时,都返回“无更新”。
- 下载地址针对私有 OSS 每次检查时重新解析,当前有效期为 24 小时;失败或过期后重新检查,不要永久缓存。
- Android 与 iOS 都可能返回 `downloadUrl`;iOS 是否可安装取决于 App Store、TestFlight、企业签等实际分发方式。
- `storeUrl` 有值时优先用于系统商店跳转;无值时根据平台和实际分发方案处理。
常见失败:`平台参数不合法`、`版本号不合法`。
---
## 7. 消息中心接口
基础路径:`/app/message`。全部需要登录,只能读写当前用户消息。
一期消息仅包含设备告警:设备离线、头套低电、脚环低电。消息由服务端生成,APP 不提供新增、修改内容或删除接口。
### 7.1 消息分页列表
```http
GET /app/message/list?pageNum=1&pageSize=20&readFlag=0
```
| Query 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---:|---:|---|
| `pageNum` | number | 否 | `1` | 小于 1 时回落为 1 |
| `pageSize` | number | 否 | `20` | 小于 1 时回落为 20,大于 100 时截为 100 |
| `readFlag` | string | 否 | 全部 | `0` 未读、`1` 已读;其他值通常得到空结果 |
排序:`createTime` 倒序,再按 `messageId` 倒序。
响应项:
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `messageId` | string \| number | 否 | 雪花 ID,按字符串保存和回传 |
| `deviceNo` | string | 是 | 关联设备编号,可用于跳详情 |
| `alertId` | string \| number | 是 | 关联告警 ID |
| `alertType` | string | 是 | 告警类型 |
| `alertLevel` | string | 是 | `0` 提示、`1` 警告、`2` 严重 |
| `title` | string | 否 | 消息标题 |
| `content` | string | 是 | 消息内容 |
| `readFlag` | string | 否 | `0` 未读、`1` 已读 |
| `readTime` | string | 是 | 首次标记已读时间 |
| `createTime` | string | 否 | 消息产生时间 |
示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"total": 1,
"rows": [
{
"messageId": "1970000000000000001",
"deviceNo": "BM202609200001",
"alertId": "1970000000000000000",
"alertType": "low_battery_hood",
"alertLevel": "1",
"title": "头套电量过低",
"content": "头套电量过低(当前 18%)",
"readFlag": "0",
"readTime": null,
"createTime": "2026-09-20 12:00:00"
}
]
}
}
```
### 7.2 查询未读数
```http
GET /app/message/unread-count
```
```json
{
"code": 200,
"msg": "操作成功",
"data": 3
}
```
返回当前用户未读消息总数。APP 可在启动、进入消息页、下拉刷新、标记已读后重新获取。当前 APP 接口没有实时推送连接,角标实时性取决于客户端刷新策略。
### 7.3 标记单条已读
```http
PUT /app/message/{messageId}/read
```
无请求体。成功:`data = null`。
该接口幂等:重复调用不修改首次 `readTime`。消息不存在或不属于当前用户时也返回成功,不暴露其他用户的消息 ID 是否存在。
### 7.4 全部标记已读
```http
PUT /app/message/read-all
```
无请求体。仅更新当前用户的未读消息;没有未读消息时仍成功。
### 7.5 告警枚举与点击行为
| `alertType` | 标题/含义 | 默认 `alertLevel` |
|---|---|---|
| `offline` | 设备离线 | `2` 严重 |
| `low_battery_hood` | 头套电量过低 | `1` 警告 |
| `low_battery_ring` | 脚环电量过低 | `1` 警告 |
点击消息建议流程:
```text
消息列表
-> 本地先标记已读
-> GET /app/device/{deviceNo}
-> 成功则进入设备详情
-> 返回“设备不存在”则提示设备已解绑或不可访问,留在消息页
```
历史消息归告警发生时的用户,即使设备后来解绑或换绑也不会迁移。
---
## 8. TypeScript 契约
以下类型按当前真实序列化行为编写,可直接作为 APP API 层的基础定义。
```ts
/** Java Long 可能按 number 或 string 返回,雪花 ID 应始终按字符串语义使用。 */
export type JsonLong = string | number;
export interface ApiResponse<T> {
code: number;
msg: string;
data: T | null;
}
export interface PageResult<T> {
total: number;
rows: T[];
}
export type SmsScene = 'register' | 'login' | 'resetPwd' | 'bind' | 'rebind';
export type EmailScene = 'register' | 'login' | 'resetPwd';
export type DeviceStatus = '0' | '1' | '2';
export type HoodState = '0' | '1';
export type ReadFlag = '0' | '1';
export type AlertLevel = '0' | '1' | '2';
export type AppPlatform = 'android' | 'ios';
export interface AppToken {
token: string;
}
export interface AppUserProfile {
userName: string;
nickName: string;
phoneNumber: string | null;
email: string | null;
phoneBound: boolean;
userType: 'app_user';
createTime: string;
}
export interface AppAgreement {
type: 'user' | 'privacy';
url: string;
version: string;
updateTime: string;
}
export interface AppDevice {
deviceNo: string;
name: string;
imei: string;
model: string | null;
status: DeviceStatus;
signalLevel: 0 | 1 | 2 | 3 | null;
signalText: string | null;
hoodConnected: HoodState | null;
hoodConnectedText: string | null;
hoodBattery: number | null;
ringBattery: number | null;
/** BigDecimal 经全局 Jackson 配置固定返回字符串。 */
lng: string | null;
/** BigDecimal 经全局 Jackson 配置固定返回字符串。 */
lat: string | null;
address: string | null;
}
export interface AppDeviceDetail extends AppDevice {
hoodCoverOn: HoodState | null;
}
export interface AppHoodResult {
deviceNo: string;
hoodCoverOn: HoodState;
hoodCoverOnText: '打开' | '遮盖';
}
export interface DeviceLocation {
deviceNo: string;
lng: string | null;
lat: string | null;
address: string | null;
lastOnlineTime: string | null;
}
export interface AppStreamPlayUrls {
flv: string | null;
hls: string | null;
rtmp: string | null;
rtsp: string | null;
webrtc: string | null;
mjpeg: string | null;
}
export interface AppStreamPlay {
deviceNo: string;
streamId: string;
app: string;
online: boolean;
/** 设备侧地址,仅用于诊断,APP 播放器不使用。 */
pushUrl: string;
playToken: string;
expireAt: JsonLong;
expireSeconds: number;
playUrls: AppStreamPlayUrls;
}
export interface AppStreamStatus {
deviceNo: string;
streamId: string;
online: boolean;
}
export interface AppVersionLatest {
platform: AppPlatform;
hasUpdate: boolean;
forceUpdate: boolean;
latestVersionCode: number | null;
latestVersionName: string | null;
changelog: string | null;
fileSize: JsonLong | null;
md5: string | null;
downloadUrl: string | null;
storeUrl: string | null;
currentVersionCode: number;
}
export interface AppMessage {
messageId: JsonLong;
deviceNo: string | null;
alertId: JsonLong | null;
alertType: 'offline' | 'low_battery_hood' | 'low_battery_ring' | string;
alertLevel: AlertLevel | null;
title: string;
content: string | null;
readFlag: ReadFlag;
readTime: string | null;
createTime: string;
}
```
请求 DTO:
```ts
export interface SmsCodeRequest {
phoneNumber: string;
scene: SmsScene;
}
export interface PhoneRegisterRequest {
phoneNumber: string;
code: string;
password: string;
nickName?: string;
}
export interface PhoneLoginRequest {
phoneNumber: string;
password: string;
}
export interface PhoneCodeLoginRequest {
phoneNumber: string;
code: string;
}
export interface PhoneResetPasswordRequest {
phoneNumber: string;
code: string;
newPassword: string;
}
export interface EmailCodeRequest {
email: string;
scene: EmailScene;
}
export interface EmailRegisterRequest {
email: string;
code: string;
password: string;
nickName?: string;
}
export interface EmailLoginRequest {
email: string;
password: string;
}
export interface EmailCodeLoginRequest {
email: string;
code: string;
}
export interface EmailResetPasswordRequest {
email: string;
code: string;
newPassword: string;
}
export interface ChangePasswordRequest {
oldPassword: string;
newPassword: string;
}
export interface BindPhoneRequest {
newPhoneNumber: string;
code: string;
}
export interface BindDeviceRequest {
imei: string;
name: string;
}
export interface RenameDeviceRequest {
name: string;
}
export interface HoodControlRequest {
coverOn: boolean;
}
export interface StreamStartRequest {
deviceNo: string;
}
export interface VersionCheckQuery {
platform: AppPlatform;
versionCode: number;
versionName?: string;
deviceModel?: string;
osVersion?: string;
}
export interface MessageQuery {
pageNum?: number;
pageSize?: number;
readFlag?: ReadFlag;
}
```
### 8.1 Axios 请求封装示例
```ts
import axios, { type AxiosRequestConfig } from 'axios';
export class ApiError extends Error {
constructor(
public readonly code: number,
message: string
) {
super(message);
}
}
const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15_000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
clientid: 'app'
}
});
http.interceptors.request.use(config => {
const token = localStorage.getItem('app_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
const response = await http.request<ApiResponse<T>>(config);
const body = response.data;
// 业务异常大多仍是 HTTP 200,必须检查响应体 code。
if (body.code === 401) {
localStorage.removeItem('app_token');
// 这里通知路由层跳转登录页,避免 API 层直接依赖具体导航实现。
throw new ApiError(body.code, body.msg);
}
if (body.code !== 200) {
throw new ApiError(body.code, body.msg);
}
return body.data as T;
}
```
眼罩接口可单独覆盖超时:
```ts
export function controlHood(deviceNo: string, coverOn: boolean) {
return request<AppHoodResult>({
method: 'PUT',
url: `/app/device/${encodeURIComponent(deviceNo)}/hood`,
data: { coverOn },
// 服务端默认等待设备回执 5 秒,客户端应预留网络开销。
timeout: 10_000
});
}
```
---
## 9. 推荐业务流程
### 9.1 手机号注册与登录
```text
POST /app/auth/sms/code (scene=register)
-> POST /app/auth/register
-> POST /app/auth/login 或 /app/auth/sms-login
-> 保存 data.token
-> GET /app/user/profile
-> GET /app/device/list
```
### 9.2 邮箱注册与首次绑定手机号
```text
POST /app/auth/email/code (scene=register)
-> POST /app/auth/email/register
-> POST /app/auth/email/login 或 /app/auth/email/code-login
-> 保存 data.token
-> GET /app/user/profile
-> phoneBound=false
-> POST /app/auth/sms/code (scene=bind,目标是新手机号)
-> PUT /app/user/phone
-> 重新 GET /app/user/profile
-> phoneBound=true 后允许绑定设备
```
### 9.3 绑定设备
```text
GET /app/user/profile
-> 确认 phoneBound=true
-> 扫码取得 15 位 IMEI
-> POST /app/device/bind
-> GET /app/device/list 刷新设备卡片
```
### 9.4 眼罩控制
```text
禁用按钮并展示 loading
-> PUT /app/device/{deviceNo}/hood
-> code=200:使用 data.hoodCoverOn 更新界面
-> 失败:保持原状态,直接展示 msg
-> 恢复按钮
```
### 9.5 直播
```text
POST /app/stream/start
-> 从 playUrls 选择非空且客户端支持的协议
-> online=false 时每 2-3 秒查询 status
-> 播放 token 即将过期或网络切换时调用 play-url
-> 离开直播页调用 stop
```
### 9.6 冷启动检查更新
```text
GET /app/version/latest(无需登录)
-> hasUpdate=false:继续启动
-> 可选更新:可关闭弹窗
-> 强制更新:不可跳过,使用 storeUrl 或 downloadUrl
-> 下载 URL 失效:重新调用 latest 获取新地址
```
### 9.7 消息角标
```text
APP 启动/回前台 -> GET /app/message/unread-count
进入消息页 -> GET /app/message/list?pageNum=1&pageSize=20
点击消息 -> PUT /app/message/{id}/read
-> 重新获取未读数
-> 尝试进入对应设备详情
```
---
## 10. 常见业务提示
| 模块 | 常见 `msg` |
|---|---|
| 验证码 | `验证码发送过于频繁,请稍后再试`、`验证码错误`、`验证码已过期`、`验证码错误次数过多,请稍后再试` |
| 注册登录 | `手机号已注册`、`邮箱已注册`、`账号不存在`、`手机号或密码错误`、`邮箱或密码错误`、`账号已被停用` |
| 密码 | `原密码错误`、`新密码不能与原密码相同`、`密码需为 8-20 位,且包含大小写字母和数字` |
| 手机号绑定 | `请先绑定手机号`、`新手机号与当前手机号相同`、`手机号已注册` |
| 设备 | `设备不存在`、`设备已被绑定`、`重复绑定`、`设备已离线`、`电量过低`、`指令下发失败` |
| 直播 | `MJPEG 直播地址未配置`、`ZLM 地址未配置`、`操作过于频繁,请稍后再试`、`客户端自定义视频源已停用` |
| 版本 | `平台参数不合法`、`版本号不合法` |
前端不要根据 `msg` 反推业务状态或做程序分支;程序逻辑应优先根据 `code` 和响应字段处理,`msg` 只用于用户提示。当前后端尚未为业务异常提供更细的稳定错误码,这是现有契约的限制。
---
## 11. 完整接口索引
| # | 方法 | 路径 | 登录 | 状态 |
|---:|---|---|---:|---|
| 1 | POST | `/app/auth/sms/code` | 否 | 可用 |
| 2 | POST | `/app/auth/register` | 否 | 可用 |
| 3 | POST | `/app/auth/login` | 否 | 可用 |
| 4 | POST | `/app/auth/sms-login` | 否 | 可用 |
| 5 | POST | `/app/auth/reset-pwd` | 否 | 可用 |
| 6 | POST | `/app/auth/logout` | 否;退出当前会话时带 token | 可用 |
| 7 | POST | `/app/auth/email/code` | 否 | 可用 |
| 8 | POST | `/app/auth/email/register` | 否 | 可用 |
| 9 | POST | `/app/auth/email/login` | 否 | 可用 |
| 10 | POST | `/app/auth/email/code-login` | 否 | 可用 |
| 11 | POST | `/app/auth/email/reset-pwd` | 否 | 可用 |
| 12 | GET | `/app/user/profile` | 是 | 可用 |
| 13 | PUT | `/app/user/password` | 是 | 可用 |
| 14 | PUT | `/app/user/phone` | 是 | 可用 |
| 15 | DELETE | `/app/user/account` | 是 | 可用 |
| 16 | GET | `/app/user/agreements` | 是 | 可用 |
| 17 | GET | `/app/device/list` | 是 | 可用 |
| 18 | GET | `/app/device/{deviceNo}` | 是 | 可用 |
| 19 | POST | `/app/device/bind` | 是 | 可用 |
| 20 | PUT | `/app/device/{deviceNo}/name` | 是 | 可用 |
| 21 | DELETE | `/app/device/{deviceNo}` | 是 | 可用 |
| 22 | PUT | `/app/device/{deviceNo}/hood` | 是 | 可用 |
| 23 | GET | `/app/device/{deviceNo}/location` | 是 | 可用 |
| 24 | GET | `/app/device/{deviceNo}/live` | 是 | 可用 |
| 25 | POST | `/app/stream/start` | 是 | 可用 |
| 26 | POST | `/app/stream/stop/{deviceNo}` | 是 | 可用 |
| 27 | GET | `/app/stream/play-url/{deviceNo}` | 是 | 可用 |
| 28 | POST | `/app/stream/open` | 是 | 已停用,固定失败 |
| 29 | POST | `/app/stream/close/{deviceNo}` | 是 | 可用 |
| 30 | GET | `/app/stream/status/{deviceNo}` | 是 | 可用 |
| 31 | GET | `/app/version/latest` | 否 | 可用 |
| 32 | GET | `/app/message/list` | 是 | 可用 |
| 33 | GET | `/app/message/unread-count` | 是 | 可用 |
| 34 | PUT | `/app/message/{messageId}/read` | 是 | 可用 |
| 35 | PUT | `/app/message/read-all` | 是 | 可用 |
动态媒体端点不计入上述 35 个业务接口:
| 方法 | 路径 | 鉴权 | 返回 |
|---|---|---|---|
| GET | `/mjpeg/play/{deviceNo}?token=...` | 播放 token | MJPEG multipart 流 |
| GET | `/mjpeg/play/{deviceNo}/snapshot?token=...` | 播放 token | 单帧 JPEG |
---
## 12. 代码事实来源
接口路径和鉴权:
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/controller/app/AppAuthController.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/controller/app/AppUserController.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/controller/app/AppDeviceController.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/controller/app/AppVersionAppController.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/controller/app/AppMessageController.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/zlm/controller/AppStreamController.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/mjpeg/MjpegStreamController.java`
请求与响应字段:
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/domain/bo/`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/domain/vo/`
业务规则:
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/service/impl/`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/sms/AppSmsService.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/email/AppEmailService.java`
- `ruoyi-modules/ruoyi-device/src/main/java/org/dromara/device/stream/`
公共契约:
- `ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java`
- `ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/PageResult.java`
- `ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/config/JacksonConfig.java`
- `ruoyi-common/ruoyi-common-json/src/main/java/org/dromara/common/json/handler/BigNumberSerializer.java`
- `ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java`
- `ruoyi-common/ruoyi-common-satoken/src/main/resources/common-satoken.yml`