Files
wechat-scan/REQUIREMENTS.md
T
gjm 3790821ce0 feat: 阶段 1 最小闭环 - 建表 SQL、/wechat 事件、/auth 接口、首次关注免费授权
- sql/schema.sql: users/authorizations/auth_scenes/usage_logs + sessions 建表
- db.py: aiomysql 连接池,lifespan 内初始化与释放
- wechat_api.py: access_token 缓存 + 临时二维码创建
- auth.py: /auth/create_scene、/auth/status,handle_scan 事务内幂等处理扫码
- wechat.py: 接入 DB 生命周期,处理 subscribe/SCAN 事件;移除多余的 openid query 参数
- 首次关注赠送 7 天免费授权,has_claimed_free 条件更新保证幂等
- config.py/.env.example: 新增 FREE_AUTH_DAYS/SCENE_TTL_SECONDS/SESSION_TTL_HOURS
2026-09-26 22:25:02 +08:00

287 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 微信扫码授权服务 — 需求文档
## 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 等待(建议:是)