#!/bin/bash # 安职通 一键部署脚本 # 部署到 154.8.162.18 -> /var/www/turbohr/ -> https://on.hr8ai.top/ # # 用法: bash deploy.sh # 首次部署: bash deploy.sh --init set -e # =========================================== # 配置区 # =========================================== REMOTE_HOST="154.8.162.18" REMOTE_USER="root" REMOTE_PASS="Why_701208" REMOTE_DIR="/var/www/turbohr" DOMAIN="on.hr8ai.top" APP_NAME="turbohr" BACKEND_PORT=3400 LOCAL_DIR="$(cd "$(dirname "$0")" && pwd)" # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[deploy]${NC} $1"; } warn() { echo -e "${YELLOW}[warn]${NC} $1"; } err() { echo -e "${RED}[error]${NC} $1"; } # 检查 sshpass if ! command -v sshpass &> /dev/null; then err "请先安装 sshpass: macOS -> brew install hudochenkov/sshpass/sshpass" exit 1 fi SSH_CMD="sshpass -p '$REMOTE_PASS' ssh -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST}" SCP_CMD="sshpass -p '$REMOTE_PASS' scp -o StrictHostKeyChecking=no" # =========================================== # 远程执行命令的封装 # =========================================== remote_exec() { sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "$1" } remote_copy() { sshpass -p "$REMOTE_PASS" scp -o StrictHostKeyChecking=no -r "$1" "${REMOTE_USER}@${REMOTE_HOST}:$2" } # =========================================== # 首次初始化:创建数据库 + Nginx 配置(系统依赖已预装) # =========================================== init_server() { log "========== 首次初始化服务器 ==========" remote_exec " set -e # 服务器已有: Node.js, PM2, PostgreSQL 15, Nginx, Redis, 宝塔面板 # 端口 3000-3300 已被占用,本应用使用端口 ${BACKEND_PORT} echo '[1/4] 创建 PostgreSQL 数据库...' sudo -u postgres psql -c \"CREATE DATABASE turbohr;\" 2>/dev/null || echo ' 数据库 turbohr 已存在' sudo -u postgres psql -c \"CREATE USER turbohr WITH PASSWORD 'turbohr2026';\" 2>/dev/null || echo ' 用户 turbohr 已存在' sudo -u postgres psql -c \"GRANT ALL PRIVILEGES ON DATABASE turbohr TO turbohr;\" 2>/dev/null || true sudo -u postgres psql -c \"ALTER USER turbohr WITH SUPERUSER;\" 2>/dev/null || true echo ' PostgreSQL 数据库 turbohr 就绪' echo '[2/4] 创建部署目录...' mkdir -p ${REMOTE_DIR}/backend mkdir -p ${REMOTE_DIR}/frontend/dist mkdir -p ${REMOTE_DIR}/backend/uploads echo '[3/4] 配置 Nginx (conf.d)...' cat > /etc/nginx/conf.d/${DOMAIN}.conf << 'NGINXEOF' server { listen 80; server_name __DOMAIN__; # 前端静态文件 location / { root __REMOTE_DIR__/frontend/dist; index index.html; try_files \$uri \$uri/ /index.html; # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)\$ { expires 30d; add_header Cache-Control \"public, immutable\"; } } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:__BACKEND_PORT__; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # SSE 流式输出支持(AI 对话) proxy_buffering off; proxy_cache off; proxy_set_header Connection ''; # 超时设置(AI 生成可能较慢) proxy_read_timeout 300s; proxy_send_timeout 300s; # 文件上传 client_max_body_size 10m; } # 上传文件访问 location /uploads/ { alias __REMOTE_DIR__/backend/uploads/; } # 安全头 add_header X-Frame-Options \"SAMEORIGIN\"; add_header X-Content-Type-Options \"nosniff\"; add_header X-XSS-Protection \"1; mode=block\"; # Gzip 压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; gzip_min_length 1000; } NGINXEOF # 替换占位符 sed -i 's|__DOMAIN__|${DOMAIN}|g' /etc/nginx/conf.d/${DOMAIN}.conf sed -i 's|__REMOTE_DIR__|${REMOTE_DIR}|g' /etc/nginx/conf.d/${DOMAIN}.conf sed -i 's|__BACKEND_PORT__|${BACKEND_PORT}|g' /etc/nginx/conf.d/${DOMAIN}.conf nginx -t && systemctl reload nginx echo '[4/4] 检查 certbot...' if ! command -v certbot &> /dev/null; then echo ' certbot 未安装,SSL 将在部署后手动配置' else echo ' certbot 已安装' fi echo '' echo '=========================================' echo ' 系统初始化完成!' echo ' 数据库: turbohr (用户: turbohr)' echo ' 端口: ${BACKEND_PORT} (避开 3000-3300)' echo ' Nginx: ${DOMAIN} -> :80 (conf.d)' echo '=========================================' " log "系统初始化完成" } # =========================================== # 构建前端 # =========================================== build_frontend() { log "========== 构建前端 ==========" cd "${LOCAL_DIR}/frontend" if [ ! -d node_modules ]; then log "安装前端依赖..." npm install fi log "构建前端..." npm run build if [ ! -d dist ]; then err "前端构建失败,dist 目录不存在" exit 1 fi log "前端构建完成: dist/ ($(du -sh dist | cut -f1))" } # =========================================== # 构建后端 # =========================================== build_backend() { log "========== 构建后端 ==========" cd "${LOCAL_DIR}/backend" if [ ! -d node_modules ]; then log "安装后端依赖..." npm install fi log "生成 Prisma Client..." npx prisma generate # 服务器用 tsx 直接运行 TS 源码,跳过 tsc 编译 log "跳过 tsc 编译(服务器使用 tsx 运行)" } # =========================================== # 部署到服务器 # =========================================== deploy_files() { log "========== 部署文件到服务器 ==========" # 同步后端(排除 node_modules、.env、uploads、备份文件) log "[1/4] 同步后端代码..." sshpass -p "$REMOTE_PASS" rsync -avz --delete \ --exclude='node_modules' \ --exclude='.env' \ --exclude='uploads/*' \ --exclude='*.dump' \ --exclude='backups' \ "${LOCAL_DIR}/backend/" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/backend/" # 同步后端 package.json 和 prisma(确保依赖和 schema 同步) log "[2/4] 同步前端静态文件..." sshpass -p "$REMOTE_PASS" rsync -avz --delete \ "${LOCAL_DIR}/frontend/dist/" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/frontend/dist/" # 在服务器上安装后端依赖 + 生成 Prisma Client log "[3/4] 服务器端安装依赖..." remote_exec "cd ${REMOTE_DIR}/backend && npm install --production=false 2>&1 | tail -5" log "[4/4] 生成 Prisma Client..." remote_exec "cd ${REMOTE_DIR}/backend && npx prisma generate 2>&1 | tail -3" } # =========================================== # 配置 .env(首次或更新) # =========================================== setup_env() { log "========== 配置环境变量 ==========" # 检查远程是否已有 .env,如果没有则创建 remote_exec " if [ ! -f ${REMOTE_DIR}/backend/.env ]; then echo '创建 .env 文件...' cat > ${REMOTE_DIR}/backend/.env << 'ENVEOF' # 数据库 DATABASE_URL=postgresql://turbohr:turbohr2026@localhost:5432/turbohr?schema=public # JWT JWT_SECRET=turbohr-jwt-secret-2026-change-me JWT_REFRESH_SECRET=turbohr-refresh-secret-2026-change-me # DashScope (通义千问) - 请填写实际 API Key DASHSCOPE_API_KEY=sk-xxx DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # 加密 ENCRYPTION_KEY=turbohr-32byte-encryption-key-here # 存储 SUPABASE_URL= SUPABASE_KEY= # 部署 PORT=${BACKEND_PORT} CORS_ORIGIN=https://${DOMAIN} ENVEOF echo '.env 已创建,请编辑 ${REMOTE_DIR}/backend/.env 填写实际配置' else echo '.env 已存在,跳过创建' fi " } # =========================================== # 数据库迁移 # =========================================== migrate_db() { log "========== 数据库迁移 ==========" remote_exec "cd ${REMOTE_DIR}/backend && npx prisma db push --accept-data-loss 2>&1 | tail -10" log "数据库 schema 同步完成" } # =========================================== # 配置 PM2 + 启动服务 # =========================================== start_service() { log "========== 启动服务 ==========" remote_exec " cd ${REMOTE_DIR}/backend # 停止旧进程 pm2 delete ${APP_NAME} 2>/dev/null || true # 使用 tsx 运行 TypeScript 源码(避免 ESM 扩展名问题) pm2 start npx --name ${APP_NAME} --max-memory-restart 512M -- tsx src/index.ts # 保存 PM2 配置 pm2 save # 配置开机自启 pm2 startup systemd -u root --hp /root 2>/dev/null | tail -3 echo 'PM2 进程:' pm2 list " log "服务已启动" } # =========================================== # 配置 SSL 证书 # =========================================== setup_ssl() { log "========== 配置 SSL 证书 ==========" remote_exec " # 使用 --nginx 插件自动配置 SSL certbot --nginx -d ${DOMAIN} --non-interactive --agree-tos --register-unsafely-without-email --redirect 2>&1 && echo 'SSL 证书配置成功' || echo 'SSL 证书配置失败,可稍后手动执行: certbot --nginx -d ${DOMAIN}' " } # =========================================== # 验证部署 # =========================================== verify() { log "========== 验证部署 ==========" # 检查后端 BACKEND_CODE=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${BACKEND_PORT}/api/v1/health 2>/dev/null || echo '000'") log "后端 /api/v1/health -> HTTP ${BACKEND_CODE}" # 检查前端 FRONTEND_CODE=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/ 2>/dev/null || echo '000'") log "前端 (Nginx) -> HTTP ${FRONTEND_CODE}" # 检查域名 DOMAIN_CODE=$(curl -s -o /dev/null -w '%{http_code}' "https://${DOMAIN}/" 2>/dev/null || echo '000') log "域名 https://${DOMAIN} -> HTTP ${DOMAIN_CODE}" # PM2 状态 remote_exec "pm2 describe ${APP_NAME} 2>/dev/null | grep -E 'status|restarts|uptime|pid'" echo "" echo "=========================================" echo " 安职通 部署完成!" echo " 访问地址: https://${DOMAIN}/" echo " 后端 API: https://${DOMAIN}/api/v1/" echo " PM2 管理: pm2 list (服务器上)" echo "=========================================" } # =========================================== # 主流程 # =========================================== IS_INIT=false if [ "$1" = "--init" ]; then IS_INIT=true fi log "开始部署 安职通 到 ${REMOTE_HOST}" log "域名: ${DOMAIN} 目录: ${REMOTE_DIR}" echo "" if [ "$IS_INIT" = true ]; then init_server setup_env echo "" warn "首次初始化完成,请先编辑服务器上的 ${REMOTE_DIR}/backend/.env 填写实际配置" warn "然后重新运行: bash deploy.sh" exit 0 fi build_frontend build_backend deploy_files setup_env migrate_db start_service verify