feat: 数字医信一键部署脚本 + 文档整理
scripts/deploy-szyx.sh (新增):
七步幂等脚本,覆盖从预检到验收全流程:
1. Docker / Compose / 磁盘 / 端口预检
2. 写入数字医信专属配置(.env / secrets.env / xuqm.env /
nginx / sdk-json),所有值固化为默认值,支持环境变量覆盖
3. 登录 ACR (crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com)
4. 启动 MySQL + Redis 并轮询就绪
5. 启动 base profile 业务容器并等待 actuator/health
6. 迁移数字医信生产租户(szyx@bjca.org.cn),已迁移时自动跳过
7. 验收:health / PRIVATE 模式 / 两个 appKey SDK config / 注册阻断 / 前端
docs/configuration.md:
补充 Spring Boot SPRING_DATASOURCE_* 覆盖说明(application.yml
硬编码生产 DB URL 的关键陷阱),nginx 服务端口对照表,
docs-site 镜像可选说明。
docs/runbook.md:
新增租户迁移章节,含前提、命令、自动步骤、验证示例。
README.md:
快速参考:migrate-tenant.sh 用法、三条部署注意事项。
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
f6189a5283
当前提交
f0649e9305
20
README.md
20
README.md
@ -64,6 +64,26 @@ MySQL、Redis 支持两种模式:
|
||||
./scripts/disable-service.sh im
|
||||
```
|
||||
|
||||
## 租户迁移
|
||||
|
||||
将公有化平台的存量租户迁移到私有化部署(需源 MySQL 网络可达):
|
||||
|
||||
```bash
|
||||
./scripts/migrate-tenant.sh \
|
||||
--src-host <生产MySQL地址> \
|
||||
--src-user <用户名> \
|
||||
--src-password '<密码>' \
|
||||
--tenant <租户邮箱或用户名>
|
||||
```
|
||||
|
||||
加 `--dry-run` 只打印 SQL 不执行。详见 `docs/runbook.md`。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `tenant-service` 运行在容器内 **9001** 端口,nginx 代理必须指向该端口,不是 8080。
|
||||
- `application.yml` 中数据库 URL 硬编码了生产地址,私有化部署依赖 `docker-compose.yml` 中的 `SPRING_DATASOURCE_*` 覆盖,不能删除 `environment:` 节。
|
||||
- `docs-site` 镜像可选,不存在时 nginx 和 healthcheck 可正常工作(warn 级别)。
|
||||
|
||||
## 接手入口
|
||||
|
||||
- 实时部署进度:`.deploy-state/progress.md`
|
||||
|
||||
@ -38,6 +38,38 @@ TENANT_REGISTER_ENABLED=false
|
||||
TENANT_BOOTSTRAP_ENABLED=true
|
||||
```
|
||||
|
||||
## Spring Boot 数据库 URL 覆盖(重要)
|
||||
|
||||
`tenant-service` 和 `file-service` 的 `application.yml` 中数据库 URL 编译期写死了生产地址。
|
||||
私有化部署**必须**通过 `docker-compose.yml` 的 `environment:` 节覆盖 Spring Boot 配置:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: "jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT:-3306}/${MYSQL_DATABASE:-xuqm_private}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true"
|
||||
SPRING_DATASOURCE_USERNAME: "${MYSQL_USERNAME:-xuqm}"
|
||||
SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}"
|
||||
SPRING_DATA_REDIS_HOST: "${REDIS_HOST}"
|
||||
SPRING_DATA_REDIS_PORT: "${REDIS_PORT:-6379}"
|
||||
SPRING_DATA_REDIS_PASSWORD: "${REDIS_PASSWORD}"
|
||||
SPRING_DATA_REDIS_DATABASE: "${REDIS_DATABASE:-0}"
|
||||
```
|
||||
|
||||
这些 `${VAR}` 在启动时由 Docker Compose 从 `.env` 文件展开,优先级高于 `env_file:` 中的同名变量。
|
||||
当前 `docker-compose.yml` 已包含上述覆盖,不需要手动修改。
|
||||
|
||||
## Nginx 服务端口
|
||||
|
||||
| 服务 | 容器内端口 | 说明 |
|
||||
|------|-----------|------|
|
||||
| tenant-service | **9001** | Spring Boot `server.port=9001`,nginx 必须代理到该端口 |
|
||||
| file-service | **8086** | nginx 代理 `/file/` 路径时使用 |
|
||||
| ops-web | 80 | nginx 代理 `/ops` 路径时使用 |
|
||||
| tenant-web | 80 | nginx 代理 `/` 根路径时使用 |
|
||||
|
||||
## docs-site 镜像
|
||||
|
||||
`docs-site` 镜像在部分 ACR namespace 下不存在。`docker-compose.yml` 已将 nginx 对 `docs-site` 的依赖标记为 `required: false`,镜像缺失时 nginx 仍可正常启动,`healthcheck.sh` 将 docs-site 容器计为可选(WARN 而非 FAIL)。
|
||||
|
||||
## `config/sdk/xuqm-private-sdk.json`
|
||||
|
||||
私有化 SDK 初始化配置,由 `scripts/render-config.sh` 生成。
|
||||
|
||||
@ -47,6 +47,54 @@
|
||||
|
||||
禁用服务不会删除数据,重新启用后继续使用原配置和数据目录。
|
||||
|
||||
## 租户迁移(公有化 → 私有化)
|
||||
|
||||
将公有化平台的存量租户迁移到私有化部署。
|
||||
|
||||
### 前提条件
|
||||
|
||||
- 私有化基础服务已通过 `healthcheck.sh`。
|
||||
- 源 MySQL 可从部署机器网络连通(`mysql -h SRC_HOST ...` 成功)。
|
||||
- 私有化部署为单租户模式:迁移会**清空**当前 bootstrap 租户后写入迁移租户。
|
||||
|
||||
### 执行迁移
|
||||
|
||||
```bash
|
||||
# Dry-run 确认要迁移的数据
|
||||
./scripts/migrate-tenant.sh \
|
||||
--src-host <生产MySQL> \
|
||||
--src-user <用户名> \
|
||||
--src-password '<密码>' \
|
||||
--src-db xuqm_tenant \
|
||||
--tenant <租户邮箱或用户名> \
|
||||
--dry-run
|
||||
|
||||
# 确认无误后正式执行
|
||||
./scripts/migrate-tenant.sh \
|
||||
--src-host <生产MySQL> \
|
||||
--src-user <用户名> \
|
||||
--src-password '<密码>' \
|
||||
--src-db xuqm_tenant \
|
||||
--tenant <租户邮箱或用户名>
|
||||
```
|
||||
|
||||
迁移脚本会自动:
|
||||
1. 从源库导出 `t_tenant`、`t_app`、`t_feature_service`(含厂商配置)。
|
||||
2. 用显式列名 INSERT 规避生产与私有化 MySQL 的列序差异。
|
||||
3. 清空私有化部署的 bootstrap 租户后写入迁移数据。
|
||||
4. 重启 `tenant-service` 清空内存缓存。
|
||||
5. 通过 `/api/sdk/config` 和 `/api/private/deployment/status` 验证结果。
|
||||
|
||||
### 验证
|
||||
|
||||
```bash
|
||||
# SDK config 应返回 200
|
||||
curl "http://DEPLOY_HOST/api/sdk/config?appKey=<app_key>&platform=ANDROID"
|
||||
|
||||
# 部署状态应为 PRIVATE,注册应为 false
|
||||
curl "http://DEPLOY_HOST/api/private/deployment/status"
|
||||
```
|
||||
|
||||
## 接手规则
|
||||
|
||||
任何 agent 开始执行前必须先查看:
|
||||
|
||||
543
scripts/deploy-szyx.sh
可执行文件
543
scripts/deploy-szyx.sh
可执行文件
@ -0,0 +1,543 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-szyx.sh — 数字医信私有化一键部署脚本
|
||||
#
|
||||
# 用途:在目标机器上完成以下全部步骤:
|
||||
# 1. 预检(Docker、Compose、磁盘、端口)
|
||||
# 2. 写入数字医信专属配置(.env / secrets.env / xuqm.env / nginx)
|
||||
# 3. 登录 ACR 镜像仓库
|
||||
# 4. 启动基础设施容器(MySQL、Redis)并等待就绪
|
||||
# 5. 启动业务容器(base profile)并等待健康
|
||||
# 6. 迁移数字医信生产租户数据
|
||||
# 7. 最终验收并输出登录指引
|
||||
#
|
||||
# 幂等性:可重复执行。已运行的容器不会被重建;已迁移的租户不会被二次清空。
|
||||
#
|
||||
# 前提:
|
||||
# - Docker 和 Docker Compose v2 已安装
|
||||
# - 目标机器与生产 MySQL (39.107.53.187) 网络可达
|
||||
# - 在 XuqmGroup-PrivateDeploy 仓库根目录下执行本脚本
|
||||
# 或通过 ssh 执行:sshpass -p '...' ssh xuqm@HOST 'cd /opt/xuqm-private && bash scripts/deploy-szyx.sh'
|
||||
#
|
||||
# 覆盖默认值(可通过环境变量传入):
|
||||
# DEPLOY_HOST 目标机器 IP / 主机名,用于验收 HTTP 检查(默认 127.0.0.1)
|
||||
# REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022)
|
||||
# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 常量 — 数字医信专属配置
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CUSTOMER_NAME="数字医信"
|
||||
CUSTOMER_SHORT="szyx"
|
||||
|
||||
# 镜像仓库
|
||||
REGISTRY="crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com/xuqmgroup"
|
||||
REGISTRY_HOST="crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com"
|
||||
REGISTRY_USER="xuqinmin12"
|
||||
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-xuqinmin1022}"
|
||||
IMAGE_TAG="latest"
|
||||
|
||||
# 部署主机(用于健康检查 HTTP 请求)
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-127.0.0.1}"
|
||||
CONSOLE_BASE="http://${DEPLOY_HOST}"
|
||||
|
||||
# MySQL(managed 模式,由 Docker 容器托管)
|
||||
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-XuqmRoot@2026}"
|
||||
MYSQL_PASSWORD="${MYSQL_PASSWORD:-XuqmMysql@2026}"
|
||||
MYSQL_DATABASE="xuqm_private"
|
||||
MYSQL_USERNAME="xuqm"
|
||||
|
||||
# Redis(managed 模式)
|
||||
REDIS_PASSWORD="${REDIS_PASSWORD:-XuqmRedis@2026}"
|
||||
|
||||
# 数字医信登录密码(不在脚本里重置,使用生产密码原文迁移)
|
||||
TENANT_EMAIL="szyx@bjca.org.cn"
|
||||
TENANT_USERNAME="szyx"
|
||||
|
||||
# 源生产 MySQL
|
||||
SRC_HOST="39.107.53.187"
|
||||
SRC_PORT="3306"
|
||||
SRC_USER="xuqm"
|
||||
SRC_PASSWORD="Xuqm@2026"
|
||||
SRC_DB="xuqm_tenant"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 内部变量
|
||||
# ---------------------------------------------------------------------------
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
STEP=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 工具函数
|
||||
# ---------------------------------------------------------------------------
|
||||
log() { printf '\n\033[1;36m[%d/%d] %s\033[0m\n' "$STEP" "$TOTAL_STEPS" "$*"; }
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[33m⚠\033[0m %s\n' "$*"; }
|
||||
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
step() { STEP=$((STEP+1)); log "$*"; }
|
||||
|
||||
wait_http() {
|
||||
local url="$1" max="${2:-120}" interval=3
|
||||
local waited=0
|
||||
while [ "$waited" -lt "$max" ]; do
|
||||
code="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 4 "$url" 2>/dev/null || echo 000)"
|
||||
[ "$code" = "200" ] || [ "$code" = "204" ] && return 0
|
||||
sleep "$interval"
|
||||
waited=$((waited + interval))
|
||||
printf ' waiting %s ... HTTP %s\n' "$url" "$code"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
container_running() {
|
||||
docker ps --filter "name=$1" --filter "status=running" --format '{{.Names}}' 2>/dev/null | grep -q "$1"
|
||||
}
|
||||
|
||||
TOTAL_STEPS=7
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 0. 确认工作目录
|
||||
# ---------------------------------------------------------------------------
|
||||
[ -f "$ROOT_DIR/docker-compose.yml" ] || \
|
||||
fail "请在 XuqmGroup-PrivateDeploy 仓库根目录执行本脚本(当前: $ROOT_DIR)"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||
printf '\033[1;35m %s 私有化一键部署脚本\033[0m\n' "$CUSTOMER_NAME"
|
||||
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||
printf ' 部署目录: %s\n' "$ROOT_DIR"
|
||||
printf ' 目标主机: %s\n' "$DEPLOY_HOST"
|
||||
printf ' 镜像仓库: %s\n' "$REGISTRY"
|
||||
printf ' 租户: %s (%s)\n\n' "$CUSTOMER_NAME" "$TENANT_EMAIL"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1 — 预检
|
||||
# ---------------------------------------------------------------------------
|
||||
step "预检(Docker / Compose / 磁盘 / 端口)"
|
||||
|
||||
command -v docker >/dev/null 2>&1 || fail "Docker 未安装"
|
||||
docker info >/dev/null 2>&1 || fail "Docker daemon 未运行"
|
||||
ok "Docker $(docker --version | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | head -1)"
|
||||
|
||||
docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 未安装"
|
||||
ok "Docker Compose $(docker compose version --short 2>/dev/null || echo 'v2')"
|
||||
|
||||
command -v mysql >/dev/null 2>&1 || fail "mysql 客户端未安装(迁移步骤需要): apt install -y mysql-client"
|
||||
ok "mysql 客户端已安装"
|
||||
|
||||
DISK_FREE_GB="$(df -BG "$ROOT_DIR" | awk 'NR==2{gsub(/G/,"",$4); print $4}')"
|
||||
[ "${DISK_FREE_GB:-0}" -ge 10 ] || \
|
||||
fail "磁盘可用空间不足(需 ≥10 GB,当前 ${DISK_FREE_GB:-?} GB)"
|
||||
ok "磁盘可用: ${DISK_FREE_GB} GB"
|
||||
|
||||
for port in 80 443; do
|
||||
if ss -tlnp 2>/dev/null | grep -q ":${port} " || \
|
||||
netstat -tlnp 2>/dev/null | grep -q ":${port} "; then
|
||||
warn "端口 ${port} 已被占用(继续执行,如已是本脚本容器则无影响)"
|
||||
else
|
||||
ok "端口 ${port} 空闲"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2 — 写入配置
|
||||
# ---------------------------------------------------------------------------
|
||||
step "写入数字医信专属配置"
|
||||
|
||||
# .env
|
||||
cat > "$ROOT_DIR/.env" <<EOF
|
||||
PRIVATE_VERSION=2026.05.18-private.1
|
||||
REGISTRY=${REGISTRY}
|
||||
REGISTRY_USER=${REGISTRY_USER}
|
||||
REGISTRY_PASSWORD=${REGISTRY_PASSWORD}
|
||||
IMAGE_TAG=${IMAGE_TAG}
|
||||
COMPOSE_PROFILES=base,infra-mysql,infra-redis
|
||||
|
||||
MYSQL_MODE=managed
|
||||
MYSQL_HOST=mysql
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_DATABASE=${MYSQL_DATABASE}
|
||||
MYSQL_USERNAME=${MYSQL_USERNAME}
|
||||
|
||||
REDIS_MODE=managed
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_DATABASE=0
|
||||
|
||||
CONSOLE_DOMAIN=${CONSOLE_BASE}
|
||||
OPS_DOMAIN=${CONSOLE_BASE}
|
||||
DOCS_DOMAIN=${CONSOLE_BASE}
|
||||
FILE_DOMAIN=${CONSOLE_BASE}
|
||||
IM_DOMAIN=
|
||||
UPDATE_DOMAIN=
|
||||
LICENSE_DOMAIN=
|
||||
PUSH_DOMAIN=
|
||||
|
||||
ENABLE_FILE=true
|
||||
ENABLE_IM=false
|
||||
ENABLE_PUSH=false
|
||||
ENABLE_UPDATE=false
|
||||
ENABLE_LICENSE=false
|
||||
|
||||
TENANT_BOOTSTRAP_EMAIL=${TENANT_EMAIL}
|
||||
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||||
EOF
|
||||
ok ".env 已写入"
|
||||
|
||||
# config/secrets.env
|
||||
mkdir -p "$ROOT_DIR/config"
|
||||
cat > "$ROOT_DIR/config/secrets.env" <<EOF
|
||||
MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||
REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
EOF
|
||||
chmod 600 "$ROOT_DIR/config/secrets.env"
|
||||
ok "config/secrets.env 已写入 (chmod 600)"
|
||||
|
||||
# config/xuqm.env — 业务服务容器内配置
|
||||
cat > "$ROOT_DIR/config/xuqm.env" <<EOF
|
||||
DEPLOYMENT_MODE=PRIVATE
|
||||
TENANT_REGISTER_ENABLED=false
|
||||
TENANT_BOOTSTRAP_ENABLED=true
|
||||
|
||||
ENABLE_FILE=true
|
||||
ENABLE_IM=false
|
||||
ENABLE_PUSH=false
|
||||
ENABLE_UPDATE=false
|
||||
ENABLE_LICENSE=false
|
||||
|
||||
MYSQL_HOST=mysql
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_DATABASE=${MYSQL_DATABASE}
|
||||
MYSQL_USERNAME=${MYSQL_USERNAME}
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_DATABASE=0
|
||||
|
||||
CONSOLE_DOMAIN=${CONSOLE_BASE}
|
||||
OPS_DOMAIN=${CONSOLE_BASE}
|
||||
DOCS_DOMAIN=${CONSOLE_BASE}
|
||||
FILE_DOMAIN=${CONSOLE_BASE}
|
||||
IM_DOMAIN=
|
||||
UPDATE_DOMAIN=
|
||||
LICENSE_DOMAIN=
|
||||
PUSH_DOMAIN=
|
||||
|
||||
SDK_FILE_SERVICE_URL=${CONSOLE_BASE}
|
||||
SDK_IM_API_URL=
|
||||
SDK_IM_WS_URL=
|
||||
EOF
|
||||
ok "config/xuqm.env 已写入"
|
||||
|
||||
# config/tenant/bootstrap.env
|
||||
mkdir -p "$ROOT_DIR/config/tenant"
|
||||
cat > "$ROOT_DIR/config/tenant/bootstrap.env" <<EOF
|
||||
TENANT_BOOTSTRAP_EMAIL=${TENANT_EMAIL}
|
||||
TENANT_BOOTSTRAP_USERNAME=${TENANT_USERNAME}
|
||||
TENANT_BOOTSTRAP_PASSWORD=change-me-on-first-login
|
||||
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||||
EOF
|
||||
ok "config/tenant/bootstrap.env 已写入"
|
||||
|
||||
# config/nginx/conf.d/xuqm.conf — 完整路由
|
||||
mkdir -p "$ROOT_DIR/config/nginx/conf.d"
|
||||
cat > "$ROOT_DIR/config/nginx/conf.d/xuqm.conf" <<'NGINX_CONF'
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
location /health {
|
||||
return 200 "ok\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# tenant-service 运行在 9001 端口
|
||||
location /api/ {
|
||||
proxy_pass http://tenant-service:9001/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location /actuator/ {
|
||||
proxy_pass http://tenant-service:9001/actuator/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# file-service 运行在 8086 端口
|
||||
location /file/ {
|
||||
proxy_pass http://file-service:8086/file/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
client_max_body_size 500m;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location /ops {
|
||||
proxy_pass http://ops-web:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://tenant-web:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
NGINX_CONF
|
||||
ok "config/nginx/conf.d/xuqm.conf 已写入"
|
||||
|
||||
# 生成 SDK JSON 和 docs-runtime.json
|
||||
mkdir -p "$ROOT_DIR/config/sdk" "$ROOT_DIR/config/docs" "$ROOT_DIR/.deploy-state"
|
||||
cat > "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" <<EOF
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"configVersion": "2026.05.18-private.1",
|
||||
"deployment": "PRIVATE",
|
||||
"appKey": "ak_private_default",
|
||||
"controlBaseUrl": "${CONSOLE_BASE}",
|
||||
"fileBaseUrl": "${CONSOLE_BASE}",
|
||||
"imApiBaseUrl": "",
|
||||
"imWsUrl": "",
|
||||
"features": {
|
||||
"file": true,
|
||||
"im": false,
|
||||
"push": false,
|
||||
"update": false,
|
||||
"license": false
|
||||
},
|
||||
"connectTimeoutMs": 10000,
|
||||
"readTimeoutMs": 30000,
|
||||
"logLevel": "WARN"
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$ROOT_DIR/config/docs/docs-runtime.json" <<EOF
|
||||
{
|
||||
"deployment": "PRIVATE",
|
||||
"privateVersion": "2026.05.18-private.1",
|
||||
"domains": { "console": "${CONSOLE_BASE}", "file": "${CONSOLE_BASE}" },
|
||||
"features": { "file": true, "im": false, "push": false, "update": false, "license": false }
|
||||
}
|
||||
EOF
|
||||
|
||||
mkdir -p "$ROOT_DIR/data/uploads" "$ROOT_DIR/data/mysql" "$ROOT_DIR/data/redis" \
|
||||
"$ROOT_DIR/data/backups" "$ROOT_DIR/logs"
|
||||
ok "SDK JSON 和目录结构就绪"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 3 — 登录镜像仓库
|
||||
# ---------------------------------------------------------------------------
|
||||
step "登录 ACR 镜像仓库"
|
||||
|
||||
printf '%s' "$REGISTRY_PASSWORD" | \
|
||||
docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin 2>/dev/null \
|
||||
&& ok "登录成功: $REGISTRY_HOST" \
|
||||
|| fail "ACR 登录失败,请检查 REGISTRY_USER / REGISTRY_PASSWORD"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 4 — 启动基础设施(MySQL + Redis)
|
||||
# ---------------------------------------------------------------------------
|
||||
step "启动基础设施容器(MySQL / Redis)"
|
||||
|
||||
docker compose \
|
||||
--env-file "$ROOT_DIR/.env" \
|
||||
-f "$ROOT_DIR/docker-compose.yml" \
|
||||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||||
--profile infra-mysql --profile infra-redis \
|
||||
up -d mysql redis 2>/dev/null || \
|
||||
docker compose \
|
||||
--env-file "$ROOT_DIR/.env" \
|
||||
-f "$ROOT_DIR/docker-compose.yml" \
|
||||
--profile infra-mysql --profile infra-redis \
|
||||
up -d mysql redis
|
||||
|
||||
# 等待 MySQL 就绪(最多 90 秒)
|
||||
printf ' 等待 MySQL 启动'
|
||||
for i in $(seq 1 30); do
|
||||
if docker exec "$(docker ps -qf name=mysql | head -1)" \
|
||||
mysqladmin -u root -p"${MYSQL_ROOT_PASSWORD}" ping --silent 2>/dev/null; then
|
||||
printf '\n'
|
||||
ok "MySQL 就绪"
|
||||
break
|
||||
fi
|
||||
printf '.'
|
||||
sleep 3
|
||||
[ "$i" -eq 30 ] && { printf '\n'; fail "MySQL 启动超时(90s)"; }
|
||||
done
|
||||
|
||||
# 等待 Redis 就绪
|
||||
for i in $(seq 1 10); do
|
||||
if docker exec "$(docker ps -qf name=redis | head -1)" \
|
||||
redis-cli -a "$REDIS_PASSWORD" --no-auth-warning PING 2>/dev/null | grep -q PONG; then
|
||||
ok "Redis 就绪"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
[ "$i" -eq 10 ] && fail "Redis 启动超时(20s)"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 5 — 启动业务容器
|
||||
# ---------------------------------------------------------------------------
|
||||
step "拉取镜像并启动业务容器(base profile)"
|
||||
|
||||
docker compose \
|
||||
--env-file "$ROOT_DIR/.env" \
|
||||
-f "$ROOT_DIR/docker-compose.yml" \
|
||||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||||
--profile base --profile infra-mysql --profile infra-redis \
|
||||
up -d 2>/dev/null || \
|
||||
docker compose \
|
||||
--env-file "$ROOT_DIR/.env" \
|
||||
-f "$ROOT_DIR/docker-compose.yml" \
|
||||
--profile base --profile infra-mysql --profile infra-redis \
|
||||
up -d
|
||||
|
||||
# 等待 tenant-service 健康(最多 120 秒)
|
||||
printf ' 等待 tenant-service 启动'
|
||||
for i in $(seq 1 40); do
|
||||
code="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 4 \
|
||||
"http://${DEPLOY_HOST}/actuator/health" 2>/dev/null || echo 000)"
|
||||
if [ "$code" = "200" ]; then
|
||||
printf '\n'
|
||||
ok "tenant-service 健康 (HTTP 200)"
|
||||
break
|
||||
fi
|
||||
printf '.'
|
||||
sleep 3
|
||||
[ "$i" -eq 40 ] && {
|
||||
printf '\n'
|
||||
warn "tenant-service 未在 120s 内响应,继续执行(可能仍在初始化)"
|
||||
break
|
||||
}
|
||||
done
|
||||
|
||||
# 简要容器状态
|
||||
printf '\n 容器状态:\n'
|
||||
docker compose \
|
||||
--env-file "$ROOT_DIR/.env" \
|
||||
-f "$ROOT_DIR/docker-compose.yml" \
|
||||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||||
ps --format 'table {{.Name}}\t{{.Status}}' 2>/dev/null \
|
||||
|| docker ps --format ' {{.Names}}\t{{.Status}}' | grep -E "xuqm|mysql|redis" || true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 6 — 迁移数字医信租户
|
||||
# ---------------------------------------------------------------------------
|
||||
step "迁移数字医信租户数据(从生产 MySQL 导入)"
|
||||
|
||||
# 检查是否已迁移(目标 DB 中是否已有数字医信的租户记录)
|
||||
MYSQL_CTR="$(docker ps -qf name=mysql | head -1)"
|
||||
EXISTING="$(docker exec "$MYSQL_CTR" \
|
||||
mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||
-N -e "SELECT COUNT(*) FROM t_tenant WHERE email='${TENANT_EMAIL}'" 2>/dev/null || echo 0)"
|
||||
|
||||
if [ "${EXISTING:-0}" -ge 1 ]; then
|
||||
ok "数字医信租户已存在于私有化 DB,跳过迁移"
|
||||
else
|
||||
printf ' 连通性检查生产 MySQL %s ...\n' "$SRC_HOST"
|
||||
MYSQL_PWD="$SRC_PASSWORD" mysql \
|
||||
-h "$SRC_HOST" -P "$SRC_PORT" -u "$SRC_USER" \
|
||||
--connect-timeout=10 "$SRC_DB" \
|
||||
-e "SELECT 1" >/dev/null 2>&1 \
|
||||
|| fail "无法连接生产 MySQL ${SRC_HOST}:${SRC_PORT},请检查网络或凭据"
|
||||
ok "生产 MySQL 连通"
|
||||
|
||||
bash "$ROOT_DIR/scripts/migrate-tenant.sh" \
|
||||
--src-host "$SRC_HOST" \
|
||||
--src-port "$SRC_PORT" \
|
||||
--src-user "$SRC_USER" \
|
||||
--src-password "$SRC_PASSWORD" \
|
||||
--src-db "$SRC_DB" \
|
||||
--tenant "$TENANT_EMAIL"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 7 — 最终验收
|
||||
# ---------------------------------------------------------------------------
|
||||
step "最终验收"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local label="$1" actual="$2" expected="$3"
|
||||
if printf '%s' "$actual" | grep -q "$expected"; then
|
||||
ok "PASS $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
warn "FAIL $label (got: $actual)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# actuator/health
|
||||
HEALTH="$(curl -skL --max-time 5 "http://${DEPLOY_HOST}/actuator/health" 2>/dev/null || true)"
|
||||
check "actuator/health" "$HEALTH" '"status":"UP"'
|
||||
|
||||
# PRIVATE 模式
|
||||
STATUS="$(curl -skL --max-time 5 "http://${DEPLOY_HOST}/api/private/deployment/status" 2>/dev/null || true)"
|
||||
check "deployment mode=PRIVATE" "$STATUS" '"mode":"PRIVATE"'
|
||||
check "tenantRegisterEnabled=false" "$STATUS" '"tenantRegisterEnabled":false'
|
||||
|
||||
# 数字医信两个 App SDK config
|
||||
for APPKEY in ak_c6fce237cae94ef5ab71fda6 ak_1178fd37b8f54cefb7031744; do
|
||||
SDK="$(curl -skL --max-time 5 \
|
||||
"http://${DEPLOY_HOST}/api/sdk/config?appKey=${APPKEY}&platform=ANDROID" 2>/dev/null || true)"
|
||||
check "sdk/config $APPKEY" "$SDK" '"code":200'
|
||||
done
|
||||
|
||||
# 注册阻断
|
||||
REG="$(curl -skL --max-time 5 -o /dev/null -w '%{http_code}' \
|
||||
-X POST "http://${DEPLOY_HOST}/api/auth/register" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"blocked@test.com","username":"blocked","password":"Test@123"}' 2>/dev/null || echo 000)"
|
||||
# 注册应返回 4xx(阻断)或 400(字段校验),不应返回 200
|
||||
if [ "$REG" != "200" ] && [ "$REG" != "000" ]; then
|
||||
ok "PASS 注册阻断 (HTTP $REG)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
warn "FAIL 注册阻断返回 $REG"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
# 前端可访问
|
||||
WEB="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 5 "http://${DEPLOY_HOST}/" 2>/dev/null || echo 000)"
|
||||
check "前端 HTTP" "$WEB" "200"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 结果汇总
|
||||
# ---------------------------------------------------------------------------
|
||||
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||
printf '\033[1;35m 验收结果: %d PASS / %d FAIL\033[0m\n' "$PASS" "$FAIL"
|
||||
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||
|
||||
printf '\n \033[1m访问地址:\033[0m %s\n' "${CONSOLE_BASE}"
|
||||
printf ' \033[1m运营后台:\033[0m %s/ops\n' "${CONSOLE_BASE}"
|
||||
printf ' \033[1m租户账号:\033[0m %s\n' "${TENANT_EMAIL}"
|
||||
printf ' \033[1m租户用户名:\033[0m %s\n' "${TENANT_USERNAME}"
|
||||
printf ' \033[1m密码:\033[0m 同公有化平台密码(未重置)\n'
|
||||
printf '\n \033[1m应用列表:\033[0m\n'
|
||||
printf ' 医网信 ak_c6fce237cae94ef5ab71fda6\n'
|
||||
printf ' 临床知识库 ak_1178fd37b8f54cefb7031744\n'
|
||||
printf '\n \033[1m部署目录:\033[0m %s\n' "$ROOT_DIR"
|
||||
printf ' \033[1m审计日志:\033[0m %s/logs/audit.log\n' "$ROOT_DIR"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
printf '\n\033[33m %d 项验收未通过,请查看容器日志:\033[0m\n' "$FAIL"
|
||||
printf ' docker compose logs --tail 50 tenant-service\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\n\033[1;32m 部署成功!\033[0m\n\n'
|
||||
正在加载...
在新工单中引用
屏蔽一个用户