fix: 使用请求 Host 头动态构建 notify_url + 迁移文档/测试
Some checks failed
Deploy Backend / deploy (push) Has been cancelled

根因: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 一致,迁移后自动捕获配置错误
This commit is contained in:
2026-07-23 12:09:52 +08:00
parent 65157475cb
commit 0b0757939b
3 changed files with 168 additions and 34 deletions

View File

@@ -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 配置 |

View File

@@ -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[@]}

View File

@@ -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<String> {
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);