Files
wechat-scan/REQUIREMENTS.md
T

286 lines
8.6 KiB
Markdown
Raw Normal View History

# 微信扫码授权服务 — 需求文档
## 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 等待(建议:是)