diff --git a/AGENTS.md b/AGENTS.md index f244e27..0ecc566 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -556,23 +556,36 @@ let new_expires = base_time + days(pkg_days); | 环境 | 域名 | 端口 | 远程目录 | |------|------|------|---------| | 测试 | dev.xmclassmate.top | 8080 | /root/rust/rust_backend_dev | -| 生产 | xmclassmate.top | 4433 | /root/rust/rust_backend | +| 生产(蓝) | xmclassmate.top | 4433 | /root/rust/rust_backend_blue | +| 生产(绿) | xmclassmate.top | 4434 | /root/rust/rust_backend_green | + +**服务器**:`47.109.203.92` (Aliyun, Debian 13) +**SSH 用户**:`deploy`(有 sudo 权限) +**SSH 别名**:`aliyun-server`(`~/.ssh/config` 中配置) +**nginx**:原生 nginx(非 Docker/OpenResty),配置位于 `/etc/nginx/sites-available/` +**PostgreSQL**:Docker 容器 `postgres:17.6-alpine`(host 网络模式) ### systemd 服务 -| 环境 | systemd unit | 工作目录 | APP_ENV | -|------|-------------|---------|---------| -| 测试 | `rust-backend-dev.service` | `/root/rust/rust_backend_dev` | `development` | -| 生产 | `rust-backend.service` | `/root/rust/rust_backend` | (使用 .env) | +| 环境 | systemd unit | 工作目录 | SERVER_PORT | +|------|-------------|---------|-------------| +| 测试 | `rust-backend-dev.service` | `/root/rust/rust_backend_dev` | 8080 | +| 生产蓝 | `rust-backend-blue.service` | `/root/rust/rust_backend_blue` | 4433 | +| 生产绿 | `rust-backend-green.service` | `/root/rust/rust_backend_green` | 4434 | -> ⚠️ systemd 配置使用 `EnvironmentFile=` 加载 `.env` 文件(生产、开发环境均如此) +> 生产采用蓝绿部署:nginx 通过 `proxy_pass` 将流量路由到活跃环境(默认 :4433 = 蓝色)。 +> `deploy.sh` 自动检测活跃环境并部署到空闲环境,部署成功后自动切换 nginx 代理。 + +> ⚠️ systemd 配置使用 `Environment=` 直接加载环境变量(非 `EnvironmentFile`)。 > 注意:`EnvironmentFile` 不支持多行值,多行 PEM 密钥需 base64 编码或单行格式 ### 部署命令 ```bash -./deploy.sh development # 部署到测试服务器 -./deploy.sh production # 部署到生产服务器 +./deploy.sh development # 部署到测试服务器 +./deploy.sh production # 生产蓝绿部署(自动检测目标) +./deploy.sh production --target blue # 指定部署到蓝色 +./deploy.sh production --target green # 指定部署到绿色 ``` **deploy.sh 选项:** @@ -584,6 +597,8 @@ let new_expires = base_time + days(pkg_days); | `--skip-tests` | 跳过部署后测试 | | `--rollback` | 回滚到上一个备份版本 | | `--backup-list` | 列出可用备份 | +| `--target blue\|green` | 蓝绿部署目标 | +| `--remote-host IP` | 部署目标服务器(默认 `aliyun-server`) | | `--init-env` | 初始化远程 .env(生成随机 JWT_SECRET + 推送模板到服务器) | | `--help, -h` | 显示帮助信息 | diff --git a/deploy.sh b/deploy.sh index 29c68e3..661e14b 100755 --- a/deploy.sh +++ b/deploy.sh @@ -31,7 +31,7 @@ source "${SCRIPT_DIR}/lib/test.sh" # ---------- 默认值 ---------- PROJECT_NAME="rust-backend" REMOTE_USER="root" -REMOTE_HOST="1panel-server" +REMOTE_HOST="aliyun-server" APP_ENV="production" DRY_RUN=false; SKIP_CONFIRM=false; SKIP_TESTS=false @@ -65,8 +65,8 @@ while [[ $# -gt 0 ]]; do echo " --dry-run 预览模式" echo " --yes, -y 跳过确认" echo " --skip-tests 跳过部署后测试" - echo " --target blue|green 蓝绿部署目标(默认单实例)" - echo " --remote-host IP 部署目标服务器(默认 1panel-server)" + echo " --target blue|green 蓝绿部署目标" + echo " --remote-host IP 部署目标服务器(默认 aliyun-server)" echo " --rollback 回滚到指定备份" echo " --backup-list 列出可用备份" echo " --logs [N] 查看后端日志(默认50行)" @@ -85,12 +85,15 @@ while [[ $# -gt 0 ]]; do esac; shift done +# 提前建立 SSH 连接(蓝绿检测需要 remote()) +ssh_connect + # ---------- 蓝绿目标自动检测 + 配置 ---------- # 优先级: 手动 --target > 自动检测 detect_blue_green() { local proxy_conf; proxy_conf=$(get_proxy_conf) local active - active=$(remote "docker exec ${NGINX_CONTAINER} cat ${proxy_conf} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:\d+' | head -1 || echo "") + active=$(remote "sudo cat ${proxy_conf} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:\d+' | head -1 || echo "") case "${APP_ENV}:${active}" in *:4433) echo "blue" ;; @@ -133,7 +136,7 @@ case "${APP_ENV}" in BACKEND_PORT="8080" fi TEST_DOMAIN="https://dev.xmclassmate.top" - DB_CONTAINER="1Panel-postgresql-FtMo" + DB_CONTAINER="postgres" DB_NAME="milkydata_dev" DB_USER="milkydata" ;; production) @@ -147,14 +150,13 @@ case "${APP_ENV}" in BACKEND_PORT="4433" fi TEST_DOMAIN="https://xmclassmate.top" - DB_CONTAINER="1Panel-postgresql-FtMo" + DB_CONTAINER="postgres" DB_NAME="milkydata" DB_USER="milkydata" ;; esac # ---------- 快捷命令 ---------- GIT_VERSION=$(get_git_info) -ssh_connect if [ "$LIST_BACKUPS" = true ]; then list_backups; exit 0; fi if [ "$INIT_ENV" = true ]; then init_env; exit 0; fi diff --git a/docs/BLUE-GREEN-DEPLOY.md b/docs/BLUE-GREEN-DEPLOY.md index 183da84..1908313 100644 --- a/docs/BLUE-GREEN-DEPLOY.md +++ b/docs/BLUE-GREEN-DEPLOY.md @@ -1,4 +1,9 @@ -# 蓝绿部署计划 +# 蓝绿部署(新服务器) + +> **服务器**:47.109.203.92 (Aliyun, Debian 13) +> **nginx**:原生 nginx(非 Docker/OpenResty) +> **PostgreSQL**:Docker 容器 `postgres:17.6-alpine` +> **部署用户**:`deploy`(有 sudo 权限) ## 域名与环境映射 @@ -28,7 +33,7 @@ xmclassmate.top 生产 :4433 / :4434 milkydata ``` dev.xmclassmate.top │ - OpenResty → :8080(单实例) + nginx → :8080(单实例) │ milkydata_dev ``` @@ -48,9 +53,9 @@ dev.xmclassmate.top ``` xmclassmate.top │ - OpenResty proxy + nginx proxy(直接路由) / \ -蓝色 :4433 绿色 :4434 + 蓝色 :4433 绿色 :4434 (active) (idle) \ / milkydata(共享) @@ -70,16 +75,18 @@ xmclassmate.top 两个 service 的 `DATABASE_URL` 都指向 `milkydata`,仅端口不同。 -**OpenResty proxy 配置:** +**nginx proxy 配置:** ``` # /www/sites/xmclassmate.top/proxy/root.conf location ^~ / { - proxy_pass http://127.0.0.1:4433; # deploy.sh 切换此端口 + proxy_pass https://127.0.0.1:4433; # deploy.sh 切换此端口 } ``` -切换时 `sed` 替换端口,`nginx -s reload`。 +> 生产环境使用 HTTPS proxy(`proxy_pass https://...`),因为后端自身处理 TLS。 + +切换时 `sed` 替换端口,`sudo nginx -s reload`。 ### 部署流程 @@ -110,9 +117,11 @@ location ^~ / { ### 切换脚本 ```bash -./scripts/switch-env.sh --env prod --switch green +./scripts/switch-env.sh --switch green # 或切回蓝色 -./scripts/switch-env.sh --env prod --switch blue +./scripts/switch-env.sh --switch blue +# 查看当前状态 +./scripts/switch-env.sh --status ``` ### 回滚 @@ -203,11 +212,11 @@ cargo build --release && rsync ... systemctl restart ← ❌ | # | 步骤 | 说明 | 状态 | |---|------|------|------| | 1 | 数据迁移 | 已完成 | ✅ | -| 2 | 准备生产 service 文件(填入实际密钥) | 待完成 | ⬜ | -| 3 | 部署新代码到 `:4433` | 待完成 | ⬜ | +| 2 | 准备生产 service 文件 | 已完成 | ✅ | +| 3 | 部署新代码到 `:4433` 和 `:4434` | 已完成 | ✅ | | 4 | `env.ts` 设 `CURRENT_ENV = 'production'` | 已完成 | ✅ | -| 5 | 微信开发者工具上传小程序 | 待完成 | ⬜ | -| 6 | 1Panel 确认 `proxy_pass` 指向 `:4433` | 已完成 | ✅ | +| 5 | 微信开发者工具上传小程序 | 已完成 | ✅ | +| 6 | 确认 nginx `proxy_pass` 指向 :4433 | 已完成 | ✅ | ## 实施状态 @@ -215,11 +224,11 @@ cargo build --release && rsync ... systemctl restart ← ❌ | 组件 | 状态 | |------|------| -| Blue service (:4433) | ✅ 运行中,ConfigID `71ba8887` | -| Green service (:4434) | ✅ 运行中,活动中 | -| `deploy.sh --target blue\|green` | ✅ 已支持 | -| `switch-env.sh --switch blue\|green` | ✅ 自动切换 | -| OpenResty proxy | ✅ 指向活动中环境 | +| Blue service (:4433) | ✅ 运行中,当前活跃 | +| Green service (:4434) | ✅ 运行中,待命中 | +| `deploy.sh production --target blue/green` | ✅ 已支持 | +| `switch-env.sh --switch blue/green` | ✅ 自动切换 nginx proxy | +| nginx proxy | ✅ 指向活动中环境 | ### 下次部署流程 diff --git a/lib/common.sh b/lib/common.sh index a92053f..35e7b00 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -107,7 +107,6 @@ check_service_file() { } # ---------- 蓝绿部署:检查目标是否为当前活动环境 ---------- -NGINX_CONTAINER="1Panel-openresty-ABu5" get_proxy_conf() { if [ "${APP_ENV}" = "development" ]; then @@ -122,7 +121,7 @@ check_not_active() { local proxy_conf; proxy_conf=$(get_proxy_conf) local active_env - active_env=$(remote "docker exec ${NGINX_CONTAINER} cat ${proxy_conf} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:\d+' | head -1 || echo "") + active_env=$(remote "sudo cat ${proxy_conf} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:\d+' | head -1 || echo "") local current_target case "$active_env" in @@ -271,7 +270,7 @@ backup_database() { local ts; ts=$(date +%Y%m%d_%H%M%S) local bf="${REMOTE_DIR}/backups/${DB_NAME}_${ts}.dump" remote "mkdir -p ${REMOTE_DIR}/backups" - if remote "docker exec ${DB_CONTAINER} pg_dump -Fc -U ${DB_USER} ${DB_NAME} > /tmp/backup_${ts}.dump && mv /tmp/backup_${ts}.dump '${bf}'" 2>&1; then + if remote "sudo docker exec ${DB_CONTAINER} pg_dump -Fc -U ${DB_USER} ${DB_NAME} > /tmp/backup_${ts}.dump && mv /tmp/backup_${ts}.dump '${bf}'" 2>&1; then log_info "数据库备份成功: ${DB_NAME}_${ts}.dump" cleanup_old_db_backups else @@ -332,16 +331,16 @@ run_migrations() { [ -n "$sql_files" ] && backup_database # 创建迁移追踪表 - remote "docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" <<<'CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMPTZ DEFAULT NOW());' 2>/dev/null || true + remote "sudo docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" <<<'CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMPTZ DEFAULT NOW());' 2>/dev/null || true for f in $sql_files; do local name; name=$(basename "$f") log_info "检查迁移: ${name}" - local done; done=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"SELECT COUNT(*) FROM _migrations WHERE name='${name}'\"" 2>/dev/null || echo "0") + local done; done=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"SELECT COUNT(*) FROM _migrations WHERE name='${name}'\"" 2>/dev/null || echo "0") [ "$done" != "0" ] && { log_info " 跳过(已执行): ${name}"; continue; } log_info " 执行: ${name}" - remote "cat '${f}' | docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" && \ - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"INSERT INTO _migrations (name) VALUES ('${name}')\"" && \ + remote "cat '${f}' | sudo docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" && \ + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"INSERT INTO _migrations (name) VALUES ('${name}')\"" && \ log_info " 完成: ${name}" || log_warn " 迁移 ${name} 可能已执行或失败" done log_info "数据库迁移完成" @@ -459,12 +458,12 @@ check_ssl_expiry() { log_step "检查 SSL 证书到期时间..." [ "$DRY_RUN" = true ] && { log_dry "openssl x509 -checkend 2592000"; return 0; } - # 从 OpenResty 容器内读取证书 - local cert_info; cert_info=$(remote "docker exec 1Panel-openresty-ABu5 openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -enddate -checkend 2592000 2>/dev/null" || echo "") + # 读取 SSL 证书 + local cert_info; cert_info=$(remote "sudo openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -enddate -checkend 2592000 2>/dev/null" || echo "") if echo "$cert_info" | grep -q "Certificate will expire"; then - local expiry; expiry=$(remote "docker exec 1Panel-openresty-ABu5 openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -enddate 2>/dev/null | cut -d= -f2" || echo "未知") - local days_left; days_left=$(remote "docker exec 1Panel-openresty-ABu5 openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -checkend 86400 2>/dev/null | grep -c 'will expire'" || echo "0") + local expiry; expiry=$(remote "sudo openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -enddate 2>/dev/null | cut -d= -f2" || echo "未知") + local days_left; days_left=$(remote "sudo openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -checkend 86400 2>/dev/null | grep -c 'will expire'" || echo "0") log_warn "SSL 证书将于 ${expiry} 到期(不足 30 天)!" [ "$days_left" -gt "0" ] && log_warn " 警告:证书将在 1 天内到期,请立即续期!" else diff --git a/lib/test.sh b/lib/test.sh index 09620c7..23e635b 100644 --- a/lib/test.sh +++ b/lib/test.sh @@ -9,7 +9,7 @@ BASE_URL="http://127.0.0.1" TEST_USER_ID="" setup_test_user() { - TEST_USER_ID=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" + TEST_USER_ID=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" INSERT INTO users (openid, name, type) VALUES ('test_\$(date +%s)', 'test_user', 2) RETURNING id; \"" 2>/dev/null | tr -d ' ' || echo "") @@ -19,7 +19,7 @@ setup_test_user() { cleanup_test_user() { if [ -n "$TEST_USER_ID" ]; then - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \" + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \" DELETE FROM payment_orders WHERE user_id = ${TEST_USER_ID}; DELETE FROM users WHERE id = ${TEST_USER_ID}; \"" 2>/dev/null || true @@ -74,13 +74,13 @@ test_basic_connectivity() { } test_database_connection() { - local r; r=$(remote "docker exec ${DB_CONTAINER} pg_isready -U ${DB_USER} 2>/dev/null" || echo "failed") + local r; r=$(remote "sudo docker exec ${DB_CONTAINER} pg_isready -U ${DB_USER} 2>/dev/null" || echo "failed") [[ "$r" =~ "accepting connections" ]] && log_info " PostgreSQL 连接正常" || { log_warn " PostgreSQL 连接失败"; return 1; } - r=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT 1'" 2>/dev/null || echo "failed") + r=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT 1'" 2>/dev/null || echo "failed") [ "$r" = "1" ] && log_info " 数据库查询正常" || { log_warn " 数据库查询失败"; return 1; } - r=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT tablename FROM pg_tables WHERE schemaname='\''public'\'' LIMIT 1'" 2>/dev/null || echo "failed") + r=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT tablename FROM pg_tables WHERE schemaname='\''public'\'' LIMIT 1'" 2>/dev/null || echo "failed") [ -n "$r" ] && log_info " 表存在: ${r}" || { log_warn " 表查询失败"; return 1; } log_info "数据库连接测试通过"; return 0 } @@ -173,10 +173,10 @@ test_payment_endpoints() { } test_migration_status() { - local r; r=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT COUNT(*) FROM _migrations'" 2>/dev/null || echo "0") + local r; r=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT COUNT(*) FROM _migrations'" 2>/dev/null || echo "0") [ "$r" -gt "0" ] 2>/dev/null && log_info " _migrations 表存在,已执行 ${r} 个迁移 ✅" || { log_warn " _migrations 表不存在或无记录"; return 0; } - local last; last=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT name FROM _migrations ORDER BY applied_at DESC LIMIT 1'" 2>/dev/null || echo "") + local last; last=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT name FROM _migrations ORDER BY applied_at DESC LIMIT 1'" 2>/dev/null || echo "") [ -n "$last" ] && log_info " 最新迁移: ${last} ✅" return 0 } @@ -212,11 +212,11 @@ test_payment_flow() { # 创建测试订单 local sql="INSERT INTO payment_orders (user_id, order_no, package_type, amount, status, expires_at) VALUES (${TEST_USER_ID}, '${test_no}', '${pkg_type}', 100, 'pending', NOW() + INTERVAL '${expected_days} days');" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql}\"" 2>/dev/null || true # 验证到期时间间隔 local interval_days - interval_days=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" + interval_days=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" SELECT EXTRACT(DAY FROM (expires_at - created_at)) FROM payment_orders WHERE order_no = '${test_no}'; \"" 2>/dev/null || echo "0") interval_days=${interval_days%.*} @@ -230,7 +230,7 @@ test_payment_flow() { # 清理测试订单 local del="DELETE FROM payment_orders WHERE order_no = '${test_no}';" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${del}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${del}\"" 2>/dev/null || true done # 阶段 2:测试提交订单后的会员时长(仅开发环境有 mock 支付) @@ -267,7 +267,7 @@ test_payment_flow() { # 5. 验证会员时长 local member_expires - member_expires=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" + member_expires=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" SELECT EXTRACT(DAY FROM (membership_expires_at - NOW()))::int FROM users WHERE id = ${TEST_USER_ID}; \"" 2>/dev/null || echo "0") @@ -281,9 +281,9 @@ test_payment_flow() { # 6. 清理:取消会员 local clean="UPDATE users SET is_member = false, membership_expires_at = NULL WHERE id = ${TEST_USER_ID};" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${clean}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${clean}\"" 2>/dev/null || true local cancel="UPDATE payment_orders SET status = 'cancelled' WHERE order_no = '${order_no}' AND status = 'paid';" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${cancel}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${cancel}\"" 2>/dev/null || true else log_warn " 未获取到订单号,跳过 mock 确认测试" fi @@ -323,16 +323,16 @@ test_payment_dedup() { INSERT INTO payment_orders (user_id, order_no, package_type, amount, status, expires_at) VALUES (${user_id}, '${test_no1}', 'monthly', 590, 'pending', NOW() + INTERVAL '30 days'); INSERT INTO payment_orders (user_id, order_no, package_type, amount, status, expires_at) VALUES (${user_id}, '${test_no2}', 'monthly', 590, 'pending', NOW() + INTERVAL '30 days'); SQLEOF -docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -v ON_ERROR_STOP=1 -f ${sql_file} +sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -v ON_ERROR_STOP=1 -f ${sql_file} rm -f ${sql_file}" 2>&1 || { log_warn " SQL 执行异常"; all_ok=false; } # 2. 查询数量 local pending_count - pending_count=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"SELECT COUNT(*) FROM payment_orders WHERE user_id = ${user_id} AND package_type = 'monthly' AND status = 'pending';\"" 2>/dev/null || echo "0") + pending_count=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"SELECT COUNT(*) FROM payment_orders WHERE user_id = ${user_id} AND package_type = 'monthly' AND status = 'pending';\"" 2>/dev/null || echo "0") pending_count=$(echo "$pending_count" | tr -d ' ') # 3. 清理 - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"DELETE FROM payment_orders WHERE order_no LIKE '${uuid_prefix}%';\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"DELETE FROM payment_orders WHERE order_no LIKE '${uuid_prefix}%';\"" 2>/dev/null || true if [ -n "$pending_count" ] && [ "$pending_count" -ge 2 ] 2>/dev/null; then log_info " 同用户同套餐有 ${pending_count} 个 pending 订单(去重逻辑正常)✅" @@ -391,20 +391,20 @@ test_payment_refund() { # 创建已支付订单(模拟一个月会员) local sql_create="INSERT INTO payment_orders (user_id, order_no, package_type, amount, status, paid_at, expires_at) VALUES (${TEST_USER_ID}, '${test_no}', 'monthly', 590, 'paid', NOW(), NOW() + INTERVAL '30 days');" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_create}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_create}\"" 2>/dev/null || true # 设置用户为会员 local sql_member="UPDATE users SET is_member = true, membership_expires_at = NOW() + INTERVAL '30 days' WHERE id = ${TEST_USER_ID};" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_member}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_member}\"" 2>/dev/null || true # 执行退款(直接调用 db 函数等价操作) local sql_refund="UPDATE payment_orders SET status = 'refunded' WHERE order_no = '${test_no}';" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_refund}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_refund}\"" 2>/dev/null || true # 清理:取消会员 + 删除测试订单 local clean_member="UPDATE users SET is_member = false, membership_expires_at = NULL WHERE id = ${TEST_USER_ID};" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${clean_member}\"" 2>/dev/null || true - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"DELETE FROM payment_orders WHERE order_no = '${test_no}';\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${clean_member}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"DELETE FROM payment_orders WHERE order_no = '${test_no}';\"" 2>/dev/null || true log_info " 退款 + 会员取消测试完成 ✅" log_info "退款测试通过" @@ -422,15 +422,15 @@ test_payment_verify_detection() { # 构造一个 expiry = paid_at + 1 天的异常订单(模拟 ELSE INTERVAL '0 days') local sql_create="INSERT INTO payment_orders (user_id, order_no, package_type, amount, status, paid_at, expires_at) VALUES (${TEST_USER_ID}, '${test_no}', 'half_year', 3190, 'paid', NOW(), NOW() + INTERVAL '1 day');" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_create}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_create}\"" 2>/dev/null || true # 设置会员为 paid_at + 1 天(模拟 0 天会员时长) local sql_member="UPDATE users SET is_member = true, membership_expires_at = NOW() + INTERVAL '1 day' WHERE id = ${TEST_USER_ID};" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_member}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql_member}\"" 2>/dev/null || true # 验证 actual_days < 25 能被检测到 local actual_days - actual_days=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" + actual_days=$(remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" SELECT EXTRACT(DAY FROM (u.membership_expires_at - po.paid_at)) FROM payment_orders po JOIN users u ON u.id = po.user_id WHERE po.order_no = '${test_no}'; @@ -439,8 +439,8 @@ test_payment_verify_detection() { # 清理 local clean="UPDATE users SET is_member = false, membership_expires_at = NULL WHERE id = ${TEST_USER_ID};" - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${clean}\"" 2>/dev/null || true - remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"DELETE FROM payment_orders WHERE order_no = '${test_no}';\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${clean}\"" 2>/dev/null || true + remote "sudo docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"DELETE FROM payment_orders WHERE order_no = '${test_no}';\"" 2>/dev/null || true if [ "$actual_days" -lt 25 ] 2>/dev/null; then log_info " 异常时长 ${actual_days} 天可被 verify_membership_after_payment 检测到(阈值 25 天)✅" diff --git a/scripts/switch-env.sh b/scripts/switch-env.sh index e7b11d3..ca48295 100755 --- a/scripts/switch-env.sh +++ b/scripts/switch-env.sh @@ -12,11 +12,9 @@ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" ENV_FILE="$PROJECT_ROOT/.env" # ---------- 配置 ---------- -# 1Panel 管理的 OpenResty 容器的实际配置文件路径 -# 容器内路径(host: /opt/1panel/apps/openresty/openresty/www/sites/...) ROOT_CONF="/www/sites/xmclassmate.top/proxy/root.conf" -NGINX_CONTAINER="1Panel-openresty-ABu5" -SSH_HOST="root@1panel-server" +NGINX_SSL_CONF="/www/sites/xmclassmate.top/ssl/fullchain.pem" +SSH_HOST="deploy@47.109.203.92" # ---------- 颜色输出 ---------- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' @@ -41,7 +39,7 @@ usage() { # ---------- 获取当前活动环境 ---------- get_active() { - local proxy_conf; proxy_conf=$(ssh "$SSH_HOST" "docker exec ${NGINX_CONTAINER} cat ${ROOT_CONF}" 2>/dev/null || echo "") + local proxy_conf; proxy_conf=$(ssh "$SSH_HOST" "sudo cat ${ROOT_CONF}" 2>/dev/null || echo "") if echo "$proxy_conf" | grep -q "127.0.0.1:4433"; then echo "blue" elif echo "$proxy_conf" | grep -q "127.0.0.1:4434"; then @@ -104,15 +102,15 @@ switch_to() { # 修改 proxy_pass(两个端口都是 HTTPS) local target_proto="https" - info "修改 OpenResty proxy_pass → ${target_proto}://127.0.0.1:${target_port}..." - ssh "$SSH_HOST" "docker exec ${NGINX_CONTAINER} sh -c \"sed -i 's|proxy_pass https\?://127.0.0.1:[0-9]*|proxy_pass ${target_proto}://127.0.0.1:${target_port}|' ${ROOT_CONF}\"" 2>&1 || { + info "修改 nginx proxy_pass → ${target_proto}://127.0.0.1:${target_port}..." + ssh "$SSH_HOST" "sudo sed -i 's|proxy_pass https\?://127.0.0.1:[0-9]*|proxy_pass ${target_proto}://127.0.0.1:${target_port}|' ${ROOT_CONF}" 2>&1 || { error "修改 proxy_pass 失败" exit 1 } # 重新加载 nginx info "重新加载 nginx..." - ssh "$SSH_HOST" "docker exec ${NGINX_CONTAINER} nginx -s reload" 2>&1 || { + ssh "$SSH_HOST" "sudo nginx -s reload" 2>&1 || { error "nginx 重载失败" exit 1 }