# 微信扫码授权服务 — 需求文档 ## 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_` - `SCAN` 事件:EventKey 直接为 `` 处理逻辑见第 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 等待(建议:是)