文件
jw-beauty/docs/architecture/01-服务端架构.md
T
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

14 KiB
原始文件 Blame 文件历史

服务端架构

概述

服务端是一个运行在腾讯云 SCFServerless Cloud Function)上的 Node.js 应用,以 HTTP 函数形式对外暴露 RESTful API。不依赖任何 Web 框架(如 Express/Koa),所有路由和请求处理均为手写实现。

技术栈: Node.js + mysql2 + jsonwebtoken + bcryptjs + cos-nodejs-sdk-v5


目录结构

server/
├── index.js                  # 入口:re-export src/index.js
├── scf_bootstrap             # SCF Web 函数冷启动脚本
├── package.json
├── scripts/
│   ├── init-db.js            # 数据库初始化(建表 + 种子数据)
│   └── local-server.js       # 本地开发 HTTP 服务器
├── sql/
│   └── schema.sql            # 完整 DDL10 张表)
└── src/
    ├── index.js              # SCF 入口:导出 main_handler(event, context)
    ├── app.js                # 请求分发:解析 → 路由匹配 → 执行 → 响应
    ├── config.js             # 环境变量集中配置
    ├── lib/
    │   ├── auth.js           # JWT 签发/验证、bcrypt 密码哈希
    │   ├── cos.js            # 腾讯云 COS 客户端(预签名 URL)
    │   ├── db.js             # MySQL 连接池、query/one/transaction
    │   ├── log.js            # 操作日志写入
    │   ├── request.js        # SCF event → ctx 对象解析
    │   ├── response.js       # ok/fail/http 响应构造
    │   ├── router.js         # 轻量正则路由器
    │   ├── utils.js          # 日期格式化工具
    │   └── wechat.js         # 微信小程序 APIcode2Session、手机号)
    └── routes/
        ├── admin.js          # 管理后台 CRUD
        ├── auth.js           # 微信登录 + token 刷新
        ├── device.js         # 设备绑定/解绑/命令/事件
        ├── firmware.js       # 固件管理 + OTA 检查
        ├── subscription.js   # 订阅状态/购买/核销
        ├── treatment.js      # 护理记录同步/查询
        └── user.js           # 用户信息 CRUD

请求生命周期

一个请求从进入到返回的完整流程:

SCF 触发 / HTTP 请求
     │
     ▼
scf_bootstrap (PORT=9000, node scripts/local-server.js)
     │
     ▼
local-server.js 收集请求体,构造 API Gateway event 对象
     │
     ▼
src/index.js → main_handler(event, context)
     │
     ▼
src/app.js → handle(event)
     │
     ├── createContext(event)    ← 解析 method/path/headers/query/body/ip
     ├── OPTIONS ?              → 直接返回 204(CORS 预检)
     ├── /health ?              → 返回 { status: 'ok' }
     ├── router.match(method, path)
     │    ├── 无匹配            → 404 not_found
     │    └── 有匹配            → 提取 URL 参数,调用 handler(ctx)
     │         ├── handler 返回值 → http(200, body)
     │         └── handler 抛异常 → http(500, fail(3001, 'server_error'))
     │
     ▼
SCF API Gateway 响应格式:
{
  isBase64Encoded: false,
  statusCode: 200,
  headers: { 'Access-Control-Allow-Origin': '*', ... },
  body: '{"code":0,"message":"success","data":{...}}'
}

关键设计点

  • 无中间件链:没有 Koa/Express 那样的中间件栈。鉴权在每个路由处理函数开头手动调用 requireUser(ctx)requireAdmin(ctx)
  • CORS:所有响应都带 Access-Control-Allow-Origin: *,允许 Content-Type, Authorization, X-Device-Id, X-App-Version, X-Platform 请求头。
  • 路由器:自研轻量实现,把 /api/v1/device/:device_id 这样的路径转换成正则表达式,匹配时提取参数写入 ctx.params

配置结构

所有配置通过 dotenv.env 文件加载,集中定义在 src/config.js

配置项 环境变量 默认值 说明
db.host DB_HOST - MySQL 地址
db.port DB_PORT 3306 MySQL 端口
db.user DB_USER root
db.password DB_PASSWORD -
db.database DB_NAME jw_beauty
jwt.secret JWT_SECRET dev-user-secret 用户 token 签名密钥
jwt.adminSecret ADMIN_JWT_SECRET dev-admin-secret 管理员 token 签名密钥
jwt.expiresIn - 7d token 有效期
cos.secretId TENCENT_SECRET_ID - COS 密钥
cos.secretKey TENCENT_SECRET_KEY - COS 密钥
cos.bucket COS_BUCKET jw-bucket-1426323813
cos.region COS_REGION ap-guangzhou
wechat.appid WECHAT_APPID - 小程序 AppID
wechat.secret WECHAT_SECRET - 小程序密钥

生产环境保护:如果 NODE_ENV=production 且 JWT 密钥仍为默认值,进程启动时会直接抛异常,防止带着测试密钥上线。


数据库层

连接方式

使用 mysql2/promise,惰性初始化单例连接池:

  • 连接数上限:5
  • 命名占位符::param_name(通过 namedPlaceholders: true 启用)
  • 时区:+08:00

查询工具

函数 说明
query(sql, params) 执行查询,返回行数组
one(sql, params) 执行查询,返回第一行或 null
transaction(work) 获取连接 → BEGIN → 执行 work(conn) → COMMIT/ROLLBACK → 释放
limitClause(pageSize, offset) 返回 LIMIT N OFFSET M 字符串片段

数据表一览

共 10 张表,全部 InnoDB + utf8mb4_unicode_ci

表名 用途 主键
users 小程序用户 user_id (自增)
devices 设备信息 device_id (字符串)
bindings 用户-设备绑定关系 binding_id (自增)
subscriptions 用户订阅 subscription_id (自增)
treatment_records 护理记录 record_id (自增),session_id 唯一索引
device_events 设备事件日志 event_id (自增)
device_commands 远程指令队列 command_id (自增)
operation_logs 操作审计日志 log_id (自增)
admin_accounts 管理员账号 admin_id (自增)
system_settings 系统配置键值对 setting_key (字符串)
firmware_files 固件文件记录 firmware_id (自增)

核心表详细结构

users

字段 类型 说明
user_id BIGINT UNSIGNED AUTO_INCREMENT 主键
openid VARCHAR(64) UNIQUE 微信 openid
nickname VARCHAR(100) 昵称
avatar VARCHAR(500) 头像 URL
phone VARCHAR(32) 手机号
gender TINYINT 0=未知
status TINYINT 1=正常

devices

字段 类型 说明
device_id VARCHAR(32) 主键,设备编号
product_id VARCHAR(64) 产品型号,默认 HOX_LIGHT_MASK
device_secret VARCHAR(128) 设备密钥
firmware_version VARCHAR(32) 固件版本
status TINYINT 1=未激活 2=在线 3=离线 4=禁用
battery TINYINT UNSIGNED 电量
temperature TINYINT UNSIGNED 温度
last_online_at DATETIME 最后在线时间

bindings

字段 类型 说明
binding_id BIGINT AUTO_INCREMENT 主键
user_id BIGINT 外键 → users
device_id VARCHAR(32) 外键 → devices
bind_token CHAR(16) 绑定令牌(16位 hex
bind_expires DATETIME 令牌过期时间(10 分钟)
bind_status TINYINT 1=已绑定 2=已解绑 3=待确认

subscriptions

字段 类型 说明
subscription_id BIGINT AUTO_INCREMENT 主键
user_id BIGINT 外键 → users
plan VARCHAR(32) trial / monthly / yearly
status TINYINT 1=生效 2=过期 3=取消
amount DECIMAL(10,2) 金额
start_time / expire_time DATETIME 有效期

treatment_records

字段 类型 说明
record_id BIGINT AUTO_INCREMENT 主键
session_id VARCHAR(64) UNIQUE 会话 ID(唯一标识一次护理)
device_id / user_id 设备和用户
regions VARCHAR(255) 护理区域(逗号分隔或位掩码)
total_duration_ms INT UNSIGNED 护理时长(毫秒)
mode TINYINT 0=普通 1=智能
avg_pd DECIMAL(8,4) 平均光密度
pd_json JSON 详细光密度数据

鉴权系统

双密钥 JWT

系统使用两套独立的 JWT 密钥:

用户 token:  JWT_SECRET       → type: 'user',  payload: { user_id, openid }
管理员 token: ADMIN_JWT_SECRET → type: 'admin', payload: { admin_id, username, role }

两种 token 都是 7 天有效期。

用户登录流程

小程序 wx.login()  →  code
      │
      ▼
POST /api/v1/auth/login { code }
      │
      ├── server 调用微信 jscode2session 换取 openid
      ├── 查询/自动创建 users 记录
      ├── 签发 JWT(含 user_id, openid
      └── 返回 { token, user_id, user_info, expires_in: 604800 }

管理员登录流程

POST /api/v1/admin/login { username, password }
      │
      ├── 查询 admin_accounts 表
      ├── bcrypt 验证密码
      │   └── 失败 → 尝试旧 SHA-256 验证
      │        └── 成功 → 自动迁移密码到 bcrypt
      ├── 签发 JWT(含 admin_id, username, role
      └── 返回 { token, admin_id, username, real_name, role }

Token 刷新

POST /api/v1/auth/refresh

  • token 未过期且剩余 > 7 天 → 原样返回
  • token 未过期且剩余 ≤ 7 天 → 签发新 token
  • token 已过期但在 3 天宽限期内 → 签发新 token
  • token 已过期超过 3 天 → 拒绝,需重新登录

API 接口清单

认证与用户

方法 路径 鉴权 说明
POST /api/v1/auth/login 微信登录,返回 JWT
POST /api/v1/auth/refresh Bearer (可过期) 刷新 token
GET /api/v1/user/profile 用户 获取个人信息
PUT /api/v1/user/profile 用户 修改昵称/头像/性别
POST /api/v1/user/phone 用户 绑定手机号(微信授权)

设备

方法 路径 鉴权 说明
POST /api/v1/device/bind 用户 发起绑定(生成 bind_token,10分钟有效)
POST /api/v1/device/bind/confirm 用户 确认绑定(BLE 握手完成后调用)
POST /api/v1/device/unbind 用户 解绑设备
GET /api/v1/device/list 用户 已绑定设备列表
GET /api/v1/device/:device_id 用户 设备详情(校验归属)
GET /api/v1/device/command/pending 用户 拉取待执行远程指令
POST /api/v1/device/command/result 用户 上报指令执行结果
POST /api/v1/device/event 用户 上报设备事件

订阅

方法 路径 鉴权 说明
GET /api/v1/subscription 用户 查询当前订阅状态
POST /api/v1/subscription/purchase 用户 购买(未接入支付,仅生成订单号)
POST /api/v1/subscription/verify 管理员 后台核销订阅(临时方案)

护理记录

方法 路径 鉴权 说明
GET /api/v1/treatment/history 用户 分页查询历史记录
POST /api/v1/treatment/sync 用户 同步护理记录(INSERT ON DUPLICATE
GET /api/v1/treatment/:record_id 用户 单条记录详情(按 session_id 查)

固件

方法 路径 鉴权 说明
GET /api/v1/firmware/latest 用户 OTA 更新检查
GET /api/v1/admin/firmware 管理员 固件列表
POST /api/v1/admin/firmware 管理员 上传固件记录
POST /api/v1/admin/firmware/:id/status 管理员 启用/禁用固件

管理后台

方法 路径 鉴权 说明
POST /api/v1/admin/login 管理员登录
GET /api/v1/admin/dashboard 管理员 仪表盘统计
GET/POST /api/v1/admin/devices 管理员 设备列表/预生成产品码
GET /api/v1/admin/devices/:id 管理员 设备详情
POST /api/v1/admin/devices/:id/unbind 管理员 强制解绑
POST /api/v1/admin/devices/:id/command 管理员 下发远程指令
GET /api/v1/admin/devices/:id/commands 管理员 指令历史
GET /api/v1/admin/users 管理员 用户列表
GET /api/v1/admin/users/:id 管理员 用户详情(含设备和最近护理)
GET/POST /api/v1/admin/subscriptions 管理员 订阅列表/创建订阅
GET /api/v1/admin/records 管理员 所有护理记录
GET /api/v1/admin/logs 管理员 操作日志
GET/POST /api/v1/admin/settings 管理员 系统设置读写

外部服务集成

微信小程序 API

接口 用途
jscode2session 用 code 换取 openid(登录)
cgi-bin/token 获取 access_token(缓存在内存中,带 60s 安全边际)
getuserphonenumber 通过授权码获取用户手机号

开发环境绕过:如果 NODE_ENV=development 且 code 为空或以 dev_ 开头,返回合成的 openid,不调用微信。

腾讯云 COS

用于固件文件存储。getObjectUrl(key, expiresSeconds) 生成预签名下载 URL,默认 3600 秒有效。


错误码约定

所有响应格式为 { code, message, data }

code 含义
0 成功
404 路由不存在
1001 鉴权失败(token 无效/过期)
1002 管理员权限不足
1004 用户不存在
1005 资源不存在(设备/记录)
1006 设备未绑定
2001 参数校验失败
3001 服务器内部错误

部署方式

腾讯云 SCF(Web 函数)
     ├── 入口:scf_bootstrap → node scripts/local-server.js
     ├── 端口:9000
     ├── 运行时:Node.js
     └── 触发方式:API Gateway HTTP 触发

本地开发:npm startnode scripts/local-server.js → 监听 config.port(默认 3000)。