# Production One-Click Deploy Backend Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 新增一份生产一键部署脚本,在脚本同目录完成发布包校验、旧版后端备份、后端进程停止、新版 `jar` 替换、后端重启和部署结果验证。 **Architecture:** 以现有 [bin/prod/deploy_release.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_release.sh) 和 [bin/prod/restart_java.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/restart_java.sh) 为参考,但将解包、备份、PID 管理、进程识别、端口等待全部内联到新的单脚本 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh)。实现只覆盖单实例发布链路,不接入 Nginx、不拆独立启停脚本、不引入额外配置文件。 **Tech Stack:** POSIX shell、`unzip`、`find`、`pgrep`、`nohup`、`ss`、`sh -n` --- ### Task 1: 锁定脚本接口与参考实现边界 **Files:** - Inspect: `docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md` - Inspect: `bin/prod/deploy_release.sh` - Inspect: `bin/prod/restart_java.sh` - [ ] **Step 1: 核对设计文档中的后端职责边界** Run: `rg -n "单脚本|JAVA_BIN|backend/backend.pid|63310|loan.pricing.home|TERM|KILL" docs/superpowers/specs/2026-04-01-production-one-click-deploy-design.md` Expected: 能定位单脚本、自定义 `JAVA_BIN`、PID 文件、进程标记和端口等待等核心约束。 - [ ] **Step 2: 抽取现有生产脚本中可复用的后端逻辑** Run: `rg -n "assert_single_jar|extract_release_package|cleanup|BACKEND_JAR|collect_backend_pids|stop_backend|start_backend|BACKEND_MARKER|BACKEND_PORT" bin/prod/deploy_release.sh bin/prod/restart_java.sh` Expected: 能定位现有发布包解压校验、PID 管理和端口等待逻辑,作为新脚本参考来源。 - [ ] **Step 3: 确认新脚本目标文件尚不存在** Run: `test ! -f bin/prod/deploy_from_package.sh && echo "missing"` Expected: 输出 `missing`,说明可以新增独立脚本而不覆盖现有 `/home/webapp` 发布脚本。 - [ ] **Step 4: 提交本任务** ```bash git add docs/superpowers/plans/2026-04-01-production-one-click-deploy-backend-plan.md git commit -m "新增生产一键部署后端计划" ``` ### Task 2: 创建单脚本基础骨架与公共函数 **Files:** - Create: `bin/prod/deploy_from_package.sh` - [ ] **Step 1: 写入脚本头部配置和使用说明** 在 [bin/prod/deploy_from_package.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing/bin/prod/deploy_from_package.sh) 顶部写入最小骨架: ```sh #!/bin/sh set -eu JAVA_BIN="/home/webapp/env/java/bin/java" BACKEND_PORT=63310 SPRING_PROFILE="pro" JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" ``` 并补齐以下说明函数: ```sh usage() { cat <<'EOF' 用法: ./bin/prod/deploy_from_package.sh EOF } ``` - [ ] **Step 2: 实现脚本目录定位和路径常量** 至少补齐以下路径变量: ```sh SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) BACKEND_DIR="$SCRIPT_DIR/backend" FRONTEND_DIR="$SCRIPT_DIR/frontend" BACKEND_JAR_TARGET="$BACKEND_DIR/ruoyi-admin.jar" BACKEND_PID_FILE="$BACKEND_DIR/backend.pid" BACKEND_LOG_FILE="$BACKEND_DIR/backend-console.log" FRONTEND_DIST_ARCHIVE="$FRONTEND_DIR/dist.zip" FRONTEND_DIST_DIR="$FRONTEND_DIR/dist" BACKEND_MARKER="-Dloan.pricing.home=$SCRIPT_DIR" ``` - [ ] **Step 3: 实现日志、时间戳和清理函数** 至少补齐: ```sh timestamp() { date "+%Y%m%d%H%M%S" } log_info() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" } log_error() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2 } ``` 以及: ```sh cleanup() { if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then rm -rf "$WORK_DIR" fi } ``` - [ ] **Step 4: 做语法校验** Run: `sh -n bin/prod/deploy_from_package.sh` Expected: 无输出,返回码为 0。 - [ ] **Step 5: 提交本任务** ```bash git add bin/prod/deploy_from_package.sh git commit -m "新增生产一键部署脚本骨架" ``` ### Task 3: 实现发布包发现、解压和后端备份 **Files:** - Modify: `bin/prod/deploy_from_package.sh` - [ ] **Step 1: 实现目录与命令前置校验** 补齐以下校验: ```sh require_dir() { if [ ! -d "$1" ]; then log_error "缺少目录: $1" exit 1 fi } require_command() { if ! command -v "$1" >/dev/null 2>&1; then log_error "缺少命令: $1" exit 1 fi } ``` 在主流程中校验: ```sh require_dir "$BACKEND_DIR" require_dir "$FRONTEND_DIR" require_command unzip require_command find require_command pgrep require_command ss ``` - [ ] **Step 2: 实现同目录唯一发布 zip 查找** 新增函数: ```sh find_release_archive() { archives=$(find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip') count=$(printf '%s\n' "$archives" | sed '/^$/d' | wc -l | tr -d ' ') if [ "$count" -ne 1 ]; then log_error "脚本同目录发布 zip 数量不正确,期望 1 个,实际 $count 个" exit 1 fi printf '%s\n' "$archives" } ``` - [ ] **Step 3: 实现解压与唯一 `jar` 校验** 补齐: ```sh extract_release_package() { release_archive="$1" release_extract_dir="$2" mkdir -p "$release_extract_dir" unzip -oq "$release_archive" -d "$release_extract_dir" } assert_single_jar() { search_dir="$1" count=$(find "$search_dir" -type f -name '*.jar' | wc -l | tr -d ' ') if [ "$count" -ne 1 ]; then log_error "后端 jar 数量不正确,期望 1 个,实际 $count 个" exit 1 fi find "$search_dir" -type f -name '*.jar' | head -n 1 } ``` - [ ] **Step 4: 实现旧版后端备份和新 `jar` 替换** 补齐: ```sh backup_backend_jar() { if [ -f "$BACKEND_JAR_TARGET" ]; then mv "$BACKEND_JAR_TARGET" "$BACKEND_JAR_TARGET.$(timestamp).bak" fi } deploy_backend_jar() { source_jar="$1" mv "$source_jar" "$BACKEND_JAR_TARGET" } ``` - [ ] **Step 5: 用临时目录构造后端备份场景做静态验证** Run: ```bash tmpdir=$(mktemp -d) mkdir -p "$tmpdir/backend" "$tmpdir/frontend/package" touch "$tmpdir/backend/ruoyi-admin.jar" touch "$tmpdir/frontend/dist.zip" touch "$tmpdir/package/app.jar" (cd "$tmpdir/package" && zip -q release.zip app.jar >/dev/null) test -f "$tmpdir/backend/ruoyi-admin.jar" rm -rf "$tmpdir" ``` Expected: 命令执行成功,用于确认计划中的文件命名和目录约定可被临时目录复现。 - [ ] **Step 6: 做语法校验** Run: `sh -n bin/prod/deploy_from_package.sh` Expected: 无输出,返回码为 0。 - [ ] **Step 7: 提交本任务** ```bash git add bin/prod/deploy_from_package.sh git commit -m "实现生产部署脚本后端发布包处理" ``` ### Task 4: 实现后端进程停止、启动与端口等待 **Files:** - Modify: `bin/prod/deploy_from_package.sh` - [ ] **Step 1: 实现托管进程识别函数** 补齐以下函数: ```sh is_managed_backend_pid() { pid="$1" if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then return 1 fi args=$(ps -o args= -p "$pid" 2>/dev/null || true) case "$args" in *"$BACKEND_MARKER"*"$BACKEND_JAR_TARGET"*|*"$BACKEND_JAR_TARGET"*"$BACKEND_MARKER"*) return 0 ;; esac return 1 } ``` - [ ] **Step 2: 实现 PID 收集与停止流程** 补齐: ```sh collect_backend_pids() { pids="" if [ -f "$BACKEND_PID_FILE" ]; then file_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true) if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then pids="$pids $file_pid" fi fi marker_pids=$(pgrep -f "$BACKEND_MARKER" 2>/dev/null || true) for pid in $marker_pids; do if is_managed_backend_pid "$pid"; then pids="$pids $pid" fi done printf '%s\n' "$(echo "$pids" | xargs 2>/dev/null || true)" } ``` 以及 `stop_backend()`,要求: - 先发 `TERM` - 等待最多 30 秒 - 超时后发 `KILL` - 最后删除 `backend.pid` - [ ] **Step 3: 实现启动流程** 补齐 `start_backend()`,要求: ```sh nohup "$JAVA_BIN" $JAVA_OPTS "$BACKEND_MARKER" -jar "$BACKEND_JAR_TARGET" \ --spring.profiles.active="$SPRING_PROFILE" \ --server.port="$BACKEND_PORT" >> "$BACKEND_LOG_FILE" 2>&1 & ``` 并在启动后: - 写入 `backend/backend.pid` - 轮询 `ss -lnt | grep ":63310 "` 最长 30 秒 - 若未监听成功则报错退出 - [ ] **Step 4: 实现主流程调用顺序** 主流程必须按以下顺序调用: ```sh backup_backend_jar stop_backend deploy_backend_jar "$backend_jar_source" start_backend ``` 不得把 `stop_backend` 放到备份后面之外的位置,避免旧进程继续占用将被替换的 `jar`。 - [ ] **Step 5: 做语法校验** Run: `sh -n bin/prod/deploy_from_package.sh` Expected: 无输出,返回码为 0。 - [ ] **Step 6: 提交本任务** ```bash git add bin/prod/deploy_from_package.sh git commit -m "实现生产部署脚本后端启停逻辑" ``` ### Task 5: 补后端实施记录并验证关键链路 **Files:** - Modify: `bin/prod/deploy_from_package.sh` - Create: `doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md` - [ ] **Step 1: 做脚本静态语法校验** Run: `sh -n bin/prod/deploy_from_package.sh` Expected: 无输出,返回码为 0。 - [ ] **Step 2: 检查脚本中的关键配置与标记是否落位** Run: `rg -n "JAVA_BIN=|BACKEND_PORT=63310|SPRING_PROFILE=|BACKEND_MARKER=|backend.pid|backend-console.log|pgrep -f|ss -lnt" bin/prod/deploy_from_package.sh` Expected: 能看到 Java 路径、端口、进程标记、PID 文件、日志文件和端口等待逻辑都已写入脚本。 - [ ] **Step 3: 编写后端实施记录** 在 [doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md](/Users/wkc/Desktop/loan-pricing/loan-pricing/doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md) 至少记录: ```markdown - 新增脚本 `bin/prod/deploy_from_package.sh` - 脚本内固定 `JAVA_BIN` - 后端发布包解压与唯一 jar 校验规则 - 旧版 jar 时间戳备份规则 - PID 文件、进程标记、端口 63310 等待逻辑 - 执行的验证命令与结果 ``` - [ ] **Step 4: 核对实施记录路径** Run: `ls doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md` Expected: 文件存在于仓库 `doc/` 目录。 - [ ] **Step 5: 提交本任务** ```bash git add bin/prod/deploy_from_package.sh doc/implementation-report-2026-04-01-production-one-click-deploy-backend.md git commit -m "完成生产一键部署后端实现" ```