- request.js: also remove admin_token_expiry when clearing auth on 1001/1002 response, preventing stale expiry value in storage - 01-服务端架构.md: fix table count from 10 to 11 - 03-管理后台架构.md: add /api/v1 prefix to all API endpoint paths
14 KiB
14 KiB
服务端架构
概述
服务端是一个运行在腾讯云 SCF(Serverless 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 # 完整 DDL(11 张表)
└── 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 # 微信小程序 API(code2Session、手机号)
└── 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 start → node scripts/local-server.js → 监听 config.port(默认 3000)。