fix: 使用请求 Host 头动态构建 notify_url + 迁移文档/测试
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
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:
@@ -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 配置 |
|
||||
|
||||
39
lib/test.sh
39
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[@]}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user