create-ssnode-cli 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/index.js +17 -21
  3. package/package.json +1 -1
  4. package/secrets.js +32 -34
  5. package/templates/backend/Dockerfile +18 -8
  6. package/templates/backend/config/config.base.yaml +130 -0
  7. package/templates/backend/config/config.develop.yaml +8 -75
  8. package/templates/backend/config/config.prod.yaml +12 -67
  9. package/templates/backend/docker-compose.yml +30 -22
  10. package/templates/backend/ops/docker/config.sh +30 -15
  11. package/templates/backend/ops/docker/install.sh +25 -34
  12. package/templates/backend/ops/docker/run-container.sh +100 -0
  13. package/templates/backend/ops/docker/start.sh +37 -8
  14. package/templates/backend/ops/docker/stop.sh +1 -1
  15. package/templates/backend/ops/ensure-encrypt.cjs +217 -0
  16. package/templates/backend/ops/install.sh +249 -148
  17. package/templates/backend/ops/lib-encrypt.sh +98 -0
  18. package/templates/backend/ops/lib.sh +22 -0
  19. package/templates/backend/ops/pm2/config.sh +11 -6
  20. package/templates/backend/ops/pm2/install.sh +27 -3
  21. package/templates/backend/ops/pm2/start.sh +1 -1
  22. package/templates/backend/ops/start.sh +7 -5
  23. package/templates/backend/ops/status.sh +7 -5
  24. package/templates/backend/ops/stop.sh +7 -5
  25. package/templates/backend/ops/systemd/config.sh +13 -6
  26. package/templates/backend/ops/systemd/install.sh +28 -1
  27. package/templates/backend/ops/systemd/start.sh +1 -1
  28. package/templates/backend/ops/versionswitch.sh +34 -122
  29. package/templates/backend/package-lock.json +7561 -0
  30. package/templates/backend/package.json +3 -1
  31. package/templates/backend/scripts/build.cjs +104 -10
  32. package/templates/backend/scripts/config-crypt.cjs +108 -0
  33. package/templates/backend/scripts/lib/ssh.cjs +72 -3
  34. package/templates/backend/scripts/publish.cjs +17 -9
  35. package/templates/backend/scripts/pulldocker.cjs +208 -0
  36. package/templates/backend/scripts/pushdocker.cjs +323 -0
  37. package/templates/backend/scripts/rollback.cjs +6 -3
  38. package/templates/backend/scripts/server.config.example.cjs +43 -15
  39. package/templates/backend/scripts/slowsql-report.mjs +2 -2
  40. package/templates/backend/src/common/api-log/api-log.service.ts +1 -1
  41. package/templates/backend/src/common/config-crypto.ts +133 -0
  42. package/templates/backend/src/config/config.interface.ts +22 -3
  43. package/templates/backend/src/config/configuration.ts +173 -86
  44. package/templates/backend/src/database/migrations/0001-init.migration.ts +1 -1
  45. package/templates/backend/src/database/slow-sql.logger.ts +1 -1
  46. package/templates/backend/src/database/sqlite.config.ts +2 -2
  47. package/templates/backend/src/main.ts +3 -3
  48. package/templates/backend/src/modules/ops/ops-probe.interface.ts +3 -3
  49. package/templates/backend/src/modules/ops/ops.module.ts +2 -11
  50. package/templates/backend/src/modules/user/user.service.ts +2 -2
  51. package/templates/docs/config-encryption.md +164 -0
  52. package/templates/root/AGENTS.md +5 -4
  53. package/templates/root/CLAUDE.md +1 -1
  54. package/templates/root/README.md +25 -15
  55. package/templates/root/_gitignore +5 -0
  56. package/templates/root/scripts/publish.cjs +20 -2
  57. package/templates/root/scripts/rollback.cjs +20 -2
  58. package/templates/web/config/router.config.ts +8 -0
  59. package/templates/web/package.json +5 -1
  60. package/templates/web/scripts/compress-src-images.cjs +332 -0
  61. package/templates/web/scripts/lib/interactive.cjs +134 -0
  62. package/templates/web/scripts/publish.cjs +299 -200
  63. package/templates/web/scripts/rollback.cjs +110 -41
  64. package/templates/web/scripts/server.config.example.json +17 -0
  65. package/templates/web/src/layouts/Background.module.less +0 -4
  66. package/templates/web/src/layouts/Background.tsx +48 -74
  67. package/templates/web/src/layouts/components/PageTabs/SortableTab.tsx +133 -0
  68. package/templates/web/src/layouts/components/PageTabs/index.module.less +105 -0
  69. package/templates/web/src/layouts/components/PageTabs/index.tsx +110 -0
  70. package/templates/web/src/layouts/components/PageTabs/usePageTabs.ts +224 -0
  71. package/templates/web/src/pages/Backend/Image/index.tsx +4 -4
  72. package/templates/web/src/pages/Maintenance/index.module.less +177 -0
  73. package/templates/web/src/pages/Maintenance/index.tsx +142 -0
  74. package/templates/web/src/services/api.ts +4 -0
  75. package/templates/web/src/services/app-request.ts +8 -1
  76. package/templates/web/src/services/maintenance.ts +67 -0
  77. package/templates/web/src/services/request.ts +5 -0
  78. package/transforms.js +21 -28
  79. package/templates/backend/ops/docker/.env.example +0 -40
  80. package/templates/backend/ops/sqlite/README.md +0 -57
  81. package/templates/backend/ops/sqlite/backup.sh +0 -65
  82. package/templates/backend/ops/sqlite/config.sh +0 -91
  83. package/templates/backend/ops/sqlite/healthcheck.sh +0 -25
  84. package/templates/backend/ops/sqlite/restore.sh +0 -105
  85. package/templates/backend/src/modules/ops/sqlite-backup-list.probe.ts +0 -100
  86. package/templates/backend/src/modules/ops/sqlite-backup-log.probe.ts +0 -63
  87. package/templates/backend/src/modules/ops/sqlite-backup.probe.ts +0 -144
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ ###
3
+ # Description 配置加密维护(lib)—— 三模式 mode install 共用(被 source,零动作)
4
+ ###
5
+ # 由 systemd/pm2/docker 的 install.sh source(调用方已 source 各自 config.sh,
6
+ # MODE/APP_ROOT 已就位)。加密模型见 docs/config-encryption.md:
7
+ # 仓库与传输层明文维护(开发零密钥零加密操作),服务器侧落盘为密文 ——
8
+ # · $APP_ROOT/.prod.secrets 存在 → relock:校验既有密文能解 + 就地加密新增明文
9
+ # (改部署位 config.yaml 明文 → 重跑 ./install.sh 即完成轮换,服务器上不需要 config-crypt)
10
+ # · 首部署选了加密(CONFIG_ENCRYPT_INIT=1,顶层 install.sh bootstrap 交互置位)且无密钥文件
11
+ # → init:生成本机根密钥写 .prod.secrets(600,现场打印一次供抄录进密码管理器),随后同 relock
12
+ # · 都不满足(明文模式)→ 零动作,应用以 security.encrypt=false 接受明文
13
+ # 加密本体在 ops/ensure-encrypt.cjs(零依赖 node 脚本,行级改写保留注释)。
14
+ # node 执行器解析:systemd = config.sh 的 NODE_BIN > PATH;pm2 = PATH;
15
+ # docker = 一次性容器(调用时镜像已构建;--user 以运行者 uid 跑,写回文件的宿主属主不变)。
16
+ # 参数:ensure_config_encryption [<docker镜像引用>](仅 docker 模式需要,= 本次构建的镜像)
17
+
18
+ ensure_config_encryption() {
19
+ local secrets="$APP_ROOT/.prod.secrets" cfg_dir="$APP_ROOT/config" image="${1:-}" key=""
20
+ [[ -f "$cfg_dir/config.yaml" ]] || { echo "⚠ 缺 $cfg_dir/config.yaml,跳过配置加密" >&2; return 0; }
21
+
22
+ if [[ -f "$secrets" ]]; then
23
+ key="$(sed -nE 's/^CONFIG_SECRET_KEY=//p' "$secrets" | head -1 | sed -E "s/^['\"]?//; s/['\"]?$//")"
24
+ [[ -n "$key" ]] || { echo "✗ $secrets 无 CONFIG_SECRET_KEY(补齐后重跑 ./install.sh)" >&2; return 1; }
25
+ elif [[ "${CONFIG_ENCRYPT_INIT:-0}" = "1" ]]; then
26
+ # 生成本机根密钥:openssl(宿主恒有优先)> ensure-encrypt.cjs genkey(宿主无 openssl 时,
27
+ # systemd/pm2 用 NODE_BIN、docker 用一次性容器跑)
28
+ if command -v openssl >/dev/null 2>&1; then
29
+ key="$(openssl rand -base64 32 | tr -d '\n')"
30
+ else
31
+ key="$(encrypt_genkey "$image")" || return 1
32
+ fi
33
+ [[ -n "$key" ]] || { echo "✗ 根密钥生成失败(openssl 与 node 均不可用)" >&2; return 1; }
34
+ {
35
+ echo '# 配置密文根密钥 —— 首部署时由 install.sh 在本机生成,勿外传;务必备份进密码管理器。'
36
+ echo '# 轮换:改部署位 config/config.yaml 明文 → 重跑 ./install.sh 自动重新加密(手册 docs/config-encryption.md)。'
37
+ echo "CONFIG_SECRET_KEY=$key"
38
+ } > "$secrets"
39
+ chmod 600 "$secrets"
40
+ echo ""
41
+ echo "✓ 配置加密根密钥已生成 → $secrets (600;仅此一次打印,存进密码管理器):"
42
+ echo " $key"
43
+ else
44
+ return 0 # 明文模式:无密钥文件即不加密,应用按 security.encrypt=false 接受明文
45
+ fi
46
+
47
+ echo "▶ 配置加密:校验既有密文 + 就地加密部署位 config.yaml 的非空明文敏感值 ..."
48
+ encrypt_relock "$image" "$key" "$cfg_dir"
49
+ }
50
+
51
+ # ensure-encrypt.cjs 定位(共享 ops,lay_out_ops 已就位)
52
+ encrypt_helper() {
53
+ local h="$APP_ROOT/ops/ensure-encrypt.cjs"
54
+ [[ -f "$h" ]] || { echo "✗ 缺 $h(共享 ops 未就位;重跑 ./install.sh 完成部署)" >&2; return 1; }
55
+ printf '%s' "$h"
56
+ }
57
+
58
+ # genkey(宿主无 openssl 时的兜底):systemd/pm2 走 NODE_BIN/PATH,docker 走一次性容器
59
+ encrypt_genkey() {
60
+ local image="${1:-}" h node_bin out
61
+ h="$(encrypt_helper)" || return 1
62
+ if [[ "$MODE" == "docker" ]]; then
63
+ [[ -n "$image" ]] || { echo "✗ docker 模式配置加密缺镜像引用" >&2; return 1; }
64
+ out="$(docker run --rm -v "$h":/app/ensure-encrypt.cjs:ro "$image" node /app/ensure-encrypt.cjs genkey 2>/dev/null)" \
65
+ || { echo "✗ 一次性容器 genkey 失败" >&2; return 1; }
66
+ else
67
+ encrypt_resolve_node || return 1
68
+ out="$("$ENCRYPT_NODE_BIN" "$h" genkey)" || { echo "✗ genkey 失败" >&2; return 1; }
69
+ fi
70
+ printf '%s' "$out"
71
+ }
72
+
73
+ # node 执行器解析(systemd/pm2 宿主直跑;结果写 ENCRYPT_NODE_BIN)
74
+ encrypt_resolve_node() {
75
+ if [[ "$MODE" == "systemd" && -x "${NODE_BIN:-}" ]]; then
76
+ ENCRYPT_NODE_BIN="$NODE_BIN"
77
+ else
78
+ ENCRYPT_NODE_BIN="$(command -v node || true)"
79
+ fi
80
+ [[ -n "$ENCRYPT_NODE_BIN" ]] || { echo "✗ 找不到 node($MODE 模式配置加密需要;明文模式可忽略,修好 NODE_BIN/PATH 后重跑)" >&2; return 1; }
81
+ }
82
+
83
+ # relock:加密维护本体(systemd/pm2 宿主直跑;docker 一次性容器,--user 保宿主文件属主)
84
+ encrypt_relock() {
85
+ local image="$1" key="$2" cfg_dir="$3" h
86
+ h="$(encrypt_helper)" || return 1
87
+ if [[ "$MODE" == "docker" ]]; then
88
+ [[ -n "$image" ]] || { echo "✗ docker 模式配置加密缺镜像引用(应传本次构建的镜像)" >&2; return 1; }
89
+ docker run --rm --user "$(id -u):$(id -g)" \
90
+ -e CONFIG_SECRET_KEY="$key" \
91
+ -v "$h":/app/ensure-encrypt.cjs:ro \
92
+ -v "$cfg_dir":/app/config \
93
+ "$image" node /app/ensure-encrypt.cjs relock /app/config
94
+ else
95
+ encrypt_resolve_node || return 1
96
+ CONFIG_SECRET_KEY="$key" "$ENCRYPT_NODE_BIN" "$h" relock "$cfg_dir"
97
+ fi
98
+ }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ ###ops 共享函数库(source 时零动作)—— 跨 mode 公共能力,随包全量分发。###
3
+ # 身份纪律:ops 不烙项目内容,服务名/容器名/镜像名一律运行时从 current/package.json
4
+ # 的 name 派生(pkg_name)—— 与镜像 tag/version(同文件)同源自洽,改名只改 package.json。
5
+ # 覆盖口子在各 mode config.sh 的 ${VAR:-$(pkg_name)}:身份需与包名分叉的项目
6
+ # (如 scoped 包名剥 @scope 后不合意)export SERVICE/CONTAINER_NAME/IMAGE/APP_NAME 即可。
7
+ #
8
+ # current/package.json 服务器恒在:docker = current 真目录;systemd/pm2 = current 软链,
9
+ # 且顶层 install.sh 先 go_live/落 release 后才派发 mode install;缺文件 = 部署完整性问题。
10
+
11
+ # 读 APP_ROOT/current/package.json 的 name(与各 config.sh 读 version 同款解析),
12
+ # 剥 @scope/ 取 basename,校验可用作 systemd unit / docker 容器 / pm2 app 名。
13
+ pkg_name() {
14
+ local f n
15
+ [[ -n "${APP_ROOT:-}" ]] || { echo "✗ pkg_name: APP_ROOT 未导出(应由顶层脚本或 config.sh 解析)" >&2; return 1; }
16
+ f="$APP_ROOT/current/package.json"
17
+ [[ -f "$f" ]] || { echo "✗ pkg_name: 缺 $f(current 未落位或部署不完整)" >&2; return 1; }
18
+ n="$(grep -m1 '"name"' "$f" | sed -E 's/.*"name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' || true)"
19
+ n="${n##*/}" # 剥 @scope/(如 @iscp/backend → backend)
20
+ [[ "$n" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || { echo "✗ pkg_name: 包名 $n 不可用作服务名(仅允许 [A-Za-z0-9._-])" >&2; return 1; }
21
+ printf '%s' "$n"
22
+ }
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  # pm2 部署共用配置 —— 运维按环境改这一处即可。
3
- # 配置项(端口/dataDir/signKey 等)在 current/config/config.<ENV>.yaml 里,
4
- # 改 yaml + pm2 restart 即生效,不重新打包服务。
3
+ # 配置项(端口/密钥/阈值等)在部署位 APP_ROOT/config/config.yaml 里(ecosystem 注入
4
+ # CONFIG_DIR,跨版本共享),改 yaml + pm2 restart 即生效,不重新打包服务。
5
5
 
6
- # pm2 应用名
7
- APP_NAME="fullstack-template-backend"
6
+ # pm2 应用名:不再此处定值 —— 运行时由 lib.sh::pkg_name 从 current/package.json 的 name
7
+ # 派生(见文件末 resolve_app_root 之后的赋值)。
8
8
  # 环境(决定读 config.<ENV>.yaml)
9
9
  ENV="prod"
10
10
  # 后端根目录(current / releases / data / logs 所在)
11
- # - 正常由顶层 baked 脚本(start/stop/install/versionswitch)导出 APP_ROOT 环境变量,此处直读、锁定不询问。
11
+ # - 正常由顶层脚本(start/stop/install/versionswitch,运行时自定位)导出 APP_ROOT 环境变量,此处直读、锁定不询问。
12
12
  # - 直接跑 mode 脚本(未经顶层封装)时,按脚本位置推导为 ops/<mode> 的上两级(即 APP_ROOT)。
13
13
  # - 也可 export APP_ROOT=/xxx 显式指定(锁定、不询问)。
14
14
  APP_ROOT="${APP_ROOT:-}"
@@ -27,7 +27,7 @@ derive_paths() {
27
27
  LOG_DIR="$APP_ROOT/logs"
28
28
  }
29
29
 
30
- # ── 解析 APP_ROOT ── 优先级:顶层 baked 脚本导出的环境变量 > 脚本位置推导。
30
+ # ── 解析 APP_ROOT ── 优先级:顶层脚本导出的环境变量 > 脚本位置推导。
31
31
  # 正常由顶层 start/stop/install/versionswitch 导出 APP_ROOT,此处直读;直接跑 mode 脚本时按位置推导。
32
32
  resolve_app_root() {
33
33
  if [[ -z "${APP_ROOT:-}" ]]; then
@@ -47,3 +47,8 @@ resolve_app_root() {
47
47
 
48
48
  # 各脚本 source 时自动解析一次(此后 CURRENT_DIR/... 等即就绪)
49
49
  resolve_app_root
50
+
51
+ # 共享函数库 + pm2 应用名派生(须在 resolve_app_root 之后:pkg_name 读 current/package.json,
52
+ # APP_ROOT/current 就绪才能取)。export APP_NAME 可覆盖派生值(身份与包名分叉的项目用)。
53
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib.sh"
54
+ APP_NAME="${APP_NAME:-$(pkg_name)}"
@@ -15,12 +15,36 @@ command -v "$PM2_BIN" >/dev/null 2>&1 || { echo "✗ pm2 不存在: $PM2_BIN (
15
15
 
16
16
  mkdir -p "$LOG_DIR"
17
17
 
18
+ # 配置加密维护(每次部署):首部署选「开启加密」时生成本机根密钥;有根密钥则把部署位
19
+ # config.yaml 新明文就地加密(改明文 + 重跑 ./install.sh = 轮换闭环,见 docs/config-encryption.md)。
20
+ source "$DIR/../lib-encrypt.sh"
21
+ ensure_config_encryption
22
+
18
23
  # JS 安全双引号转义(任意值合法):先转 \ 再转 "。替代 bash4.4+ 的 ${var@Q},
19
24
  # 兼容服务器 bash 4.2(CentOS/RHEL 7)。生成的字符串直接进 ecosystem.config.cjs。
20
25
  q() { local v="$1"; v="${v//\\/\\\\}"; v="${v//\"/\\\"}"; printf '"%s"' "$v"; }
21
26
 
22
- # 生成 ecosystem(pm2 读 cwd=current,配置走 release 自带 config.yaml;env 注入
23
- # NODE_ENV 选 yaml + DB_DIR 指向跨版本共享 data 目录 = APP_ROOT/data)
27
+ # 配置密文根密钥:从 $APP_ROOT/.prod.secrets(600)读 CONFIG_SECRET_KEY 注入 ecosystem env。
28
+ # 缺文件 = 明文模式(不注,应用按 security.encrypt=false 接受明文);但部署位 config.yaml
29
+ # 带 ENC(...) 而无密钥 = 部署不完整,fail-fast(应用启动同样解不开,这里拦在部署期)。
30
+ SECRETS_FILE="$APP_ROOT/.prod.secrets"
31
+ SECRET_ENV_KV=""
32
+ if [[ -f "$SECRETS_FILE" ]]; then
33
+ chmod 600 "$SECRETS_FILE" 2>/dev/null || true
34
+ secret_key="$(sed -nE 's/^CONFIG_SECRET_KEY=//p' "$SECRETS_FILE" | head -1 | sed -E "s/^['\"]?//; s/['\"]?$//")"
35
+ [[ -n "$secret_key" ]] || { echo "✗ $SECRETS_FILE 无 CONFIG_SECRET_KEY(补齐后重跑)" >&2; exit 1; }
36
+ SECRET_ENV_KV="CONFIG_SECRET_KEY: $(q "$secret_key"), "
37
+ elif grep -q 'ENC(' "$APP_ROOT/config/config.yaml" 2>/dev/null; then
38
+ echo "✗ 缺 $SECRETS_FILE 但部署位 config.yaml 含 ENC(...) 密文(重跑 ./install.sh <release 包> 重新部署)" >&2
39
+ exit 1
40
+ else
41
+ echo "▶ 明文模式:无 $SECRETS_FILE,ecosystem 不注 CONFIG_SECRET_KEY"
42
+ fi
43
+
44
+ # 生成 ecosystem(pm2 读 cwd=current;配置经 CONFIG_DIR=APP_ROOT/config 指向部署位
45
+ # (跨版本共享,不随 release 换目录),configuration.ts 据此加载 base + 环境 overlay
46
+ # (config.yaml),改它 + pm2 restart 即生效;env 注入 DB_DIR 指向跨版本共享 data 目录 =
47
+ # APP_ROOT/data;SLOW_SQL_DIR/API_LOG_DIR 同口径指向 APP_ROOT/logs(切版本不散落)
24
48
  ECO_PATH="$APP_ROOT/ecosystem.config.cjs"
25
49
  echo " 写入 ecosystem → $ECO_PATH"
26
50
  cat > "$ECO_PATH" <<EOF
@@ -34,7 +58,7 @@ module.exports = {
34
58
  out_file: $(q "$LOG_DIR/$APP_NAME.log"),
35
59
  error_file: $(q "$LOG_DIR/$APP_NAME.log"),
36
60
  merge_logs: true,
37
- env: { NODE_ENV: $(q "$ENV"), DB_DIR: $(q "$DATA_DIR") }
61
+ env: { NODE_ENV: $(q "$ENV"), CONFIG_DIR: $(q "$APP_ROOT/config"), DB_DIR: $(q "$DATA_DIR"), ${SECRET_ENV_KV}SLOW_SQL_DIR: $(q "$LOG_DIR/slowsql"), API_LOG_DIR: $(q "$LOG_DIR/apilog") }
38
62
  }]
39
63
  };
40
64
  EOF
@@ -7,7 +7,7 @@ source "$DIR/config.sh"
7
7
  echo "▶ 启动 $APP_NAME..."
8
8
  if $PM2_BIN jlist 2>/dev/null | grep -q "\"name\":\"$APP_NAME\""; then
9
9
  # restart(非 reload):本应用 exec_mode=fork,pm2 reload 仅供 cluster 模式、fork 下不可靠;
10
- # restart 对 running/stopped 均幂等,且重启进程 → 重读 current/config/config.prod.yaml(改配置+./start.sh 轮换)。
10
+ # restart 对 running/stopped 均幂等,且重启进程 → 重读部署位 APP_ROOT/config/config.yaml(跨版本共享;改配置+./start.sh 轮换)。
11
11
  $PM2_BIN restart "$APP_NAME" --update-env
12
12
  echo "✓ restarted."
13
13
  else
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  ###
3
- # Description backend 启动薄封装 —— 按 baked MODE 派发到 ops/<mode>/start.sh
3
+ # Description backend 启动薄封装 —— 按 MODE 派发到 ops/<mode>/start.sh
4
4
  ###
5
- # 由 install.sh 首次部署时拷到 APP_ROOT 根并 baked APP_ROOT/MODE 字面量。
5
+ # install.sh 首次部署拷到 APP_ROOT 根。零配置(不 baked):
6
+ # APP_ROOT = 脚本所在目录(整个 APP_ROOT 搬迁后开箱即用)
7
+ # MODE = $APP_ROOT/.mode(首次部署时由 install.sh 写入)
6
8
  # 直接在 APP_ROOT 下跑: ./start.sh
7
9
  set -euo pipefail
8
- APP_ROOT="${APP_ROOT:-}" # baked
9
- MODE="${MODE:-}" # baked
10
- [[ -n "$APP_ROOT" && -n "$MODE" ]] || { echo "✗ 后台服务目录/MODE 未 baked(先跑 install.sh 完成首次部署)" >&2; exit 1; }
10
+ APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ MODE="$(cat "$APP_ROOT/.mode" 2>/dev/null || true)"
12
+ [[ -n "$MODE" ]] || { echo "✗ 缺 $APP_ROOT/.mode(先跑 ./install.sh 完成首次部署)" >&2; exit 1; }
11
13
  export APP_ROOT MODE
12
14
  exec bash "$APP_ROOT/ops/$MODE/start.sh"
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  ###
3
- # Description backend 状态薄封装 —— 按 baked MODE 派发到 ops/<mode>/status.sh
3
+ # Description backend 状态薄封装 —— 按 MODE 派发到 ops/<mode>/status.sh
4
4
  ###
5
- # 由 install.sh 首次部署时拷到 APP_ROOT 根并 baked APP_ROOT/MODE 字面量。
5
+ # install.sh 首次部署拷到 APP_ROOT 根。零配置(不 baked):
6
+ # APP_ROOT = 脚本所在目录(整个 APP_ROOT 搬迁后开箱即用)
7
+ # MODE = $APP_ROOT/.mode(首次部署时由 install.sh 写入)
6
8
  # 直接在 APP_ROOT 下跑: ./status.sh
7
9
  set -euo pipefail
8
- APP_ROOT="${APP_ROOT:-}" # baked
9
- MODE="${MODE:-}" # baked
10
- [[ -n "$APP_ROOT" && -n "$MODE" ]] || { echo "✗ 后台服务目录/MODE 未 baked(先跑 install.sh 完成首次部署)" >&2; exit 1; }
10
+ APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ MODE="$(cat "$APP_ROOT/.mode" 2>/dev/null || true)"
12
+ [[ -n "$MODE" ]] || { echo "✗ 缺 $APP_ROOT/.mode(先跑 ./install.sh 完成首次部署)" >&2; exit 1; }
11
13
  export APP_ROOT MODE
12
14
  exec bash "$APP_ROOT/ops/$MODE/status.sh"
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  ###
3
- # Description backend 停止薄封装 —— 按 baked MODE 派发到 ops/<mode>/stop.sh
3
+ # Description backend 停止薄封装 —— 按 MODE 派发到 ops/<mode>/stop.sh
4
4
  ###
5
- # 由 install.sh 首次部署时拷到 APP_ROOT 根并 baked APP_ROOT/MODE 字面量。
5
+ # install.sh 首次部署拷到 APP_ROOT 根。零配置(不 baked):
6
+ # APP_ROOT = 脚本所在目录(整个 APP_ROOT 搬迁后开箱即用)
7
+ # MODE = $APP_ROOT/.mode(首次部署时由 install.sh 写入)
6
8
  # 直接在 APP_ROOT 下跑: ./stop.sh
7
9
  set -euo pipefail
8
- APP_ROOT="${APP_ROOT:-}" # baked
9
- MODE="${MODE:-}" # baked
10
- [[ -n "$APP_ROOT" && -n "$MODE" ]] || { echo "✗ 后台服务目录/MODE 未 baked(先跑 install.sh 完成首次部署)" >&2; exit 1; }
10
+ APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ MODE="$(cat "$APP_ROOT/.mode" 2>/dev/null || true)"
12
+ [[ -n "$MODE" ]] || { echo "✗ 缺 $APP_ROOT/.mode(先跑 ./install.sh 完成首次部署)" >&2; exit 1; }
11
13
  export APP_ROOT MODE
12
14
  exec bash "$APP_ROOT/ops/$MODE/stop.sh"
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env bash
2
2
  # systemd 部署共用配置 —— 运维按环境改这一处即可,各脚本都 source 它。
3
3
  # 改完不用重新打包服务:systemd unit 读的是这里的值(install 时生成),配置项
4
- # (端口/dataDir/signKey 等)在 current/config/config.<ENV>.yaml 里,改 yaml + restart 即生效。
4
+ # (端口/密钥/阈值等)在部署位 APP_ROOT/config/config.yaml 里(unit 注入 CONFIG_DIR,
5
+ # 跨版本共享),改 yaml + restart 即生效。
5
6
 
6
- # 服务名(unit 文件名 = $SERVICE.service)
7
- SERVICE="fullstack-template-backend"
8
- # 环境(决定进程读 config.<ENV>.yaml,prod 不开 synchronize、启动自动跑迁移)
7
+ # 服务名:不再此处定值 —— 运行时由 lib.sh::pkg_name 从 current/package.json 的 name 派生
8
+ # (见文件末 resolve_app_root 之后的赋值;unit 文件名 = $SERVICE.service)。
9
+ # 环境(NODE_ENV;prod 不开 synchronize、启动自动跑迁移。yaml 文件选择见 configuration.ts:
10
+ # CONFIG_DIR 里 config.yaml(部署位固定名)优先)
9
11
  ENV="prod"
10
12
  # 后端根目录(current / releases / data / logs 所在)
11
- # - 正常由顶层 baked 脚本(start/stop/install/versionswitch)导出 APP_ROOT 环境变量,此处直读、锁定不询问。
13
+ # - 正常由顶层脚本(start/stop/install/versionswitch,运行时自定位)导出 APP_ROOT 环境变量,此处直读、锁定不询问。
12
14
  # - 直接跑 mode 脚本(未经顶层封装)时,按脚本位置推导为 ops/<mode> 的上两级(即 APP_ROOT)。
13
15
  # - 也可 export APP_ROOT=/xxx 显式指定(锁定、不询问)。
14
16
  APP_ROOT="${APP_ROOT:-}"
@@ -27,7 +29,7 @@ derive_paths() {
27
29
  LOG_DIR="$APP_ROOT/logs"
28
30
  }
29
31
 
30
- # ── 解析 APP_ROOT ── 优先级:顶层 baked 脚本导出的环境变量 > 脚本位置推导。
32
+ # ── 解析 APP_ROOT ── 优先级:顶层脚本导出的环境变量 > 脚本位置推导。
31
33
  # 正常由顶层 start/stop/install/versionswitch 导出 APP_ROOT,此处直读;直接跑 mode 脚本时按位置推导。
32
34
  resolve_app_root() {
33
35
  if [[ -z "${APP_ROOT:-}" ]]; then
@@ -48,5 +50,10 @@ resolve_app_root() {
48
50
  # 各脚本 source 时自动解析一次(此后 CURRENT_DIR/... 等即就绪)
49
51
  resolve_app_root
50
52
 
53
+ # 共享函数库 + 服务名派生(须在 resolve_app_root 之后:pkg_name 读 current/package.json,
54
+ # APP_ROOT/current 就绪才能取)。export SERVICE 可覆盖派生值(身份与包名分叉的项目用)。
55
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib.sh"
56
+ SERVICE="${SERVICE:-$(pkg_name)}"
57
+
51
58
  # 非 root 自动 sudo(systemctl 写 /etc/systemd 需 root)
52
59
  if [[ $EUID -ne 0 ]]; then SUDO="sudo"; else SUDO=""; fi
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # 安装 systemd 服务:生成 unit → daemon-reload → enable → restart(新代码生效)。
3
- # 由顶层 install.sh/versionswitch.sh 派发(APP_ROOT 已 baked/导出,config.sh 锁定不弹菜单)。
3
+ # 由顶层 install.sh/versionswitch.sh 派发(APP_ROOT 已导出,config.sh 锁定不弹菜单)。
4
4
  # 前提:current 已就绪 —— current 是软链 → releases/<ver>-<ts>(install.sh IS_SYMLINK=1 + go_live ln -sfn),
5
5
  # unit 的 WorkingDirectory=current 跟着软链走;每次部署重生成 unit + restart,新 release 生效。
6
6
  set -euo pipefail
@@ -30,7 +30,30 @@ echo " 工作目录: $CURRENT_DIR"
30
30
 
31
31
  $SUDO mkdir -p "$LOG_DIR"
32
32
 
33
+ # 配置加密维护(每次部署):首部署选「开启加密」时生成本机根密钥;有根密钥则把部署位
34
+ # config.yaml 新明文就地加密(改明文 + 重跑 ./install.sh = 轮换闭环,见 docs/config-encryption.md)。
35
+ source "$DIR/../lib-encrypt.sh"
36
+ ensure_config_encryption
37
+
38
+ # 配置密文根密钥:unit 以 EnvironmentFile 读 $APP_ROOT/.prod.secrets(600,仅属主可读),
39
+ # 不直接写进 unit(unit 在 /etc/systemd 下可被多读;secrets 文件独立收紧权限、轮换不动 unit)。
40
+ # 缺文件 = 明文模式(unit 不注 EnvironmentFile);但部署位 config.yaml 带 ENC(...) 而无密钥
41
+ # = 部署不完整,fail-fast(应用启动也会因解不开密文失败,这里拦在部署期更好排查)。
42
+ SECRETS_FILE="$APP_ROOT/.prod.secrets"
43
+ ENVFILE_LINE=""
44
+ if [[ -f "$SECRETS_FILE" ]]; then
45
+ $SUDO chmod 600 "$SECRETS_FILE" 2>/dev/null || true
46
+ ENVFILE_LINE="EnvironmentFile=$SECRETS_FILE"
47
+ elif grep -q 'ENC(' "$APP_ROOT/config/config.yaml" 2>/dev/null; then
48
+ err "缺 $SECRETS_FILE 但部署位 config.yaml 含 ENC(...) 密文(重跑 ./install.sh <release 包> 重新部署)"
49
+ exit 1
50
+ else
51
+ say "明文模式:无 $SECRETS_FILE,unit 不注 EnvironmentFile(应用按 security.encrypt=false 接受明文)"
52
+ fi
53
+
33
54
  # unit 字段全走 current 软链 / APP_ROOT / 持久化 config.sh,跨版本逐字节不变。
55
+ # CONFIG_DIR=APP_ROOT/config:部署位配置(跨版本共享,不随 release 换目录),configuration.ts
56
+ # 据此加载 base + 环境 overlay(config.yaml);改它 + systemctl restart 即生效。
34
57
  UNIT_PATH="/etc/systemd/system/$SERVICE.service"
35
58
  NEW_UNIT="$(cat <<EOF
36
59
  [Unit]
@@ -42,7 +65,11 @@ Type=simple
42
65
  User=$RUN_USER
43
66
  WorkingDirectory=$CURRENT_DIR
44
67
  Environment=NODE_ENV=$ENV
68
+ Environment=CONFIG_DIR=$APP_ROOT/config
45
69
  Environment=DB_DIR=$APP_ROOT/data
70
+ Environment=SLOW_SQL_DIR=$LOG_DIR/slowsql
71
+ Environment=API_LOG_DIR=$LOG_DIR/apilog
72
+ $ENVFILE_LINE
46
73
  ExecStart=$NODE_BIN $CURRENT_DIR/dist/main.js
47
74
  Restart=on-failure
48
75
  RestartSec=5
@@ -6,7 +6,7 @@ source "$DIR/config.sh"
6
6
 
7
7
  echo "▶ 启动 $SERVICE..."
8
8
  # restart(非 start):服务在跑时 systemctl start 是 no-op、不重载配置;restart 始终重启
9
- # 进程 → 重读 current/config/config.prod.yaml,适用于「服务器改 yaml 后直接 ./start.sh 轮换」。
9
+ # 进程 → 重读部署位 APP_ROOT/config/config.yaml(跨版本共享),适用于「服务器改配置后直接 ./start.sh 轮换」。
10
10
  $SUDO systemctl restart "$SERVICE"
11
11
  $SUDO systemctl --no-pager --full status "$SERVICE" | head -n 12 || true
12
12
  echo "✓ restarted. 状态: ./status.sh | 日志: ./logs.sh"
@@ -2,18 +2,21 @@
2
2
  ###
3
3
  # Description backend 版本切换薄封装 —— 按模式切换激活版本(可升可降,不限于回滚)
4
4
  ###
5
- # 由 install.sh 首次部署时拷到 APP_ROOT 根并 baked APP_ROOT/MODE 字面量。
5
+ # install.sh 首次部署拷到 APP_ROOT 根。零配置(不 baked):
6
+ # APP_ROOT = 脚本所在目录(整个 APP_ROOT 搬迁后开箱即用)
7
+ # MODE = $APP_ROOT/.mode(首次部署时由 install.sh 写入)
6
8
  # 直接在 APP_ROOT 下跑: ./versionswitch.sh [序号或版本片段]
7
9
  #
8
- # systemd/pm2(current=软链):列 releases 备份,选一个 → 原子改链 current → 刷新 config →
10
+ # systemd/pm2(current=软链):列 releases 备份,选一个 → 原子改链 current →
9
11
  # 派发 mode install 重启。旧 release 保留在 releases(可再切换)。data/logs 不动。
10
- # docker(current=真实目录):列镜像 tag,选一个 → 停删容器 → 用目标 tag 重 run(env 同 .env)。
12
+ # docker(current=真实目录):列镜像 tag,选一个 → 停删容器 → 用目标 tag 重 run(env 同 .prod.docker.env)。
13
+ # 配置/端口不随版本走:部署位 APP_ROOT/config/config.yaml(跨版本共享,systemd/pm2 经
14
+ # CONFIG_DIR、docker 经挂载注入)与 .prod.docker.env 的 PORT(docker 权威)恒定,
15
+ # 切版本 = 只换代码,配置/端口原样。
11
16
  set -euo pipefail
12
- APP_ROOT="${APP_ROOT:-}"
13
- MODE="${MODE:-}"
14
- [[ -n "$APP_ROOT" ]] || { echo "✗ 后台服务目录未 baked" >&2; exit 1; }
15
- [[ -n "$MODE" ]] || MODE="$(cat "$APP_ROOT/.mode" 2>/dev/null || true)"
16
- [[ -n "$MODE" ]] || { echo "✗ 缺 MODE" >&2; exit 1; }
17
+ APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ MODE="$(cat "$APP_ROOT/.mode" 2>/dev/null || true)"
19
+ [[ -n "$MODE" ]] || { echo "✗ 缺 $APP_ROOT/.mode(先跑 ./install.sh 完成首次部署)" >&2; exit 1; }
17
20
 
18
21
  say() { printf '\033[36m▶\033[0m %s\n' "$*"; }
19
22
  ok() { printf '\033[32m✓\033[0m %s\n' "$*"; }
@@ -35,14 +38,13 @@ sort_releases() {
35
38
  done < <(ls -1d "$rel"/* 2>/dev/null || true) | sort -r -k1,1 | cut -f2-
36
39
  }
37
40
 
38
- # 后台服务访问地址:端口以 config.prod.yaml app.port 为权威(yaml 即运行端口)。
39
- # systemd/pm2:current 软链 → 运行中 release 的 config/config.prod.yaml;
40
- # docker:运行中容器所用镜像 → image_yaml_port(docker run --entrypoint cat 读其 /app/config)。
41
+ # 后台服务访问地址:端口 = 部署位权威。
42
+ # systemd/pm2:APP_ROOT/config/config.yaml 的 app.port(CONFIG_DIR 注入,跨版本恒定);
43
+ # docker:运行容器注入的 PORT env → .prod.docker.env PORT → 镜像内 yaml 兜底。
41
44
  # 前缀固定 /api。外网访问用服务器 IP 替换 localhost。
42
45
  service_addr() {
43
46
  local port=""
44
47
  if [[ "$MODE" == "docker" ]]; then
45
- # 优先取运行容器注入的 PORT env(选项 2/3 覆盖时 app 实际监听此端口);否则取镜像内 yaml
46
48
  port="$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$CONTAINER_NAME" 2>/dev/null \
47
49
  | sed -nE 's/^PORT=//p' | head -1 || true)"
48
50
  if [[ -z "$port" ]]; then
@@ -50,7 +52,7 @@ service_addr() {
50
52
  [[ -n "$img" ]] && port="$(image_yaml_port "$img")"
51
53
  fi
52
54
  else
53
- local cfg="$APP_ROOT/current/config/config.prod.yaml"
55
+ local cfg="$APP_ROOT/config/config.yaml"
54
56
  [[ -f "$cfg" ]] && port="$(yaml_port "$cfg" || true)"
55
57
  fi
56
58
  printf 'http://localhost:%s/api' "${port:-?}"
@@ -70,8 +72,9 @@ yaml_port() {
70
72
  printf '%s' "$p" | sed -E "s/^['\"]?//; s/['\"]?$//; s/^[[:space:]]+//; s/[[:space:]]+$//"
71
73
  }
72
74
 
73
- # 读 docker 镜像内 /app/config/config.prod.yaml 的 app.port(dump 出来本地解析,复用同款口径)。
74
- # 镜像缺 config/ 或 docker 异常 → 空串(不阻断切换)。仅 docker 切换端口校验用。
75
+ # 读 docker 镜像内 /app/config/config.prod.yaml 的 app.port(镜像内兜底配置;部署位挂载
76
+ # 时的运行端口以 PORT env 为准,此函数仅在容器无 PORT env 的异常形态下兜底)。
77
+ # 镜像缺 config/ 或 docker 异常 → 空串(不阻断切换)。
75
78
  image_yaml_port() {
76
79
  local img="$1" text
77
80
  command -v docker >/dev/null 2>&1 || { printf ''; return 1; }
@@ -83,65 +86,6 @@ image_yaml_port() {
83
86
  ' | sed -E "s/^['\"]?//; s/['\"]?$//; s/^[[:space:]]+//; s/[[:space:]]+$//"
84
87
  }
85
88
 
86
- # 规范化绝对路径(软链穿透;cd 失败返回空)。用于判断 current 是否指向同一 release。
87
- real_dir() { (cd "$1" 2>/dev/null && pwd -P) || true; }
88
-
89
- # 切换端口选择(仅当目标 != 当前运行版本时调用):端口以 config.prod.yaml 为权威(yaml 即
90
- # 运行端口),切换到不同端口版本 = 运行端口将真实变更,nginx 若指向旧端口会不通。三选一:
91
- # 1) 用目标端口(不改动,nginx 需指向目标端口)
92
- # 2) 用当前运行端口(推荐,nginx 无需改动 —— 把目标 config app.port 改成当前端口)
93
- # 3) 输入新端口(把目标 config app.port 改成新端口,nginx 需指向新端口)
94
- # 结果写全局:
95
- # CHOSEN_PORT = 最终运行端口(供后续 -p/healthcheck/service_addr 用)
96
- # CHOSEN_PORT_EDIT=1 表示需要把目标 config.prod.yaml 的 app.port 改成 CHOSEN_PORT(选项 2/3)
97
- # 任一端口读不到 → 跳过(不阻断)。取消 → die(未切链/未重建容器)。
98
- choose_switch_port() {
99
- local cur_port="$1" tgt_port="$2" ans np
100
- CHOSEN_PORT=""; CHOSEN_PORT_EDIT=0
101
- if [[ -z "$cur_port" || -z "$tgt_port" ]]; then
102
- say "端口校验跳过(无法读取某方 yaml app.port,不阻断)"
103
- return 0
104
- fi
105
- [[ "$cur_port" != "$tgt_port" ]] || return 0
106
- echo
107
- say "⚠ 端口不一致:当前运行端口 app.port=$cur_port,切换目标 app.port=$tgt_port"
108
- echo " (端口以 config.prod.yaml 为权威;nginx 反代 localhost:<app.port>)"
109
- echo " 1) 使用切换版本的端口 $tgt_port(切换后 nginx 需指向 $tgt_port)"
110
- echo " 2) 使用运行版本的端口 $cur_port(推荐,nginx 无需改动)"
111
- echo " 3) 重新输入新的端口"
112
- read -rp " 选择 [2]: " ans || ans=""
113
- ans="${ans:-2}"
114
- case "$ans" in
115
- 1) CHOSEN_PORT="$tgt_port"; CHOSEN_PORT_EDIT=0 ;;
116
- 2) CHOSEN_PORT="$cur_port"; CHOSEN_PORT_EDIT=1 ;;
117
- 3) read -rp " 输入新端口: " np || np=""
118
- [[ "$np" =~ ^[0-9]{2,5}$ ]] || die "端口非法: ${np:-空}(未切链/未重建容器)"
119
- CHOSEN_PORT="$np"; CHOSEN_PORT_EDIT=1 ;;
120
- *) die "已取消切换(未切链/未重建容器)" ;;
121
- esac
122
- }
123
-
124
- # 把 release 自带 config.prod.yaml 的 app.port 改为指定值(yaml 为权威;systemd/pm2 切换后
125
- # current→该 release,app 直读此 yaml 监听 CHOSEN_PORT,nginx 指向 CHOSEN_PORT 即通)。
126
- # 只改顶层 app: 段内的 port: 行 —— 其它段(database/sso 等)自带的 port: 不动
127
- # (与 install.sh 的 set_app_port 同口径;app 段无 port 行则报错不落盘)。
128
- set_yaml_port() {
129
- local cfg="$1" port="$2"
130
- [[ -f "$cfg" ]] || { err "缺 $cfg,无法改端口"; return 1; }
131
- if ! awk -v p="$port" '
132
- /^[^[:space:]#]/ { inapp = ($0 ~ /^app:[[:space:]]*(#.*)?$/) }
133
- inapp && !done && /^[[:space:]]*port:/ { sub(/:[[:space:]]*.*/, ": " p); done=1 }
134
- { print }
135
- END { exit done ? 0 : 1 }
136
- ' "$cfg" > "$cfg.__port"; then
137
- rm -f "$cfg.__port"
138
- err "$cfg 的 app: 段内无 port: 行,无法改端口"
139
- return 1
140
- fi
141
- mv "$cfg.__port" "$cfg"
142
- ok "已更新 $cfg 的 app.port=$port"
143
- }
144
-
145
89
  # 内联选号:从数组 items + 用户输入(序号或片段)算 idx(不用 nameref,兼容旧 bash)
146
90
  # 用法: pick_idx <数组名> <用户输入>;结果写全局 PICK_IDX
147
91
  pick_idx() {
@@ -199,25 +143,8 @@ switch_symlink() {
199
143
  echo
200
144
  say "切换到: ${items[$PICK_IDX]}"
201
145
 
202
- # 切换端口选择:仅当目标 != 当前运行版本时,比对两者各自 release yaml 的 app.port。
203
- # current 是软链 → real_dir 穿透到运行中的 release 目录,读其 config/config.prod.yaml。
204
- local cur_dir; cur_dir="$(real_dir "$APP_ROOT/current")"
205
- local tgt_dir; tgt_dir="$(real_dir "$target")"
206
- if [[ -n "$cur_dir" && -n "$tgt_dir" && "$cur_dir" != "$tgt_dir" ]]; then
207
- choose_switch_port \
208
- "$(yaml_port "$cur_dir/config/config.prod.yaml")" \
209
- "$(yaml_port "$tgt_dir/config/config.prod.yaml")"
210
- # 选项 2/3:把目标 release 自带 config.prod.yaml 的端口改为运行/新端口(yaml 为权威;
211
- # 切换后 current→tgt_dir,app 直读此 yaml 监听 CHOSEN_PORT,nginx 指向 CHOSEN_PORT 不变即通)。
212
- if [[ "${CHOSEN_PORT_EDIT:-0}" == "1" ]]; then
213
- set_yaml_port "$tgt_dir/config/config.prod.yaml" "$CHOSEN_PORT"
214
- fi
215
- else
216
- say "目标即当前版本,端口校验跳过"
217
- fi
218
-
219
- # config 随 release:切换即用目标版本自带的 config.yaml(运维在 current/config.yaml
220
- # 上做的改动属于当时那个版本,不跨版本迁移)。data/ 跨版本共享,不动。
146
+ # 配置/端口不随版本走:部署位 APP_ROOT/config/config.yaml(跨版本共享,CONFIG_DIR 注入)
147
+ # 恒定,切换 = 只换代码,配置原样 —— 老模型「比对本/目标 release yaml 端口」已删除。
221
148
 
222
149
  # 切链前确认目标完整(防竞态:列表后目录被删)
223
150
  [[ -f "$target/dist/main.js" ]] \
@@ -254,7 +181,7 @@ switch_symlink() {
254
181
  switch_docker() {
255
182
  local dcfg="$APP_ROOT/ops/docker/config.sh"
256
183
  [[ -f "$dcfg" ]] || die "缺 $dcfg"
257
- # source docker config(APP_ROOT 由 baked)
184
+ # source docker config(APP_ROOT 已由顶层自定位)
258
185
  export APP_ROOT
259
186
  source "$dcfg"
260
187
 
@@ -275,52 +202,37 @@ switch_docker() {
275
202
  echo
276
203
  say "切换到镜像: $IMAGE:$tag"
277
204
 
278
- # docker 端口权威 = .env 的 PORT(跨 tag 恒定,env 注入;切镜像不改端口)。
205
+ # docker 端口权威 = .prod.docker.env 的 PORT(跨 tag 恒定,env 注入;切镜像不改端口)。
279
206
  # 当前运行版本 = 运行中容器所用镜像(docker inspect 取 ref);切换不改 current/,故不读 current/config。
280
207
  local cur_img=""
281
208
  cur_img="$(docker inspect --format '{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || true)"
282
209
 
283
- # env:对齐本次 tag(PORT 在 .env 由运维权威,不覆盖;只对齐 TAG/DATA_DIR/CONTAINER_NAME)
284
- local env_flag=""
285
- if [[ -f "$APP_ROOT/.env" ]]; then
286
- _sed_i "$APP_ROOT/.env" \
210
+ # env:对齐本次 tag(PORT 在 .prod.docker.env 由运维权威,不覆盖;只对齐 TAG/DATA_DIR/CONTAINER_NAME)
211
+ if [[ -f "$ENV_FILE" ]]; then
212
+ _sed_i "$ENV_FILE" \
287
213
  -e "s|^TAG=.*|TAG=$tag|" \
288
214
  -e "s|^DATA_DIR=.*|DATA_DIR=$APP_ROOT/data|" \
289
215
  -e "s|^CONTAINER_NAME=.*|CONTAINER_NAME=$CONTAINER_NAME|"
290
- env_flag="--env-file $APP_ROOT/.env"
291
216
  else
292
- err "缺 $APP_ROOT/.env,无法注入密钥"; exit 1
217
+ err "缺 $ENV_FILE,容器 env 无从注入"; exit 1
293
218
  fi
294
219
 
295
- # 仅当 .env PORT 与当前运行容器的 PORT 不同时告警(运维改了 .env,切换后端口将变,nginx 需指向新端口)。
296
- # 端口权威 = .env PORT(app 读 PORT 监听);宿主映射 = 容器监听 = PORT。
220
+ # 仅当 .prod.docker.env PORT 与当前运行容器的 PORT 不同时告警(运维改过 env,切换后端口将变,nginx 需指向新端口)。
221
+ # 端口权威 = .prod.docker.env PORT(config.sh source 时经 read_env_port 已赋 PORT/HOST_PORT/CONTAINER_PORT,
222
+ # run-container.sh 的 -p/-e PORT 均用此值)。
297
223
  local env_port cur_port
298
- env_port="$(grep -E '^PORT=' "$APP_ROOT/.env" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')"
224
+ env_port="$(grep -E '^PORT=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')"
299
225
  [[ -z "$env_port" ]] && env_port="3001"
300
226
  cur_port="$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$CONTAINER_NAME" 2>/dev/null \
301
227
  | sed -nE 's/^PORT=//p' | head -1 || true)"
302
228
  [[ -z "$cur_port" ]] && cur_port="$(image_yaml_port "$cur_img" 2>/dev/null || true)"
303
229
  if [[ -n "$cur_port" && "$cur_port" != "$env_port" ]]; then
304
- say "⚠ .env PORT=$env_port 与当前运行端口 $cur_port 不同(切换后端口将变为 $env_port,nginx 需指向 $env_port)"
230
+ say "⚠ $ENV_FILE 的 PORT=$env_port 与当前运行端口 $cur_port 不同(切换后端口将变为 $env_port,nginx 需指向 $env_port)"
305
231
  fi
306
- HOST_PORT="$env_port"
307
- CONTAINER_PORT="$env_port"
308
- local port_env=( -e PORT="$env_port" )
309
232
 
310
- say "重建容器 $CONTAINER_NAME(tag $tag)..."
311
- docker stop "$CONTAINER_NAME" 2>/dev/null || true
312
- docker rm "$CONTAINER_NAME" 2>/dev/null || true
313
- docker run -d \
314
- --name "$CONTAINER_NAME" \
315
- --restart "$RESTART_POLICY" \
316
- -p "$HOST_PORT:$CONTAINER_PORT" \
317
- $env_flag \
318
- "${port_env[@]}" \
319
- -e NODE_ENV=prod \
320
- -e DB_DIR="$CONTAINER_DATA_DIR" \
321
- -e UPLOAD_STORAGE_PATH="$CONTAINER_DATA_DIR/uploads" \
322
- -v "$APP_ROOT/data:$CONTAINER_DATA_DIR" \
323
- "$IMAGE:$tag"
233
+ # 容器创建命令收敛于 run-container.sh(与 start.sh/install.sh 共用,参数勿在调用方手拷)
234
+ source "$APP_ROOT/ops/docker/run-container.sh"
235
+ run_container "$IMAGE:$tag"
324
236
  ok "版本切换完成 ($CONTAINER_NAME, tag $tag)"
325
237
  echo " 后台服务: $(service_addr) (外网用服务器 IP 替换 localhost)"
326
238
  echo " 状态: cd $APP_ROOT && ./ops/docker/status.sh"