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

45 KiB
Raw Blame History

智能飞禽物联平台 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

本地后端默认地址:

http://localhost:8081

当前 Servlet Context Path 为 /,因此完整示例为:

POST http://localhost:8081/app/auth/login
GET  http://localhost:8081/app/device/list

生产环境只替换协议、域名和端口,本文中的接口路径不变。建议 APP 只在一个环境配置项中维护 Base URL,不要在页面中拼接域名。

1.2 请求头与登录态

建议所有 APP JSON 请求统一发送:

Content-Type: application/json;charset=utf-8
clientid: app

登录后的受保护接口还必须发送:

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 图片/视频流外,所有接口都返回:

export interface ApiResponse<T> {
  code: number;
  msg: string;
  data: T | null;
}

带业务数据的成功响应:

{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "token": "eyJ..."
  }
}

只返回操作结果的成功响应:

{
  "code": 200,
  "msg": "修改成功",
  "data": null
}

业务失败响应:

{
  "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 分页结构

消息列表使用分页响应:

export interface PageResult<T> {
  total: number;
  rows: T[];
}

示例:

{
  "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 发送短信验证码

POST /app/auth/sms/code

请求体:

{
  "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 已有手机号的账号更换手机号

真实短信模式成功:

{
  "code": 200,
  "msg": "验证码已发送",
  "data": null
}

开发环境 device.app.sms.mock=true 时,验证码会直接放在 msg 中,例如 "123456",仅用于联调。生产环境不会在响应中返回验证码。

常见失败:验证码场景不合法、验证码发送过于频繁,请稍后再试、验证码短信发送失败。

2.2 手机号注册

POST /app/auth/register

请求体:

{
  "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 手机号密码登录

POST /app/auth/login
{
  "phoneNumber": "13800138000",
  "password": "Password1"
}

登录成功:

{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "token": "eyJ..."
  }
}

密码登录只要求密码非空,不在登录阶段用“新密码规则”拦截历史密码。

常见失败:账号不存在、手机号或密码错误、账号已被停用。

2.4 短信验证码登录

POST /app/auth/sms-login
{
  "phoneNumber": "13800138000",
  "code": "123456"
}

验证码必须由 scene=login 获取。成功响应与 2.3 相同。

服务端先判断账号存在且可用,再验证验证码。短信登录不会自动注册新账号。

2.5 手机号找回密码

POST /app/auth/reset-pwd
{
  "phoneNumber": "13800138000",
  "code": "123456",
  "newPassword": "NewPassword1"
}

验证码必须由 scene=resetPwd 获取。成功:msg = "密码重置成功"。

2.6 退出登录

POST /app/auth/logout
Authorization: Bearer <token>
clientid: app

无请求体。成功:msg = "退出成功"。

该接口幂等:未登录或 token 已失效时调用仍按成功处理。它只退出当前 token,不影响同一账号的其他设备会话。APP 收到成功后应立即删除本地 token。

2.7 发送邮箱验证码

POST /app/auth/email/code
{
  "email": "User@gmail.com",
  "scene": "register"
}
字段 类型 必填 规则
email string 是 合法邮箱,最长 50 字符;服务端统一转为小写
scene string 是 仅支持 register、login、resetPwd

成功:msg = "验证码已发送至邮箱"。

邮箱 mock 模式也不会把验证码返回给 APP,只会写开发日志。若配置了邮箱域名白名单,不在白名单内会返回 该邮箱域名暂不支持注册。

2.8 邮箱注册

POST /app/auth/email/register
{
  "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 邮箱密码登录

POST /app/auth/email/login
{
  "email": "user@gmail.com",
  "password": "Password1"
}

成功响应与 2.3 相同。常见失败:账号不存在、邮箱或密码错误、账号已被停用。

2.10 邮箱验证码登录

POST /app/auth/email/code-login
{
  "email": "user@gmail.com",
  "code": "123456"
}

验证码必须由 scene=login 获取。成功响应与 2.3 相同,不会自动注册账号。

2.11 邮箱找回密码

POST /app/auth/email/reset-pwd
{
  "email": "user@gmail.com",
  "code": "123456",
  "newPassword": "NewPassword1"
}

验证码必须由 scene=resetPwd 获取。成功:msg = "密码重置成功"。


3. 个人中心接口

基础路径:/app/user。本组全部需要登录,只能操作当前账号。

3.1 获取当前用户资料

GET /app/user/profile

响应:ApiResponse<AppUserProfile>。

{
  "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 修改密码

PUT /app/user/password
{
  "oldPassword": "Password1",
  "newPassword": "NewPassword1"
}
字段 类型 必填 规则
oldPassword string 是 当前密码
newPassword string 是 8-20 位,含大小写字母和数字;不能与旧密码相同

成功:msg = "密码修改成功"。当前实现不会在改密成功后自动退出本设备,APP 可按产品要求主动重新登录。

3.3 首次绑定或更换手机号

PUT /app/user/phone
{
  "newPhoneNumber": "13900139000",
  "code": "123456"
}

发送验证码前必须根据 profile.phoneBound 选择场景:

phoneBound 短信 scene 含义
false bind 邮箱账号首次绑定手机号
true rebind 更换已有手机号

验证码发送到新手机号。成功:msg = "手机号更换成功",首次绑定也使用该文案。

常见失败:新手机号与当前手机号相同、手机号已注册、验证码错误、验证码已过期。

3.4 注销账号

DELETE /app/user/account

无请求体。服务端依次执行:解绑全部设备、停用账号、注销该账号全部会话。成功:msg = "注销成功"。

这是不可在 APP 内直接撤销的账号操作。前端应二次确认;成功后清除 token、用户缓存、设备缓存并返回登录页。

3.5 查询用户协议或隐私政策

GET /app/user/agreements?type=user
Query 参数 类型 必填 取值
type string 是 user 或 privacy,区分大小写

响应:

{
  "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 我的设备列表

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 是 最新地址

示例:

{
  "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 设备详情

GET /app/device/{deviceNo}

响应字段与列表项相同,额外返回:

字段 类型 说明
hoodCoverOn string 0 打开、1 遮盖;可能因设备尚未上报而为 null

4.3 绑定设备

POST /app/device/bind
{
  "imei": "123456789012345",
  "name": "客厅设备"
}
字段 类型 必填 规则
imei string 是 15 位数字,通常来自扫码
name string 是 1-20 个字符

成功:msg = "添加成功"。

前置条件:当前账号必须已绑定手机号。常见失败:请先绑定手机号、设备不存在、重复绑定、设备已被绑定。

4.4 重命名设备

PUT /app/device/{deviceNo}/name
{
  "name": "新的设备名称"
}

Body 只传 name,不要重复传 deviceNo。成功:msg = "修改成功"。

4.5 解绑设备

DELETE /app/device/{deviceNo}

无请求体。成功:msg = "解绑成功"。

解绑后设备回到未激活状态,但设备建档与历史告警仍保留。历史消息归告警发生时的用户,不会转给下一位绑定用户。

4.6 控制眼罩

PUT /app/device/{deviceNo}/hood
{
  "coverOn": true
}
coverOn 物理动作
true 遮盖眼罩
false 打开眼罩

coverOn 必须显式传入,不能省略。

成功响应:

{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "deviceNo": "BM202609200001",
    "hoodCoverOn": "1",
    "hoodCoverOnText": "遮盖"
  }
}

接口会等待设备回执,默认最长 5 秒,另有很短的服务端宽限时间。前端建议:

  • 单次请求超时设置为至少 8-10 秒。
  • 请求期间展示 loading 并禁用重复点击。
  • 只有收到 code=200 后才用 data.hoodCoverOn 更新页面。
  • 设备离线时两个方向都拒绝;低电只拒绝“遮盖”,不阻止“打开”。

常见失败:设备不存在、设备已离线、电量过低、指令下发失败。

4.7 获取设备定位

GET /app/device/{deviceNo}/location

响应:

{
  "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 获取直播播放信息

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 开始推流

POST /app/stream/start
{
  "deviceNo": "BM202609200001"
}

前置条件:设备属于当前用户且 status=1。接口向设备发送开始推流指令,然后立即返回播放信息;它不等待第一帧,因此刚开播时 online 可能为 false。

当前 MJPEG 模式响应示例:

{
  "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 停止推流

POST /app/stream/stop/{deviceNo}

通知设备停止推流,并关闭服务端当前流。响应:

{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "deviceNo": "BM202609200001",
    "streamId": "BM202609200001",
    "online": false
  }
}

限流:同一设备控制维度、同一 IP 60 秒内最多 20 次。离开直播页时建议调用一次,但不要在组件反复渲染时重复调用。

5.3 获取播放地址

GET /app/stream/play-url/{deviceNo}

只签发新的播放 token 和地址,不通知设备开播。响应结构与 5.1 相同。限流:同一设备、同一 IP 60 秒内最多 60 次。

适用场景:

  • 播放 URL 即将过期时刷新。
  • APP 从后台回到前台后重新获取凭证。
  • 已开播但播放器重连。

5.4 仅关闭服务端流

POST /app/stream/close/{deviceNo}

只关闭当前直播后端的流或 MJPEG 会话,不通知设备停止推流。正常用户流程优先使用 5.2 stop。

成功:data = null。限流:同一设备、同一 IP 60 秒内最多 20 次。

5.5 查询流状态

GET /app/stream/status/{deviceNo}

响应:ApiResponse<AppStreamStatus>,结构见 5.2。限流:同一设备、同一 IP 60 秒内最多 60 次。

若需要等待第一帧,可每 2-3 秒轮询一次,成功在线或达到客户端等待上限后停止轮询。

5.6 已停用的旧接口

POST /app/stream/open

此接口固定失败:msg = "客户端自定义视频源已停用"。新 APP 不得调用,也不要允许用户输入自定义拉流地址。

5.7 MJPEG 播放地址说明

playUrls.mjpeg 指向:

GET /mjpeg/play/{deviceNo}?token=<playToken>

它返回 multipart/x-mixed-replace 长连接,不返回统一 JSON。APP 可使用支持 MJPEG 的图片/流组件直接打开完整 URL。

调试单帧地址:

GET /mjpeg/play/{deviceNo}/snapshot?token=<playToken>
HTTP 状态 含义
200 有流或有最新帧
401 token 无效、过期、IP 不匹配或 MJPEG 未启用
404 当前没有活动流或尚无帧

不要自行构造或长期缓存播放 URL,始终使用业务接口返回的完整地址。


6. APP 版本检查

基础路径:/app/version。本组免登录,供冷启动最早阶段调用。

6.1 检查最新版本

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 比较大小。

无更新响应:

{
  "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
  }
}

有更新响应:

{
  "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 消息分页列表

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 否 消息产生时间

示例:

{
  "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 查询未读数

GET /app/message/unread-count
{
  "code": 200,
  "msg": "操作成功",
  "data": 3
}

返回当前用户未读消息总数。APP 可在启动、进入消息页、下拉刷新、标记已读后重新获取。当前 APP 接口没有实时推送连接,角标实时性取决于客户端刷新策略。

7.3 标记单条已读

PUT /app/message/{messageId}/read

无请求体。成功:data = null。

该接口幂等:重复调用不修改首次 readTime。消息不存在或不属于当前用户时也返回成功,不暴露其他用户的消息 ID 是否存在。

7.4 全部标记已读

PUT /app/message/read-all

无请求体。仅更新当前用户的未读消息;没有未读消息时仍成功。

7.5 告警枚举与点击行为

alertType 标题/含义 默认 alertLevel
offline 设备离线 2 严重
low_battery_hood 头套电量过低 1 警告
low_battery_ring 脚环电量过低 1 警告

点击消息建议流程:

消息列表
  -> 本地先标记已读
  -> GET /app/device/{deviceNo}
  -> 成功则进入设备详情
  -> 返回“设备不存在”则提示设备已解绑或不可访问,留在消息页

历史消息归告警发生时的用户,即使设备后来解绑或换绑也不会迁移。


8. TypeScript 契约

以下类型按当前真实序列化行为编写,可直接作为 APP API 层的基础定义。

/** 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:

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 请求封装示例

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;
}

眼罩接口可单独覆盖超时:

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 手机号注册与登录

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 邮箱注册与首次绑定手机号

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 绑定设备

GET /app/user/profile
  -> 确认 phoneBound=true
  -> 扫码取得 15 位 IMEI
  -> POST /app/device/bind
  -> GET /app/device/list 刷新设备卡片

9.4 眼罩控制

禁用按钮并展示 loading
  -> PUT /app/device/{deviceNo}/hood
  -> code=200:使用 data.hoodCoverOn 更新界面
  -> 失败:保持原状态,直接展示 msg
  -> 恢复按钮

9.5 直播

POST /app/stream/start
  -> 从 playUrls 选择非空且客户端支持的协议
  -> online=false 时每 2-3 秒查询 status
  -> 播放 token 即将过期或网络切换时调用 play-url
  -> 离开直播页调用 stop

9.6 冷启动检查更新

GET /app/version/latest(无需登录)
  -> hasUpdate=false:继续启动
  -> 可选更新:可关闭弹窗
  -> 强制更新:不可跳过,使用 storeUrl 或 downloadUrl
  -> 下载 URL 失效:重新调用 latest 获取新地址

9.7 消息角标

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