#!/usr/bin/env bash # 客服云 — 腾讯云 / 任意 Linux 一键生产部署 # 用法: # ./deploy/scripts/deploy.sh # 构建并启动 # ./deploy/scripts/deploy.sh --seed # 启动后导入演示账号数据 # ./deploy/scripts/deploy.sh --pull-only # 仅 pull/up,不强制 rebuild(需已有镜像) # ./deploy/scripts/deploy.sh --down # 停止并移除容器(保留数据卷) # ./deploy/scripts/deploy.sh --status # 查看状态 set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT_DIR" COMPOSE_FILE="docker-compose.prod.yml" ENV_FILE="deploy/.env.prod" ENV_EXAMPLE="deploy/.env.prod.example" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[deploy]${NC} $*"; } warn() { echo -e "${YELLOW}[warn]${NC} $*"; } err() { echo -e "${RED}[error]${NC} $*" >&2; } need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then err "缺少命令: $1" exit 1 fi } compose() { docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@" } rand_hex() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 24 else head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 48 fi } ensure_env() { if [[ ! -f "$ENV_FILE" ]]; then if [[ ! -f "$ENV_EXAMPLE" ]]; then err "找不到 $ENV_EXAMPLE" exit 1 fi log "未找到 $ENV_FILE,从示例生成…" cp "$ENV_EXAMPLE" "$ENV_FILE" local jwt dbpass minio_pass jwt="$(rand_hex)" dbpass="$(rand_hex)" minio_pass="K$(rand_hex | head -c 16)" # macOS / GNU sed 兼容 if sed --version >/dev/null 2>&1; then sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${jwt}|" "$ENV_FILE" sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=${dbpass}|" "$ENV_FILE" sed -i "s|^STORAGE_SECRET_KEY=.*|STORAGE_SECRET_KEY=${minio_pass}|" "$ENV_FILE" else sed -i '' "s|^JWT_SECRET=.*|JWT_SECRET=${jwt}|" "$ENV_FILE" sed -i '' "s|^DB_PASSWORD=.*|DB_PASSWORD=${dbpass}|" "$ENV_FILE" sed -i '' "s|^STORAGE_SECRET_KEY=.*|STORAGE_SECRET_KEY=${minio_pass}|" "$ENV_FILE" fi warn "已生成随机 JWT_SECRET / DB_PASSWORD / STORAGE_SECRET_KEY" warn "请编辑 $ENV_FILE,至少设置:" warn " APP_BASE_URL=http://你的公网IP或域名" warn " STORAGE_PUBLIC_BASE_URL=http://你的公网IP或域名/files" echo read -r -p "设置完成后按回车继续(或 Ctrl+C 退出先改配置): " _ fi # shellcheck disable=SC1090 set -a # 去除 Windows CRLF source <(sed 's/\r$//' "$ENV_FILE" | grep -v '^\s*#' | grep -v '^\s*$' || true) set +a if [[ -z "${JWT_SECRET:-}" || "${JWT_SECRET}" == *"请改"* ]]; then err "请在 $ENV_FILE 中设置有效的 JWT_SECRET" exit 1 fi if [[ -z "${DB_PASSWORD:-}" || "${DB_PASSWORD}" == *"请改"* ]]; then err "请在 $ENV_FILE 中设置有效的 DB_PASSWORD" exit 1 fi if [[ -z "${STORAGE_PUBLIC_BASE_URL:-}" || "${STORAGE_PUBLIC_BASE_URL}" == *"你的"* ]]; then err "请在 $ENV_FILE 中设置 STORAGE_PUBLIC_BASE_URL(如 http://1.2.3.4/files)" exit 1 fi if [[ -z "${STORAGE_ACCESS_KEY:-}" || -z "${STORAGE_SECRET_KEY:-}" ]]; then err "请设置 STORAGE_ACCESS_KEY / STORAGE_SECRET_KEY" exit 1 fi if [[ ${#STORAGE_SECRET_KEY} -lt 8 ]]; then err "STORAGE_SECRET_KEY 长度至少 8 位(MinIO 要求)" exit 1 fi } wait_healthy() { local url="${1:-http://127.0.0.1:${HTTP_PORT:-80}/health}" local i log "等待服务就绪: $url" for i in $(seq 1 60); do if curl -fsS "$url" >/dev/null 2>&1; then log "健康检查通过" return 0 fi sleep 2 done err "健康检查超时,请执行: docker compose -f $COMPOSE_FILE --env-file $ENV_FILE logs" return 1 } print_summary() { local base="${APP_BASE_URL:-http://127.0.0.1:${HTTP_PORT:-80}}" echo log "========== 部署完成 ==========" echo " 访问地址: ${base}" echo " 登录页: ${base}/login" echo " 健康检查: ${base}/health" echo " Widget 预览: ${base}/widget/preview" echo " 图片前缀: ${STORAGE_PUBLIC_BASE_URL}" echo echo " 常用命令:" echo " 查看状态 ./deploy/scripts/deploy.sh --status" echo " 查看日志 docker compose -f $COMPOSE_FILE --env-file $ENV_FILE logs -f" echo " 导入种子 ./deploy/scripts/deploy.sh --seed-only" echo " 停止服务 ./deploy/scripts/deploy.sh --down" echo echo " 安全提示: 安全组仅放行 80/443;改默认密码;生产务必使用 HTTPS。" echo "==============================" } cmd_up() { local seed=0 rebuild=1 while [[ $# -gt 0 ]]; do case "$1" in --seed) seed=1 ;; --pull-only) rebuild=0 ;; *) ;; esac shift done need_cmd docker need_cmd curl if ! docker compose version >/dev/null 2>&1; then err "需要 Docker Compose V2(docker compose)" exit 1 fi ensure_env log "拉取基础镜像…" compose pull postgres minio minio-init || true if [[ "$rebuild" -eq 1 ]]; then log "构建并启动全部服务(api + web + db + minio)…" compose up -d --build --remove-orphans else log "启动服务(不强制 rebuild)…" compose up -d --remove-orphans fi wait_healthy "http://127.0.0.1:${HTTP_PORT:-80}/health" || true wait_healthy "http://127.0.0.1:${HTTP_PORT:-80}/healthz" || true if [[ "$seed" -eq 1 ]]; then log "导入演示种子数据…" compose --profile seed run --rm seed || warn "种子导入失败(可能已存在数据)" fi compose ps print_summary } cmd_seed_only() { need_cmd docker ensure_env log "运行 seed…" compose --profile seed run --rm seed log "种子完成。默认租户管理员 kefu_admin / kefu_admin123 (若 seed 成功)" } cmd_down() { need_cmd docker ensure_env log "停止容器(保留数据卷)…" compose down log "已停止。删除数据卷请手动: docker volume rm kefu-cloud_kefu_cloud_pgdata kefu-cloud_kefu_cloud_miniodata" } cmd_status() { need_cmd docker if [[ -f "$ENV_FILE" ]]; then compose ps echo curl -fsS "http://127.0.0.1:${HTTP_PORT:-80}/health" && echo || warn "health 不可达" else err "缺少 $ENV_FILE" exit 1 fi } main() { local action="${1:-up}" case "$action" in up|--up|"") shift || true cmd_up "$@" ;; --seed) # 兼容:./deploy.sh --seed cmd_up --seed ;; --seed-only) cmd_seed_only ;; --down|down) cmd_down ;; --status|status) # 尝试读 HTTP_PORT if [[ -f "$ENV_FILE" ]]; then # shellcheck disable=SC1090 set -a; source <(sed 's/\r$//' "$ENV_FILE" | grep -v '^\s*#' | grep -v '^\s*$' || true); set +a fi cmd_status ;; --pull-only) cmd_up --pull-only ;; -h|--help|help) sed -n '2,12p' "$0" ;; *) err "未知参数: $action (见 --help)" exit 1 ;; esac } main "$@"