# 生产一键部署脚本设计文档

## 1. 背景

当前仓库已经存在生产环境部署脚本 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 和 Java 管理脚本 [bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh)。但这套脚本依赖固定的 `/home/webapp` 目录结构、独立启停脚本以及安装好的 Nginx，不符合本次“在脚本同目录直接完成发布”的目标。

本次需要设计一份新的生产一键部署脚本，直接放在发布目录内执行。该目录中同时存在：

- 部署脚本本身
- 1 个发布 zip
- `backend/` 目录
- `frontend/` 目录

发布 zip 内固定包含 1 个后端 `jar` 和 1 个前端 `dist.zip`。脚本执行时需要完成旧版本备份、新版本替换、后端重启和前端解压部署。

## 2. 已确认约束

- 交付形态必须是单脚本，自包含，不拆分独立重启脚本
- Java 可执行路径直接写在脚本内常量中，不通过命令行参数或外部配置文件传入
- 发布包从脚本同目录读取
- 发布包内必须且只能包含 1 个后端 `jar` 和 1 个前端 `dist.zip`
- 旧版后端 `jar` 与旧版前端 `dist` 目录必须通过“原地重命名 + 时间戳”的方式保留
- 后端启动参数固定沿用当前生产约束：`--spring.profiles.active=pro --server.port=63310`
- 不增加兼容性、补丁性、兜底性或回滚性方案
- 方案必须保持最短路径实现，并可完成全链路逻辑验证

## 3. 当前资产与现状

### 3.1 现有生产脚本

[bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 已具备以下能力：

- 解压发布包
- 校验包内 `jar` 和 `dist.zip`
- 备份旧版后端与前端
- 替换产物
- 调用独立 Java 管理脚本完成重启

但该脚本默认依赖 `/home/webapp` 固定目录、`restart_java.sh` 外部脚本和 Nginx 管理流程，超出了本次需求边界。

### 3.2 现有 Java 管理逻辑

[bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh) 已实现：

- 使用 PID 文件记录后端进程
- 通过 `-Dloan.pricing.home` 标记识别托管进程
- 停止时先 `TERM` 再按需 `KILL`
- 启动后等待端口 `63310` 监听成功

这部分逻辑可以作为新单脚本中的后端启停参考，但最终不再拆成第二个脚本文件。

## 4. 方案对比

### 方案一：单脚本自包含部署

做法：

- 新增一份单独的部署脚本
- 在脚本顶部写死 `JAVA_BIN`、端口、日志路径和目录约定
- 执行时自动读取脚本同目录发布包
- 将解包、备份、替换、启停全部内联到同一文件

优点：

- 与本次“一起写在一个脚本里”的目标完全一致
- 发布目录内即可直接执行，路径最短
- 不依赖独立重启脚本或外部配置文件
- 用户只需维护一个脚本文件

缺点：

- 后续如果要单独管理 `start`、`stop`、`status`，需要再扩展脚本

### 方案二：保留现有双脚本结构，只调整入口

做法：

- 保留部署脚本和重启脚本两份文件
- 让部署脚本在同目录发布模式下调用重启脚本

优点：

- 可复用现有结构，改动相对少

缺点：

- 不满足“写在一起”的明确要求
- 交付物仍有两个脚本，使用路径不够直接

### 方案三：极简覆盖式脚本

做法：

- 不维护 PID 文件
- 直接按文件名覆盖
- 使用宽泛的 Java 进程匹配方式停止旧进程

优点：

- 脚本最短

缺点：

- 容易误伤其他 Java 进程
- 状态不可控
- 不满足全链路可验证要求

## 5. 设计结论

采用方案一：单脚本自包含部署。

最终交付为 1 份新的生产一键部署脚本，负责在脚本同目录完成完整发布链路。脚本以当前仓库已有生产脚本为参考，但不保留 Nginx 管理、根目录安装和双脚本调用等超出本次范围的设计。

## 6. 脚本结构设计

### 6.1 目录约定

脚本执行目录固定为脚本所在目录。目录内约定如下：

- `deploy_release.sh` 或本次确定的新脚本文件
- 1 个发布 zip
- `backend/`
- `frontend/`

脚本不依赖仓库根目录运行，也不依赖外部工作目录传参。

### 6.2 脚本内固定配置

脚本顶部固定声明以下配置项：

- `JAVA_BIN`
- `BACKEND_PORT=63310`
- `SPRING_PROFILE=pro`
- `JAVA_OPTS`
- `BACKEND_PID_FILE=backend/backend.pid`
- `BACKEND_LOG_FILE=backend/backend-console.log`
- `BACKEND_JAR_TARGET=backend/ruoyi-admin.jar`
- `FRONTEND_DIST_DIR=frontend/dist`
- `FRONTEND_DIST_ARCHIVE=frontend/dist.zip`

其中 `JAVA_BIN` 由维护者直接修改脚本内常量，不再设计其他配置入口。

### 6.3 执行流程

脚本执行顺序固定如下：

1. 定位脚本所在目录
2. 校验 `backend/` 与 `frontend/` 目录存在，不存在则直接失败
3. 在脚本同目录查找唯一一个发布 zip
4. 解压发布 zip 到临时目录
5. 校验临时目录内必须且只能找到 1 个 `jar` 和 1 个 `dist.zip`
6. 将 `backend/` 目录下现有 `jar` 重命名为带时间戳的备份文件
7. 将 `frontend/dist` 重命名为 `dist-时间戳`
8. 停止当前脚本托管的旧后端进程
9. 将新 `jar` 移入 `backend/ruoyi-admin.jar`
10. 将新 `dist.zip` 移入 `frontend/dist.zip`
11. 删除当前 `frontend/dist`
12. 解压新 `frontend/dist.zip` 到 `frontend/dist/`
13. 启动新的后端 `jar`
14. 等待端口 `63310` 监听成功
15. 输出部署完成信息

## 7. 后端启停设计

### 7.1 进程识别规则

为避免误杀机器上的其他 Java 进程，脚本只管理自己启动的后端实例。

识别规则如下：

- 启动时附加标记参数：`-Dloan.pricing.home=<脚本目录>`
- 优先读取 `backend/backend.pid`
- 如果 PID 文件失效，则使用 `pgrep -f` 按以下组合特征识别：
  - `-Dloan.pricing.home=<脚本目录>`
  - `backend/ruoyi-admin.jar`

只有同时满足托管标记和目标 jar 特征的进程，才允许被停止。

### 7.2 停止流程

停止逻辑如下：

1. 收集当前托管进程 PID
2. 如无托管进程，则清理失效 PID 文件并直接继续部署
3. 对托管进程发送 `TERM`
4. 等待最多 30 秒
5. 若仍存在残留进程，则发送 `KILL`
6. 删除 `backend/backend.pid`

### 7.3 启动流程

启动逻辑如下：

1. 校验 `JAVA_BIN` 可执行
2. 校验 `backend/ruoyi-admin.jar` 存在
3. 使用 `nohup` 后台启动
4. 写入 `backend/backend.pid`
5. 日志输出到 `backend/backend-console.log`
6. 使用固定参数启动：
   - `-Dloan.pricing.home=<脚本目录>`
   - `--spring.profiles.active=pro`
   - `--server.port=63310`
7. 轮询端口监听状态，最长等待 30 秒

## 8. 备份与替换设计

### 8.1 后端备份

如果 `backend/` 目录中存在旧版 `jar`，则将其原地重命名为：

- `原文件名.YYYYMMDDHHMMSS.bak`

不新增独立备份目录，避免引入额外结构。

### 8.2 前端备份

如果 `frontend/dist` 目录存在，则将其原地重命名为：

- `dist-YYYYMMDDHHMMSS`

如果 `frontend/dist.zip` 已存在，则允许被新版本覆盖，不再为历史 `dist.zip` 单独追加备份规则。本次备份目标仅聚焦于用户明确提出的旧 `jar` 和旧前端 `dist`。

### 8.3 新产物替换

替换规则如下：

- 后端统一落到 `backend/ruoyi-admin.jar`
- 前端压缩包统一落到 `frontend/dist.zip`
- 前端静态资源统一解压到 `frontend/dist/`

该规则确保部署目录在每次发布后保持稳定结构，便于后续维护。

## 9. 失败处理设计

本次失败处理仅保留必要的强校验，不增加回滚或兼容分支。

以下场景直接失败退出：

- 脚本同目录下没有发布 zip
- 脚本同目录下存在多个发布 zip
- 解压后 `jar` 数量不是 1
- 解压后 `dist.zip` 数量不是 1
- `JAVA_BIN` 不可执行
- 新 `jar` 无法写入目标位置
- `dist.zip` 解压失败
- 后端 30 秒内未监听 `63310`

脚本退出时需要清理临时解压目录，避免残留中间文件。

## 10. 交付文件设计

本次设计对应的交付物限定为以下内容：

- 1 个生产一键部署脚本
- 1 份设计文档
- 1 份实施记录

建议脚本路径为：

- [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh)

保留 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 不动，可以避免影响现有 `/home/webapp` 部署方式；新需求使用独立脚本承载，边界更清晰。

## 11. 验证设计

本次验证只覆盖该单脚本的一键部署链路，必须包含以下检查：

1. 执行 `sh -n` 校验脚本语法
2. 校验同目录只有 1 个发布 zip 时可继续执行
3. 校验没有 zip 或存在多个 zip 时直接失败
4. 校验发布包内必须正好有 1 个 `jar` 和 1 个 `dist.zip`
5. 校验旧 `jar` 被改名为带时间戳备份文件
6. 校验旧 `frontend/dist` 被改名为带时间戳目录
7. 校验新 `jar` 最终位于 `backend/ruoyi-admin.jar`
8. 校验新 `dist.zip` 最终位于 `frontend/dist.zip`
9. 校验前端资源成功解压到 `frontend/dist/`
10. 校验后端进程成功启动并监听 `63310`
11. 校验 `backend/backend.pid` 和 `backend/backend-console.log` 被正确生成

## 12. 非目标

本次明确不包含以下内容：

- 不接入 Nginx 启停或重载
- 不处理 Java 安装
- 不生成独立的后端重启脚本
- 不增加回滚脚本
- 不新增命令行参数模式
- 不新增外部配置文件
- 不兼容多实例或多应用部署场景
