diff --git a/deploy.sh b/deploy.sh index 3cae8ec..91ade65 100755 --- a/deploy.sh +++ b/deploy.sh @@ -14,7 +14,8 @@ # --tail [N] 实时跟踪日志 # --status 查看服务状态 # --post-logs 部署后显示日志 -# --init-env 初始化远程 .env +# --init-env 初始化 systemd service 模板 +# --deploy-service 上传 systemd service 文件到服务器 # =========================================== set -euo pipefail @@ -32,7 +33,7 @@ APP_ENV="production" DRY_RUN=false; SKIP_CONFIRM=false; SKIP_TESTS=false ROLLBACK=false; LIST_BACKUPS=false; VIEW_LOGS=false; TAIL_LOGS=false LOG_LINES=50; VIEW_STATUS=false; POST_DEPLOY_LOGS=false; INIT_ENV=false -BACKUP_PATH="" +DEPLOY_SERVICE=false; BACKUP_PATH="" # ---------- 选项解析 ---------- while [[ $# -gt 0 ]]; do @@ -49,6 +50,7 @@ while [[ $# -gt 0 ]]; do --status) VIEW_STATUS=true ;; --post-logs) POST_DEPLOY_LOGS=true ;; --init-env) INIT_ENV=true ;; + --deploy-service) DEPLOY_SERVICE=true ;; --help|-h) echo "用法: $0 [development|production] [options]" echo "选项:" @@ -61,7 +63,8 @@ while [[ $# -gt 0 ]]; do echo " --tail [N] 实时跟踪日志" echo " --status 查看服务状态" echo " --post-logs 部署后显示日志" - echo " --init-env 初始化远程 .env" + echo " --init-env 初始化 systemd service 模板" + echo " --deploy-service 上传 systemd service 文件到服务器" exit 0 ;; *) if [[ "$1" == "development" || "$1" == "production" ]]; then @@ -98,6 +101,7 @@ ssh_connect if [ "$LIST_BACKUPS" = true ]; then list_backups; exit 0; fi if [ "$INIT_ENV" = true ]; then init_env; exit 0; fi +if [ "$DEPLOY_SERVICE" = true ]; then deploy_service_file; exit 0; fi if [ "$VIEW_LOGS" = true ]; then view_logs; exit 0; fi if [ "$TAIL_LOGS" = true ]; then tail_logs; exit 0; fi if [ "$VIEW_STATUS" = true ]; then status; exit 0; fi @@ -107,19 +111,20 @@ if [ "$ROLLBACK" = true ]; then rollback; exit 0; fi show_deploy_info confirm_deploy check_dependencies -check_env_file +check_service_file +check_ssl_expiry build_project backup_old_version cleanup_old_backups 5 upload_binary -upload_dir "配置文件" config upload_dir "迁移文件" migrations upload_script "测试脚本" test_deployment.sh test_deployment.sh upload_script "清理脚本" scripts/cleanup_refresh_tokens.sh scripts/cleanup_refresh_tokens.sh upload_script "备份脚本" scripts/backup-db.sh scripts/backup-db.sh +deploy_service_file restart_service run_migrations diff --git a/lib/common.sh b/lib/common.sh index c876b02..07e8130 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -62,71 +62,101 @@ EOF curl -s -X POST "${WEBHOOK_URL}" -H "Content-Type: application/json" -d "${payload}" >/dev/null 2>&1 || true } -# ---------- 环境配置 ---------- -check_env_file() { - log_step "检查远程 .env 文件..." - [ "$DRY_RUN" = true ] && { log_dry "test -f ${REMOTE_DIR}/.env"; return 0; } +# ---------- 环境配置(systemd service)---------- +get_service_file_local() { echo "${SCRIPT_DIR}/deploy/rust-backend-${APP_ENV}.service"; } - if ! remote "test -f '${REMOTE_DIR}/.env'" 2>/dev/null; then - log_error ".env 文件不存在: ${REMOTE_DIR}/.env"; exit 1 +check_service_file() { + log_step "检查远程 systemd service 文件..." + [ "$DRY_RUN" = true ] && { log_dry "检查 ${SERVICE_NAME}"; return 0; } + + if ! remote "systemctl cat '${SERVICE_NAME}'" &>/dev/null; then + log_error "systemd service ${SERVICE_NAME} 不存在,请先用 --deploy-service 安装" + exit 1 fi - local env_content; env_content=$(remote "cat '${REMOTE_DIR}/.env'" 2>/dev/null || echo "") - [ -z "$env_content" ] && { log_error ".env 文件为空"; exit 1; } + local content; content=$(remote "systemctl cat '${SERVICE_NAME}'" 2>/dev/null || echo "") + [ -z "$content" ] && { log_error "service 内容为空"; exit 1; } - local missing=0 ph=0 commented=0 - local required_vars=("JWT_SECRET" "WECHAT_SECRET" "WECHAT_APPID" "DATABASE_URL") - [ "${APP_ENV}" = "production" ] && required_vars+=("SSL_KEY_PATH" "SSL_CERT_PATH") + local missing=0 ph=0 + local required_vars=("DATABASE_URL" "JWT_SECRET" "WECHAT_APPID" "WECHAT_SECRET") + [ "${APP_ENV}" = "production" ] && required_vars+=("SSL_KEY_PATH" "SSL_CERT_PATH" "ALIPAY_APP_ID") for var in "${required_vars[@]}"; do - local val; val=$(echo "$env_content" | grep -E "^${var}=" | head -1 | sed "s/^${var}=//") + local val; val=$(echo "$content" | grep -E "^Environment=${var}=" | head -1 | sed "s/^Environment=${var}=//") if [ -z "$val" ]; then - log_error " 缺少: ${var}="; missing=$((missing+1)) + log_error " 缺少: Environment=${var}="; missing=$((missing+1)) elif echo "$val" | grep -qiE "^(your_|placeholder|change_me|xxx|test_)"; then log_warn " 占位符: ${var}=${val}"; ph=$((ph+1)) fi - echo "$env_content" | grep -qE "^#\s*${var}=" && { log_warn " 已注释: # ${var}="; commented=$((commented+1)); } done - [ $missing -gt 0 ] && { log_error ".env 检查失败:${missing} 个缺失"; exit 1; } - [ $ph -gt 0 ] || [ $commented -gt 0 ] && log_warn ".env 存在 ${ph} 个占位符、${commented} 个注释行,建议修正" - log_info ".env 文件检查通过" + [ $missing -gt 0 ] && { log_error "service 配置检查失败:${missing} 个缺失"; exit 1; } + [ $ph -gt 0 ] && log_warn "service 存在 ${ph} 个占位符,部署后可能无法正常运行" + log_info "service 配置检查通过" +} + +deploy_service_file() { + log_step "部署 systemd service 文件..." + [ "$DRY_RUN" = true ] && { log_dry "上传 ${SERVICE_FILE_LOCAL} → /etc/systemd/system/${SERVICE_NAME}"; return 0; } + + if [ ! -f "$(get_service_file_local)" ]; then + log_error "本地 service 文件不存在: ${SERVICE_FILE_LOCAL}" + log_info "可通过 --init-env 生成模板" + exit 1 + fi + + rsync -avzP "$(get_service_file_local)" "${REMOTE_USER}@${REMOTE_HOST}:/etc/systemd/system/${SERVICE_NAME}" + remote "systemctl daemon-reload" + log_info "service 文件部署完成,使用 systemctl restart ${SERVICE_NAME} 重启" } init_env() { - log_step "初始化远程 .env 文件..." - [ "$DRY_RUN" = true ] && { log_dry "在 ${REMOTE_HOST}:${REMOTE_DIR}/.env 生成配置"; return 0; } + log_step "初始化 systemd service 文件(从模板生成)..." + [ "$DRY_RUN" = true ] && { log_dry "生成 ${SERVICE_FILE_LOCAL}"; return 0; } - local exists=false - remote "test -f '${REMOTE_DIR}/.env'" 2>/dev/null && exists=true - - if [ "$exists" = true ]; then - log_warn ".env 已存在,继续将覆盖!" + if [ -f "$(get_service_file_local)" ]; then + log_warn "${SERVICE_FILE_LOCAL} 已存在,继续将覆盖!" read -p "是否覆盖? [y/N] " -n 1 -r; echo [[ ! $REPLY =~ ^[Yy]$ ]] && { log_warn "已取消"; return 0; } - remote "cp '${REMOTE_DIR}/.env' '${REMOTE_DIR}/.env.backup.$(date +%Y%m%d_%H%M%S)'" 2>/dev/null || true + cp "$(get_service_file_local)" "${SERVICE_FILE_LOCAL}.bak.$(date +%Y%m%d_%H%M%S)" fi local jwt_secret jwt_secret=$(openssl rand -base64 32 2>/dev/null | tr -d '\n' || echo "change-me-$(date +%s)-$$") - cat < '${REMOTE_DIR}/.env' && chmod 600 '${REMOTE_DIR}/.env'" -# ASD Rust Backend - ${APP_ENV} 环境配置 -# 由 deploy.sh --init-env 自动生成于 $(date '+%Y-%m-%d %H:%M:%S') -APP_ENV=${APP_ENV} -DATABASE_URL=请替换为实际数据库连接字符串 -WECHAT_APPID=请替换为实际微信 AppID -WECHAT_SECRET=请替换为实际微信 Secret -JWT_SECRET=${jwt_secret} -RUST_LOG=info -FREE_USER_DATA_LIMIT=20 -# ===== 支付宝支付配置(可选) ===== -# ALIPAY_APP_ID=... -# ALIPAY_GATEWAY=... -ENV_EOF + mkdir -p "${SCRIPT_DIR}/deploy" + cat > "$(get_service_file_local)" << SERVEOF +[Unit] +Description=Rust Backend ${APP_ENV^} Service +After=network.target postgresql.service +Wants=network.target - log_info ".env 已生成,文件权限 600" +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=${REMOTE_DIR} +ExecStart=${REMOTE_DIR}/rust-backend +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +Environment=DATABASE_URL=请替换为实际数据库连接 +Environment=WECHAT_APPID=请替换为实际微信 AppID +Environment=WECHAT_SECRET=请替换为实际微信 Secret +Environment=JWT_SECRET=${jwt_secret} +Environment=SSL_KEY_PATH= +Environment=SSL_CERT_PATH= +Environment=RUST_LOG=info +Environment=APP_VERSION=0.3.0 +Environment=FREE_USER_DATA_LIMIT=20 +SERVEOF + + chmod 600 "$(get_service_file_local)" + log_info "service 模板已生成: ${SERVICE_FILE_LOCAL}" log_warn "请手动填写 DATABASE_URL / WECHAT_APPID / WECHAT_SECRET" + log_info "然后使用 ./deploy.sh ${APP_ENV} --deploy-service 上传" } # ---------- 编译 ---------- @@ -266,6 +296,11 @@ run_migrations() { restart_service() { log_step "重启服务 '${SERVICE_NAME}'..." [ "$DRY_RUN" = true ] && { log_dry "systemctl restart ${SERVICE_NAME}"; return 0; } + # 部署前先确保 service 文件存在 + if ! remote "systemctl cat '${SERVICE_NAME}'" &>/dev/null; then + log_warn "${SERVICE_NAME} 未安装,尝试部署 service 文件..." + deploy_service_file + fi remote "systemctl restart ${SERVICE_NAME}" && log_info "服务重启成功" || log_error "服务重启失败!" sleep 3 } @@ -363,6 +398,25 @@ show_deploy_info() { echo -e "${BLUE}========================================${NC}" } +# ---------- SSL 证书检查 ---------- +check_ssl_expiry() { + [ "${APP_ENV}" != "production" ] && return 0 # 非生产环境跳过 + 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 "") + + 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") + log_warn "SSL 证书将于 ${expiry} 到期(不足 30 天)!" + [ "$days_left" -gt "0" ] && log_warn " 警告:证书将在 1 天内到期,请立即续期!" + else + log_info "SSL 证书有效期超过 30 天 ✅" + fi +} + # ---------- 依赖检查 ---------- check_dependencies() { log_step "检查依赖..."