chore: scaffold private deployment repository

这个提交包含在:
徐勤民 2026-05-18 19:49:31 +08:00
当前提交 4ada03183a
共有 51 个文件被更改,包括 1261 次插入0 次删除

14
.deploy-state/current.json 普通文件
查看文件

@ -0,0 +1,14 @@
{
"privateVersion": "2026.05.18-private.1",
"profiles": ["base"],
"mysql": {"mode": "external", "status": "UNKNOWN"},
"redis": {"mode": "external", "status": "UNKNOWN"},
"services": {
"file": true,
"im": false,
"push": false,
"update": false,
"license": false
},
"lastHealthcheck": null
}

1
.deploy-state/history.log 普通文件
查看文件

@ -0,0 +1 @@

查看文件

@ -0,0 +1,5 @@
{
"status": "UNKNOWN",
"checks": []
}

9
.deploy-state/progress.md 普通文件
查看文件

@ -0,0 +1,9 @@
# Deployment Progress
| Time | Step | Status | Notes |
|------|------|--------|-------|
| 2026-05-18T19:48:27+0800 | configure | STARTED | rendering initial config |
| 2026-05-18T19:48:27+0800 | render-config | STARTED | rendering runtime files |
| 2026-05-18T19:48:27+0800 | render-config | DONE | runtime files rendered |
| 2026-05-18T19:48:27+0800 | configure | DONE | config ready |

41
.env.example 普通文件
查看文件

@ -0,0 +1,41 @@
PRIVATE_VERSION=2026.05.18-private.1
REGISTRY=registry.example.com/xuqm
IMAGE_TAG=2026.05.18-private.1
COMPOSE_PROFILES=base
ENABLE_FILE=true
ENABLE_IM=false
ENABLE_PUSH=false
ENABLE_UPDATE=false
ENABLE_LICENSE=false
MYSQL_MODE=external
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=xuqm_private
MYSQL_USERNAME=xuqm
MYSQL_PASSWORD=change-me
MYSQL_ROOT_PASSWORD=
MYSQL_DATA_DIR=./data/mysql
REDIS_MODE=external
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=change-me
REDIS_DATABASE=0
REDIS_DATA_DIR=./data/redis
CONSOLE_DOMAIN=https://console.customer.com
OPS_DOMAIN=https://ops.customer.com
DOCS_DOMAIN=https://docs.customer.com
FILE_DOMAIN=https://file.customer.com
IM_DOMAIN=https://im.customer.com
UPDATE_DOMAIN=https://update.customer.com
LICENSE_DOMAIN=https://license.customer.com
PUSH_DOMAIN=https://push.customer.com
TENANT_BOOTSTRAP_EMAIL=admin@customer.com
TENANT_BOOTSTRAP_PASSWORD=change-me-on-first-login
TENANT_BOOTSTRAP_APP_KEY=ak_private_default

18
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,18 @@
.env
config/secrets.env
logs/*.log
data/mysql/*
data/redis/*
data/uploads/*
data/update/*
data/backups/*
!data/mysql/.gitkeep
!data/redis/.gitkeep
!data/uploads/.gitkeep
!data/update/.gitkeep
!data/backups/.gitkeep
dist/
*.tar.gz
*.bak
.DS_Store

75
README.md 普通文件
查看文件

@ -0,0 +1,75 @@
# XuqmGroup Private Deploy
私有化部署仓库只负责客户环境交付,不包含业务源码和 demo 前后端。
## 快速开始
```bash
./scripts/configure.sh
vim .env
vim config/secrets.env
./scripts/install.sh --profile base
./scripts/healthcheck.sh
```
生产部署前必须完成:
- 配置镜像仓库 `REGISTRY` 和版本 `IMAGE_TAG`
- 选择 MySQL/Redis 模式:`external` 使用客户自备服务,`managed` 由脚本创建容器服务。
- 配置控制台、文档站、文件、IM、Push、Update、License 域名。
- 配置 SMTP、Push 厂商、应用市场发布凭据。
- 确认证书和反向代理策略,默认 Nginx 配置只作为模板入口。
## 部署模式
MySQL、Redis 支持两种模式:
- `external`:客户自备连接,脚本只校验连通性和权限。
- `managed`:脚本新建服务,自动创建数据库、账号、密码和数据目录。
生产环境默认推荐 `external/external`
托管模式示例:
```bash
./scripts/install.sh --profile base --mysql-mode managed --redis-mode managed
```
外部模式示例:
```bash
./scripts/install.sh --profile base --mysql-mode external --redis-mode external
```
## 可选服务
- `base`:基础控制台、运营平台、文档站、文件服务。
- `im`IM HTTP / WebSocket。
- `push`:厂商推送。
- `update`版本管理、RN 热更新、应用市场自动发布。
- `license`License 校验。
后期启用:
```bash
./scripts/enable-service.sh im
./scripts/enable-service.sh push
./scripts/enable-service.sh update
./scripts/enable-service.sh license
```
禁用服务只修改部署配置并停止对应容器,不删除数据:
```bash
./scripts/disable-service.sh im
```
## 接手入口
- 实时部署进度:`.deploy-state/progress.md`
- 最近运行状态:`.deploy-state/current.json`
- 最近健康检查:`.deploy-state/last-healthcheck.json`
- 脚本审计日志:`logs/audit.log`
- 交付说明:`docs/runbook.md`
- 配置说明:`docs/configuration.md`
- 验收清单:`docs/acceptance-checklist.md`

2
VERSION 普通文件
查看文件

@ -0,0 +1,2 @@
2026.05.18-private.1

查看文件

@ -0,0 +1,20 @@
{
"deployment": "PRIVATE",
"privateVersion": "2026.05.18-private.1",
"domains": {
"console": "https://console.customer.com",
"docs": "https://docs.customer.com",
"file": "https://file.customer.com",
"im": "https://im.customer.com",
"update": "https://update.customer.com",
"license": "https://license.customer.com",
"push": "https://push.customer.com"
},
"features": {
"file": true,
"im": false,
"push": false,
"update": false,
"license": false
}
}

6
config/infra.env 普通文件
查看文件

@ -0,0 +1,6 @@
MYSQL_MODE=external
MYSQL_DATA_DIR=./data/mysql
REDIS_MODE=external
REDIS_DATA_DIR=./data/redis
FILE_STORAGE_MODE=local

6
config/mail/smtp.env 普通文件
查看文件

@ -0,0 +1,6 @@
SMTP_HOST=
SMTP_PORT=465
SMTP_USERNAME=
SMTP_TLS=true
SMTP_SSL=true

查看文件

@ -0,0 +1,13 @@
server {
listen 80;
server_name _;
location /health {
return 200 "ok\n";
}
location / {
proxy_pass http://tenant-web:80;
}
}

12
config/nginx/nginx.conf 普通文件
查看文件

@ -0,0 +1,12 @@
events {}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
client_max_body_size 1024m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
include /etc/nginx/conf.d/*.conf;
}

查看文件

@ -0,0 +1,4 @@
LOG_FORMAT=json
LOG_RETENTION_DAYS=30
LOG_COLLECTOR=none

查看文件

@ -0,0 +1,3 @@
METRICS_ENABLED=false
PROMETHEUS_ENABLED=false

查看文件

@ -0,0 +1,24 @@
{
"schemaVersion": 1,
"configVersion": "2026.05.18-private.1",
"deployment": "PRIVATE",
"appKey": "ak_private_default",
"controlBaseUrl": "https://console.customer.com",
"fileBaseUrl": "https://file.customer.com",
"imApiBaseUrl": "https://im.customer.com",
"imWsUrl": "wss://im.customer.com/ws/im",
"pushBaseUrl": "https://push.customer.com",
"updateBaseUrl": "https://update.customer.com",
"licenseBaseUrl": "https://license.customer.com",
"docsBaseUrl": "https://docs.customer.com",
"features": {
"file": true,
"im": false,
"push": false,
"update": false,
"license": false
},
"connectTimeoutMs": 10000,
"readTimeoutMs": 30000,
"logLevel": "WARN"
}

查看文件

@ -0,0 +1,5 @@
MYSQL_PASSWORD=change-me
MYSQL_ROOT_PASSWORD=
REDIS_PASSWORD=change-me
SMTP_PASSWORD=

查看文件

@ -0,0 +1,4 @@
TENANT_BOOTSTRAP_EMAIL=admin@customer.com
TENANT_BOOTSTRAP_PASSWORD=change-me-on-first-login
TENANT_BOOTSTRAP_APP_KEY=ak_private_default

7
config/vendors/push.env vendored 普通文件
查看文件

@ -0,0 +1,7 @@
HUAWEI_PUSH_ENABLED=false
MI_PUSH_ENABLED=false
OPPO_PUSH_ENABLED=false
VIVO_PUSH_ENABLED=false
HONOR_PUSH_ENABLED=false
APNS_ENABLED=false

6
config/vendors/store-submit.env vendored 普通文件
查看文件

@ -0,0 +1,6 @@
HUAWEI_STORE_ENABLED=false
MI_STORE_ENABLED=false
OPPO_STORE_ENABLED=false
VIVO_STORE_ENABLED=false
HONOR_STORE_ENABLED=false

28
config/xuqm.env 普通文件
查看文件

@ -0,0 +1,28 @@
DEPLOYMENT_MODE=PRIVATE
TENANT_REGISTER_ENABLED=false
TENANT_BOOTSTRAP_ENABLED=true
ENABLE_FILE=true
ENABLE_IM=false
ENABLE_PUSH=false
ENABLE_UPDATE=false
ENABLE_LICENSE=false
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=xuqm_private
MYSQL_USERNAME=xuqm
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DATABASE=0
CONSOLE_DOMAIN=https://console.customer.com
OPS_DOMAIN=https://ops.customer.com
DOCS_DOMAIN=https://docs.customer.com
FILE_DOMAIN=https://file.customer.com
IM_DOMAIN=https://im.customer.com
UPDATE_DOMAIN=https://update.customer.com
LICENSE_DOMAIN=https://license.customer.com
PUSH_DOMAIN=https://push.customer.com

1
data/backups/.gitkeep 普通文件
查看文件

@ -0,0 +1 @@

1
data/mysql/.gitkeep 普通文件
查看文件

@ -0,0 +1 @@

1
data/redis/.gitkeep 普通文件
查看文件

@ -0,0 +1 @@

1
data/update/.gitkeep 普通文件
查看文件

@ -0,0 +1 @@

1
data/uploads/.gitkeep 普通文件
查看文件

@ -0,0 +1 @@

30
docker-compose.infra.yml 普通文件
查看文件

@ -0,0 +1,30 @@
services:
mysql:
image: mysql:8.4
profiles: ["infra-mysql"]
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USERNAME}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-time-zone=+08:00
ports:
- "${MYSQL_PORT:-3306}:3306"
volumes:
- ./data/mysql:/var/lib/mysql
restart: unless-stopped
redis:
image: redis:7.4-alpine
profiles: ["infra-redis"]
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- ./data/redis:/data
restart: unless-stopped

90
docker-compose.yml 普通文件
查看文件

@ -0,0 +1,90 @@
services:
tenant-service:
image: ${REGISTRY}/tenant-service:${IMAGE_TAG}
profiles: ["base"]
env_file:
- ./config/xuqm.env
- ./config/secrets.env
- ./config/tenant/bootstrap.env
restart: unless-stopped
file-service:
image: ${REGISTRY}/file-service:${IMAGE_TAG}
profiles: ["base"]
env_file:
- ./config/xuqm.env
- ./config/secrets.env
volumes:
- ./data/uploads:/data/uploads
restart: unless-stopped
tenant-web:
image: ${REGISTRY}/tenant-web:${IMAGE_TAG}
profiles: ["base"]
restart: unless-stopped
ops-web:
image: ${REGISTRY}/ops-web:${IMAGE_TAG}
profiles: ["base"]
restart: unless-stopped
docs-site:
image: ${REGISTRY}/docs-site:${IMAGE_TAG}
profiles: ["base"]
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:
image: nginx:1.27-alpine
profiles: ["base"]
ports:
- "80:80"
- "443:443"
volumes:
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./config/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
- tenant-service
- tenant-web
- ops-web
- docs-site
restart: unless-stopped
im-service:
image: ${REGISTRY}/im-service:${IMAGE_TAG}
profiles: ["im"]
env_file:
- ./config/xuqm.env
- ./config/secrets.env
restart: unless-stopped
push-service:
image: ${REGISTRY}/push-service:${IMAGE_TAG}
profiles: ["push"]
env_file:
- ./config/xuqm.env
- ./config/secrets.env
- ./config/vendors/push.env
restart: unless-stopped
update-service:
image: ${REGISTRY}/update-service:${IMAGE_TAG}
profiles: ["update"]
env_file:
- ./config/xuqm.env
- ./config/secrets.env
- ./config/vendors/store-submit.env
volumes:
- ./data/update:/data/update
restart: unless-stopped
license-service:
image: ${REGISTRY}/license-service:${IMAGE_TAG}
profiles: ["license"]
env_file:
- ./config/xuqm.env
- ./config/secrets.env
restart: unless-stopped

查看文件

@ -0,0 +1,36 @@
# 验收清单
## 基础部署
- `./scripts/configure.sh` 可重复执行。
- `./scripts/install.sh --profile base` 可完成部署。
- `./scripts/healthcheck.sh` 输出 `PASS` 或明确错误码。
- 文档站加载 `config/sdk/xuqm-private-sdk.json`
- 租户注册入口关闭。
- 首次启动自动创建内置租户。
## 中间件
- `external` MySQL/Redis 不启动本地容器。
- `managed` MySQL/Redis 自动启动并持久化数据。
- 托管模式自动生成密码并写入 `config/secrets.env`
## 可选服务
- `im` 可后期独立启用和禁用。
- `push` 可后期独立启用和禁用。
- `update` 可后期独立启用和禁用。
- `license` 可后期独立启用和禁用。
- 禁用任一可选服务时,基础服务可继续运行。
## 厂商能力
- Push 支持华为、小米、OPPO、vivo、荣耀。
- 应用市场自动发布支持华为、小米、OPPO、vivo、荣耀。
- 厂商凭据缺失时返回明确诊断,不影响基础服务启动。
## 公有化隔离
- 公有化域名 `dev.xuqinmin.com` 不写入私有化 SDK 配置。
- 私有化改造不影响公有化配置和部署链路。
- 公有化回归通过后才能发布私有化版本。

56
docs/configuration.md 普通文件
查看文件

@ -0,0 +1,56 @@
# 配置说明
## `.env`
部署入口配置,控制镜像版本、服务 profile、域名和 MySQL/Redis 模式。
关键字段:
- `REGISTRY`:私有 Docker 镜像仓库。
- `IMAGE_TAG`:本次部署镜像版本。
- `COMPOSE_PROFILES`:启用的服务集合,例如 `base,im,push,update,license`
- `MYSQL_MODE``external` 或 `managed`
- `REDIS_MODE``external` 或 `managed`
- `ENABLE_IM`、`ENABLE_PUSH`、`ENABLE_UPDATE`、`ENABLE_LICENSE`:运行时功能开关。
## `config/secrets.env`
敏感配置文件,不提交 Git。
关键字段:
- `MYSQL_PASSWORD`
- `MYSQL_ROOT_PASSWORD`
- `REDIS_PASSWORD`
- `SMTP_PASSWORD`
托管模式下,如果密码为空或为 `change-me`,脚本会自动生成并写回该文件。
## `config/xuqm.env`
业务服务共享配置,包含私有化运行模式、单租户初始化、域名和基础中间件连接信息。
私有化必须保持:
```env
DEPLOYMENT_MODE=PRIVATE
TENANT_REGISTER_ENABLED=false
TENANT_BOOTSTRAP_ENABLED=true
```
## `config/sdk/xuqm-private-sdk.json`
私有化 SDK 初始化配置,由 `scripts/render-config.sh` 生成。
文档站和客户应用示例必须使用该文件,不再指向 `dev.xuqinmin.com` 公有化逻辑。
## `config/vendors`
厂商能力配置:
- `push.env`华为、小米、OPPO、vivo、荣耀 Push 凭据。
- `store-submit.env`华为、小米、OPPO、vivo、荣耀应用市场自动发布凭据。
## `config/mail/smtp.env`
邮件服务配置。生产环境必须使用客户提供的 SMTP 服务。

58
docs/runbook.md 普通文件
查看文件

@ -0,0 +1,58 @@
# 私有化部署运行手册
## 目标
本仓库用于客户私有环境交付,只编排已有 Docker 镜像,不包含业务源码、不构建 demo 服务。
## 标准流程
1. 执行 `./scripts/configure.sh` 生成 `.env``config/secrets.env`
2. 修改 `.env`镜像仓库、镜像版本、域名、可选服务、MySQL/Redis 模式。
3. 修改 `config/secrets.env`数据库密码、Redis 密码、SMTP 密码等敏感配置。
4. 修改 `config/mail/smtp.env`、`config/vendors/*.env`。
5. 执行 `./scripts/install.sh --profile base` 部署基础服务。
6. 按需执行 `./scripts/enable-service.sh im|push|update|license` 启用可选服务。
7. 执行 `./scripts/healthcheck.sh` 并检查 `.deploy-state/last-healthcheck.json`
## MySQL/Redis 模式
`external` 模式:
- 客户提供连接地址、账号、密码、数据库名。
- 部署脚本只使用连接配置,不启动新服务。
- 生产默认使用该模式。
`managed` 模式:
- 部署脚本通过 Docker Compose 启动 MySQL/Redis。
- 密码为空或为 `change-me` 时自动生成并写入 `config/secrets.env`
- 数据目录写入 `data/mysql``data/redis`
## 可选服务
`im`、`push`、`update`、`license` 均可不部署,后期独立启用。
启用命令:
```bash
./scripts/enable-service.sh im
./scripts/install.sh --profile base,im
```
禁用命令:
```bash
./scripts/disable-service.sh im
```
禁用服务不会删除数据,重新启用后继续使用原配置和数据目录。
## 接手规则
任何 agent 开始执行前必须先查看:
- `.deploy-state/progress.md`
- `.deploy-state/current.json`
- `logs/audit.log`
执行关键步骤后必须追加进度,确保中断后可继续。

16
image-manifest.json 普通文件
查看文件

@ -0,0 +1,16 @@
{
"schemaVersion": 1,
"privateVersion": "2026.05.18-private.1",
"registry": "registry.example.com/xuqm",
"images": [
{"name": "tenant-service", "required": true},
{"name": "file-service", "required": true},
{"name": "tenant-web", "required": true},
{"name": "ops-web", "required": true},
{"name": "docs-site", "required": true},
{"name": "im-service", "required": false, "profile": "im"},
{"name": "push-service", "required": false, "profile": "push"},
{"name": "update-service", "required": false, "profile": "update"},
{"name": "license-service", "required": false, "profile": "license"}
]
}

1
logs/.gitkeep 普通文件
查看文件

@ -0,0 +1 @@

16
scripts/backup.sh 可执行文件
查看文件

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
audit "backup" "STARTED" "creating config backup"
progress "backup" "STARTED" "creating config backup"
mkdir -p "$ROOT_DIR/data/backups"
tar --exclude='config/secrets.env' -czf "$ROOT_DIR/data/backups/config-$(date +%Y%m%d%H%M%S).tar.gz" \
-C "$ROOT_DIR" VERSION .env config .deploy-state
audit "backup" "DONE" "backup created"
progress "backup" "DONE" "backup created"

19
scripts/configure.sh 可执行文件
查看文件

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
audit "configure" "STARTED" "rendering initial config"
progress "configure" "STARTED" "rendering initial config"
if [ ! -f "$ROOT_DIR/.env" ]; then
cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env"
fi
ensure_secret_file
"$ROOT_DIR/scripts/render-config.sh"
audit "configure" "DONE" "config ready"
progress "configure" "DONE" "config ready"

30
scripts/disable-service.sh 可执行文件
查看文件

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
SERVICE="${1:-}"
[ -n "$SERVICE" ] || fail_json "XUQM_PRIVATE_1002" "service name is required" "disable-service"
if [ ! -f "$ROOT_DIR/.env" ]; then
cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env"
fi
load_env
audit "disable-service" "STARTED" "$SERVICE"
progress "disable-service" "STARTED" "$SERVICE"
case "$SERVICE" in
im) set_env_value "$ROOT_DIR/.env" "ENABLE_IM" "false" ;;
push) set_env_value "$ROOT_DIR/.env" "ENABLE_PUSH" "false" ;;
update) set_env_value "$ROOT_DIR/.env" "ENABLE_UPDATE" "false" ;;
license) set_env_value "$ROOT_DIR/.env" "ENABLE_LICENSE" "false" ;;
*) fail_json "XUQM_PRIVATE_1002" "unknown service: $SERVICE" "disable-service" ;;
esac
set_env_value "$ROOT_DIR/.env" "COMPOSE_PROFILES" "$(remove_profile "${COMPOSE_PROFILES:-base}" "$SERVICE")"
"$ROOT_DIR/scripts/render-config.sh"
audit "disable-service" "DONE" "$SERVICE"
progress "disable-service" "DONE" "$SERVICE"

17
scripts/doctor.sh 可执行文件
查看文件

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
audit "doctor" "STARTED" "collecting diagnostics"
progress "doctor" "STARTED" "collecting diagnostics"
mkdir -p "$ROOT_DIR/dist"
tar --exclude='config/secrets.env' --exclude='data' --exclude='*.tar.gz' \
-czf "$ROOT_DIR/dist/doctor-$(date +%Y%m%d%H%M%S).tar.gz" \
-C "$ROOT_DIR" VERSION .deploy-state config logs README.md
audit "doctor" "DONE" "diagnostics collected"
progress "doctor" "DONE" "diagnostics collected"

30
scripts/enable-service.sh 可执行文件
查看文件

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
SERVICE="${1:-}"
[ -n "$SERVICE" ] || fail_json "XUQM_PRIVATE_1002" "service name is required" "enable-service"
if [ ! -f "$ROOT_DIR/.env" ]; then
cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env"
fi
load_env
audit "enable-service" "STARTED" "$SERVICE"
progress "enable-service" "STARTED" "$SERVICE"
case "$SERVICE" in
im) set_env_value "$ROOT_DIR/.env" "ENABLE_IM" "true" ;;
push) set_env_value "$ROOT_DIR/.env" "ENABLE_PUSH" "true" ;;
update) set_env_value "$ROOT_DIR/.env" "ENABLE_UPDATE" "true" ;;
license) set_env_value "$ROOT_DIR/.env" "ENABLE_LICENSE" "true" ;;
*) fail_json "XUQM_PRIVATE_1002" "unknown service: $SERVICE" "enable-service" ;;
esac
set_env_value "$ROOT_DIR/.env" "COMPOSE_PROFILES" "$(add_profile "${COMPOSE_PROFILES:-base}" "$SERVICE")"
"$ROOT_DIR/scripts/render-config.sh"
audit "enable-service" "DONE" "$SERVICE"
progress "enable-service" "DONE" "$SERVICE"

查看文件

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
OUT_DIR="${1:-$ROOT_DIR/dist}"
mkdir -p "$OUT_DIR"
audit "export-offline-bundle" "STARTED" "$OUT_DIR"
progress "export-offline-bundle" "STARTED" "$OUT_DIR"
tar --exclude='data' --exclude='dist' --exclude='.git' -czf "$OUT_DIR/xuqm-private-$(cat "$ROOT_DIR/VERSION").tar.gz" -C "$ROOT_DIR" .
audit "export-offline-bundle" "DONE" "$OUT_DIR"
progress "export-offline-bundle" "DONE" "$OUT_DIR"

34
scripts/healthcheck.sh 可执行文件
查看文件

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
load_env
audit "healthcheck" "STARTED" "running checks"
progress "healthcheck" "STARTED" "running checks"
require_cmd docker
STATUS="UP"
WARNINGS="[]"
if ! docker ps >/dev/null 2>&1; then
STATUS="DOWN"
fi
cat > "$ROOT_DIR/.deploy-state/last-healthcheck.json" <<EOF
{
"status": "$STATUS",
"version": "${PRIVATE_VERSION:-2026.05.18-private.1}",
"mysql": {"mode": "${MYSQL_MODE:-external}", "status": "UNKNOWN"},
"redis": {"mode": "${REDIS_MODE:-external}", "status": "UNKNOWN"},
"warnings": $WARNINGS
}
EOF
audit "healthcheck" "$STATUS" "healthcheck finished"
progress "healthcheck" "$STATUS" "healthcheck finished"
[ "$STATUS" = "UP" ] || fail_json "XUQM_PRIVATE_4001" "docker is not available" "healthcheck"

25
scripts/install-mysql.sh 可执行文件
查看文件

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
load_env
[ "${MYSQL_MODE:-external}" = "managed" ] || exit 0
audit "install-mysql" "STARTED" "managed mysql"
progress "install-mysql" "STARTED" "managed mysql"
require_cmd docker
ensure_secret_file
MYSQL_PASSWORD="$(ensure_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_PASSWORD" "${MYSQL_PASSWORD:-}" "$(random_secret)")"
MYSQL_ROOT_PASSWORD="$(ensure_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_ROOT_PASSWORD" "${MYSQL_ROOT_PASSWORD:-}" "$(random_secret)")"
load_env
export MYSQL_ROOT_PASSWORD MYSQL_DATABASE MYSQL_USERNAME MYSQL_PASSWORD MYSQL_PORT
COMPOSE_PROFILES=infra-mysql compose up -d mysql
audit "install-mysql" "DONE" "managed mysql started"
progress "install-mysql" "DONE" "managed mysql started"

23
scripts/install-redis.sh 可执行文件
查看文件

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
load_env
[ "${REDIS_MODE:-external}" = "managed" ] || exit 0
audit "install-redis" "STARTED" "managed redis"
progress "install-redis" "STARTED" "managed redis"
require_cmd docker
ensure_secret_file
REDIS_PASSWORD="$(ensure_env_value "$ROOT_DIR/config/secrets.env" "REDIS_PASSWORD" "${REDIS_PASSWORD:-}" "$(random_secret)")"
load_env
export REDIS_PASSWORD REDIS_PORT
COMPOSE_PROFILES=infra-redis compose up -d redis
audit "install-redis" "DONE" "managed redis started"
progress "install-redis" "DONE" "managed redis started"

109
scripts/install.sh 可执行文件
查看文件

@ -0,0 +1,109 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
load_env
if [ ! -f "$ROOT_DIR/.env" ]; then
cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env"
fi
ensure_secret_file
load_env
PROFILE="${COMPOSE_PROFILES:-base}"
OFFLINE_BUNDLE=""
usage() {
cat <<'EOF'
Usage: ./scripts/install.sh [options]
Options:
--profile <profiles> Docker Compose profiles, for example: base,im,push,update,license
--mysql-mode <mode> external or managed
--redis-mode <mode> external or managed
--offline-bundle <path> Load images from an offline bundle before deployment
-h, --help Show help
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--profile)
PROFILE="${2:-}"
shift 2
;;
--profile=*)
PROFILE="${1#--profile=}"
shift
;;
--mysql-mode)
MYSQL_MODE="${2:-}"
set_env_value "$ROOT_DIR/.env" "MYSQL_MODE" "$MYSQL_MODE"
set_env_value "$ROOT_DIR/config/infra.env" "MYSQL_MODE" "$MYSQL_MODE"
shift 2
;;
--mysql-mode=*)
MYSQL_MODE="${1#--mysql-mode=}"
set_env_value "$ROOT_DIR/.env" "MYSQL_MODE" "$MYSQL_MODE"
set_env_value "$ROOT_DIR/config/infra.env" "MYSQL_MODE" "$MYSQL_MODE"
shift
;;
--redis-mode)
REDIS_MODE="${2:-}"
set_env_value "$ROOT_DIR/.env" "REDIS_MODE" "$REDIS_MODE"
set_env_value "$ROOT_DIR/config/infra.env" "REDIS_MODE" "$REDIS_MODE"
shift 2
;;
--redis-mode=*)
REDIS_MODE="${1#--redis-mode=}"
set_env_value "$ROOT_DIR/.env" "REDIS_MODE" "$REDIS_MODE"
set_env_value "$ROOT_DIR/config/infra.env" "REDIS_MODE" "$REDIS_MODE"
shift
;;
--offline-bundle)
OFFLINE_BUNDLE="${2:-}"
shift 2
;;
--offline-bundle=*)
OFFLINE_BUNDLE="${1#--offline-bundle=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail_json "XUQM_PRIVATE_4002" "unknown install option: $1" "install"
;;
esac
done
[ -n "$PROFILE" ] || fail_json "XUQM_PRIVATE_4003" "profile cannot be empty" "install"
set_env_value "$ROOT_DIR/.env" "COMPOSE_PROFILES" "$PROFILE"
audit "install" "STARTED" "profile=$PROFILE"
progress "install" "STARTED" "profile=$PROFILE"
require_cmd docker
if [ -n "$OFFLINE_BUNDLE" ]; then
[ -f "$OFFLINE_BUNDLE" ] || fail_json "XUQM_PRIVATE_4004" "offline bundle not found: $OFFLINE_BUNDLE" "install"
docker load -i "$OFFLINE_BUNDLE"
fi
if [ "${MYSQL_MODE:-external}" = "managed" ]; then
"$ROOT_DIR/scripts/install-mysql.sh"
fi
if [ "${REDIS_MODE:-external}" = "managed" ]; then
"$ROOT_DIR/scripts/install-redis.sh"
fi
"$ROOT_DIR/scripts/render-config.sh"
COMPOSE_PROFILES="$PROFILE" compose up -d
"$ROOT_DIR/scripts/healthcheck.sh"
audit "install" "DONE" "profile=$PROFILE"
progress "install" "DONE" "profile=$PROFILE"

136
scripts/lib.sh 可执行文件
查看文件

@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
AUDIT_LOG="$ROOT_DIR/logs/audit.log"
PROGRESS_FILE="$ROOT_DIR/.deploy-state/progress.md"
now() {
date +"%Y-%m-%dT%H:%M:%S%z"
}
audit() {
local action="$1"
local status="$2"
local message="${3:-}"
mkdir -p "$(dirname "$AUDIT_LOG")"
printf '{"time":"%s","action":"%s","status":"%s","message":"%s"}\n' \
"$(now)" "$action" "$status" "$message" >> "$AUDIT_LOG"
}
progress() {
local step="$1"
local status="$2"
local notes="${3:-}"
mkdir -p "$(dirname "$PROGRESS_FILE")"
if [ ! -f "$PROGRESS_FILE" ]; then
printf '# Deployment Progress\n\n| Time | Step | Status | Notes |\n|------|------|--------|-------|\n' > "$PROGRESS_FILE"
fi
printf '| %s | %s | %s | %s |\n' "$(now)" "$step" "$status" "$notes" >> "$PROGRESS_FILE"
}
fail_json() {
local code="$1"
local message="$2"
local step="${3:-unknown}"
printf '{"code":"%s","message":"%s","step":"%s","recoverable":true}\n' "$code" "$message" "$step" >&2
audit "$step" "FAILED" "$message"
progress "$step" "FAILED" "$message"
exit 1
}
load_env() {
set -a
[ -f "$ROOT_DIR/.env" ] && . "$ROOT_DIR/.env"
[ -f "$ROOT_DIR/config/infra.env" ] && . "$ROOT_DIR/config/infra.env"
[ -f "$ROOT_DIR/config/xuqm.env" ] && . "$ROOT_DIR/config/xuqm.env"
[ -f "$ROOT_DIR/config/secrets.env" ] && . "$ROOT_DIR/config/secrets.env"
set +a
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail_json "XUQM_PRIVATE_4001" "missing required command: $1" "preflight"
}
random_secret() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 32 | tr -d '\n'
else
printf '%s_%s' "$(date +%s)" "$RANDOM"
fi
}
ensure_secret_file() {
if [ ! -f "$ROOT_DIR/config/secrets.env" ]; then
cp "$ROOT_DIR/config/secrets.env.example" "$ROOT_DIR/config/secrets.env"
chmod 600 "$ROOT_DIR/config/secrets.env" || true
fi
}
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
mkdir -p "$(dirname "$file")"
touch "$file"
if grep -q "^${key}=" "$file"; then
sed -i.bak "s|^${key}=.*|${key}=${value}|" "$file"
rm -f "${file}.bak"
else
printf '%s=%s\n' "$key" "$value" >> "$file"
fi
}
ensure_env_value() {
local file="$1"
local key="$2"
local current="${3:-}"
local generated="$4"
if [ -z "$current" ] || [ "$current" = "change-me" ]; then
set_env_value "$file" "$key" "$generated"
printf '%s' "$generated"
else
printf '%s' "$current"
fi
}
profile_contains() {
local profiles="$1"
local target="$2"
case ",${profiles}," in
*,"${target}",*) return 0 ;;
*) return 1 ;;
esac
}
add_profile() {
local profiles="${1:-base}"
local target="$2"
if profile_contains "$profiles" "$target"; then
printf '%s' "$profiles"
else
printf '%s,%s' "$profiles" "$target"
fi
}
remove_profile() {
local profiles="$1"
local target="$2"
local result=""
local item
IFS=',' read -r -a items <<< "$profiles"
for item in "${items[@]}"; do
[ "$item" = "$target" ] && continue
[ -z "$item" ] && continue
if [ -z "$result" ]; then
result="$item"
else
result="$result,$item"
fi
done
printf '%s' "${result:-base}"
}
compose() {
docker compose --env-file "$ROOT_DIR/.env" -f "$ROOT_DIR/docker-compose.yml" -f "$ROOT_DIR/docker-compose.infra.yml" "$@"
}

99
scripts/render-config.sh 可执行文件
查看文件

@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
load_env
strip_protocol() {
local value="$1"
value="${value#https://}"
value="${value#http://}"
printf '%s' "$value"
}
ws_url() {
local value="$1"
local host
host="$(strip_protocol "$value")"
if [ -z "$host" ]; then
printf ''
else
printf 'wss://%s/ws/im' "$host"
fi
}
audit "render-config" "STARTED" "rendering runtime files"
progress "render-config" "STARTED" "rendering runtime files"
mkdir -p "$ROOT_DIR/config/docs" "$ROOT_DIR/config/sdk"
cat > "$ROOT_DIR/config/docs/docs-runtime.json" <<EOF
{
"deployment": "PRIVATE",
"privateVersion": "${PRIVATE_VERSION:-2026.05.18-private.1}",
"domains": {
"console": "${CONSOLE_DOMAIN:-}",
"docs": "${DOCS_DOMAIN:-}",
"file": "${FILE_DOMAIN:-}",
"im": "${IM_DOMAIN:-}",
"update": "${UPDATE_DOMAIN:-}",
"license": "${LICENSE_DOMAIN:-}",
"push": "${PUSH_DOMAIN:-}"
},
"features": {
"file": ${ENABLE_FILE:-true},
"im": ${ENABLE_IM:-false},
"push": ${ENABLE_PUSH:-false},
"update": ${ENABLE_UPDATE:-false},
"license": ${ENABLE_LICENSE:-false}
}
}
EOF
cat > "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" <<EOF
{
"schemaVersion": 1,
"configVersion": "${PRIVATE_VERSION:-2026.05.18-private.1}",
"deployment": "PRIVATE",
"appKey": "${TENANT_BOOTSTRAP_APP_KEY:-ak_private_default}",
"controlBaseUrl": "${CONSOLE_DOMAIN:-}",
"fileBaseUrl": "${FILE_DOMAIN:-}",
"imApiBaseUrl": "${IM_DOMAIN:-}",
"imWsUrl": "$(ws_url "${IM_DOMAIN:-}")",
"pushBaseUrl": "${PUSH_DOMAIN:-}",
"updateBaseUrl": "${UPDATE_DOMAIN:-}",
"licenseBaseUrl": "${LICENSE_DOMAIN:-}",
"docsBaseUrl": "${DOCS_DOMAIN:-}",
"features": {
"file": ${ENABLE_FILE:-true},
"im": ${ENABLE_IM:-false},
"push": ${ENABLE_PUSH:-false},
"update": ${ENABLE_UPDATE:-false},
"license": ${ENABLE_LICENSE:-false}
},
"connectTimeoutMs": 10000,
"readTimeoutMs": 30000,
"logLevel": "WARN"
}
EOF
cat > "$ROOT_DIR/.deploy-state/current.json" <<EOF
{
"privateVersion": "${PRIVATE_VERSION:-2026.05.18-private.1}",
"profiles": ["${COMPOSE_PROFILES:-base}"],
"mysql": {"mode": "${MYSQL_MODE:-external}", "status": "UNKNOWN"},
"redis": {"mode": "${REDIS_MODE:-external}", "status": "UNKNOWN"},
"services": {
"file": ${ENABLE_FILE:-true},
"im": ${ENABLE_IM:-false},
"push": ${ENABLE_PUSH:-false},
"update": ${ENABLE_UPDATE:-false},
"license": ${ENABLE_LICENSE:-false}
},
"lastHealthcheck": null
}
EOF
audit "render-config" "DONE" "runtime files rendered"
progress "render-config" "DONE" "runtime files rendered"

8
scripts/restore.sh 可执行文件
查看文件

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
fail_json "XUQM_PRIVATE_4003" "restore is not implemented yet; use backup manifest manually" "restore"

8
scripts/rollback.sh 可执行文件
查看文件

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
fail_json "XUQM_PRIVATE_4003" "rollback implementation pending release history" "rollback"

11
scripts/upgrade.sh 可执行文件
查看文件

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$ROOT_DIR/scripts/lib.sh"
audit "upgrade" "STARTED" "${1:-}"
progress "upgrade" "STARTED" "${1:-}"
"$ROOT_DIR/scripts/backup.sh"
fail_json "XUQM_PRIVATE_4003" "upgrade implementation pending compatibility matrix" "upgrade"

查看文件

@ -0,0 +1,36 @@
# 客户交付记录
## 基础信息
- 客户:
- 部署日期:
- 私有化版本:
- 镜像版本:
- 部署模式external / managed
## 域名
- 控制台:
- 运营后台:
- 文档站:
- 文件服务:
- IM
- Push
- Update
- License
## 可选服务
- IM未部署 / 已部署
- Push未部署 / 已部署
- Update未部署 / 已部署
- License未部署 / 已部署
## 验收
- 基础部署:
- 单租户初始化:
- 文档站 SDK 引导:
- 厂商 Push
- 应用市场自动发布:
- 备份恢复:

24
templates/env.external.tpl 普通文件
查看文件

@ -0,0 +1,24 @@
PRIVATE_VERSION=2026.05.18-private.1
REGISTRY=registry.example.com/xuqm
IMAGE_TAG=2026.05.18-private.1
COMPOSE_PROFILES=base
MYSQL_MODE=external
MYSQL_HOST=mysql.customer.internal
MYSQL_PORT=3306
MYSQL_DATABASE=xuqm_private
MYSQL_USERNAME=xuqm
REDIS_MODE=external
REDIS_HOST=redis.customer.internal
REDIS_PORT=6379
REDIS_DATABASE=0
CONSOLE_DOMAIN=https://console.customer.com
OPS_DOMAIN=https://ops.customer.com
DOCS_DOMAIN=https://docs.customer.com
FILE_DOMAIN=https://file.customer.com
IM_DOMAIN=https://im.customer.com
UPDATE_DOMAIN=https://update.customer.com
LICENSE_DOMAIN=https://license.customer.com
PUSH_DOMAIN=https://push.customer.com

24
templates/env.managed.tpl 普通文件
查看文件

@ -0,0 +1,24 @@
PRIVATE_VERSION=2026.05.18-private.1
REGISTRY=registry.example.com/xuqm
IMAGE_TAG=2026.05.18-private.1
COMPOSE_PROFILES=base
MYSQL_MODE=managed
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_DATABASE=xuqm_private
MYSQL_USERNAME=xuqm
REDIS_MODE=managed
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DATABASE=0
CONSOLE_DOMAIN=https://console.customer.com
OPS_DOMAIN=https://ops.customer.com
DOCS_DOMAIN=https://docs.customer.com
FILE_DOMAIN=https://file.customer.com
IM_DOMAIN=https://im.customer.com
UPDATE_DOMAIN=https://update.customer.com
LICENSE_DOMAIN=https://license.customer.com
PUSH_DOMAIN=https://push.customer.com