|
|
#!/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 "$@"
|