From 3a8adda365cfe17b396cf0a47fe3204caf456118 Mon Sep 17 00:00:00 2001 From: LittleGuo Date: Sun, 27 Sep 2026 15:18:43 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20CODEBUDDY.md=20?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E7=AB=A0=E8=8A=82=E8=87=B3=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=202=20=E7=8E=B0=E7=8A=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 项目概述补充 /auth、/usage 与 MySQL 已接入 - 架构补齐 db.py / wechat_api.py / auth.py / usage.py / sql/schema.sql - 新增「授权与扣减状态机」与「数据库」两节:互斥原则、惰性激活、 免费授权幂等、积分扣减的 SET 求值顺序、session 校验、日志节流 - 约定补充:新增对外路由需同步加 Nginx location 块 --- CODEBUDDY.md | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/CODEBUDDY.md b/CODEBUDDY.md index ef27403..1c885af 100644 --- a/CODEBUDDY.md +++ b/CODEBUDDY.md @@ -4,7 +4,7 @@ This file provides guidance to CodeBuddy Code when working with code in this rep ## 项目概述 -微信公众号扫码授权服务,基于 FastAPI。当前实现微信服务器验证(GET /wechat)与消息/事件接收(POST /wechat)。MySQL 与 Redis 已在配置层声明,但业务代码尚未使用。 +微信公众号扫码授权服务,基于 FastAPI,为 Windows MFC 桌面程序提供微信扫码授权与使用扣减。已实现微信服务器验证与消息/事件接收(`/wechat`)、扫码授权(`/auth`)、使用扣减(`/usage`)以及首次关注赠送 7 天免费授权。MySQL 已接入(aiomysql 连接池,5 张表);Redis 仅在配置层声明,尚未使用。 ## 常用命令 @@ -12,6 +12,9 @@ This file provides guidance to CodeBuddy Code when working with code in this rep # 安装依赖(建议先创建/激活虚拟环境 venv) pip install -r requirements.txt +# 建库建表(MySQL 8,创建 wechat_api 库与 5 张表) +mysql -u root -p < sql/schema.sql + # 本地开发(uvicorn reload,监听 127.0.0.1:8000) python run_local.py # 等价于: @@ -29,21 +32,46 @@ cp .env.example .env # 然后填入真实值 ## 架构 ``` -config.py # 从 .env 读取配置(dotenv),模块级常量 -wechat.py # FastAPI app 本体:路由 + 签名校验 + XML 解析/构造 -run_local.py # 开发启动入口(reload=True) -run_server.py # 生产启动入口(reload=False, workers=1) +config.py # 从 .env 读取配置(dotenv),模块级常量 +db.py # aiomysql 连接池:init_pool / close_pool / acquire(autocommit) +wechat_api.py # 微信开放接口:access_token 内存缓存 + 临时二维码创建 +wechat.py # FastAPI app 本体:lifespan + /wechat 路由 + 签名校验 + XML 解析/构造 +auth.py # /auth/* 路由 + 扫码授权业务逻辑(含 pending 激活) +usage.py # /usage/consume 路由:会话校验与使用扣减 +run_local.py # 开发启动入口(reload=True) +run_server.py # 生产启动入口(reload=False, workers=1) +sql/schema.sql # 建表脚本(5 张表,含索引与外键) ``` - **配置**:所有敏感值经 `config.py` 从 `.env` 读取,`.env` 已被 `.gitignore` 排除。`.env.example` 是字段模板。新增配置项需同时更新这两处。 -- **应用入口**:两个启动脚本均以 `"wechat:app"` 字符串形式加载 `wechat.py` 中的 `app`,因此模块名/对象名不可随意重命名。 +- **应用入口**:两个启动脚本均以 `"wechat:app"` 字符串形式加载 `wechat.py` 中的 `app`,因此模块名/对象名不可随意重命名。`lifespan` 在启动时初始化 MySQL 连接池——**MySQL 不可达或库表不存在时应用会直接启动失败**。 +- **路由挂载**:`wechat.py` 通过 `include_router` 挂载 `auth.router` 与 `usage.router`。新增 MFC 侧接口应新建独立模块的 router,而不是塞进 `wechat.py`。 - **微信交互协议**: - 所有请求先经 `verify_signature()`(token+timestamp+nonce 字典序拼接后 SHA1 比对)校验,失败返回 403。 - GET 校验通过后原样返回 `echostr`。 - POST 解析微信推送的 XML(`MsgType`/`FromUserName`/`Event` 等),通过 `_reply_text()` 构造文本回复 XML 返回。新增消息类型处理应在 `wechat_message()` 的事件/消息分支中扩展。 + - `subscribe` 事件 EventKey 形如 `qrscene_`,`SCAN` 事件 EventKey 直接是 ``,由 `_parse_scene_key()` 统一提取。 - **注意**:`_reply_text()` 中 ToUserName/FromUserName 是反置的(回复时收发方互换),这是微信协议要求。 +## 授权与扣减状态机 + +- **互斥原则**:同一用户同一时刻最多一条 `status = 'active'` 的授权。 +- **惰性激活**:`auth.activate_pending_authorization()` 先把已失效的 active 标记为 expired/exhausted,再无 active 时按 FIFO 激活一条 pending。调用点有三处:`/auth/status`、扫码事件 `handle_scan()`、`/usage/consume`。**必须在 `get_active_authorization()` 之前调用**(后者带惰性置失效的副作用)。时间授权激活时按原时长从当前时刻重新锚定。 +- **免费授权幂等**:靠 `UPDATE users SET has_claimed_free = 1 WHERE id = ? AND has_claimed_free = 0` 的 `rowcount == 1` 作闸门,只有把 0 改 1 的那一次才真正插入授权。 +- **积分扣减**:条件 `UPDATE ... WHERE id = ? AND status = 'active' AND remaining_points > 0` + `rowcount` 判定,禁止应用层先读后写。**`status` 的赋值必须写在 `remaining_points` 自减之前**——MySQL 的 SET 从左到右求值,否则 `IF` 会读到已减 1 的值,判空差 1。 +- **session_token**:随机生成、存 `sessions` 表、有效期 24 小时、绑定签发时的 `device_id`。`/usage/consume` 严格校验 token 存在、未过期且 `device_id` 一致,任一不符返回 `invalid_token`。 +- **usage_logs**:积分授权每次调用都写(计费凭证);时间授权按 `(user_id, device_id)` 在 `USAGE_LOG_THROTTLE_SECONDS`(默认 60 秒)窗口内节流。 +- **失败语义**:`/usage/consume` 一律返回 HTTP 200,用 `{"ok": false, "reason": "expired | exhausted | invalid_token"}` 表达失败。 + +## 数据库 + +5 张表(定义见 `sql/schema.sql`):`users`、`authorizations`(授权,type 分 time/points)、`auth_scenes`(扫码场景,300 秒一次性)、`usage_logs`(使用日志)、`sessions`(会话令牌)。 + +- `auth_scenes` 被授权后 status 不再回到 pending,重复扫码只处理第一次。 +- 外键:删除 user 会级联删除其 authorizations / usage_logs / sessions。 +- 场景过期、授权到期/耗尽的惰性标记在读取时完成,没有后台定时任务。 + ## 约定 - 代码注释与文档字符串使用中文。 -- 生产环境仅监听 127.0.0.1,对外暴露依赖 Nginx 反向代理。 +- 生产环境仅监听 127.0.0.1,对外暴露依赖 Nginx 反向代理。新增对外路由(如 `/auth/`、`/usage/`)时,必须同步在服务器 Nginx 配置里加对应的 `location` 块,否则公网请求会落到占位响应。