feat: complete tenant site provisioning flow

This commit is contained in:
2026-08-01 17:27:51 +08:00
parent d58bcd97e9
commit ecf506df8a
39 changed files with 22760 additions and 271 deletions

View File

@@ -92,7 +92,7 @@ API 不自动迁移数据库。
### 平台端
- 租户、Owner、域名、状态、员工、角色和审计告警。
- 租户、Owner、域名、状态、员工、角色和审计告警。租户开通在单一事务中创建 Owner、Pending 主域名、默认试用和 v1 前端配置;域名 DNS/TLS Active 后才允许平台领取一次性 Owner 激活链接。
- 平台公共题库、分类节点、题目、导入和资源上传。
- SaaS Feature、额度定义、套餐版本、报价、订单、支付、退款、订阅、发票和催缴。
- 平台级 CRM、短信渠道/模板和支付应用配置。

View File

@@ -79,6 +79,14 @@ Development 默认平台 Host 是 `localhost` 和 `127.0.0.1`。Production 启
客户端不得通过任意 header、query 或转发头绕过以上路径和可信代理限制。
### 主域名与 Owner 一次性激活
新租户创建时必须提供主域名。域名统一转换为小写 ASCII/IDN Host并使用随机 32 字节 Base64Url TXT 值验证所有权;只有 DNS 与 TLS 都成功后才进入 `Active`。平台领取 Owner 激活链接前还会校验租户、Active 主域名和试用/订阅状态。
激活链接格式固定为 `https://{primaryHost}/activate/{activationId}#token={token}`。Token 位于 fragment不进入 HTTP 请求、服务器访问日志或 RefererPostgreSQL 只保存 SHA-256 哈希。Grant 绑定签发时的 `DomainId`,浏览器激活要求请求 Host、已解析 Tenant、Grant 和 Active 主域名完全一致。并发签发由事务 advisory lock 和部分唯一索引收敛为一个有效 Grant幂等重放只返回 Grant ID 与过期时间,不能恢复明文。
浏览器激活在受审计事务中消费 Grant、设置密码、清除强制改密状态并创建数据库 Session。成功响应仅写入 HttpOnly access/refresh Cookie 与可读 CSRF Cookie不向 JavaScript 返回 Token Pair密码策略失败会整体回滚Grant 不会提前消费。
## 数据库租户隔离
当前 PostgreSQL 连接角色不依赖 RLS。租户隔离由以下机制共同完成

View File

@@ -75,6 +75,11 @@ Redis key 使用环境前缀;配置解析会强制 `AbortOnConnectFail=false`
"GatewayBaseUrl": null,
"GatewayApiKey": null
},
"TenantProvisioning": {
"DefaultBaseOfferingCode": "starter",
"DefaultTrialDays": 14,
"OwnerActivationMinutes": 30
},
"SaasSubscriptions": {
"Enabled": true,
"BatchSize": 100,
@@ -95,6 +100,8 @@ Redis key 使用环境前缀;配置解析会强制 `AbortOnConnectFail=false`
域名只有在 `AllowedCnameTargets`、DNS JSON endpoint、Gateway URL 和 API key 配置完成后,才可能从 Pending/Failed 进入 Active。仅 DNS 验证成功不代表 TLS 已就绪。
`TenantProvisioning:DefaultBaseOfferingCode` 必须指向 Active 基础套餐中当前有效的最新 Published 版本。Production API 启动时会查询 PostgreSQL 验证该版本存在;开通事务也会再次校验,缺失时返回 `default_offering_unavailable`,不会留下半成品租户。平台运营流程为:创建租户并抄录 CNAME/TXT → 等待域名 Active → 领取一次性激活链接 → 通过既有安全渠道交付 Owner。链接关闭后无法再次查看需要补发时必须填写原因并撤销旧链接。
后台任务状态和 `RunAfter` 存在 PostgreSQL。Worker 使用 `FOR UPDATE SKIP LOCKED`、五分钟租约和有限重试处理即时、延时及失败待重试任务;周期循环使用 PostgreSQL advisory lock 防止多实例重复执行。当前六个循环分别处理域名、订阅生命周期、Feature 用量、通用任务、授权缓存失效和商业账务。商业账务会生成续费应收、提醒、外部催缴投递和已审批退款Webhook 必须使用 HTTPS、Host allowlist、签名和私网地址拒绝。`Worker:Enabled=false` 会关闭全部循环,通常只用于测试或维护。
API 和 Worker 必须使用同一 PostgreSQL 数据库与一致的对象存储配置。迁移必须在两者启动前由 `Tiku.DbMigrator` 单独执行。

View File

@@ -1,158 +1,210 @@
# 本地开发与运行
# 空数据库到租户建站
本页用于从全新开发环境启动当前 TIKU Backend。数据库迁移由 DbMigrator 执行API 不会自动创建或更新 schema。
本页记录 Development 环境从全新 PostgreSQL 数据库完成平台管理员、租户、Owner 激活、网站发布和学生端访问的真实流程。数据库迁移`Tiku.DbMigrator` 执行API 不会自动更新 Schema。
## 1. 准备环境
必需:
- .NET 10 SDK
- PostgreSQL
- `psql``createdb` 等 PostgreSQL 命令行工具。
可选:
- Redis 7
- ClamAV验证资源安全扫描时需要
需要 .NET 10、Node.js 24+、npm 11+、PostgreSQL、Redis以及 `psql``createdb``dropdb`
```bash
dotnet --version
node --version
npm --version
pg_isready -h 127.0.0.1 -p 5432
psql --version
redis-cli -h 127.0.0.1 -p 6379 ping
```
## 2. 还原并构建
首次拉取代码后安装依赖:
```bash
git clone <repository-url> TIKU-BACKEND
cd TIKU-BACKEND
dotnet restore TIKU-BACKEND.slnx
dotnet build TIKU-BACKEND.slnx --no-restore
npm --prefix Tiku.PlatformAdmin.Web install
npm --prefix /path/to/tiku-saas-web install
```
## 3. 创建 PostgreSQL 数据库
## 2. 创建数据库
当前系统用户能本地登录 PostgreSQL 时:
以下操作只针对本地数据库 `tiku`。如果它已经包含需要保留的数据,请先备份,不要执行清理命令。
```bash
createdb -h 127.0.0.1 -U "$(whoami)" tiku
dropdb --if-exists -h 127.0.0.1 -U <数据库用户> tiku
createdb -h 127.0.0.1 -U <数据库用户> tiku
```
Development 未显式配置连接串时API、DbMigrator 和设计时 EF 工具默认使用
```text
Host=localhost;Database=tiku;Username=<当前系统用户>
```
其他用户、端口或认证方式使用环境变量:
Development 未配置连接串时默认使用当前系统用户连接本机 `tiku`。其他用户或端口应显式设置
```bash
export DATABASE_URL='Host=127.0.0.1;Port=5432;Database=tiku;Username=<数据库用户>;Password=<本地密码>'
export ConnectionStrings__Database='Host=127.0.0.1;Port=5432;Database=tiku;Username=<数据库用户>;Password=<本地密码>'
```
不要把含密码的连接串写入 `appsettings*.json`、README 或 Git。
不要把连接串、密码或 Token 写入 `appsettings*.json`、README 或 Git。
## 4. 执行迁移和 seed
## 3. 初始化目录、starter 套餐和首个平台账号
先设置一次性 Bootstrap 参数,再执行生产式空库初始化:
```bash
ASPNETCORE_ENVIRONMENT=Development dotnet run --project Tiku.DbMigrator
export ASPNETCORE_ENVIRONMENT=Development
export TIKU_BOOTSTRAP_PLATFORM_ADMIN_EMAIL='<平台管理员邮箱>'
export TIKU_BOOTSTRAP_PLATFORM_ADMIN_PASSWORD='<临时密码>'
export TIKU_BOOTSTRAP_PLATFORM_ADMIN_NAME='<显示名称>'
dotnet run --project Tiku.DbMigrator -- \
--skip-development-seed \
--bootstrap-platform-admin
```
DbMigrator 会
该命令按固定顺序执行
1. 执行所有 EF Core Migration
2. seed 内置 Feature、Permission、菜单和额度目录
3. 在全新 Development 数据库创建平台超级管理员。
1. EF Core Migration
2. Feature、Permission、菜单和额度目录
3. 内置 `starter` 套餐及其已发布版本;
4. 可选的平台超级管理员 Bootstrap。
```text
账号admin@tiku.local
密码:首次创建时随机生成,只在当前终端输出一次
```
`starter` 是零元、CNY、已发布的建站基础套餐包含 `core.backoffice``marketing.site_content`。首个平台账号使用临时密码,首次登录必须改密。
重复运行是幂等的,不会重置密码或再次显示临时密码。首次登录必须改密;不要为了找回密码删除已有业务数据的数据库。
Migration 需要 `citext``ltree``pg_trgm` 扩展。执行迁移的 PostgreSQL 用户必须有创建扩展的权限,或由管理员预先安装。
## 5. 启动 API
清除 Bootstrap 密码并再运行一次 Migrator确认日常重复执行不会创建演示租户或重复目录
```bash
dotnet run --project Tiku.Api
unset TIKU_BOOTSTRAP_PLATFORM_ADMIN_PASSWORD
unset TIKU_BOOTSTRAP_PLATFORM_ADMIN_EMAIL
unset TIKU_BOOTSTRAP_PLATFORM_ADMIN_NAME
dotnet run --project Tiku.DbMigrator -- --skip-development-seed
```
默认 Development 入口:
Migration 需要 `citext``ltree``pg_trgm` 扩展。执行用户必须可以创建这些扩展,或由数据库管理员预先安装。
- 平台管理端:首次在 `Tiku.PlatformAdmin.Web` 执行 `npm install`;之后启动 `Tiku.Api` 时会在 Development 自动启动前端,访问 <http://localhost:5173>
## 4. 启动真实服务
确保 Redis 已运行,然后从后端仓库启动 API
```bash
export ASPNETCORE_ENVIRONMENT=Development
export ConnectionStrings__Redis='localhost:6379,abortConnect=false'
dotnet run --project Tiku.Api --launch-profile http
```
API 在 Development 会同时拉起平台管理端 Vite 服务:
- 平台管理端:<http://localhost:5173>
- API<http://localhost:5090>
- Scalar<http://localhost:5090/scalar/v1>
- OpenAPI JSON<http://localhost:5090/openapi/v1.json>
- Liveness<http://localhost:5090/api/health>
- OpenAPI<http://localhost:5090/openapi/v1.json>
- Readiness<http://localhost:5090/api/health/ready>
OpenAPI 和 Scalar 仅在 Development 映射。接口路径、输入字段、响应模型和授权要求以这里生成的文档为准
不要使用 `http://localhost:5090/platform-admin/` 作为开发入口;该路径受 API 授权保护,未登录访问返回 401
## 6. 可选:启动 Redis
本地单实例开发可以不配置 Redis。需要验证安全频控、Feature 缓存和 Output Cache 时,先启动本地服务,再设置:
`tiku-saas-web` 仓库另开终端,使用真实 API 模式启动租户前端:
```bash
export ConnectionStrings__Redis='localhost:6379,abortConnect=false'
VITE_DATA_MODE=api \
VITE_DEV_API_TARGET='http://localhost:5090' \
npm run dev -- --host 0.0.0.0 --port 5180
```
Production 必须配置 RedisPostgreSQL 仍是用户、Session、权限、套餐和用量的权威数据源
浏览器始终请求同源 `/api`Vite 只在服务端把它代理到 `VITE_DEV_API_TARGET`。代理保留原始 Host因此 `school.localhost` 能由后端解析到正确租户
## 7. 启动 Worker 与 ClamAV
## 5. 浏览器建站流程
API 不处理后台循环。另开终端启动 Worker
### 5.1 平台首次登录
1. 打开 <http://localhost:5173>
2. 使用 Bootstrap 邮箱和临时密码登录;
3. 按页面要求设置新密码;
4. 进入“租户管理”。
### 5.2 创建租户和本地域名
点击“新建租户”,至少填写:
- 租户短编码,例如 `school`
- 租户名称;
- 主域名 `school.localhost`
- Owner 姓名;
- Owner 邮箱或手机号。
不选择套餐时,后端自动使用 `starter` 和默认试用天数。Development 配置只对精确的 `localhost``*.localhost` 启用 DNS/TLS 旁路通常数秒内显示“DNS 与 TLS 已激活”。其他域名仍走真实 DNS 和网关流程。
### 5.3 签发并消费 Owner 链接
1. 域名 Active 后点击“领取激活链接”;
2. 填写审计原因并确认;
3. 立即保存弹窗中的一次性链接;
4. 用完整链接打开 `http://school.localhost:5180/activate/...#token=...`
5. Owner 设置密码后自动进入 `/manage/onboarding`
Token 只在签发成功弹窗显示一次。租户前端读入 Fragment 后立即清除地址栏;后端消费后不能重放。平台管理员看不到 Owner 密码,也不需要审批 Owner 激活。
### 5.4 配置并发布
向导依次完成:品牌信息、模板、主题样式、页面模块、桌面/移动预览、发布上线。保存草稿不会影响学生端;发布成功后 Runtime 读取已发布配置。
打开或刷新 <http://school.localhost:5180/>,应看到新的品牌、导航、主题和首页模块。退出租户后台后,可在 <http://school.localhost:5180/manage/login> 使用 Owner 账号重新登录。
## 6. 预期状态
| 阶段 | 预期状态 |
| --- | --- |
| 刚创建域名 | `pending` |
| Development 后台任务处理完成 | 域名 `active` |
| Owner 尚未领取链接 | `ready_to_issue` |
| 链接签发 | `issued` |
| Owner 激活并登录 | 进入 `/manage/onboarding` |
| 草稿完成但未发布 | `ready_to_launch` |
| 发布完成 | 学生端显示已发布配置 |
## 7. 常见问题
### `.localhost` 一直 Pending
确认 API 使用 `ASPNETCORE_ENVIRONMENT=Development`,并加载:
```json
{
"TenantDomains": {
"PollSeconds": 2,
"EnableDevelopmentLocalhostBypass": true
}
}
```
API 日志应出现 `Processed ... pending Development tenant domains`。非 Development 环境启用该旁路会在启动时失败。
### 激活页只出现 OPTIONS没有 POST
不要把 API 地址配置为浏览器请求 Base URL。租户前端真实模式必须使用同源 `/api`,开发代理目标使用:
```bash
dotnet run --project Tiku.Worker
VITE_DEV_API_TARGET=http://localhost:5090
```
`Worker__Enabled=false` 仅用于测试或维护。后台任务状态、租约、重试和 `RunAfter` 存在 PostgreSQL多个 Worker 通过 advisory lock 和任务租约协调。域名 DNS/TLS 流程只有在 `TenantDomains` 的 CNAME target 和 Gateway 配置完整后才能激活自定义域名
确保旧的 `VITE_API_BASE_URL` 未注入进程,并重启 Vite
上传确认会创建 `asset_security_scan` 任务。Worker 通过 TCP 3310 连接 ClamAV且 ClamAV `StreamMaxLength` 必须不小于 `Storage:MaxUploadBytes`(当前默认均为 500 MiB。本地可以使用容器启动 ClamAV并确保该限制已配置ClamAV 不可用时任务会重试,资源保持不可访问。
### 激活失败后地址栏已没有 Token
## 8. 开发验证
如果后端尚未消费 Token可重新打开平台最初交付的完整链接。已经消费、过期或丢失明文时必须由平台撤销并重新签发不能从数据库或日志恢复。
```bash
curl --fail http://localhost:5090/api/health
curl --fail http://localhost:5090/api/health/ready
### 发布后旧标签仍显示筹备页
dotnet test TIKU-BACKEND.slnx --no-build
dotnet format TIKU-BACKEND.slnx --verify-no-changes --no-restore
dotnet ef migrations has-pending-model-changes \
--project Tiku.Infrastructure \
--startup-project Tiku.DbMigrator \
--no-build
git diff --check
```
草稿与已发布配置相互隔离。确认向导显示发布成功后,刷新学生端标签,让它重新请求 Runtime Bootstrap。
`Tiku.IntegrationTests` 会创建临时 PostgreSQL 数据库,验证 API、授权、迁移和租户隔离。测试账户和测试数据库只用于自动化验证。
### Cookie 没有建立
## 常见问题
### 连接 PostgreSQL 失败
```bash
pg_isready -h 127.0.0.1 -p 5432
psql -h 127.0.0.1 -U <数据库用户> -d postgres -c 'select current_user;'
```
确认当前终端的 `DATABASE_URL` 指向真实存在的数据库,并且 API 与 DbMigrator 使用同一连接配置。
### 无法创建 PostgreSQL 扩展
请让数据库管理员安装 `citext``ltree``pg_trgm`,或授予迁移用户创建这些扩展所需的权限。
### 没看到管理员临时密码
临时密码只在全新 Development 数据库第一次创建管理员时显示。已有管理员时 DbMigrator 会跳过;应使用正常密码恢复流程。
必须通过 `school.localhost:5180` 访问激活和后台,不能改用 `localhost:5180` 或把 `tenantId` 填进请求。浏览器 Session 使用 Secure/HttpOnly Cookie写请求使用 CSRF 双提交 Token前端不得降级保存 JWT。
### Readiness 返回 503
匿名 readiness 只返回总体 `status``checkedAt`。配置了 Redis 连接串但服务未启动时会返回 503依赖细节需要使用具有 `platform:operations:view` 权限的平台账号访问 `/api/platform-admin/operations/health`
确认 PostgreSQL 和 Redis 均可访问。匿名 readiness 只返回总体状态;依赖详情需要具有 `platform:operations:view` 权限的平台账号
### API 出现 HTTPS 重定向警告
## 8. 清理
仅使用 HTTP launch profile 时可能无法确定 HTTPS 端口,不影响 `http://localhost:5090` 的本地访问。需要验证 HTTPS 时使用项目的 `https` profile。
先停止 API、平台 Vite 和租户 Vite再只删除明确的本地数据库
```bash
dropdb -h 127.0.0.1 -U <数据库用户> tiku
```
该操作不可恢复会删除本轮创建的平台账号、租户、Session、草稿和发布配置。
更多配置见[配置与后台任务](operations.md),安全边界见[认证、授权与租户隔离](architecture/security-and-tenancy.md)。