3f39c56f4b
- 部署脚本 deploy.sh:一键部署到 154.8.162.18,端口3400,Nginx conf.d - 应用名改为「企业用工专家」(登录/注册/侧边栏/员工端等6处) - 移动端适配修复:Attendance/Dashboard/Settings/Contracts 6处grid响应式 - 帮助系统:HelpModal 组件 + TopNav 帮助图标 + RAG语义搜索集成 - 后端 rag.service.ts 新增23条帮助种子数据 + searchHelp() - 前端搜索调用 /ai/rag/help-search,RAG不可用时回退关键词匹配 - 网页图标 favicon.svg(indigo主色+建筑人形图案) - index.html 标题改为「企业用工专家」
387 lines
11 KiB
Bash
Executable File
387 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
|
|
|
|
log "编译 TypeScript..."
|
|
npx tsc
|
|
|
|
if [ ! -d dist ]; then
|
|
err "后端编译失败,dist 目录不存在"
|
|
exit 1
|
|
fi
|
|
|
|
log "后端编译完成: dist/ ($(du -sh dist | cut -f1))"
|
|
}
|
|
|
|
# ===========================================
|
|
# 部署到服务器
|
|
# ===========================================
|
|
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
|