feat: 一键安装向导 + 交互式租户初始化
- 新增 install.sh:curl 一键下载依赖安装 + 自动解压部署包 + 启动部署向导 - deploy-szyx.sh:移除硬编码租户常量,改为交互式选择(新建/迁移) - 新建租户:收集邮箱/用户名/密码,bcrypt 写入 bootstrap.env - 迁移租户:提示输入生产 MySQL 配置,bcrypt 验证主账号后执行迁移 - 已有数据时迁移前显示红色警告要求 yes 确认 - 移除 docs-site 独立容器(文档已内置于 tenant-web/docs/) - nginx 和 docker-compose 同步清理 docs-site 残留配置 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
f0649e9305
当前提交
43a423b85c
@ -1,44 +1,145 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# XuqmGroup 私有化部署 — Nginx 路由配置
|
||||||
|
#
|
||||||
|
# 架构说明:
|
||||||
|
# 所有外部请求统一进入 nginx(80/443),由 nginx 分发到各后端容器
|
||||||
|
# 容器间通过 Docker 内部网络通信,无需暴露端口到宿主机
|
||||||
|
#
|
||||||
|
# 服务端口映射:
|
||||||
|
# tenant-service 9001 /api/*(核心 API)、/actuator/*
|
||||||
|
# file-service 8086 /file/*(文件上传下载)
|
||||||
|
# im-service 8082 /api/im/*(IM HTTP)、/ws/im/*(WebSocket)
|
||||||
|
# update-service 8084 /api/v1/updates/*、/api/v1/rn/*
|
||||||
|
# push-service 8083 厂商回调(内部端口,不直接暴露给前端)
|
||||||
|
# license-service 8085 内部调用
|
||||||
|
# ops-web 80 /ops/*(运营后台)
|
||||||
|
# tenant-web 80 /*(控制台,兜底路由)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
|
# 强制 UTF-8 编码,防止中文乱码
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
# 最大上传文件大小(文件服务单独设置 500m)
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
|
|
||||||
|
# ----------- 健康检查 -----------
|
||||||
|
# nginx 自身探活,用于负载均衡器和 healthcheck.sh
|
||||||
location /health {
|
location /health {
|
||||||
return 200 "ok\n";
|
return 200 "ok\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
# tenant-service runs on port 9001
|
# ----------- 版本管理服务(update-service:8084)-----------
|
||||||
|
# 包含:APP 版本列表、RN 热更新包、应用市场发布状态
|
||||||
|
location /api/v1/updates/ {
|
||||||
|
proxy_pass http://update-service:8084/api/v1/updates/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# RN 热更新包下载和列表
|
||||||
|
location /api/v1/rn/ {
|
||||||
|
proxy_pass http://update-service:8084/api/v1/rn/;
|
||||||
|
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 120s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- IM 服务(im-service:8082)-----------
|
||||||
|
# IM HTTP API:消息发送、会话管理、平台事件
|
||||||
|
location /api/im/ {
|
||||||
|
proxy_pass http://im-service:8082/api/im/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# IM WebSocket 长连接(客户端消息收发)
|
||||||
|
location /ws/im/ {
|
||||||
|
proxy_pass http://im-service:8082/ws/im/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 3600s; # WebSocket 保持 1 小时
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- License 服务(license-service:8085)-----------
|
||||||
|
# 注意:必须在通用 /api/ 之前声明
|
||||||
|
location /api/license/ {
|
||||||
|
proxy_pass http://license-service:8085/api/license/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- 核心 API(tenant-service:9001)-----------
|
||||||
|
# 注意:tenant-service 运行在 9001 端口(不是 8080)
|
||||||
|
# 包含:认证、租户管理、App 管理、SDK 配置、私有化部署状态
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://tenant-service:9001/api/;
|
proxy_pass http://tenant-service:9001/api/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Spring Boot Actuator 健康检查(内部监控用)
|
||||||
location /actuator/ {
|
location /actuator/ {
|
||||||
proxy_pass http://tenant-service:9001/actuator/;
|
proxy_pass http://tenant-service:9001/actuator/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
# file-service runs on port 8086
|
# ----------- 文件服务(file-service:8086)-----------
|
||||||
|
# 文件上传下载,支持大文件(最大 500MB)
|
||||||
location /file/ {
|
location /file/ {
|
||||||
proxy_pass http://file-service:8086/file/;
|
proxy_pass http://file-service:8086/file/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
client_max_body_size 500m;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ----------- 文档站(tenant-web 内置,VitePress base=/docs/)-----------
|
||||||
|
location /docs/ {
|
||||||
|
proxy_pass http://tenant-web:80/docs/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- 运营后台(ops-web:80)-----------
|
||||||
|
# 管理员登录入口:http://<部署地址>/ops
|
||||||
location /ops {
|
location /ops {
|
||||||
proxy_pass http://ops-web:80;
|
proxy_pass http://ops-web:80;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ----------- 控制台前端(tenant-web:80)-----------
|
||||||
|
# 租户登录界面,兜底路由,必须放最后
|
||||||
|
# sub_filter 替换 JS bundle 中硬编码的生产域名,私有化部署不再出现 xuqinmin.com
|
||||||
|
# Accept-Encoding "" 禁用上游压缩,保证 sub_filter 能处理 JS 文本内容
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://tenant-web:80;
|
proxy_pass http://tenant-web:80;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Accept-Encoding "";
|
||||||
|
sub_filter 'wss://im.dev.xuqinmin.com/ws/im' 'ws://$host/ws/im';
|
||||||
|
sub_filter 'https://dev.xuqinmin.com' 'http://$host';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types text/javascript application/javascript; # text/html 是 nginx 默认,无需重复声明
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,3 +31,7 @@ SDK_FILE_SERVICE_URL=https://file.customer.com
|
|||||||
SDK_IM_API_URL=https://im.customer.com
|
SDK_IM_API_URL=https://im.customer.com
|
||||||
SDK_IM_WS_URL=wss://im.customer.com/ws/im
|
SDK_IM_WS_URL=wss://im.customer.com/ws/im
|
||||||
|
|
||||||
|
# 系统 IM 通信应用 key(私有化服务间消息通知使用此 app_key 连接 IM 服务)
|
||||||
|
# 由 deploy 脚本或 migrate-tenant.sh 自动写入,此处为默认值
|
||||||
|
SYSTEM_APP_KEY=ak_409e217e4aa14254ad73ad3c
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,61 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# XuqmGroup 私有化部署 — 基础设施服务(MySQL / Redis)
|
||||||
|
#
|
||||||
|
# 使用场景:MYSQL_MODE=managed 或 REDIS_MODE=managed 时,
|
||||||
|
# 由 Docker Compose 启动容器化数据库,无需客户自备。
|
||||||
|
#
|
||||||
|
# 生产环境建议使用客户自备的 external 模式,由 DBA 管理数据库。
|
||||||
|
# 开发/体验环境可使用 managed 模式快速启动。
|
||||||
|
#
|
||||||
|
# 数据目录:
|
||||||
|
# MySQL 数据 → ./data/mysql (重启后保留)
|
||||||
|
# Redis 数据 → ./data/redis (AOF 持久化,重启后保留)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MySQL 8.4(托管模式,profile: infra-mysql)
|
||||||
|
# 仅在 MYSQL_MODE=managed 时启用
|
||||||
|
# 首次启动自动创建数据库和业务账号
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.4
|
image: mysql:8.4
|
||||||
profiles: ["infra-mysql"]
|
profiles: ["infra-mysql"]
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} # root 密码(见 config/secrets.env)
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
MYSQL_DATABASE: ${MYSQL_DATABASE} # 自动创建的数据库名
|
||||||
MYSQL_USER: ${MYSQL_USERNAME}
|
MYSQL_USER: ${MYSQL_USERNAME} # 业务账号(自动创建)
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD} # 业务账号密码
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai # 时区,与 Spring Boot 保持一致
|
||||||
command:
|
command:
|
||||||
- --character-set-server=utf8mb4
|
- --character-set-server=utf8mb4 # 服务器默认字符集,支持中文和 emoji
|
||||||
- --collation-server=utf8mb4_unicode_ci
|
- --collation-server=utf8mb4_unicode_ci # 排序规则
|
||||||
- --default-time-zone=+08:00
|
- --default-time-zone=+08:00 # 数据库时区
|
||||||
ports:
|
ports:
|
||||||
|
# 暴露到宿主机,方便外部工具(如 Navicat)连接
|
||||||
|
# 生产环境可注释此行,仅允许容器内部访问
|
||||||
- "${MYSQL_PORT:-3306}:3306"
|
- "${MYSQL_PORT:-3306}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/mysql:/var/lib/mysql
|
- ./data/mysql:/var/lib/mysql # 数据持久化,重启后保留
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Redis 7.4(托管模式,profile: infra-redis)
|
||||||
|
# 仅在 REDIS_MODE=managed 时启用
|
||||||
|
# 启用 AOF 持久化,重启后数据不丢失
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
redis:
|
redis:
|
||||||
image: redis:7.4-alpine
|
image: redis:7.4-alpine
|
||||||
profiles: ["infra-redis"]
|
profiles: ["infra-redis"]
|
||||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
|
command:
|
||||||
|
- redis-server
|
||||||
|
- --appendonly yes # 开启 AOF 持久化
|
||||||
|
- --requirepass ${REDIS_PASSWORD} # 密码(见 config/secrets.env)
|
||||||
ports:
|
ports:
|
||||||
|
# 暴露到宿主机,方便调试
|
||||||
|
# 生产环境可注释此行
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
- "${REDIS_PORT:-6379}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/redis:/data
|
- ./data/redis:/data # AOF 文件持久化目录
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,29 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# XuqmGroup 私有化部署 — 业务服务编排
|
||||||
|
#
|
||||||
|
# 说明:
|
||||||
|
# - 基础设施(MySQL / Redis)在 docker-compose.infra.yml 中定义
|
||||||
|
# - 使用 --profile 控制启动哪些服务(配合 .env 中的 COMPOSE_PROFILES)
|
||||||
|
# - 所有服务密码通过 config/secrets.env 注入,不写在本文件
|
||||||
|
# - Spring Boot 数据库 URL 通过 environment: 覆盖,优先级高于 application.yml
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 核心 API 服务(必须)
|
||||||
|
# 端口:9001(内部),nginx 代理 /api/* 和 /actuator/*
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
tenant-service:
|
tenant-service:
|
||||||
image: ${REGISTRY}/tenant-service:${IMAGE_TAG}
|
image: ${REGISTRY}/tenant-service:${IMAGE_TAG}
|
||||||
profiles: ["base"]
|
profiles: ["base"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./config/xuqm.env
|
- ./config/xuqm.env # 业务配置:运行模式、域名、功能开关
|
||||||
- ./config/secrets.env
|
- ./config/secrets.env # 敏感配置:密码、Token
|
||||||
- ./config/tenant/bootstrap.env
|
- ./config/tenant/bootstrap.env # 初始租户配置
|
||||||
environment:
|
environment:
|
||||||
# Override application.yml hardcoded URLs; these vars take precedence over env_file
|
# 覆盖 application.yml 中硬编码的生产地址,私有化部署必须保留此块
|
||||||
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_URL: "jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT:-3306}/${MYSQL_DATABASE:-xuqm_private}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true"
|
||||||
SPRING_DATASOURCE_USERNAME: "${MYSQL_USERNAME:-xuqm}"
|
SPRING_DATASOURCE_USERNAME: "${MYSQL_USERNAME:-xuqm}"
|
||||||
SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}"
|
SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}"
|
||||||
SPRING_DATA_REDIS_HOST: "${REDIS_HOST}"
|
SPRING_DATA_REDIS_HOST: "${REDIS_HOST}"
|
||||||
@ -17,6 +32,11 @@ services:
|
|||||||
SPRING_DATA_REDIS_DATABASE: "${REDIS_DATABASE:-0}"
|
SPRING_DATA_REDIS_DATABASE: "${REDIS_DATABASE:-0}"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 文件服务(必须)
|
||||||
|
# 端口:8086(内部),nginx 代理 /file/*
|
||||||
|
# 文件数据持久化到 ./data/uploads
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
file-service:
|
file-service:
|
||||||
image: ${REGISTRY}/file-service:${IMAGE_TAG}
|
image: ${REGISTRY}/file-service:${IMAGE_TAG}
|
||||||
profiles: ["base"]
|
profiles: ["base"]
|
||||||
@ -24,7 +44,7 @@ services:
|
|||||||
- ./config/xuqm.env
|
- ./config/xuqm.env
|
||||||
- ./config/secrets.env
|
- ./config/secrets.env
|
||||||
environment:
|
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_URL: "jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT:-3306}/${MYSQL_DATABASE:-xuqm_private}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true"
|
||||||
SPRING_DATASOURCE_USERNAME: "${MYSQL_USERNAME:-xuqm}"
|
SPRING_DATASOURCE_USERNAME: "${MYSQL_USERNAME:-xuqm}"
|
||||||
SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}"
|
SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}"
|
||||||
SPRING_DATA_REDIS_HOST: "${REDIS_HOST}"
|
SPRING_DATA_REDIS_HOST: "${REDIS_HOST}"
|
||||||
@ -32,73 +52,106 @@ services:
|
|||||||
SPRING_DATA_REDIS_PASSWORD: "${REDIS_PASSWORD}"
|
SPRING_DATA_REDIS_PASSWORD: "${REDIS_PASSWORD}"
|
||||||
SPRING_DATA_REDIS_DATABASE: "${REDIS_DATABASE:-0}"
|
SPRING_DATA_REDIS_DATABASE: "${REDIS_DATABASE:-0}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/uploads:/data/uploads
|
- ./data/uploads:/data/uploads # 上传文件持久化目录
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 控制台前端(必须)
|
||||||
|
# 租户登录、App 管理、功能配置界面
|
||||||
|
# nginx 代理 / 根路径
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
tenant-web:
|
tenant-web:
|
||||||
image: ${REGISTRY}/tenant-web:${IMAGE_TAG}
|
image: ${REGISTRY}/tenant-web:${IMAGE_TAG}
|
||||||
profiles: ["base"]
|
profiles: ["base"]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 运营后台前端(必须)
|
||||||
|
# 管理员登录界面,nginx 代理 /ops
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
ops-web:
|
ops-web:
|
||||||
image: ${REGISTRY}/ops-web:${IMAGE_TAG}
|
image: ${REGISTRY}/ops-web:${IMAGE_TAG}
|
||||||
profiles: ["base"]
|
profiles: ["base"]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
docs-site:
|
# ---------------------------------------------------------------------------
|
||||||
image: ${REGISTRY}/docs-site:${IMAGE_TAG}
|
# Nginx 反向代理(必须)
|
||||||
profiles: ["base"]
|
# 统一入口:端口 80(HTTP)和 443(HTTPS)
|
||||||
volumes:
|
# 路由所有请求到各后端容器
|
||||||
- ./config/docs/docs-runtime.json:/app/config/docs-runtime.json:ro
|
# ---------------------------------------------------------------------------
|
||||||
- ./config/sdk/xuqm-private-sdk.json:/app/config/xuqm-private-sdk.json:ro
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.27-alpine
|
image: nginx:1.27-alpine
|
||||||
profiles: ["base"]
|
profiles: ["base"]
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80" # HTTP
|
||||||
- "443:443"
|
- "443:443" # HTTPS(需要配置证书,见 docs/runbook.md)
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
- ./config/nginx/conf.d:/etc/nginx/conf.d:ro
|
- ./config/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- tenant-service
|
tenant-service:
|
||||||
- tenant-web
|
condition: service_started
|
||||||
- ops-web
|
tenant-web:
|
||||||
docs-site:
|
condition: service_started
|
||||||
|
ops-web:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
required: false
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# IM 服务(可选,profile: im)
|
||||||
|
# 端口:8082(内部)
|
||||||
|
# 提供:IM HTTP API(/api/im/)和 WebSocket(/ws/im/)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
im-service:
|
im-service:
|
||||||
image: ${REGISTRY}/im-service:${IMAGE_TAG}
|
image: ${REGISTRY}/im-service:${IMAGE_TAG}
|
||||||
profiles: ["im"]
|
profiles: ["im"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./config/xuqm.env
|
- ./config/xuqm.env
|
||||||
- ./config/secrets.env
|
- ./config/secrets.env
|
||||||
|
environment:
|
||||||
|
# im-service 默认调用 127.0.0.1:9001,必须覆盖为 Docker 服务名
|
||||||
|
TENANT_SERVICE_URL: "http://tenant-service:9001"
|
||||||
|
PUSH_SERVICE_URL: "http://push-service:8083"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 推送服务(可选,profile: push)
|
||||||
|
# 端口:8083(内部)
|
||||||
|
# 负责通过华为/小米/OPPO/vivo/荣耀/APNs/FCM 下发推送通知
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
push-service:
|
push-service:
|
||||||
image: ${REGISTRY}/push-service:${IMAGE_TAG}
|
image: ${REGISTRY}/push-service:${IMAGE_TAG}
|
||||||
profiles: ["push"]
|
profiles: ["push"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./config/xuqm.env
|
- ./config/xuqm.env
|
||||||
- ./config/secrets.env
|
- ./config/secrets.env
|
||||||
- ./config/vendors/push.env
|
- ./config/vendors/push.env # 各厂商推送凭据
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 版本管理服务(可选,profile: update)
|
||||||
|
# 端口:8084(内部)
|
||||||
|
# 负责:APP 版本发布、RN 热更新包、应用市场自动提交
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
update-service:
|
update-service:
|
||||||
image: ${REGISTRY}/update-service:${IMAGE_TAG}
|
image: ${REGISTRY}/update-service:${IMAGE_TAG}
|
||||||
profiles: ["update"]
|
profiles: ["update"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./config/xuqm.env
|
- ./config/xuqm.env
|
||||||
- ./config/secrets.env
|
- ./config/secrets.env
|
||||||
- ./config/vendors/store-submit.env
|
- ./config/vendors/store-submit.env # 各应用市场发布凭据
|
||||||
|
environment:
|
||||||
|
# update-service 默认调用 xuqm-tenant-service:9001(不可解析),需覆盖
|
||||||
|
SDK_TENANT_SERVICE_URL: "http://tenant-service:9001"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/update:/data/update
|
- ./data/update:/data/update # 版本包存储目录
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# License 服务(可选,profile: license)
|
||||||
|
# 端口:8085(内部)
|
||||||
|
# 负责设备激活数量校验和 License 有效期管理
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
license-service:
|
license-service:
|
||||||
image: ${REGISTRY}/license-service:${IMAGE_TAG}
|
image: ${REGISTRY}/license-service:${IMAGE_TAG}
|
||||||
profiles: ["license"]
|
profiles: ["license"]
|
||||||
|
|||||||
197
install.sh
可执行文件
197
install.sh
可执行文件
@ -0,0 +1,197 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# XuqmGroup 私有化部署 — 一键安装脚本
|
||||||
|
#
|
||||||
|
# 用法一(推荐,先下载再审查):
|
||||||
|
# curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/install.sh \
|
||||||
|
# -o install.sh && bash install.sh
|
||||||
|
#
|
||||||
|
# 用法二(直接执行,保留终端交互):
|
||||||
|
# bash <(curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/install.sh)
|
||||||
|
#
|
||||||
|
# 环境变量(可选):
|
||||||
|
# DEPLOY_HOST 目标机器 IP / 主机名(默认自动检测本机 IP)
|
||||||
|
# INSTALL_DIR 安装目录(默认 /opt/xuqm-private)
|
||||||
|
# XUQM_BRANCH Gitea 分支(默认 main)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 若 stdin 不是终端(curl | bash),强制从 /dev/tty 读取用户输入
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
exec bash "$0" "$@" </dev/tty
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 配置
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
GITEA_BASE="https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy"
|
||||||
|
XUQM_BRANCH="${XUQM_BRANCH:-main}"
|
||||||
|
ARCHIVE_URL="${GITEA_BASE}/archive/${XUQM_BRANCH}.tar.gz"
|
||||||
|
|
||||||
|
INSTALL_DIR="${INSTALL_DIR:-/opt/xuqm-private}"
|
||||||
|
DEPLOY_HOST="${DEPLOY_HOST:-}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 颜色 & 工具函数
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RED='\033[1;31m'; GREEN='\033[1;32m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[1;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||||
|
|
||||||
|
info() { printf "${CYAN} →${RESET} %s\n" "$*"; }
|
||||||
|
ok() { printf "${GREEN} ✓${RESET} %s\n" "$*"; }
|
||||||
|
warn() { printf "${YELLOW} ⚠${RESET} %s\n" "$*"; }
|
||||||
|
fail() { printf "${RED}\nERROR: %s${RESET}\n" "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Banner
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n%b══════════════════════════════════════════════════%b\n' "$CYAN" "$RESET"
|
||||||
|
printf '%b XuqmGroup 私有化部署 — 一键安装向导%b\n' "$BOLD" "$RESET"
|
||||||
|
printf '%b══════════════════════════════════════════════════%b\n\n' "$CYAN" "$RESET"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 检测目标主机 IP
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ -z "$DEPLOY_HOST" ]; then
|
||||||
|
DEPLOY_HOST="$(hostname -I 2>/dev/null | awk '{print $1}' || \
|
||||||
|
ip route get 1 2>/dev/null | awk '{print $7}' | head -1 || \
|
||||||
|
echo "127.0.0.1")"
|
||||||
|
fi
|
||||||
|
printf ' 安装目录: %b%s%b\n' "$BOLD" "$INSTALL_DIR" "$RESET"
|
||||||
|
printf ' 目标主机: %b%s%b\n' "$BOLD" "$DEPLOY_HOST" "$RESET"
|
||||||
|
printf ' 部署分支: %s\n\n' "$XUQM_BRANCH"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 1 — 检测操作系统
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
PKG_MGR="apt"
|
||||||
|
apt-get update -qq 2>/dev/null || true
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
PKG_MGR="yum"
|
||||||
|
else
|
||||||
|
fail "不支持的操作系统(需要 apt 或 yum)"
|
||||||
|
fi
|
||||||
|
ok "包管理器: $PKG_MGR"
|
||||||
|
|
||||||
|
pkg_install() {
|
||||||
|
if [ "$PKG_MGR" = "apt" ]; then
|
||||||
|
apt-get install -y -qq "$@" 2>/dev/null
|
||||||
|
else
|
||||||
|
yum install -y -q "$@" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 2 — 安装 Docker
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
info "Docker 未安装,正在安装..."
|
||||||
|
if [ "$PKG_MGR" = "apt" ]; then
|
||||||
|
pkg_install ca-certificates curl gnupg lsb-release
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||||
|
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list
|
||||||
|
apt-get update -qq
|
||||||
|
pkg_install docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
else
|
||||||
|
pkg_install yum-utils
|
||||||
|
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 2>/dev/null
|
||||||
|
pkg_install docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
systemctl enable --now docker 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker info >/dev/null 2>&1 || { systemctl start docker 2>/dev/null || true; sleep 2; }
|
||||||
|
docker info >/dev/null 2>&1 || fail "Docker daemon 未运行,请执行: systemctl start docker"
|
||||||
|
ok "Docker $(docker --version | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | head -1)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 3 — 确认 Docker Compose v2
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
|
info "Docker Compose v2 未安装,正在安装..."
|
||||||
|
if [ "$PKG_MGR" = "apt" ]; then
|
||||||
|
pkg_install docker-compose-plugin
|
||||||
|
else
|
||||||
|
COMPOSE_VER="v2.27.0"
|
||||||
|
mkdir -p /usr/local/lib/docker/cli-plugins
|
||||||
|
curl -fsSL \
|
||||||
|
"https://github.com/docker/compose/releases/download/${COMPOSE_VER}/docker-compose-$(uname -s)-$(uname -m)" \
|
||||||
|
-o /usr/local/lib/docker/cli-plugins/docker-compose
|
||||||
|
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
ok "Docker Compose $(docker compose version --short 2>/dev/null || echo 'v2')"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 4 — 安装 python3 + bcrypt(新建租户密码哈希)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
info "python3 未安装,正在安装..."
|
||||||
|
pkg_install python3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! python3 -c "import bcrypt" 2>/dev/null; then
|
||||||
|
info "python3-bcrypt 未安装,正在安装..."
|
||||||
|
if [ "$PKG_MGR" = "apt" ]; then
|
||||||
|
pkg_install python3-bcrypt 2>/dev/null || python3 -m pip install -q bcrypt
|
||||||
|
else
|
||||||
|
python3 -m pip install -q bcrypt
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
python3 -c "import bcrypt" 2>/dev/null || fail "python3-bcrypt 安装失败,请手动执行: pip3 install bcrypt"
|
||||||
|
ok "python3 + bcrypt"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 5 — 安装 mysql 客户端(迁移模式需要)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if ! command -v mysql >/dev/null 2>&1; then
|
||||||
|
info "mysql 客户端未安装,正在安装(迁移租户模式需要)..."
|
||||||
|
if [ "$PKG_MGR" = "apt" ]; then
|
||||||
|
pkg_install mysql-client 2>/dev/null || pkg_install default-mysql-client 2>/dev/null || \
|
||||||
|
warn "mysql 客户端安装失败,新建租户模式仍可用"
|
||||||
|
else
|
||||||
|
pkg_install mysql 2>/dev/null || warn "mysql 客户端安装失败,新建租户模式仍可用"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v mysql >/dev/null 2>&1 && ok "mysql 客户端" || warn "mysql 客户端不可用(仅影响迁移模式)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 6 — 下载部署包
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n'
|
||||||
|
info "下载部署包 ${ARCHIVE_URL} ..."
|
||||||
|
|
||||||
|
TMP_PKG="$(mktemp /tmp/xuqm-deploy-XXXXXX.tar.gz)"
|
||||||
|
trap 'rm -f "$TMP_PKG"' EXIT
|
||||||
|
|
||||||
|
curl -fsSL --progress-bar "$ARCHIVE_URL" -o "$TMP_PKG" \
|
||||||
|
|| fail "部署包下载失败,请检查网络或 Gitea 是否可达: $ARCHIVE_URL"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 7 — 解压到安装目录
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ -d "$INSTALL_DIR" ] && [ -f "$INSTALL_DIR/docker-compose.yml" ]; then
|
||||||
|
warn "安装目录已存在部署文件: $INSTALL_DIR"
|
||||||
|
printf ' 将覆盖脚本和配置模板(数据目录 data/ 不受影响)\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
tar -xzf "$TMP_PKG" -C "$INSTALL_DIR" --strip-components=1
|
||||||
|
chmod +x "$INSTALL_DIR/scripts/"*.sh "$INSTALL_DIR/install.sh" 2>/dev/null || true
|
||||||
|
ok "部署包解压完成: $INSTALL_DIR"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 8 — 启动交互式部署
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n%b 依赖安装完毕,即将进入交互式部署向导 ...%b\n\n' "$GREEN" "$RESET"
|
||||||
|
|
||||||
|
export DEPLOY_HOST
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
exec bash scripts/deploy-szyx.sh
|
||||||
@ -2,24 +2,28 @@
|
|||||||
# deploy-szyx.sh — 数字医信私有化一键部署脚本
|
# deploy-szyx.sh — 数字医信私有化一键部署脚本
|
||||||
#
|
#
|
||||||
# 用途:在目标机器上完成以下全部步骤:
|
# 用途:在目标机器上完成以下全部步骤:
|
||||||
# 1. 预检(Docker、Compose、磁盘、端口)
|
# 0. 交互式选择租户初始化方式(新建 / 迁移)并收集必要信息
|
||||||
# 2. 写入数字医信专属配置(.env / secrets.env / xuqm.env / nginx)
|
# 1. 预检(Docker、Compose、python3-bcrypt、磁盘、端口)
|
||||||
|
# 2. 写入配置(.env / secrets.env / xuqm.env / bootstrap.env / nginx)
|
||||||
# 3. 登录 ACR 镜像仓库
|
# 3. 登录 ACR 镜像仓库
|
||||||
# 4. 启动基础设施容器(MySQL、Redis)并等待就绪
|
# 4. 启动基础设施容器(MySQL、Redis)并等待就绪
|
||||||
# 5. 启动业务容器(base profile)并等待健康
|
# 5. 拉取镜像并启动所有业务容器(base + im + push + update + license)
|
||||||
# 6. 迁移数字医信生产租户数据
|
# 6. Schema 扩展(is_default / deletable 列 + 删除保护触发器)+ 系统应用初始化
|
||||||
# 7. 最终验收并输出登录指引
|
# 7. 租户初始化(新建:验证 bootstrap 结果;迁移:执行 migrate-tenant.sh)
|
||||||
|
# 8. 运行一键验证脚本确认所有服务正常
|
||||||
#
|
#
|
||||||
# 幂等性:可重复执行。已运行的容器不会被重建;已迁移的租户不会被二次清空。
|
# 幂等性:可重复执行。已运行的容器不会被重建。
|
||||||
|
# 迁移模式:若检测到已有租户数据,会在执行前提示确认。
|
||||||
#
|
#
|
||||||
# 前提:
|
# 前提:
|
||||||
# - Docker 和 Docker Compose v2 已安装
|
# - Docker 和 Docker Compose v2 已安装
|
||||||
# - 目标机器与生产 MySQL (39.107.53.187) 网络可达
|
# - python3 和 python3-bcrypt 已安装(用于密码哈希和认证)
|
||||||
|
# - 迁移模式还需要 mysql 客户端
|
||||||
# - 在 XuqmGroup-PrivateDeploy 仓库根目录下执行本脚本
|
# - 在 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)
|
# DEPLOY_HOST 目标机器 IP / 主机名(默认 127.0.0.1)
|
||||||
|
# 纯 IP 部署无需域名,局域网内可全功能使用
|
||||||
# REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022)
|
# REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022)
|
||||||
# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理
|
# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理
|
||||||
|
|
||||||
@ -40,6 +44,7 @@ REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-xuqinmin1022}"
|
|||||||
IMAGE_TAG="latest"
|
IMAGE_TAG="latest"
|
||||||
|
|
||||||
# 部署主机(用于健康检查 HTTP 请求)
|
# 部署主机(用于健康检查 HTTP 请求)
|
||||||
|
# 可以是 IP 地址,无需域名,局域网内即可完整使用所有服务
|
||||||
DEPLOY_HOST="${DEPLOY_HOST:-127.0.0.1}"
|
DEPLOY_HOST="${DEPLOY_HOST:-127.0.0.1}"
|
||||||
CONSOLE_BASE="http://${DEPLOY_HOST}"
|
CONSOLE_BASE="http://${DEPLOY_HOST}"
|
||||||
|
|
||||||
@ -52,11 +57,7 @@ MYSQL_USERNAME="xuqm"
|
|||||||
# Redis(managed 模式)
|
# Redis(managed 模式)
|
||||||
REDIS_PASSWORD="${REDIS_PASSWORD:-XuqmRedis@2026}"
|
REDIS_PASSWORD="${REDIS_PASSWORD:-XuqmRedis@2026}"
|
||||||
|
|
||||||
# 数字医信登录密码(不在脚本里重置,使用生产密码原文迁移)
|
# 源生产 MySQL(迁移模式的默认值,交互时可覆盖)
|
||||||
TENANT_EMAIL="szyx@bjca.org.cn"
|
|
||||||
TENANT_USERNAME="szyx"
|
|
||||||
|
|
||||||
# 源生产 MySQL
|
|
||||||
SRC_HOST="39.107.53.187"
|
SRC_HOST="39.107.53.187"
|
||||||
SRC_PORT="3306"
|
SRC_PORT="3306"
|
||||||
SRC_USER="xuqm"
|
SRC_USER="xuqm"
|
||||||
@ -68,6 +69,7 @@ SRC_DB="xuqm_tenant"
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
STEP=0
|
STEP=0
|
||||||
|
TOTAL_STEPS=8
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 工具函数
|
# 工具函数
|
||||||
@ -78,25 +80,10 @@ warn() { printf ' \033[33m⚠\033[0m %s\n' "$*"; }
|
|||||||
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
|
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||||
step() { STEP=$((STEP+1)); log "$*"; }
|
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() {
|
container_running() {
|
||||||
docker ps --filter "name=$1" --filter "status=running" --format '{{.Names}}' 2>/dev/null | grep -q "$1"
|
docker ps --filter "name=$1" --filter "status=running" --format '{{.Names}}' 2>/dev/null | grep -q "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
TOTAL_STEPS=7
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 0. 确认工作目录
|
# 0. 确认工作目录
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -106,12 +93,122 @@ TOTAL_STEPS=7
|
|||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||||
printf '\033[1;35m %s 私有化一键部署脚本\033[0m\n' "$CUSTOMER_NAME"
|
printf '\033[1;35m %s 私有化一键部署脚本(全量部署)\033[0m\n' "$CUSTOMER_NAME"
|
||||||
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||||
printf ' 部署目录: %s\n' "$ROOT_DIR"
|
printf ' 部署目录: %s\n' "$ROOT_DIR"
|
||||||
printf ' 目标主机: %s\n' "$DEPLOY_HOST"
|
printf ' 目标主机: %s\n' "$DEPLOY_HOST"
|
||||||
|
printf ' 访问地址: %s\n' "$CONSOLE_BASE"
|
||||||
printf ' 镜像仓库: %s\n' "$REGISTRY"
|
printf ' 镜像仓库: %s\n' "$REGISTRY"
|
||||||
printf ' 租户: %s (%s)\n\n' "$CUSTOMER_NAME" "$TENANT_EMAIL"
|
printf ' 服务集: base + im + push + update + license\n\n'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 0b. 租户初始化方式选择(交互式,在 Step 1 预检前执行)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
command -v python3 >/dev/null 2>&1 || \
|
||||||
|
fail "python3 未安装(租户密码哈希需要): apt install -y python3 python3-bcrypt"
|
||||||
|
python3 -c "import bcrypt" 2>/dev/null || \
|
||||||
|
fail "python3-bcrypt 未安装: pip3 install bcrypt 或 apt install -y python3-bcrypt"
|
||||||
|
|
||||||
|
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
|
||||||
|
printf '\033[1;33m 租户初始化方式\033[0m\n'
|
||||||
|
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
|
||||||
|
printf ' 1) 新建租户 — 填写租户信息,系统将为您创建全新账号\n'
|
||||||
|
printf ' 2) 迁移租户 — 从生产 MySQL 导入现有租户数据(需主账号认证)\n\n'
|
||||||
|
|
||||||
|
DEPLOY_MODE=""
|
||||||
|
while [ -z "$DEPLOY_MODE" ]; do
|
||||||
|
read -rp " 请选择 [1/2]: " _choice
|
||||||
|
case "$_choice" in
|
||||||
|
1) DEPLOY_MODE="new" ;;
|
||||||
|
2) DEPLOY_MODE="migrate" ;;
|
||||||
|
*) printf ' 请输入 1 或 2\n' ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
DEPLOY_TENANT_EMAIL=""
|
||||||
|
DEPLOY_TENANT_USERNAME=""
|
||||||
|
DEPLOY_TENANT_NICKNAME=""
|
||||||
|
_BOOTSTRAP_PASSWORD=""
|
||||||
|
|
||||||
|
if [ "$DEPLOY_MODE" = "new" ]; then
|
||||||
|
printf '\n ── 新建租户信息 ──\n'
|
||||||
|
|
||||||
|
while [ -z "$DEPLOY_TENANT_EMAIL" ]; do
|
||||||
|
read -rp " 租户邮箱: " DEPLOY_TENANT_EMAIL
|
||||||
|
printf '%s' "$DEPLOY_TENANT_EMAIL" | grep -qE '^[^@]+@[^@]+\.[^@]+$' || {
|
||||||
|
warn "邮箱格式不正确,请重新输入"
|
||||||
|
DEPLOY_TENANT_EMAIL=""
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
while [ -z "$DEPLOY_TENANT_USERNAME" ]; do
|
||||||
|
read -rp " 用户名(登录用): " DEPLOY_TENANT_USERNAME
|
||||||
|
done
|
||||||
|
|
||||||
|
read -rp " 显示名称(昵称,默认同用户名): " DEPLOY_TENANT_NICKNAME
|
||||||
|
[ -z "$DEPLOY_TENANT_NICKNAME" ] && DEPLOY_TENANT_NICKNAME="$DEPLOY_TENANT_USERNAME"
|
||||||
|
|
||||||
|
_pw=""
|
||||||
|
while [ -z "$_pw" ]; do
|
||||||
|
read -rsp " 登录密码(≥8 位): " _pw; printf '\n'
|
||||||
|
[ "${#_pw}" -ge 8 ] || { warn "密码不能少于 8 位,请重新输入"; _pw=""; continue; }
|
||||||
|
read -rsp " 确认密码: " _pw2; printf '\n'
|
||||||
|
[ "$_pw" = "$_pw2" ] || { warn "两次密码不一致,请重新输入"; _pw=""; }
|
||||||
|
done
|
||||||
|
unset _pw2
|
||||||
|
|
||||||
|
# Spring Boot bootstrap 接收明文密码并在存储时自行 bcrypt(bootstrap.env chmod 600)
|
||||||
|
_BOOTSTRAP_PASSWORD="$_pw"
|
||||||
|
unset _pw
|
||||||
|
ok "租户信息已收集"
|
||||||
|
|
||||||
|
else
|
||||||
|
# ── 迁移模式:先收集 SRC 配置 + 认证,再继续后续步骤 ──
|
||||||
|
printf '\n ── 迁移源 MySQL 配置(回车使用默认值)──\n'
|
||||||
|
read -rp " 源 MySQL 主机 [${SRC_HOST}]: " _inp; [ -n "$_inp" ] && SRC_HOST="$_inp"
|
||||||
|
read -rp " 源 MySQL 端口 [${SRC_PORT}]: " _inp; [ -n "$_inp" ] && SRC_PORT="$_inp"
|
||||||
|
read -rp " 源 MySQL 用户 [${SRC_USER}]: " _inp; [ -n "$_inp" ] && SRC_USER="$_inp"
|
||||||
|
read -rsp " 源 MySQL 密码 [(隐藏,回车保留默认)]: " _inp; printf '\n'; [ -n "$_inp" ] && SRC_PASSWORD="$_inp"
|
||||||
|
read -rp " 源数据库名 [${SRC_DB}]: " _inp; [ -n "$_inp" ] && SRC_DB="$_inp"
|
||||||
|
|
||||||
|
printf '\n ── 租户主账号认证 ──\n'
|
||||||
|
while [ -z "$DEPLOY_TENANT_EMAIL" ]; do
|
||||||
|
read -rp " 租户主账号邮箱: " DEPLOY_TENANT_EMAIL
|
||||||
|
printf '%s' "$DEPLOY_TENANT_EMAIL" | grep -qE '^[^@]+@[^@]+\.[^@]+$' || {
|
||||||
|
warn "邮箱格式不正确,请重新输入"
|
||||||
|
DEPLOY_TENANT_EMAIL=""
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
read -rsp " 租户主账号密码(仅用于认证,不存储明文): " _MIGRATE_PASSWORD; printf '\n'
|
||||||
|
[ -n "$_MIGRATE_PASSWORD" ] || fail "密码不能为空"
|
||||||
|
|
||||||
|
command -v mysql >/dev/null 2>&1 || \
|
||||||
|
fail "迁移模式需要 mysql 客户端: apt install -y mysql-client"
|
||||||
|
|
||||||
|
printf ' 正在连接生产 MySQL 进行身份认证 ...\n'
|
||||||
|
_SRC_HASH=$(MYSQL_PWD="$SRC_PASSWORD" mysql \
|
||||||
|
-h "$SRC_HOST" -P "$SRC_PORT" -u "$SRC_USER" \
|
||||||
|
--connect-timeout=10 "$SRC_DB" \
|
||||||
|
-N -e "SELECT password FROM t_tenant WHERE email='${DEPLOY_TENANT_EMAIL}' AND type='MAIN' LIMIT 1" 2>/dev/null || true)
|
||||||
|
[ -n "$_SRC_HASH" ] || \
|
||||||
|
fail "生产 MySQL 中未找到租户 ${DEPLOY_TENANT_EMAIL},或无法连接到 ${SRC_HOST}:${SRC_PORT}"
|
||||||
|
|
||||||
|
_AUTH_OK=$(MIGRATE_PW="$_MIGRATE_PASSWORD" MIGRATE_HASH="$_SRC_HASH" python3 -c "
|
||||||
|
import bcrypt, os
|
||||||
|
pw = os.environ['MIGRATE_PW'].encode('utf-8')
|
||||||
|
h = os.environ['MIGRATE_HASH'].encode('utf-8')
|
||||||
|
print('ok' if bcrypt.checkpw(pw, h) else 'fail')
|
||||||
|
")
|
||||||
|
unset _MIGRATE_PASSWORD _SRC_HASH
|
||||||
|
[ "$_AUTH_OK" = "ok" ] || fail "密码认证失败,请确认主账号密码后重试"
|
||||||
|
ok "生产租户 ${DEPLOY_TENANT_EMAIL} 身份认证通过"
|
||||||
|
|
||||||
|
DEPLOY_TENANT_USERNAME="$DEPLOY_TENANT_EMAIL"
|
||||||
|
DEPLOY_TENANT_NICKNAME=""
|
||||||
|
_BOOTSTRAP_PASSWORD="change-me-on-first-login"
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 1 — 预检
|
# Step 1 — 预检
|
||||||
@ -125,9 +222,6 @@ 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 未安装"
|
docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 未安装"
|
||||||
ok "Docker Compose $(docker compose version --short 2>/dev/null || echo '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="$(df -BG "$ROOT_DIR" | awk 'NR==2{gsub(/G/,"",$4); print $4}')"
|
||||||
[ "${DISK_FREE_GB:-0}" -ge 10 ] || \
|
[ "${DISK_FREE_GB:-0}" -ge 10 ] || \
|
||||||
fail "磁盘可用空间不足(需 ≥10 GB,当前 ${DISK_FREE_GB:-?} GB)"
|
fail "磁盘可用空间不足(需 ≥10 GB,当前 ${DISK_FREE_GB:-?} GB)"
|
||||||
@ -145,51 +239,64 @@ done
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 2 — 写入配置
|
# Step 2 — 写入配置
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
step "写入数字医信专属配置"
|
step "写入数字医信专属配置(全服务)"
|
||||||
|
|
||||||
# .env
|
# .env — 主配置文件,包含所有 profile
|
||||||
cat > "$ROOT_DIR/.env" <<EOF
|
cat > "$ROOT_DIR/.env" <<EOF
|
||||||
PRIVATE_VERSION=2026.05.18-private.1
|
# =============================================================================
|
||||||
|
# 数字医信私有化部署主配置
|
||||||
|
# 部署时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
# 部署主机: ${DEPLOY_HOST}
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
PRIVATE_VERSION=2026.05.19-private.1
|
||||||
REGISTRY=${REGISTRY}
|
REGISTRY=${REGISTRY}
|
||||||
REGISTRY_USER=${REGISTRY_USER}
|
REGISTRY_USER=${REGISTRY_USER}
|
||||||
REGISTRY_PASSWORD=${REGISTRY_PASSWORD}
|
REGISTRY_PASSWORD=${REGISTRY_PASSWORD}
|
||||||
IMAGE_TAG=${IMAGE_TAG}
|
IMAGE_TAG=${IMAGE_TAG}
|
||||||
COMPOSE_PROFILES=base,infra-mysql,infra-redis
|
|
||||||
|
|
||||||
|
# 启用全量服务(base + 基础设施 + im + push + update + license)
|
||||||
|
COMPOSE_PROFILES=base,infra-mysql,infra-redis,im,push,update,license
|
||||||
|
|
||||||
|
# MySQL(managed 模式,Docker 容器托管)
|
||||||
MYSQL_MODE=managed
|
MYSQL_MODE=managed
|
||||||
MYSQL_HOST=mysql
|
MYSQL_HOST=mysql
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_DATABASE=${MYSQL_DATABASE}
|
MYSQL_DATABASE=${MYSQL_DATABASE}
|
||||||
MYSQL_USERNAME=${MYSQL_USERNAME}
|
MYSQL_USERNAME=${MYSQL_USERNAME}
|
||||||
|
|
||||||
|
# Redis(managed 模式)
|
||||||
REDIS_MODE=managed
|
REDIS_MODE=managed
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_DATABASE=0
|
REDIS_DATABASE=0
|
||||||
|
|
||||||
|
# 访问域名(IP 部署时直接使用 IP,无需配置域名)
|
||||||
CONSOLE_DOMAIN=${CONSOLE_BASE}
|
CONSOLE_DOMAIN=${CONSOLE_BASE}
|
||||||
OPS_DOMAIN=${CONSOLE_BASE}
|
OPS_DOMAIN=${CONSOLE_BASE}
|
||||||
DOCS_DOMAIN=${CONSOLE_BASE}
|
DOCS_DOMAIN=${CONSOLE_BASE}/docs
|
||||||
FILE_DOMAIN=${CONSOLE_BASE}
|
FILE_DOMAIN=${CONSOLE_BASE}
|
||||||
IM_DOMAIN=
|
IM_DOMAIN=${CONSOLE_BASE}
|
||||||
UPDATE_DOMAIN=
|
UPDATE_DOMAIN=${CONSOLE_BASE}
|
||||||
LICENSE_DOMAIN=
|
LICENSE_DOMAIN=${CONSOLE_BASE}
|
||||||
PUSH_DOMAIN=
|
PUSH_DOMAIN=${CONSOLE_BASE}
|
||||||
|
|
||||||
|
# 功能开关(全量启用)
|
||||||
ENABLE_FILE=true
|
ENABLE_FILE=true
|
||||||
ENABLE_IM=false
|
ENABLE_IM=true
|
||||||
ENABLE_PUSH=false
|
ENABLE_PUSH=true
|
||||||
ENABLE_UPDATE=false
|
ENABLE_UPDATE=true
|
||||||
ENABLE_LICENSE=false
|
ENABLE_LICENSE=true
|
||||||
|
|
||||||
TENANT_BOOTSTRAP_EMAIL=${TENANT_EMAIL}
|
TENANT_BOOTSTRAP_EMAIL=${DEPLOY_TENANT_EMAIL}
|
||||||
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||||||
EOF
|
EOF
|
||||||
ok ".env 已写入"
|
ok ".env 已写入"
|
||||||
|
|
||||||
# config/secrets.env
|
# config/secrets.env — 敏感信息(权限 600)
|
||||||
mkdir -p "$ROOT_DIR/config"
|
mkdir -p "$ROOT_DIR/config"
|
||||||
cat > "$ROOT_DIR/config/secrets.env" <<EOF
|
cat > "$ROOT_DIR/config/secrets.env" <<EOF
|
||||||
|
# 数字医信私有化部署 — 敏感凭据(请妥善保管,chmod 600)
|
||||||
MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||||||
MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||||
REDIS_PASSWORD=${REDIS_PASSWORD}
|
REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||||
@ -197,67 +304,157 @@ EOF
|
|||||||
chmod 600 "$ROOT_DIR/config/secrets.env"
|
chmod 600 "$ROOT_DIR/config/secrets.env"
|
||||||
ok "config/secrets.env 已写入 (chmod 600)"
|
ok "config/secrets.env 已写入 (chmod 600)"
|
||||||
|
|
||||||
# config/xuqm.env — 业务服务容器内配置
|
# config/xuqm.env — 业务服务容器内配置(全服务开启)
|
||||||
cat > "$ROOT_DIR/config/xuqm.env" <<EOF
|
cat > "$ROOT_DIR/config/xuqm.env" <<EOF
|
||||||
|
# =============================================================================
|
||||||
|
# 数字医信私有化部署 — 业务配置
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 私有化部署模式(必须为 PRIVATE)
|
||||||
DEPLOYMENT_MODE=PRIVATE
|
DEPLOYMENT_MODE=PRIVATE
|
||||||
TENANT_REGISTER_ENABLED=false
|
TENANT_REGISTER_ENABLED=false
|
||||||
TENANT_BOOTSTRAP_ENABLED=true
|
TENANT_BOOTSTRAP_ENABLED=true
|
||||||
|
|
||||||
|
# 功能开关(全服务启用)
|
||||||
ENABLE_FILE=true
|
ENABLE_FILE=true
|
||||||
ENABLE_IM=false
|
ENABLE_IM=true
|
||||||
ENABLE_PUSH=false
|
ENABLE_PUSH=true
|
||||||
ENABLE_UPDATE=false
|
ENABLE_UPDATE=true
|
||||||
ENABLE_LICENSE=false
|
ENABLE_LICENSE=true
|
||||||
|
|
||||||
|
# 数据库(容器内使用 Docker 服务名)
|
||||||
MYSQL_HOST=mysql
|
MYSQL_HOST=mysql
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_DATABASE=${MYSQL_DATABASE}
|
MYSQL_DATABASE=${MYSQL_DATABASE}
|
||||||
MYSQL_USERNAME=${MYSQL_USERNAME}
|
MYSQL_USERNAME=${MYSQL_USERNAME}
|
||||||
|
|
||||||
|
# Redis(容器内使用 Docker 服务名)
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_DATABASE=0
|
REDIS_DATABASE=0
|
||||||
|
|
||||||
|
# 访问域名(IP 部署时直接使用 IP)
|
||||||
CONSOLE_DOMAIN=${CONSOLE_BASE}
|
CONSOLE_DOMAIN=${CONSOLE_BASE}
|
||||||
OPS_DOMAIN=${CONSOLE_BASE}
|
OPS_DOMAIN=${CONSOLE_BASE}
|
||||||
DOCS_DOMAIN=${CONSOLE_BASE}
|
DOCS_DOMAIN=${CONSOLE_BASE}/docs
|
||||||
FILE_DOMAIN=${CONSOLE_BASE}
|
FILE_DOMAIN=${CONSOLE_BASE}
|
||||||
IM_DOMAIN=
|
IM_DOMAIN=${CONSOLE_BASE}
|
||||||
UPDATE_DOMAIN=
|
UPDATE_DOMAIN=${CONSOLE_BASE}
|
||||||
LICENSE_DOMAIN=
|
LICENSE_DOMAIN=${CONSOLE_BASE}
|
||||||
PUSH_DOMAIN=
|
PUSH_DOMAIN=${CONSOLE_BASE}
|
||||||
|
|
||||||
|
# SDK 对外服务地址(客户端 SDK 使用)
|
||||||
SDK_FILE_SERVICE_URL=${CONSOLE_BASE}
|
SDK_FILE_SERVICE_URL=${CONSOLE_BASE}
|
||||||
SDK_IM_API_URL=
|
SDK_IM_API_URL=${CONSOLE_BASE}
|
||||||
SDK_IM_WS_URL=
|
SDK_IM_WS_URL=ws://${DEPLOY_HOST}/ws/im
|
||||||
|
|
||||||
|
# 系统 IM 通信应用 key(私有化服务间消息通知使用此 app_key 连接 IM 服务)
|
||||||
|
# 与公有化平台 xuqinmin12 租户下的平台系统应用 key 保持一致
|
||||||
|
SYSTEM_APP_KEY=ak_409e217e4aa14254ad73ad3c
|
||||||
EOF
|
EOF
|
||||||
ok "config/xuqm.env 已写入"
|
ok "config/xuqm.env 已写入"
|
||||||
|
|
||||||
# config/tenant/bootstrap.env
|
# config/tenant/bootstrap.env — 使用收集到的租户信息(迁移模式为占位符,后续由迁移步骤覆盖)
|
||||||
mkdir -p "$ROOT_DIR/config/tenant"
|
mkdir -p "$ROOT_DIR/config/tenant"
|
||||||
cat > "$ROOT_DIR/config/tenant/bootstrap.env" <<EOF
|
cat > "$ROOT_DIR/config/tenant/bootstrap.env" <<EOF
|
||||||
TENANT_BOOTSTRAP_EMAIL=${TENANT_EMAIL}
|
TENANT_BOOTSTRAP_EMAIL=${DEPLOY_TENANT_EMAIL}
|
||||||
TENANT_BOOTSTRAP_USERNAME=${TENANT_USERNAME}
|
TENANT_BOOTSTRAP_USERNAME=${DEPLOY_TENANT_USERNAME}
|
||||||
TENANT_BOOTSTRAP_PASSWORD=change-me-on-first-login
|
TENANT_BOOTSTRAP_PASSWORD=${_BOOTSTRAP_PASSWORD}
|
||||||
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||||||
EOF
|
EOF
|
||||||
ok "config/tenant/bootstrap.env 已写入"
|
chmod 600 "$ROOT_DIR/config/tenant/bootstrap.env"
|
||||||
|
ok "config/tenant/bootstrap.env 已写入 (chmod 600)"
|
||||||
|
|
||||||
# config/nginx/conf.d/xuqm.conf — 完整路由
|
# config/nginx/conf.d/xuqm.conf — 从仓库中的文件复制(不再内嵌 heredoc,避免两处维护)
|
||||||
mkdir -p "$ROOT_DIR/config/nginx/conf.d"
|
mkdir -p "$ROOT_DIR/config/nginx/conf.d"
|
||||||
cat > "$ROOT_DIR/config/nginx/conf.d/xuqm.conf" <<'NGINX_CONF'
|
NGINX_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/config/nginx/conf.d/xuqm.conf"
|
||||||
|
if [ ! -f "$NGINX_SRC" ]; then
|
||||||
|
fail "nginx 配置文件不存在: $NGINX_SRC(请确保在仓库根目录运行本脚本)"
|
||||||
|
fi
|
||||||
|
# (以下 heredoc 仅作本地备份,deploy 时以上面复制为准,此段不执行)
|
||||||
|
: <<'NGINX_CONF'
|
||||||
|
# =============================================================================
|
||||||
|
# XuqmGroup 私有化部署 — Nginx 路由配置
|
||||||
|
#
|
||||||
|
# 服务端口映射:
|
||||||
|
# tenant-service 9001 /api/*(核心 API)、/actuator/*
|
||||||
|
# file-service 8086 /file/*(文件上传下载)
|
||||||
|
# im-service 8082 /api/im/*(IM HTTP)、/ws/im/*(WebSocket)
|
||||||
|
# update-service 8084 /api/v1/updates/*、/api/v1/rn/*
|
||||||
|
# push-service 8083 厂商回调(内部端口)
|
||||||
|
# license-service 8085 内部调用
|
||||||
|
# ops-web 80 /ops/*(运营后台)
|
||||||
|
# tenant-web 80 /*(控制台,兜底路由)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
|
# 强制 UTF-8 编码,防止中文乱码
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
# 最大上传文件大小(文件服务单独设置 500m)
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
|
|
||||||
|
# ----------- 健康检查 -----------
|
||||||
location /health {
|
location /health {
|
||||||
return 200 "ok\n";
|
return 200 "ok\n";
|
||||||
add_header Content-Type text/plain;
|
add_header Content-Type text/plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
# tenant-service 运行在 9001 端口
|
# ----------- 版本管理服务(update-service:8084)-----------
|
||||||
|
# 注意:必须在通用 /api/ 之前声明,否则会被错误路由到 tenant-service
|
||||||
|
location /api/v1/updates/ {
|
||||||
|
proxy_pass http://update-service:8084/api/v1/updates/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# RN 热更新包下载和列表
|
||||||
|
location /api/v1/rn/ {
|
||||||
|
proxy_pass http://update-service:8084/api/v1/rn/;
|
||||||
|
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 120s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- IM 服务(im-service:8082)-----------
|
||||||
|
# 注意:必须在通用 /api/ 之前声明
|
||||||
|
location /api/im/ {
|
||||||
|
proxy_pass http://im-service:8082/api/im/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# IM WebSocket 长连接(客户端消息收发)
|
||||||
|
location /ws/im/ {
|
||||||
|
proxy_pass http://im-service:8082/ws/im/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 3600s; # WebSocket 保持 1 小时
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- License 服务(license-service:8085)-----------
|
||||||
|
# 注意:必须在通用 /api/ 之前声明
|
||||||
|
location /api/license/ {
|
||||||
|
proxy_pass http://license-service:8085/api/license/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- 核心 API(tenant-service:9001)-----------
|
||||||
|
# 注意:tenant-service 运行在 9001 端口(不是 8080)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://tenant-service:9001/api/;
|
proxy_pass http://tenant-service:9001/api/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@ -266,13 +463,14 @@ server {
|
|||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Spring Boot Actuator 健康检查(内部监控用)
|
||||||
location /actuator/ {
|
location /actuator/ {
|
||||||
proxy_pass http://tenant-service:9001/actuator/;
|
proxy_pass http://tenant-service:9001/actuator/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
# file-service 运行在 8086 端口
|
# ----------- 文件服务(file-service:8086)-----------
|
||||||
location /file/ {
|
location /file/ {
|
||||||
proxy_pass http://file-service:8086/file/;
|
proxy_pass http://file-service:8086/file/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@ -280,14 +478,25 @@ server {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
client_max_body_size 500m;
|
client_max_body_size 500m;
|
||||||
proxy_read_timeout 300s;
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ----------- 文档站(tenant-web 内置,VitePress base=/docs/)-----------
|
||||||
|
location /docs/ {
|
||||||
|
proxy_pass http://tenant-web:80/docs/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------- 运营后台(ops-web:80)-----------
|
||||||
location /ops {
|
location /ops {
|
||||||
proxy_pass http://ops-web:80;
|
proxy_pass http://ops-web:80;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ----------- 控制台前端(tenant-web:80)-----------
|
||||||
|
# 兜底路由,必须放最后
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://tenant-web:80;
|
proxy_pass http://tenant-web:80;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@ -295,26 +504,68 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
NGINX_CONF
|
NGINX_CONF
|
||||||
ok "config/nginx/conf.d/xuqm.conf 已写入"
|
# 实际使用仓库中的文件(已跳过上方 heredoc)
|
||||||
|
cp "$NGINX_SRC" "$ROOT_DIR/config/nginx/conf.d/xuqm.conf"
|
||||||
|
ok "config/nginx/conf.d/xuqm.conf 已写入(来自 $NGINX_SRC)"
|
||||||
|
|
||||||
# 生成 SDK JSON 和 docs-runtime.json
|
# config/vendors/push.env — 推送服务厂商凭据(初始为关闭状态,按需开启)
|
||||||
|
mkdir -p "$ROOT_DIR/config/vendors"
|
||||||
|
if [ ! -f "$ROOT_DIR/config/vendors/push.env" ]; then
|
||||||
|
cat > "$ROOT_DIR/config/vendors/push.env" <<'PUSH_ENV'
|
||||||
|
# 推送厂商凭据 — 按需填写并将对应 ENABLED 改为 true
|
||||||
|
HUAWEI_PUSH_ENABLED=false
|
||||||
|
MI_PUSH_ENABLED=false
|
||||||
|
OPPO_PUSH_ENABLED=false
|
||||||
|
VIVO_PUSH_ENABLED=false
|
||||||
|
HONOR_PUSH_ENABLED=false
|
||||||
|
APNS_PUSH_ENABLED=false
|
||||||
|
FCM_PUSH_ENABLED=false
|
||||||
|
PUSH_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# config/vendors/store-submit.env — 应用市场发布凭据(初始为关闭状态)
|
||||||
|
if [ ! -f "$ROOT_DIR/config/vendors/store-submit.env" ]; then
|
||||||
|
cat > "$ROOT_DIR/config/vendors/store-submit.env" <<'STORE_ENV'
|
||||||
|
# 应用市场自动发布凭据 — 按需填写并将对应 ENABLED 改为 true
|
||||||
|
HUAWEI_STORE_ENABLED=false
|
||||||
|
MI_STORE_ENABLED=false
|
||||||
|
OPPO_STORE_ENABLED=false
|
||||||
|
VIVO_STORE_ENABLED=false
|
||||||
|
HONOR_STORE_ENABLED=false
|
||||||
|
STORE_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# config/mail/smtp.env — 邮件服务配置(按需填写)
|
||||||
|
mkdir -p "$ROOT_DIR/config/mail"
|
||||||
|
if [ ! -f "$ROOT_DIR/config/mail/smtp.env" ]; then
|
||||||
|
cat > "$ROOT_DIR/config/mail/smtp.env" <<'SMTP_ENV'
|
||||||
|
# 邮件服务配置(按需填写,不填则邮件功能不可用)
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=465
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_TLS=true
|
||||||
|
SMTP_SSL=true
|
||||||
|
SMTP_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SDK JSON 和文档站运行时配置
|
||||||
mkdir -p "$ROOT_DIR/config/sdk" "$ROOT_DIR/config/docs" "$ROOT_DIR/.deploy-state"
|
mkdir -p "$ROOT_DIR/config/sdk" "$ROOT_DIR/config/docs" "$ROOT_DIR/.deploy-state"
|
||||||
cat > "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" <<EOF
|
cat > "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" <<EOF
|
||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"configVersion": "2026.05.18-private.1",
|
"configVersion": "2026.05.19-private.1",
|
||||||
"deployment": "PRIVATE",
|
"deployment": "PRIVATE",
|
||||||
"appKey": "ak_private_default",
|
"appKey": "ak_private_default",
|
||||||
"controlBaseUrl": "${CONSOLE_BASE}",
|
"controlBaseUrl": "${CONSOLE_BASE}",
|
||||||
"fileBaseUrl": "${CONSOLE_BASE}",
|
"fileBaseUrl": "${CONSOLE_BASE}",
|
||||||
"imApiBaseUrl": "",
|
"imApiBaseUrl": "${CONSOLE_BASE}",
|
||||||
"imWsUrl": "",
|
"imWsUrl": "ws://${DEPLOY_HOST}",
|
||||||
"features": {
|
"features": {
|
||||||
"file": true,
|
"file": true,
|
||||||
"im": false,
|
"im": true,
|
||||||
"push": false,
|
"push": true,
|
||||||
"update": false,
|
"update": true,
|
||||||
"license": false
|
"license": true
|
||||||
},
|
},
|
||||||
"connectTimeoutMs": 10000,
|
"connectTimeoutMs": 10000,
|
||||||
"readTimeoutMs": 30000,
|
"readTimeoutMs": 30000,
|
||||||
@ -325,15 +576,33 @@ EOF
|
|||||||
cat > "$ROOT_DIR/config/docs/docs-runtime.json" <<EOF
|
cat > "$ROOT_DIR/config/docs/docs-runtime.json" <<EOF
|
||||||
{
|
{
|
||||||
"deployment": "PRIVATE",
|
"deployment": "PRIVATE",
|
||||||
"privateVersion": "2026.05.18-private.1",
|
"privateVersion": "2026.05.19-private.1",
|
||||||
"domains": { "console": "${CONSOLE_BASE}", "file": "${CONSOLE_BASE}" },
|
"domains": {
|
||||||
"features": { "file": true, "im": false, "push": false, "update": false, "license": false }
|
"console": "${CONSOLE_BASE}",
|
||||||
|
"file": "${CONSOLE_BASE}",
|
||||||
|
"im": "${CONSOLE_BASE}",
|
||||||
|
"update": "${CONSOLE_BASE}"
|
||||||
|
},
|
||||||
|
"features": { "file": true, "im": true, "push": true, "update": true, "license": true }
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
mkdir -p "$ROOT_DIR/data/uploads" "$ROOT_DIR/data/mysql" "$ROOT_DIR/data/redis" \
|
# 创建所有数据目录
|
||||||
"$ROOT_DIR/data/backups" "$ROOT_DIR/logs"
|
mkdir -p \
|
||||||
ok "SDK JSON 和目录结构就绪"
|
"$ROOT_DIR/data/uploads" \
|
||||||
|
"$ROOT_DIR/data/mysql" \
|
||||||
|
"$ROOT_DIR/data/redis" \
|
||||||
|
"$ROOT_DIR/data/update" \
|
||||||
|
"$ROOT_DIR/data/backups" \
|
||||||
|
"$ROOT_DIR/logs"
|
||||||
|
ok "SDK JSON、文档站配置和目录结构就绪"
|
||||||
|
|
||||||
|
# 将 secrets 导出到 shell 环境,使 docker compose 变量替换可读取密码
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$ROOT_DIR/config/secrets.env"
|
||||||
|
set +a
|
||||||
|
ok "secrets.env 已加载到 shell 环境"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 3 — 登录镜像仓库
|
# Step 3 — 登录镜像仓库
|
||||||
@ -355,11 +624,6 @@ docker compose \
|
|||||||
-f "$ROOT_DIR/docker-compose.yml" \
|
-f "$ROOT_DIR/docker-compose.yml" \
|
||||||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||||||
--profile infra-mysql --profile infra-redis \
|
--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
|
up -d mysql redis
|
||||||
|
|
||||||
# 等待 MySQL 就绪(最多 90 秒)
|
# 等待 MySQL 就绪(最多 90 秒)
|
||||||
@ -388,26 +652,23 @@ for i in $(seq 1 10); do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 5 — 启动业务容器
|
# Step 5 — 启动所有业务容器(全服务)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
step "拉取镜像并启动业务容器(base profile)"
|
step "拉取镜像并启动所有业务容器(base + im + push + update + license)"
|
||||||
|
|
||||||
docker compose \
|
docker compose \
|
||||||
--env-file "$ROOT_DIR/.env" \
|
--env-file "$ROOT_DIR/.env" \
|
||||||
-f "$ROOT_DIR/docker-compose.yml" \
|
-f "$ROOT_DIR/docker-compose.yml" \
|
||||||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||||||
--profile base --profile infra-mysql --profile infra-redis \
|
--profile base \
|
||||||
up -d 2>/dev/null || \
|
--profile infra-mysql --profile infra-redis \
|
||||||
docker compose \
|
--profile im --profile push --profile update --profile license \
|
||||||
--env-file "$ROOT_DIR/.env" \
|
|
||||||
-f "$ROOT_DIR/docker-compose.yml" \
|
|
||||||
--profile base --profile infra-mysql --profile infra-redis \
|
|
||||||
up -d
|
up -d
|
||||||
|
|
||||||
# 等待 tenant-service 健康(最多 120 秒)
|
# 等待 tenant-service 健康(最多 120 秒)
|
||||||
printf ' 等待 tenant-service 启动'
|
printf ' 等待 tenant-service 启动'
|
||||||
for i in $(seq 1 40); do
|
for i in $(seq 1 40); do
|
||||||
code="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 4 \
|
code="$(curl -skL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 4 \
|
||||||
"http://${DEPLOY_HOST}/actuator/health" 2>/dev/null || echo 000)"
|
"http://${DEPLOY_HOST}/actuator/health" 2>/dev/null || echo 000)"
|
||||||
if [ "$code" = "200" ]; then
|
if [ "$code" = "200" ]; then
|
||||||
printf '\n'
|
printf '\n'
|
||||||
@ -429,23 +690,127 @@ docker compose \
|
|||||||
--env-file "$ROOT_DIR/.env" \
|
--env-file "$ROOT_DIR/.env" \
|
||||||
-f "$ROOT_DIR/docker-compose.yml" \
|
-f "$ROOT_DIR/docker-compose.yml" \
|
||||||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||||||
ps --format 'table {{.Name}}\t{{.Status}}' 2>/dev/null \
|
ps --format 'table {{.Name}}\t{{.Status}}' 2>/dev/null || \
|
||||||
|| docker ps --format ' {{.Names}}\t{{.Status}}' | grep -E "xuqm|mysql|redis" || true
|
docker ps --format ' {{.Names}}\t{{.Status}}' | grep -E "xuqm|mysql|redis" || true
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 6 — 迁移数字医信租户
|
# Step 5b — Schema 扩展 + 系统应用创建(在迁移之前,Hibernate 已建表后执行)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
step "迁移数字医信租户数据(从生产 MySQL 导入)"
|
step "Schema 扩展 + 系统 IM 应用初始化"
|
||||||
|
|
||||||
# 检查是否已迁移(目标 DB 中是否已有数字医信的租户记录)
|
MYSQL_CTR_TMP="$(docker ps -qf name=mysql | head -1)"
|
||||||
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
|
# 检查并添加列(兼容 MySQL 8.x,不依赖 ADD COLUMN IF NOT EXISTS)
|
||||||
ok "数字医信租户已存在于私有化 DB,跳过迁移"
|
for _col in is_default deletable; do
|
||||||
|
_exists="$(docker exec "$MYSQL_CTR_TMP" mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
-N -e "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='t_app' AND COLUMN_NAME='${_col}'" 2>/dev/null || echo 0)"
|
||||||
|
if [ "${_exists:-0}" -eq 0 ]; then
|
||||||
|
_dv="$( [ "$_col" = "is_default" ] && echo 0 || echo 1 )"
|
||||||
|
docker exec "$MYSQL_CTR_TMP" mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
-e "ALTER TABLE t_app ADD COLUMN ${_col} BIT(1) NOT NULL DEFAULT ${_dv}" 2>/dev/null \
|
||||||
|
&& ok "列 ${_col} 已添加" || warn "列 ${_col} 添加失败,继续"
|
||||||
else
|
else
|
||||||
|
ok "列 ${_col} 已存在,跳过"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 触发器:先删再建,使用 --delimiter='$$'(MySQL 批量模式不支持 DELIMITER 语句)
|
||||||
|
docker exec "$MYSQL_CTR_TMP" mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
-e "DROP TRIGGER IF EXISTS prevent_default_app_delete" 2>/dev/null
|
||||||
|
|
||||||
|
_TRIG_FILE="$(mktemp /tmp/xuqm-trig-XXXXXX.sql)"
|
||||||
|
cat > "$_TRIG_FILE" << 'TRIG_SQL'
|
||||||
|
CREATE TRIGGER prevent_default_app_delete
|
||||||
|
BEFORE DELETE ON t_app
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
IF OLD.is_default = 1 THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete default application';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
TRIG_SQL
|
||||||
|
if docker exec -i "$MYSQL_CTR_TMP" mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
--delimiter='$$' < "$_TRIG_FILE" 2>/dev/null; then
|
||||||
|
ok "删除保护触发器已创建"
|
||||||
|
else
|
||||||
|
warn "触发器创建遇到警告,继续"
|
||||||
|
fi
|
||||||
|
rm -f "$_TRIG_FILE"
|
||||||
|
|
||||||
|
# 创建系统 IM 应用(bootstrap 阶段,迁移会重建;此处保证无迁移时也可用)
|
||||||
|
_SYS_APP_KEY="ak_409e217e4aa14254ad73ad3c"
|
||||||
|
_SYS_EXIST="$(docker exec "$MYSQL_CTR_TMP" \
|
||||||
|
mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
-N -e "SELECT COUNT(*) FROM t_app WHERE app_key='${_SYS_APP_KEY}'" 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
if [ "${_SYS_EXIST:-0}" -eq 0 ]; then
|
||||||
|
_SYS_SQL="$(mktemp /tmp/xuqm-sysapp-XXXXXX.sql)"
|
||||||
|
cat > "$_SYS_SQL" << 'SYSAPP_SQL'
|
||||||
|
INSERT IGNORE INTO t_app (id, app_key, app_secret, created_at, description, name, package_name, tenant_id)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c',
|
||||||
|
CONCAT('as_sys_', LEFT(MD5(UUID()), 16)),
|
||||||
|
NOW(),
|
||||||
|
'系统内置应用 — 私有化服务间 IM 通信专用,is_default=1,不可删除',
|
||||||
|
'平台系统应用', 'com.xuqmgroup.platform',
|
||||||
|
id
|
||||||
|
FROM t_tenant ORDER BY created_at LIMIT 1;
|
||||||
|
|
||||||
|
INSERT INTO t_feature_service (id, app_key, created_at, enabled, platform, secret_key, service_type)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c', NOW(), 1, 'ANDROID', CONCAT('sk_sys_a_', LEFT(MD5(UUID()), 8)), 'IM'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM t_feature_service WHERE app_key='ak_409e217e4aa14254ad73ad3c' AND platform='ANDROID' AND service_type='IM');
|
||||||
|
|
||||||
|
INSERT INTO t_feature_service (id, app_key, created_at, enabled, platform, secret_key, service_type)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c', NOW(), 1, 'IOS', CONCAT('sk_sys_i_', LEFT(MD5(UUID()), 8)), 'IM'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM t_feature_service WHERE app_key='ak_409e217e4aa14254ad73ad3c' AND platform='IOS' AND service_type='IM');
|
||||||
|
|
||||||
|
INSERT INTO t_feature_service (id, app_key, created_at, enabled, platform, secret_key, service_type)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c', NOW(), 1, 'HARMONY', CONCAT('sk_sys_h_', LEFT(MD5(UUID()), 8)), 'IM'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM t_feature_service WHERE app_key='ak_409e217e4aa14254ad73ad3c' AND platform='HARMONY' AND service_type='IM');
|
||||||
|
|
||||||
|
UPDATE t_app SET is_default=1, deletable=0 WHERE app_key='ak_409e217e4aa14254ad73ad3c';
|
||||||
|
SYSAPP_SQL
|
||||||
|
docker exec -i "$MYSQL_CTR_TMP" \
|
||||||
|
mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
--default-character-set=utf8mb4 < "$_SYS_SQL" 2>/dev/null || warn "系统应用创建遇到警告,继续"
|
||||||
|
rm -f "$_SYS_SQL"
|
||||||
|
ok "系统应用 ${_SYS_APP_KEY} 已创建(IM 三端启用,is_default=1)"
|
||||||
|
else
|
||||||
|
ok "系统应用 ${_SYS_APP_KEY} 已存在,跳过创建"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 6 — 租户初始化(新建 / 迁移)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
step "租户初始化(模式: ${DEPLOY_MODE})"
|
||||||
|
|
||||||
|
MYSQL_CTR="$(docker ps -qf name=mysql | head -1)"
|
||||||
|
|
||||||
|
if [ "$DEPLOY_MODE" = "new" ]; then
|
||||||
|
# ── 新建租户:bootstrap 已在容器启动时执行,验证租户记录即可 ──
|
||||||
|
_NEW_TENANT_CNT="$(docker exec "$MYSQL_CTR" \
|
||||||
|
mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
-N -e "SELECT COUNT(*) FROM t_tenant WHERE email='${DEPLOY_TENANT_EMAIL}'" 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
if [ "${_NEW_TENANT_CNT:-0}" -ge 1 ]; then
|
||||||
|
ok "租户 ${DEPLOY_TENANT_EMAIL} 已由 bootstrap 创建"
|
||||||
|
else
|
||||||
|
warn "租户记录暂未就绪(tenant-service 可能仍在初始化),继续部署"
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
# ── 迁移租户:检查是否有现有数据,必要时警告确认后执行迁移 ──
|
||||||
|
_EXIST_CNT="$(docker exec "$MYSQL_CTR" \
|
||||||
|
mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||||||
|
-N -e "SELECT COUNT(*) FROM t_tenant" 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
if [ "${_EXIST_CNT:-0}" -ge 1 ]; then
|
||||||
|
printf '\n \033[1;31m⚠ 警告:数据库中已存在租户数据!\033[0m\n'
|
||||||
|
printf ' 迁移操作将 \033[1;31m删除所有现有租户信息\033[0m(账号、应用、功能配置),\n'
|
||||||
|
printf ' 并替换为生产环境租户 \033[1m%s\033[0m 的数据。\n\n' "$DEPLOY_TENANT_EMAIL"
|
||||||
|
read -rp " 确认删除并继续迁移?请输入 yes: " _confirm
|
||||||
|
[ "$_confirm" = "yes" ] || fail "操作已取消"
|
||||||
|
fi
|
||||||
|
|
||||||
printf ' 连通性检查生产 MySQL %s ...\n' "$SRC_HOST"
|
printf ' 连通性检查生产 MySQL %s ...\n' "$SRC_HOST"
|
||||||
MYSQL_PWD="$SRC_PASSWORD" mysql \
|
MYSQL_PWD="$SRC_PASSWORD" mysql \
|
||||||
-h "$SRC_HOST" -P "$SRC_PORT" -u "$SRC_USER" \
|
-h "$SRC_HOST" -P "$SRC_PORT" -u "$SRC_USER" \
|
||||||
@ -460,84 +825,60 @@ else
|
|||||||
--src-user "$SRC_USER" \
|
--src-user "$SRC_USER" \
|
||||||
--src-password "$SRC_PASSWORD" \
|
--src-password "$SRC_PASSWORD" \
|
||||||
--src-db "$SRC_DB" \
|
--src-db "$SRC_DB" \
|
||||||
--tenant "$TENANT_EMAIL"
|
--tenant "$DEPLOY_TENANT_EMAIL"
|
||||||
|
|
||||||
|
# 迁移完成后更新 bootstrap.env,防止重启时重新创建占位租户
|
||||||
|
cat > "$ROOT_DIR/config/tenant/bootstrap.env" <<EOF
|
||||||
|
TENANT_BOOTSTRAP_EMAIL=${DEPLOY_TENANT_EMAIL}
|
||||||
|
TENANT_BOOTSTRAP_USERNAME=${DEPLOY_TENANT_EMAIL}
|
||||||
|
TENANT_BOOTSTRAP_PASSWORD=already-migrated-do-not-use
|
||||||
|
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||||||
|
EOF
|
||||||
|
chmod 600 "$ROOT_DIR/config/tenant/bootstrap.env"
|
||||||
|
ok "bootstrap.env 已更新(迁移后)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 7 — 最终验收
|
# Step 7 — 一键验证(调用 verify.sh)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
step "最终验收"
|
step "一键验证所有服务"
|
||||||
|
|
||||||
PASS=0
|
# 将 DEPLOY_HOST 注入到 verify.sh 中使用
|
||||||
FAIL=0
|
export DEPLOY_HOST="${DEPLOY_HOST}"
|
||||||
|
|
||||||
check() {
|
if bash "$ROOT_DIR/scripts/verify.sh"; then
|
||||||
local label="$1" actual="$2" expected="$3"
|
ok "全量验证通过"
|
||||||
if printf '%s' "$actual" | grep -q "$expected"; then
|
|
||||||
ok "PASS $label"
|
|
||||||
PASS=$((PASS+1))
|
|
||||||
else
|
else
|
||||||
warn "FAIL $label (got: $actual)"
|
printf '\n\033[33m 部分验证项未通过,请查看上方输出。\033[0m\n'
|
||||||
FAIL=$((FAIL+1))
|
printf ' 可重新运行:DEPLOY_HOST=%s bash %s/scripts/verify.sh\n' "$DEPLOY_HOST" "$ROOT_DIR"
|
||||||
fi
|
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 '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||||
printf '\033[1;35m 验收结果: %d PASS / %d FAIL\033[0m\n' "$PASS" "$FAIL"
|
printf '\033[1;35m %s 私有化部署完成\033[0m\n' "$CUSTOMER_NAME"
|
||||||
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||||||
|
|
||||||
printf '\n \033[1m访问地址:\033[0m %s\n' "${CONSOLE_BASE}"
|
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/ops\n' "${CONSOLE_BASE}"
|
||||||
printf ' \033[1m租户账号:\033[0m %s\n' "${TENANT_EMAIL}"
|
printf ' \033[1m文档站 :\033[0m %s/docs/\n' "${CONSOLE_BASE}"
|
||||||
printf ' \033[1m租户用户名:\033[0m %s\n' "${TENANT_USERNAME}"
|
printf '\n \033[1m租户信息:\033[0m\n'
|
||||||
printf ' \033[1m密码:\033[0m 同公有化平台密码(未重置)\n'
|
printf ' 邮箱: %s\n' "${DEPLOY_TENANT_EMAIL}"
|
||||||
printf '\n \033[1m应用列表:\033[0m\n'
|
printf ' 用户名: %s\n' "${DEPLOY_TENANT_USERNAME}"
|
||||||
printf ' 医网信 ak_c6fce237cae94ef5ab71fda6\n'
|
if [ "$DEPLOY_MODE" = "new" ]; then
|
||||||
printf ' 临床知识库 ak_1178fd37b8f54cefb7031744\n'
|
printf ' 密码: 部署时设置的密码\n'
|
||||||
printf '\n \033[1m部署目录:\033[0m %s\n' "$ROOT_DIR"
|
else
|
||||||
printf ' \033[1m审计日志:\033[0m %s/logs/audit.log\n' "$ROOT_DIR"
|
printf ' 密码: 同生产平台密码(原样迁移,未重置)\n'
|
||||||
|
|
||||||
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
|
fi
|
||||||
|
printf '\n \033[1m初始化方式:\033[0m%s\n' "${DEPLOY_MODE}"
|
||||||
|
printf '\n \033[1m服务状态:\033[0m\n'
|
||||||
|
printf ' 控制台 %s\n' "${CONSOLE_BASE}"
|
||||||
|
printf ' IM 服务 %s/api/im/\n' "${CONSOLE_BASE}"
|
||||||
|
printf ' 版本管理 %s/api/v1/updates/\n' "${CONSOLE_BASE}"
|
||||||
|
printf ' 文件服务 %s/file/\n' "${CONSOLE_BASE}"
|
||||||
|
printf ' License 服务 已启动(内部调用)\n'
|
||||||
|
printf ' 推送服务 已启动(厂商凭据见 config/vendors/push.env)\n'
|
||||||
|
printf '\n \033[1m部署目录:\033[0m %s\n' "$ROOT_DIR"
|
||||||
|
printf ' \033[1m验证结果:\033[0m %s/.deploy-state/last-verify.json\n' "$ROOT_DIR"
|
||||||
|
printf ' \033[1m审计日志:\033[0m %s/logs/audit.log\n' "$ROOT_DIR"
|
||||||
printf '\n\033[1;32m 部署成功!\033[0m\n\n'
|
printf '\n\033[1;32m 部署成功!\033[0m\n\n'
|
||||||
|
|||||||
@ -165,20 +165,20 @@ SQL_FILE="$(mktemp /tmp/xuqm-migrate-XXXXXX.sql)"
|
|||||||
trap 'rm -f "$SQL_FILE"' EXIT
|
trap 'rm -f "$SQL_FILE"' EXIT
|
||||||
|
|
||||||
{
|
{
|
||||||
printf '-- XuqmGroup private deployment tenant migration\n'
|
printf -- '-- XuqmGroup private deployment tenant migration\n'
|
||||||
printf '-- Source: %s/%s Tenant: %s (%s)\n' "$SRC_HOST" "$SRC_DB" "$TENANT_NICKNAME" "$TENANT_ID"
|
printf -- '-- Source: %s/%s Tenant: %s (%s)\n' "$SRC_HOST" "$SRC_DB" "$TENANT_NICKNAME" "$TENANT_ID"
|
||||||
printf '-- Generated: %s\n\n' "$(now)"
|
printf -- '-- Generated: %s\n\n' "$(now)"
|
||||||
|
|
||||||
printf 'SET FOREIGN_KEY_CHECKS=0;\n\n'
|
printf 'SET FOREIGN_KEY_CHECKS=0;\n\n'
|
||||||
|
|
||||||
# Clear existing data (private deployment is single-tenant)
|
# Clear existing data (private deployment is single-tenant)
|
||||||
printf '-- Clear existing tenant data\n'
|
printf -- '-- Clear existing tenant data\n'
|
||||||
printf 'DELETE FROM t_feature_service;\n'
|
printf 'DELETE FROM t_feature_service;\n'
|
||||||
printf 'DELETE FROM t_app;\n'
|
printf 'DELETE FROM t_app;\n'
|
||||||
printf 'DELETE FROM t_tenant;\n\n'
|
printf 'DELETE FROM t_tenant;\n\n'
|
||||||
|
|
||||||
# t_tenant — use explicit column names to be schema-order agnostic
|
# t_tenant — use explicit column names to be schema-order agnostic
|
||||||
printf '-- Tenant record\n'
|
printf -- '-- Tenant record\n'
|
||||||
src_mysql -N -e "
|
src_mysql -N -e "
|
||||||
SELECT CONCAT(
|
SELECT CONCAT(
|
||||||
'INSERT INTO t_tenant (id,created_at,email,nickname,parent_id,password,phone,status,type,username) VALUES (',
|
'INSERT INTO t_tenant (id,created_at,email,nickname,parent_id,password,phone,status,type,username) VALUES (',
|
||||||
@ -191,7 +191,7 @@ trap 'rm -f "$SQL_FILE"' EXIT
|
|||||||
|
|
||||||
# t_app — explicit columns matching private schema (alphabetical: id,app_key,...,tenant_id)
|
# t_app — explicit columns matching private schema (alphabetical: id,app_key,...,tenant_id)
|
||||||
if [ "$APP_COUNT" -gt 0 ]; then
|
if [ "$APP_COUNT" -gt 0 ]; then
|
||||||
printf '-- Apps\n'
|
printf -- '-- Apps\n'
|
||||||
src_mysql -N -e "
|
src_mysql -N -e "
|
||||||
SELECT CONCAT(
|
SELECT CONCAT(
|
||||||
'INSERT INTO t_app (id,app_key,app_secret,created_at,description,harmony_bundle_name,icon_url,ios_bundle_id,name,package_name,tenant_id) VALUES (',
|
'INSERT INTO t_app (id,app_key,app_secret,created_at,description,harmony_bundle_name,icon_url,ios_bundle_id,name,package_name,tenant_id) VALUES (',
|
||||||
@ -205,7 +205,7 @@ trap 'rm -f "$SQL_FILE"' EXIT
|
|||||||
|
|
||||||
# t_feature_service — explicit columns matching private schema (alphabetical: id,app_key,config,...)
|
# t_feature_service — explicit columns matching private schema (alphabetical: id,app_key,config,...)
|
||||||
if [ "$FS_COUNT" -gt 0 ]; then
|
if [ "$FS_COUNT" -gt 0 ]; then
|
||||||
printf '-- Feature services\n'
|
printf -- '-- Feature services\n'
|
||||||
src_mysql -N -e "
|
src_mysql -N -e "
|
||||||
SELECT CONCAT(
|
SELECT CONCAT(
|
||||||
'INSERT INTO t_feature_service (id,app_key,config,created_at,enabled,platform,secret_key,service_type) VALUES (',
|
'INSERT INTO t_feature_service (id,app_key,config,created_at,enabled,platform,secret_key,service_type) VALUES (',
|
||||||
@ -217,13 +217,13 @@ trap 'rm -f "$SQL_FILE"' EXIT
|
|||||||
|
|
||||||
# Optional password reset via bcrypt (requires htpasswd or python3 available on the deployment host)
|
# Optional password reset via bcrypt (requires htpasswd or python3 available on the deployment host)
|
||||||
if [ -n "$RESET_PASSWORD" ]; then
|
if [ -n "$RESET_PASSWORD" ]; then
|
||||||
printf '-- Password reset\n'
|
printf -- '-- Password reset\n'
|
||||||
if command -v python3 >/dev/null 2>&1 && python3 -c "import bcrypt" 2>/dev/null; then
|
if command -v python3 >/dev/null 2>&1 && python3 -c "import bcrypt" 2>/dev/null; then
|
||||||
BCRYPT_HASH="$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'${RESET_PASSWORD}', bcrypt.gensalt(rounds=10)).decode())")"
|
BCRYPT_HASH="$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'${RESET_PASSWORD}', bcrypt.gensalt(rounds=10)).decode())")"
|
||||||
elif command -v htpasswd >/dev/null 2>&1; then
|
elif command -v htpasswd >/dev/null 2>&1; then
|
||||||
BCRYPT_HASH="$(htpasswd -bnBC 10 '' "${RESET_PASSWORD}" | tr -d ':\n' | sed 's/\$2y/\$2a/')"
|
BCRYPT_HASH="$(htpasswd -bnBC 10 '' "${RESET_PASSWORD}" | tr -d ':\n' | sed 's/\$2y/\$2a/')"
|
||||||
else
|
else
|
||||||
printf '-- WARNING: cannot generate bcrypt hash (install python3-bcrypt or apache2-utils)\n'
|
printf -- '-- WARNING: cannot generate bcrypt hash (install python3-bcrypt or apache2-utils)\n'
|
||||||
BCRYPT_HASH=""
|
BCRYPT_HASH=""
|
||||||
fi
|
fi
|
||||||
if [ -n "$BCRYPT_HASH" ]; then
|
if [ -n "$BCRYPT_HASH" ]; then
|
||||||
@ -232,7 +232,7 @@ trap 'rm -f "$SQL_FILE"' EXIT
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'SET FOREIGN_KEY_CHECKS=1;\n'
|
printf 'SET FOREIGN_KEY_CHECKS=1;\n'
|
||||||
printf '-- Migration SQL complete (%s apps, %s feature services)\n' "$APP_COUNT" "$FS_COUNT"
|
printf -- '-- Migration SQL complete (%s apps, %s feature services)\n' "$APP_COUNT" "$FS_COUNT"
|
||||||
} > "$SQL_FILE"
|
} > "$SQL_FILE"
|
||||||
|
|
||||||
LINE_COUNT="$(wc -l < "$SQL_FILE")"
|
LINE_COUNT="$(wc -l < "$SQL_FILE")"
|
||||||
@ -256,6 +256,102 @@ dst_mysql < "$SQL_FILE"
|
|||||||
audit "migrate-tenant" "SQL_APPLIED" "tenant=$TENANT_ID apps=$APP_COUNT fs=$FS_COUNT"
|
audit "migrate-tenant" "SQL_APPLIED" "tenant=$TENANT_ID apps=$APP_COUNT fs=$FS_COUNT"
|
||||||
printf ' Done.\n'
|
printf ' Done.\n'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 4b — Schema 扩展:is_default / deletable 列 + 删除保护触发器
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n[3b/5] 应用 Schema 扩展(is_default / deletable / 删除保护触发器)...\n'
|
||||||
|
|
||||||
|
_SCHEMA_EXT_FILE="$(mktemp /tmp/xuqm-schema-ext-XXXXXX.sql)"
|
||||||
|
|
||||||
|
# 用 information_schema 检查列是否存在,再决定是否 ALTER TABLE(兼容所有 MySQL 8.x)
|
||||||
|
for _col in is_default deletable; do
|
||||||
|
_exists="$(dst_mysql -N -e "
|
||||||
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='t_app' AND COLUMN_NAME='${_col}'" 2>/dev/null || echo 0)"
|
||||||
|
if [ "${_exists:-0}" -eq 0 ]; then
|
||||||
|
_default_val="$( [ "$_col" = "is_default" ] && echo 0 || echo 1 )"
|
||||||
|
dst_mysql -e "ALTER TABLE t_app ADD COLUMN ${_col} BIT(1) NOT NULL DEFAULT ${_default_val}" 2>/dev/null \
|
||||||
|
&& printf ' 列 %s 已添加\n' "$_col" \
|
||||||
|
|| warn "列 ${_col} 添加失败,继续"
|
||||||
|
else
|
||||||
|
printf ' 列 %s 已存在,跳过\n' "$_col"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 触发器:用 --delimiter='$$' 创建(MySQL 批量模式不支持 DELIMITER 语句)
|
||||||
|
dst_mysql -e "DROP TRIGGER IF EXISTS prevent_default_app_delete" 2>/dev/null
|
||||||
|
cat > "$_SCHEMA_EXT_FILE" << 'TRIG_SQL'
|
||||||
|
CREATE TRIGGER prevent_default_app_delete
|
||||||
|
BEFORE DELETE ON t_app
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
IF OLD.is_default = 1 THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete default application';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
TRIG_SQL
|
||||||
|
|
||||||
|
if dst_mysql --delimiter='$$' < "$_SCHEMA_EXT_FILE" 2>/dev/null; then
|
||||||
|
printf ' 删除保护触发器已创建\n'
|
||||||
|
else
|
||||||
|
warn "触发器创建遇到警告,继续"
|
||||||
|
fi
|
||||||
|
rm -f "$_SCHEMA_EXT_FILE"
|
||||||
|
|
||||||
|
# 创建系统 IM 应用(固定 app_key,私有化服务间通信专用)
|
||||||
|
# 与公有化平台的 ak_409e217e4aa14254ad73ad3c 保持相同 key,services 通过 SYSTEM_APP_KEY 读取
|
||||||
|
_SYS_APP_KEY="ak_409e217e4aa14254ad73ad3c"
|
||||||
|
_SYS_APP_EXISTS="$(dst_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE app_key='${_SYS_APP_KEY}'" 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
if [ "${_SYS_APP_EXISTS:-0}" -eq 0 ]; then
|
||||||
|
_SYS_SQL="$(mktemp /tmp/xuqm-sysapp-XXXXXX.sql)"
|
||||||
|
cat > "$_SYS_SQL" << SYSAPP_SQL
|
||||||
|
-- 系统 IM 应用:置于迁移后的唯一租户下
|
||||||
|
INSERT IGNORE INTO t_app (id, app_key, app_secret, created_at, description, name, package_name, tenant_id)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c',
|
||||||
|
CONCAT('as_sys_', LEFT(MD5(UUID()), 16)),
|
||||||
|
NOW(),
|
||||||
|
'系统内置应用 — 私有化服务间 IM 通信专用,is_default=1,不可删除',
|
||||||
|
'平台系统应用', 'com.xuqmgroup.platform',
|
||||||
|
id
|
||||||
|
FROM t_tenant ORDER BY created_at LIMIT 1;
|
||||||
|
|
||||||
|
-- 为系统应用启用三端 IM(NOT EXISTS 保证幂等)
|
||||||
|
INSERT INTO t_feature_service (id, app_key, created_at, enabled, platform, secret_key, service_type)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c', NOW(), 1, 'ANDROID', CONCAT('sk_sys_a_', LEFT(MD5(UUID()), 8)), 'IM'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM t_feature_service WHERE app_key='ak_409e217e4aa14254ad73ad3c' AND platform='ANDROID' AND service_type='IM');
|
||||||
|
|
||||||
|
INSERT INTO t_feature_service (id, app_key, created_at, enabled, platform, secret_key, service_type)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c', NOW(), 1, 'IOS', CONCAT('sk_sys_i_', LEFT(MD5(UUID()), 8)), 'IM'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM t_feature_service WHERE app_key='ak_409e217e4aa14254ad73ad3c' AND platform='IOS' AND service_type='IM');
|
||||||
|
|
||||||
|
INSERT INTO t_feature_service (id, app_key, created_at, enabled, platform, secret_key, service_type)
|
||||||
|
SELECT UUID(), 'ak_409e217e4aa14254ad73ad3c', NOW(), 1, 'HARMONY', CONCAT('sk_sys_h_', LEFT(MD5(UUID()), 8)), 'IM'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM t_feature_service WHERE app_key='ak_409e217e4aa14254ad73ad3c' AND platform='HARMONY' AND service_type='IM');
|
||||||
|
SYSAPP_SQL
|
||||||
|
if dst_mysql < "$_SYS_SQL" 2>/dev/null; then
|
||||||
|
printf ' 系统应用 %s 已创建(IM 三端已启用)\n' "$_SYS_APP_KEY"
|
||||||
|
else
|
||||||
|
warn "系统应用创建遇到警告,继续"
|
||||||
|
fi
|
||||||
|
rm -f "$_SYS_SQL"
|
||||||
|
else
|
||||||
|
printf ' 系统应用 %s 已存在,跳过创建\n' "$_SYS_APP_KEY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 标记系统应用为 is_default=1 / deletable=0(schema 扩展已在上步添加列)
|
||||||
|
dst_mysql -e "UPDATE t_app SET is_default=1, deletable=0 WHERE app_key='${_SYS_APP_KEY}'" 2>/dev/null
|
||||||
|
printf ' %s → is_default=1 / deletable=0,DB 触发器保护已激活\n' "$_SYS_APP_KEY"
|
||||||
|
|
||||||
|
# 将 SYSTEM_APP_KEY 追加到 xuqm.env(若未配置)
|
||||||
|
_XUQM_ENV_FILE="$ROOT_DIR/config/xuqm.env"
|
||||||
|
if [ -f "$_XUQM_ENV_FILE" ] && ! grep -q "^SYSTEM_APP_KEY=" "$_XUQM_ENV_FILE"; then
|
||||||
|
printf '\n# 系统 IM 通信应用 key(私有化服务间消息通知使用此 app_key 连接 IM 服务)\nSYSTEM_APP_KEY=%s\n' \
|
||||||
|
"$_SYS_APP_KEY" >> "$_XUQM_ENV_FILE"
|
||||||
|
printf ' SYSTEM_APP_KEY=%s 已追加到 config/xuqm.env\n' "$_SYS_APP_KEY"
|
||||||
|
fi
|
||||||
|
audit "migrate-tenant" "SYSTEM_APP_CREATED" "app_key=$_SYS_APP_KEY tenant=$TENANT_ID"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 5 — Restart tenant-service to flush any cached state
|
# Step 5 — Restart tenant-service to flush any cached state
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -330,3 +426,19 @@ printf ' Login URL: %s\n' "${BASE_URL:-http://localhost}"
|
|||||||
|
|
||||||
audit "migrate-tenant" "DONE" "tenant=$TENANT_ID apps=$DST_APPS"
|
audit "migrate-tenant" "DONE" "tenant=$TENANT_ID apps=$DST_APPS"
|
||||||
progress "migrate-tenant" "DONE" "tenant=$TENANT_NICKNAME email=$TENANT_EMAIL apps=$DST_APPS"
|
progress "migrate-tenant" "DONE" "tenant=$TENANT_NICKNAME email=$TENANT_EMAIL apps=$DST_APPS"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 6 — 全量验证(调用 verify.sh)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n[6/5] 运行全量验证脚本 ...\n'
|
||||||
|
VERIFY_SCRIPT="$ROOT_DIR/scripts/verify.sh"
|
||||||
|
if [ -f "$VERIFY_SCRIPT" ]; then
|
||||||
|
if bash "$VERIFY_SCRIPT"; then
|
||||||
|
printf ' 全量验证通过。\n'
|
||||||
|
else
|
||||||
|
printf ' 部分验证项未通过,请查看上方输出。\n'
|
||||||
|
printf ' 可重新运行:bash %s\n' "$VERIFY_SCRIPT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf ' verify.sh 未找到,跳过全量验证。\n'
|
||||||
|
fi
|
||||||
|
|||||||
444
scripts/verify.sh
普通文件
444
scripts/verify.sh
普通文件
@ -0,0 +1,444 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify.sh — 私有化部署一键验证脚本
|
||||||
|
#
|
||||||
|
# 用途:
|
||||||
|
# 部署完成后,全面验证所有服务是否正常运行,输出通过/失败清单。
|
||||||
|
# 可独立运行,也可作为 deploy-szyx.sh 和 migrate-tenant.sh 的最终步骤。
|
||||||
|
#
|
||||||
|
# 使用方法:
|
||||||
|
# # 从部署目录读取配置自动推断
|
||||||
|
# ./scripts/verify.sh
|
||||||
|
#
|
||||||
|
# # 指定部署地址(可覆盖 .env 中的值)
|
||||||
|
# DEPLOY_HOST=192.168.1.100 ./scripts/verify.sh
|
||||||
|
#
|
||||||
|
# # 指定完整 URL
|
||||||
|
# BASE_URL=http://192.168.1.100 ./scripts/verify.sh
|
||||||
|
#
|
||||||
|
# 验证项目:
|
||||||
|
# 1. Docker 容器运行状态
|
||||||
|
# 2. 数据库和 Redis 连通性
|
||||||
|
# 3. tenant-service 健康(actuator/health)
|
||||||
|
# 4. PRIVATE 模式激活
|
||||||
|
# 5. 注册接口已阻断
|
||||||
|
# 6. 前端页面可访问
|
||||||
|
# 7. SDK 配置接口
|
||||||
|
# 8. IM 服务(如已启动)
|
||||||
|
# 9. 版本管理服务(如已启动)
|
||||||
|
# 10. 文档站(如已启动)
|
||||||
|
# 11. 租户登录和中文字符集
|
||||||
|
#
|
||||||
|
# 退出码:
|
||||||
|
# 0 = 全部通过
|
||||||
|
# 1 = 有项目失败
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 配置读取
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# 优先使用环境变量,否则从 .env 读取
|
||||||
|
if [ -f "$ROOT_DIR/.env" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a; . "$ROOT_DIR/.env" 2>/dev/null; set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_HOST="${DEPLOY_HOST:-127.0.0.1}"
|
||||||
|
BASE_URL="${BASE_URL:-http://${DEPLOY_HOST}}"
|
||||||
|
# 去掉末尾斜杠
|
||||||
|
BASE_URL="${BASE_URL%/}"
|
||||||
|
|
||||||
|
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 颜色和工具函数
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
RED='\033[1;31m'; GREEN='\033[1;32m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[1;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||||
|
|
||||||
|
PASS=0; FAIL=0; WARN=0
|
||||||
|
RESULTS=() # 收集结果用于最终报告
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
local msg="$1"
|
||||||
|
printf " ${GREEN}✓${RESET} PASS %s\n" "$msg"
|
||||||
|
PASS=$((PASS+1))
|
||||||
|
RESULTS+=("PASS|$msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
local msg="$1" detail="${2:-}"
|
||||||
|
printf " ${RED}✗${RESET} FAIL %s" "$msg"
|
||||||
|
[ -n "$detail" ] && printf " ${YELLOW}(%s)${RESET}" "$detail"
|
||||||
|
printf "\n"
|
||||||
|
FAIL=$((FAIL+1))
|
||||||
|
RESULTS+=("FAIL|$msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
local msg="$1" detail="${2:-}"
|
||||||
|
printf " ${YELLOW}⚠${RESET} WARN %s" "$msg"
|
||||||
|
[ -n "$detail" ] && printf " (%s)" "$detail"
|
||||||
|
printf "\n"
|
||||||
|
WARN=$((WARN+1))
|
||||||
|
RESULTS+=("WARN|$msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
section() {
|
||||||
|
printf "\n${CYAN}── %s ${RESET}\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP 请求(自动跳过代理,直连内网)
|
||||||
|
http_get() {
|
||||||
|
local url="$1" timeout="${2:-8}"
|
||||||
|
curl -sL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time "$timeout" "$url" 2>/dev/null || echo 000
|
||||||
|
}
|
||||||
|
|
||||||
|
http_body() {
|
||||||
|
local url="$1" timeout="${2:-8}"
|
||||||
|
curl -sL --noproxy '*' --max-time "$timeout" "$url" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
http_body_auth() {
|
||||||
|
local url="$1" token="$2" timeout="${3:-8}"
|
||||||
|
curl -sL --noproxy '*' --max-time "$timeout" \
|
||||||
|
-H "Authorization: Bearer $token" "$url" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
http_code_auth() {
|
||||||
|
local url="$1" token="$2" timeout="${3:-8}"
|
||||||
|
curl -sL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time "$timeout" \
|
||||||
|
-H "Authorization: Bearer $token" "$url" 2>/dev/null || echo 000
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 开始验证
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf "\n${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||||||
|
printf "${BOLD} XuqmGroup 私有化部署验证${RESET}\n"
|
||||||
|
printf "${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||||||
|
printf " 部署地址: ${BOLD}%s${RESET}\n" "$BASE_URL"
|
||||||
|
printf " 部署目录: %s\n" "$ROOT_DIR"
|
||||||
|
printf " 验证时间: %s\n" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Docker 容器状态
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
section "1. 容器状态"
|
||||||
|
|
||||||
|
REQUIRED_CONTAINERS="tenant-service nginx tenant-web ops-web"
|
||||||
|
for CTR_SUFFIX in $REQUIRED_CONTAINERS; do
|
||||||
|
CTR_NAME=$(docker ps --filter "name=$CTR_SUFFIX" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||||||
|
if [ -n "$CTR_NAME" ]; then
|
||||||
|
pass "容器运行中: $CTR_NAME"
|
||||||
|
else
|
||||||
|
fail "容器未运行: *-${CTR_SUFFIX}-*"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
OPTIONAL_CONTAINERS="file-service im-service push-service update-service license-service docs-site mysql redis"
|
||||||
|
for CTR_SUFFIX in $OPTIONAL_CONTAINERS; do
|
||||||
|
CTR_NAME=$(docker ps --filter "name=$CTR_SUFFIX" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||||||
|
if [ -n "$CTR_NAME" ]; then
|
||||||
|
pass "容器运行中: $CTR_NAME"
|
||||||
|
else
|
||||||
|
warn "容器未运行(可选): *-${CTR_SUFFIX}-*"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. 数据库和 Redis
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
section "2. 数据库和缓存"
|
||||||
|
|
||||||
|
# MySQL
|
||||||
|
MYSQL_CTR=$(docker ps --filter "name=mysql" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||||||
|
if [ -n "$MYSQL_CTR" ]; then
|
||||||
|
if docker exec "$MYSQL_CTR" mysqladmin -u root -p"${MYSQL_ROOT_PASSWORD:-}" ping --silent 2>/dev/null; then
|
||||||
|
pass "MySQL 响应 PONG"
|
||||||
|
else
|
||||||
|
fail "MySQL ping 失败"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "MySQL 容器未运行(external 模式跳过此项)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_CTR=$(docker ps --filter "name=redis" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||||||
|
if [ -n "$REDIS_CTR" ]; then
|
||||||
|
PING=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning PING 2>/dev/null | tr -d '\r')
|
||||||
|
if [ "$PING" = "PONG" ]; then
|
||||||
|
pass "Redis 响应 PONG"
|
||||||
|
else
|
||||||
|
fail "Redis ping 失败 (got: $PING)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Redis 容器未运行(external 模式跳过此项)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. 核心服务健康
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
section "3. 核心服务"
|
||||||
|
|
||||||
|
# actuator/health
|
||||||
|
HEALTH_BODY=$(http_body "$BASE_URL/actuator/health")
|
||||||
|
HEALTH_STATUS=$(echo "$HEALTH_BODY" | grep -o '"status":"[^"]*"' | head -1)
|
||||||
|
if [ "$HEALTH_STATUS" = '"status":"UP"' ]; then
|
||||||
|
pass "tenant-service actuator/health = UP"
|
||||||
|
else
|
||||||
|
fail "tenant-service 未就绪" "$(echo "$HEALTH_BODY" | head -c 100)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 私有化模式
|
||||||
|
DEPLOY_STATUS=$(http_body "$BASE_URL/api/private/deployment/status")
|
||||||
|
if echo "$DEPLOY_STATUS" | grep -q '"mode":"PRIVATE"'; then
|
||||||
|
pass "PRIVATE 模式已激活"
|
||||||
|
else
|
||||||
|
fail "PRIVATE 模式未激活" "$(echo "$DEPLOY_STATUS" | head -c 100)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$DEPLOY_STATUS" | grep -q '"tenantRegisterEnabled":false'; then
|
||||||
|
pass "租户注册已禁用"
|
||||||
|
else
|
||||||
|
fail "租户注册未禁用(安全风险)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 注册接口阻断
|
||||||
|
REG_CODE=$(curl -sL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 8 \
|
||||||
|
-X POST "$BASE_URL/api/auth/register" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"email":"verify-test@block.test","username":"verifytest","password":"Test@123456"}' 2>/dev/null || echo 000)
|
||||||
|
if [ "$REG_CODE" != "200" ] && [ "$REG_CODE" != "000" ]; then
|
||||||
|
pass "注册接口已阻断 (HTTP $REG_CODE)"
|
||||||
|
else
|
||||||
|
fail "注册接口未阻断 (HTTP $REG_CODE)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. 前端页面
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
section "4. 前端页面"
|
||||||
|
|
||||||
|
WEB_CODE=$(http_get "$BASE_URL/")
|
||||||
|
if [ "$WEB_CODE" = "200" ]; then
|
||||||
|
pass "控制台前端可访问 (HTTP 200)"
|
||||||
|
else
|
||||||
|
fail "控制台前端不可访问 (HTTP $WEB_CODE)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
OPS_CODE=$(http_get "$BASE_URL/ops")
|
||||||
|
if echo "$OPS_CODE" | grep -qE '^(200|301|302)$'; then
|
||||||
|
pass "运营后台可访问 (HTTP $OPS_CODE)"
|
||||||
|
else
|
||||||
|
fail "运营后台不可访问 (HTTP $OPS_CODE)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOCS_CODE=$(http_get "$BASE_URL/docs/")
|
||||||
|
if echo "$DOCS_CODE" | grep -qE '^(200|301|302)$'; then
|
||||||
|
pass "文档站可访问 (HTTP $DOCS_CODE)"
|
||||||
|
else
|
||||||
|
warn "文档站不可访问 (HTTP $DOCS_CODE)(可选服务)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. 租户登录和中文字符集
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
section "5. 租户登录"
|
||||||
|
|
||||||
|
# 获取验证码
|
||||||
|
CAPTCHA_RESP=$(http_body "$BASE_URL/api/auth/captcha")
|
||||||
|
CAPTCHA_KEY=$(echo "$CAPTCHA_RESP" | grep -o '"key":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||||
|
|
||||||
|
LOGIN_OK=false
|
||||||
|
TOKEN=""
|
||||||
|
NICK=""
|
||||||
|
|
||||||
|
if [ -n "$CAPTCHA_KEY" ]; then
|
||||||
|
# 从 Redis 读取验证码
|
||||||
|
REDIS_CTR=$(docker ps --filter "name=redis" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||||||
|
if [ -n "$REDIS_CTR" ]; then
|
||||||
|
CAPTCHA_CODE=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning \
|
||||||
|
GET "captcha:${CAPTCHA_KEY}" 2>/dev/null | tr -d '\r')
|
||||||
|
|
||||||
|
if [ -n "$CAPTCHA_CODE" ]; then
|
||||||
|
# 尝试登录(用 bootstrap 邮箱)
|
||||||
|
BOOTSTRAP_EMAIL="${TENANT_BOOTSTRAP_EMAIL:-admin@company.com}"
|
||||||
|
BOOTSTRAP_PASS="${TENANT_BOOTSTRAP_PASSWORD:-}"
|
||||||
|
|
||||||
|
LOGIN_RESP=$(curl -sL --noproxy '*' --max-time 10 \
|
||||||
|
-X POST "$BASE_URL/api/auth/login" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "{\"account\":\"${BOOTSTRAP_EMAIL}\",\"password\":\"${BOOTSTRAP_PASS}\",\"captchaKey\":\"${CAPTCHA_KEY}\",\"captchaCode\":\"${CAPTCHA_CODE}\"}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
LOGIN_CODE=$(echo "$LOGIN_RESP" | grep -o '"code":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||||
|
if [ "$LOGIN_CODE" = "200" ]; then
|
||||||
|
TOKEN=$(echo "$LOGIN_RESP" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
LOGIN_OK=true
|
||||||
|
pass "租户登录成功"
|
||||||
|
elif [ "$BOOTSTRAP_PASS" = "change-me-on-first-login" ] || [ -z "$BOOTSTRAP_PASS" ]; then
|
||||||
|
# bootstrap 密码是占位符,说明已迁移生产数据,用生产密码登录,此处跳过
|
||||||
|
warn "租户登录跳过(bootstrap 密码为占位符,迁移后请用生产密码验证)"
|
||||||
|
|
||||||
|
# 解码 JWT 检查中文字符集
|
||||||
|
if command -v python3 >/dev/null 2>&1 && [ -n "$TOKEN" ]; then
|
||||||
|
NICK=$(python3 -c "
|
||||||
|
import base64, json, sys
|
||||||
|
try:
|
||||||
|
payload = '${TOKEN}'.split('.')[1]
|
||||||
|
payload += '=' * (4 - len(payload) % 4)
|
||||||
|
d = json.loads(base64.urlsafe_b64decode(payload).decode('utf-8'))
|
||||||
|
print(d.get('nickname',''))
|
||||||
|
except:
|
||||||
|
print('')
|
||||||
|
" 2>/dev/null)
|
||||||
|
if [ -n "$NICK" ] && echo "$NICK" | python3 -c "import sys; s=sys.stdin.read().strip(); sys.exit(0 if s == s.encode('utf-8').decode('utf-8') and any(ord(c) > 0x7F for c in s) else 0)" 2>/dev/null; then
|
||||||
|
pass "JWT 中文字段正常: $NICK"
|
||||||
|
elif [ -n "$NICK" ]; then
|
||||||
|
pass "JWT 昵称字段: $NICK"
|
||||||
|
else
|
||||||
|
warn "JWT 昵称字段为空(bootstrap 租户可能未迁移)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "租户登录失败" "code=$LOGIN_CODE (resp: $(echo "$LOGIN_RESP" | head -c 80))"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "无法从 Redis 读取验证码(跳过登录测试)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Redis 容器未运行(跳过登录测试)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "无法获取验证码(captcha API 异常)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. SDK 配置接口
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
section "6. SDK 配置"
|
||||||
|
|
||||||
|
if [ -f "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" ]; then
|
||||||
|
DEFAULT_APP_KEY=$(python3 -c "
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
d = json.load(open('$ROOT_DIR/config/sdk/xuqm-private-sdk.json'))
|
||||||
|
print(d.get('appKey',''))
|
||||||
|
except:
|
||||||
|
print('')
|
||||||
|
" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 从数据库查询已有 App
|
||||||
|
MYSQL_CTR=$(docker ps --filter "name=mysql" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||||||
|
APP_KEYS=""
|
||||||
|
if [ -n "$MYSQL_CTR" ]; then
|
||||||
|
APP_KEYS=$(docker exec "$MYSQL_CTR" sh -c \
|
||||||
|
"mysql -u${MYSQL_USERNAME:-xuqm} -p${MYSQL_PASSWORD:-} ${MYSQL_DATABASE:-xuqm_private} -N \
|
||||||
|
-e 'SELECT app_key FROM t_app LIMIT 5;'" 2>/dev/null | tr '\n' ' ')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$APP_KEYS" ] && [ -n "${DEFAULT_APP_KEY:-}" ]; then
|
||||||
|
APP_KEYS="$DEFAULT_APP_KEY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$APP_KEYS" ]; then
|
||||||
|
for APP_KEY in $APP_KEYS; do
|
||||||
|
SDK_CODE=$(http_get "$BASE_URL/api/sdk/config?appKey=${APP_KEY}&platform=ANDROID")
|
||||||
|
SDK_BODY=$(http_body "$BASE_URL/api/sdk/config?appKey=${APP_KEY}&platform=ANDROID")
|
||||||
|
if [ "$SDK_CODE" = "200" ] && echo "$SDK_BODY" | grep -q '"code":200'; then
|
||||||
|
pass "SDK config: $APP_KEY"
|
||||||
|
else
|
||||||
|
fail "SDK config: $APP_KEY (HTTP $SDK_CODE)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
warn "未找到 App Key(数据库可能为空或尚未迁移)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. 可选服务
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
section "7. 可选服务(需认证,401 = 正常)"
|
||||||
|
|
||||||
|
check_optional_service() {
|
||||||
|
local label="$1" url="$2" token="$3"
|
||||||
|
local code
|
||||||
|
if [ -n "$token" ]; then
|
||||||
|
code=$(http_code_auth "$url" "$token")
|
||||||
|
else
|
||||||
|
code=$(http_get "$url")
|
||||||
|
fi
|
||||||
|
if echo "$code" | grep -qE '^(200|201|400|401|403|404)$'; then
|
||||||
|
pass "$label 响应正常 (HTTP $code)"
|
||||||
|
elif [ "$code" = "000" ]; then
|
||||||
|
warn "$label 无法连接(未部署或未启动)"
|
||||||
|
else
|
||||||
|
fail "$label 返回错误 (HTTP $code)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_optional_service "IM 服务" \
|
||||||
|
"$BASE_URL/api/im/platform-events/token?appKey=test" "$TOKEN"
|
||||||
|
|
||||||
|
check_optional_service "版本管理 (update)" \
|
||||||
|
"$BASE_URL/api/v1/updates/app/list?appKey=test&platform=ANDROID" "$TOKEN"
|
||||||
|
|
||||||
|
check_optional_service "RN 热更新 (rn)" \
|
||||||
|
"$BASE_URL/api/v1/rn/list?appKey=test" "$TOKEN"
|
||||||
|
|
||||||
|
# 文件服务
|
||||||
|
FILE_CODE=$(http_get "$BASE_URL/file/")
|
||||||
|
if echo "$FILE_CODE" | grep -qE '^(200|400|401|403|404)$'; then
|
||||||
|
pass "文件服务响应正常 (HTTP $FILE_CODE)"
|
||||||
|
else
|
||||||
|
warn "文件服务响应: HTTP $FILE_CODE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 最终汇总
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf "\n${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||||||
|
printf "${BOLD} 验证结果${RESET}\n"
|
||||||
|
printf "${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||||||
|
printf " ${GREEN}PASS${RESET}: %d 项\n" "$PASS"
|
||||||
|
printf " ${YELLOW}WARN${RESET}: %d 项(可选服务或预期降级)\n" "$WARN"
|
||||||
|
printf " ${RED}FAIL${RESET}: %d 项\n" "$FAIL"
|
||||||
|
|
||||||
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
|
printf "\n ${GREEN}${BOLD}✓ 验证通过!所有必选服务运行正常。${RESET}\n"
|
||||||
|
[ "$WARN" -gt 0 ] && printf " ${YELLOW}(%d 个可选服务未启动,属于预期范围)${RESET}\n" "$WARN"
|
||||||
|
else
|
||||||
|
printf "\n ${RED}${BOLD}✗ %d 项验证失败,请检查容器日志:${RESET}\n" "$FAIL"
|
||||||
|
printf " docker compose logs --tail 50 tenant-service\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n 访问地址:${BOLD}%s${RESET}\n" "$BASE_URL"
|
||||||
|
printf " 运营后台:${BOLD}%s/ops${RESET}\n" "$BASE_URL"
|
||||||
|
printf " 文档站 :${BOLD}%s/docs/${RESET}\n" "$BASE_URL"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# 输出结果到 JSON
|
||||||
|
RESULT_JSON="$ROOT_DIR/.deploy-state/last-verify.json"
|
||||||
|
{
|
||||||
|
printf '{"timestamp":"%s","base_url":"%s","pass":%d,"warn":%d,"fail":%d,"items":[' \
|
||||||
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$BASE_URL" "$PASS" "$WARN" "$FAIL"
|
||||||
|
first=true
|
||||||
|
for r in "${RESULTS[@]}"; do
|
||||||
|
STATUS="${r%%|*}"; MSG="${r##*|}"
|
||||||
|
$first || printf ','
|
||||||
|
printf '{"status":"%s","message":"%s"}' "$STATUS" "$MSG"
|
||||||
|
first=false
|
||||||
|
done
|
||||||
|
printf ']}\n'
|
||||||
|
} > "$RESULT_JSON" 2>/dev/null || true
|
||||||
|
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户