#!/usr/bin/env bash # 打包 Docker 交付物:预构建应用镜像 + compose + 数据库 SQL(不含前后端源码) # 目标运行环境:甲方 Linux x86_64 云主机(固定 linux/amd64,与发版机 macOS/ARM 无关) set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" # 交付镜像架构(甲方云主机默认 amd64;勿改为 arm64 除非已确认对方为 ARM 云) export DOCKER_PLATFORM=linux/amd64 export DOCKER_BUILDKIT=1 VERSION="${1:-$(date +%Y%m%d)}" OUT_DIR="${ROOT}/dist" PKG_NAME="zkxw-docker-deploy-${VERSION}" STAGING="${OUT_DIR}/${PKG_NAME}" ARCHIVE="${OUT_DIR}/${PKG_NAME}.tar.gz" IMAGES_TAR="${STAGING}/images/zkxw-app-images.tar" BACKEND_IMAGE="zkxw-backend:${VERSION}" WEB_IMAGE="zkxw-web:${VERSION}" echo "==> [1/4] 构建应用镜像(平台 ${DOCKER_PLATFORM},供 Linux 云主机)" "${ROOT}/scripts/build-docker-images.sh" "${VERSION}" echo "==> [2/4] 导出镜像到 tar" rm -rf "${STAGING}" mkdir -p "${STAGING}/images" "${STAGING}/deploy/docker" "${STAGING}/docs/sql" docker save -o "${IMAGES_TAR}" "${BACKEND_IMAGE}" "${WEB_IMAGE}" echo "==> [3/4] 组装部署文件(无 backend/ web-admin 源码)" cp "${ROOT}/deploy/docker/docker-compose.yml" "${STAGING}/deploy/docker/" sed -e "s|^ZKXW_BACKEND_IMAGE=.*|ZKXW_BACKEND_IMAGE=${BACKEND_IMAGE}|" \ -e "s|^ZKXW_WEB_IMAGE=.*|ZKXW_WEB_IMAGE=${WEB_IMAGE}|" \ "${ROOT}/deploy/docker/.env.example" > "${STAGING}/deploy/docker/.env.example" # 从本地 deploy/docker/.env 注入运行时密钥与微信配置到交付包 .env.example # (不注入 MYSQL/SECRET_KEY/MINIO 密码,避免覆盖甲方现网库口令) LOCAL_ENV="${ROOT}/deploy/docker/.env" STAGING_ENV="${STAGING}/deploy/docker/.env.example" if [[ -f "${LOCAL_ENV}" ]]; then if python3 - "${LOCAL_ENV}" "${STAGING_ENV}" <<'PY' import sys from pathlib import Path local_env, staging_env = map(Path, sys.argv[1:3]) # 打包时注入:登录 RSA + 微信/小程序(业务通知依赖);不注入库口令 INJECT_KEYS = ( "LOGIN_RSA_PRIVATE_KEY_PEM", "WXPAY_MINI_APPID", "WXPAY_SECRET", "WX_MP_NOTIFY_ENABLED", "WX_MP_APPID", "WX_MP_SECRET", "WX_MP_TEMPLATE_APPROVAL", "WX_MP_TEMPLATE_CC", "WX_MP_TEMPLATE_APPLICANT_PROGRESS", "WX_MP_TPL_FIELD_TITLE", "WX_MP_TPL_FIELD_APPLICANT", "WX_MP_TPL_FIELD_NODE", "WX_MP_TPL_FIELD_TIME", "WX_MP_TPL_FIELD_PROCESS", "WX_MP_TPL_CC_FIELD_TITLE", "WX_MP_TPL_CC_FIELD_APPLICANT", "WX_MP_TPL_CC_FIELD_TIME", "WX_MP_TPL_CC_FIELD_PROCESS", "WX_MP_TPL_PROGRESS_FIELD_BUSINESS", "WX_MP_TPL_PROGRESS_FIELD_PROCESS", "WX_MP_TPL_PROGRESS_FIELD_APPROVER", "WX_MP_TPL_PROGRESS_FIELD_RESULT", "WX_MP_TPL_PROGRESS_FIELD_TIME", ) def parse_env(text: str) -> dict[str, str]: out: dict[str, str] = {} for line in text.splitlines(): s = line.strip() if not s or s.startswith("#") or "=" not in line: continue # 保留行首 key=(允许值内含 =) if line.lstrip().startswith("#"): continue key, _, val = line.partition("=") key = key.strip() if key: out[key] = line # 整行,保留原样(含引号/换行转义) return out local_map = parse_env(local_env.read_text()) injected: list[str] = [] missing: list[str] = [] for k in INJECT_KEYS: if k in local_map and local_map[k].partition("=")[2].strip(): injected.append(k) else: missing.append(k) lines_out: list[str] = [] seen: set[str] = set() for line in staging_env.read_text().splitlines(): raw = line.lstrip() if raw.startswith("#"): # 更新注释:标明微信/RSA 可能已由打包注入 if "登录 RSA" in line: lines_out.append( "# 登录 RSA(打包时若本地 deploy/docker/.env 有值则已注入)" ) continue if "微信服务号" in line or "微信支付 / 小程序" in line: lines_out.append(line) if "微信服务号" in line: lines_out.append( "# 下列 WX_MP_* / 模板字段:打包时从本地 deploy/docker/.env 注入(有则覆盖)" ) continue lines_out.append(line) continue key = line.partition("=")[0].strip() if "=" in line else "" # 去掉注释形态的同名键,稍后写注入行 if key.startswith("#"): maybe = key.lstrip("#").strip() if maybe in INJECT_KEYS: continue lines_out.append(line) continue if key in INJECT_KEYS: if key in local_map and local_map[key].partition("=")[2].strip(): lines_out.append(local_map[key]) seen.add(key) else: lines_out.append(line) seen.add(key) continue lines_out.append(line) # 本地有、example 里没有的键:追加到文件末尾 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()] if extra: lines_out.append("") lines_out.append("# ---- 打包自 deploy/docker/.env 追加 ----") lines_out.extend(extra) staging_env.write_text("\n".join(lines_out) + "\n") # 同时生成可直接 cp 的 .env(与 .env.example 相同;甲方改库密码即可) staging_env.with_name(".env").write_text(staging_env.read_text()) print("injected:" + ",".join(injected) if injected else "injected:(none)") if missing: print("missing_or_empty:" + ",".join(missing)) sys.exit(0 if injected else 2) PY then echo " 已从 deploy/docker/.env 注入微信/小程序/RSA 等到交付包 .env.example 与 .env" elif [[ $? -eq 2 ]]; then echo " 警告: deploy/docker/.env 中微信/RSA 等注入项为空,交付包仍用 .env.example 占位" >&2 cp "${STAGING}/deploy/docker/.env.example" "${STAGING}/deploy/docker/.env" else echo " 错误: 注入交付包环境变量失败" >&2 exit 1 fi else echo " 警告: 无 deploy/docker/.env,跳过注入;仅提供 .env.example" cp "${STAGING}/deploy/docker/.env.example" "${STAGING}/deploy/docker/.env" fi cp "${ROOT}/deploy/docker/README.md" "${STAGING}/deploy/docker/" cp -R "${ROOT}/deploy/docker/mysql" "${STAGING}/deploy/docker/" cp "${ROOT}/docs/sql/zkxw.sql" "${STAGING}/docs/sql/zkxw.sql" cp "${ROOT}/deploy/docker/README.md" "${STAGING}/部署说明.md" cp "${ROOT}/deploy/docker/部署指导手册.html" "${STAGING}/部署指导手册.html" cp "${ROOT}/deploy/docker/部署指导手册.html" "${STAGING}/deploy/docker/部署指导手册.html" cat > "${STAGING}/VERSION.txt" </dev/null || echo unknown) deploy_dir=deploy/docker EOF echo "==> [4/4] 生成压缩包" mkdir -p "${OUT_DIR}" tar -czf "${ARCHIVE}" -C "${OUT_DIR}" "${PKG_NAME}" echo "" echo "完成: ${ARCHIVE}" echo "应用镜像: $(du -h "${IMAGES_TAR}" | cut -f1)" echo "总包大小: $(du -h "${ARCHIVE}" | cut -f1)" echo "" echo "包内不含前后端源码,包含:" echo " - images/zkxw-app-images.tar(zkxw-backend + zkxw-web,web 含 /oa-h5/)" echo " - deploy/docker/ (compose + .env.example + .env + mysql init)" echo " - docs/sql/zkxw.sql" echo " - 部署指导手册.html(客户安装手册,浏览器打开)" echo "" echo "甲方服务器执行:" echo " tar -xzf ${PKG_NAME}.tar.gz" echo " cd ${PKG_NAME}" echo " docker load -i images/zkxw-app-images.tar" echo " cd deploy/docker" echo " # 新装: cp .env.example .env 后改 MYSQL/SECRET/MINIO 密码" echo " # 升级: 保留现网库密码;可用包内已注入的微信配置,或合并 .env.example 中 WX_*/WXPAY_* 段" echo " docker compose up -d"