Files
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

8.6 KiB
Raw Permalink Blame 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

请求:

{ "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 首次扫码授权

  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 等待(建议:是)