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
424 行
15 KiB
Markdown
424 行
15 KiB
Markdown
# 小程序架构
|
||
|
||
## 概述
|
||
|
||
微信原生小程序(非 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, ... }
|
||
```
|