feat(auth): 添加 Refresh Token 双 Token 机制
- 添加 /api/refresh-token 接口支持 Token 续期 - 登录接口返回 access_token 和 refresh_token - 新增 refresh_tokens 表存储 refresh_token - 部署脚本添加数据库备份和迁移功能 - deploy.sh 添加 4 项 API 测试 - 更新 AGENTS.md 文档
This commit is contained in:
520
deploy.sh
520
deploy.sh
@@ -27,6 +27,9 @@ SKIP_TESTS=false
|
||||
ROLLBACK=false
|
||||
LIST_BACKUPS=false
|
||||
|
||||
# 全局变量(部署流程中使用)
|
||||
BACKUP_PATH=""
|
||||
|
||||
# 解析选项
|
||||
shift 2>/dev/null || true
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -70,12 +73,20 @@ case "${APP_ENV}" in
|
||||
development)
|
||||
REMOTE_DIR="/root/rust/rust_backend_dev"
|
||||
SERVICE_NAME="rust-backend-dev.service"
|
||||
TEST_DOMAIN="https://xmclassmate.top/dev"
|
||||
TEST_DOMAIN="https://dev.xmclassmate.top"
|
||||
BACKEND_PORT="8080"
|
||||
DB_CONTAINER="1Panel-postgresql-FtMo"
|
||||
DB_NAME="milkydata_dev"
|
||||
DB_USER="milkydata"
|
||||
;;
|
||||
production)
|
||||
REMOTE_DIR="/root/rust/rust_backend"
|
||||
SERVICE_NAME="rust-backend.service"
|
||||
TEST_DOMAIN="https://xmclassmate.top"
|
||||
BACKEND_PORT="4433"
|
||||
DB_CONTAINER="1Panel-postgresql-FtMo"
|
||||
DB_NAME="milkydata"
|
||||
DB_USER="milkydata"
|
||||
;;
|
||||
*)
|
||||
echo "无效的环境: ${APP_ENV}"
|
||||
@@ -174,7 +185,7 @@ list_backups() {
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 回滚
|
||||
# 回滚(手动触发)
|
||||
rollback() {
|
||||
log_step "查找最新备份..."
|
||||
local latest_backup
|
||||
@@ -226,8 +237,85 @@ rollback() {
|
||||
fi
|
||||
|
||||
log_info "回滚完成"
|
||||
log_to_file "ROLLBACK to $(basename $latest_backup)"
|
||||
send_webhook "rollback" "回滚完成: $(basename $latest_backup)"
|
||||
log_to_file "MANUAL_ROLLBACK to $(basename $latest_backup)"
|
||||
send_webhook "rollback" "手动回滚完成: $(basename $latest_backup)"
|
||||
}
|
||||
|
||||
# 回滚到上一个正常版本(测试失败时自动触发)
|
||||
rollback_on_failure() {
|
||||
local backup_path="$1"
|
||||
|
||||
if [ -z "$backup_path" ]; then
|
||||
log_error "回滚失败: 未指定备份路径"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_error "========== 测试失败,自动回滚 =========="
|
||||
echo -e " 原因: 部署后测试未通过"
|
||||
echo -e " 回滚到: ${backup_path}"
|
||||
echo -e " 服务: ${SERVICE_NAME}"
|
||||
echo -e "=========================================="
|
||||
echo ""
|
||||
|
||||
log_step "停止服务..."
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl stop ${SERVICE_NAME}" 2>/dev/null || log_warn "停止服务失败,继续尝试..."
|
||||
else
|
||||
log_dry "systemctl stop ${SERVICE_NAME}"
|
||||
fi
|
||||
|
||||
log_step "恢复上一个正常版本..."
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cp '${backup_path}' '${REMOTE_DIR}/${PROJECT_NAME}'"
|
||||
else
|
||||
log_dry "cp '${backup_path}' '${REMOTE_DIR}/${PROJECT_NAME}'"
|
||||
fi
|
||||
|
||||
log_step "启动服务..."
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl start ${SERVICE_NAME}"
|
||||
sleep 3
|
||||
else
|
||||
log_dry "systemctl start ${SERVICE_NAME}"
|
||||
fi
|
||||
|
||||
log_info "自动回滚完成"
|
||||
log_to_file "AUTO_ROLLBACK to $(basename $backup_path) - tests failed"
|
||||
send_webhook "failure" "部署测试失败,已自动回滚到: $(basename $backup_path)"
|
||||
}
|
||||
|
||||
# 清理旧备份,只保留最近 N 个
|
||||
cleanup_old_backups() {
|
||||
local keep_count="${1:-5}"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "保留最近 ${keep_count} 个备份,删除其余"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local backup_count
|
||||
backup_count=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null | wc -l" || echo "0")
|
||||
|
||||
if [ "$backup_count" -le "$keep_count" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local to_delete
|
||||
to_delete=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null | tail -n +$((keep_count + 1))" || echo "")
|
||||
|
||||
if [ -z "$to_delete" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_step "清理旧备份(保留 ${keep_count} 个)..."
|
||||
echo "$to_delete" | while read -r file; do
|
||||
if [ -n "$file" ]; then
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -f '${file}'" 2>/dev/null && echo " 已删除: ${file}" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
@@ -296,23 +384,24 @@ build_project() {
|
||||
fi
|
||||
}
|
||||
|
||||
# 备份旧版本
|
||||
# 备份旧版本并保存回滚标记
|
||||
backup_old_version() {
|
||||
log_step "备份旧版本..."
|
||||
local binary_path="./target/release/${PROJECT_NAME}"
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_path="${REMOTE_DIR}/${PROJECT_NAME}.backup.${timestamp}"
|
||||
BACKUP_PATH="${REMOTE_DIR}/${PROJECT_NAME}.backup.${timestamp}"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'if [ -f ${REMOTE_DIR}/${PROJECT_NAME} ]; then cp ${REMOTE_DIR}/${PROJECT_NAME} ${backup_path}; fi'"
|
||||
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'if [ -f ${REMOTE_DIR}/${PROJECT_NAME} ]; then cp ${REMOTE_DIR}/${PROJECT_NAME} ${BACKUP_PATH}; echo \"\${BACKUP_PATH}\" > ${REMOTE_DIR}/.last_deployed; fi'"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
|
||||
if [ -f '${REMOTE_DIR}/${PROJECT_NAME}' ]; then
|
||||
cp '${REMOTE_DIR}/${PROJECT_NAME}' '${backup_path}'
|
||||
echo '已备份到: ${backup_path}'
|
||||
cp '${REMOTE_DIR}/${PROJECT_NAME}' '${BACKUP_PATH}'
|
||||
echo '${BACKUP_PATH}' > '${REMOTE_DIR}/.last_deployed'
|
||||
echo '已备份到: ${BACKUP_PATH}'
|
||||
else
|
||||
echo '无旧版本需要备份'
|
||||
fi
|
||||
@@ -350,6 +439,23 @@ upload_config() {
|
||||
log_info "配置文件上传完成"
|
||||
}
|
||||
|
||||
# 上传迁移文件
|
||||
upload_migrations() {
|
||||
log_step "上传迁移文件..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "rsync -avzP ./migrations/ '${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/migrations/'"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -d "./migrations" ]; then
|
||||
log_warn "迁移目录不存在,跳过"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rsync -avzP --progress ./migrations/ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/migrations/"
|
||||
log_info "迁移文件上传完成"
|
||||
}
|
||||
|
||||
# 上传测试脚本
|
||||
upload_test_script() {
|
||||
log_step "上传测试脚本..."
|
||||
@@ -363,6 +469,24 @@ upload_test_script() {
|
||||
log_info "测试脚本上传完成"
|
||||
}
|
||||
|
||||
# 上传清理脚本(定时任务用)
|
||||
upload_cleanup_script() {
|
||||
log_step "上传清理脚本..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "rsync -avzP ./scripts/cleanup_refresh_tokens.sh '${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/scripts/'"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p ./scripts 2>/dev/null || true
|
||||
if [ -f "./scripts/cleanup_refresh_tokens.sh" ]; then
|
||||
rsync -avzP --progress ./scripts/cleanup_refresh_tokens.sh "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/scripts/"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "chmod +x ${REMOTE_DIR}/scripts/cleanup_refresh_tokens.sh"
|
||||
log_info "清理脚本上传完成"
|
||||
else
|
||||
log_warn "清理脚本不存在,跳过"
|
||||
fi
|
||||
}
|
||||
|
||||
# 重启远程服务
|
||||
restart_service() {
|
||||
log_step "重启服务 '${SERVICE_NAME}'..."
|
||||
@@ -381,11 +505,11 @@ restart_service() {
|
||||
sleep 3
|
||||
}
|
||||
|
||||
# 运行部署后测试
|
||||
run_tests() {
|
||||
log_step "执行部署后测试..."
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'TEST_DOMAIN=\"${TEST_DOMAIN}\" ${REMOTE_DIR}/test_deployment.sh'"
|
||||
log_dry "步骤1: curl http://127.0.0.1:${BACKEND_PORT}/health"
|
||||
log_dry "步骤2: curl ${TEST_DOMAIN}/health"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -394,16 +518,353 @@ run_tests() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "TEST_DOMAIN=\"${TEST_DOMAIN}\" ${REMOTE_DIR}/test_deployment.sh"; then
|
||||
log_info "测试通过"
|
||||
else
|
||||
log_error "测试失败!请检查服务状态。"
|
||||
log_step "步骤1: 测试本地后端..."
|
||||
if ! test_local_backend; then
|
||||
rollback_and_exit "本地后端测试失败"
|
||||
fi
|
||||
|
||||
log_step "步骤2: 测试域名访问..."
|
||||
if ! test_domain_access; then
|
||||
rollback_and_exit "域名访问测试失败"
|
||||
fi
|
||||
|
||||
log_info "测试通过"
|
||||
return 0
|
||||
}
|
||||
|
||||
test_local_backend() {
|
||||
local max_retries=3
|
||||
local retry=0
|
||||
|
||||
while [ $retry -lt $max_retries ]; do
|
||||
retry=$((retry + 1))
|
||||
log_info "尝试 ${retry}/${max_retries}: 测试本地后端 (127.0.0.1:${BACKEND_PORT})"
|
||||
|
||||
# 测试 1: 健康检查
|
||||
log_info " [1/4] 测试 /health..."
|
||||
local health_code
|
||||
health_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 http://127.0.0.1:${BACKEND_PORT}/health" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$health_code" != "200" ]; then
|
||||
log_warn " /health 异常 (HTTP ${health_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
log_info " /health 正常"
|
||||
|
||||
# 测试 2: 登录接口存在(预期 400,微信 code 无效)
|
||||
log_info " [2/4] 测试 /api/login..."
|
||||
local login_code
|
||||
login_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test_code\"'" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$login_code" = "400" ]; then
|
||||
log_info " /api/login 正常 (HTTP ${login_code}, 微信返回 400 expected)"
|
||||
elif [ "$login_code" = "200" ]; then
|
||||
log_info " /api/login 正常 (HTTP ${login_code})"
|
||||
else
|
||||
log_warn " /api/login 异常 (HTTP ${login_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
# 测试 3: Refresh Token 接口存在
|
||||
log_info " [3/4] 测试 /api/refresh-token..."
|
||||
local refresh_code
|
||||
refresh_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/api/refresh-token -H 'Content-Type: application/json' -d '{\"refresh_token\":\"invalid\"}'" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$refresh_code" = "401" ]; then
|
||||
log_info " /api/refresh-token 正常 (HTTP ${refresh_code}, 无效 token expected)"
|
||||
elif [ "$refresh_code" = "200" ]; then
|
||||
log_info " /api/refresh-token 正常 (HTTP ${refresh_code})"
|
||||
else
|
||||
log_warn " /api/refresh-token 异常 (HTTP ${refresh_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
# 测试 4: 根路径
|
||||
log_info " [4/4] 测试 / (根路径)..."
|
||||
local root_code
|
||||
root_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 http://127.0.0.1:${BACKEND_PORT}/" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$root_code" = "200" ] || [ "$root_code" = "404" ]; then
|
||||
log_info " / 正常 (HTTP ${root_code})"
|
||||
else
|
||||
log_warn " / 异常 (HTTP ${root_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
log_info "本地后端所有测试通过"
|
||||
return 0
|
||||
done
|
||||
|
||||
log_error "本地后端测试失败"
|
||||
return 1
|
||||
}
|
||||
|
||||
test_domain_access() {
|
||||
local max_retries=3
|
||||
local retry=0
|
||||
|
||||
while [ $retry -lt $max_retries ]; do
|
||||
retry=$((retry + 1))
|
||||
log_info "尝试 ${retry}/${max_retries}: 测试域名访问 (${TEST_DOMAIN})"
|
||||
|
||||
# 测试 1: 健康检查
|
||||
log_info " [1/4] 测试 /health..."
|
||||
local health_code
|
||||
health_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k ${TEST_DOMAIN}/health" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$health_code" != "200" ]; then
|
||||
log_warn " /health 异常 (HTTP ${health_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
log_info " /health 正常"
|
||||
|
||||
# 测试 2: 登录接口
|
||||
log_info " [2/4] 测试 /api/login..."
|
||||
local login_code
|
||||
login_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k -X POST ${TEST_DOMAIN}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test\"'" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$login_code" = "400" ] || [ "$login_code" = "200" ]; then
|
||||
log_info " /api/login 正常 (HTTP ${login_code})"
|
||||
else
|
||||
log_warn " /api/login 异常 (HTTP ${login_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
# 测试 3: Refresh Token 接口
|
||||
log_info " [3/4] 测试 /api/refresh-token..."
|
||||
local refresh_code
|
||||
refresh_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k -X POST ${TEST_DOMAIN}/api/refresh-token -H 'Content-Type: application/json' -d '{\"refresh_token\":\"invalid\"}'" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$refresh_code" = "401" ] || [ "$refresh_code" = "200" ]; then
|
||||
log_info " /api/refresh-token 正常 (HTTP ${refresh_code})"
|
||||
else
|
||||
log_warn " /api/refresh-token 异常 (HTTP ${refresh_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
# 测试 4: 根路径
|
||||
log_info " [4/4] 测试 / (根路径)..."
|
||||
local root_code
|
||||
root_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k ${TEST_DOMAIN}/" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$root_code" = "200" ] || [ "$root_code" = "404" ]; then
|
||||
log_info " / 正常 (HTTP ${root_code})"
|
||||
else
|
||||
log_warn " / 异常 (HTTP ${root_code})"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
log_info "域名访问所有测试通过"
|
||||
return 0
|
||||
done
|
||||
|
||||
log_error "域名访问测试失败"
|
||||
return 1
|
||||
}
|
||||
|
||||
rollback_and_exit() {
|
||||
local reason="$1"
|
||||
log_error "测试失败: ${reason}"
|
||||
log_error "自动回滚到上一个正常版本..."
|
||||
|
||||
local last_known_good
|
||||
last_known_good=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat ${REMOTE_DIR}/.last_deployed 2>/dev/null || echo """)
|
||||
|
||||
if [ -z "$last_known_good" ]; then
|
||||
log_error "无法回滚: 未找到上一个正常版本记录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rollback_on_failure "$last_known_good"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 备份数据库
|
||||
backup_database() {
|
||||
log_step "备份数据库..."
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'bash -c \"docker exec ${DB_CONTAINER} pg_dump -Fc -U ${DB_USER} ${DB_NAME} > ${REMOTE_DIR}/backups/backup.dump\"'"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_file="${REMOTE_DIR}/backups/${DB_NAME}_${timestamp}.dump"
|
||||
|
||||
log_info "创建数据库备份..."
|
||||
|
||||
# 创建备份目录
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p ${REMOTE_DIR}/backups"
|
||||
|
||||
# 在远程执行备份(使用 stdout 重定向)
|
||||
if ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"bash -c 'docker exec ${DB_CONTAINER} pg_dump -Fc -U ${DB_USER} ${DB_NAME} > /tmp/backup_${timestamp}.dump && mv /tmp/backup_${timestamp}.dump \"${backup_file}\"'" 2>&1; then
|
||||
log_info "数据库备份成功: ${DB_NAME}_${timestamp}.dump"
|
||||
|
||||
# 清理旧数据库备份(保留2个)
|
||||
cleanup_old_db_backups
|
||||
else
|
||||
log_warn "数据库备份失败,继续部署"
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理旧数据库备份(保留2个)
|
||||
cleanup_old_db_backups() {
|
||||
local keep_count="${1:-1}"
|
||||
|
||||
local backup_count
|
||||
backup_count=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"ls -t ${REMOTE_DIR}/backups/${DB_NAME}_*.dump 2>/dev/null | wc -l" || echo "0")
|
||||
|
||||
if [ "$backup_count" -le "$keep_count" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local to_delete
|
||||
to_delete=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"ls -t ${REMOTE_DIR}/backups/${DB_NAME}_*.dump 2>/dev/null | tail -n +$((keep_count + 1))" || echo "")
|
||||
|
||||
if [ -z "$to_delete" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_step "清理旧数据库备份(保留 ${keep_count} 个)..."
|
||||
echo "$to_delete" | while read -r file; do
|
||||
if [ -n "$file" ]; then
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -f '${file}'" 2>/dev/null && echo " 已删除: $(basename ${file})" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 回滚数据库(手动触发)
|
||||
rollback_database() {
|
||||
local backup_file="$1"
|
||||
|
||||
if [ -z "$backup_file" ]; then
|
||||
log_error "回滚失败: 未指定备份文件"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_step "回滚数据库..."
|
||||
log_warn "从备份恢复: ${backup_file}"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "pg_restore -c -U ${DB_USER} -d ${DB_NAME} ${backup_file}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 检查备份文件是否存在
|
||||
if ! ssh "${REMOTE_USER}@${REMOTE_HOST}" "[ -f '${backup_file}' ]"; then
|
||||
log_error "备份文件不存在: ${backup_file}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 停止服务避免数据库冲突
|
||||
log_info "停止服务..."
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl stop ${SERVICE_NAME}" 2>/dev/null || true
|
||||
|
||||
# 执行恢复(通过 stdin 管道)
|
||||
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat '${backup_file}'" | \
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"docker exec -i ${DB_CONTAINER} pg_restore -c -U ${DB_USER} -d ${DB_NAME}" 2>&1; then
|
||||
log_info "数据库恢复成功"
|
||||
else
|
||||
log_error "数据库恢复失败"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 重启服务
|
||||
log_info "重启服务..."
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl start ${SERVICE_NAME}"
|
||||
|
||||
log_info "数据库回滚完成"
|
||||
}
|
||||
|
||||
# 执行数据库迁移
|
||||
run_migrations() {
|
||||
log_step "执行数据库迁移..."
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_dry "检查并执行 migrations/*.sql"
|
||||
local migrations
|
||||
migrations=$(ls ./migrations/*.sql 2>/dev/null | wc -l || echo "0")
|
||||
log_dry "发现 ${migrations} 个迁移文件"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 检查是否有迁移文件
|
||||
local sql_files
|
||||
sql_files=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls ${REMOTE_DIR}/migrations/*.sql 2>/dev/null | sort" || echo "")
|
||||
|
||||
if [ -n "$sql_files" ]; then
|
||||
# 有迁移文件,先备份数据库
|
||||
backup_database
|
||||
else
|
||||
log_info "没有发现迁移文件,跳过"
|
||||
fi
|
||||
|
||||
log_info "检查迁移文件..."
|
||||
if ! ssh "${REMOTE_USER}@${REMOTE_HOST}" "[ -d ${REMOTE_DIR}/migrations ]"; then
|
||||
log_warn "迁移目录不存在,跳过"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local sql_files
|
||||
sql_files=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls ${REMOTE_DIR}/migrations/*.sql 2>/dev/null | sort" || echo "")
|
||||
|
||||
if [ -z "$sql_files" ]; then
|
||||
log_warn "没有找到迁移文件"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for sql_file in $sql_files; do
|
||||
local filename
|
||||
filename=$(basename "$sql_file")
|
||||
local table_check=$(echo "$filename" | sed 's/[0-9]*_//' | sed 's/.sql$//')
|
||||
|
||||
log_info "检查迁移: $filename"
|
||||
|
||||
if [ "$table_check" = "add_refresh_tokens" ]; then
|
||||
local exists
|
||||
exists=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
|
||||
"docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"SELECT 1 FROM information_schema.tables WHERE table_name = 'refresh_tokens'\" 2>/dev/null" || echo "0")
|
||||
if [ "$exists" = "1" ]; then
|
||||
log_info " 表 refresh_tokens 已存在,跳过"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info " 执行迁移: $filename"
|
||||
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" < "$sql_file"; then
|
||||
log_info " 完成: $filename"
|
||||
else
|
||||
log_warn " 迁移可能已执行或失败,继续"
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "数据库迁移完成"
|
||||
}
|
||||
|
||||
# 确认部署
|
||||
confirm_deploy() {
|
||||
if [ "$SKIP_CONFIRM" = true ]; then
|
||||
if [ "$DRY_RUN" = true ] || [ "$SKIP_CONFIRM" = true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -474,20 +935,31 @@ main() {
|
||||
ssh_connect
|
||||
build_project
|
||||
backup_old_version
|
||||
cleanup_old_backups 5
|
||||
upload_binary
|
||||
upload_config
|
||||
upload_migrations
|
||||
upload_test_script
|
||||
upload_cleanup_script
|
||||
restart_service
|
||||
run_tests
|
||||
run_migrations
|
||||
|
||||
log_to_file "DEPLOY success"
|
||||
send_webhook "success" "部署完成"
|
||||
if run_tests; then
|
||||
log_to_file "DEPLOY success"
|
||||
send_webhook "success" "部署完成"
|
||||
|
||||
echo ""
|
||||
log_info "========== 部署完成! =========="
|
||||
echo -e " Git: ${GIT_VERSION}"
|
||||
echo -e " 环境: ${APP_ENV}"
|
||||
echo ""
|
||||
echo ""
|
||||
log_info "========== 部署完成! =========="
|
||||
echo -e " Git: ${GIT_VERSION}"
|
||||
echo -e " 环境: ${APP_ENV}"
|
||||
echo ""
|
||||
else
|
||||
log_error "========== 部署失败,已自动回滚 =========="
|
||||
echo -e " Git: ${GIT_VERSION}"
|
||||
echo -e " 环境: ${APP_ENV}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
Reference in New Issue
Block a user