文件
Guoguo c5f6033ccf fix: improve UX and fix admin console display issues
Miniprogram:
- Add back buttons to all custom-nav pages
- Add debug/mock buttons (BLE connect, wear check, treatment)
  controlled by __DEV__ flag (hidden in prod)

Admin console:
- Fix log page using nonexistent fields (operator_type, target_type)
  now correctly reads admin_id/user_id/action/detail from API
- Fix subscription date fields (started_at→start_time, expired_at→expire_time)
- Wire up subscription detail/extend/renew action buttons
- Wire up device detail "查看完整日志" button
- Add "创建订阅" button to subscription toolbar
- Fix subscription status mapping (2=expired, 3=cancelled)

Docs:
- Add detailed architecture docs for server, miniprogram, admin console
2026-04-28 18:32:26 -07:00

424 行
15 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 小程序架构
## 概述
微信原生小程序(非 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, ... }
```