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

10 KiB
Raw Permalink Blame History

name, description, type
name description type
生产服务器部署环境 ethereal-realm.top 的部署拓扑、自建 Git 代码服务器(47.93.46.28:1216,已弃用 GitHub)、MySQL/nginx 实际配置与几个反直觉的坑(pypi 不可达、MySQL 非 Docker、skip_name_resolve、nginx 只代理部分路径) 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。