# Water IoT APP 接口文档

> **业务接口 Base URL**: `/app/v1`
> **认证接口前缀**: `/auth`、`/resource`
> **认证方式**: Sa-Token；`/app/v1/**` 业务接口需携带 `Authorization: Bearer <token>`，登录/注册/普通验证码接口免登录
> **响应编码**: UTF-8
> **最后更新**: 2026-07-14

---

## 目录

- [通用说明](#通用说明)
  - [统一响应结构 R\<T\>](#统一响应结构-rt)
  - [分页响应结构 TableDataInfo\<T\>](#分页响应结构-tabledatainfot)
  - [分页请求参数](#分页请求参数)
  - [特殊标注说明](#特殊标注说明)
- [零、登录、注册与验证码](#零登录注册与验证码)
  - [0.1 获取租户列表](#01-获取租户列表)
  - [0.2 获取图形验证码](#02-获取图形验证码)
  - [0.3 获取通用手机号或邮箱验证码](#03-获取通用手机号或邮箱验证码)
  - [0.4 获取短信验证码](#04-获取短信验证码)
  - [0.5 获取邮箱验证码](#05-获取邮箱验证码)
  - [0.6 用户登录](#06-用户登录)
  - [0.7 用户注册](#07-用户注册)
  - [0.8 忘记密码](#08-忘记密码)
  - [0.9 退出登录](#09-退出登录)
  - [0.10 获取账号注销验证码](#010-获取账号注销验证码)
  - [0.11 注销账号](#011-注销账号)
- [一、设备管理](#一设备管理)
  - [1.1 查询设备列表（分页）](#11-查询设备列表分页)
  - [1.2 绑定设备状态检查](#12-绑定设备状态检查)
  - [1.3 绑定已上线设备](#13-绑定已上线设备)
  - [1.4 获取设备详情](#14-获取设备详情)
  - [1.5 修改设备信息](#15-修改设备信息)
  - [1.6 删除设备（解绑）](#16-删除设备解绑)
  - [1.7 手动开关设备](#17-手动开关设备)
- [二、排程管理](#二排程管理)
  - [2.1 查询排程列表（分页）](#21-查询排程列表分页)
  - [2.2 新增排程](#22-新增排程)
  - [2.3 排程详情](#23-排程详情)
  - [2.4 修改排程状态](#24-修改排程状态)
  - [2.5 修改排程](#25-修改排程)
  - [2.6 删除排程](#26-删除排程)
- [三、排程-设备关联](#三排程-设备关联)
  - [3.1 查询排程可关联的设备](#31-查询排程可关联的设备)
  - [3.2 绑定排程与设备](#32-绑定排程与设备)
  - [3.3 删除排程与设备绑定](#33-删除排程与设备绑定)
- [四、浇水记录](#四浇水记录)
  - [4.1 查询浇水记录列表（分页）](#41-查询浇水记录列表分页)
  - [4.2 查询设备最新进行中的浇水记录](#42-查询设备最新进行中的浇水记录)
  - [4.3 删除浇水记录](#43-删除浇水记录)
- [五、数据统计](#五数据统计)
  - [5.1 数据统计](#51-数据统计)
- [六、用户管理](#六用户管理)
  - [6.1 获取用户信息](#61-获取用户信息)
  - [6.2 修改用户信息](#62-修改用户信息)
  - [6.3 修改登录密码](#63-修改登录密码)
  - [6.4 找回密码](#64-找回密码)
- [七、文件上传](#七文件上传)
  - [7.1 上传图片](#71-上传图片)
- [八、版本检查](#八版本检查)
  - [8.1 检查APP版本更新](#81-检查app版本更新)
- [九、内部调试接口](#九内部调试接口)
  - [9.1 MQTT绑定命令测试](#91-mqtt绑定命令测试)
- [附录：数据模型](#附录数据模型)

---

## 通用说明

### 统一响应结构 R\<T\>

大多数接口返回统一的 `R<T>` 包装结构：

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": { }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | `int` | `200` 表示成功，`500` 表示失败 |
| `msg` | `string` | 提示消息（支持 i18n 国际化） |
| `data` | `T` | 业务数据，类型视接口而定；无数据时为 `null` |

**判断逻辑**：`code === 200` 为成功，其余均为失败。

---

### 分页响应结构 TableDataInfo\<T\>

列表查询接口返回 `TableDataInfo<T>`：

```json
{
  "code": 200,
  "msg": "查询成功",
  "total": 100,
  "rows": [ ]
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | `int` | `200` 表示成功 |
| `msg` | `string` | 提示消息 |
| `total` | `long` | 总记录数 |
| `rows` | `T[]` | 当前页数据列表 |

---

### 分页请求参数

所有分页查询接口（GET 请求）支持以下 Query 参数：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | `int` | 否 | 当前页码，默认 `1` |
| `pageSize` | `int` | 否 | 每页条数，默认查全部 |

> **注意**：设备列表、排程列表、浇水记录列表的排序方向固定为 `createTime desc`（服务端强制设置），前端无需传排序参数。

---

### 特殊标注说明

| 标注 | 说明 |
|------|------|
| `@ApiEncrypt` | 启用 `api-decrypt` 后强制请求体加密；默认配置关闭时仍使用普通 JSON |
| `@RepeatSubmit` | 防重复提交，相同请求在短时间内只能提交一次 |
| `@Log` | 操作会被记录到系统日志 |

> 当前默认配置 `api-decrypt.enabled=false`，因此 `@ApiEncrypt` 接口可直接发送普通 JSON。
> 当部署环境启用接口加密后，标注 `@ApiEncrypt` 的 POST/PUT 接口必须携带 `encrypt-key` 请求头并发送加密后的 body；该标注不代表免登录。

---

## 零、登录、注册与验证码

本章节接口来自 `AuthController` 和 `CaptchaController`。两个控制器使用了 `@SaIgnore`，普通登录、注册和验证码接口免登录；退出登录、账号注销验证码和账号注销仍会在方法内部校验 Token。

默认 APP 客户端配置：

| 参数 | 默认值 | 说明 |
|------|------|------|
| `clientId` | `428a8310cd442757ae699df5d894f051` | APP 客户端 ID，来源于 `sys_client` 初始化数据 |
| `tenantId` | `000000` | 默认租户 ID；关闭多租户时仍可沿用该值 |
| `userType` | `app_user` | APP 注册用户类型 |
| `grantType` | `password` / `sms` | 默认 APP 客户端支持密码、短信和社交授权 |



---

### 0.1 获取图形验证码

```
GET /auth/code
```

**认证**：免登录。验证码开启时按 IP 限流，每 60 秒最多 10 次。

**响应**：`R<CaptchaVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "captchaEnabled": true,
    "uuid": "54ef90b79e384d29a44d59f863f83a52",
    "img": "data:image/png;base64,..."
  }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `captchaEnabled` | `boolean` | 是否启用图形验证码 |
| `uuid` | `string` | 验证码缓存标识 |
| `img` | `string` | Base64 图片数据 |

> 当前密码登录和注册服务中的图形验证码校验代码已注释，APP 注册实际使用手机号/邮箱验证码。

---

### 0.2 获取通用手机号或邮箱验证码

根据 `username` 格式自动选择短信或邮件发送，主要用于注册和找回密码。

```
GET /resource/code?username={username}
```

**认证**：免登录。相同 `username` 每 60 秒最多请求 1 次。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `username` | `string` | **是** | 手机号或邮箱；手机号发送短信，其他值按邮箱发送 |

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "123456",
  "data": null
}
```

> 当前实现会把验证码同时放入响应 `msg`，生产环境存在验证码泄露风险，建议生产部署时改为固定提示文本。

---

### 0.3 获取短信验证码

用于短信验证码登录。

```
GET /resource/sms/code?phonenumber={phonenumber}
```

**认证**：免登录。相同手机号每 60 秒最多请求 1 次。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `phonenumber` | `string` | **是** | 接收验证码的手机号 |

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

---

### 0.4 获取邮箱验证码

```
GET /resource/email/code?email={email}
```

**认证**：免登录。相同邮箱每 60 秒最多请求 1 次。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `email` | `string` | **是** | 接收验证码的邮箱地址 |

**响应**：`R<Void>`。未开启邮件功能时返回 `当前系统没有开启邮箱功能！`。

---

### 0.5 用户登录

```
POST /auth/login
```

**认证**：免登录。接口标注 `@ApiEncrypt`，加密规则参见通用说明。

**密码登录请求体**（`grantType=password`）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `clientId` | `string` | **是** | APP 客户端 ID |
| `grantType` | `string` | **是** | 固定为 `password` |
| `username` | `string` | **是** | 用户名、手机号或邮箱，长度 2~30 |
| `password` | `string` | **是** | 密码，长度 5~30 |

```json
{
  "clientId": "428a8310cd442757ae699df5d894f051",
  "grantType": "password",
  "username": "13800000000",
  "password": "123456"
}
```

**短信登录请求体**（`grantType=sms`）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `clientId` | `string` | **是** | APP 客户端 ID |
| `grantType` | `string` | **是** | 固定为 `sms` |
| `phonenumber` | `string` | **是** | 手机号 |
| `smsCode` | `string` | **是** | `/resource/sms/code` 获取的验证码 |

```json
{
  "clientId": "428a8310cd442757ae699df5d894f051",
  "grantType": "sms",
  "phonenumber": "13800000000",
  "smsCode": "123456"
}
```

**响应**：`R<LoginVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "access_token": "登录Token",
    "refresh_token": null,
    "expire_in": 1800,
    "refresh_expire_in": null,
    "client_id": "428a8310cd442757ae699df5d894f051",
    "scope": null,
    "openid": null
  }
}
```

后续业务请求头：

```http
Authorization: Bearer 登录Token
```

---

### 0.6 用户注册

```
POST /auth/register
```

**认证**：免登录。接口标注 `@ApiEncrypt`；系统配置必须允许注册。

| 字段 | 类型 | 必填 | 说明                                      |
|------|------|------|-----------------------------------------|
| `clientId` | `string` | **是** | APP 客户端 ID                              |
| `grantType` | `string` | **是** | 可传 `password、sms`，用于通过基础请求校验            |
| `username` | `string` | **是** | 手机号、邮箱或用户名，长度 2~30                      |
| `password` | `string` | **是** | 密码，长度 5~30                              |
| `userType` | `string` | **是** | APP 用户固定传 `app_user`                    |
| `code` | `string` | **是** | 调用 `/resource/code?username=...` 获取的验证码 |

```json
{
  "clientId": "428a8310cd442757ae699df5d894f051",
  "grantType": "password",
  "username": "13800000000",
  "password": "123456",
  "userType": "app_user",
  "code": "123456"
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> 注册验证码以 `username` 为缓存键，因此获取验证码和注册时的 `username` 必须完全一致。

---

### 0.7 忘记密码

```
PUT /auth/forgot
```

**认证**：免登录。接口标注 `@ApiEncrypt`。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `username` | `string` | **是** | 已注册的用户名或手机号 |
| `smsCode` | `string` | **是** | 以 `username` 为缓存键的验证码，可通过 `/resource/code` 获取 |
| `password` | `string` | **是** | 新密码，长度 5~30 |

```json
{
  "username": "13800000000",
  "smsCode": "123456",
  "password": "NewPass456"
}
```

**响应**：`R<Void>`

> **当前实现限制**：邮箱分支查询结果未赋值给用户对象，因此邮箱找回密码当前会被判定为“账号未注册”；建议修复后再开放邮箱找回。

---

### 0.8 退出登录

```
POST /auth/logout
```

**认证**：需要 `Authorization: Bearer <token>`。

**响应**：`R<Void>`，成功消息为 `退出成功`。

---

### 0.9 获取账号注销验证码

根据当前登录账号优先向已绑定手机号发送短信；没有有效手机号时发送到邮箱。

```
GET /resource/account/cancel/code
```

**认证**：需要 `Authorization: Bearer <token>`。每个用户每 60 秒最多请求 1 次。

**响应**：`R<Void>`

> 验证码以当前登录用户名为缓存键。账号未绑定手机号或邮箱时返回失败。

---

### 0.11 注销账号

```
DELETE /auth/account
```

**认证**：需要 `Authorization: Bearer <token>`。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `code` | `string` | **是** | `/resource/account/cancel/code` 获取的验证码 |

```json
{
  "code": "123456"
}
```

**响应**：`R<Void>`，成功消息为 `注销成功`。

> 注销成功后会解绑并初始化用户名下设备，删除排程、浇水记录及系统用户关联数据，并退出当前账号。该操作不可恢复，超级管理员账号不允许注销。

---

## 一、设备管理

### 1.1 查询设备列表（分页）

查询当前登录用户名下的设备列表。

```
GET /app/v1/deviceList
```

**请求参数（Query）**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | `int` | 否 | 页码，默认 1 |
| `pageSize` | `int` | 否 | 每页条数 |
| `deviceNo` | `string` | 否 | 设备编号（精确查询） |
| `deviceName` | `string` | 否 | 设备名称（模糊查询） |
| `status` | `string` | 否 | 设备状态：`1`-在线 `0`-离线 `2`-到期 `3`-故障 |
| `wifiName` | `string` | 否 | WiFi 名称（模糊查询） |
| `deviceEm` | `string` | 否 | 设备型号（精确查询） |
| `deviceSn` | `string` | 否 | 设备序列号（精确查询） |
| `fwVer` | `string` | 否 | 固件版本（精确查询） |
| `macAddress` | `string` | 否 | MAC 地址（精确查询） |

**响应**：`TableDataInfo<AppDeviceVo>`

```json
{
  "code": 200,
  "msg": "查询成功",
  "total": 2,
  "rows": [
    {
      "deviceNo": "D20260101001",
      "userId": 1,
      "deviceName": "前院浇灌器",
      "deviceInitName": "智能浇灌器Pro",
      "deviceImg": "https://oss.example.com/device/xxx.png",
      "qrcode": "D20260101001",
      "status": "1",
      "workStatus": "0",
      "powerLevel": "85",
      "powerLevelUpdatatime": "2026-07-14 08:30:00",
      "wifiName": "Home-WiFi",
      "wifiPassword": "12345678",
      "deviceEm": "WATER-PRO-1",
      "deviceSn": "SN20260101001",
      "fwVer": "1.0.3",
      "macAddress": "AA:BB:CC:DD:EE:FF",
      "nickName": "张三",
      "expirationTime": "2027-01-01 00:00:00"
    }
  ]
}
```

---

### 1.2 绑定设备状态检查

检查设备绑定状态（加密接口）。

```
POST /app/v1/bindDeviceStatus
```

**请求头**

```
Content-Type: application/json
```

**请求体**（JSON；启用接口加密时发送加密后的 JSON 字符串）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceNo` | `string` | 否 | 设备编号 |
| `macAddress` | `string` | 否 | MAC 地址 |
| `deviceInitName` | `string` | 否 | 设备初始名称 |

以上三个设备标识至少填写一个。查询顺序为 `deviceNo`、`macAddress`、`deviceInitName`。

**响应**：`R<Map<String, Object>>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "bindDeviceStatus": 304,
    "bindDeviceStatusName": "设备未绑定，请先绑定用户",
    "bindDevice": {
      "deviceNo": "D20260101001",
      "macAddress": "AA:BB:CC:DD:EE:FF",
      "userId": null,
      "status": "1",
      "workStatus": "2"
    }
  }
}
```

`bindDeviceStatus` 业务状态码：

| 状态码 | 说明 |
|------|------|
| `200` | 设备已绑定当前用户，可正常使用 |
| `300` | 设备信息为空 |
| `301` | 未提供设备编号、MAC 地址或设备初始名称 |
| `302` | 设备未入库注册 |
| `303` | 设备已被其他用户绑定 |
| `304` | 设备存在但尚未绑定用户，可以继续调用 `/addDevice` |
| `305` | 设备尚未完成配网 |

> 这些是 `data.bindDeviceStatus` 的业务状态码；接口外层 `code` 在正常处理时仍为 `200`。

---

### 1.3 绑定已上线设备

绑定通过 MQTT 注册上线的设备到当前用户（加密接口）。

```
POST /app/v1/addDevice
```

**请求头**

```
Content-Type: application/json
```

**请求体**（JSON；启用接口加密时发送加密后的 JSON 字符串）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceNo` | `string` | **是** | MQTT 注册后服务端分配的设备编号 |
| `deviceName` | `string` | 否 | 设备名称 |
| `status` | `string` | 否 | 绑定后保留/设置的设备在线状态 |
| `wifiName` | `string` | 否 | WiFi 名称 |
| `wifiPassword` | `string` | 否 | WiFi 密码 |

**响应**：`R<AppDeviceVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "deviceNo": "D20260101001",
    "userId": 1,
    "deviceName": "前院浇灌器",
    "deviceInitName": "智能浇灌器Pro",
    "deviceImg": "https://oss.example.com/device/xxx.png",
    "status": "1",
    "workStatus": "0",
    "macAddress": "AA:BB:CC:DD:EE:FF",
    "expirationTime": "2027-01-01 00:00:00"
  }
}
```

---

### 1.4 获取设备详情

```
GET /app/v1/device/{deviceNo}
```

**路径参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceNo` | `string` | **是** | 设备编号 |

**响应**：`R<AppDeviceVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "deviceNo": "D20260101001",
    "userId": 1,
    "deviceName": "前院浇灌器",
    "deviceInitName": "智能浇灌器Pro",
    "deviceImg": "https://oss.example.com/device/xxx.png",
    "qrcode": "D20260101001",
    "status": "1",
    "workStatus": "0",
    "powerLevel": "85",
    "powerLevelUpdatatime": "2026-07-14 08:30:00",
    "wifiName": "Home-WiFi",
    "wifiPassword": "12345678",
    "deviceEm": "WATER-PRO-1",
    "deviceSn": "SN20260101001",
    "fwVer": "1.0.3",
    "macAddress": "AA:BB:CC:DD:EE:FF",
    "nickName": "张三",
    "expirationTime": "2027-01-01 00:00:00"
  }
}
```

> **权限**：只能查询当前用户名下的设备，否则返回错误。

---

### 1.5 修改设备信息

```
PUT /app/v1/updateDeviceInfo
```

> `@RepeatSubmit` — 防重复提交

**请求头**

```
Content-Type: application/json
```

**请求体**：`AppDeviceBo`（JSON）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceNo` | `string` | **是** | 设备编号（EditGroup 校验） |
| `deviceName` | `string` | 否 | 设备名称 |
| `deviceImg` | `string` | 否 | 设备图片 URL |
| `wifiName` | `string` | 否 | WiFi 名称 |
| `wifiPassword` | `string` | 否 | WiFi 密码 |

```json
{
  "deviceNo": "D20260101001",
  "deviceName": "后院浇灌器",
  "deviceImg": "https://oss.example.com/device/new.png"
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **权限**：只能修改当前用户名下的设备。

---

### 1.6 删除设备（解绑）

解绑一个或多个设备。

```
DELETE /app/v1/deleteDevice/{deviceNos}
```

**路径参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceNos` | `string` | **是** | 设备编号，多个用逗号分隔。例：`D001,D002` |

**示例**

```
DELETE /app/v1/deleteDevice/D20260101001,D20260101002
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **权限**：只能解绑当前用户名下的设备。

---

### 1.7 手动开关设备

手动开启或关闭设备工作状态，并可通过 MQTT 下发指令到设备。

```
PUT /app/v1/switchDevice
```

> `@RepeatSubmit` — 防重复提交

**请求头**

```
Content-Type: application/json
```

**请求体**（JSON）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceNo` | `string` | **是** | 设备编号 |
| `workStatus` | `string` | **是** | 工作状态：`1`-开启 `0`-关闭 |
| `durationMin` | `int` | 条件必填 | 持续时长（分钟）；`workStatus=1` 时必须大于 `0`，关闭时可不传 |

```json
{
  "deviceNo": "D20260101001",
  "workStatus": "1",
  "durationMin": 20
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **说明**：开启时 `startTime` 由服务端按当前时间生成，格式为 `yyyy-MM-dd HH:mm:ss`。

---

## 二、排程管理

### 2.1 查询排程列表（分页）

```
GET /app/v1/schedulelist
```

**请求参数（Query）**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | `int` | 否 | 页码 |
| `pageSize` | `int` | 否 | 每页条数 |
| `name` | `string` | 否 | 排程名称（模糊查询） |
| `status` | `string` | 否 | 状态：`0`-关闭 `1`-开启 |

**响应**：`TableDataInfo<AppScheduleVo>`

```json
{
  "code": 200,
  "msg": "查询成功",
  "total": 1,
  "rows": [
    {
      "id": 1,
      "userId": 1,
      "name": "工作日早晨浇水",
      "status": "1",
      "details": null,
      "deviceNos": null
    }
  ]
}
```

> **注意**：列表查询返回的 `details` 和 `deviceNos` 为 `null`，完整信息请通过 [2.3 排程详情](#23-排程详情) 获取。

---

### 2.2 新增排程

```
POST /app/v1/addschedule
```

> `@RepeatSubmit` — 防重复提交

**请求头**

```
Content-Type: application/json
```

**请求体**：`AppScheduleBo`（JSON，AddGroup 校验）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | `string` | 否 | 排程名称 |
| `status` | `string` | 否 | 状态：`0`-关闭 `1`-开启 |
| `details` | `array` | **是** | 排程详情列表（至少一天） |

**`details` 数组元素结构（`AppScheduleDetail`）**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `weekday` | `int` | **是** | 星期几：`1`-周一 ~ `7`-周日 |
| `timeData` | `array` | **是** | 时间段列表 |
| `zones` | `int` | 否 | 浇水区域（位掩码） |
| `triggerType` | `string` | 否 | 触发类型：`0`-排程（默认） |
| `status` | `string` | 否 | 状态：`0`-关闭（默认） |

**`timeData` 数组元素结构（`TimeSlot`）**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `startTime` | `string` | **是** | 开始时间，格式 `HH:mm` |
| `durationMin` | `int` | 否 | 持续时长（分钟） |

```json
{
  "name": "工作日早晨浇水",
  "status": "1",
  "details": [
    {
      "weekday": 1,
      "timeData": [
        { "startTime": "06:30", "durationMin": 15 },
        { "startTime": "18:00", "durationMin": 20 }
      ],
      "triggerType": "0",
      "status": "1"
    },
    {
      "weekday": 2,
      "timeData": [
        { "startTime": "06:30", "durationMin": 15 }
      ],
      "triggerType": "0",
      "status": "1"
    }
  ]
}
```

**响应**：`R<AppScheduleVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "id": 10,
    "userId": 1,
    "name": "工作日早晨浇水",
    "status": "1",
    "details": [
      {
        "id": 101,
        "weekday": 1,
        "timeData": [
          { "startTime": "06:30", "durationMin": 15 },
          { "startTime": "18:00", "durationMin": 20 }
        ],
        "triggerType": "0",
        "status": "1"
      }
    ],
    "deviceNos": null
  }
}
```

---

### 2.3 排程详情

获取排程完整信息，包含排程详情列表和已绑定设备列表。

```
GET /app/v1/schedule/{id}
```

**路径参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | `long` | **是** | 排程 ID |

**响应**：`R<AppScheduleVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "id": 10,
    "userId": 1,
    "name": "工作日早晨浇水",
    "status": "1",
    "details": [
      {
        "id": 1,
        "weekday": 1,
        "timeData": [
          { "startTime": "06:30", "durationMin": 15 },
          { "startTime": "18:00", "durationMin": 20 }
        ],
        "triggerType": "0",
        "status": "1"
      }
    ],
    "deviceNos": [
      {
        "deviceNo": "D20260101001",
        "userId": 1,
        "deviceName": "前院浇灌器",
        "status": "1",
        "workStatus": "0"
      }
    ]
  }
}
```

> **说明**：
> - `details` 返回该排程当前查询到的全部明细，不在控制器中额外过滤状态。
> - `deviceNos` 为已绑定设备的完整 `AppDeviceVo` 列表。
> - `timeData` 已从 JSON 字符串解析为数组。

---

### 2.4 修改排程状态

开启或关闭排程，状态变更后通过 MQTT 通知所有已绑定设备。

```
PUT /app/v1/editScheduleStatus
```

**请求头**

```
Content-Type: application/json
```

**请求体**：`AppScheduleBo`（JSON）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | `long` | **是** | 排程 ID |
| `status` | `string` | **是** | 目标状态：`0`-关闭 `1`-开启 |

```json
{
  "id": 10,
  "status": "0"
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **MQTT 通知**：
> - 关闭（`status=0`）：向绑定设备下发排程取消指令。
> - 开启（`status=1`）：向绑定设备重新下发完整排程信息。

---

### 2.5 修改排程

修改排程内容，更新后通过 MQTT 向所有绑定设备重新下发排程。

```
PUT /app/v1/updataschedule
```

> `@RepeatSubmit` — 防重复提交

**请求头**

```
Content-Type: application/json
```

**请求体**：`AppScheduleBo`（JSON，EditGroup 校验）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | `long` | **是** | 排程 ID（EditGroup 校验） |
| `name` | `string` | 否 | 排程名称 |
| `status` | `string` | 否 | 状态：`0`-关闭 `1`-开启 |
| `details` | `array` | 否 | 排程详情列表（同 [2.2 新增排程](#22-新增排程)） |

```json
{
  "id": 10,
  "name": "周末浇水计划",
  "status": "1",
  "details": [
    {
      "id": 101,
      "weekday": 6,
      "timeData": [
        { "startTime": "08:00", "durationMin": 30 }
      ],
      "triggerType": "0",
      "status": "1"
    }
  ]
}
```

**响应**：`R<Object>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "id": 10,
    "userId": 1,
    "name": "周末浇水计划",
    "status": "1",
    "details": [ ]
  }
}
```

> **说明**：`data` 为更新后的 `AppScheduleVo` 对象。修改已有排程明细时应传明细 `id`，否则服务端无法按主键更新该明细。

---

### 2.6 删除排程

删除一个或多个排程，并通知所有绑定设备解绑。

```
DELETE /app/v1/deleteschedule/{ids}
```

**路径参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `ids` | `long` | **是** | 排程 ID，多个用逗号分隔。例：`10,11` |

**示例**

```
DELETE /app/v1/deleteschedule/10,11
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **MQTT 通知**：删除成功后，向每个排程关联的所有设备下发解绑指令。

---

## 三、排程-设备关联

### 3.1 查询排程可关联的设备

查询当前用户名下**未被其他排程关联**的设备列表。

```
GET /app/v1/scheduleDeviceList/{scheduleId}
```

**路径参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `scheduleId` | `long` | **是** | 排程 ID |

**响应**：`R<Map<String, Object>>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "deviceList": [
      {
        "deviceNo": "D20260101003",
        "userId": 1,
        "deviceName": "阳台浇灌器",
        "status": "1",
        "workStatus": "0"
      }
    ]
  }
}
```

> **说明**：返回的设备列表为当前用户名下未与任何排程关联的设备。已关联其他排程的设备不会出现在列表中。

---

### 3.2 绑定排程与设备

将一个或多个设备绑定到指定排程，绑定后通过 MQTT 向设备下发排程信息。

```
POST /app/v1/addScheduleDevice
```

> `@RepeatSubmit` — 防重复提交

**请求头**

```
Content-Type: application/json
```

**请求体**（JSON）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `scheduleId` | `long` | **是** | 排程 ID |
| `deviceNos` | `array` | **是** | 设备编号列表。也可用 `deviceNo`（逗号分隔字符串）或 `deviceIds` |

```json
{
  "scheduleId": 10,
  "deviceNos": ["D20260101001", "D20260101003"]
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **说明**：
> - `deviceNos` 支持数组或逗号分隔字符串格式。
> - 已绑定该排程的设备会自动跳过（幂等）。
> - 绑定后，服务端通过 MQTT 向设备下发排程详情。

---

### 3.3 删除排程与设备绑定

解除排程与设备的绑定关系，并通知设备取消排程。

```
DELETE /app/v1/deleteScheduleDevice
```

**请求参数（Query）**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `scheduleId` | `string` | **是** | 排程 ID |
| `deviceNo` | `string` | **是** | 设备编号 |

**示例**

```
DELETE /app/v1/deleteScheduleDevice?scheduleId=10&deviceNo=D20260101001
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **MQTT 通知**：解绑成功后，向设备下发排程解绑指令。若 MQTT 下发失败，不影响接口返回成功（仅记录日志警告）。

---

## 四、浇水记录

### 4.1 查询浇水记录列表（分页）

```
GET /app/v1/waterLogList
```

**请求参数（Query）**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | `int` | 否 | 页码 |
| `pageSize` | `int` | 否 | 每页条数 |
| `deviceNo` | `string` | 否 | 设备编号 |
| `commandId` | `string` | 否 | MQTT 命令 ID |
| `scheduleId` | `long` | 否 | 关联排程 ID |
| `startTime` | `date` | 否 | 开始时间（精确匹配） |
| `endTime` | `date` | 否 | 结束时间（精确匹配） |
| `durationMin` | `string` | 否 | 持续时间（精确匹配） |
| `zones` | `string` | 否 | 浇水区域 |
| `triggerType` | `string` | 否 | 触发类型：`0`-排程 `1`-手动 |
| `remark` | `string` | 否 | 备注（模糊匹配） |
| `params[beginTime]` | `date` | 否 | 创建时间范围开始；需与 `params[endTime]` 同时传入 |
| `params[endTime]` | `date` | 否 | 创建时间范围结束；需与 `params[beginTime]` 同时传入 |

**响应**：`TableDataInfo<AppWateringLogVo>`

```json
{
  "code": 200,
  "msg": "查询成功",
  "total": 50,
  "rows": [
    {
      "id": 1,
      "userId": 1,
      "deviceNo": "D20260101001",
      "deviceName": "前院浇灌器",
      "commandId": "CMD_20260714001",
      "scheduleId": 10,
      "startTime": "2026-07-14 06:30:00",
      "endTime": "2026-07-14 06:45:00",
      "durationMin": "15",
      "zones": "1",
      "triggerType": "0",
      "remark": null,
      "status": "0",
      "createTime": "2026-07-14 06:30:00"
    }
  ]
}
```

> **说明**：响应中 `deviceName` 由服务端关联查询后填充。

---

### 4.2 查询设备最新进行中的浇水记录

```
GET /app/v1/latestWaterLog
```

**请求参数（Query）**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceNo` | `string` | **是** | 设备编号 |

**响应**：`R<Map<String, Object>>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "startTime": "2026-07-14 08:00:00",
    "endTime": "2026-07-14 08:20:00"
  }
}
```

> **说明**：控制器按当前用户、设备编号和状态 `1` 发起查询，并取 ID 最大的一条记录。若无进行中的记录，`data` 为 `null`。时间格式为 `yyyy-MM-dd HH:mm:ss`。
>
> **当前实现限制**：`AppWateringLogServiceImpl.buildQueryWrapper` 尚未把 `status` 加入查询条件，因此现有代码实际可能取到该设备 ID 最大但已结束的记录。客户端依赖“进行中”语义前，应先修复该服务查询条件。

---

### 4.3 删除浇水记录

```
DELETE /app/v1/waterLog/{ids}
```

**路径参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `ids` | `long` | **是** | 记录 ID，多个用逗号分隔。例：`1,2,3` |

**示例**

```
DELETE /app/v1/waterLog/1,2,3
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **权限**：只能删除当前用户的浇水记录。

---

## 五、数据统计

### 5.1 数据统计

获取当前用户的浇水数据统计汇总。

```
GET /app/v1/dataStatistics
```

**请求参数**：无

**响应**：`R<Map<String, Object>>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "count": 120,
    "monCount": 15,
    "weekCount": 4,
    "schedule": 80,
    "manualNum": 40,
    "allTime": 3600,
    "averageTime": 30
  }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `count` | `long` | 总浇水次数 |
| `monCount` | `long` | 本月浇水次数 |
| `weekCount` | `long` | 本周浇水次数 |
| `schedule` | `long` | 排程触发浇水次数 |
| `manualNum` | `long` | 手动触发浇水次数 |
| `allTime` | `long` | 总浇水时间（分钟） |
| `averageTime` | `long` | 平均每次浇水时间（分钟） |

---

## 六、用户管理

### 6.1 获取用户信息

```
GET /app/v1/getUserInfo
```

**请求参数**：无

**响应**：`R<UserInfoVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "user": {
      "userId": 1,
      "deptId": 100,
      "userName": "admin",
      "nickName": "管理员",
      "email": "admin@example.com",
      "phonenumber": "138****8888",
      "sex": "0",
      "avatar": "https://oss.example.com/avatar/xxx.png",
      "status": "0",
      "loginIp": "192.168.1.1",
      "loginDate": "2026-07-14 09:00:00",
      "remark": null,
      "createTime": "2026-01-01 00:00:00"
    },
    "permissions": ["*:*:*"],
    "roles": ["admin"]
  }
}
```

> **说明**：`phonenumber` 和 `email` 字段做了脱敏处理（需 `system:user:edit` 权限才能看到完整内容）。

---

### 6.2 修改用户信息

```
PUT /app/v1/updataUser
```

> `@RepeatSubmit` — 防重复提交

**请求头**

```
Content-Type: application/json
```

**请求体**：`SysUserBo`（JSON）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `userId` | `long` | **是** | 用户 ID |
| `nickName` | `string` | 否 | 用户昵称（最长 30 字符） |
| `email` | `string` | 否 | 邮箱（需符合邮箱格式，最长 50 字符） |
| `phonenumber` | `string` | 否 | 手机号（需唯一） |
| `code` | `string` | 条件必填 | 修改手机号时使用的短信验证码 |
| `sex` | `string` | 否 | 性别：`0`-男 `1`-女 `2`-未知 |
| `avatar` | `string` | 否 | 头像 URL |

```json
{
  "userId": 1,
  "nickName": "新昵称",
  "phonenumber": "13900001111",
  "code": "123456",
  "email": "new@example.com",
  "sex": "0"
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **校验**：手机号需全局唯一；请求中包含 `phonenumber` 时，服务层会校验 `code` 短信验证码。

---

### 6.3 修改登录密码

```
PUT /app/v1/updataPassword
```

> `@RepeatSubmit` — 防重复提交

**请求头**

```
Content-Type: application/json
```

**请求体**：`SysUserVo`（JSON）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `oldPassword` | `string` | **是** | 旧密码（明文，服务端 BCrypt 校验） |
| `password` | `string` | **是** | 新密码（明文） |

```json
{
  "oldPassword": "OldPass123",
  "password": "NewPass456"
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **说明**：旧密码使用 BCrypt 校验，不匹配则返回 `user.password.not.match` 错误。

---

### 6.4 找回密码

通过手机号或邮箱找回密码（加密接口）。

```
POST /app/v1/retrievePassword
```

**请求头**

```
Content-Type: application/json
```

**请求体**（JSON；启用接口加密时发送加密后的 JSON 字符串）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `userName` | `string` | **是** | 用户名（手机号或邮箱） |
| `password` | `string` | **是** | 新密码 |
| `code` | `string` | 条件必填 | `userName` 为手机号时使用的短信验证码 |

```json
{
  "userName": "13900001111",
  "password": "NewPass456",
  "code": "123456"
}
```

**响应**：`R<Void>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": null
}
```

> **校验逻辑**：
> - 若 `userName` 为邮箱格式：校验邮箱是否已注册，未注册则报错 `user.email.not.username`。
> - 若 `userName` 为手机号格式：校验手机号是否已注册及短信验证码，未注册则报错 `user.mobile.phone.number.not.username`。
> - 当前实现仍从登录 Token 获取 `userId`，因此该接口不是匿名接口，并且控制器内未校验短信/邮箱验证码。

---

## 七、文件上传

### 7.1 上传图片

上传图片到 OSS 对象存储，仅支持图片类型。

```
POST /app/v1/uploadImage
```

**请求头**

```
Content-Type: multipart/form-data
```

**请求参数（FormData）**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `file` | `file` | **是** | 图片文件（Content-Type 必须以 `image/` 开头） |

当前 Spring Multipart 配置允许单文件最大 `100MB`、单次请求最大 `100MB`；实际部署还可能受网关和 OSS 配置限制。

**响应**：`R<SysOssUploadVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "url": "https://oss.example.com/2026/07/14/xxx.png",
    "fileName": "screenshot.png",
    "ossId": "123456"
  }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `url` | `string` | 文件访问 URL |
| `fileName` | `string` | 原始文件名 |
| `ossId` | `string` | 对象存储记录 ID |

---

## 八、版本检查

### 8.1 检查APP版本更新

```
GET /app/v1/checkVersion
```

**请求参数（Query）**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `platform` | `string` | 否 | 平台类型：`android`（默认）或 `ios` |
| `currentVersion` | `string` | **是** | APP 当前版本号 |

**示例**

```
GET /app/v1/checkVersion?platform=android&currentVersion=1.0.2
```

**响应**：`R<AppVersionCheckVo>`

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "platform": "android",
    "currentVersion": "1.0.2",
    "latestVersion": "1.0.3",
    "versionCode": 3,
    "updateAvailable": true,
    "forceUpdate": false,
    "downloadUrl": "https://oss.example.com/app/water-1.0.3.apk",
    "releaseNotes": "1. 修复设备绑定问题\n2. 优化排程功能"
  }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `platform` | `string` | 平台类型 |
| `currentVersion` | `string` | 客户端当前版本 |
| `latestVersion` | `string` | 服务端最新版本 |
| `versionCode` | `int` | 版本序号（用于排序） |
| `updateAvailable` | `boolean` | 是否有可更新版本 |
| `forceUpdate` | `boolean` | 是否强制更新 |
| `downloadUrl` | `string` | 安装包下载地址 |
| `releaseNotes` | `string` | 更新说明 |

> 当前版本判断采用版本字符串是否完全相等：`currentVersion` 与 `latestVersion` 不相等时，`updateAvailable=true`，未进行语义化版本大小比较。

---

## 九、内部调试接口

### 9.1 MQTT绑定命令测试

```
GET /app/v1/test
```

该接口会直接调用 `sendBindDeviceCommand("01")`，向固定设备编号 `01` 下发绑定命令，并返回生成的命令 ID。

**响应**：`R<Void>`（运行时实际将命令 ID 作为消息文本返回）

```json
{
  "code": 200,
  "msg": "生成的命令ID",
  "data": null
}
```

> 该路由仅用于后端联调，不属于 APP 正式业务接口。生产环境建议删除、关闭或增加管理员权限控制。

---

## 附录：数据模型

### AppDeviceVo（设备信息）

| 字段 | 类型 | 说明 |
|------|------|------|
| `deviceNo` | `string` | 设备编号 |
| `userId` | `long` | 用户 ID |
| `deviceName` | `string` | 设备名称 |
| `deviceInitName` | `string` | 设备初始名称 |
| `deviceImg` | `string` | 设备图片 URL |
| `qrcode` | `string` | 二维码内容 |
| `status` | `string` | 设备状态：`1`-在线 `0`-离线 `2`-到期 `3`-故障 |
| `workStatus` | `string` | 工作状态：`0`-休息 `1`-工作 `2`-初始/未配网 |
| `powerLevel` | `string` | 电量百分比 |
| `powerLevelUpdatatime` | `date` | 电量更新时间 |
| `wifiName` | `string` | WiFi 名称 |
| `wifiPassword` | `string` | WiFi 密码 |
| `deviceEm` | `string` | 设备型号 |
| `deviceSn` | `string` | 设备序列号 |
| `fwVer` | `string` | 固件版本 |
| `macAddress` | `string` | MAC 地址 |
| `nickName` | `string` | 用户昵称 |
| `expirationTime` | `date` | 到期时间 |

### AppScheduleVo（排程信息）

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | `long` | 排程 ID |
| `userId` | `long` | 用户 ID |
| `name` | `string` | 排程名称 |
| `status` | `string` | 状态：`0`-关闭 `1`-开启 |
| `details` | `array` | 排程详情列表；新增和详情接口会返回，列表接口通常为空 |
| `deviceNos` | `array` | 已绑定设备列表（仅详情接口返回） |

### AppScheduleDetail（排程详情）

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | `long` | 详情 ID |
| `weekday` | `int` | 星期几：`1`-周一 ~ `7`-周日 |
| `timeData` | `array` | 时间段列表 |
| `triggerType` | `string` | 触发类型：`0`-排程 `1`-手动 |
| `status` | `string` | 状态：`0`-关闭 `1`-开启 |

### TimeSlot（时间段）

| 字段 | 类型 | 说明 |
|------|------|------|
| `startTime` | `string` | 开始时间（格式 `HH:mm`） |
| `durationMin` | `int` | 持续时长（分钟） |

### AppWateringLogVo（浇水记录）

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | `long` | 记录 ID |
| `userId` | `long` | 用户 ID |
| `deviceNo` | `string` | 设备编号 |
| `deviceName` | `string` | 设备名称 |
| `commandId` | `string` | 命令 ID |
| `scheduleId` | `long` | 关联排程 ID |
| `startTime` | `date` | 开始时间 |
| `endTime` | `date` | 结束时间 |
| `durationMin` | `string` | 持续时间（分钟） |
| `zones` | `string` | 浇水区域 |
| `triggerType` | `string` | 触发类型：`0`-排程 `1`-手动 |
| `remark` | `string` | 备注 |
| `status` | `string` | 状态：`0`-已结束 `1`-进行中 |
| `createTime` | `date` | 创建时间 |

### 枚举值速查

| 枚举 | 值 | 说明 |
|------|------|------|
| **设备状态 `status`** | `1` | 在线 |
| | `0` | 离线 |
| | `2` | 到期 |
| | `3` | 故障 |
| **工作状态 `workStatus`** | `0` | 休息 |
| | `1` | 工作 |
| | `2` | 初始/未配网 |
| **排程状态 `status`** | `0` | 关闭 |
| | `1` | 开启 |
| **触发类型 `triggerType`** | `0` | 排程触发 |
| | `1` | 手动触发 |
| **浇水记录状态 `status`** | `0` | 已结束 |
| | `1` | 进行中 |
| **星期 `weekday`** | `1`~`7` | 周一~周日 |
| **用户性别 `sex`** | `0` | 男 |
| | `1` | 女 |
| | `2` | 未知 |
| **强制更新 `forceUpdate`** | `0` | 否 |
| | `1` | 是 |

---

> **联调提示**：
> 1. `/app/v1/**` 业务接口及明确标注“需要登录”的认证接口，请求头需携带 `Authorization: Bearer <token>`；登录、注册、忘记密码、租户列表和普通验证码接口免登录。
> 2. 当前 `api-decrypt.enabled=false` 时发送普通 JSON；启用后，标注 `@ApiEncrypt` 的接口（绑定设备状态检查、绑定设备、找回密码）需按约定加密请求体并携带 `encrypt-key`。
> 3. 标注 `@RepeatSubmit` 的接口，短时间内重复提交会被拦截，前端应在收到响应前禁用提交按钮。
> 4. 删除类接口的路径参数支持逗号分隔多个 ID。
> 5. 分页接口排序固定为 `createTime desc`，前端无需传排序参数。
> 6. 时间字段统一使用 `yyyy-MM-dd HH:mm:ss` 格式，时间段 `startTime` 使用 `HH:mm` 格式。
