- 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
8.6 KiB
8.6 KiB
微信扫码授权服务 — 需求文档
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
请求:
{ "device_id": "设备唯一标识" }
响应:
{
"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
响应:
{
"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
请求:
{
"session_token": "xxx",
"device_id": "xxx"
}
响应:
{
"ok": true,
"authorization": {
"type": "time",
"end_at": "2026-10-03T12:00:00"
}
}
失败时:
{
"ok": false,
"reason": "expired | exhausted | invalid_token"
}
7. 核心业务流程
7.1 首次扫码授权
- MFC 启动,检查本地 session_token,无则调用
/auth/create_scene - MFC 显示二维码,每 2 秒轮询
/auth/status - 用户微信扫码
- 微信推送事件到
/wechat,服务端解析 scene_str - 服务端查找或创建 user(按 openid)
- 如果
has_claimed_free = 0:创建时间授权,start_at = now(),end_at = now() + 7 天,source = free,status = active,has_claimed_free = 1 - 如果
has_claimed_free = 1:读取该用户当前有效授权(active 状态) - 将 auth_scenes.status 置为
authorized,绑定 user_id - MFC 轮询到 authorized,拿到 session_token,关闭弹窗
7.2 再次扫码
触发条件:时间授权到期,或积分耗尽,或换设备。
流程同上,但服务端在步骤 6 时:
- 若无有效授权 → 返回
need_purchase - 若有有效授权 → 正常返回 authorized
7.3 每次使用扣减
- MFC 调用
/usage/consume - 服务端校验 session_token
- 查找该用户当前 active 授权
- 时间授权:检查
now() < end_at,未过期则记录 usage_logs,直接返回 ok - 积分授权:事务内
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 - 时间授权到期或积分耗尽时,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 等待(建议:是)