#!/usr/bin/env bash # deploy.sh — admin-xy 生产发布脚本 # # 用法: # ./deploy.sh # 构建镜像并部署(首次或更新) # ./deploy.sh --no-cache # 强制全量重新构建镜像 # ./deploy.sh --stop # 停止并移除 admin-xy 前后端容器(保留数据和配置) # ./deploy.sh --restart # 重启 admin-xy 前后端容器(不重新构建) # # 前提: # - Docker 及 Docker Compose 已安装 # - /opt/admin-xy/.env 已配置(首次运行时自动生成模板) set -euo pipefail # ── 常量 ──────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RELEASE_DIR="/opt/admin-xy" ENV_FILE="${RELEASE_DIR}/.env" COMPOSE_FILE="${RELEASE_DIR}/docker-compose.yml" GIT_CONFIG_DIR="${RELEASE_DIR}/git" ADMIN_SERVICES=(server web) # ── 辅助函数 ───────────────────────────────────────────────────────────────── log() { echo "[$(date '+%H:%M:%S')] $*"; } err() { echo "[ERROR] $*" >&2; exit 1; } warn() { echo "[WARN] $*" >&2; } require_root() { if [[ $EUID -ne 0 ]]; then err "该脚本需要 root 权限(请使用 sudo ./deploy.sh)" fi } load_env_file() { local line key value while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^[[:space:]]*$ ]] && continue [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ "$line" == *"="* ]] || continue key="${line%%=*}" value="${line#*=}" key="${key#"${key%%[![:space:]]*}"}" key="${key%"${key##*[![:space:]]}"}" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" if [[ "$value" != \"*\" && "$value" != \'*\' ]]; then value="${value%%[[:space:]]#*}" fi [[ "$value" =~ ^\"(.*)\"$ ]] && value="${BASH_REMATCH[1]}" [[ "$value" =~ ^\'(.*)\'$ ]] && value="${BASH_REMATCH[1]}" export "${key}=${value}" done < "${ENV_FILE}" } write_git_config_files() { local git_name git_email credentials_file git_name="${GIT_AUTHOR_NAME:-dev-assistant}" git_email="${GIT_AUTHOR_EMAIL:-dev-assistant@example.com}" credentials_file="${GIT_CONFIG_DIR}/.git-credentials" mkdir -p "${GIT_CONFIG_DIR}" chmod 700 "${GIT_CONFIG_DIR}" cat > "${GIT_CONFIG_DIR}/.gitconfig" < "${credentials_file}" if [[ -n "${GITEE_USERNAME:-}" && -n "${GITEE_TOKEN:-}" ]]; then printf 'https://%s:%s@gitee.com\n' "${GITEE_USERNAME}" "${GITEE_TOKEN}" > "${credentials_file}" else warn "未配置 GITEE_USERNAME/GITEE_TOKEN,容器内 git push 可能需要重新认证" fi chmod 600 "${GIT_CONFIG_DIR}/.gitconfig" "${credentials_file}" } # ── 参数解析 ────────────────────────────────────────────────────────────────── ACTION="deploy" NO_CACHE="" for arg in "$@"; do case "$arg" in --no-cache) NO_CACHE="--no-cache" ;; --stop) ACTION="stop" ;; --restart) ACTION="restart" ;; --help|-h) sed -n '2,13p' "$0" | sed 's/^# \?//' exit 0 ;; *) err "未知参数:$arg(使用 --help 查看用法)" ;; esac done # ── 发布目录初始化 ──────────────────────────────────────────────────────────── init_release_dir() { log "初始化发布目录 ${RELEASE_DIR} ..." mkdir -p "${RELEASE_DIR}" # 写入 .env 模板(仅首次,避免覆盖已有配置) if [[ ! -f "${ENV_FILE}" ]]; then log "生成 .env 配置模板 → ${ENV_FILE}" cat > "${ENV_FILE}" <<'EOF' # ── 数据库连接(必填)──────────────────────────────────────────────────────── # JDBC URL 示例:jdbc:mysql://127.0.0.1:3306/adminxy?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia%2FShanghai SPRING_DATASOURCE_ADMIN_URL=jdbc:mysql://127.0.0.1:3306/adminxy?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia%2FShanghai SPRING_DATASOURCE_ADMIN_USERNAME=adminxy SPRING_DATASOURCE_ADMIN_PASSWORD=changeme # ── JWT 密钥(必填,至少 32 个字符)───────────────────────────────────────── JWT_SECRET=change-me-in-production-32bytes-min-secret-key-please # ── 容器内 Git 身份与 Gitee HTTPS 凭据(可选,用于 Agent 提交/推送)──────── GIT_AUTHOR_NAME=dev-assistant GIT_AUTHOR_EMAIL=dev-assistant@example.com GITEE_USERNAME= GITEE_TOKEN= EOF warn "已生成 .env 模板,请编辑后再次运行:${ENV_FILE}" exit 0 fi # 校验必填变量 local missing=() load_env_file [[ -z "${SPRING_DATASOURCE_ADMIN_URL:-}" ]] && missing+=("SPRING_DATASOURCE_ADMIN_URL") [[ -z "${SPRING_DATASOURCE_ADMIN_USERNAME:-}" ]] && missing+=("SPRING_DATASOURCE_ADMIN_USERNAME") [[ -z "${SPRING_DATASOURCE_ADMIN_PASSWORD:-}" ]] && missing+=("SPRING_DATASOURCE_ADMIN_PASSWORD") [[ -z "${JWT_SECRET:-}" ]] && missing+=("JWT_SECRET") if [[ ${#missing[@]} -gt 0 ]]; then err "以下必填项未配置:${missing[*]}(请检查 ${ENV_FILE})" fi write_git_config_files # 同步 docker-compose.yml log "同步 docker-compose.yml → ${COMPOSE_FILE}" cp "${SCRIPT_DIR}/docker-compose.yml" "${COMPOSE_FILE}" } # ── 构建镜像 ────────────────────────────────────────────────────────────────── build_images() { log "构建 Docker 镜像 ..." "${SCRIPT_DIR}/build-images.sh" ${NO_CACHE} } # ── 部署 ───────────────────────────────────────────────────────────────────── deploy() { log "启动 admin-xy 前后端服务 ..." docker compose \ --project-directory "${RELEASE_DIR}" \ --file "${COMPOSE_FILE}" \ --env-file "${ENV_FILE}" \ up -d "${ADMIN_SERVICES[@]}" log "当前 admin-xy 前后端容器状态:" docker compose \ --project-directory "${RELEASE_DIR}" \ --file "${COMPOSE_FILE}" \ ps "${ADMIN_SERVICES[@]}" } # ── 停止 ───────────────────────────────────────────────────────────────────── stop_services() { if [[ ! -f "${COMPOSE_FILE}" ]]; then err "未找到 ${COMPOSE_FILE},无法停止(是否已部署?)" fi log "停止并移除 admin-xy 前后端容器 ..." docker compose \ --project-directory "${RELEASE_DIR}" \ --file "${COMPOSE_FILE}" \ stop "${ADMIN_SERVICES[@]}" docker compose \ --project-directory "${RELEASE_DIR}" \ --file "${COMPOSE_FILE}" \ rm -f "${ADMIN_SERVICES[@]}" log "admin-xy 前后端容器已停止并移除。" } # ── 重启 ───────────────────────────────────────────────────────────────────── restart_services() { if [[ ! -f "${COMPOSE_FILE}" ]]; then err "未找到 ${COMPOSE_FILE},请先执行完整部署:./deploy.sh" fi log "重启 admin-xy 前后端服务 ..." docker compose \ --project-directory "${RELEASE_DIR}" \ --file "${COMPOSE_FILE}" \ --env-file "${ENV_FILE}" \ restart "${ADMIN_SERVICES[@]}" log "当前 admin-xy 前后端容器状态:" docker compose \ --project-directory "${RELEASE_DIR}" \ --file "${COMPOSE_FILE}" \ ps "${ADMIN_SERVICES[@]}" } # ── 主流程 ──────────────────────────────────────────────────────────────────── require_root case "${ACTION}" in deploy) log "=== admin-xy 生产部署开始 ===" init_release_dir build_images deploy log "=== 部署完成 ===" log "前端:http://:9001" log "后端:http://:9010" ;; stop) stop_services ;; restart) restart_services ;; esac