# 小程序架构

## 概述

微信原生小程序（非 uni-app），使用 WXML + WXSS + JS 开发。通过 BLE 5.0 与光子美容仪设备通信，通过 HTTP 与腾讯云 SCF 后端通信。

**AppID：** wxc4045074ef298510  
**项目名：** hox-beauty  
**基础库：** 3.15.2

---

## 目录结构

```
miniprogram/
├── app.js / app.json / app.wxss    # 应用入口
├── config/
│   └── env.js                       # 环境配置（API 地址）
├── pages/
│   ├── index/          # 首页（Tab）—— 设备状态、开始护理
│   ├── history/        # 记录（Tab）—— 护理历史列表
│   ├── profile/        # 我的（Tab）—— 个人中心
│   ├── login/          # 微信登录
│   ├── scan/           # 扫码绑定设备
│   ├── ble-connect/    # 蓝牙连接 + BLE 绑定
│   ├── bind-success/   # 绑定成功
│   ├── wear-check/     # 佩戴检测
│   ├── treatment-setup/# 护理模式选择
│   ├── auto-scan/      # 智能模式面部扫描
│   ├── treating/       # 护理进行中
│   ├── treatment-done/ # 护理完成
│   ├── subscribe-prompt/   # 订阅引导
│   ├── subscribe-plans/    # 订阅套餐选择
│   └── subscribe-success/  # 订阅成功
├── services/
│   ├── ble.js              # BLE 通信协议核心
│   └── command-sync.js     # 远程指令拉取与执行
├── utils/
│   ├── request.js          # HTTP 请求封装 + token 刷新
│   └── mock.js             # 开发用 mock（已禁用）
├── scripts/
│   └── test-ble-frame.js   # BLE 帧构建/解析单元测试
├── project.config.json
└── sitemap.json
```

共 15 个页面，无自定义组件，全部使用微信原生组件。

---

## 应用生命周期

### app.js

**globalData：**

| 字段 | 用途 |
|------|------|
| `userInfo` | 用户信息对象（来自 `/api/v1/user/profile`） |
| `userId` | 用户 ID 字符串 |
| `connectedDevice` / `currentDevice` | 当前绑定的设备信息 |
| `currentTreatment` | 最近一次护理结果（跨页传递） |
| `statusBarHeight` | 系统状态栏高度（默认 44） |

**onLaunch 流程：**

1. `wx.getSystemInfoSync()` → 读取 `statusBarHeight`
2. `checkLogin()` → 从 Storage 读取 token
   - 有 token → 调用 `loadProfile()` 加载用户信息
   - 无 token → 设置 `userInfo = null`（各页面自行跳转登录）

---

## 页面导航流程

### 主要业务路径

```
                    ┌─────────┐
                    │  login  │ ← 无 token 时跳转
                    └────┬────┘
                         │ wx.switchTab
                         ▼
              ┌──────────────────────┐
              │   index（首页 Tab）    │
              │  设备状态 / 订阅状态   │
              └──────┬───────────────┘
                     │
          ┌──────────┼──────────┐
          │ 无设备    │ 有设备    │ 有设备但无订阅
          ▼          ▼          ▼
       ┌──────┐  ┌────────┐  ┌─────────────────┐
       │ scan │  │ wear-  │  │subscribe-prompt │
       │ 扫码 │  │ check  │  │   订阅引导       │
       └──┬───┘  └───┬────┘  └───────┬─────────┘
          │          │               │
          ▼          ▼               ▼
    ┌───────────┐ ┌──────────────┐ ┌─────────────────┐
    │ble-connect│ │treatment-    │ │subscribe-plans  │
    │ 蓝牙配对   │ │setup 模式选择│ │   套餐选择       │
    └─────┬─────┘ └──────┬───────┘ └─────────────────┘
          │              │
          ▼         ┌────┴────┐
    ┌───────────┐   │         │
    │bind-      │ 普通模式  智能模式
    │success    │   │         │
    └───────────┘   │    ┌────┴─────┐
                    │    │auto-scan │
                    │    │ 面部扫描  │
                    │    └────┬─────┘
                    │         │
                    ▼         ▼
              ┌──────────────────┐
              │    treating      │
              │   护理进行中      │
              └────────┬─────────┘
                       │
                       ▼
              ┌──────────────────┐
              │  treatment-done  │
              │    护理完成       │
              └──────────────────┘
```

### Tab 页

| Tab | 页面 | 说明 |
|-----|------|------|
| 首页 | index | 设备连接状态、开始护理入口 |
| 记录 | history | 护理历史列表（分页、下拉刷新） |
| 我的 | profile | 个人信息、订阅状态、设置 |

### 导航方式

- Tab 间切换：`wx.switchTab`
- 业务流程前进：`wx.navigateTo`（压栈）或 `wx.redirectTo`（替换）
- 强制跳转：`wx.reLaunch`（清空页面栈，用于登录跳转）

---

## 页面详情

### login — 微信登录

- 调用 `wx.getUserProfile()` 获取头像昵称（2022.10 后始终返回「微信用户」和灰色头像）
- 调用 `app.doLogin()` → `wx.login()` 获取 code → `POST /api/v1/auth/login`
- 登录后保存 `token` 和 `token_expiry` 到 Storage
- 成功后 `wx.switchTab` 到首页

### index — 首页

- `GET /api/v1/device/list` 获取绑定设备列表
- `GET /api/v1/subscription` 获取订阅状态
- BLE：订阅 `ble.on('status')` 更新电量和设备状态
- 连接设备后自动调用 `ble.queryStatus()`
- 解绑操作：`ble.disconnect()` + `POST /api/v1/device/unbind`

### scan — 扫码绑定

- `wx.scanCode({ onlyFromCamera: true, scanType: ['qrCode'] })` 扫描设备二维码
- 支持手动输入 16 位 hex 设备号
- `POST /api/v1/device/bind` → 获取 `bind_token`
- 跳转到 `ble-connect` 页面

### ble-connect — 蓝牙连接

- 状态机：`scanning → connecting → binding → done / error`
- BLE 扫描过滤名称含 `HOX` 或 `LIGHTMASK` 的设备
- 连接后发送 BIND 命令，等待 `bind_result` 通知
- 成功后 `POST /api/v1/device/bind/confirm`

### wear-check — 佩戴检测

- 通过 `ble.queryStatus()` 检查设备 `bind_status === 1`
- 确认佩戴后进入模式选择

### treatment-setup — 模式选择

- 普通模式（mode=0）：全脸、10 分钟固定
- 智能模式（mode=1）：需要有效订阅，可选区域
- 区域位掩码：左脸(0x01) 右脸(0x02) 额头(0x04) 下巴(0x08) 鼻部(0x10)
- 普通模式直接发 `setParams` + `startTreatment` 开始护理
- 智能模式先跳到 auto-scan 扫描

### treating — 护理中

- 订阅 6 个 BLE 事件：`status`, `treatment_complete`, `exception`, `disconnected`, `reconnected`, `reconnect_failed`
- 本地 1 秒定时器作为倒计时后备
- 接收设备 `status` 报告校正剩余时间
- 每次 status 更新触发 `commandSync.sync(deviceId)` 拉取远程指令
- 断连时暂停计时器，重连后恢复
- 护理结束触发条件：设备完成通知 / 本地定时器归零 / 手动停止 / 重连失败

### treatment-done — 护理完成

- `POST /api/v1/treatment/sync` 同步记录到服务器
- 展示护理时长、区域、模式等摘要

### history — 护理记录

- `GET /api/v1/treatment/history?page=N&page_size=20` 分页加载
- 支持下拉刷新和触底加载
- 累计统计：总次数、总时长、本月次数

### profile — 我的

- `GET /api/v1/user/profile` + `GET /api/v1/subscription`
- 菜单入口：设备管理、护理记录、订阅管理、使用帮助、联系我们
- 退出登录：清除 token → `wx.reLaunch` 到 login

---

## BLE 通信协议

### GATT 服务和特征值

| 服务 UUID | 名称 | 特征值 |
|-----------|------|--------|
| FFE0 | 设备信息服务 | FFE3 (设备信息 - 读) |
| FFE1 | 数据通信服务 | FFE4 (命令 - 写), FFE5 (状态 - 通知), FFE6 (绑定信息) |
| FFE2 | OTA 服务 | FFE7 (OTA控制), FFE8 (OTA数据), FFE9 (OTA状态) |

### 帧格式

```
┌──────┬──────┬────────┬──────┬─────────────────┬──────────┐
│ 0xAA │ 0x55 │ Length │ Type │ Payload         │ Checksum │
│ 1B   │ 1B   │ 1B     │ 1B   │ Length bytes    │ 1B (XOR) │
└──────┴──────┴────────┴──────┴─────────────────┴──────────┘
```

- **Header**：固定 `0xAA 0x55`
- **Length**：Payload 字节数
- **Type**：命令/通知类型
- **Checksum**：前面所有字节的 XOR 值

### 命令类型（小程序 → 设备）

| 类型 | 值 | Payload |
|------|----|---------|
| SET_PARAMS | 0x01 | region_mask, wavelength, brightness, duration_ms(4B大端), mode, seq |
| START | 0x02 | region_mask, seq |
| STOP | 0x03 | seq |
| QUERY_STATUS | 0x04 | seq |
| BIND | 0x05 | 0x01, userId(8B), bindToken(8B), timestamp(4B大端), seq |
| UNBIND | 0x06 | 0x02, userId(8B), seq |

每条命令的 payload 末尾都有 `seq` 字节（0-255 循环），用于匹配 ACK 响应。

### 通知类型（设备 → 小程序）

| 类型 | 值 | Payload |
|------|----|---------|
| STATUS_REPORT | 0x21 | mode_state, region_mask, wavelength, brightness, remaining_ms(4B), error_code, command_seq, battery, temperature, bind_status, subscription — 共 14 字节 |
| ACK | 0x22 | seq, error_code |
| TREATMENT_COMPLETE | 0x31 | session_id(8B), regions, total_duration_ms(4B), avg_pd |
| EXCEPTION | 0x32 | error_code, temperature |
| BIND_SUCCESS | 0x33 | result (0x00=成功) |

### 命令重试机制

`writeCommandWithRetry(type, payload, maxRetries=3)`：

- 调用 `writeCommand` 发送命令
- 失败时等待 500ms 后重试
- 最多重试 3 次
- 如果错误码为 `0x0C`（BLE 断连），立即失败不重试
- ACK 超时时间：5 秒

### 自动重连机制

- 检测到 BLE 断连时自动触发
- 等待 2 秒后第一次重连尝试
- 最多 3 次，间隔 2 秒
- 重连成功后重新发现服务、重新订阅特征值通知
- 手动调用 `disconnect()` 时禁用自动重连

### 设备状态机

| 值 | 状态 | 说明 |
|----|------|------|
| 0x00 | IDLE | 空闲 |
| 0x01 | SCANNING | 面部扫描中 |
| 0x02 | ACTIVE | 护理中 |
| 0x03 | PAUSED | 已暂停 |
| 0x04 | COMPLETED | 已完成 |
| 0x05 | ERROR | 异常 |
| 0x06 | OTA | 固件升级中 |

### 区域位掩码

| 位 | 值 | 区域 |
|----|----|------|
| 0 | 0x01 | 左脸颊 |
| 1 | 0x02 | 右脸颊 |
| 2 | 0x04 | 额头 |
| 3 | 0x08 | 下巴 |
| 4 | 0x10 | 鼻部 |
| 5 | 0x20 | 左眼周 |
| 6 | 0x40 | 右眼周 |
| 全部 | 0x7F | 全脸 |

### 设备错误码

| 码 | 名称 | 说明 |
|----|------|------|
| 0x00 | SUCCESS | 成功 |
| 0x01 | ERR_REGION_INVALID | 区域无效 |
| 0x02 | ERR_REGION_EMPTY | 区域为空 |
| 0x03 | ERR_BRIGHTNESS_INVALID | 亮度无效 |
| 0x04 | ERR_DURATION_INVALID | 时长无效 |
| 0x05 | ERR_NOT_BOUND | 未绑定 |
| 0x06 | ERR_NO_SUBSCRIPTION | 无订阅 |
| 0x07 | ERR_TEMP_HIGH | 温度过高 |
| 0x08 | ERR_BATTERY_LOW | 电量不足 |
| 0x09 | ERR_ALREADY_RUNNING | 已在运行 |
| 0x0A | ERR_NOT_RUNNING | 未在运行 |
| 0x0B | ERR_OTA_FAILED | OTA 失败 |
| 0x0C | ERR_BLE_DISCONNECTED | 蓝牙断连 |

---

## 远程指令同步（command-sync.js）

服务端可以通过管理后台向设备下发远程指令，小程序在护理过程中定期拉取并执行：

```
commandSync.sync(deviceId)
     │
     ├── GET /api/v1/device/command/pending?device_id=xxx
     │   └── 返回待执行命令列表（status=1 → 标记为 status=2）
     │
     ├── 逐条执行 BLE 命令（按 opcode 分发到对应函数）
     │
     └── POST /api/v1/device/command/result
         └── 上报执行结果（成功/失败）
```

---

## HTTP 请求层（request.js）

### Token 自动刷新

- 每次请求前检查 `token_expiry`：如果距离过期不到 24 小时，先调用 `POST /api/v1/auth/refresh` 刷新
- 并发请求的刷新去重：使用 `_refreshing` 标志和 `_refreshQueue` 队列，保证同一时间只有一个刷新请求
- 刷新成功后更新 Storage 中的 `token` 和 `token_expiry`

### 鉴权失败处理

- 响应 code 为 1001 或 1002 时：清除 token → `wx.reLaunch` 到 login 页

### 请求头

```
Authorization: Bearer <token>
Content-Type: application/json
X-App-Version: 1.0.0
X-Platform: wechat
```

---

## 环境配置

| 环境 | API 地址 |
|------|---------|
| local | `http://localhost:3000` |
| test | `https://1426323813-ilxkhlxf4p.ap-guangzhou.tencentscf.com` |
| prod | 待配置（发布阻断项） |

当前设置：`ENV = 'test'`

---

## UI 模式

### 自定义导航栏

所有非 Tab 页使用 `"navigationStyle": "custom"`，通过 `statusBarHeight + 24px` 的 padding-top 避开系统状态栏：

```html
<view class="page-header" style="padding-top: {{statusBarHeight + 24}}px;">
```

### 品牌色

| 用途 | 色值 |
|------|------|
| 主色 | #E6508C（粉色） |
| 辅色 | #DCB982（金色） |
| 成功 | #52c41a（绿色） |
| 信息 | #006699（蓝色） |
| 错误 | #ff4d4f（红色） |

### 全局 CSS 类

定义在 `app.wxss`，包括：`.page-header`（带颜色变体）、`.btn-primary`（带颜色变体）、`.btn-secondary`、`.status-badge`、`.device-card`、`.tip-card`、工具类（`.mt-20`, `.mb-30`, `.text-muted` 等）。

---

## 数据流汇总

### 登录 → 绑定 → 护理 → 同步

```
1. 登录
   wx.login() → code → POST /auth/login → token + user_info → Storage

2. 绑定设备
   扫码/手动输入 → device_id → POST /device/bind → bind_token
   → BLE 扫描 + 连接 → BIND 命令(0x05) → 等待 BIND_SUCCESS(0x33)
   → POST /device/bind/confirm → 自动创建 7 天试用订阅

3. 护理
   佩戴检测 → 模式选择 → SET_PARAMS(0x01) → START(0x02)
   → 护理中（实时 STATUS 更新 + 指令同步）
   → 完成（TREATMENT_COMPLETE 通知或本地定时器归零）

4. 记录同步
   POST /treatment/sync { session_id, device_id, regions, duration, mode, ... }
```
