diff --git a/开发者文档/05-移动端开发/01-移动端工程结构与页面分域.md b/开发者文档/05-移动端开发/01-移动端工程结构与页面分域.md
index b0699f6..be850b7 100644
--- a/开发者文档/05-移动端开发/01-移动端工程结构与页面分域.md
+++ b/开发者文档/05-移动端开发/01-移动端工程结构与页面分域.md
@@ -38,33 +38,71 @@ aiot-uniapp/
│ │ └── helpers.ts # 通用辅助函数
│ │
│ ├── pages/ # 【主包】核心页面
-│ │ ├── login/
-│ │ │ ├── index.vue # 登录页
-│ │ │ └── auth.vue # 静默授权回调
│ │ ├── index/
│ │ │ └── index.vue # 工作台首页(Tab Bar)
+│ │ ├── scan/
+│ │ │ ├── index.vue # 扫码入口
+│ │ │ └── bluetooth-debug/ # 蓝牙调试页
+│ │ ├── user/
+│ │ │ └── index.vue # 个人中心
+│ │ └── contact/
+│ │ └── index.vue # 通讯录
+│ │
+│ ├── pages-core/ # 【分包】核心功能(登录、设置)
+│ │ ├── auth/
+│ │ │ ├── login.vue # 账号密码登录
+│ │ │ ├── code-login.vue # 验证码登录
+│ │ │ └── forget-password.vue
│ │ └── user/
-│ │ └── index.vue # 个人中心
+│ │ ├── profile/
+│ │ ├── settings/
+│ │ └── security/
│ │
│ ├── pages-ops/ # 【分包】Ops 现场作业
│ │ ├── work-order/
-│ │ │ ├── list.vue # 工单列表
+│ │ │ ├── index.vue # 工单列表
│ │ │ ├── detail.vue # 工单详情
-│ │ │ ├── arrival.vue # 到岗打卡(信标扫描)
-│ │ │ └── complete.vue # 完工提交
+│ │ │ ├── create.vue # 创建工单
+│ │ │ └── stats.vue # 工单统计
│ │ ├── inspection/
-│ │ │ ├── list.vue # 巡检任务列表
-│ │ │ ├── scan.vue # 扫码巡检
-│ │ │ └── record.vue # 巡检记录填报
-│ │ └── queue/
-│ │ └── index.vue # 我的队列
+│ │ │ ├── index.vue # 巡检任务列表
+│ │ │ ├── detail.vue # 巡检详情
+│ │ │ └── components/
+│ │ │ └── bluetooth-verify.vue # 蓝牙信标验证
+│ │ └── inspection/composables/
+│ │ └── use-bluetooth-scan.ts # 蓝牙扫描 Hook
│ │
-│ ├── pages-iot/ # 【分包】IoT 设备管理
-│ │ ├── device/
-│ │ │ ├── list.vue
-│ │ │ └── detail.vue
-│ │ └── alarm/
-│ │ └── index.vue
+│ ├── pages-system/ # 【分包】系统管理
+│ │ ├── area/
+│ │ ├── dept/
+│ │ ├── user/
+│ │ └── dict/
+│ │
+│ ├── pages-bpm/ # 【分包】工作流
+│ │
+│ ├── pages-infra/ # 【分包】基础设施
+│ │
+│ ├── api/ # API 接口定义
+│ │ ├── ops/ # Ops 业务 API
+│ │ │ ├── order-center/ # 工单中心
+│ │ │ ├── cleaning/ # 保洁业务
+│ │ │ ├── security/ # 安保业务
+│ │ │ └── inspection/ # 巡检业务
+│ │ ├── iot/ # IoT 设备 API
+│ │ ├── system/ # 系统管理 API
+│ │ └── login.ts # 登录相关 API
+│ │
+│ ├── store/ # Pinia 状态管理
+│ │ ├── token.ts # Token 管理(双 Token 刷新)
+│ │ ├── user.ts # 用户信息
+│ │ ├── dict.ts # 字典缓存
+│ │ └── theme.ts # 主题配置
+│ │
+│ ├── http/ # HTTP 请求封装
+│ │ ├── http.ts # 核心 HTTP 方法
+│ │ ├── types.ts # 类型定义
+│ │ └── tools/
+│ │ └── enum.ts # ResultEnum 状态码
│ │
│ ├── static/ # 静态资源
│ │ ├── images/
@@ -94,9 +132,10 @@ aiot-uniapp/
| 目录 | 说明 | 体积控制 |
|-----|------|---------|
-| `/pages/login/` | 登录、验证码、静默授权页 | < 200KB |
| `/pages/index/` | 工作台首页(Tab Bar) | < 300KB |
-| `/pages/user/` | 个人中心基础设置 | < 100KB |
+| `/pages/scan/` | 扫码入口 | < 200KB |
+| `/pages/user/` | 个人中心 | < 100KB |
+| `/pages/contact/` | 通讯录 | < 150KB |
| `components/` | 全局共享的基础 UI 组件库 | < 200KB |
**严禁将具体的业务流程(如填写工单、扫描设备)塞入主包。**
@@ -109,27 +148,36 @@ aiot-uniapp/
// pages.json
{
"pages": [
- { "path": "pages/login/index" },
{ "path": "pages/index/index" },
- { "path": "pages/user/index" }
+ { "path": "pages/scan/index" },
+ { "path": "pages/user/index" },
+ { "path": "pages/contact/index" }
],
"subPackages": [
{
- "root": "pages-ops",
+ "root": "pages-core",
"pages": [
- { "path": "work-order/list" },
- { "path": "work-order/detail" },
- { "path": "work-order/arrival" },
- { "path": "inspection/list" },
- { "path": "inspection/scan" }
+ { "path": "auth/login" },
+ { "path": "auth/code-login" },
+ { "path": "user/profile/index" }
]
},
{
- "root": "pages-iot",
+ "root": "pages-ops",
"pages": [
- { "path": "device/list" },
- { "path": "device/detail" },
- { "path": "alarm/index" }
+ { "path": "work-order/index" },
+ { "path": "work-order/detail" },
+ { "path": "work-order/create" },
+ { "path": "inspection/index" },
+ { "path": "inspection/detail" }
+ ]
+ },
+ {
+ "root": "pages-system",
+ "pages": [
+ { "path": "area/index" },
+ { "path": "dept/index" },
+ { "path": "user/index" }
]
}
],
@@ -150,21 +198,103 @@ aiot-uniapp/
---
-## 三、页面状态与生命周期规约
+## 三、页面路由与导航
-移动端页面的生命周期(`onLoad`, `onShow`, `onReady`)与 Vue 组件(`created`, `mounted`)有重叠,必须规范使用:
+### 3.1 路由配置(pages.json)
-### 3.1 生命周期使用规范
+```json
+{
+ "pages": [
+ {
+ "path": "pages/index/index",
+ "type": "home",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/scan/index",
+ "style": { "navigationBarTitleText": "扫码巡检" }
+ }
+ ],
+ "subPackages": [
+ {
+ "root": "pages-ops",
+ "pages": [
+ { "path": "work-order/index", "style": { "navigationStyle": "custom" } },
+ { "path": "work-order/detail", "style": { "navigationStyle": "custom" } },
+ { "path": "inspection/index", "style": { "navigationStyle": "custom" } },
+ { "path": "inspection/detail", "style": { "navigationStyle": "custom" } }
+ ]
+ }
+ ],
+ "tabBar": {
+ "custom": true,
+ "color": "#999999",
+ "selectedColor": "#f97316",
+ "list": [
+ { "text": "工作台", "pagePath": "pages/index/index" },
+ { "text": "我的", "pagePath": "pages/user/index" }
+ ]
+ }
+}
+```
+
+### 3.2 页面跳转方法
+
+```typescript
+// 基础导航
+import { navigateBackPlus } from '@/utils'
+
+// 跳转到工单详情(带参数)
+function goToWorkOrderDetail(orderId: number) {
+ uni.navigateTo({
+ url: `/pages-ops/work-order/detail?id=${orderId}`
+ })
+}
+
+// 返回上一页(封装版,支持指定 fallback)
+navigateBackPlus('/pages-ops/work-order/index')
+
+// 重定向(关闭当前页面)
+uni.redirectTo({ url: '/pages/index/index' })
+
+// 切换到 TabBar 页面
+uni.switchTab({ url: '/pages/index/index' })
+```
+
+### 3.3 页面参数接收
```vue
+```
+
+---
+
+## 四、页面状态与生命周期规约
+
+移动端页面的生命周期(`onLoad`, `onShow`, `onReady`)与 Vue 组件(`created`, `mounted`)有重叠,必须规范使用:
+
+### 4.1 生命周期使用规范
+
+```vue
+
```
-### 3.2 内存泄漏防护
+### 4.2 内存泄漏防护
| 资源类型 | 注册位置 | 必须清理位置 | 清理方式 |
|---------|---------|-------------|---------|
| WebSocket | `onLoad` / `onShow` | `onUnload` / `onHide` | `ws.close()` |
| EventBus 监听 | `onLoad` | `onUnload` | `uni.$off()` |
-| 蓝牙连接 | 用户操作后 | `onUnload` | `BleManager.disconnect()` |
+| 蓝牙连接 | 用户操作后 | `onUnload` | `uni.closeBLEConnection()` |
| 定时器 | 任意 | `onUnload` | `clearInterval()` |
| 页面级 Store | `onLoad` | `onUnload` | `store.$reset()` |
---
-## 四、状态管理设计
+## 五、状态管理设计
-### 4.1 分层状态管理
+### 5.1 分层状态管理
```
全局状态 (Pinia Store)
-├── userStore # 用户信息、登录态
-├── configStore # 系统配置、字典缓存
-└── bleStore # 蓝牙连接状态、当前工牌
+├── token.ts # Token 管理(双 Token 无感刷新)
+├── user.ts # 用户信息、登录态
+├── dict.ts # 系统字典缓存
+└── theme.ts # 主题配置
页面级状态 (Composable / setup)
├── useWorkOrder() # 单个工单页面的状态
├── useInspection() # 巡检页面的状态
-└── useUpload() # 图片上传的状态
+└── useBluetoothScan() # 蓝牙扫描状态
```
-### 4.2 Store 示例
+### 5.2 Token 管理(双 Token 无感刷新)
```typescript
-// stores/ble.ts
-import { defineStore } from 'pinia';
-import { ref, computed } from 'vue';
-import BleManager from '@/services/ble/BleManager';
+// store/token.ts
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
-export const useBleStore = defineStore('ble', () => {
- // State
- const connectedDevice = ref(null);
- const connectionState = ref<'disconnected' | 'connecting' | 'connected'>('disconnected');
- const lastHeartbeat = ref(0);
+interface TokenInfo {
+ accessToken: string
+ refreshToken: string
+ expiresTime: number
+}
- // Getters
- const isConnected = computed(() => connectionState.value === 'connected');
- const isDeviceOnline = computed(() => {
- if (!lastHeartbeat.value) return false;
- return Date.now() - lastHeartbeat.value < 5 * 60 * 1000; // 5分钟内有心跳
- });
-
- // Actions
- async function connect(deviceId: string) {
- connectionState.value = 'connecting';
- try {
- const success = await BleManager.connect(deviceId);
- if (success) {
- connectionState.value = 'connected';
- // 监听心跳
- BleManager.on('heartbeat', (data) => {
- lastHeartbeat.value = data.timestamp;
- });
- } else {
- connectionState.value = 'disconnected';
- }
- } catch (error) {
- connectionState.value = 'disconnected';
- throw error;
- }
+export const useTokenStore = defineStore('token', () => {
+ const tokenInfo = ref(null)
+
+ async function refreshToken() {
+ // 调用刷新接口,更新 accessToken
+ const res = await http.post('/system/auth/refresh-token', {
+ refreshToken: tokenInfo.value?.refreshToken
+ })
+ tokenInfo.value = res
}
-
- function disconnect() {
- BleManager.disconnect();
- connectedDevice.value = null;
- connectionState.value = 'disconnected';
+
+ function logout() {
+ tokenInfo.value = null
+ uni.removeStorageSync('token_info')
}
-
- return {
- connectedDevice,
- connectionState,
- lastHeartbeat,
- isConnected,
- isDeviceOnline,
- connect,
- disconnect
- };
-});
+
+ return { tokenInfo, refreshToken, logout }
+})
```
---
-## 五、关键实现要点
+## 六、关键实现要点
-### 5.1 工程结构红线
+### 6.1 工程结构红线
| 红线项 | 说明 | 后果 |
|-------|------|------|
@@ -336,22 +440,23 @@ export const useBleStore = defineStore('ble', () => {
| **必须预加载** | 首页应预加载高频分包 | 用户点击后白屏时间过长 |
| **清理必须配对** | 有注册必须有卸载 | 内存泄漏、页面异常 |
-### 5.2 目录命名规范
+### 6.2 目录命名规范
| 类型 | 命名规范 | 示例 |
|-----|---------|------|
-| 页面目录 | kebab-case | `work-order/`, `inspection-scan/` |
-| 组件文件 | kebab-case | `app-badge.vue`, `app-upload.vue` |
-| 服务文件 | PascalCase | `BleManager.ts`, `WorkOrderService.ts` |
-| 工具文件 | camelCase | `request.ts`, `helpers.ts` |
-| 常量文件 | UPPER_SNAKE | `constants.ts` (内部常量) |
+| 页面目录 | kebab-case | `work-order/`, `inspection/` |
+| 组件文件 | kebab-case | `bluetooth-verify.vue` |
+| 服务文件 | PascalCase | `BleManager.ts` |
+| 工具文件 | camelCase | `use-bluetooth-scan.ts` |
+| API 文件 | camelCase | `order-center/index.ts` |
-### 5.3 相关代码入口
+### 6.3 相关代码入口
| 模块 | 文件路径 | 说明 |
|-----|---------|------|
| 页面路由 | `src/pages.json` | 主包 + 分包配置 |
| 应用配置 | `src/manifest.json` | 小程序 AppID、权限声明 |
-| 请求封装 | `src/utils/request.ts` | 拦截器、错误处理 |
-| 蓝牙服务 | `src/services/ble/BleManager.ts` | BLE 连接管理 |
-| 全局组件 | `src/components/` | 复用 UI 组件 |
+| 请求封装 | `src/http/http.ts` | 拦截器、Token 刷新、错误处理 |
+| 蓝牙扫描 | `src/pages-ops/inspection/composables/use-bluetooth-scan.ts` | iBeacon 扫描 Hook |
+| 字典常量 | `src/utils/constants/dict-enum.ts` | DICT_TYPE 定义 |
+| 状态码 | `src/http/tools/enum.ts` | ResultEnum 定义 |
\ No newline at end of file
diff --git a/开发者文档/05-移动端开发/02-硬件交互与弱网离线策略.md b/开发者文档/05-移动端开发/02-硬件交互与弱网离线策略.md
index a2e57f0..7fa84ee 100644
--- a/开发者文档/05-移动端开发/02-硬件交互与弱网离线策略.md
+++ b/开发者文档/05-移动端开发/02-硬件交互与弱网离线策略.md
@@ -7,759 +7,455 @@ AIOT 移动端的核心价值在于与**智能工牌(BADGE)**和**蓝牙信
## 一、智能工牌(BADGE)蓝牙通信协议
-### 1.1 工牌设备模型 (`IotBadgeDevice`)
+### 1.1 工牌设备模型
```typescript
-// 工牌设备核心字段(对应后端 IotBadgeDeviceDO)
-interface IotBadgeDevice {
- deviceId: string; // 设备唯一ID(如 BD-2024-001)
- deviceName: string; // 设备名称
- deviceType: 'BADGE'; // 固定类型
- userId?: string; // 绑定用户ID
- areaId?: string; // 所属区域ID
- batteryLevel: number; // 电量百分比(0-100)
- status: BadgeStatusEnum; // 设备状态
- lastHeartbeat: number; // 最后心跳时间戳
- firmwareVersion: string; // 固件版本
+// api/ops/cleaning/index.ts
+
+/** 工牌状态枚举 */
+export enum BadgeStatus {
+ IDLE = 'IDLE', // 空闲
+ BUSY = 'BUSY', // 忙碌
+ OFFLINE = 'OFFLINE', // 离线
+ PAUSED = 'PAUSED', // 暂停中
}
-enum BadgeStatusEnum {
- IDLE = 'IDLE', // 空闲,可接受新任务
- BUSY = 'BUSY', // 忙碌,正在处理任务
- OFFLINE = 'OFFLINE', // 离线(超过5分钟无心跳)
- ERROR = 'ERROR' // 故障状态
+/** 通知类型枚举 */
+export enum NotifyType {
+ VIBRATE = 'VIBRATE', // 震动
+ VOICE = 'VOICE', // 语音
+}
+
+/** 工牌状态信息 */
+export interface BadgeStatusItem {
+ deviceId: number
+ deviceKey: string
+ status: BadgeStatus
+ currentAreaId?: number
+ currentAreaName?: string
+ batteryLevel: number // 0-100
+ lastHeartbeatTime: string
+ todayCompletedCount?: number
+ todayWorkMinutes?: number
+}
+
+/** 工牌实时状态 */
+export interface BadgeRealtimeStatus {
+ deviceId: number
+ deviceKey: string
+ status: BadgeStatus | string
+ batteryLevel: null | number
+ lastHeartbeatTime: string
+ rssi?: null | number
+ isInArea: boolean
+ areaId?: number
+ areaName?: null | string
}
```
### 1.2 蓝牙通信架构
-移动端与工牌的通信采用 **BLE(Bluetooth Low Energy)4.0/5.0** 协议栈:
-
```
┌─────────────────────────────────────────────────────────────┐
│ 移动端 (UniApp) │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 业务层 │ │ 蓝牙服务层 │ │ 设备管理层 │ │
-│ │ BadgeService │ │ BleManager │ │ DeviceManager │ │
+│ │ useBluetooth │ │ uni API │ │ BadgeService │ │
+│ │ Scan Hook │ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ┌──────▼─────────────────▼───────────────────▼─────────┐ │
│ │ UniApp 蓝牙 API 层 │ │
│ │ uni.openBluetoothAdapter │ │
-│ │ uni.startBluetoothDevicesDiscovery │ │
-│ │ uni.createBLEConnection │ │
-│ │ uni.writeBLECharacteristicValue │ │
-│ │ uni.onBLECharacteristicValueChange │ │
+│ │ uni.startBeaconDiscovery │ │
+│ │ uni.onBeaconUpdate │ │
+│ │ uni.stopBeaconDiscovery │ │
│ └─────────────────────────┬─────────────────────────────┘ │
└────────────────────────────┼───────────────────────────────┘
- │ BLE 广播/连接
+ │ BLE 广播
┌────────────────────────────┼───────────────────────────────┐
-│ 智能工牌硬件 │
+│ 蓝牙信标硬件 │
│ ┌─────────────────────────▼─────────────────────────────┐ │
-│ │ BLE 芯片 (Nordic nRF52 / TI CC2640) │ │
-│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ │
-│ │ │ GATT Server │ │ 按键中断 │ │ 震动马达 │ │ │
-│ │ │ 服务 0xFFF0 │ │ GPIO 中断 │ │ PWM 驱动 │ │ │
-│ │ └─────────────┘ └─────────────┘ └──────────────┘ │ │
+│ │ iBeacon 协议 (Apple) │ │
+│ │ UUID + Major + Minor + RSSI │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
-### 1.3 BLE 服务与特征值定义
+### 1.3 iBeacon 协议格式
-工牌设备暴露的标准 GATT 服务:
+巡检系统使用标准 iBeacon 协议进行定位验证:
-| 服务 UUID | 特征值 UUID | 属性 | 说明 |
-|-----------|-------------|------|------|
-| `0xFFF0` (主服务) | `0xFFF1` | Read/Notify | 设备信息(电量、固件版本) |
-| `0xFFF0` (主服务) | `0xFFF2` | Write | 指令下发(震动、LED 控制) |
-| `0xFFF0` (主服务) | `0xFFF3` | Notify | 按键事件上报 |
-| `0xFFF0` (主服务) | `0xFFF4` | Read/Notify | 信标扫描结果 |
-
-### 1.4 指令协议格式
-
-移动端向工牌下发的指令采用固定长度的二进制帧:
-
-```
-帧结构(16字节):
-┌────────┬────────┬────────┬────────────────┬────────────────┐
-│ 帧头 │ 指令码 │ 参数 │ 工单ID(低8B) │ 校验和 │
-│ 0xAA │ 1字节 │ 1字节 │ 8字节 │ 2字节(CRC16) │
-│ 1字节 │ │ │ │ │
-└────────┴────────┴────────┴────────────────┴────────────────┘
-
-指令码定义:
-0x01 - 震动提醒(参数=震动时长,单位100ms)
-0x02 - LED 闪烁(参数=闪烁次数)
-0x03 - 启动信标扫描(参数=扫描时长,单位秒)
-0x04 - 清除当前任务
-0x05 - 查询设备状态
-```
-
-### 1.5 工牌事件上报格式
-
-工牌向移动端上报的事件:
+| 字段 | 长度 | 说明 |
+|-----|------|------|
+| UUID | 16字节 | 信标唯一标识(区域级别) |
+| Major | 2字节 | 主标识(区域ID) |
+| Minor | 2字节 | 次标识(信标ID) |
+| RSSI | 1字节 | 信号强度(dBm) |
```typescript
-// 按键事件(特征值 0xFFF3 Notify)
-interface BadgeKeyEvent {
- eventType: 'KEY_PRESS' | 'KEY_LONG_PRESS';
- keyCode: number; // 按键编号(工牌通常只有1个确认键)
- timestamp: number; // 工牌本地时间戳
- taskId?: string; // 关联的工单ID(如果有)
+// api/ops/inspection/index.ts
+
+/** 绑定的信标 */
+export interface InspectionBoundBeaconVO {
+ major: number
+ minor: number
}
-// 信标扫描结果(特征值 0xFFF4 Notify)
-interface BeaconScanResult {
- scanId: string; // 扫描会话ID
- beacons: DetectedBeacon[]; // 检测到的信标列表
- scanDuration: number; // 实际扫描时长(ms)
- timestamp: number; // 扫描完成时间戳
-}
-
-interface DetectedBeacon {
- macAddress: string; // 信标MAC地址(大写无冒号)
- rssi: number; // 信号强度(dBm)
- uuid?: string; // iBeacon UUID(如果是iBeacon格式)
- major?: number; // iBeacon Major
- minor?: number; // iBeacon Minor
+/** 信标配置 */
+export interface InspectionBeaconConfigVO {
+ uuid: string
+ boundBeacons: InspectionBoundBeaconVO[]
+ rssiThreshold: number // 信号强度阈值(如 -75dBm)
+ timeoutMs: number // 扫描超时时间(毫秒)
}
```
---
-## 二、移动端蓝牙服务实现 (`BleManager`)
+## 二、蓝牙信标扫描实现(useBluetoothScan)
-### 2.1 服务初始化与适配器管理
+### 2.1 扫描 Hook 核心实现
```typescript
-// services/ble/BleManager.ts
-class BleManager {
- private static instance: BleManager;
- private adapterState: 'uninitialized' | 'ready' | 'scanning' | 'error' = 'uninitialized';
- private connectedDevice: BLEDevice | null = null;
- private eventCallbacks: Map = new Map();
+// pages-ops/inspection/composables/use-bluetooth-scan.ts
- static getInstance(): BleManager {
- if (!BleManager.instance) {
- BleManager.instance = new BleManager();
- }
- return BleManager.instance;
+export type ScanStatus = 'idle' | 'scanning' | 'success' | 'failed' | 'no-permission'
+
+/** 匹配到的信标信息 */
+export interface MatchedBeacon {
+ major: number
+ minor: number
+ rssi: number
+}
+
+export interface UseBluetoothScanOptions {
+ /** 信标配置(来自 list-by-area) */
+ beaconConfig: InspectionBeaconConfigVO
+}
+
+export function useBluetoothScan(options: UseBluetoothScanOptions): UseBluetoothScanReturn {
+ const { beaconConfig } = options
+ const { uuid, boundBeacons, rssiThreshold, timeoutMs } = beaconConfig
+
+ const status = ref('idle')
+ const progress = ref(0)
+ const remainingSeconds = ref(Math.ceil(timeoutMs / 1000))
+ const matchedBeacons = ref([])
+ const errorMessage = ref('')
+
+ let progressTimer: ReturnType | null = null
+ let scanTimeout: ReturnType | null = null
+ let isScanning = false
+
+ /** 是否为微信小程序环境 */
+ function isWxMp(): boolean {
+ // #ifdef MP-WEIXIN
+ return true
+ // #endif
+ return false
}
- /**
- * 初始化蓝牙适配器
- * 必须在任何蓝牙操作前调用
+ /** 本地比对:检查扫描到的信标是否匹配 boundBeacons 中任一项
+ * 条件:major 相同 && minor 相同 && rssi >= rssiThreshold
*/
- async init(): Promise {
- try {
- const [err] = await uni.openBluetoothAdapter();
- if (err) {
- if (err.errCode === 10001) {
- throw new Error('蓝牙适配器不可用,请检查设备蓝牙是否开启');
- }
- throw err;
+ function matchBeacons(
+ scannedBeacons: Array<{ major: number, minor: number, rssi: number }>
+ ): MatchedBeacon[] {
+ const matched: MatchedBeacon[] = []
+ for (const scanned of scannedBeacons) {
+ const isBound = boundBeacons.some(
+ bound => bound.major === scanned.major && bound.minor === scanned.minor,
+ )
+ if (isBound && scanned.rssi >= rssiThreshold) {
+ matched.push({ major: scanned.major, minor: scanned.minor, rssi: scanned.rssi })
}
+ }
+ return matched
+ }
- // 监听适配器状态变化
- uni.onBluetoothAdapterStateChange((res) => {
- if (!res.available) {
- this.adapterState = 'error';
- this.emit('adapterError', '蓝牙适配器已关闭');
- }
- });
+ /** 小程序真实扫描 */
+ async function startRealScan() {
+ isScanning = true
+ status.value = 'scanning'
+ progress.value = 0
+ errorMessage.value = ''
- this.adapterState = 'ready';
- return true;
- } catch (error) {
- this.adapterState = 'error';
- console.error('[BleManager] 初始化失败:', error);
- return false;
+ // 检查蓝牙权限
+ try {
+ const setting = await new Promise((resolve, reject) => {
+ uni.getSetting({ success: resolve, fail: reject })
+ })
+
+ const btAuth = setting.authSetting['scope.bluetooth']
+ if (btAuth === false) {
+ status.value = 'no-permission'
+ errorMessage.value = '蓝牙权限已被拒绝,请在设置中开启'
+ return
+ }
+ if (btAuth === undefined) {
+ await new Promise((resolve, reject) => {
+ uni.authorize({
+ scope: 'scope.bluetooth',
+ success: () => resolve(),
+ fail: () => reject(new Error('蓝牙权限被拒绝')),
+ })
+ })
+ }
+ } catch {
+ status.value = 'no-permission'
+ errorMessage.value = '需要蓝牙权限才能进行定位验证'
+ return
+ }
+
+ // 监听 Beacon 更新
+ uni.onBeaconUpdate(handleBeaconUpdate as any)
+
+ // 启动 Beacon 发现
+ try {
+ await new Promise((resolve, reject) => {
+ uni.startBeaconDiscovery({
+ uuids: [uuid],
+ success: () => resolve(),
+ fail: (err: any) => reject(new Error(err.errMsg || '启动蓝牙扫描失败')),
+ })
+ })
+ } catch (err: any) {
+ isScanning = false
+ errorMessage.value = err.message || '启动蓝牙扫描失败'
+ status.value = 'failed'
+ return
+ }
+
+ // 启动进度条
+ startProgress()
+
+ // 超时处理
+ scanTimeout = setTimeout(() => {
+ onScanFailed('定位超时,未能验证位置')
+ }, timeoutMs)
+ }
+
+ /** 处理 Beacon 更新(仅小程序) */
+ function handleBeaconUpdate(
+ res: { beacons: Array<{ uuid: string, major: number, minor: number, rssi: number }> }
+ ) {
+ if (!isScanning) return
+
+ const matched = matchBeacons(res.beacons)
+ if (matched.length > 0) {
+ onScanSuccess(matched)
}
}
- /**
- * 扫描附近的工牌设备
- * @param timeout 扫描超时时间(ms),默认10秒
- * @param filterName 设备名称过滤(可选)
- */
- async scanDevices(timeout: number = 10000, filterName?: string): Promise {
- if (this.adapterState !== 'ready') {
- throw new Error('蓝牙适配器未就绪');
- }
-
- const devices: BLEDevice[] = [];
- const discoveredMacs = new Set();
-
- return new Promise((resolve, reject) => {
- // 开始扫描
- uni.startBluetoothDevicesDiscovery({
- allowDuplicatesKey: false,
- interval: 500,
- success: () => {
- this.adapterState = 'scanning';
- },
- fail: reject
- });
-
- // 监听发现设备
- uni.onBluetoothDeviceFound((res) => {
- res.devices.forEach(device => {
- // 过滤重复设备
- if (discoveredMacs.has(device.deviceId)) return;
-
- // 名称过滤
- if (filterName && !device.name?.includes(filterName)) return;
-
- // 只收集信号强度足够的设备(-85dBm以内)
- if (device.RSSI && device.RSSI > -85) {
- discoveredMacs.add(device.deviceId);
- devices.push({
- deviceId: device.deviceId,
- name: device.name || 'Unknown',
- rssi: device.RSSI,
- advertisData: device.advertisData
- });
- }
- });
- });
-
- // 超时后停止扫描并返回结果
- setTimeout(() => {
- uni.stopBluetoothDevicesDiscovery();
- this.adapterState = 'ready';
- resolve(devices);
- }, timeout);
- });
+ return {
+ status,
+ progress,
+ remainingSeconds,
+ matchedBeacons,
+ errorMessage,
+ startScan: isWxMp() ? startRealScan : startMockScan,
+ stopScan,
+ cleanup,
}
}
```
-### 2.2 设备连接与 GATT 通信
+### 2.2 蓝牙验证组件
-```typescript
-// services/ble/BleManager.ts (续)
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-class BleManager {
- /**
- * 连接指定工牌设备并发现服务
- */
- async connect(deviceId: string): Promise {
- try {
- // 1. 建立 BLE 连接
- await uni.createBLEConnection({ deviceId });
-
- // 2. 等待连接稳定
- await this.delay(500);
-
- // 3. 获取服务列表
- const services = await uni.getBLEDeviceServices({ deviceId });
- const targetService = services.services.find(
- s => s.uuid.toUpperCase().includes('FFF0')
- );
-
- if (!targetService) {
- throw new Error('未找到工牌服务(FFF0)');
- }
+
+ {{ status === 'success' ? '定位成功' : '定位验证中...' }}
+
- // 4. 获取特征值
- const characteristics = await uni.getBLEDeviceCharacteristics({
- deviceId,
- serviceId: targetService.uuid
- });
+
+
+
+
+
+
+ {{ displayProgress }}%
+
+
+
- // 5. 启用 Notify(按键事件、信标扫描结果)
- const notifyChars = ['FFF3', 'FFF4'];
- for (const charUuid of notifyChars) {
- const char = characteristics.characteristics.find(
- c => c.uuid.toUpperCase().includes(charUuid)
- );
- if (char?.properties.notify) {
- await uni.notifyBLECharacteristicValueChange({
- deviceId,
- serviceId: targetService.uuid,
- characteristicId: char.uuid,
- state: true
- });
- }
- }
+
+
+
+
+
+
+ {{ status === 'no-permission' ? '缺少蓝牙权限' : '定位失败' }}
+
+ {{ errorMessage }}
- // 6. 监听特征值变化
- uni.onBLECharacteristicValueChange((res) => {
- this.handleCharacteristicChange(res);
- });
+
+
+
+ {{ status === 'no-permission' ? '打开设置' : '重新定位' }}
+
+
+
+ 跳过验证
+ (标记位置异常)
+
+
+
+
+
- this.connectedDevice = {
- deviceId,
- serviceId: targetService.uuid,
- characteristics: characteristics.characteristics
- };
+
```
---
-## 三、信标感应与到岗打卡机制
+## 三、信标感应与到岗打卡流程
-### 3.1 打卡流程时序
+### 3.1 巡检打卡流程时序
```mermaid
sequenceDiagram
participant M as 移动端(UniApp)
- participant B as 智能工牌(BADGE)
- participant BE as 蓝牙信标(BEACON)
participant API as 后端API
+ participant BLE as 蓝牙信标
- Note over M,API: 工单状态: CONFIRMED(已确认)
+ Note over M,API: 巡检前必须验证位置
- M->>API: 启动打卡流程
POST /ops/work-order/{id}/arrival-check
- API-->>M: 返回该区域绑定的信标MAC列表
+ M->>API: GET /ops/inspection/template/list-by-area
{areaId}
+ API-->>M: 返回信标配置 {uuid, boundBeacons, rssiThreshold, timeoutMs}
- M->>B: 发送指令 0x03
启动信标扫描(30秒)
- activate B
+ M->>BLE: 启动 iBeacon 扫描
+ activate BLE
- B->>BE: 扫描周围BLE广播
- BE-->>B: 广播帧(MAC + RSSI)
+ BLE-->>M: 广播帧 {uuid, major, minor, rssi}
- B->>M: Notify 扫描结果(特征值 FFF4)
- M->>M: 校验MAC地址白名单
计算信号强度阈值
+ M->>M: 本地比对 major/minor
校验 rssi >= rssiThreshold
alt 检测到有效信标
- M->>API: 提交打卡
POST /ops/work-order/{id}/arrival
{beaconMac, rssi, timestamp}
- API->>API: 校验信标归属
校验地理围栏
- API-->>M: 状态流转: ARRIVED
- M->>B: 发送指令 0x02
LED闪烁2次(成功提示)
+ M->>M: 状态转为 success
+ M->>API: POST /ops/inspection/submit
{isLocationException: 0}
+ API-->>M: 巡检记录创建成功
else 未检测到有效信标
- M->>M: 提示"未在指定区域"
- M->>B: 发送指令 0x01
震动3次(失败提示)
+ M->>M: 超时后状态转为 failed
+ M->>API: POST /ops/inspection/submit
{isLocationException: 1}
+ API-->>M: 记录异常位置巡检
end
- deactivate B
+ deactivate BLE
```
-### 3.2 打卡校验逻辑 (`verifyLocation`)
+### 3.2 巡检提交接口
```typescript
-// services/ops/WorkOrderService.ts
+// api/ops/inspection/index.ts
-class WorkOrderService {
- /**
- * 执行到岗打卡
- * 核心约束:必须检测到指定区域的蓝牙信标
- */
- async checkArrival(workOrderId: string): Promise {
- try {
- // 1. 获取工单详情和区域信标列表
- const [orderDetail, beaconList] = await Promise.all([
- this.getWorkOrderDetail(workOrderId),
- this.getAreaBeacons(workOrderId)
- ]);
-
- // 2. 前置校验
- if (orderDetail.status !== 'CONFIRMED') {
- throw new Error(`当前状态 ${orderDetail.status} 不允许打卡`);
- }
-
- if (!BleManager.isConnected()) {
- throw new Error('工牌未连接,请先连接设备');
- }
-
- // 3. 启动信标扫描
- const scanResult = await this.scanBeaconsWithTimeout(30000);
-
- // 4. 匹配信标白名单
- const matchedBeacon = this.matchBeacon(scanResult, beaconList);
-
- if (!matchedBeacon) {
- return {
- success: false,
- code: 'BEACON_NOT_FOUND',
- message: '未检测到指定区域的信标设备,请确认已到达现场'
- };
- }
-
- // 5. 信号强度校验(防止远距离作弊)
- if (matchedBeacon.rssi < -75) {
- return {
- success: false,
- code: 'SIGNAL_TOO_WEAK',
- message: '信号强度不足,请靠近信标设备后重试',
- data: { rssi: matchedBeacon.rssi }
- };
- }
-
- // 6. 提交后端校验
- const result = await http.post(`/ops/work-order/${workOrderId}/arrival`, {
- beaconMac: matchedBeacon.macAddress,
- rssi: matchedBeacon.rssi,
- scanTime: scanResult.timestamp,
- detectedBeacons: scanResult.beacons.length
- });
-
- // 7. 成功反馈
- if (result.success) {
- // 工牌 LED 闪烁提示
- await BleManager.sendCommand(0x02, 2);
-
- // 本地状态更新
- this.updateLocalOrderStatus(workOrderId, 'ARRIVED');
- }
-
- return result;
- } catch (error) {
- console.error('[WorkOrderService] 打卡失败:', error);
- throw error;
- }
- }
-
- /**
- * 扫描信标(带超时控制)
- */
- private async scanBeaconsWithTimeout(timeout: number): Promise {
- return new Promise((resolve, reject) => {
- const timer = setTimeout(() => {
- reject(new Error('信标扫描超时'));
- }, timeout);
-
- const onScanResult = (data: BeaconScanResult) => {
- clearTimeout(timer);
- BleManager.off('beaconScan', onScanResult);
- resolve(data);
- };
-
- BleManager.on('beaconScan', onScanResult);
-
- // 触发工牌开始扫描
- BleManager.sendCommand(0x03, Math.floor(timeout / 1000));
- });
- }
-
- /**
- * 匹配信标白名单
- */
- private matchBeacon(
- scanResult: BeaconScanResult,
- whitelist: AreaBeacon[]
- ): DetectedBeacon | null {
- const whitelistMacs = whitelist.map(b =>
- b.macAddress.replace(/:/g, '').toUpperCase()
- );
-
- // 按信号强度排序,优先匹配信号最强的
- const sortedBeacons = scanResult.beacons
- .filter(b => whitelistMacs.includes(b.macAddress.toUpperCase()))
- .sort((a, b) => b.rssi - a.rssi);
-
- return sortedBeacons[0] || null;
- }
+/** 提交巡检请求 */
+export interface InspectionSubmitReqVO {
+ areaId: number
+ /** 0=正常 1=异常 */
+ isLocationException: number
+ remark: string
+ tags?: string[]
+ photos?: string[]
+ items: {
+ templateId: number
+ isPassed: boolean
+ remark: string
+ tags?: string[]
+ }[]
}
-export default new WorkOrderService();
+/** 提交巡检结果 */
+export function submitInspection(data: InspectionSubmitReqVO) {
+ return http.post('/ops/inspection/submit', data)
+}
```
---
## 四、弱网与离线策略
-### 4.1 离线操作队列 (`OfflineActionQueue`)
-
-```typescript
-// services/offline/OfflineActionQueue.ts
-
-/**
- * 离线操作队列
- * 用于弱网环境下缓存非关键操作,待网络恢复后异步上报
- *
- * ⚠️ 注意:工单状态扭转(接单、完工)不支持离线,必须强联网
- */
-
-interface OfflineAction {
- id: string;
- type: 'INSPECTION_RECORD' | 'LOCATION_HEARTBEAT' | 'MATERIAL_CONSUME';
- payload: any;
- timestamp: number;
- retryCount: number;
- maxRetries: number;
-}
-
-class OfflineActionQueue {
- private readonly STORAGE_KEY = 'aiot_offline_action_queue';
- private isSyncing = false;
- private syncInterval: number | null = null;
-
- /**
- * 初始化队列监听
- * 在 App 启动时调用
- */
- init() {
- // 监听网络状态变化
- uni.onNetworkStatusChange((res) => {
- if (res.isConnected) {
- this.flushQueue();
- }
- });
-
- // 定期尝试同步(每30秒)
- this.syncInterval = setInterval(() => {
- this.flushQueue();
- }, 30000);
- }
-
- /**
- * 添加离线操作
- */
- async enqueue(action: Omit): Promise {
- const queue = this.getQueue();
- const newAction: OfflineAction = {
- ...action,
- id: this.generateId(),
- timestamp: Date.now(),
- retryCount: 0
- };
-
- queue.push(newAction);
- await this.saveQueue(queue);
-
- console.log(`[OfflineQueue] 操作入队: ${action.type}, 队列长度: ${queue.length}`);
- }
-
- /**
- * 刷新队列(网络恢复时调用)
- */
- async flushQueue(): Promise {
- if (this.isSyncing) return;
-
- const queue = this.getQueue();
- if (queue.length === 0) return;
-
- // 检查网络状态
- const [networkErr, networkRes] = await uni.getNetworkType();
- if (networkErr || networkRes.networkType === 'none') return;
-
- this.isSyncing = true;
- const failedActions: OfflineAction[] = [];
-
- for (const action of queue) {
- try {
- await this.executeAction(action);
- console.log(`[OfflineQueue] 操作同步成功: ${action.id}`);
- } catch (error) {
- action.retryCount++;
- if (action.retryCount < action.maxRetries) {
- failedActions.push(action);
- } else {
- console.error(`[OfflineQueue] 操作重试耗尽: ${action.id}`, error);
- // 可在此触发告警或记录到日志服务
- }
- }
- }
-
- await this.saveQueue(failedActions);
- this.isSyncing = false;
- }
-
- /**
- * 执行单个操作
- */
- private async executeAction(action: OfflineAction): Promise {
- switch (action.type) {
- case 'INSPECTION_RECORD':
- await http.post('/ops/inspection/record', action.payload);
- break;
- case 'LOCATION_HEARTBEAT':
- await http.post('/ops/location/heartbeat', action.payload);
- break;
- case 'MATERIAL_CONSUME':
- await http.post('/inventory/consume', action.payload);
- break;
- default:
- throw new Error(`未知操作类型: ${action.type}`);
- }
- }
-
- private getQueue(): OfflineAction[] {
- try {
- const data = uni.getStorageSync(this.STORAGE_KEY);
- return data ? JSON.parse(data) : [];
- } catch {
- return [];
- }
- }
-
- private async saveQueue(queue: OfflineAction[]): Promise {
- uni.setStorageSync(this.STORAGE_KEY, JSON.stringify(queue));
- }
-
- private generateId(): string {
- return `off_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- }
-}
-
-export default new OfflineActionQueue();
-```
-
-### 4.2 离线策略决策矩阵
+### 4.1 离线策略决策矩阵
| 操作类型 | 是否支持离线 | 策略 | 原因 |
|---------|-------------|------|------|
| **工单接单** | ❌ 否 | 强联网 + 分布式锁 | 涉及并发抢单,必须实时校验 |
| **工单完工** | ❌ 否 | 强联网 + 照片直传 | 需要实时校验定位、照片完整性 |
-| **巡检记录** | ✅ 是 | 离线队列缓存 | 非关键状态扭转,可延迟上报 |
-| **位置心跳** | ✅ 是 | 离线队列 + 采样降频 | 允许部分丢失,网络恢复后批量上报 |
-| **耗材登记** | ✅ 是 | 离线队列缓存 | 库存扣减可延迟,但需在完工前同步完成 |
-| **信标打卡** | ❌ 否 | 强联网 | 必须实时校验信标归属和地理围栏 |
+| **巡检记录** | ✅ 是 | 本地缓存后上报 | 非关键状态扭转,可延迟上报 |
+| **位置心跳** | ✅ 是 | 采样降频 + 批量上报 | 允许部分丢失 |
+| **图片上传** | ⚠️ 弱网优化 | 压缩 + 分片 + 重试 | 弱网时降低质量 |
-### 4.3 弱网图片上传策略
+### 4.2 弱网图片上传策略
```typescript
// services/upload/ImageUploadService.ts
@@ -774,44 +470,41 @@ class ImageUploadService {
* 3. 降级:弱网时降低图片质量
*/
async captureAndUpload(options: UploadOptions): Promise {
- // 1. 拍照(强制使用相机,禁止相册选择)
+ // 1. 拍照(强制使用相机)
const [err, res] = await uni.chooseImage({
sourceType: ['camera'],
- sizeType: ['compressed'], // 先使用压缩
+ sizeType: ['compressed'],
count: 1
- });
+ })
- if (err) throw new Error('拍照失败');
+ if (err) throw new Error('拍照失败')
- const tempFilePath = res.tempFilePaths[0];
+ const tempFilePath = res.tempFilePaths[0]
// 2. 获取网络类型,决定压缩策略
- const [, networkRes] = await uni.getNetworkType();
- const isWeakNetwork = ['2g', '3g'].includes(networkRes.networkType);
+ const [, networkRes] = await uni.getNetworkType()
+ const isWeakNetwork = ['2g', '3g'].includes(networkRes.networkType)
// 3. 二次压缩(弱网时更激进)
const compressedPath = await this.compressImage(tempFilePath, {
quality: isWeakNetwork ? 60 : 80,
maxWidth: 1280,
maxHeight: 1280
- });
+ })
- // 4. 上传
+ // 4. 上传(弱网时增加重试次数和超时)
const fileUrl = await this.uploadWithRetry(compressedPath, {
maxRetries: isWeakNetwork ? 5 : 3,
timeout: isWeakNetwork ? 60000 : 30000
- });
+ })
// 5. 清理临时文件
- uni.removeSavedFile({ filePath: tempFilePath });
- uni.removeSavedFile({ filePath: compressedPath });
+ uni.removeSavedFile({ filePath: tempFilePath })
+ uni.removeSavedFile({ filePath: compressedPath })
- return fileUrl;
+ return fileUrl
}
- /**
- * 图片压缩
- */
private async compressImage(
src: string,
options: { quality: number; maxWidth: number; maxHeight: number }
@@ -821,157 +514,159 @@ class ImageUploadService {
quality: options.quality,
compressedWidth: options.maxWidth,
compressedHeight: options.maxHeight
- });
-
- if (err) throw err;
- return res.tempFilePath;
+ })
+ if (err) throw err
+ return res.tempFilePath
}
- /**
- * 带重试的上传
- */
private async uploadWithRetry(
filePath: string,
options: { maxRetries: number; timeout: number }
): Promise {
- let lastError: Error | null = null;
+ let lastError: Error | null = null
for (let i = 0; i < options.maxRetries; i++) {
try {
- const result = await this.doUpload(filePath, options.timeout);
- return result;
+ return await this.doUpload(filePath, options.timeout)
} catch (error) {
- lastError = error as Error;
-
+ lastError = error as Error
// 指数退避
- const delay = Math.min(1000 * Math.pow(2, i), 10000);
- await new Promise(r => setTimeout(r, delay));
+ const delay = Math.min(1000 * Math.pow(2, i), 10000)
+ await new Promise(r => setTimeout(r, delay))
}
}
- throw lastError || new Error('上传失败,已重试' + options.maxRetries + '次');
- }
-
- private async doUpload(filePath: string, timeout: number): Promise {
- return new Promise((resolve, reject) => {
- const uploadTask = uni.uploadFile({
- url: `${API_BASE}/oss/upload`,
- filePath,
- name: 'file',
- timeout,
- success: (res) => {
- if (res.statusCode === 200) {
- const data = JSON.parse(res.data);
- resolve(data.url);
- } else {
- reject(new Error(`上传失败: ${res.statusCode}`));
- }
- },
- fail: reject
- });
- });
+ throw lastError || new Error('上传失败')
}
}
+```
-export default new ImageUploadService();
+### 4.3 HTTP 请求封装(含 Token 刷新)
+
+```typescript
+// http/http.ts
+
+let refreshing = false
+let taskQueue: (() => void)[] = []
+
+export function http(options: CustomRequestOptions) {
+ return new Promise((resolve, reject) => {
+ uni.request({
+ ...options,
+ success: async (res) => {
+ let responseData = res.data as IResponse
+
+ // 解密响应数据(如启用加密)
+ const encryptHeader = ApiEncrypt.getEncryptHeader()
+ const isEncryptResponse = res.header[encryptHeader] === 'true'
+ if (isEncryptResponse && typeof responseData === 'string') {
+ responseData = ApiEncrypt.decryptResponse(responseData)
+ }
+
+ const { code } = responseData
+ const isTokenExpired = res.statusCode === 401 || code === 401
+
+ if (isTokenExpired) {
+ const tokenStore = useTokenStore()
+ if (!isDoubleTokenMode) {
+ tokenStore.logout()
+ toLoginPage()
+ return reject(res)
+ }
+
+ // 无感刷新 Token
+ const { refreshToken } = tokenStore.tokenInfo || {}
+ if (refreshToken) {
+ taskQueue.push(() => resolve(http(options)))
+ }
+
+ if (refreshToken && !refreshing) {
+ refreshing = true
+ try {
+ await tokenStore.refreshToken()
+ refreshing = false
+ taskQueue.forEach(task => task())
+ } catch (refreshErr) {
+ refreshing = false
+ tokenStore.logout()
+ toLoginPage()
+ } finally {
+ taskQueue = []
+ }
+ }
+ return reject(res)
+ }
+
+ // 成功处理
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
+ uni.showToast({
+ title: responseData.msg || '请求错误',
+ icon: 'none',
+ })
+ return reject(responseData)
+ }
+ return resolve(responseData.data)
+ }
+
+ reject(res)
+ },
+ fail(err) {
+ uni.showToast({
+ icon: 'none',
+ title: '网络错误,换个网络试试',
+ })
+ reject(err)
+ },
+ })
+ })
+}
```
---
-## 五、基础数据缓存策略 (`Stale-while-revalidate`)
+## 五、常见坑点与调试技巧
+
+### 5.1 蓝牙调试
```typescript
-// services/cache/DataCacheService.ts
+// 开启蓝牙日志(开发环境)
+uni.onBeaconUpdate((res) => {
+ console.log('[BLE] Beacon 更新:', res.beacons.map(b => ({
+ uuid: b.uuid,
+ major: b.major,
+ minor: b.minor,
+ rssi: b.rssi
+ })))
+})
-/**
- * 基础数据缓存服务
- * 采用 Stale-while-revalidate 策略:
- * 1. 优先使用本地缓存渲染界面(无等待)
- * 2. 后台静默请求最新数据
- * 3. 如有更新,替换缓存并触发界面刷新
- */
+// 监听蓝牙状态变化
+uni.onBluetoothAdapterStateChange((res) => {
+ console.log('[BLE] 适配器状态:', res.available, res.discovering)
+})
+```
-interface CacheEntry {
- data: T;
- timestamp: number;
- version: string;
-}
+### 5.2 常见问题速查
-class DataCacheService {
- private readonly DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24小时
+| 问题 | 原因 | 解决方案 |
+|-----|------|---------|
+| 扫描不到信标 | 未授权定位权限 | iOS/Android 均需定位权限才能扫描 BLE |
+| 信标扫描为空 | UUID 不匹配 | 检查 uuid 是否与 boundBeacons 对应 |
+| 定位验证失败 | RSSI 低于阈值 | 靠近信标,确保信号强度 >= rssiThreshold |
+| 图片上传超时 | 弱网 + 未压缩 | 检查压缩参数,或切换网络重试 |
+| Token 刷新失败 | refreshToken 过期 | 重新登录 |
+| 页面返回白屏 | 内存泄漏 | 检查 onUnload 是否清理资源 |
- /**
- * 获取缓存数据(带后台刷新)
- */
- async getWithRevalidate(
- key: string,
- fetcher: () => Promise<{ data: T; version: string }>,
- options: { ttl?: number; immediate?: boolean } = {}
- ): Promise {
- const ttl = options.ttl || this.DEFAULT_TTL;
- const cached = this.getFromStorage(key);
+### 5.3 调试工具
- // 如果有有效缓存,先返回缓存
- if (cached && Date.now() - cached.timestamp < ttl) {
- // 后台刷新(不阻塞)
- if (!options.immediate) {
- this.refreshInBackground(key, fetcher, cached.version);
- }
- return cached.data;
- }
+```typescript
+// pages/scan/bluetooth-debug/index.vue
+// 蓝牙调试页面(仅 MP-WEIXIN)
- // 无缓存或已过期,强制刷新
- const result = await fetcher();
- this.saveToStorage(key, result.data, result.version);
- return result.data;
- }
-
- /**
- * 后台静默刷新
- */
- private async refreshInBackground(
- key: string,
- fetcher: () => Promise<{ data: T; version: string }>,
- currentVersion: string
- ): Promise {
- try {
- const result = await fetcher();
-
- // 版本变化才更新
- if (result.version !== currentVersion) {
- this.saveToStorage(key, result.data, result.version);
-
- // 触发全局事件,通知界面刷新
- uni.$emit('cache:updated', { key, data: result.data });
-
- console.log(`[DataCache] ${key} 已更新到版本 ${result.version}`);
- }
- } catch (error) {
- console.warn(`[DataCache] ${key} 后台刷新失败:`, error);
- // 后台刷新失败不影响当前显示
- }
- }
-
- private getFromStorage(key: string): CacheEntry | null {
- try {
- const data = uni.getStorageSync(`cache_${key}`);
- return data ? JSON.parse(data) : null;
- } catch {
- return null;
- }
- }
-
- private saveToStorage(key: string, data: T, version: string): void {
- const entry: CacheEntry = {
- data,
- timestamp: Date.now(),
- version
- };
- uni.setStorageSync(`cache_${key}`, JSON.stringify(entry));
- }
-}
-
-export default new DataCacheService();
+// 功能:
+// 1. 扫描周围所有 iBeacon
+// 2. 显示 UUID/Major/Minor/RSSI
+// 3. 测试特定信标匹配逻辑
```
---
@@ -982,10 +677,10 @@ export default new DataCacheService();
| 红线项 | 说明 | 后果 |
|-------|------|------|
-| **禁止前端判定打卡成功** | 信标打卡必须由后端 `verifyLocation` 校验 | 作弊风险 |
-| **必须启用 Notify** | 按键事件、信标扫描结果通过 Notify 接收 | 事件丢失 |
-| **MTU 分包** | BLE 单次传输限制 20 字节,长帧必须分包 | 指令截断 |
-| **连接超时处理** | 工牌可能进入休眠,连接失败需引导用户唤醒 | 用户体验差 |
+| **必须申请权限** | 蓝牙 + 定位权限缺一不可 | 扫描失败 |
+| **本地比对信标** | major/minor/rssi 三重校验 | 定位作弊 |
+| **超时必处理** | 扫描超时后必须停止 discovery | 耗电/内存泄漏 |
+| **Mock 降级** | H5/App 环境使用 Mock 数据 | 开发调试 |
### 6.2 弱网策略红线
@@ -994,44 +689,15 @@ export default new DataCacheService();
| **状态扭转不强联** | 接单、完工必须实时联网 | 数据不一致 |
| **照片必须压缩** | 原图上传在弱网下几乎必然失败 | 操作卡死 |
| **按钮必须防抖** | 提交类按钮点击后立即 disabled | 重复提交 |
-| **离线队列容量限制** | 防止无限堆积导致存储溢出 | 应用崩溃 |
+| **Token 无感刷新** | 401 时自动刷新,对用户透明 | 体验好 |
### 6.3 相关代码入口
| 模块 | 文件路径 | 核心类/函数 |
|-----|---------|------------|
-| 蓝牙管理 | `services/ble/BleManager.ts` | `BleManager` |
-| 工单服务 | `services/ops/WorkOrderService.ts` | `checkArrival()` |
-| 离线队列 | `services/offline/OfflineActionQueue.ts` | `enqueue()`, `flushQueue()` |
-| 图片上传 | `services/upload/ImageUploadService.ts` | `captureAndUpload()` |
-| 数据缓存 | `services/cache/DataCacheService.ts` | `getWithRevalidate()` |
-
----
-
-## 七、调试与排障
-
-### 7.1 蓝牙调试
-
-```typescript
-// 开启蓝牙日志(开发环境)
-uni.setBLEMTU({ deviceId, mtu: 512 }); // 尝试协商更大 MTU
-
-// 监听所有蓝牙事件
-uni.onBluetoothDeviceFound((res) => {
- console.log('[BLE] 发现设备:', res.devices.map(d => ({
- name: d.name,
- deviceId: d.deviceId,
- RSSI: d.RSSI
- })));
-});
-```
-
-### 7.2 常见问题
-
-| 问题 | 原因 | 解决方案 |
-|-----|------|---------|
-| 扫描不到工牌 | 工牌休眠/电量耗尽 | 按工牌按键唤醒,或充电后重试 |
-| 连接后无响应 | 服务发现失败 | 重启蓝牙适配器,重新连接 |
-| 信标扫描为空 | 未授权定位权限 | iOS/Android 均需定位权限才能扫描 BLE |
-| 打卡提示"未在区域" | 信标 MAC 不匹配 | 检查信标是否绑定到正确区域 |
-| 图片上传超时 | 弱网 + 未压缩 | 检查压缩参数,或切换网络重试 |
+| 蓝牙扫描 Hook | `pages-ops/inspection/composables/use-bluetooth-scan.ts` | `useBluetoothScan()` |
+| 蓝牙验证组件 | `pages-ops/inspection/components/bluetooth-verify.vue` | `BluetoothVerify` |
+| 巡检 API | `api/ops/inspection/index.ts` | `getInspectionForm()`, `submitInspection()` |
+| 工牌 API | `api/ops/cleaning/index.ts` | `getBadgeStatusList()`, `sendDeviceNotify()` |
+| HTTP 封装 | `http/http.ts` | `http()`, Token 刷新逻辑 |
+| 图片上传 | `services/upload/ImageUploadService.ts` | `captureAndUpload()` |
\ No newline at end of file
diff --git a/开发者文档/08-附录/01-术语表.md b/开发者文档/08-附录/01-术语表.md
new file mode 100644
index 0000000..df79c0c
--- /dev/null
+++ b/开发者文档/08-附录/01-术语表.md
@@ -0,0 +1,200 @@
+# 01-术语表
+
+本文档汇总 AIOT 系统中使用的专业术语,按领域分类整理。
+
+---
+
+## 一、IoT 设备术语
+
+### 1.1 工牌(Badge)
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 智能工牌 | Smart Badge | 保洁/安保人员佩戴的 BLE 设备,用于接收工单通知、上报位置 |
+| 工牌状态 | Badge Status | IDLE(空闲) / BUSY(忙碌) / PAUSED(暂停) / OFFLINE(离线) |
+| 工牌通知 | Badge Notify | 向工牌发送震动/语音通知 |
+| 心跳 | Heartbeat | 工牌定期上报在线状态 |
+| 电量 | Battery Level | 工牌剩余电量百分比 (0-100) |
+| RSSI | Received Signal Strength Indicator | 信号强度指示,单位 dBm |
+
+### 1.2 信标(Beacon)
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 蓝牙信标 | Bluetooth Beacon | 固定部署的 BLE 广播设备,用于定位 |
+| iBeacon | Apple iBeacon | Apple 制定的 BLE 广播协议 |
+| UUID | Universally Unique Identifier | 信标唯一标识,16字节 |
+| Major | Major ID | 主标识,通常对应区域ID |
+| Minor | Minor ID | 次标识,通常对应信标ID |
+| 信号阈值 | RSSI Threshold | 最小可接收信号强度,如 -75dBm |
+
+### 1.3 设备状态
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 未激活 | Inactive | 设备未激活状态 |
+| 在线 | Online | 设备正常连接 |
+| 离线 | Offline | 设备断开连接 |
+
+---
+
+## 二、工单业务术语
+
+### 2.1 工单类型
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 保洁工单 | Cleaning Order | 保洁作业任务 |
+| 安保工单 | Security Order | 安保处理任务 |
+| 维修工单 | Repair Order | 设备维修任务 |
+| 客服工单 | Service Order | 客服服务任务 |
+
+### 2.2 工单状态(状态机)
+
+```mermaid
+stateDiagram-v2
+ [*] --> PENDING: 创建
+ PENDING --> QUEUED: 进入队列
+ QUEUED --> DISPATCHED: 推送到工牌
+ DISPATCHED --> CONFIRMED: 保洁员确认
+ CONFIRMED --> ARRIVED: 到达现场
+ ARRIVED --> COMPLETED: 完成作业
+ ARRIVED --> PAUSED: 暂停
+ PAUSED --> ARRIVED: 恢复
+ PENDING --> CANCELLED: 取消
+ QUEUED --> CANCELLED: 取消
+ DISPATCHED --> CANCELLED: 取消
+ CONFIRMED --> CANCELLED: 取消
+ ARRIVED --> CANCELLED: 取消
+```
+
+| 状态 | 英文 | 说明 |
+|-----|------|------|
+| 待分配 | PENDING | 工单已创建,等待分配 |
+| 排队中 | QUEUED | 已推荐保洁员,在队列中等待 |
+| 已推送 | DISPATCHED | 已推送到工牌,等待确认 |
+| 已确认 | CONFIRMED | 保洁员按下确认按钮 |
+| 已到岗 | ARRIVED | 到达现场,开始作业 |
+| 进行中 | IN_PROGRESS | 作业进行中 |
+| 已暂停 | PAUSED | 临时暂停 |
+| 已完成 | COMPLETED | 作业完成 |
+| 已取消 | CANCELLED | 工单取消 |
+
+### 2.3 优先级
+
+| 优先级 | 英文 | 说明 |
+|-------|------|------|
+| P0 紧急 | P0 Urgent | 最高优先级,可打断当前任务 |
+| P1 重要 | P1 High | 高优先级,优先处理 |
+| P2 普通 | P2 Normal | 普通优先级 |
+| P3 低优 | P3 Low | 低优先级 |
+
+### 2.4 触发来源
+
+| 来源 | 英文 | 说明 |
+|-----|------|------|
+| 蓝牙信标 | IOT_BEACON | 信标触发(如厕所溢水) |
+| 客流阈值 | IOT_TRAFFIC | 客流统计触发 |
+| 视频告警 | VIDEO_ALARM | AI 视频分析告警 |
+| 门禁告警 | ACCESS_ALARM | 门禁系统告警 |
+| 巡更告警 | PATROL_ALARM | 巡更异常告警 |
+| 紧急按钮 | PANIC_BUTTON | 紧急求助按钮 |
+| 手动创建 | MANUAL | 人工创建工单 |
+
+---
+
+## 三、巡检术语
+
+### 3.1 巡检要素
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 巡检区域 | Inspection Area | 需要巡检的物理区域 |
+| 巡检模板 | Inspection Template | 该区域检查项的配置 |
+| 检查项 | Inspection Item | 具体的检查内容 |
+| 巡检记录 | Inspection Record | 一次巡检的结果记录 |
+
+### 3.2 巡检结果
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 合格 | Passed | 检查项通过 |
+| 不合格 | Failed | 检查项未通过 |
+| 位置异常 | Location Exception | 未在指定区域完成巡检 |
+| 归属判定 | Attribution | 异常原因:个人责任/突发状况/正常 |
+
+---
+
+## 四、人员术语
+
+### 4.1 角色
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 保洁员 | Cleaner | 执行保洁任务的人员 |
+| 安保员 | Security Guard | 执行安保任务的人员 |
+| 维修员 | Repairman | 执行维修任务的人员 |
+| 巡检员 | Inspector | 执行巡检任务的人员 |
+| 调度员 | Dispatcher | 负责工单分配的人员 |
+
+### 4.2 人员状态
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 空闲 | IDLE | 可接受新任务 |
+| 忙碌 | BUSY | 正在执行任务 |
+| 暂停 | PAUSED | 临时离开 |
+| 离线 | OFFLINE | 不在线 |
+
+---
+
+## 五、区域术语
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 区域 | Area | 物理空间划分 |
+| 父区域 | Parent Area | 上级区域 |
+| 子区域 | Child Area | 下级区域 |
+| 功能类型 | Function Type | 区域功能:厕所/电梯/大堂等 |
+| 地理围栏 | Geo-fence | 虚拟边界,用于位置校验 |
+
+---
+
+## 六、移动端术语
+
+### 6.1 小程序
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| 主包 | Main Package | 小程序主包,2MB 限制 |
+| 分包 | SubPackage | 业务分包,2MB 限制 |
+| 预加载 | Preload | 提前加载分包资源 |
+| TabBar | Tab Bar | 底部导航栏 |
+
+### 6.2 蓝牙
+
+| 术语 | 英文 | 说明 |
+|-----|------|------|
+| BLE | Bluetooth Low Energy | 低功耗蓝牙 |
+| GATT | Generic Attribute Profile | 通用属性配置文件 |
+| MTU | Maximum Transmission Unit | 最大传输单元,默认20字节 |
+| Notify | Notification | BLE 通知机制 |
+| 广播 | Advertising | BLE 设备广播数据 |
+| 扫描 | Scanning | 搜索周围 BLE 设备 |
+
+---
+
+## 七、缩写对照
+
+| 缩写 | 全称 | 中文 |
+|-----|------|------|
+| AIOT | Artificial Intelligence of Things | 智联网 |
+| BLE | Bluetooth Low Energy | 低功耗蓝牙 |
+| RSSI | Received Signal Strength Indicator | 接收信号强度指示 |
+| UUID | Universally Unique Identifier | 通用唯一标识符 |
+| API | Application Programming Interface | 应用程序接口 |
+| HTTP | HyperText Transfer Protocol | 超文本传输协议 |
+| JSON | JavaScript Object Notation | JavaScript 对象表示法 |
+| DOM | Document Object Model | 文档对象模型 |
+| Pinia | - | Vue 状态管理库 |
+| UniApp | - | 跨端开发框架 |
diff --git a/开发者文档/08-附录/02-错误码清单.md b/开发者文档/08-附录/02-错误码清单.md
new file mode 100644
index 0000000..ea539ed
--- /dev/null
+++ b/开发者文档/08-附录/02-错误码清单.md
@@ -0,0 +1,242 @@
+# 02-错误码清单
+
+本文档汇总 AIOT 系统中使用的错误码,包括后端统一错误码和前端/移动端特定错误。
+
+---
+
+## 一、后端统一错误码
+
+### 1.1 全局错误码(0-999)
+
+| 错误码 | 错误信息 | 说明 |
+|-------|---------|------|
+| 0 | 成功 | 请求成功 |
+| 400 | 请求参数不正确 | 参数校验失败 |
+| 401 | 账号未登录 | Token 过期或无效 |
+| 403 | 没有该操作权限 | 权限不足 |
+| 404 | 请求未找到 | 资源不存在 |
+| 405 | 请求方法不正确 | HTTP 方法错误 |
+| 423 | 请求失败,请稍后重试 | 并发请求锁定 |
+| 429 | 请求过于频繁,请稍后重试 | 限流触发 |
+| 500 | 系统异常 | 服务器内部错误 |
+| 501 | 功能未实现/未开启 | 功能未开放 |
+| 502 | 错误的配置项 | 配置错误 |
+| 900 | 重复请求,请稍后重试 | 重复提交 |
+| 901 | 演示模式,禁止写操作 | Demo 模式限制 |
+| 999 | 未知错误 | 未分类错误 |
+
+### 1.2 OPS 模块错误码(1-020-xxx-xxx)
+
+#### 区域管理(1-020-001-xxx)
+
+| 错误码 | 错误信息 | 说明 |
+|-------|---------|------|
+| 1020001000 | 区域不存在 | AREA_NOT_FOUND |
+| 1020001001 | 该区域下存在子区域,请先处理子区域 | AREA_HAS_CHILDREN |
+| 1020001002 | 该区域已绑定设备,请先解除绑定 | AREA_HAS_DEVICES |
+| 1020001003 | 不能将父级设置为自己或子孙节点 | AREA_PARENT_LOOP |
+| 1020001004 | 区域编码已存在 | AREA_CODE_EXISTS |
+
+#### 区域设备关联(1-020-002-xxx)
+
+| 错误码 | 错误信息 | 说明 |
+|-------|---------|------|
+| 1020002000 | 设备不存在 | DEVICE_NOT_FOUND |
+| 1020002001 | 该工牌已绑定至此区域 | DEVICE_ALREADY_BOUND |
+| 1020002002 | 该区域已绑定{类型},一个区域只能绑定一个 | DEVICE_TYPE_ALREADY_BOUND |
+| 1020002003 | 设备关联关系不存在 | DEVICE_RELATION_NOT_FOUND |
+| 1020002004 | IoT 设备服务不可用,请稍后重试 | IOT_SERVICE_UNAVAILABLE |
+
+#### 安保工单(1-020-003-xxx)
+
+| 错误码 | 错误信息 | 说明 |
+|-------|---------|------|
+| 1020003000 | 工单不存在 | SECURITY_ORDER_NOT_FOUND |
+| 1020003001 | 工单类型不匹配,期望安保工单 | SECURITY_ORDER_TYPE_MISMATCH |
+| 1020003002 | 该安保人员已绑定到此区域 | SECURITY_AREA_USER_DUPLICATE |
+| 1020003003 | 绑定记录不存在 | SECURITY_AREA_USER_NOT_FOUND |
+| 1020003004 | 目标安保人员未绑定到工单所属区域或已停用 | SECURITY_ASSIGNEE_NOT_BOUND_TO_AREA |
+| 1020003005 | 当前工单状态不允许手动派单,仅 PENDING 状态可操作 | SECURITY_ORDER_STATUS_NOT_ALLOW_DISPATCH |
+| 1020003006 | 已完成或已取消的工单不允许升级优先级 | SECURITY_ORDER_PRIORITY_UPGRADE_NOT_ALLOWED |
+
+#### 巡检模块(1-020-004-xxx)
+
+| 错误码 | 错误信息 | 说明 |
+|-------|---------|------|
+| 1020004000 | 巡检模板不存在 | INSPECTION_TEMPLATE_NOT_FOUND |
+| 1020004001 | 巡检记录不存在 | INSPECTION_RECORD_NOT_FOUND |
+| 1020004002 | 该区域未启用,无法巡检 | INSPECTION_AREA_NOT_ACTIVE |
+
+---
+
+## 二、前端/移动端错误码
+
+### 2.1 HTTP 状态码(ResultEnum)
+
+```typescript
+// http/tools/enum.ts
+
+export enum ResultEnum {
+ Success0 = 0, // 成功
+ Success200 = 200, // 成功
+ Error = 400, // 错误
+ Unauthorized = 401, // 未授权
+ Forbidden = 403, // 禁止访问
+ NotFound = 404, // 未找到
+ MethodNotAllowed = 405, // 方法不允许
+ RequestTimeout = 408, // 请求超时
+ InternalServerError = 500, // 服务器错误
+ NotImplemented = 501, // 未实现
+ BadGateway = 502, // 网关错误
+ ServiceUnavailable = 503, // 服务不可用
+ GatewayTimeout = 504, // 网关超时
+ HttpVersionNotSupported = 505, // HTTP版本不支持
+}
+```
+
+### 2.2 蓝牙扫描状态(ScanStatus)
+
+```typescript
+// pages-ops/inspection/composables/use-bluetooth-scan.ts
+
+export type ScanStatus =
+ | 'idle' // 空闲
+ | 'scanning' // 扫描中
+ | 'success' // 定位成功
+ | 'failed' // 定位失败
+ | 'no-permission' // 缺少权限
+```
+
+### 2.3 蓝牙扫描错误信息
+
+| 状态 | 错误信息 | 处理建议 |
+|-----|---------|---------|
+| no-permission | 蓝牙权限已被拒绝,请在设置中开启 | 引导用户打开设置 |
+| no-permission | 需要蓝牙权限才能进行定位验证 | 申请蓝牙权限 |
+| failed | 启动蓝牙扫描失败 | 检查蓝牙是否开启 |
+| failed | 定位超时,未能验证位置 | 靠近信标重试 |
+
+### 2.4 网络错误提示
+
+```typescript
+// http/tools/enum.ts
+
+export function ShowMessage(status: number | string): string {
+ switch (status) {
+ case 400: return '请求错误(400),请检查网络或联系管理员!'
+ case 401: return '未授权,请重新登录(401),请检查网络或联系管理员!'
+ case 403: return '拒绝访问(403),请检查网络或联系管理员!'
+ case 404: return '请求出错(404),请检查网络或联系管理员!'
+ case 408: return '请求超时(408),请检查网络或联系管理员!'
+ case 500: return '服务器错误(500),请检查网络或联系管理员!'
+ case 501: return '服务未实现(501),请检查网络或联系管理员!'
+ case 502: return '网络错误(502),请检查网络或联系管理员!'
+ case 503: return '服务不可用(503),请检查网络或联系管理员!'
+ case 504: return '网络超时(504),请检查网络或联系管理员!'
+ case 505: return 'HTTP版本不受支持(505),请检查网络或联系管理员!'
+ default: return `连接出错(${status})!,请检查网络或联系管理员!`
+ }
+}
+```
+
+---
+
+## 三、错误码处理规范
+
+### 3.1 后端错误处理
+
+```java
+// Java 后端统一返回格式
+public class CommonResult {
+ private Integer code; // 错误码
+ private String msg; // 错误信息
+ private T data; // 数据
+
+ public static CommonResult success(T data) {
+ return new CommonResult<>(0, "成功", data);
+ }
+
+ public static CommonResult error(ErrorCode errorCode) {
+ return new CommonResult<>(errorCode.getCode(), errorCode.getMsg(), null);
+ }
+}
+```
+
+### 3.2 前端错误处理
+
+```typescript
+// http/http.ts 统一错误处理
+
+success: async (res) => {
+ const { code, msg } = responseData
+
+ // Token 过期处理
+ if (res.statusCode === 401 || code === 401) {
+ const tokenStore = useTokenStore()
+ if (!isDoubleTokenMode) {
+ tokenStore.logout()
+ toLoginPage()
+ return reject(res)
+ }
+ // 无感刷新 Token...
+ }
+
+ // 业务错误处理
+ if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
+ uni.showToast({
+ title: msg || '请求错误',
+ icon: 'none',
+ })
+ return reject(responseData)
+ }
+
+ return resolve(responseData.data)
+},
+
+fail(err) {
+ uni.showToast({
+ icon: 'none',
+ title: '网络错误,换个网络试试',
+ })
+ reject(err)
+}
+```
+
+### 3.3 错误码对照表
+
+| 场景 | 后端错误码 | 前端处理 |
+|-----|-----------|---------|
+| 参数错误 | 400 | Toast 提示具体错误 |
+| Token 过期 | 401 | 自动刷新或跳转登录 |
+| 权限不足 | 403 | Toast 提示无权限 |
+| 资源不存在 | 404 | 显示空状态或返回 |
+| 服务器错误 | 500 | Toast 提示系统异常 |
+| 请求频繁 | 429 | Toast 提示稍后重试 |
+| 蓝牙权限拒绝 | - | 引导打开系统设置 |
+| 定位超时 | - | 提示靠近信标重试 |
+| 网络断开 | - | Toast 提示网络错误 |
+
+---
+
+## 四、错误码速查
+
+### 4.1 按模块速查
+
+| 模块 | 错误码范围 | 文件 |
+|-----|-----------|------|
+| 全局 | 0-999 | GlobalErrorCodeConstants.java |
+| 区域 | 1-020-001-xxx | ErrorCodeConstants.java |
+| 设备 | 1-020-002-xxx | ErrorCodeConstants.java |
+| 安保 | 1-020-003-xxx | ErrorCodeConstants.java |
+| 巡检 | 1-020-004-xxx | ErrorCodeConstants.java |
+
+### 4.2 按场景速查
+
+| 场景 | 常见错误码 | 解决方案 |
+|-----|-----------|---------|
+| 登录失败 | 401 | 检查账号密码,重新登录 |
+| 派单失败 | 1020003005 | 确认工单状态为 PENDING |
+| 信标绑定失败 | 1020002001 | 解绑后重新绑定 |
+| 巡检提交失败 | 1020004002 | 确认区域已启用 |
+| 蓝牙扫描失败 | no-permission | 开启蓝牙和定位权限 |
+| 图片上传失败 | 408/504 | 压缩图片后重试 |