Files

380 lines
11 KiB
Bash
Executable File

#!/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