#!/usr/bin/env bash # 宝塔宿主机 MySQL + Docker Redis — 192.168.2.14:30325 # 用法(/home/project/chajia): # bash deploy/install-bt-host-mysql.sh # 若尚无 .env,会自动从 deploy/.env.bt-host-mysql.example 复制 set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.bt-host-mysql.yml" ENV_FILE="${ROOT_DIR}/.env" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[deploy-bt]${NC} $*"; } warn() { echo -e "${YELLOW}[deploy-bt]${NC} $*"; } err() { echo -e "${RED}[deploy-bt]${NC} $*" >&2; } # 只读取单个变量,避免 source .env 时 RPA_SELECTOR 里的 >> / Pick up 被 bash 当命令执行 read_env() { local key="$1" local line line="$(grep -E "^[[:space:]]*${key}=" "$ENV_FILE" 2>/dev/null | tail -1 || true)" if [[ -z "$line" ]]; then echo "" return fi line="${line#*=}" line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" if [[ "$line" == \"*\" && "$line" == *\" ]]; then line="${line:1:${#line}-2}" elif [[ "$line" == \'*\' && "$line" == *\' ]]; then line="${line:1:${#line}-2}" fi echo "$line" } validate_env() { local example="${ROOT_DIR}/deploy/.env.bt-host-mysql.example" if [[ ! -f "$ENV_FILE" ]]; then if [[ -f "$example" ]]; then log "未找到 .env,从 deploy/.env.bt-host-mysql.example 自动复制" cp "$example" "$ENV_FILE" else err "未找到 .env 且缺少 ${example}" exit 1 fi fi DATABASE_URL="$(read_env DATABASE_URL)" JWT_SECRET="$(read_env JWT_SECRET)" HOST_SERVICE_TOKENS="$(read_env HOST_SERVICE_TOKENS)" CUSTOMER_REGISTRY="$(read_env CUSTOMER_REGISTRY)" APP_PORT="$(read_env APP_PORT)" PUBLIC_HOST="$(read_env PUBLIC_HOST)" RPA_MOCK_MODE="$(read_env RPA_MOCK_MODE)" local missing=0 for key in DATABASE_URL JWT_SECRET HOST_SERVICE_TOKENS CUSTOMER_REGISTRY; do local val="${!key:-}" if [[ -z "$val" ]] || [[ "$val" == CHANGE_ME* ]]; then err "请在 .env 中填写: $key" missing=1 fi done if [[ "${RPA_MOCK_MODE:-false}" == "true" ]]; then warn "RPA_MOCK_MODE=true 仅用于测试,生产请设为 false" fi validate_database_credentials if [[ $missing -ne 0 ]]; then exit 1 fi } validate_database_credentials() { local pass db_url pass="$(read_env MYSQL_PASSWORD)" db_url="$(read_env DATABASE_URL)" if [[ "$db_url" == *"7WGSADrnDRTh6firE"* ]]; then err "DATABASE_URL 含已知错误密码(7WGSADrn...),宝塔正确密码为 7WGSADm..." err "请修改 .env 中 MYSQL_PASSWORD 与 DATABASE_URL 后重试" exit 1 fi if [[ -n "$pass" && -n "$db_url" && "$db_url" != *":${pass}@"* ]]; then err "DATABASE_URL 中的密码与 MYSQL_PASSWORD 不一致" err "请保持两者与宝塔 MySQL 用户 chajia 的密码完全相同" exit 1 fi if [[ "$db_url" == *"@127.0.0.1:3307"* ]] || [[ "$db_url" == *"@localhost:3307"* ]]; then err "DATABASE_URL 指向本机开发库 3307,宝塔部署应使用 host.docker.internal:3306" exit 1 fi } preflight_mysql() { local db_pass db_pass="$(read_env MYSQL_PASSWORD)" if [[ -z "$db_pass" ]]; then return 0 fi log "测试宝塔 MySQL 连接..." if docker run --rm --add-host=host.docker.internal:host-gateway mysql:8 \ mysql -h host.docker.internal -u chajia -p"${db_pass}" -e "SELECT 1" chajia >/dev/null 2>&1; then log "MySQL 连接正常" return 0 fi err "MySQL 认证失败(P1000):请核对 .env 中 MYSQL_PASSWORD 与 DATABASE_URL 是否与宝塔一致" err "常见原因:重新上传代码覆盖了 .env(上传包不再含根目录 .env,请手动保留服务器 .env)" err "修复示例:" err " MYSQL_PASSWORD=宝塔里 chajia 用户的真实密码" err " DATABASE_URL=mysql://chajia:真实密码@host.docker.internal:3306/chajia" exit 1 } deploy_docker() { if ! command -v docker >/dev/null 2>&1; then err "未安装 Docker" exit 1 fi if ! docker compose version >/dev/null 2>&1; then err "需要 Docker Compose V2" exit 1 fi cd "$ROOT_DIR" local compose=(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE") log "构建并启动(无容器 MySQL,Redis 在 Docker 内)..." "${compose[@]}" up -d --build log "校验数据库迁移(连接宝塔 MySQL)..." "${compose[@]}" run --rm --no-deps rpa-worker npx prisma migrate deploy log "同步默认管理员账号(admin_demo / kj123456)..." "${compose[@]}" run --rm --no-deps rpa-worker npx prisma db seed log "容器状态:" "${compose[@]}" ps } smoke_check() { local port="${APP_PORT:-30325}" local url="http://127.0.0.1:${port}" log "健康检查: ${url}" if curl -sf -o /dev/null -w "%{http_code}" "$url" | grep -qE '200|307|308'; then log "Web 服务正常" else warn "Web 暂未响应,请查看:docker compose -f deploy/docker-compose.bt-host-mysql.yml logs -f next-app" fi preflight_service_token "$port" } preflight_service_token() { local port="${1:-30325}" local token="${2:-chajia-neibu-2026}" log "校验 Service Token (${token})..." local http_code http_code="$(curl -s -o /dev/null -w "%{http_code}" \ "http://127.0.0.1:${port}/api/quotes/history?page=1&size=1" \ -H "Authorization: Bearer ${token}" \ -H "X-Customer-Id: CUST_001" \ -H "Cache-Control: no-store" 2>/dev/null || echo "000")" if [[ "$http_code" == "401" ]]; then err "Service Token 无效:请在 .env 的 HOST_SERVICE_TOKENS 中包含 \"${token}\"" err "示例:HOST_SERVICE_TOKENS={\"${token}\":{\"customerId\":\"CUST_001\",\"permissions\":[\"pricing:markup:write\"]}}" err "修改后执行:docker compose --env-file .env -f deploy/docker-compose.bt-host-mysql.yml restart next-app" exit 1 fi if [[ "$http_code" =~ ^[245] ]]; then log "Service Token 校验通过 (HTTP ${http_code})" else warn "Service Token 未确认 (HTTP ${http_code}),请手动测试 embed-demo" fi preflight_quote_poll_route "$port" } preflight_quote_poll_route() { local port="${1:-30325}" log "校验 GET /api/quotes/{id} 路由..." local body http_code body="$(curl -s \ "http://127.0.0.1:${port}/api/quotes/QTE_smoke_test" \ -H "Authorization: Bearer chajia-neibu-2026" \ -H "X-Customer-Id: CUST_001" \ -H "Accept: application/json" \ -H "Cache-Control: no-store" 2>/dev/null || true)" if [[ "$body" == \{* ]]; then log "询价轮询 API 正常(返回 JSON)" return 0 fi err "GET /api/quotes/{id} 返回 HTML 404,embed-demo 会卡在「正在查询报价」" err "原因:上传包缺少 app/api/quotes/[quote_id]/route.ts(Windows 打包方括号路径问题)" err "请重新打包上传后执行:bash deploy/install-bt-host-mysql.sh" exit 1 } print_summary() { local port="${APP_PORT:-30325}" local host="${PUBLIC_HOST:-<服务器内网IP>}" echo "" log "========== 部署完成(宝塔 MySQL 模式)==========" echo " 内网访问: http://${host}:${port}" echo " 管理端: http://${host}:${port}/admin/alerts" echo " 嵌入演示: http://${host}:${port}/embed-demo" echo " API 基址: http://${host}:${port}" echo "" echo " 其它内网系统请求头:" echo " Authorization: Bearer chajia-neibu-2026" echo " X-Customer-Id: CUST_001" echo "" echo " 常用命令:" echo " docker compose --env-file .env -f deploy/docker-compose.bt-host-mysql.yml logs -f rpa-worker" echo " docker compose --env-file .env -f deploy/docker-compose.bt-host-mysql.yml restart" echo "" warn "若 embed-demo 长时间「正在查询报价」,多为 RPA Worker 被慢任务阻塞:" warn " docker compose --env-file .env -f deploy/docker-compose.bt-host-mysql.yml restart rpa-worker" warn "建议在 .env 设置 RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true(模板已默认开启)" } main() { validate_env preflight_mysql deploy_docker smoke_check print_summary } main "$@"