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

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

144 lines
10 KiB
Markdown
Raw Permalink 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.
---
name: 生产服务器部署环境
description: ethereal-realm.top 的部署拓扑、自建 Git 代码服务器(47.93.46.28:1216,已弃用 GitHub)、MySQL/nginx 实际配置与几个反直觉的坑(pypi 不可达、MySQL 非 Docker、skip_name_resolve、nginx 只代理部分路径)
type: project
---
生产服务器 `deploy@ethereal-realm.top`(Alibaba Cloud Linux 3,公网 `39.107.55.179`,内网 `172.17.151.49`)的实际环境,与 REQUIREMENTS.md 的描述有出入。
**部署拓扑**
- 应用代码:`/home/deploy/app`(git 仓库,remote 指向自建 Git 服务器,见下)
- 虚拟环境:`/home/deploy/venv`(Python 3.11),**不是** 项目目录下的 venv
- 服务:systemd `wechat-api.service`(`/etc/systemd/system/`),`ExecStart=/home/deploy/venv/bin/python /home/deploy/app/run_server.py`,`Restart=always`,监听 `127.0.0.1:8000`
- 入口:systemd `cloudflared-quick.service`(`cloudflared tunnel --url http://127.0.0.1:80`,
快速隧道,无配置文件、无路径限制,`Restart=always`,`User=deploy`)→ nginx:80 → 应用。
**注意服务名是 `cloudflared-quick` 不是 `cloudflared`**——查 journal 时用错名字会显示空,误判成裸进程。
- nginx 配置:`/etc/nginx/conf.d/wechat-api.conf`,`server_name ethereal-realm.top`
- `deploy` 在 `wheel` 组,`sudo` 需要密码 → 涉及 sudo 的操作必须让用户手动执行
**代码仓库拓扑(2026-09-27 起)**
用户自建了一台 Linux 代码服务器 `gjm@47.93.46.28:1216`(CentOS 7.2,git 1.8.3.1),
裸仓库在 `/srv/git/wx-scan-authorize.git`(owner `gjm`)。
```
本地 d:/wx-scan-authorize ──push──> 代码服务器 47.93.46.28 (唯一远端) ──pull──> 应用服务器 ~/app
```
- 本地:只有一个远端 `origin` = 代码服务器。`git push` / `git pull` 不带参数即可。
- 应用服务器:`origin`=`ssh://gjm@47.93.46.28:1216/srv/git/wx-scan-authorize.git`,
直接 `git pull`。两端读写都已验证。
- **GitHub 已弃用**(2026-09-27 用户决定):国内访问不稳定,不再作备份。本地 remote
已删除,`origin` 这个名字改指代码服务器。GitHub 上那个仓库(`LittleGuo/wx-scan-authorize`)
是否删除由用户决定。
- 代码服务器上 `gjm` **没有免密 sudo**,涉及 root 的操作(装软件等)要让用户做。
- 该裸仓库 `HEAD` 曾误指 `refs/heads/master`(`git init --bare` 默认值),已改为
`refs/heads/main`。以后新建裸仓库记得顺手改,否则 clone 会拿到空工作区。
**域名被阿里云 ICP 拦截(2026-09-27 确认)**
> 用户已于 2026-09-27 提交 ICP 备案,预计不久下来。因此**决定不搞 Cloudflare 命名隧道**
> (那需要账号 + 把 ethereal-realm.top 的 NS 改到 Cloudflare,代价不值)。隧道只是过渡,
> 备案通过后应把公众号后台 URL 改回 `http://ethereal-realm.top/wechat` 并撤掉隧道。
`http://ethereal-realm.top` 在公网访问会返回阿里云的拦截页
(403 `Non-compliance ICP Filing`),因为备案还没做。**直连公网 IP
`http://39.107.55.179` 一切正常**(nginx 的 `/auth/`、`/usage/` 都能打到应用)。
影响面:
- 微信服务器的回调推送也进不来 —— 公众号后台「服务器配置」URL 若指向该域名,
`subscribe`/`SCAN` 事件永远到不了应用(journal 里完全没有 `/wechat` POST)。
- 可用的公网入口是 Cloudflare **快速隧道**(`cloudflared-quick.service`,已 enabled +
`Restart=always`,所以 SSH 断开/进程崩溃/服务器重启都能自动拉起)。隧道 URL 形如
`https://<随机词>-<随机词>-<随机词>-<随机词>.trycloudflare.com`,**每次重启都会变**
(这是快速隧道的固有行为,不是配置问题),捞当前值:
`journalctl -u cloudflared-quick | grep -oE "https://[a-z0-9-]+\.trycloudflare\.com" | tail -1`。
公众号后台的服务器配置要填 `<隧道URL>/wechat`。若推送突然不来了,先查这个 URL 是否变过。
**微信真实推送已打通(2026-09-27 19:12 验证)**
绕开 ICP 的办法:把测试号后台的服务器配置 URL 指向 Cloudflare 快速隧道,
`https://catering-thumbnails-pirates-workstation.trycloudflare.com/wechat`(Token 不变)。
微信随即发来 GET 校验(来源 `162.62.81.123`,腾讯云网段),签名复算一致、返回 200,
后台配置成功。之后真实扫码产生了第一条**非模拟**推送:
```
162.62.80.57 - "POST /wechat?signature=...&timestamp=...&nonce=...&openid=ohve625WiuQVosBDGLVwxGT68S4c" 200 OK
```
整条链路结果(库内):新建 `users.id=2`(真实 openid `ohve625WiuQVosBDGLVwxGT68S4c`),
发 7 天免费时间授权(`authorizations.id=2`,`2026-09-27 19:12:43 → 2026-10-04 19:12:43`),
`auth_scenes.id=9` 置 authorized,`usage_logs.id=2` 记账一条。
- **微信推送 URL 里会带一个 `openid=` 查询参数**(官方文档没写)。我们忽略它——
`verify_signature()` 只用 token/timestamp/nonce,业务用 XML 正文的 `FromUserName`。
已确认代码里没有任何地方往 query 拼 openid,是微信自己加的。
- 隧道**已经是 systemd 服务** `cloudflared-quick.service`(enabled + `Restart=always`),
服务器重启/进程崩溃都会自动拉起,公众号后台配置不会因进程死掉而失效。
但快速隧道 URL **每次重启仍会变**——所以重启后仍要重新捞 URL 并回填公众号后台。
- 库里的早期手工测试数据(`users.id=1 / openid='oTEST_CLIENT_01'`,其
`authorizations.id=1` 的 `end_at` 早于 `start_at`)**已于 2026-09-27 20:19 清理干净**,
连同 9 条无归属的过期测试场景。清理前整库备份在
`/home/deploy/backup-cleanup-20260927-201903.sql`(11 KB)。
现在库里只剩真实数据:`users.id=2`(openid `ohve625WiuQVosBDGLVwxGT68S4c`)。
- **`end_at` 倒挂不是 bug**(这条结论仍然成立):生产代码路径
(`_grant_free_authorization`)在同一条 INSERT 里取 `NOW()` 算 start/end,不可能倒挂。
- **清理用户数据时注意 `auth_scenes` 的删除规则是 `SET NULL` 不是 `CASCADE`**
(`authorizations`/`usage_logs`/`sessions` 才是 CASCADE)。直接删 `users` 会在
`auth_scenes` 留下一条 `user_id=NULL` 的孤儿记录,状态可能还是 `authorized`——
客户端轮询时可能读到。**正确顺序是先删该用户的 `auth_scenes`,再删 `users`**。
**几个反直觉的坑**
1. **服务器连不上 pypi.org**(超时),但阿里云镜像正常(0.06s)。装包必须加
`-i https://mirrors.aliyun.com/pypi/simple/`,否则 pip 会长时间挂住。
2. **MySQL 是原生安装的 8.0.46,不是 REQUIREMENTS 里写的 Docker**。且
`skip_name_resolve=ON` + `bind_address=127.0.0.1`,导致 `root@localhost`
匹配不了 TCP 连接(报 `ERROR 1130 Host '127.0.0.1' is not allowed`)。
应用用专用账号 `wechat@127.0.0.1`(只授权 `wechat_api.*`),凭据在服务器 `.env` 里。
注意:本机 Windows 的 `.env` 仍是 `MYSQL_USER=root`,连的是本机 MySQL,两份不一样。
3. **nginx 有两个 server 块,访问行为取决于 Host 头**(2026-09-27 发现,很反直觉):
| 入口 | 匹配的 server 块 | 行为 |
|---|---|---|
| `Host: ethereal-realm.top` | `wechat-api.conf` | **精确白名单**:只放行 `/wechat`、`/auth/`、`/usage/`、`/admin`,其余落到 `location /` 的占位响应 `return 200 'wechat api ok'` |
| `Host: 39.107.55.179`(IP 直连)| `app.conf` | **全量代理**:`listen 80 default_server` + `location / { proxy_pass http://fastapi; }`,所有路径都转发到 8000 |
`app.conf` 是 Sep 25 就存在的早期配置(不是本次部署引入)。后果:
- **IP 直连会暴露 FastAPI 的 `/docs` 与 `/openapi.json`**(实测 200 + 真实 schema);
域名路径下这两个路径返回占位文本,不暴露。
- MFC 插件当前 `-DCADOCR_WX_BASE_URL=http://39.107.55.179` 正是依赖 IP 直连,
所以**不能简单删掉 `app.conf`**(删了 IP 访问会落到 nginx.conf 内置的静态
server,`/auth/`、`/usage/` 全部 404)。要收紧的话,应把 `wechat-api.conf`
改成 `listen 80 default_server` 并删 `app.conf`,而不是单独删。
- 新增对外接口必须**同时**在 `wechat-api.conf` 加 `location` 块(给域名路径),
否则备案后走域名会落到占位响应。
- 改 nginx 的流程见 `user_shell_workflow.md`:**写成脚本 + `sudo bash ~/xxx.sh`**,
不要给内联长命令。
- **`/docs` 暴露问题用户 2026-09-27 决定暂不处理**(原话选了"暂不处理"):判断是
风险低(只泄露接口结构、不含数据,`/admin` 另有强密码),等 ICP 备案通过、插件
切回域名后再一并处理。**不要再自作主张去关 docs 或删 `app.conf`**——要动先问。
**`/admin` 已于 2026-09-27 20:13 上线**:`wechat-api.conf` 新增 `location /admin`
块(显式透传 `Authorization` 头),应用侧 `ADMIN_USER=admin`、密码为 24 位随机串
存于服务器 `.env`(用户已自行保存)。`/admin` 需重启应用才生效——`ADMIN_PASSWORD`
是进程启动时读入的模块级常量。
4. **GitHub 已弃用,不要再往那儿推**(2026-09-27 用户决定)。服务器到 github.com 的连接
本就间歇性失败(`git pull` 报 `Empty reply from server` 或超时;`curl -sI` 探到 200
也**不代表** git 协议可用,实测出现过 curl 200 而 pull 仍失败)。国内访问不稳定,
用户明确不要 GitHub 备份了。所有代码走自建 Git 服务器。
若哪天需要从 GitHub 临时取东西(例如查旧仓库),仍可用 bundle 绕:
本地 `git bundle create /tmp/x.bundle main`,
`ssh deploy@... 'cat > /tmp/x.bundle' < /tmp/x.bundle`,服务器上
`git fetch /tmp/x.bundle main && git merge --ff-only FETCH_HEAD`。
**Why:** 这些都是服务器实际状态,仓库代码和 git 历史里看不出来;REQUIREMENTS.md 的
技术栈描述(Docker MySQL)与实际不符,照着文档操作会踩坑。
**How to apply:** 涉及服务器操作时,直接用上面的真实路径/账号/镜像;给用户的命令要
考虑他需要手动 sudo;新增对外路由时提醒同步改 nginx。