# 小程序架构 ## 概述 微信原生小程序(非 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 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 ``` ### 品牌色 | 用途 | 色值 | |------|------| | 主色 | #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, ... } ```