Files
wechat-scan/CODEBUDDY.md
T
gjm 6c5770b754 chore: 新增 .ai-memory 记忆备份,换机器可恢复
CodeBuddy 的记忆原本只存在本机用户目录(不进版本控制),换机器即丢失。
把 5 个记忆文件备份进仓库,clone 后让助手「从 .ai-memory/ 导入记忆」即可恢复。

- README 说明用途、导入方法与维护约定
- CODEBUDDY.md 补充同步约定(改完记忆须同步本目录)
- 内容仅含基础设施信息(IP/域名/用户名),无任何密码或密钥
2026-09-27 20:27:45 +08:00

99 lines
7.1 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.
# CODEBUDDY.md
This file provides guidance to CodeBuddy Code when working with code in this repository.
## 项目概述
微信公众号扫码授权服务,基于 FastAPI,为 Windows MFC 桌面程序提供微信扫码授权与使用扣减。已实现微信服务器验证与消息/事件接收(`/wechat`)、扫码授权(`/auth`)、使用扣减(`/usage`)、只读管理后台(`/admin`)以及首次关注赠送 7 天免费授权。MySQL 已接入(aiomysql 连接池,5 张表);Redis 仅在配置层声明,尚未使用。
## 常用命令
```bash
# 安装依赖(建议先创建/激活虚拟环境 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
# 等价于:
uvicorn wechat:app --reload --host 127.0.0.1 --port 8000
# 服务器部署(不 reload,单 worker,监听 127.0.0.1:8000,由 Nginx 反代)
python run_server.py
# 初始化本地配置
cp .env.example .env # 然后填入真实值
```
当前仓库没有测试框架、lint 或构建配置;如需运行单个测试,需先引入 pytest 等工具。
## 架构
```
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 路由:会话校验与使用扣减
admin.py # /admin 只读管理后台:Basic Auth + 服务端渲染 HTML
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`,因此模块名/对象名不可随意重命名。`lifespan` 在启动时初始化 MySQL 连接池——**MySQL 不可达或库表不存在时应用会直接启动失败**。
- **路由挂载**:`wechat.py` 通过 `include_router` 挂载 `auth.router`、`usage.router` 与 `admin.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_<scene_str>`,`SCAN` 事件 EventKey 直接是 `<scene_str>`,由 `_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"}` 表达失败。
## 管理后台(/admin)
只读单页,用于查看用户与授权现状:概览统计、用户+当前授权、最近使用记录、会话令牌、扫码场景。
- **鉴权**:HTTP Basic Auth,凭据取自 `.env` 的 `ADMIN_USER` / `ADMIN_PASSWORD`,用 `secrets.compare_digest` 做定时安全比较。
- **`ADMIN_PASSWORD` 为空时 `/admin` 返回 503 而非放行**——不要把它当成可选项,否则等于把全库用户数据公开。
- **只提供 GET**,没有任何写操作(改授权/加积分/封号一律不做)。
- **不引模板引擎**:HTML 由 f-string 拼装,所有入库字段经 `html.escape()`;CSS 内联,不依赖任何 CDN(服务器出网不可靠)。
- 页脚会显示数据库名;页头显示数据生成时间。不做自动刷新。
- `admin.py` 被 `wechat.py` import,因此**不要在 `admin.py` 里反向 import `wechat`**(循环依赖)。
## 数据库
5 张表(定义见 `sql/schema.sql`):`users`、`authorizations`(授权,type 分 time/points)、`auth_scenes`(扫码场景,300 秒一次性)、`usage_logs`(使用日志)、`sessions`(会话令牌)。
- `auth_scenes` 被授权后 status 不再回到 pending,重复扫码只处理第一次。
- 外键:删除 user 会级联删除其 authorizations / usage_logs / sessions。
- 场景过期、授权到期/耗尽的惰性标记在读取时完成,没有后台定时任务。
## AI 记忆备份(.ai-memory/)
`.ai-memory/` 是 CodeBuddy 持久记忆的仓库副本——记忆本体存在本机
`~/.codebuddy/projects/<工作目录名>/memory/`,不进版本控制。
- **改完记忆必须同步**:把记忆目录的文件复制到 `.ai-memory/` 并提交,否则换机器会丢。
- 该目录**会进 Git**,因此**绝不可写入密码、token、私钥**——那些一律留在服务器 `.env`。
- 新环境恢复:clone 后让助手「从 `.ai-memory/` 导入记忆」。
## 约定
- 代码注释与文档字符串使用中文。
- 生产环境仅监听 127.0.0.1,对外暴露依赖 Nginx 反向代理。新增对外路由(如 `/auth/`、`/usage/`)时,必须同步在服务器 Nginx 配置里加对应的 `location` 块,否则公网请求会落到占位响应。