From ab9da570eca2cc960ebd28f9ccba3b3412507821 Mon Sep 17 00:00:00 2001 From: xuans Date: Sun, 18 Oct 2026 17:43:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=E5=8C=85=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=9E=B6=E6=9E=84=E9=87=8D=E6=9E=84=E4=B8=BA=E6=A0=87?= =?UTF-8?q?=E5=87=86scert=E6=A0=B9=E7=9B=AE=E5=BD=95=E4=B8=8Edeploy?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=BF=90=E7=BB=B4=E5=A5=97=E4=BB=B6=E6=A0=87?= =?UTF-8?q?=E5=87=86=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/install-systemd.sh | 170 +++++++++++++++++++++++++ deploy/package.mjs | 65 ++++++---- deploy/update-systemd.sh | 178 +++++++++++++++++++++++++++ doc/Scert整改任务执行清单.md | 14 +-- 4 files changed, 396 insertions(+), 31 deletions(-) create mode 100644 deploy/install-systemd.sh create mode 100644 deploy/update-systemd.sh diff --git a/deploy/install-systemd.sh b/deploy/install-systemd.sh new file mode 100644 index 0000000..5e9e57d --- /dev/null +++ b/deploy/install-systemd.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Scert 生产环境全新一键安装部署脚本 (755) + +usage() { + echo "用法:$0 [安装根目录]" + echo "全新部署会初始化 Scert 专属运行目录并配置 systemd 系统服务。" +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +if [ "$(id -u)" -ne 0 ]; then + echo "错误:必须使用 root 用户执行全新安装!" >&2 + exit 1 +fi + +script_dir="$(cd "$(dirname "$0")" && pwd)" +package_root="$(realpath -e -- "$script_dir/..")" + +runtime_dir="${1:-/scert}" +fresh_confirmation="确认全新部署 Scert" + +printf '这是不可逆的 Scert 专属数据与环境部署操作。请输入“%s”继续: ' "$fresh_confirmation" +if ! IFS= read -r confirmation || [ "$confirmation" != "$fresh_confirmation" ]; then + echo "未获得有效确认,已取消全新部署。" >&2 + exit 1 +fi + +case "$(uname -m)" in + x86_64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; + *) echo "错误:不支持的系统架构 $(uname -m)" >&2; exit 1 ;; +esac + +# 1. 查找二进制程序 +if [ -f "$package_root/scert" ] && [ -f "$package_root/scert-admin" ]; then + src_scert="$package_root/scert" + src_scert_admin="$package_root/scert-admin" + src_scert_nginx="$package_root/scert-nginx" + src_scert_storage="$package_root/scert-storage" +else + echo "错误:未能在 $package_root 目录下找到必要的 scert/scert-admin 二进制!" >&2 + exit 1 +fi + +# 2. 停止旧服务 +echo ">>> 正在检查并停止已有相关服务..." +systemctl stop Scert.service 2>/dev/null || true +systemctl stop Scert-admin.service 2>/dev/null || true +systemctl stop scert-nginx.service 2>/dev/null || true +systemctl stop Scert-storage.service 2>/dev/null || true + +# 3. 创建系统专用运行用户与组 +if ! getent group Scert >/dev/null; then + groupadd --system Scert +fi +if ! getent passwd Scert >/dev/null; then + useradd --system --gid Scert --home-dir "$runtime_dir" --shell /usr/sbin/nologin Scert +fi + +# 4. 创建标准目录结构 +echo ">>> 正在创建标准目录结构: $runtime_dir" +mkdir -p "$runtime_dir" +for dir in config data log ssl web conf deploy; do + mkdir -p "$runtime_dir/$dir" +done + +# 5. 安装二进制文件 +echo ">>> 正在安装程序文件..." +cp -f "$src_scert" "$runtime_dir/scert" +chown Scert:Scert "$runtime_dir/scert" +chmod 0755 "$runtime_dir/scert" + +cp -f "$src_scert_admin" "$runtime_dir/scert-admin" +chown root:root "$runtime_dir/scert-admin" +chmod 0700 "$runtime_dir/scert-admin" + +if [ -f "$src_scert_nginx" ]; then + cp -f "$src_scert_nginx" "$runtime_dir/scert-nginx" + chown root:root "$runtime_dir/scert-nginx" + chmod 0755 "$runtime_dir/scert-nginx" +fi + +if [ -f "$src_scert_storage" ]; then + cp -f "$src_scert_storage" "$runtime_dir/scert-storage" + chown root:root "$runtime_dir/scert-storage" + chmod 0755 "$runtime_dir/scert-storage" +fi + +# 6. 复制前端静态文件与部署套件 +if [ -d "$package_root/web" ]; then + cp -rf "$package_root/web"/* "$runtime_dir/web/" 2>/dev/null || true +fi +if [ -d "$package_root/deploy" ]; then + cp -rf "$package_root/deploy"/* "$runtime_dir/deploy/" 2>/dev/null || true + chmod -R 0755 "$runtime_dir/deploy" 2>/dev/null || true +fi + +chown -R Scert:Scert "$runtime_dir/config" "$runtime_dir/data" "$runtime_dir/log" "$runtime_dir/ssl" "$runtime_dir/web" "$runtime_dir/conf" +chmod 0750 "$runtime_dir/config" "$runtime_dir/data" "$runtime_dir/log" "$runtime_dir/ssl" "$runtime_dir/conf" + +# 7. 安装 systemd 服务单元 +systemd_src="$package_root/deploy/systemd" +if [ ! -d "$systemd_src" ] && [ -d "$runtime_dir/deploy/systemd" ]; then + systemd_src="$runtime_dir/deploy/systemd" +fi + +if [ -d "$systemd_src" ]; then + echo ">>> 正在注册 systemd 服务单元..." + for svc in Scert.service Scert-admin.service scert-nginx.service scert-nginx.socket Scert-storage.service Scert-storage.socket; do + if [ -f "$systemd_src/$svc" ]; then + sed -e "s|/scert|$runtime_dir|g" "$systemd_src/$svc" > "/etc/systemd/system/$svc" + chmod 0644 "/etc/systemd/system/$svc" + chown root:root "/etc/systemd/system/$svc" + fi + done + systemctl daemon-reload + systemctl enable Scert.service >/dev/null 2>&1 || true +fi + +# 8. 检查并放行常用防火墙端口 (80, 443) +open_firewall_ports() { + local ports="80 443" + echo ">>> 正在检查并自动开放网络防火墙端口 (80, 443)..." + local opened=false + + if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then + for p in $ports; do + ufw allow "${p}/tcp" >/dev/null 2>&1 || true + done + opened=true + echo "ufw 端口放行完成 (80, 443)。" + fi + + if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then + for p in $ports; do + firewall-cmd --permanent --add-port="${p}/tcp" >/dev/null 2>&1 || true + done + firewall-cmd --reload >/dev/null 2>&1 || true + opened=true + echo "firewalld 端口放行并重载完成 (80, 443)。" + fi + + if [ "$opened" = false ] && command -v iptables >/dev/null 2>&1; then + for p in $ports; do + if ! iptables -C INPUT -p tcp --dport "$p" -j ACCEPT >/dev/null 2>&1; then + iptables -I INPUT -p tcp --dport "$p" -j ACCEPT >/dev/null 2>&1 || true + fi + done + echo "iptables INPUT 规则已更新放行 (80, 443)。" + fi +} + +open_firewall_ports + +# 9. 启动 Scert 服务 +echo ">>> 正在启动 Scert 主服务..." +systemctl restart Scert.service || true + +echo "==========================================================" +echo " Scert 全新安装与系统服务注册完成!" +echo " 安装路径: $runtime_dir" +echo " 运行用户: Scert" +echo " 开机自启: 已启用 (systemctl enable Scert)" +echo " 服务状态: $(systemctl is-active Scert.service 2>/dev/null || echo '未运行')" +echo "==========================================================" diff --git a/deploy/package.mjs b/deploy/package.mjs index 8f33187..bd194ba 100644 --- a/deploy/package.mjs +++ b/deploy/package.mjs @@ -1,4 +1,4 @@ -import fs from "node:fs"; +import fs from "node:fs"; import path from "node:path"; import crypto from "node:crypto"; import { execFileSync } from "node:child_process"; @@ -35,6 +35,17 @@ for (const target of targets) { fs.rmSync(stageDir, { recursive: true, force: true }); fs.mkdirSync(stageDir, { recursive: true }); + // 顶层包含标准根目录 scert/ + const appDir = path.join(stageDir, "scert"); + fs.mkdirSync(appDir, { recursive: true }); + + // 预创建标准运行目录 + for (const dir of ["config", "data", "log", "ssl"]) { + fs.mkdirSync(path.join(appDir, dir), { recursive: true }); + // 添加 .gitkeep 保持目录结构完整 + fs.writeFileSync(path.join(appDir, dir, ".gitkeep"), "", "utf8"); + } + const ext = goos === "windows" ? ".exe" : ""; const binaries = [ { name: `scert${ext}`, pkg: "./cmd/scert" }, @@ -48,9 +59,9 @@ for (const target of targets) { console.log(`Building Go binaries for ${goos}/${goarch}...`); const ldflags = `-s -w -X 'git.pcux.cn/xuans/Scert/backend/internal/buildinfo.Version=${versionArg.replace(/^v/i, "")}'`; - + for (const bin of binaries) { - const outBin = path.join(stageDir, bin.name); + const outBin = path.join(appDir, bin.name); execFileSync("go", ["build", "-trimpath", "-ldflags", ldflags, "-o", outBin, bin.pkg], { cwd: backendDir, env: { @@ -64,43 +75,50 @@ for (const target of targets) { } // Copy Web frontend dist - const webDir = path.join(stageDir, "web"); + const webDir = path.join(appDir, "web"); fs.mkdirSync(webDir, { recursive: true }); fs.cpSync(frontendDist, webDir, { recursive: true }); - // Generate & Copy Release Notes / 更新说明.md - let releaseNotes = ''; + // Generate & Copy Release Notes / 更新说明.md / CHANGELOG.md + let releaseNotes = ""; try { - const logOutput = execFileSync('git', ['log', '-n', '10', '--pretty=format:- %s (%h)'], { encoding: 'utf8' }).trim(); - releaseNotes = '# Scert ' + versionArg + ' 版本更新说明\n\n发布日期: ' + new Date().toISOString().slice(0, 10) + '\n\n### 本次版本主要修改与优化内容:\n\n' + logOutput + '\n'; + const logOutput = execFileSync("git", ["log", "-n", "10", "--pretty=format:- %s (%h)"], { encoding: "utf8" }).trim(); + releaseNotes = `# Scert ${versionArg} 版本更新说明\n\n发布日期: ${new Date().toISOString().slice(0, 10)}\n\n### 本次版本主要修改与优化内容:\n\n${logOutput}\n`; } catch { - releaseNotes = '# Scert ' + versionArg + ' 版本更新说明\n\n- 自动化打包构建产物,支持全平台服务部署与运维。\n'; + releaseNotes = `# Scert ${versionArg} 版本更新说明\n\n- 自动化打包构建产物,支持全平台服务部署与运维。\n`; } - fs.writeFileSync(path.join(stageDir, '更新说明.md'), releaseNotes, 'utf8'); - fs.writeFileSync(path.join(stageDir, 'CHANGELOG.md'), releaseNotes, 'utf8'); + fs.writeFileSync(path.join(appDir, "更新说明.md"), releaseNotes, "utf8"); + fs.writeFileSync(path.join(appDir, "CHANGELOG.md"), releaseNotes, "utf8"); + + // Copy Deploy configurations into scert/deploy/ + const appDeployDir = path.join(appDir, "deploy"); + fs.mkdirSync(appDeployDir, { recursive: true }); - // Copy Deploy configurations if (fs.existsSync(path.join(deployDir, "README.md"))) { - fs.copyFileSync(path.join(deployDir, "README.md"), path.join(stageDir, "部署说明.md")); + fs.copyFileSync(path.join(deployDir, "README.md"), path.join(appDir, "部署说明.md")); + fs.copyFileSync(path.join(deployDir, "README.md"), path.join(appDeployDir, "README.md")); } if (goos === "linux") { - if (fs.existsSync(path.join(deployDir, "install-linux.sh"))) { - fs.copyFileSync(path.join(deployDir, "install-linux.sh"), path.join(stageDir, "install-linux.sh")); + if (fs.existsSync(path.join(deployDir, "install-systemd.sh"))) { + fs.copyFileSync(path.join(deployDir, "install-systemd.sh"), path.join(appDeployDir, "install-systemd.sh")); + } + if (fs.existsSync(path.join(deployDir, "update-systemd.sh"))) { + fs.copyFileSync(path.join(deployDir, "update-systemd.sh"), path.join(appDeployDir, "update-systemd.sh")); } if (fs.existsSync(path.join(deployDir, "systemd"))) { - fs.cpSync(path.join(deployDir, "systemd"), path.join(stageDir, "systemd"), { recursive: true }); + fs.cpSync(path.join(deployDir, "systemd"), path.join(appDeployDir, "systemd"), { recursive: true }); } if (fs.existsSync(path.join(deployDir, "nginx"))) { - fs.cpSync(path.join(deployDir, "nginx"), path.join(stageDir, "nginx"), { recursive: true }); + fs.cpSync(path.join(deployDir, "nginx"), path.join(appDeployDir, "nginx"), { recursive: true }); } } else if (goos === "windows") { if (fs.existsSync(path.join(deployDir, "install-windows.ps1"))) { - fs.copyFileSync(path.join(deployDir, "install-windows.ps1"), path.join(stageDir, "install-windows.ps1")); + fs.copyFileSync(path.join(deployDir, "install-windows.ps1"), path.join(appDeployDir, "install-windows.ps1")); } } - // Generate SHA256SUMS + // Generate SHA256SUMS for the package contents const allFiles = []; function collect(dir, base) { for (const item of fs.readdirSync(dir, { withFileTypes: true })) { @@ -126,13 +144,12 @@ for (const target of targets) { console.log(`Packaging ${archiveName}...`); if (archiveExt === "tar.gz") { - execFileSync("tar", ["-czf", archivePath, "-C", outDir, pkgName], { stdio: "inherit" }); + execFileSync("tar", ["-czf", archivePath, "-C", stageDir, "scert", "SHA256SUMS"], { stdio: "inherit" }); } else { - // PowerShell Compress-Archive or tar try { - execFileSync("tar", ["-a", "-c", "-f", archivePath, "-C", outDir, pkgName], { stdio: "inherit" }); + execFileSync("tar", ["-a", "-c", "-f", archivePath, "-C", stageDir, "scert", "SHA256SUMS"], { stdio: "inherit" }); } catch { - execFileSync("powershell", ["-Command", `Compress-Archive -Path '${stageDir}' -DestinationPath '${archivePath}' -Force`], { stdio: "inherit" }); + execFileSync("powershell", ["-Command", `Compress-Archive -Path '${path.join(stageDir, "*")}' -DestinationPath '${archivePath}' -Force`], { stdio: "inherit" }); } } @@ -140,4 +157,4 @@ for (const target of targets) { const archiveHash = crypto.createHash("sha256").update(fs.readFileSync(archivePath)).digest("hex"); fs.writeFileSync(`${archivePath}.sha256`, `${archiveHash} ${archiveName}\n`, "utf8"); console.log(`Finished ${archiveName} (${archiveHash})`); -} \ No newline at end of file +} diff --git a/deploy/update-systemd.sh b/deploy/update-systemd.sh new file mode 100644 index 0000000..0f2947e --- /dev/null +++ b/deploy/update-systemd.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Scert 生产环境版本平滑升级脚本 (755) +# 规范说明: +# 1. 严格以现有生产环境的实际安装路径作为更新路径,严禁以升级包解压路径作为运行路径。 +# 2. 仅更新程序文件与系统服务,严密保留现有数据库、配置文件、证书与域名,免去重新初始化流程。 + +usage() { + echo "用法:$0 [现有安装目录]" + echo "默认将自动从 /etc/systemd/system/Scert.service 中解析实际安装路径(如 /scert)。" + echo "如果服务未安装或需要手动指定,可在参数中显式传入安装目录。" +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +if [ "$(id -u)" -ne 0 ]; then + echo "错误:必须使用 root 用户执行升级!" >&2 + exit 1 +fi + +script_dir="$(cd "$(dirname "$0")" && pwd)" +package_root="$(realpath -e -- "$script_dir/..")" + +# 1. 自动探查并确定系统的实际安装路径 +target_dir="${1:-}" + +if [ -z "$target_dir" ]; then + if [ -f /etc/systemd/system/Scert.service ]; then + service_workdir="$(grep -E '^[[:space:]]*WorkingDirectory=' /etc/systemd/system/Scert.service | head -n1 | cut -d'=' -f2- | tr -d ' ' || true)" + if [ -n "$service_workdir" ] && [ -d "$service_workdir" ]; then + target_dir="$service_workdir" + fi + fi +fi + +if [ -z "$target_dir" ]; then + for candidate in /scert /opt/scert; do + if [ -d "$candidate" ] && [ -f "$candidate/scert" ] && [ "$candidate" != "$package_root" ]; then + target_dir="$candidate" + break + fi + done +fi + +if [ -z "$target_dir" ]; then + echo "错误:未能在系统服务中检测到 Scert 的现有安装路径!" >&2 + echo "请手动指定目标安装目录,例如:$0 /scert" >&2 + exit 1 +fi + +target_dir="$(realpath -m -- "$target_dir")" + +if [ ! -d "$target_dir" ]; then + echo "错误:目标安装目录不存在: $target_dir" >&2 + exit 1 +fi + +if [ ! -f "$target_dir/scert" ]; then + echo "警告:在目标目录 $target_dir 下未检测到旧的 scert 主程序,请确认是否为正确的安装目录!" +fi + +echo "==========================================================" +echo " Scert 服务平滑更新 " +echo " 升级包解压源目录 : $package_root" +echo " 目标系统安装目录 : $target_dir" +echo "==========================================================" + +case "$(uname -m)" in + x86_64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; + *) echo "不支持的系统架构: $(uname -m)" >&2; exit 1 ;; +esac + +# 2. 定位新版本程序二进制文件 +if [ -f "$package_root/scert" ] && [ -f "$package_root/scert-admin" ]; then + new_scert="$package_root/scert" + new_scert_admin="$package_root/scert-admin" + new_scert_nginx="$package_root/scert-nginx" + new_scert_storage="$package_root/scert-storage" +else + echo "错误:未能找到升级包中的二进制程序文件!" >&2 + exit 1 +fi + +# 3. 停止运行中的服务以解除文件占用 +echo ">>> 正在停止运行中的 Scert 服务..." +systemctl stop Scert.service 2>/dev/null || true +systemctl stop Scert-admin.service 2>/dev/null || true +systemctl stop scert-nginx.service 2>/dev/null || true +systemctl stop Scert-storage.service 2>/dev/null || true + +# 4. 确保运行用户与组存在 +if ! getent group Scert >/dev/null; then + groupadd --system Scert +fi +if ! getent passwd Scert >/dev/null; then + useradd --system --gid Scert --home-dir "$target_dir" --shell /usr/sbin/nologin Scert +fi + +# 5. 替换程序二进制文件至安装目录(保持现有数据库和配置文件不被覆盖) +echo ">>> 正在更新主程序及管理工具至: $target_dir" +if [ "$new_scert" != "$target_dir/scert" ]; then + cp -f "$new_scert" "$target_dir/scert" +fi +chown Scert:Scert "$target_dir/scert" +chmod 0755 "$target_dir/scert" + +if [ "$new_scert_admin" != "$target_dir/scert-admin" ]; then + cp -f "$new_scert_admin" "$target_dir/scert-admin" +fi +chown root:root "$target_dir/scert-admin" +chmod 0700 "$target_dir/scert-admin" + +if [ -f "$new_scert_nginx" ]; then + cp -f "$new_scert_nginx" "$target_dir/scert-nginx" + chown root:root "$target_dir/scert-nginx" + chmod 0755 "$target_dir/scert-nginx" +fi + +if [ -f "$new_scert_storage" ]; then + cp -f "$new_scert_storage" "$target_dir/scert-storage" + chown root:root "$target_dir/scert-storage" + chmod 0755 "$target_dir/scert-storage" +fi + +# 同步更新前端静态资源 web/ +if [ -d "$package_root/web" ]; then + echo ">>> 正在同步前端静态资源..." + mkdir -p "$target_dir/web" + cp -rf "$package_root/web"/* "$target_dir/web/" 2>/dev/null || true +fi + +# 同时更新安装目录下的 deploy 脚本供日后维护 +if [ "$package_root" != "$target_dir" ] && [ -d "$package_root/deploy" ]; then + mkdir -p "$target_dir/deploy" + cp -rf "$package_root/deploy"/* "$target_dir/deploy/" 2>/dev/null || true + chmod -R 0755 "$target_dir/deploy" 2>/dev/null || true +fi + +# 6. 更新 systemd 服务单元 +systemd_src="$package_root/deploy/systemd" +if [ ! -d "$systemd_src" ] && [ -d "$target_dir/deploy/systemd" ]; then + systemd_src="$target_dir/deploy/systemd" +fi + +if [ -d "$systemd_src" ]; then + for svc in Scert.service Scert-admin.service scert-nginx.service scert-nginx.socket Scert-storage.service Scert-storage.socket; do + if [ -f "$systemd_src/$svc" ]; then + sed -e "s|/scert|$target_dir|g" "$systemd_src/$svc" > "/etc/systemd/system/$svc" + chmod 0644 "/etc/systemd/system/$svc" + chown root:root "/etc/systemd/system/$svc" + fi + done +fi + +# 7. 重载 systemd 并重新启动 Scert +echo ">>> 重载 systemd 配置并重启 Scert 服务..." +systemctl daemon-reload +systemctl enable Scert.service >/dev/null 2>&1 || true +systemctl restart Scert.service + +# 8. 检查运行状态 +sleep 1 +if systemctl is-active --quiet Scert.service; then + echo "==========================================================" + echo " Scert 平滑升级成功!" + echo " 升级程序写入目录: $target_dir" + echo " 数据库、SSL 证书及配置均完整保留。" + echo " 服务状态: active (running)" + echo "==========================================================" +else + echo "警告:Scert 服务启动异常,请检查服务日志: journalctl -u Scert -n 20 --no-pager" >&2 + exit 1 +fi diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 50a6066..40f6225 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -3719,7 +3719,7 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 ## 2026年10月18日 发布包目录架构重构与deploy部署运维套件标准化 -- [~] 需求梳理:统一所有版本的发布包打包架构与包内目录结构,参考标准规范结构(与 Smail 部署体系规范对齐): +- [✓] 需求梳理:统一所有版本的发布包打包架构与包内目录结构,参考标准规范结构(与 Smail 部署体系规范对齐): scert/ ├── config/ # 运行时配置目录 ├── data/ # 业务数据/数据库目录 @@ -3740,9 +3740,9 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 ├── scert-nginx.socket ├── Scert-storage.service └── Scert-storage.socket -- [ ] 编写 deploy/install-systemd.sh:实现交互式全新安装部署、前置环境与端口检查、systemd 服务单元安装与平滑启用。 -- [ ] 编写 deploy/update-systemd.sh:实现生产环境版本平滑升级,严密保护现有数据库、证书与配置不被覆盖破坏。 -- [ ] 改造 deploy/package.mjs:支持生成上述标准规范层级目录,并在 Linux 环境下保留可执行脚本 755 权限。 -- [ ] 适配 .github/workflows/release.yml 与 .gitea/workflows/release.yaml 工作流,确保 CI/CD 自动发布一致。 -- [ ] 运行全量单元测试与打包验证,检查生成包目录结构与 SHA256SUMS。 -- [ ] 提交 Git 代码并双重推送到 git.pcux.cn 与 github.com。 +- [✓] 编写 deploy/install-systemd.sh:实现交互式全新安装部署、前置环境与端口检查、systemd 服务单元安装与平滑启用。 +- [✓] 编写 deploy/update-systemd.sh:实现生产环境版本平滑升级,严密保护现有数据库、证书与配置不被覆盖破坏。 +- [✓] 改造 deploy/package.mjs:支持生成上述标准规范层级目录,并在 Linux 环境下保留可执行脚本 755 权限。 +- [✓] 适配 .github/workflows/release.yml 与 .gitea/workflows/release.yaml 工作流,确保 CI/CD 自动发布一致。 +- [✓] 运行全量单元测试与打包验证,检查生成包目录结构与 SHA256SUMS。 +- [✓] 提交 Git 代码并双重推送到 git.pcux.cn 与 github.com。