多租户:一户一库 / Schema(MySQL + PostgreSQL)
默认 TENANCY_DRIVER=off:整站单库。
设为 database 后:每租户独立 database(MySQL)或 database/schema(PostgreSQL);租户认证与业务都在该租户库。平台控制台使用独立账号与 /api/platform,不切租户库。
架构
text
TENANCY_DRIVER=database
├── 平台库 DB_*
│ ├── tenants
│ ├── platform_admins
│ └── personal_access_tokens(platform_admin)
└── 租户库
├── admins / RBAC / 业务
└── personal_access_tokens(admin)| 租户后台 | 平台控制台 | |
|---|---|---|
| 入口 | /login | /platform/login |
| API | /api/admin | /api/platform |
| Token | token | platform_token |
| 开户 migrate | — | 平台 UI 异步入队 / CLI;HTTP 开户仍禁止同步 migrate |
| 开户状态 | — | provision_status: pending → migrating → ready/failed |
配置
ini
TENANCY_DRIVER=database
TENANCY_RESOLVER=subdomain # 公网推荐;本地可用 header
TENANCY_HEADER=X-Tenant-ID
TENANCY_ALLOW_HEADER_FALLBACK= # 空=subdomain 禁止客户端回落
TENANCY_SUBDOMAIN_RESERVED=www,api,admin,platform,static,assets
TENANCY_DATABASE_PREFIX=tenant_
TENANCY_SCHEMA_PREFIX=tenant_
TENANCY_PLATFORM_CONNECTION= # 可选;钉死平台连接名,默认取 database.default
TENANCY_ALLOW_PLATFORM_DB_CREDENTIALS=false # 公网默认 false;同机开发可 true
# 远程 host 始终要求独立 username/password
TENANCY_POSTGRES_SSLMODE= # 空则回落 DB_SSLMODE / disable
TENANCY_BACKUP_KEEP=10 # tenant:backup 保留份数;0=不清理
TENANCY_POOL_MAX_IDLE_CONNS=2
TENANCY_POOL_MAX_OPEN_CONNS=20
PLATFORM_ADMIN_USERNAME=admin
PLATFORM_ADMIN_PASSWORD=secret
PLATFORM_ADMIN_NAME=平台管理员
# header 解析租户时,浏览器跨域需放行(默认已含):
# CORS_ALLOWED_HEADERS=...,X-Tenant-ID前端:VITE_TENANCY_ENABLED=true(或 VITE_TENANCY_DRIVER=database)。
Docker 本地开启
默认 docker compose / 快速开始 为单库(TENANCY_DRIVER=off)。本地要试用一户一库时:
- 先按快速开始把栈跑起来(
.env来自.env.docker.example)。 - 在根目录
.env中增加或改成:
ini
TENANCY_DRIVER=database
TENANCY_RESOLVER=header
TENANCY_HEADER=X-Tenant-ID
# 同机 MySQL 容器可共用平台库账号建 tenant_*(仅本地)
TENANCY_ALLOW_PLATFORM_DB_CREDENTIALS=true
PLATFORM_ADMIN_USERNAME=admin
PLATFORM_ADMIN_PASSWORD=secret
PLATFORM_ADMIN_NAME=平台管理员- 重启 API 容器使配置生效:
bash
docker compose up -d app
# 或
docker compose restart app- 平台首启 + 开示例租户(容器内二进制为
/www/main):
bash
docker compose exec app /www/main artisan platform:install
docker compose exec app /www/main artisan tenant:create acme "Acme" --migrate- 前端本地开发时开启租户提示(
html/.env或html-react/.env):
ini
VITE_TENANCY_ENABLED=true
VITE_TENANCY_DRIVER=database
VITE_TENANCY_HEADER=X-Tenant-ID- 访问:
| 入口 | 说明 |
|---|---|
/platform/login | 平台控制台(PLATFORM_ADMIN_*) |
/login + Header X-Tenant-ID: acme,或 /login?tenant_code=acme | 租户后台(租户库管理员,以 seed 为准) |
开户后可在平台租户列表点 迁移(异步,可选 seed),无需再跑 CLI;QUEUE_CONNECTION=sync 时任务在请求内同步执行,生产请用 Redis + long-running worker。
注意:
- 公网请改用
TENANCY_RESOLVER=subdomain,并保持TENANCY_ALLOW_PLATFORM_DB_CREDENTIALS=false。 - Compose 默认仍是演示单库。
生产要点
- 平台连接钉死:
PlatformOrmQuery使用tenancy.platform_connection,不跟随 migrate 时临时翻转的database.default。 - 生产 Artisan:
APP_ENV=production白名单含tenant:*/platform:*(开户/迁移可用)。 - 连接回收:
Forget会Close+Fresh动态连接池。 - 开户状态:HTTP/CLI 创建后为
pending;平台 UI 异步迁移或 CLItenant:migrate→migrating→ready/failed;未 ready 禁止业务绑定。UI 入队后若 worker 未消费,约 30 分钟后允许重试(防永久卡住)。 - 账号隔离:远程库必须独立凭据;同机共用平台账号仅当
TENANCY_ALLOW_PLATFORM_DB_CREDENTIALS=true(公网默认 false)。 - 异步运维:单户 migrate / seed / backup 走
tenant_ops(long-running);生产需 Redis 队列 + long-running worker。migrate-all/backup-all/restore仍仅 CLI。
公网部署(推荐)
TENANCY_RESOLVER=subdomain:租户以acme.example.com访问;apex/www/platform等保留域不接受 Header/Query 冒充(除非显式TENANCY_ALLOW_HEADER_FALLBACK=true)。- 子域与 Header/body 冲突 →
tenant_hint_conflict(400)。 - 支付回调:
POST /api/payment/notify/{type}/{tenant_code}(渠道不会带租户 Header);tenancy 开启时须带{tenant_code}。 - 连接池:每租户
TENANCY_POOL_MAX_*(默认 idle 2 / open 20),避免几百商户打满 MySQL。 - 平台控制台走
platform.或独立域名;勿与租户子域混用。
运维增强
- Landlord 迁移跳过:平台表迁移(
tenants/platform_admins/jobs/ provision/migrate meta)在tenant_*连接上SkipOnTenantConnection空跑,避免污染租户库。 - Migrate / 运维可见性:
last_migrate_error/migrated_at;平台 UI 另有last_op/last_op_status/last_op_message/last_backup_path。失败写provision_status=failed(seed 失败不降级已 ready)。 - 连接探测 / 异步操作:
POST .../ping;.../migrate|seed|backup入队。 - 登录限流:
loginlimiter 键含 body/query/header/subdomain租户提示,避免跨租户互相锁号。 - 日志:带
tenant_code/tenant_id前缀(app/utils/logger)。 - PG sslmode:
TENANCY_POSTGRES_SSLMODE或DB_SSLMODE。 - 备份/恢复:
tenant:backup [--keep=N]、tenant:backup-all;PG schema 隔离备份用pg_dump -n,恢复用PGOPTIONS=--search_path。 - CLI 范围:
RunTenantScope仅遍历 active + ready;tenant:migrate-all仍可覆盖 pending(单独查询)。 - 未绑定隔离:tenancy 开启但 ctx 未绑定时,
CacheKey→t_unbound:*,StoragePrefix→tenants/_unbound_/(不与共享根冲突)。
首启(推荐)
bash
# .env 中 TENANCY_DRIVER=database,并配置 APP_KEY、DB_*
go run . artisan platform:install -u admin -p 'your-password'
# 或依赖 .env 的 PLATFORM_ADMIN_*:
# go run . artisan platform:install
# 同机开户 + migrate + seed
go run . artisan tenant:create acme "Acme" --migrate
# 远程库已存在
go run . artisan tenant:create remote "Remote" \
--host=10.0.0.8 --port=3306 --username=u --password=secret \
--database=tenant_remote --skip-create --migrate
# 前端
# /platform/login → 平台管理租户
# /login?tenant_code=acme → 租户后台命令
bash
go run . artisan platform:install [-u] [-p] [--name=]
go run . artisan platform:admin {username} {password} [--name=]
go run . artisan tenant:create {code} {name} \
[--driver=mysql|postgres] [--isolation=database|schema] \
[--host=] [--port=] [--username=] [--password=] \
[--database=] [--schema=] [--skip-create] [--migrate]
go run . artisan tenant:migrate {id|code}
go run . artisan tenant:migrate-all
go run . artisan tenant:seed {id|code} [--class=...]
go run . artisan tenant:seed-all
go run . artisan tenant:list
go run . artisan tenant:enable|disable {id|code}
go run . artisan tenant:backup {id|code} [--keep=N]
go run . artisan tenant:backup-all [--keep=N]
go run . artisan tenant:restore {id|code} {sql路径}
# tenancy 开启时,以下命令默认遍历启用租户;可用 --tenant={code|id} 限定
go run . artisan order:create-sharding-tables [--tenant=]
go run . artisan payment:create-sharding-tables [--tenant=]
go run . artisan search:init-orders-index [--tenant=]
go run . artisan search:sync-orders [--tenant=]
go run . artisan search:retry-outbox [--tenant=]
go run . artisan app:clear-logs [--tenant=]
go run . artisan app:clear-chunks [--tenant=]
go run . artisan db:analyze-stats [--tenant=]
go run . artisan db:optimize-tables {tables...} [--tenant=]
# 写多数据命令:tenancy 开启时必须指定 --tenant
go run . artisan order:generate-test-data --tenant={code} --count=1000
go run . artisan payment:generate-test-data --tenant={code} --count=1000建库与密码
host空:在平台实例CREATEhost有值:连目标主机系统库再CREATE--skip-create/skip_create:库已存在,只登记tenants.password:仅enc:v1:+APP_KEY密文;无明文兼容- HTTP
POST /api/platform/tenants:禁止migrate=true(返回tenant_migrate_via_cli)
平台 API
| Method | Path | 说明 |
|---|---|---|
| POST | /api/platform/login | 登录 |
| GET | /api/platform/info | 当前管理员 |
| POST | /api/platform/logout | 登出 |
| GET/POST | /api/platform/tenants | 列表 / 开户(无同步 migrate) |
| GET/PUT | /api/platform/tenants/{id} | 详情 / 更新连接 |
| PUT | /api/platform/tenants/{id}/status | 启停 |
| POST | /api/platform/tenants/{id}/ping | 探测租户库连通性 |
| POST | /api/platform/tenants/{id}/migrate | 异步 migrate(body 可选 with_seed) |
| POST | /api/platform/tenants/{id}/seed | 异步 seed |
| POST | /api/platform/tenants/{id}/backup | 异步备份 |
平台控制台列表可操作单户 Ping / 迁移 / 种子 / 备份(入队 tenant_ops,long-running 队列)。migrate-all / restore / backup-all 仍仅 CLI。
公开支付回调(非 platform):
| Method | Path | 说明 |
|---|---|---|
| POST | /api/payment/notify/{type}/{tenant} | wechat|alipay;路径绑定租户 |
| POST | /api/payment/notify/{type} | tenancy 关闭时可用;开启时 tenant_required |
代码约定
| API | 用途 |
|---|---|
tenancy.Enabled() | 是否一户一库 |
OrmQuery(ctx) | 租户业务默认入口 |
PlatformOrmQuery(ctx) | 平台元数据 / 平台 token |
tenancyctx.Detach(ctx) | 异步 goroutine 保留租户连接 |
tenancy.CacheKey / StoragePrefix | 缓存键 / 对象存储路径前缀 |
RunTenantScope / --tenant | CLI 按租户执行 |
SchemaHasTable / WithSchemaContext | 请求路径 Schema(SchemaConnLock) |
硬性规则
- 业务用
OrmQuery(ctx);平台路由不挂Tenant中间件;平台元数据用PlatformOrmQuery(钉死平台连接)。 - 隔离是切库/切 Schema,不是行级
tenant_idGlobalScope。 - 缓存/上传(含
chunks/、导出、导入与附件临时目录)走租户前缀。 - 搜索索引绑定后为
{code}_orders;未绑定 fail-closed,禁止回退共享orders。 - 导出/搜索队列缺
tenant_id时 fail-closed,禁止写到平台库。 - HTTP 手动跑定时任务自动带当前
--tenant,禁止扫全租户;cron 可遍历启用租户。 - 队列表
jobs/failed_jobs读平台连接(QUEUE_DATABASE_CONNECTION)。 - 浏览器跨域 header 解析租户时,
CORS_ALLOWED_HEADERS须含X-Tenant-ID。 - 订单搜索用
search:*/SyncOrderSearch(SEARCH_*),勿再接旧 ES outbox 链路。 - IP 黑名单:进程内短 TTL(约 30s)缓存启用名单;CRUD 后立即失效。查库失败时在约 5 分钟内回退最近成功缓存,超时仍 fail-closed(503)。
- 仅
provision_status=ready的租户可绑定业务;HTTP 开户禁止同步 migrate,可走平台 UI 异步迁移或 CLItenant:migrate。 - 远程租户库禁止空账号回落平台 root;公网默认
TENANCY_ALLOW_PLATFORM_DB_CREDENTIALS=false。 - 平台表迁移不得落在租户库(
SkipOnTenantConnection);migrate 失败须可在平台侧看到last_migrate_error。 - PG schema 隔离的 backup/restore 必须限定 schema;登录对
tenant_not_ready返回 403(非 500)。 - 公网优先 subdomain;支付回调必须带
{type}/{tenant_code}路径。
