fix(app): 修复 AppController 安全与查询问题

- 加强异常处理、类型安全和图片上传校验
- 优化设备相关查询,避免重复访问数据源
- 补充并记录并发测试与审查修复实施计划
This commit is contained in:
yuhaiming
2026-07-17 08:20:44 +08:00
parent 4a4aabf19e
commit 70e2356f22
82 changed files with 6070 additions and 583 deletions

View File

@@ -38,14 +38,15 @@
| Topic 模式 | 正则 | 处理器 | 说明 |
|------------|------|--------|------|
| `/{identity}/publish/register` | `^/([^/]+)/publish/register$` | `DeviceRegisterHandler` | 设备注册/上线 |
| `/{identity}/publish/power` | `^/([^/]+)/publish/power$` | `DeviceDataHandler` | 电量数据上报 |
| `/{identity}/publish/register` | `^/([^/]+)/publish/register$` | `DeviceRegisterHandler` | 设备注册 |
| `/{identity}/publish/status` | `^/([^/]+)/publish/status$` | `DeviceStatusHandler` | 设备 LWT 离线通知(仅处理 offline |
| `/{identity}/publish/power` | `^/([^/]+)/publish/power$` | `DeviceDataHandler` | 电量数据上报及在线心跳 |
| `/{identity}/publish/finish/key` | `^/([^/]+)/publish/finish/key$` | `KeyFinishHandler` | 按键/手动浇水完成 |
| `/{identity}/publish/finish/schedule` | `^/([^/]+)/publish/finish/schedule$` | `ScheduleFinishHandler` | 排程浇水完成 |
| `/{identity}/publish/error` | `^/([^/]+)/publish/error$` | `ErromesHandler` | 设备异常告警 |
| `/{identity}/publish/ack` | `^/([^/]+)/publish/ack$` | `DeviceCommandAckHandler` | 命令执行确认 |
> `{identity}` 可以是设备编号deviceNo MAC 地址, `DeviceIdentityResolver` 统一解析为 deviceNo。
> 首次注册可使用 MAC 地址;注册完成后的业务 Topic 使用 deviceNo。LWT 离线 Topic 兼容 MAC 地址,服务端通过 `DeviceIdentityResolver` 解析为 deviceNo。
### 2.2 下行 Topic服务端 → 设备)
@@ -304,8 +305,9 @@ long nextRetryAt; // 下次重试时间戳
**服务端处理**:
1. 通过 MAC 或设备编号解析入库设备
2. 更新设备注册信息(名称、电量、固件版本等)
3. 刷新设备在线状态到 Redis
4. 自动回复设备编号(`registerDeviceNo` 命令)
3. 自动回复设备编号(`registerDeviceNo` 命令)
> 注册成功即将设备标记为在线;设备取得 deviceNo 后仍必须定时上报电量,以持续刷新在线心跳。
---
@@ -317,11 +319,36 @@ long nextRetryAt; // 下次重试时间戳
}
```
**服务端处理**: 更新设备电量 + 刷新在线状态
**服务端处理**: 更新设备电量,将数据库设备状态改为在线,并将 Redis 在线心跳刷新为 600 秒
设备必须周期上报,即使电量没有变化也要发送。建议每 5 分钟上报一次,连续 10 分钟未收到有效电量消息且期间无 ACK 刷新时,服务端将设备判定为离线。
---
### 5.3 按键浇水完成 — `/{identity}/publish/finish/key`
### 5.3 设备离线遗嘱 — `/{identity}/publish/status`
```json
{
"status": "offline"
}
```
**服务端处理**: 收到非 retained 的 `offline``0` 后立即将设备改为离线。`online``1` 不再用于上线,服务端会忽略,上线状态只由电量心跳维护。
设备应将该消息配置为 MQTT LWT建议 `retain=false`。Topic 第一段优先使用 deviceNo设备注册前无法取得 deviceNo 时可使用 MAC 地址。
兼容设备也可以发送以下格式,服务端会使用 `deviceMac` 解析设备,并将 `offline=true`(字符串或布尔值)按离线处理:
```json
{
"deviceMac": "AA:BB:CC:DD:EE:FF",
"offline": "true"
}
```
---
### 5.4 按键浇水完成 — `/{identity}/publish/finish/key`
```json
{
@@ -340,7 +367,7 @@ long nextRetryAt; // 下次重试时间戳
---
### 5.4 排程浇水完成 — `/{identity}/publish/finish/schedule`
### 5.5 排程浇水完成 — `/{identity}/publish/finish/schedule`
```json
{
@@ -358,7 +385,7 @@ long nextRetryAt; // 下次重试时间戳
---
### 5.5 设备异常告警 — `/{identity}/publish/error`
### 5.6 设备异常告警 — `/{identity}/publish/error`
```json
{
@@ -371,7 +398,7 @@ long nextRetryAt; // 下次重试时间戳
---
### 5.6 命令 ACK — `/{identity}/publish/ack`
### 5.7 命令 ACK — `/{identity}/publish/ack`
```json
{
@@ -411,6 +438,7 @@ long nextRetryAt; // 下次重试时间戳
| `maxRetryCount` | 3 | 最大重试次数 |
| `retryIntervalMs` | 5000 | 重试间隔ms |
| `scanIntervalMs` | 5000 | 定时扫描间隔ms |
| `ackLockWaitMs` | 3000 | ACK 等待同一命令重试锁的最长时间ms |
| `pendingTtlSeconds` | 86400 | pending 命令 TTL |
| `ackTtlSeconds` | 86400 | ACK 结果缓存 TTL |
@@ -495,8 +523,14 @@ mqtt:
pending-set-key: "mqtt:command:pending:ids"
retry-lock-key-prefix: "lock:mqtt:command:retry:"
retry-lock-ttl-ms: 30000
ack-lock-wait-ms: 3000
device-status-cache-prefix: "mqtt:device:status:"
device-status-cache-ttl-seconds: 300
device-status-cache-ttl-seconds: 600
offline-check:
enabled: true
ttl-compat-enabled: true
interval-ms: 30000
```
---
@@ -538,10 +572,13 @@ public class MyNewHandler implements MqttTopicHandler {
## 10. 设备在线状态检测
设备在线状态通过 Redis 缓存管理:
设备在线状态由电量上报和离线遗嘱共同管理:
- **写入时机**: 设备注册、电量上报、ACK 确认时刷新
- **上线时机**: 注册成功或收到包含 `powerLevel` 的有效电量上报时刷新
- **ACK 兼容逻辑**: 保留现有 ACK 在线刷新逻辑,收到有效 ACK 也会延长在线缓存
- **缓存格式**: `{ "deviceNo": "01", "status": "1", "lastReportTime": "2026-06-25T14:30:00Z" }`
- **TTL**: 默认 300 秒(5 分钟)
- **离线判定**: 缓存过期 = 设备离线
- **TTL**: 默认 600 秒(10 分钟)
- **超时离线**: 每 30 秒扫描一次非离线设备,缓存过期后更新数据库状态为离线
- **遗嘱离线**: 收到非 retained 的 LWT `status=offline/0` 时立即离线
- **忽略在线状态**: `status=online/1` 不会刷新在线状态
- **并发保护**: 使用分布式锁(`lock:mqtt:device:status:{deviceNo}`)防止并发写入