286 lines
8.6 KiB
Markdown
286 lines
8.6 KiB
Markdown
# 微信扫码授权服务 — 需求文档
|
||||
|
|
|
|||
|
|
## 1. 项目概述
|
|||
|
|
|
|||
|
|
为一个 Windows MFC 桌面程序提供微信扫码授权服务。用户通过微信扫码关注公众号(当前使用微信测试号),服务端据此判断授权状态,MFC 端轮询后决定是否放行。后续支持用户充值获得时长或积分。
|
|||
|
|
|
|||
|
|
## 2. 技术栈
|
|||
|
|
|
|||
|
|
- 操作系统:Alibaba Cloud Linux 3
|
|||
|
|
- Web 框架:Python 3.10+ / FastAPI
|
|||
|
|
- ASGI 服务器:Uvicorn
|
|||
|
|
- 反向代理:Nginx(已配置,Cloudflare Tunnel 作为当前 HTTPS 入口)
|
|||
|
|
- 数据库:MySQL 8.0(Docker 部署,监听 127.0.0.1:3306)
|
|||
|
|
- 微信侧:微信公众平台测试号(后续迁移正式服务号)
|
|||
|
|
|
|||
|
|
## 3. 微信测试号配置
|
|||
|
|
|
|||
|
|
在.env文件内.
|
|||
|
|
URL:`http://ethereal-realm.top/wechat` |
|
|||
|
|
|
|||
|
|
## 4. 系统架构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
MFC 客户端
|
|||
|
|
│ HTTPS
|
|||
|
|
▼
|
|||
|
|
Cloudflare Tunnel / Nginx
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
FastAPI 服务
|
|||
|
|
├── /wechat 接收微信事件推送与 URL 验证
|
|||
|
|
├── /auth/* MFC 请求授权相关接口
|
|||
|
|
└── /usage/* MFC 上报使用与扣减
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
MySQL
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
服务端为唯一权威。MFC 端不缓存任何可信授权数据,仅保存会话令牌。
|
|||
|
|
|
|||
|
|
## 5. 数据库设计
|
|||
|
|
|
|||
|
|
### 5.1 users — 用户表
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| id | BIGINT PK AUTO_INCREMENT | |
|
|||
|
|
| openid | VARCHAR(64) UNIQUE NOT NULL | 微信 OpenID |
|
|||
|
|
| nickname | VARCHAR(100) | 昵称(可选) |
|
|||
|
|
| created_at | DATETIME | 首次关注时间 |
|
|||
|
|
| last_seen_at | DATETIME | 最近活跃时间 |
|
|||
|
|
| has_claimed_free | TINYINT DEFAULT 0 | 是否已领过免费授权 |
|
|||
|
|
|
|||
|
|
### 5.2 authorizations — 授权表
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| id | BIGINT PK AUTO_INCREMENT | |
|
|||
|
|
| user_id | BIGINT NOT NULL | 外键 users.id |
|
|||
|
|
| type | ENUM('time','points') | 授权类型 |
|
|||
|
|
| start_at | DATETIME NULL | 时间授权开始 |
|
|||
|
|
| end_at | DATETIME NULL | 时间授权结束 |
|
|||
|
|
| remaining_points | INT DEFAULT 0 | 积分余额 |
|
|||
|
|
| total_points | INT DEFAULT 0 | 积分总量 |
|
|||
|
|
| source | ENUM('free','purchase','admin') | 来源 |
|
|||
|
|
| status | ENUM('pending','active','expired','exhausted','cancelled') | 状态 |
|
|||
|
|
| created_at | DATETIME | |
|
|||
|
|
| updated_at | DATETIME | |
|
|||
|
|
|
|||
|
|
索引:`idx_user_status (user_id, status)`
|
|||
|
|
|
|||
|
|
### 5.3 auth_scenes — 扫码场景表
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| id | BIGINT PK AUTO_INCREMENT | |
|
|||
|
|
| scene_str | VARCHAR(128) UNIQUE NOT NULL | 二维码场景值 |
|
|||
|
|
| device_id | VARCHAR(128) | MFC 设备标识 |
|
|||
|
|
| status | ENUM('pending','scanned','authorized','expired') | |
|
|||
|
|
| user_id | BIGINT NULL | 扫码用户 |
|
|||
|
|
| created_at | DATETIME | |
|
|||
|
|
| expires_at | DATETIME NOT NULL | 默认 300 秒 |
|
|||
|
|
| authorized_at | DATETIME NULL | |
|
|||
|
|
|
|||
|
|
索引:`idx_scene_status (scene_str, status)`
|
|||
|
|
|
|||
|
|
### 5.4 usage_logs — 使用日志
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| id | BIGINT PK AUTO_INCREMENT | |
|
|||
|
|
| user_id | BIGINT NOT NULL | |
|
|||
|
|
| device_id | VARCHAR(128) | |
|
|||
|
|
| authorization_id | BIGINT | 本次扣减的授权 |
|
|||
|
|
| cost_type | ENUM('time','points') | |
|
|||
|
|
| cost_points | INT DEFAULT 0 | 时间授权为 0 |
|
|||
|
|
| used_at | DATETIME | |
|
|||
|
|
|
|||
|
|
索引:`idx_user_time (user_id, used_at)`
|
|||
|
|
|
|||
|
|
### 5.5 orders — 订单表(阶段 3 使用)
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| id | BIGINT PK AUTO_INCREMENT | |
|
|||
|
|
| order_no | VARCHAR(64) UNIQUE NOT NULL | |
|
|||
|
|
| user_id | BIGINT NOT NULL | |
|
|||
|
|
| amount | DECIMAL(10,2) NOT NULL | |
|
|||
|
|
| product_id | BIGINT | 商品 ID |
|
|||
|
|
| status | ENUM('pending','paid','failed','refunded') | |
|
|||
|
|
| paid_at | DATETIME NULL | |
|
|||
|
|
| created_at | DATETIME | |
|
|||
|
|
|
|||
|
|
## 6. API 接口设计
|
|||
|
|
|
|||
|
|
### 6.1 微信侧
|
|||
|
|
|
|||
|
|
**GET /wechat**
|
|||
|
|
微信服务器 URL 验证。校验 signature,返回 echostr。
|
|||
|
|
|
|||
|
|
**POST /wechat**
|
|||
|
|
接收微信事件推送,解析 XML:
|
|||
|
|
|
|||
|
|
- `subscribe` 事件:EventKey 形如 `qrscene_<scene_str>`
|
|||
|
|
- `SCAN` 事件:EventKey 直接为 `<scene_str>`
|
|||
|
|
|
|||
|
|
处理逻辑见第 7 节。
|
|||
|
|
|
|||
|
|
### 6.2 MFC 侧
|
|||
|
|
|
|||
|
|
**POST /auth/create_scene**
|
|||
|
|
|
|||
|
|
请求:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "device_id": "设备唯一标识" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
响应:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"scene_str": "pc_xxxxx",
|
|||
|
|
"qr_url": "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=...",
|
|||
|
|
"expires_in": 300
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
说明:服务端生成唯一 scene_str,调用微信接口生成临时二维码,写入 auth_scenes,返回二维码图片 URL。
|
|||
|
|
|
|||
|
|
**GET /auth/status?scene=xxx**
|
|||
|
|
|
|||
|
|
响应:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"status": "pending | authorized | expired | need_purchase",
|
|||
|
|
"session_token": "当 status=authorized 时返回",
|
|||
|
|
"authorization": {
|
|||
|
|
"type": "time | points",
|
|||
|
|
"end_at": "2026-10-03T12:00:00",
|
|||
|
|
"remaining_points": 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**POST /usage/consume**
|
|||
|
|
|
|||
|
|
请求:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"session_token": "xxx",
|
|||
|
|
"device_id": "xxx"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
响应:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"ok": true,
|
|||
|
|
"authorization": {
|
|||
|
|
"type": "time",
|
|||
|
|
"end_at": "2026-10-03T12:00:00"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
失败时:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"ok": false,
|
|||
|
|
"reason": "expired | exhausted | invalid_token"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 核心业务流程
|
|||
|
|
|
|||
|
|
### 7.1 首次扫码授权
|
|||
|
|
|
|||
|
|
1. MFC 启动,检查本地 session_token,无则调用 `/auth/create_scene`
|
|||
|
|
2. MFC 显示二维码,每 2 秒轮询 `/auth/status`
|
|||
|
|
3. 用户微信扫码
|
|||
|
|
4. 微信推送事件到 `/wechat`,服务端解析 scene_str
|
|||
|
|
5. 服务端查找或创建 user(按 openid)
|
|||
|
|
6. 如果 `has_claimed_free = 0`:创建时间授权,`start_at = now()`,`end_at = now() + 7 天`,`source = free`,`status = active`,`has_claimed_free = 1`
|
|||
|
|
7. 如果 `has_claimed_free = 1`:读取该用户当前有效授权(active 状态)
|
|||
|
|
8. 将 auth_scenes.status 置为 `authorized`,绑定 user_id
|
|||
|
|
9. MFC 轮询到 authorized,拿到 session_token,关闭弹窗
|
|||
|
|
|
|||
|
|
### 7.2 再次扫码
|
|||
|
|
|
|||
|
|
触发条件:时间授权到期,或积分耗尽,或换设备。
|
|||
|
|
|
|||
|
|
流程同上,但服务端在步骤 6 时:
|
|||
|
|
|
|||
|
|
- 若无有效授权 → 返回 `need_purchase`
|
|||
|
|
- 若有有效授权 → 正常返回 authorized
|
|||
|
|
|
|||
|
|
### 7.3 每次使用扣减
|
|||
|
|
|
|||
|
|
1. MFC 调用 `/usage/consume`
|
|||
|
|
2. 服务端校验 session_token
|
|||
|
|
3. 查找该用户当前 active 授权
|
|||
|
|
4. 时间授权:检查 `now() < end_at`,未过期则记录 usage_logs,直接返回 ok
|
|||
|
|
5. 积分授权:事务内 `UPDATE authorizations SET remaining_points = remaining_points - 1, status = IF(remaining_points - 1 <= 0, 'exhausted', 'active') WHERE id = ? AND remaining_points > 0 AND status = 'active'`,影响行数 1 则记录 usage_logs 并返回 ok,否则返回 exhausted
|
|||
|
|
6. 时间授权到期或积分耗尽时,MFC 下次使用会收到失败,弹出二维码引导再次扫码
|
|||
|
|
|
|||
|
|
## 8. 授权规则
|
|||
|
|
|
|||
|
|
**互斥原则**:同一用户同一时刻最多只有一条 `status = active` 的授权。
|
|||
|
|
|
|||
|
|
**新授权下发规则**:
|
|||
|
|
|
|||
|
|
- 若用户当前无 active 授权 → 新授权直接 active
|
|||
|
|
- 若用户当前有 active 授权 → 新授权以 `status = pending` 保存,待当前授权到期或耗尽后,由定时任务或下次请求时激活
|
|||
|
|
|
|||
|
|
**免费授权**:仅限首次关注,每个 openid 一次,7 天时间授权。
|
|||
|
|
|
|||
|
|
**时间与积分不叠加**:用户同时拥有时间和积分授权时,以当前 active 的为准,另一条保持 pending。active 结束后自动切换。
|
|||
|
|
|
|||
|
|
## 9. 边界与异常处理
|
|||
|
|
|
|||
|
|
- scene_str 一次性:auth_scenes 被授权后 status 不再回到 pending
|
|||
|
|
- scene 过期:超过 expires_at 后 status 置为 expired,MFC 轮询返回 expired,提示刷新二维码
|
|||
|
|
- 重复扫码:同一 scene 被多次扫描,只处理第一次,后续忽略
|
|||
|
|
- 已关注用户扫码:必须处理 SCAN 事件(EventKey 无 qrscene_ 前缀)
|
|||
|
|
- 未关注用户扫码:处理 subscribe 事件(EventKey 有 qrscene_ 前缀)
|
|||
|
|
- 并发扣减:必须使用数据库事务,禁止应用层先读后写
|
|||
|
|
- session_token:随机生成,存服务端,有效期 24 小时,过期需重新扫码
|
|||
|
|
|
|||
|
|
## 10. 开发阶段划分
|
|||
|
|
|
|||
|
|
**阶段 1:最小闭环**
|
|||
|
|
|
|||
|
|
- 建库建表
|
|||
|
|
- 实现 `/wechat` 的 GET 验证与 POST 事件接收
|
|||
|
|
- 实现 `/auth/create_scene`、`/auth/status`
|
|||
|
|
- 实现首次关注赠送 7 天免费授权
|
|||
|
|
- 用 curl 或 Postman 模拟微信事件,验证状态流转
|
|||
|
|
|
|||
|
|
**阶段 2:扣减与再扫码**
|
|||
|
|
|
|||
|
|
- 实现 `/usage/consume`
|
|||
|
|
- 实现时间到期与积分耗尽后的再次扫码流程
|
|||
|
|
- 实现 pending 授权自动激活
|
|||
|
|
|
|||
|
|
**阶段 3:充值**
|
|||
|
|
|
|||
|
|
- 实现 orders 表与卡密兑换接口
|
|||
|
|
- 后续接入微信支付 Native 扫码
|
|||
|
|
|
|||
|
|
## 11. 验收标准
|
|||
|
|
|
|||
|
|
- 首次扫码关注后,MFC 能收到 authorized 并正常使用
|
|||
|
|
- 7 天后再次使用,MFC 收到 expired,弹出二维码
|
|||
|
|
- 已关注用户再次扫码,服务端能收到 SCAN 事件并正确识别
|
|||
|
|
- 积分扣减在并发请求下不会出现负数
|
|||
|
|
- scene_str 一次性,重复扫描不产生副作用
|
|||
|
|
- 所有授权判断均在服务端完成,MFC 本地无法伪造
|
|||
|
|
|
|||
|
|
## 12. 待确认
|
|||
|
|
|
|||
|
|
- 用户充值获得积分后,若当前处于时间授权 active 状态,积分授权是否进入 pending 等待(建议:是)
|