# SmartClean 自动化打包部署方案（无 Docker 版）

## 一、目标

基于现有 build.sh 构建能力，实现 **一键构建 → 远程备份 → 上传部署 → 服务重启 → 健康检查 → 失败回滚 → 飞书通知** 全流程自动化。不依赖 Docker，直接操作远程服务器。

## 二、前提条件

| 条件 | 说明 |
|------|------|
| SSH 免密登录 | 本机 → 182 服务器配好 SSH Key，免输密码 |
| 服务器目录约定 | 统一部署路径，脚本按约定操作 |
| 现有构建脚本 | 复用已有的 build.sh |

### SSH 免密配置（一次性）

```bash
# 生成密钥（如已有可跳过）
ssh-keygen -t ed25519

# 将公钥复制到服务器
ssh-copy-id root@192.168.1.182

# 验证
ssh root@192.168.1.182 "echo ok"
```

## 三、整体架构

```
本机 (Mac)                                   远程服务器 (192.168.1.182)
┌──────────────────────┐                    ┌──────────────────────────────┐
│                      │                    │                              │
│  源码 → build.sh 构建 │                    │  /opt/smartclean/            │
│                      │   SCP 上传          │  ├── web/                    │
│  产物:               │ ──────────────→    │  │   └── ROOT.war            │
│  - ROOT.war          │                    │  ├── task/                   │
│  - task.jar          │   SSH 远程执行      │  │   └── task.jar            │
│  - dist/             │ ──────────────→    │  ├── front/                  │
│                      │                    │  │   └── dist/               │
│  deploy.sh 控制全流程  │                    │  ├── backups/                │
│                      │                    │  │   ├── 20260415-153000/    │
│                      │                    │  │   └── 20260415-140000/    │
│                      │                    │  └── scripts/                │
│                      │                    │      ├── restart-web.sh      │
│                      │                    │      ├── restart-task.sh     │
│                      │                    │      └── restart-front.sh    │
└──────────────────────┘                    └──────────────────────────────┘
```

## 四、服务器目录规划

```
/opt/smartclean/                    # 部署根目录
├── web/
│   ├── tomcat/                     # Tomcat 安装目录
│   │   └── webapps/
│   │       └── ROOT.war            # Web 服务 WAR 包
│   └── logs/
├── task/
│   ├── task.jar                    # Task 服务 JAR 包
│   ├── task.pid                    # 进程 PID 文件
│   └── logs/
├── front/
│   └── dist/                       # 前端静态文件（Nginx 指向此目录）
├── backups/                        # 版本备份（自动保留最近 5 个）
│   ├── 20260415-153000/
│   │   ├── ROOT.war
│   │   ├── task.jar
│   │   └── dist/
│   └── 20260415-140000/
│       └── ...
└── scripts/                        # 服务器端管理脚本
    ├── restart-web.sh
    ├── restart-task.sh
    └── restart-front.sh
```

## 五、新增文件结构

```
smartclean/
├── build.sh                          # 已有，构建脚本
└── deploy/
    ├── deploy.sh                     # 一键部署主脚本（本机执行）
    ├── rollback.sh                   # 一键回滚脚本（本机执行）
    ├── config.sh                     # 部署配置（服务器地址、路径等）
    ├── .env                          # 敏感信息（密码等，不入 git）
    └── remote/                       # 服务器端脚本（首次部署时自动上传）
        ├── setup.sh                  # 服务器初始化（创建目录结构，一次性）
        ├── restart-web.sh            # 重启 Web 服务（Tomcat）
        ├── restart-task.sh           # 重启 Task 服务（JAR）
        └── restart-front.sh          # 重新加载前端（Nginx reload）
```

## 六、配置文件

### 6.1 部署配置

```bash
# deploy/config.sh

# ===== 服务器配置 =====
DEPLOY_HOST="192.168.1.182"
DEPLOY_USER="root"
DEPLOY_BASE="/opt/smartclean"

# ===== 服务器目录 =====
REMOTE_WEB_DIR="$DEPLOY_BASE/web"
REMOTE_TASK_DIR="$DEPLOY_BASE/task"
REMOTE_FRONT_DIR="$DEPLOY_BASE/front"
REMOTE_BACKUP_DIR="$DEPLOY_BASE/backups"
REMOTE_SCRIPTS_DIR="$DEPLOY_BASE/scripts"

# ===== Tomcat 配置 =====
TOMCAT_HOME="/opt/smartclean/web/tomcat"
TOMCAT_WEBAPPS="$TOMCAT_HOME/webapps"

# ===== Nginx 配置 =====
NGINX_HTML="/opt/smartclean/front/dist"

# ===== Task 服务配置 =====
TASK_JAR_NAME="xiaoqu-intellectual-task-0.0.1-SNAPSHOT.jar"
TASK_PROFILE="prod"
TASK_JVM_OPTS="-Xms256m -Xmx512m"

# ===== 本地构建产物路径 =====
LOCAL_WAR="backend/xiaoqu-intellectual-web/target/ROOT.war"
LOCAL_TASK_JAR="backend/xiaoqu-intellectual-task/target/$TASK_JAR_NAME"
LOCAL_FRONT_DIST="frontend/witcleansystem/dist"

# ===== 备份保留数量 =====
MAX_BACKUPS=5

# ===== 健康检查 =====
HEALTHCHECK_URL="http://$DEPLOY_HOST:8095/dropDown/districtTree"
HEALTHCHECK_RETRIES=20
HEALTHCHECK_INTERVAL=5

# ===== 飞书通知 =====
FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/5703e8cc-6998-46a6-af9d-8c5102cc8c1e"
```

## 七、服务器端脚本

### 7.1 服务器初始化脚本（一次性执行）

```bash
# deploy/remote/setup.sh
#!/bin/bash
# 在服务器上创建标准目录结构，仅需执行一次

DEPLOY_BASE="/opt/smartclean"

mkdir -p "$DEPLOY_BASE"/{web/logs,task/logs,front,backups,scripts}

echo "目录结构创建完成:"
find "$DEPLOY_BASE" -maxdepth 2 -type d
```

### 7.2 重启 Web 服务

```bash
# deploy/remote/restart-web.sh
#!/bin/bash
# 重启 Tomcat（部署 ROOT.war）

TOMCAT_HOME="/opt/smartclean/web/tomcat"

echo "[INFO] 停止 Tomcat..."
"$TOMCAT_HOME/bin/shutdown.sh" 2>/dev/null
sleep 3

# 确保进程已停
TOMCAT_PID=$(ps -ef | grep "catalina" | grep -v grep | awk '{print $2}')
if [ -n "$TOMCAT_PID" ]; then
    echo "[WARN] Tomcat 未正常关闭，强制终止 PID=$TOMCAT_PID"
    kill -9 $TOMCAT_PID
    sleep 1
fi

# 清理旧的解压目录，保留 WAR
rm -rf "$TOMCAT_HOME/webapps/ROOT"
rm -rf "$TOMCAT_HOME/work/Catalina"

echo "[INFO] 启动 Tomcat..."
"$TOMCAT_HOME/bin/startup.sh"

echo "[INFO] Tomcat 已启动"
```

### 7.3 重启 Task 服务

```bash
# deploy/remote/restart-task.sh
#!/bin/bash
# 重启 Task 服务（Spring Boot JAR）

TASK_DIR="/opt/smartclean/task"
JAR_FILE="$TASK_DIR/task.jar"
PID_FILE="$TASK_DIR/task.pid"
LOG_FILE="$TASK_DIR/logs/task.log"
PROFILE="${1:-prod}"
JVM_OPTS="${2:--Xms256m -Xmx512m}"

# 停止旧进程
if [ -f "$PID_FILE" ]; then
    OLD_PID=$(cat "$PID_FILE")
    if kill -0 "$OLD_PID" 2>/dev/null; then
        echo "[INFO] 停止旧进程 PID=$OLD_PID"
        kill "$OLD_PID"
        sleep 3
        # 强制终止
        if kill -0 "$OLD_PID" 2>/dev/null; then
            kill -9 "$OLD_PID"
        fi
    fi
    rm -f "$PID_FILE"
fi

# 启动新进程
echo "[INFO] 启动 Task 服务 (profile=$PROFILE)..."
nohup java $JVM_OPTS \
    -jar "$JAR_FILE" \
    --spring.profiles.active=$PROFILE \
    > "$LOG_FILE" 2>&1 &

echo $! > "$PID_FILE"
echo "[INFO] Task 服务已启动, PID=$(cat "$PID_FILE")"
```

### 7.4 重新加载前端

```bash
# deploy/remote/restart-front.sh
#!/bin/bash
# 重新加载 Nginx（前端静态文件已更新）

echo "[INFO] 测试 Nginx 配置..."
nginx -t 2>&1
if [ $? -ne 0 ]; then
    echo "[ERROR] Nginx 配置有误"
    exit 1
fi

echo "[INFO] 重新加载 Nginx..."
nginx -s reload

echo "[INFO] Nginx 已重新加载"
```

## 八、一键部署主脚本

```bash
# deploy/deploy.sh
#!/bin/bash
#
# SmartClean 一键部署脚本（无 Docker 版）
#
# 用法:
#   ./deploy.sh              # 构建并部署所有服务
#   ./deploy.sh web          # 仅构建部署 Web 服务
#   ./deploy.sh task         # 仅构建部署 Task 服务
#   ./deploy.sh front        # 仅构建部署前端
#   ./deploy.sh front-test   # 构建测试环境前端并部署
#   ./deploy.sh backend      # 构建部署后端（web + task）
#   ./deploy.sh rollback     # 回滚到上一版本
#   ./deploy.sh setup        # 首次初始化服务器目录

set -e

DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$DEPLOY_DIR")"
source "$DEPLOY_DIR/config.sh"

# ===== 版本号 =====
GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD)
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
VERSION="${TIMESTAMP}-${GIT_HASH}"
BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD)
COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s')

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info()  { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }

SSH_CMD="ssh $DEPLOY_USER@$DEPLOY_HOST"
SCP_CMD="scp"

# ===== 飞书通知 =====
notify_feishu() {
    local title="$1" content="$2" color="$3"
    curl -s -X POST "$FEISHU_WEBHOOK" \
      -H "Content-Type: application/json" \
      -d "{
        \"msg_type\": \"interactive\",
        \"card\": {
          \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"},
          \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}]
        }
      }" > /dev/null 2>&1
}

# ===== 检查 SSH 连接 =====
check_ssh() {
    log_info "检查 SSH 连接..."
    if ! $SSH_CMD "echo ok" > /dev/null 2>&1; then
        log_error "无法连接到 $DEPLOY_USER@$DEPLOY_HOST"
        log_error "请先配置 SSH 免密登录: ssh-copy-id $DEPLOY_USER@$DEPLOY_HOST"
        exit 1
    fi
    log_info "SSH 连接正常"
}

# ===== 首次初始化服务器 =====
setup_server() {
    log_info "初始化服务器目录结构..."
    $SCP_CMD "$DEPLOY_DIR/remote/setup.sh" "$DEPLOY_USER@$DEPLOY_HOST:/tmp/setup.sh"
    $SSH_CMD "bash /tmp/setup.sh"

    log_info "上传服务器端管理脚本..."
    $SCP_CMD "$DEPLOY_DIR/remote/restart-web.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/"
    $SCP_CMD "$DEPLOY_DIR/remote/restart-task.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/"
    $SCP_CMD "$DEPLOY_DIR/remote/restart-front.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/"
    $SSH_CMD "chmod +x $REMOTE_SCRIPTS_DIR/*.sh"

    log_info "服务器初始化完成"
}

# ===== 本地构建 =====
build_local() {
    local target="$1"
    log_info "开始本地构建 (目标: $target)..."

    cd "$ROOT_DIR"
    bash build.sh "$target"

    # 验证产物存在
    case "$target" in
        web|backend|all)
            if [ ! -f "$ROOT_DIR/$LOCAL_WAR" ]; then
                log_error "构建产物不存在: $LOCAL_WAR"
                exit 1
            fi
            ;;
    esac

    case "$target" in
        task|backend|all)
            if [ ! -f "$ROOT_DIR/$LOCAL_TASK_JAR" ]; then
                log_error "构建产物不存在: $LOCAL_TASK_JAR"
                exit 1
            fi
            ;;
    esac

    case "$target" in
        front|front-test|all)
            if [ ! -d "$ROOT_DIR/$LOCAL_FRONT_DIST" ]; then
                log_error "构建产物不存在: $LOCAL_FRONT_DIST"
                exit 1
            fi
            ;;
    esac

    log_info "本地构建完成"
}

# ===== 远程备份 =====
backup_remote() {
    local target="$1"
    log_info "备份服务器当前版本 ($VERSION)..."

    $SSH_CMD bash <<EOF
        BACKUP="$REMOTE_BACKUP_DIR/$VERSION"
        mkdir -p "\$BACKUP"

        # 按目标备份
        case "$target" in
            web|backend|all)
                cp "$TOMCAT_WEBAPPS/ROOT.war" "\$BACKUP/" 2>/dev/null && echo "已备份 ROOT.war" || echo "ROOT.war 不存在，跳过"
                ;;
        esac

        case "$target" in
            task|backend|all)
                cp "$REMOTE_TASK_DIR/task.jar" "\$BACKUP/" 2>/dev/null && echo "已备份 task.jar" || echo "task.jar 不存在，跳过"
                ;;
        esac

        case "$target" in
            front|front-test|all)
                if [ -d "$REMOTE_FRONT_DIR/dist" ]; then
                    cp -r "$REMOTE_FRONT_DIR/dist" "\$BACKUP/"
                    echo "已备份 front/dist"
                fi
                ;;
        esac

        # 清理过期备份，保留最近 $MAX_BACKUPS 个
        cd "$REMOTE_BACKUP_DIR"
        ls -dt */ 2>/dev/null | tail -n +\$(($MAX_BACKUPS + 1)) | xargs rm -rf 2>/dev/null
        echo "当前备份列表:"
        ls -dt */ 2>/dev/null | head -5
EOF

    log_info "远程备份完成"
}

# ===== 上传产物 =====
upload_artifacts() {
    local target="$1"

    case "$target" in
        web|backend|all)
            log_info "上传 ROOT.war..."
            $SCP_CMD "$ROOT_DIR/$LOCAL_WAR" "$DEPLOY_USER@$DEPLOY_HOST:$TOMCAT_WEBAPPS/ROOT.war"
            ;;
    esac

    case "$target" in
        task|backend|all)
            log_info "上传 task.jar..."
            $SCP_CMD "$ROOT_DIR/$LOCAL_TASK_JAR" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_TASK_DIR/task.jar"
            ;;
    esac

    case "$target" in
        front|front-test|all)
            log_info "上传前端文件..."
            # 先清空远程 dist，再上传
            $SSH_CMD "rm -rf $REMOTE_FRONT_DIR/dist"
            $SCP_CMD -r "$ROOT_DIR/$LOCAL_FRONT_DIST" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_FRONT_DIR/dist"
            ;;
    esac

    log_info "产物上传完成"
}

# ===== 远程重启服务 =====
restart_services() {
    local target="$1"

    case "$target" in
        web|backend|all)
            log_info "重启 Web 服务..."
            $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-web.sh"
            ;;
    esac

    case "$target" in
        task|backend|all)
            log_info "重启 Task 服务..."
            $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-task.sh $TASK_PROFILE '$TASK_JVM_OPTS'"
            ;;
    esac

    case "$target" in
        front|front-test|all)
            log_info "重新加载前端..."
            $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-front.sh"
            ;;
    esac
}

# ===== 健康检查 =====
healthcheck() {
    local target="$1"

    # Web 服务检查
    if [ "$target" = "all" ] || [ "$target" = "web" ] || [ "$target" = "backend" ]; then
        log_info "健康检查 Web 服务 ($HEALTHCHECK_URL)..."
        for i in $(seq 1 $HEALTHCHECK_RETRIES); do
            if curl -sf "$HEALTHCHECK_URL" > /dev/null 2>&1; then
                log_info "✅ Web 服务健康 (第${i}次检查)"
                return 0
            fi
            echo -n "."
            sleep $HEALTHCHECK_INTERVAL
        done
        log_error "❌ Web 服务健康检查失败 (${HEALTHCHECK_RETRIES}次重试后)"
        return 1
    fi

    # 前端检查
    if [ "$target" = "front" ] || [ "$target" = "front-test" ]; then
        log_info "健康检查前端..."
        if curl -sf "http://$DEPLOY_HOST/" > /dev/null 2>&1; then
            log_info "✅ 前端服务健康"
            return 0
        else
            log_error "❌ 前端服务健康检查失败"
            return 1
        fi
    fi

    return 0
}

# ===== 回滚 =====
rollback() {
    log_info "查找最近的备份..."

    # 获取最近的备份目录名
    LATEST_BACKUP=$($SSH_CMD "ls -dt $REMOTE_BACKUP_DIR/*/ 2>/dev/null | head -1 | xargs basename 2>/dev/null")

    if [ -z "$LATEST_BACKUP" ]; then
        log_error "没有可回滚的备份"
        exit 1
    fi

    log_warn "回滚到版本: $LATEST_BACKUP"

    $SSH_CMD bash <<EOF
        BACKUP="$REMOTE_BACKUP_DIR/$LATEST_BACKUP"

        # 还原 WAR
        if [ -f "\$BACKUP/ROOT.war" ]; then
            cp "\$BACKUP/ROOT.war" "$TOMCAT_WEBAPPS/ROOT.war"
            echo "已还原 ROOT.war"
        fi

        # 还原 Task JAR
        if [ -f "\$BACKUP/task.jar" ]; then
            cp "\$BACKUP/task.jar" "$REMOTE_TASK_DIR/task.jar"
            echo "已还原 task.jar"
        fi

        # 还原前端
        if [ -d "\$BACKUP/dist" ]; then
            rm -rf "$REMOTE_FRONT_DIR/dist"
            cp -r "\$BACKUP/dist" "$REMOTE_FRONT_DIR/dist"
            echo "已还原 front/dist"
        fi
EOF

    # 重启所有服务
    restart_services "all"

    log_info "回滚完成"

    notify_feishu "⚠️ SmartClean 已回滚" \
      "**回滚版本:** $LATEST_BACKUP\\n**服务器:** $DEPLOY_HOST\\n**操作人:** $(whoami)@$(hostname)" \
      "yellow"
}

# ===== 主流程 =====
TARGET="${1:-all}"
DEPLOY_START=$(date +%s)

# 特殊命令
case "$TARGET" in
    setup)
        check_ssh
        setup_server
        exit 0
        ;;
    rollback)
        check_ssh
        rollback
        exit 0
        ;;
esac

log_info "======================================"
log_info "  SmartClean 自动化部署"
log_info "  版本:  $VERSION"
log_info "  分支:  $BRANCH"
log_info "  目标:  $TARGET"
log_info "  服务器: $DEPLOY_USER@$DEPLOY_HOST"
log_info "======================================"

# 1. 检查连接
check_ssh

# 2. 本地构建
build_local "$TARGET"

# 3. 远程备份当前版本
backup_remote "$TARGET"

# 4. 上传构建产物
upload_artifacts "$TARGET"

# 5. 远程重启服务
restart_services "$TARGET"

# 6. 健康检查
if healthcheck "$TARGET"; then
    ELAPSED=$(( $(date +%s) - DEPLOY_START ))
    log_info "======================================"
    log_info "  ✅ 部署成功！"
    log_info "  版本: $VERSION"
    log_info "  耗时: ${ELAPSED}s"
    log_info "  前端: http://$DEPLOY_HOST"
    log_info "  Web:  http://$DEPLOY_HOST:8095"
    log_info "  Task: http://$DEPLOY_HOST:8097"
    log_info "======================================"

    notify_feishu "✅ SmartClean 部署成功" \
      "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**目标:** $TARGET\\n**服务器:** $DEPLOY_HOST\\n**耗时:** ${ELAPSED}s\\n**操作人:** $(whoami)@$(hostname)" \
      "green"
else
    log_error "健康检查失败，自动回滚..."
    rollback

    ELAPSED=$(( $(date +%s) - DEPLOY_START ))
    notify_feishu "❌ SmartClean 部署失败（已回滚）" \
      "**版本:** $VERSION\\n**分支:** $BRANCH\\n**提交:** $COMMIT\\n**服务器:** $DEPLOY_HOST\\n**耗时:** ${ELAPSED}s\\n**状态:** 健康检查失败，已自动回滚" \
      "red"

    exit 1
fi
```

## 九、回滚脚本

```bash
# deploy/rollback.sh
#!/bin/bash
# 快捷回滚入口
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$DEPLOY_DIR/deploy.sh" rollback
```

## 十、使用方式

### 首次使用（一次性设置）

```bash
# 1. 配置 SSH 免密登录
ssh-copy-id root@192.168.1.182

# 2. 初始化服务器目录结构 + 上传管理脚本
./deploy/deploy.sh setup

# 3. 根据实际情况修改 deploy/config.sh 中的路径配置
#    特别是 TOMCAT_HOME 要对应服务器上实际的 Tomcat 安装路径
```

### 日常部署

```bash
# 全量部署（构建全部 + 部署全部）
./deploy/deploy.sh

# 只改了后端 Web 代码
./deploy/deploy.sh web

# 只改了 Task 服务代码
./deploy/deploy.sh task

# 后端都改了
./deploy/deploy.sh backend

# 只改了前端（生产包）
./deploy/deploy.sh front

# 前端测试环境包
./deploy/deploy.sh front-test

# 出问题了，一键回滚
./deploy/deploy.sh rollback
```

### 查看服务器上的备份

```bash
ssh root@192.168.1.182 "ls -lt /opt/smartclean/backups/"
```

## 十一、部署流程图（以 `./deploy.sh web` 为例）

```
本机                                    192.168.1.182
────                                    ─────────────

1. mvn clean package
   → ROOT.war 生成
                                    2. mkdir backups/20260415-153000/
                                       cp ROOT.war → backups/

3. scp ROOT.war ───────────────→   4. ROOT.war → tomcat/webapps/

                                    5. shutdown.sh → rm ROOT/ → startup.sh

6. curl http://182:8095/health
   ├── ✅ 成功 → 飞书通知"部署成功"
   └── ❌ 失败 → 触发回滚
                                    7. (回滚) cp backups/最新/ROOT.war → webapps/
                                       → restart tomcat
        飞书通知"已回滚" ←──────
```

## 十二、与 Docker 版方案对比

| 维度 | 本方案（无 Docker） | Docker 版 |
|------|---------------------|-----------|
| **依赖** | 仅需 SSH + SCP | 需要 Docker |
| **环境一致性** | 依赖服务器已有环境（JDK、Tomcat、Nginx） | 镜像自包含，环境完全一致 |
| **部署速度** | 快（只传产物，几十 MB） | 慢（构建镜像，几百 MB） |
| **适合场景** | 服务器少（1-3 台），已有运行环境 | 多环境、多服务器、需要环境隔离 |
| **回滚** | 文件级回滚（还原备份文件） | 镜像级回滚（切换镜像 tag） |
| **学习成本** | 低（Shell + SSH） | 中（需了解 Docker） |
| **改造成本** | 低（复用现有 build.sh + 服务器环境） | 中（需编写 Dockerfile、调试容器配置） |

## 十三、后续演进

| 阶段 | 动作 |
|------|------|
| **当前** | 本方案：本机执行 deploy.sh，SSH 部署到 182 |
| **阶段 2** | 多环境：config.sh 支持 `--env test/prod` 参数，切换不同服务器 |
| **阶段 3** | CI/CD：将 deploy.sh 逻辑搬到 Gitea Actions，push 自动触发 |
| **阶段 4** | 按需容器化：逐步将服务迁入 Docker，deploy.sh 改为推送镜像 |

## 十四、安全注意事项

1. **deploy/.env 不入 git**：在 `.gitignore` 中添加 `deploy/.env`
2. **SSH Key 管理**：建议为部署专用创建独立 Key，限制只能执行特定命令
3. **备份清理**：默认保留最近 5 个备份，可在 config.sh 中调整 `MAX_BACKUPS`
4. **操作审计**：每次部署的版本号、操作人、时间都会通过飞书通知记录
