package-docker-deploy.sh 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env bash
  2. # 打包 Docker 交付物:预构建应用镜像 + compose + 数据库 SQL(不含前后端源码)
  3. # 目标运行环境:甲方 Linux x86_64 云主机(固定 linux/amd64,与发版机 macOS/ARM 无关)
  4. set -euo pipefail
  5. ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  6. cd "$ROOT"
  7. # 交付镜像架构(甲方云主机默认 amd64;勿改为 arm64 除非已确认对方为 ARM 云)
  8. export DOCKER_PLATFORM=linux/amd64
  9. export DOCKER_BUILDKIT=1
  10. VERSION="${1:-$(date +%Y%m%d)}"
  11. OUT_DIR="${ROOT}/dist"
  12. PKG_NAME="zkxw-docker-deploy-${VERSION}"
  13. STAGING="${OUT_DIR}/${PKG_NAME}"
  14. ARCHIVE="${OUT_DIR}/${PKG_NAME}.tar.gz"
  15. IMAGES_TAR="${STAGING}/images/zkxw-app-images.tar"
  16. BACKEND_IMAGE="zkxw-backend:${VERSION}"
  17. WEB_IMAGE="zkxw-web:${VERSION}"
  18. echo "==> [1/4] 构建应用镜像(平台 ${DOCKER_PLATFORM},供 Linux 云主机)"
  19. "${ROOT}/scripts/build-docker-images.sh" "${VERSION}"
  20. echo "==> [2/4] 导出镜像到 tar"
  21. rm -rf "${STAGING}"
  22. mkdir -p "${STAGING}/images" "${STAGING}/deploy/docker" "${STAGING}/docs/sql"
  23. docker save -o "${IMAGES_TAR}" "${BACKEND_IMAGE}" "${WEB_IMAGE}"
  24. echo "==> [3/4] 组装部署文件(无 backend/ web-admin 源码)"
  25. cp "${ROOT}/deploy/docker/docker-compose.yml" "${STAGING}/deploy/docker/"
  26. sed -e "s|^ZKXW_BACKEND_IMAGE=.*|ZKXW_BACKEND_IMAGE=${BACKEND_IMAGE}|" \
  27. -e "s|^ZKXW_WEB_IMAGE=.*|ZKXW_WEB_IMAGE=${WEB_IMAGE}|" \
  28. "${ROOT}/deploy/docker/.env.example" > "${STAGING}/deploy/docker/.env.example"
  29. # 从本地 deploy/docker/.env 注入运行时密钥与微信配置到交付包 .env.example
  30. # (不注入 MYSQL/SECRET_KEY/MINIO 密码,避免覆盖甲方现网库口令)
  31. LOCAL_ENV="${ROOT}/deploy/docker/.env"
  32. STAGING_ENV="${STAGING}/deploy/docker/.env.example"
  33. if [[ -f "${LOCAL_ENV}" ]]; then
  34. if python3 - "${LOCAL_ENV}" "${STAGING_ENV}" <<'PY'
  35. import sys
  36. from pathlib import Path
  37. local_env, staging_env = map(Path, sys.argv[1:3])
  38. # 打包时注入:登录 RSA + 微信/小程序(业务通知依赖);不注入库口令
  39. INJECT_KEYS = (
  40. "LOGIN_RSA_PRIVATE_KEY_PEM",
  41. "WXPAY_MINI_APPID",
  42. "WXPAY_SECRET",
  43. "WX_MP_NOTIFY_ENABLED",
  44. "WX_MP_APPID",
  45. "WX_MP_SECRET",
  46. "WX_MP_TEMPLATE_APPROVAL",
  47. "WX_MP_TEMPLATE_CC",
  48. "WX_MP_TEMPLATE_APPLICANT_PROGRESS",
  49. "WX_MP_TPL_FIELD_TITLE",
  50. "WX_MP_TPL_FIELD_APPLICANT",
  51. "WX_MP_TPL_FIELD_NODE",
  52. "WX_MP_TPL_FIELD_TIME",
  53. "WX_MP_TPL_FIELD_PROCESS",
  54. "WX_MP_TPL_CC_FIELD_TITLE",
  55. "WX_MP_TPL_CC_FIELD_APPLICANT",
  56. "WX_MP_TPL_CC_FIELD_TIME",
  57. "WX_MP_TPL_CC_FIELD_PROCESS",
  58. "WX_MP_TPL_PROGRESS_FIELD_BUSINESS",
  59. "WX_MP_TPL_PROGRESS_FIELD_PROCESS",
  60. "WX_MP_TPL_PROGRESS_FIELD_APPROVER",
  61. "WX_MP_TPL_PROGRESS_FIELD_RESULT",
  62. "WX_MP_TPL_PROGRESS_FIELD_TIME",
  63. )
  64. def parse_env(text: str) -> dict[str, str]:
  65. out: dict[str, str] = {}
  66. for line in text.splitlines():
  67. s = line.strip()
  68. if not s or s.startswith("#") or "=" not in line:
  69. continue
  70. # 保留行首 key=(允许值内含 =)
  71. if line.lstrip().startswith("#"):
  72. continue
  73. key, _, val = line.partition("=")
  74. key = key.strip()
  75. if key:
  76. out[key] = line # 整行,保留原样(含引号/换行转义)
  77. return out
  78. local_map = parse_env(local_env.read_text())
  79. injected: list[str] = []
  80. missing: list[str] = []
  81. for k in INJECT_KEYS:
  82. if k in local_map and local_map[k].partition("=")[2].strip():
  83. injected.append(k)
  84. else:
  85. missing.append(k)
  86. lines_out: list[str] = []
  87. seen: set[str] = set()
  88. for line in staging_env.read_text().splitlines():
  89. raw = line.lstrip()
  90. if raw.startswith("#"):
  91. # 更新注释:标明微信/RSA 可能已由打包注入
  92. if "登录 RSA" in line:
  93. lines_out.append(
  94. "# 登录 RSA(打包时若本地 deploy/docker/.env 有值则已注入)"
  95. )
  96. continue
  97. if "微信服务号" in line or "微信支付 / 小程序" in line:
  98. lines_out.append(line)
  99. if "微信服务号" in line:
  100. lines_out.append(
  101. "# 下列 WX_MP_* / 模板字段:打包时从本地 deploy/docker/.env 注入(有则覆盖)"
  102. )
  103. continue
  104. lines_out.append(line)
  105. continue
  106. key = line.partition("=")[0].strip() if "=" in line else ""
  107. # 去掉注释形态的同名键,稍后写注入行
  108. if key.startswith("#"):
  109. maybe = key.lstrip("#").strip()
  110. if maybe in INJECT_KEYS:
  111. continue
  112. lines_out.append(line)
  113. continue
  114. if key in INJECT_KEYS:
  115. if key in local_map and local_map[key].partition("=")[2].strip():
  116. lines_out.append(local_map[key])
  117. seen.add(key)
  118. else:
  119. lines_out.append(line)
  120. seen.add(key)
  121. continue
  122. lines_out.append(line)
  123. # 本地有、example 里没有的键:追加到文件末尾
  124. extra = [local_map[k] for k in INJECT_KEYS if k not in seen and k in local_map and local_map[k].partition("=")[2].strip()]
  125. if extra:
  126. lines_out.append("")
  127. lines_out.append("# ---- 打包自 deploy/docker/.env 追加 ----")
  128. lines_out.extend(extra)
  129. staging_env.write_text("\n".join(lines_out) + "\n")
  130. # 同时生成可直接 cp 的 .env(与 .env.example 相同;甲方改库密码即可)
  131. staging_env.with_name(".env").write_text(staging_env.read_text())
  132. print("injected:" + ",".join(injected) if injected else "injected:(none)")
  133. if missing:
  134. print("missing_or_empty:" + ",".join(missing))
  135. sys.exit(0 if injected else 2)
  136. PY
  137. then
  138. echo " 已从 deploy/docker/.env 注入微信/小程序/RSA 等到交付包 .env.example 与 .env"
  139. elif [[ $? -eq 2 ]]; then
  140. echo " 警告: deploy/docker/.env 中微信/RSA 等注入项为空,交付包仍用 .env.example 占位" >&2
  141. cp "${STAGING}/deploy/docker/.env.example" "${STAGING}/deploy/docker/.env"
  142. else
  143. echo " 错误: 注入交付包环境变量失败" >&2
  144. exit 1
  145. fi
  146. else
  147. echo " 警告: 无 deploy/docker/.env,跳过注入;仅提供 .env.example"
  148. cp "${STAGING}/deploy/docker/.env.example" "${STAGING}/deploy/docker/.env"
  149. fi
  150. cp "${ROOT}/deploy/docker/README.md" "${STAGING}/deploy/docker/"
  151. cp -R "${ROOT}/deploy/docker/mysql" "${STAGING}/deploy/docker/"
  152. cp "${ROOT}/docs/sql/zkxw.sql" "${STAGING}/docs/sql/zkxw.sql"
  153. cp "${ROOT}/deploy/docker/README.md" "${STAGING}/部署说明.md"
  154. cp "${ROOT}/deploy/docker/部署指导手册.html" "${STAGING}/部署指导手册.html"
  155. cp "${ROOT}/deploy/docker/部署指导手册.html" "${STAGING}/deploy/docker/部署指导手册.html"
  156. cat > "${STAGING}/VERSION.txt" <<EOF
  157. package=${PKG_NAME}
  158. version=${VERSION}
  159. platform=${DOCKER_PLATFORM}
  160. backend_image=${BACKEND_IMAGE}
  161. web_image=${WEB_IMAGE}
  162. web_includes=web-admin+mobile-oa-h5(/oa-h5/)
  163. env_note=微信/小程序/RSA 已从发版机 deploy/docker/.env 注入(若有);库口令仍须按甲方现网填写
  164. built_at=$(date -Iseconds)
  165. git_commit=$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo unknown)
  166. deploy_dir=deploy/docker
  167. EOF
  168. echo "==> [4/4] 生成压缩包"
  169. mkdir -p "${OUT_DIR}"
  170. tar -czf "${ARCHIVE}" -C "${OUT_DIR}" "${PKG_NAME}"
  171. echo ""
  172. echo "完成: ${ARCHIVE}"
  173. echo "应用镜像: $(du -h "${IMAGES_TAR}" | cut -f1)"
  174. echo "总包大小: $(du -h "${ARCHIVE}" | cut -f1)"
  175. echo ""
  176. echo "包内不含前后端源码,包含:"
  177. echo " - images/zkxw-app-images.tar(zkxw-backend + zkxw-web,web 含 /oa-h5/)"
  178. echo " - deploy/docker/ (compose + .env.example + .env + mysql init)"
  179. echo " - docs/sql/zkxw.sql"
  180. echo " - 部署指导手册.html(客户安装手册,浏览器打开)"
  181. echo ""
  182. echo "甲方服务器执行:"
  183. echo " tar -xzf ${PKG_NAME}.tar.gz"
  184. echo " cd ${PKG_NAME}"
  185. echo " docker load -i images/zkxw-app-images.tar"
  186. echo " cd deploy/docker"
  187. echo " # 新装: cp .env.example .env 后改 MYSQL/SECRET/MINIO 密码"
  188. echo " # 升级: 保留现网库密码;可用包内已注入的微信配置,或合并 .env.example 中 WX_*/WXPAY_* 段"
  189. echo " docker compose up -d"