From 0b0757939bf2a57d3f153db1108a9e558169e13a Mon Sep 17 00:00:00 2001 From: milky0217 Date: Thu, 23 Jul 2026 12:09:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E8=AF=B7=E6=B1=82=20H?= =?UTF-8?q?ost=20=E5=A4=B4=E5=8A=A8=E6=80=81=E6=9E=84=E5=BB=BA=20notify=5F?= =?UTF-8?q?url=20+=20=E8=BF=81=E7=A7=BB=E6=96=87=E6=A1=A3/=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:base_url 默认硬编码为 dev.xmclassmate.top,导致支付宝通知回调 (notify_url)指向开发服务器,RSA2 签名验证失败,订单延迟 12 分钟。 修复: - 新增 get_base_url():优先用 APP_BASE_URL 环境变量,否则从请求 Host 头获取当前域名,确保 notify_url 始终指向正确的服务器 - 生产 .env 已设置 APP_BASE_URL=https://xmclassmate.top - .env.example 新增 APP_BASE_URL 文档 迁移文档: - SERVER-MIGRATION-PLAN.md 新增阶段 3 关键配置章节 - 12 项迁移检查清单 + 历史问题复盘 部署测试: - test.sh 新增 test_notify_url_domain:校验支付表单 notify_url 域名与 BASE_URL 一致,迁移后自动捕获配置错误 --- docs/SERVER-MIGRATION-PLAN.md | 143 +++++++++++++++++++++++++++------- lib/test.sh | 39 ++++++++++ src/handlers/payment.rs | 20 ++++- 3 files changed, 168 insertions(+), 34 deletions(-) diff --git a/docs/SERVER-MIGRATION-PLAN.md b/docs/SERVER-MIGRATION-PLAN.md index 63c777a..0751698 100644 --- a/docs/SERVER-MIGRATION-PLAN.md +++ b/docs/SERVER-MIGRATION-PLAN.md @@ -2,12 +2,12 @@ ## 新旧对比 -| 项目 | 旧服务器 (1panel-server) | 新服务器 (47.109.203.92) | -|------|------------------------|-------------------------| -| 系统 | - | Debian 13.5 | -| CPU | - | 2 核 | -| 内存 | - | 1.6 GiB | -| 磁盘 | - | 40G (可用 36G) | +| 项目 | 旧服务器 (1panel-server) | 新服务器 (xxx.xxx.xxx.xxx) | +|------|------------------------|---------------------------| +| 系统 | - | 待填写 | +| CPU | - | 待填写 | +| 内存 | - | 待填写 | +| 磁盘 | - | 待填写 | | 区域 | - | 成都 | | 角色 | 当前生产+开发 | 目标生产+开发 | @@ -36,64 +36,147 @@ pg_dump -Fc milkydata > /tmp/milkydata.dump pg_restore -d milkydata /tmp/milkydata.dump ``` -### 1.4 启动 OpenResty +### 1.4 安装并配置 nginx ```bash -docker run -d --name openresty \ - --network host \ - -v /www/sites:/www/sites \ - 1panel/openresty:1.21.4.3-3-3-focal +apt install -y nginx ``` +复制 nginx 配置文件和 SSL 证书。 ### 1.5 复制 SSL 证书 + nginx 配置 ```bash -rsync -av root@1panel-server:/www/sites/ /www/sites/ +rsync -av root@old-server:/etc/nginx/sites-available/ /etc/nginx/sites-available/ +rsync -av root@old-server:/etc/nginx/sites-enabled/ /etc/nginx/sites-enabled/ +rsync -av root@old-server:/etc/ssl/ /etc/ssl/ ``` ## 阶段 2 — 后端部署 ### 2.1 创建 systemd 服务(blue/green/dev) ```bash -# 创建目录 mkdir -p /root/rust/rust_backend_blue mkdir -p /root/rust/rust_backend_green mkdir -p /root/rust/rust_backend_dev - -# 注册 systemd service(从 deploy/ 目录上传) ``` ### 2.2 部署 Rust 后端 ```bash -# 先部署到 dev 验证 cd ASD-backend/rust-backend -deploy.sh development --remote-host 47.109.203.92 - -# 再部署 blue/green -deploy.sh production --remote-host 47.109.203.92 +deploy.sh development --remote-host xxx.xxx.xxx.xxx +deploy.sh production --remote-host xxx.xxx.xxx.xxx ``` ### 2.3 更新 deploy.sh -将默认 REMOTE_HOST 改为新服务器 IP +将默认 REMOTE_HOST 改为新服务器 IP。 -## 阶段 3 — 切换 +## 阶段 3 — ⚠️ 关键配置(迁移后必做) + +### 3.1 设置 APP_BASE_URL + +每个环境的 `.env` 文件必须设置 `APP_BASE_URL`,否则默认值为 `https://dev.xmclassmate.top`,导致支付宝通知回调发错服务器。 -### 3.1 内部验证 ```bash -curl http://127.0.0.1:8080/health # dev -curl -k https://127.0.0.1:4433/health # blue -curl -k https://127.0.0.1:4434/health # green +# 生产环境 +echo "APP_BASE_URL=https://xmclassmate.top" >> /root/rust/rust_backend_blue/.env +echo "APP_BASE_URL=https://xmclassmate.top" >> /root/rust/rust_backend_green/.env + +# 开发环境 +echo "APP_BASE_URL=https://dev.xmclassmate.top" >> /root/rust/rust_backend_dev/.env ``` -### 3.2 更新 DNS -将域名指向新服务器 IP +### 3.2 验证 notify_url -### 3.3 验证公网访问 +迁移后通过 API 检查支付表单的 notify_url 是否正确: + +```bash +# 获取访客 JWT +JWT=$(curl -s -X POST 'https://YOUR_DOMAIN/api/guest-login' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") + +# 检查支付表单的 notify_url +curl -s "https://YOUR_DOMAIN/payment/page?package=monthly&jwt=${JWT}" | grep -oP 'notify_url[^"]+"[^"]+' | head -1 +``` + +预期输出:`notify_url" value="https://YOUR_DOMAIN/payment/notify"` + +### 3.3 验证支付宝密钥匹配 + +```bash +# 确认密钥配置正确(生产用正式密钥,开发用沙箱密钥) +ssh root@NEW_SERVER 'grep "ALIPAY_APP_ID\|ALIPAY_GATEWAY" /root/rust/rust_backend_*/env' +``` + +| 环境 | APP_ID 前缀 | GATEWAY | +|------|------------|---------| +| 生产 | 202100... | `openapi.alipay.com` | +| 开发 | 902100...(沙箱) | `openapi-sandbox.dl.alipaydev.com` | + +### 3.4 验证关键端点 + +```bash +# 支付页面可访问(非 503) +curl -s -o /dev/null -w "%{http_code}" https://YOUR_DOMAIN/payment + +# 访客登录正常 +curl -s -X POST https://YOUR_DOMAIN/api/guest-login | python3 -c "import sys,json; print(json.load(sys.stdin).get('success','FAIL'))" + +# 健康检查 +curl -s https://YOUR_DOMAIN/health | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','FAIL'))" +``` + +### 3.5 迁移后支付全链路测试 + +```bash +bash tests/payment_e2e_test.sh +``` + +### 3.6 运行部署后测试 + +```bash +export TEST_DOMAIN="https://YOUR_DOMAIN" +./deploy.sh production --skip-tests=false +# 或手动运行 +export BASE_URL="https://YOUR_DOMAIN" +bash lib/test.sh +``` + +## 阶段 4 — DNS 切换 + +### 4.1 更新 DNS 记录 +将域名指向新服务器 IP。 + +### 4.2 验证公网访问 ```bash curl https://dev.xmclassmate.top/health curl https://xmclassmate.top/health +curl https://xmclassmate.top/payment ``` -## 阶段 4 — 清理(可选) +## 阶段 5 — 清理 - 迁移 GlitchTip (Sentry) - 旧服务器保留 1 周作为回退 - 确认无问题后关闭旧服务器 + +## 迁移检查清单 + +| # | 检查项 | 命令/方法 | 状态 | +|---|--------|----------|------| +| 1 | PostgreSQL 运行 | `docker ps \| grep postgres` | ⬜ | +| 2 | 数据库导入 | `pg_restore -d milkydata /tmp/xxx.dump` | ⬜ | +| 3 | nginx 配置正确 | `nginx -t` | ⬜ | +| 4 | SSL 证书有效 | `openssl s_client -connect DOMAIN:443` | ⬜ | +| 5 | 后端服务运行 | `systemctl status rust-backend-*` | ⬜ | +| 6 | APP_BASE_URL 已设置 | `grep APP_BASE_URL /root/rust/rust_backend_*/.env` | ⬜ | +| 7 | 支付宝密钥环境正确 | `grep ALIPAY_APP_ID /root/rust/rust_backend_*/.env` | ⬜ | +| 8 | notify_url 正确 | 见 3.2 验证 | ⬜ | +| 9 | 支付页面可访问 | `curl DOMAIN/payment` | ⬜ | +| 10 | 访客登录正常 | `curl -X POST DOMAIN/api/guest-login` | ⬜ | +| 11 | 全链路测试通过 | `bash tests/payment_e2e_test.sh` | ⬜ | +| 12 | 部署后测试通过 | `deploy.sh production` | ⬜ | + +## 历史问题复盘 + +| 日期 | 问题 | 根因 | 预防 | +|------|------|------|------| +| 2026-07-23 | 支付回调丢失 | `APP_BASE_URL` 未设置,默认指向 `dev.xmclassmate.top`,开发服务器无法通过 RSA2 验证 | 迁移清单第 6、8 项确保 base_url 配置正确 | +| 2026-07-18 | 保存功能 500 | nginx AppArmor 缺少 `/var/lib/nginx/** rw` | 部署后测试 `test_nginx_proxy_temp` | +| 2026-07-13 | ICP 备案号未显示 | nginx 配置路径不匹配(`/www/sites/` vs `/etc/nginx/sites-enabled/`) | 迁移清单第 3 项检查 nginx 配置 | diff --git a/lib/test.sh b/lib/test.sh index d34c050..a04d8b8 100644 --- a/lib/test.sh +++ b/lib/test.sh @@ -471,6 +471,44 @@ test_nginx_proxy_temp() { return 1 } +# ---------- 支付 notify_url 校验 ---------- + +test_notify_url_domain() { + log_info "校验支付表单 notify_url 域名..." + local domain + domain=$(echo "$BASE_URL" | sed 's|https://||' | sed 's|http://||' | sed 's|/||g') + + # 获取访客 JWT + local jwt + jwt=$(remote "curl -sk -X POST '${BASE_URL}/api/guest-login'" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null) + if [ -z "$jwt" ]; then + log_error "获取 JWT 失败,无法校验 notify_url" + return 1 + fi + + # 提取支付表单的 notify_url + local notify_url + notify_url=$(remote "curl -sk '${BASE_URL}/payment/page?package=monthly&jwt=${jwt}'" 2>/dev/null | grep -oP 'notify_url" value="\K[^"]+' | head -1) + + if [ -z "$notify_url" ]; then + log_error "无法提取 notify_url(可能为 Mock 支付模式)" + return 0 # Mock 模式下不校验 + fi + + # 检查 notify_url 的域名是否与 BASE_URL 一致 + local notify_domain + notify_domain=$(echo "$notify_url" | sed 's|https://||' | sed 's|http://||' | sed 's|/payment/notify||') + + if [ "$notify_domain" != "$domain" ]; then + log_error "notify_url 域名不匹配: 表单=${notify_domain}, 期望=${domain}" + log_error "请检查 APP_BASE_URL 配置" + return 1 + fi + + log_info " notify_url 域名 ${notify_domain} ✅" + return 0 +} + # ---------- 主测试入口 ---------- run_tests() { log_step "执行部署后测试..." @@ -493,6 +531,7 @@ run_tests() { "test_payment_maintenance:维护模式" "test_payment_refund:退款+会员撤销" "test_payment_verify_detection:验证函数检测逻辑" + "test_notify_url_domain:支付回调域名校验" ) local i=1 total=${#tests[@]} diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index 8f280ff..cf41fb6 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -272,6 +272,20 @@ fn check_payment_maintenance() -> Result<(), AppError> { Ok(()) } +/// 构建基础 URL:优先使用 APP_BASE_URL 环境变量,否则从请求 Host 头获取 +/// 确保支付宝通知回调(notify_url)正确指向当前处理请求的服务器 +fn get_base_url(req: &HttpRequest) -> String { + if let Ok(url) = std::env::var("APP_BASE_URL") { + return url; + } + let host = req + .headers() + .get("Host") + .and_then(|v| v.to_str().ok()) + .unwrap_or("xmclassmate.top"); + format!("https://{}", host) +} + fn extract_token(req: &HttpRequest) -> Option { req.headers() .get("Authorization")? @@ -783,8 +797,7 @@ pub async fn payment_page( } }; - let base_url = std::env::var("APP_BASE_URL") - .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string()); + let base_url = get_base_url(&req); let notify_url = format!("{}/payment/notify", base_url); let return_url = format!("{}/payment/success?order_no={}&jwt={}", base_url, order_no, token); @@ -848,8 +861,7 @@ pub async fn alipay_pay_page( let pkg = get_package_info(&query.package_type) .ok_or_else(|| AppError::BadRequest("无效的套餐类型".to_string()))?; - let base_url = std::env::var("APP_BASE_URL") - .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string()); + let base_url = get_base_url(&req); let notify_url = format!("{}/payment/notify", base_url); let return_url = format!("{}/payment/success?order_no={}", base_url, query.order_no);