- Remove outdated development plan from `development-plan.md`. - Update `operations.md` to include details on authorization cache configuration. - Revise `quickstart.md` for clarity on local development setup and database initialization. - Delete redundant `redis-authorization-cache.md`. - Add new `tenant-provisioning.md` to document the process of setting up a tenant from an empty database.
This commit is contained in:
@@ -2,14 +2,25 @@
|
||||
|
||||
这里仅记录当前代码已经实现的架构、运行方式和维护约束。接口细节以 Development 环境的 OpenAPI/Scalar 为准,数据库结构以 EF Core Migration 和模型快照为准。
|
||||
|
||||
## 阅读入口
|
||||
## 从这里开始
|
||||
|
||||
| 文档 | 内容 | 适合谁 |
|
||||
| --- | --- | --- |
|
||||
| [本地开发与运行](quickstart.md) | PostgreSQL 初始化、启动 API、验证命令、常见问题 | 新开发者 |
|
||||
| [系统架构与业务边界](architecture/overview.md) | 项目依赖、运行时组件、当前业务模块和后台任务链路 | 开发与评审人员 |
|
||||
| [认证、授权与租户隔离](architecture/security-and-tenancy.md) | 登录、Session、JWT、Cookie/CSRF、Realm、RBAC、Capability、DataScope、租户隔离 | API 与安全开发者 |
|
||||
| [配置与后台任务](operations.md) | 环境配置、Production 启动门禁、Redis、Worker、ClamAV 和健康检查 | 开发与运维人员 |
|
||||
| 目标 | 文档 |
|
||||
| --- | --- |
|
||||
| 首次拉取代码,启动本地开发环境 | [本地开发快速上手](quickstart.md) |
|
||||
| 从空库验收租户创建、Owner 激活和站点发布 | [空数据库到租户建站验收](tenant-provisioning.md) |
|
||||
| 理解项目分层、运行时和业务边界 | [系统架构与业务边界](architecture/overview.md) |
|
||||
| 修改认证、权限或租户数据 | [认证、授权与租户隔离](architecture/security-and-tenancy.md) |
|
||||
| 部署 API/Worker、配置依赖或排查任务 | [配置与后台任务](operations.md) |
|
||||
|
||||
## 文档边界
|
||||
|
||||
- `README.md`:项目定位、最短启动入口和仓库地图。
|
||||
- `docs/quickstart.md`:非破坏性的日常开发路径。
|
||||
- `docs/tenant-provisioning.md`:会创建或清理本地数据库的完整 SaaS 验收路径。
|
||||
- `docs/architecture/**`:当前代码的稳定设计与安全边界。
|
||||
- `docs/operations.md`:部署配置、后台处理、健康检查和恢复演练。
|
||||
|
||||
路线图、阶段计划、代码评审快照和历史迁移过程不放在当前实现文档中;需要保留时应进入 Issue、项目管理系统或明确标记的历史归档。
|
||||
|
||||
## 权威来源
|
||||
|
||||
@@ -25,4 +36,5 @@
|
||||
2. 新增租户实体时,同时验证 Query Filter、租户唯一索引、组合外键和写入拦截器。
|
||||
3. 数据库结构变更必须生成 EF Core Migration,并检查 migration script 和 pending model changes。
|
||||
4. 不在文档中保存连接密码、JWT 私钥、证书密码、Provider 密钥或平台管理员临时密码。
|
||||
5. 不再维护阶段路线图、旧后端接口对比、评审快照或开发过程记录。
|
||||
5. 代码行为变化时更新对应主题,不新增内容重叠的临时说明文件。
|
||||
6. 不维护阶段路线图、旧后端接口对比、评审快照或开发过程记录。
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
`Tiku.Api/Program.cs` 只负责组合服务、构建应用和启用请求管线。管线的关键顺序是:
|
||||
|
||||
```text
|
||||
Forwarded Headers
|
||||
-> HTTPS / 压缩 / 静态文件
|
||||
结构化请求日志 / 数据库请求指标 / 异常处理
|
||||
-> Forwarded Headers / HTTPS / 响应压缩
|
||||
-> Routing / CORS
|
||||
-> Host 租户解析
|
||||
-> 浏览器 CSRF
|
||||
@@ -49,7 +49,7 @@ Forwarded Headers
|
||||
-> Controllers
|
||||
```
|
||||
|
||||
OpenAPI 和 Scalar 只在 Development 映射。平台管理端位于独立的 `Tiku.PlatformAdmin.Web` React 工程,由 OpenAPI 生成接口契约并单独构建、部署;`Tiku.Api` 不再托管平台前端静态文件。
|
||||
OpenAPI 和 Scalar 只在 Development 映射。平台管理端位于独立的 `Tiku.PlatformAdmin.Web` React 工程,由 OpenAPI 生成接口契约;Development 通过 `Tiku.Api.csproj` 的 SPA Proxy 启动 Vite,Production 仍应独立构建和部署,API 不托管平台前端静态文件。
|
||||
|
||||
### DbMigrator
|
||||
|
||||
@@ -58,14 +58,14 @@ OpenAPI 和 Scalar 只在 Development 映射。平台管理端位于独立的 `T
|
||||
1. 解析 `ConnectionStrings:Database` 或 `DATABASE_URL`。
|
||||
2. 进入带审计原因的 System Scope。
|
||||
3. 执行 `Database.MigrateAsync()`。
|
||||
4. seed 内置 SaaS Feature、PermissionModule、BackendPermission 和 BackendMenu 目录。
|
||||
5. Development 全新数据库自动 seed 平台管理员;非 Development 仅在显式传入 `--bootstrap-platform-admin` 时创建管理员。
|
||||
4. seed 内置 SaaS Feature、PermissionModule、BackendPermission、BackendMenu 和 `starter` 套餐目录。
|
||||
5. 默认 Development seed 在尚无平台角色绑定时创建平台管理员与演示数据;`--skip-development-seed` 可关闭该行为,`--bootstrap-platform-admin` 用于显式创建首个平台管理员。
|
||||
|
||||
API 不自动迁移数据库。
|
||||
|
||||
### Worker 后台处理
|
||||
|
||||
`Tiku.Worker` 独立注册六个 Hosted Service,API 进程不注册后台循环:
|
||||
`Tiku.Worker` 独立注册六个 Hosted Service。API 不承载这些生产后台循环;仅 Development 会额外注册 `.localhost` 域名生命周期旁路,方便本地建站验收。
|
||||
|
||||
| Worker | 周期 | 当前职责 |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -83,7 +83,7 @@ Development 默认平台 Host 是 `localhost` 和 `127.0.0.1`。Production 启
|
||||
|
||||
新租户创建时必须提供主域名。域名统一转换为小写 ASCII/IDN Host,并使用随机 32 字节 Base64Url TXT 值验证所有权;只有 DNS 与 TLS 都成功后才进入 `Active`。平台领取 Owner 激活链接前还会校验租户、Active 主域名和试用/订阅状态。
|
||||
|
||||
激活链接格式固定为 `https://{primaryHost}/activate/{activationId}#token={token}`。Token 位于 fragment,不进入 HTTP 请求、服务器访问日志或 Referer;PostgreSQL 只保存 SHA-256 哈希。Grant 绑定签发时的 `DomainId`,浏览器激活要求请求 Host、已解析 Tenant、Grant 和 Active 主域名完全一致。并发签发由事务 advisory lock 和部分唯一索引收敛为一个有效 Grant;幂等重放只返回 Grant ID 与过期时间,不能恢复明文。
|
||||
Production 默认激活链接为 `https://{primaryHost}/activate/{activationId}#token={token}`;Development 可通过专用 localhost 模板指向租户前端端口。Token 位于 fragment,不进入 HTTP 请求、服务器访问日志或 Referer;PostgreSQL 只保存 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 不会提前消费。
|
||||
|
||||
@@ -124,6 +124,18 @@ Development 默认平台 Host 是 `localhost` 和 `127.0.0.1`。Production 启
|
||||
- 租户、套餐和 Feature 变更在数据库提交后直接失效当前 API 进程与 Redis 中的相关缓存。
|
||||
- 后台任务在业务事务提交后持久化到 PostgreSQL,独立 Worker 使用租约执行;延时和重试由 `RunAfter` 控制。
|
||||
|
||||
## Redis 授权缓存模式
|
||||
|
||||
`Security:AuthorizationCache:Mode` 支持三种当前实现模式:
|
||||
|
||||
- `Disabled`:直接以 PostgreSQL 完成安全状态和权限读取。
|
||||
- `Shadow`:PostgreSQL 决策仍为准,同时读取、回填并比较 Redis 结果。
|
||||
- `Active`:优先读取 Redis 安全状态与权限快照;缓存缺失或 Redis 不可用时回退 PostgreSQL。
|
||||
|
||||
缓存键按环境、realm、租户、用户和 Session 隔离,不保存 JWT、Refresh Token、手机号或邮箱。权限快照键包含持久化授权版本;用户、成员、租户、Session 或角色权限变化会推进版本并触发缓存失效。提交后若 Redis 失效失败,事件保留在 PostgreSQL,由 `AuthorizationCacheInvalidationWorker` 重试;旧版本不会覆盖新版本。
|
||||
|
||||
Redis 不是授权事实源。Redis 与 PostgreSQL 同时无法完成安全校验时返回 503 `auth_security_unavailable`,不能使用本地旧快照放行。
|
||||
|
||||
## 安全配置门禁
|
||||
|
||||
Production 还会在启动时验证 Redis、Data Protection 证书、租户 Secret master key、短信 pepper、CORS 和外部服务配置。完整配置入口见[配置与后台任务](../operations.md)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1080" viewBox="0 0 1600 1080" role="img" aria-labelledby="title desc">
|
||||
<title id="title">TIKU Backend 当前技术架构图</title>
|
||||
<desc id="desc">TIKU Backend 是 ASP.NET Core 模块化单体。API 承载 HTTP 与后台 Hosted Service,DbMigrator 负责迁移,运行时依赖 PostgreSQL、Redis 和外部服务。</desc>
|
||||
<desc id="desc">TIKU Backend 是 ASP.NET Core 模块化单体。API 承载 HTTP,Worker 独立处理后台任务,DbMigrator 负责迁移,运行时依赖 PostgreSQL、Redis 和外部服务。</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0 0L10 5L0 10Z" fill="#334155"/>
|
||||
@@ -46,7 +46,7 @@
|
||||
<circle class="badge" cx="96" cy="211" r="18"/>
|
||||
<text class="badge-text" x="96" y="211">P</text>
|
||||
<text class="node-title" x="124" y="207">平台管理端</text>
|
||||
<text class="node-text" x="84" y="239">/platform-admin 静态管理端</text>
|
||||
<text class="node-text" x="84" y="239">React / Vite 管理端</text>
|
||||
<text class="node-tiny" x="84" y="258">平台账号 · SaaS 运营</text>
|
||||
|
||||
<rect class="node-blue" x="66" y="307" width="178" height="92" rx="6"/>
|
||||
@@ -79,12 +79,12 @@
|
||||
<text class="badge-text" x="371" y="211">API</text>
|
||||
<text class="node-title" x="402" y="207">Tiku.Api</text>
|
||||
<text class="node-text" x="356" y="239">ASP.NET Core Controller API</text>
|
||||
<text class="node-tiny" x="356" y="258">OpenAPI / Scalar · 静态文件</text>
|
||||
<text class="node-tiny" x="356" y="258">OpenAPI / Scalar(Development)</text>
|
||||
|
||||
<rect class="node" x="338" y="307" width="299" height="184" rx="6"/>
|
||||
<text class="node-title" x="358" y="337">请求管线(按执行顺序)</text>
|
||||
<text class="node-text" x="358" y="367">1 日志 / 异常 / Forwarded Headers / HTTPS</text>
|
||||
<text class="node-text" x="358" y="393">2 Static / Routing / CORS</text>
|
||||
<text class="node-text" x="358" y="393">2 响应压缩 / Routing / CORS</text>
|
||||
<text class="node-text" x="358" y="419">3 可信 Host 租户解析 / Browser CSRF</text>
|
||||
<text class="node-text" x="358" y="445">4 JWT / 认证分区限流 / Rate Limiter</text>
|
||||
<text class="node-text" x="358" y="471">5 CurrentPrincipal / Authorization / Feature</text>
|
||||
@@ -148,18 +148,18 @@
|
||||
<text class="edge-label" x="844" y="418">实现 Application 接口并操作 Domain</text>
|
||||
|
||||
<!-- Processes -->
|
||||
<text class="group-title" x="1318" y="129">单体后台处理与迁移</text>
|
||||
<text class="group-title" x="1318" y="129">后台处理与迁移</text>
|
||||
<rect class="group" x="1310" y="145" width="250" height="570" rx="4"/>
|
||||
|
||||
<rect class="node-green" x="1336" y="180" width="198" height="192" rx="6"/>
|
||||
<circle class="badge" cx="1368" cy="211" r="18"/>
|
||||
<text class="badge-text" x="1368" y="211">BG</text>
|
||||
<text class="node-title" x="1398" y="207">API Hosted Services</text>
|
||||
<text class="node-text" x="1354" y="242">域名 DNS / TLS 生命周期</text>
|
||||
<text class="node-text" x="1354" y="269">SaaS 订阅生命周期</text>
|
||||
<text class="node-text" x="1354" y="296">Feature 用量校准</text>
|
||||
<text class="node-text" x="1354" y="323">后台任务租约执行</text>
|
||||
<text class="node-text" x="1354" y="350">与 HTTP 共用 API 进程</text>
|
||||
<text class="node-title" x="1398" y="207">Tiku.Worker</text>
|
||||
<text class="node-text" x="1354" y="242">独立后台运行时</text>
|
||||
<text class="node-text" x="1354" y="269">域名 / 订阅 / 用量</text>
|
||||
<text class="node-text" x="1354" y="296">任务 / 授权缓存失效</text>
|
||||
<text class="node-text" x="1354" y="323">商业账务 / Worker 心跳</text>
|
||||
<text class="node-text" x="1354" y="350">PostgreSQL 锁与任务租约</text>
|
||||
|
||||
<rect class="node-amber" x="1336" y="408" width="198" height="144" rx="6"/>
|
||||
<circle class="badge" cx="1368" cy="439" r="18"/>
|
||||
@@ -202,7 +202,7 @@
|
||||
<text class="node-title" x="678" y="854">PostgreSQL 任务租约</text>
|
||||
<text class="node-text" x="631" y="892">即时任务 / RunAfter</text>
|
||||
<text class="node-text" x="631" y="917">SKIP LOCKED / 重试</text>
|
||||
<text class="node-tiny" x="631" y="944">API Hosted Service 执行</text>
|
||||
<text class="node-tiny" x="631" y="944">Tiku.Worker 独立执行</text>
|
||||
|
||||
<rect class="node-blue" x="866" y="826" width="205" height="138" rx="6"/>
|
||||
<text class="node-title" x="890" y="856">对象存储</text>
|
||||
@@ -232,5 +232,5 @@
|
||||
|
||||
<!-- Project dependency direction -->
|
||||
<rect class="boundary" x="40" y="1025" width="1520" height="38" rx="4"/>
|
||||
<text class="boundary-text" x="61" y="1050">项目依赖方向:Api → Application + Infrastructure | DbMigrator → Infrastructure | Infrastructure → Application + Domain | Application → Domain</text>
|
||||
<text class="boundary-text" x="61" y="1050">项目依赖方向:Api / Worker → Application + Infrastructure | DbMigrator → Infrastructure | Infrastructure → Application + Domain | Application → Domain</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,335 +0,0 @@
|
||||
# TIKU Backend 后续开发计划
|
||||
|
||||
## 1. 计划目标
|
||||
|
||||
后续开发围绕教育业务闭环、SaaS 产品化、可靠性、性能和可运营性推进。每个阶段必须形成可独立验收的业务结果,不以接口数量或数据表数量作为完成标准。
|
||||
|
||||
总体目标:
|
||||
|
||||
- 建立服务端可信的练习、评分、报告和学习分析链路。
|
||||
- 完成内容生产、教学组织、学生学习和教师干预闭环。
|
||||
- 打通套餐、权益、商品、订单、支付和教育资源访问。
|
||||
- 提供平台端、租户端、教师端和学生端可实际使用的工作流。
|
||||
- 建立真实 PostgreSQL、Redis 和对象存储路径下的质量与性能门禁。
|
||||
- 保持模块化单体边界,在明确出现独立扩缩容需求前不拆分微服务。
|
||||
|
||||
## 2. 实施原则
|
||||
|
||||
- PostgreSQL 是业务状态、权限、权益、余额、订单和任务状态的权威来源。
|
||||
- Redis 只用于缓存、限流和可重建的加速数据,不作为授权或交易事实来源。
|
||||
- 客户端提交的用户、租户、角色、正确性、价格、权益和状态均不可信,必须由服务端解析或计算。
|
||||
- 所有可能被网络重试的写操作必须具备幂等语义。
|
||||
- 所有状态转换必须定义合法前置状态,并通过事务或比较并交换更新保证并发安全。
|
||||
- 所有租户数据访问必须同时满足租户隔离、操作权限、DataScope、套餐权益和资源可见性规则。
|
||||
- 导入、导出、聚合、提醒、扫描等长任务统一进入 `background_jobs`。
|
||||
- 前端接口以实时 OpenAPI 为唯一契约来源,不维护第二套手写接口模型。
|
||||
- 每个行为变更都要有回归测试;PostgreSQL 特有约束必须使用真实 PostgreSQL 验证。
|
||||
|
||||
## 3. 阶段总览
|
||||
|
||||
| 阶段 | 主题 | 建议周期 | 主要结果 |
|
||||
| --- | --- | ---: | --- |
|
||||
| P0 | 学习核心链路可信化 | 2 个迭代 | 服务端判分、幂等答题、并发安全交卷、不可变报告 |
|
||||
| P1 | 内容生产与练习编排 | 2 个迭代 | 题目生命周期、题集、练习蓝图、导入发布闭环 |
|
||||
| P2 | 教学组织与教师运营 | 2~3 个迭代 | 班级、任务、提醒、学情和学生干预 |
|
||||
| P3 | 学生学习与个性化 | 2~3 个迭代 | 错题复习、间隔学习、自适应练习和成长激励 |
|
||||
| P4 | SaaS 权益与商业闭环 | 2 个迭代 | 套餐权益、教育商品、支付、退款和激活码联动 |
|
||||
| P5 | 产品界面与契约交付 | 持续并行 | 四类用户拥有完整可操作工作流 |
|
||||
| P6 | 性能、扩展与可观测性 | 持续并行 | 业务压测、聚合读模型、多实例安全和 SLO 告警 |
|
||||
|
||||
周期用于安排依赖关系,不作为压缩验收范围的依据。阶段可以并行准备,但不得越过前置发布门禁。
|
||||
|
||||
## 4. P0:学习核心链路可信化
|
||||
|
||||
### 4.1 题目与评分快照
|
||||
|
||||
- 为练习会话题目保存题目 ID、版本 ID、题型、题干、选项、标准答案、解析、分值和评分规则快照。
|
||||
- 会话创建后不得因题库后续编辑而改变本次练习的评分结果。
|
||||
- 学生会话详情只能返回安全投影,不返回标准答案、解析或选项正确标记。
|
||||
- 为单选、多选、判断、填空建立独立服务端评分器。
|
||||
- 主观题支持 `PendingReview`、`TeacherReviewed` 和 `SelfPractice` 等明确状态。
|
||||
- 自评结果只能用于练习反馈,不计入权威成绩、积分和排行榜。
|
||||
|
||||
### 4.2 答题幂等与并发控制
|
||||
|
||||
- 答题 DTO 增加 `IdempotencyKey`、`ExpectedSessionVersion` 和 `ClientSequence`。
|
||||
- 建立答题幂等记录,保存操作类型、请求哈希、响应快照和完成状态。
|
||||
- 相同幂等键与相同请求体必须重放原响应。
|
||||
- 相同幂等键与不同请求体必须返回明确冲突。
|
||||
- 练习会话增加并发版本和最后客户端序列。
|
||||
- 使用数据库唯一约束和比较并交换更新阻止重复答案、乱序写入和多设备并发覆盖。
|
||||
- 明确答案修改策略:允许覆盖时保留版本历史;不允许覆盖时返回稳定错误码。
|
||||
|
||||
### 4.3 交卷与报告状态机
|
||||
|
||||
- 定义 `Active → Scoring → Submitted`、`Active → Expired`、`Active → Cancelled` 状态转换。
|
||||
- 交卷请求增加幂等键,并保证同一会话只能生成一份权威报告。
|
||||
- 评分、错题更新、积分事件和报告生成在同一事务边界内提交,或通过同事务创建的后台任务可靠续办。
|
||||
- 报告发布后保持不可变;重新评分必须生成新版本并记录原因、操作者和审计事件。
|
||||
- 处理评分中断、任务重试和超时租约回收。
|
||||
|
||||
### 4.4 P0 测试与发布门禁
|
||||
|
||||
- 覆盖重复答题、幂等冲突、旧版本、乱序序列、双设备并发和重复交卷。
|
||||
- 覆盖跨租户、非本人会话、过期会话、已提交会话和未授权题目。
|
||||
- 覆盖学生响应不泄露答案及解析。
|
||||
- 覆盖各客观题型的服务端判分和边界输入。
|
||||
- 使用真实 PostgreSQL 验证唯一约束、事务竞争和失败回滚。
|
||||
- 完成迁移首次执行及幂等第二次执行验证。
|
||||
|
||||
完成标准:客户端无法伪造成绩;网络重试不会产生重复答案、重复报告、重复积分或重复错题计数。
|
||||
|
||||
## 5. P1:内容生产与练习编排
|
||||
|
||||
### 5.1 内容生命周期
|
||||
|
||||
- 统一题目、目录节点、题集和练习蓝图的 `Draft → Review → Published → Archived` 生命周期。
|
||||
- 增加乐观版本、状态前置校验、发布审计和归档恢复规则。
|
||||
- 支持题目版本对比、引用关系查看和安全回滚。
|
||||
- 发布前校验题干、选项、答案、解析、分类、资源和题型结构完整性。
|
||||
- 已被历史练习引用的版本不得物理删除。
|
||||
|
||||
### 5.2 题集与练习蓝图
|
||||
|
||||
- 支持人工题集、动态规则题集和固定快照题集。
|
||||
- 练习蓝图支持知识点、题型、难度、题量、分值、随机种子和去重规则。
|
||||
- 创建练习时记录蓝图版本、抽题结果和随机种子,确保结果可重放。
|
||||
- 增加题量不足、资源不可见、跨租户引用和已归档内容的失败关闭规则。
|
||||
|
||||
### 5.3 导入与批量运营
|
||||
|
||||
- 导入流程拆分为上传、解析、预检、确认、执行和结果下载。
|
||||
- 预检结果精确到行、字段和错误代码。
|
||||
- 导入支持幂等键、来源哈希和重复内容策略。
|
||||
- 批量发布、归档和分类必须提供影响范围预览。
|
||||
- 大批量任务进入后台任务系统,并支持进度、取消、重试和结果资产。
|
||||
|
||||
### 5.4 内容质量指标
|
||||
|
||||
- 记录题目使用次数、作答人数、正确率、平均耗时、跳过率和争议率。
|
||||
- 为低质量、异常高正确率、异常低正确率和长期未使用题目提供运营筛选。
|
||||
- 质量指标采用异步增量聚合,不在学生提交请求中执行大范围统计。
|
||||
|
||||
完成标准:运营人员可以从草稿创建到发布、组卷、导入和质量复盘完成完整工作流。
|
||||
|
||||
## 6. P2:教学组织与教师运营
|
||||
|
||||
### 6.1 班级与成员
|
||||
|
||||
- 完善班级、教师、助教、学生和分组模型。
|
||||
- 支持邀请、批量导入、转班、退班、冻结和历史成员查询。
|
||||
- 所有成员变更记录操作者、原因、时间和前后状态。
|
||||
- DataScope 支持按校区、部门、班级和本人范围过滤。
|
||||
|
||||
### 6.2 学习任务
|
||||
|
||||
- 支持练习、每日一练、作业和考试任务。
|
||||
- 支持目标班级、分组、指定学生、发布时间、截止时间、补交和重做策略。
|
||||
- 发布任务时固定内容或蓝图版本,避免后续编辑改变已发布任务。
|
||||
- 学生任务列表明确返回未开始、进行中、已提交、已逾期和已批改状态。
|
||||
|
||||
### 6.3 提醒与通知
|
||||
|
||||
- 支持任务发布、即将截止、逾期、批改完成和权益到期提醒。
|
||||
- 提醒任务使用幂等键和领取租约,防止多实例重复发送。
|
||||
- 通知记录渠道、模板版本、发送状态、失败原因和重试次数。
|
||||
- 支持租户级通知策略、免打扰时段和渠道开关。
|
||||
|
||||
### 6.4 学情与干预
|
||||
|
||||
- 建立学生、班级、知识点和任务维度的学习统计。
|
||||
- 支持未完成、连续退步、薄弱知识点和异常作答行为规则。
|
||||
- 风险命中后生成教师待办,可记录联系、备注、处理结果和下次跟进时间。
|
||||
- 教师只能查看 DataScope 允许的学生和班级。
|
||||
|
||||
完成标准:教师能够发布任务、查看进度、完成批改、识别风险并记录干预结果。
|
||||
|
||||
## 7. P3:学生学习与个性化
|
||||
|
||||
### 7.1 错题复习
|
||||
|
||||
- 错题记录包含知识点、错误次数、最近错误、最近复习、掌握状态和下一次复习时间。
|
||||
- 区分未掌握、学习中、待巩固和已掌握状态。
|
||||
- 复习结果更新间隔,不以单次答对立即永久解决。
|
||||
- 错题复习生成稳定会话,并记录生成规则版本。
|
||||
|
||||
### 7.2 词汇与间隔学习
|
||||
|
||||
- 定义明确的间隔重复算法、等级、下次复习时间和遗忘重置规则。
|
||||
- 复习计划按到期时间、掌握程度和每日上限生成。
|
||||
- 算法版本和关键输入写入学习事件,保证结果可解释。
|
||||
|
||||
### 7.3 自适应练习
|
||||
|
||||
- 建立用户知识点掌握度读模型。
|
||||
- 根据最近表现、题目难度、重复间隔和任务目标选择题目。
|
||||
- 推荐结果保存输入快照、算法版本、候选集合和最终选择。
|
||||
- 提供固定规则回退,推荐服务异常时仍可生成可用练习。
|
||||
|
||||
### 7.4 成长与激励
|
||||
|
||||
- 徽章、连续学习、积分和阶段目标使用可配置规则。
|
||||
- 奖励发放必须幂等,并记录触发事件和规则版本。
|
||||
- 排行榜支持租户、班级、时间范围和隐私开关。
|
||||
- 防止通过重复提交、回放请求或自评结果刷取奖励。
|
||||
|
||||
完成标准:学生可以获得稳定、可解释且不会重复奖励的个性化学习计划。
|
||||
|
||||
## 8. P4:SaaS 权益与商业闭环
|
||||
|
||||
### 8.1 统一权益判定
|
||||
|
||||
- 建立统一权益服务,组合操作权限、DataScope、套餐 Feature、额度、区域策略和灰度状态。
|
||||
- 统一返回拒绝原因和可观测诊断信息。
|
||||
- 前端 bootstrap 只展示最终可用能力,但后端仍独立执行完整判定。
|
||||
- 权益变更后可靠失效本地缓存和 Redis 缓存。
|
||||
|
||||
### 8.2 教育商品与权益发放
|
||||
|
||||
- 商品可绑定课程、题库、练习次数、有效期和会员等级。
|
||||
- 支付成功后幂等发放权益。
|
||||
- 退款、撤销、订单关闭和订阅到期执行明确的权益回收或冻结策略。
|
||||
- 保留每次权益变更的来源订单、规则、操作者和审计记录。
|
||||
|
||||
### 8.3 激活码
|
||||
|
||||
- 支持批次、渠道、数量、有效期、领取限制和适用租户。
|
||||
- 激活过程使用事务和唯一约束,防止重复核销。
|
||||
- 激活结果与权益服务联动,并支持撤销审计。
|
||||
- 提供批次使用率、渠道效果和异常核销报表。
|
||||
|
||||
### 8.4 租户自助运营
|
||||
|
||||
- onboarding 使用可恢复状态机覆盖租户、Owner、域名、品牌、套餐和支付配置。
|
||||
- 域名验证、证书和网关配置进入后台任务并保留诊断信息。
|
||||
- 主题、导航和模块配置支持草稿、预览、发布、版本和回滚。
|
||||
- 平台支持面向指定租户查看能力、用量、账务、任务和配置异常。
|
||||
|
||||
完成标准:租户能够完成开通、配置、购买、使用、续费和故障诊断,不依赖人工修改数据库。
|
||||
|
||||
## 9. P5:产品界面与契约交付
|
||||
|
||||
### 9.1 平台端
|
||||
|
||||
- 完成租户 onboarding、域名、套餐、账务、用量、题库和运营任务工作台。
|
||||
- 所有长任务显示进度、失败原因、重试和结果下载入口。
|
||||
- 提供租户能力诊断和配置版本回滚入口。
|
||||
|
||||
### 9.2 租户管理端与教师端
|
||||
|
||||
- 完成员工、角色、学生、班级、内容、任务、学情、商品和订单工作流。
|
||||
- 页面操作权限与后端权限清单保持一致。
|
||||
- 批量操作必须先展示影响范围和失败明细。
|
||||
|
||||
### 9.3 学生端
|
||||
|
||||
- 完成运行时 bootstrap、登录、任务、练习、错题、词汇、视频、权益和订单闭环。
|
||||
- 网络重试统一携带幂等键和客户端序列。
|
||||
- 明确处理会话过期、版本冲突、权益不足和任务已结束状态。
|
||||
|
||||
### 9.4 契约门禁
|
||||
|
||||
- 每次 Controller 变更后重新生成 OpenAPI 和前端类型。
|
||||
- CI 检查 OpenAPI operation、DTO 和前端生成产物是否同步。
|
||||
- 禁止前端手写后端枚举值、权限代码和请求模型。
|
||||
|
||||
完成标准:每类用户都可以在界面中完成对应业务闭环,且不存在仅有 API、没有可操作入口的已发布能力。
|
||||
|
||||
## 10. P6:性能、扩展与可观测性
|
||||
|
||||
### 10.1 查询与读模型
|
||||
|
||||
- 将学习统计的多次顺序查询改为条件聚合或专用汇总读模型。
|
||||
- 将趋势按日期、知识点和班级的聚合下推 PostgreSQL。
|
||||
- 为高频筛选建立与实际查询匹配的复合索引,并使用真实执行计划验证。
|
||||
- 控制分页上限,避免无界列表和大对象图加载。
|
||||
- 为核心端点建立单请求数据库命令数预算。
|
||||
|
||||
### 10.2 缓存策略
|
||||
|
||||
- 缓存目录、运行时配置、Feature 快照、公共内容和稳定聚合结果。
|
||||
- 缓存键必须包含租户、资源版本和影响结果的策略版本。
|
||||
- 写操作提交成功后再执行缓存失效;失效失败必须可重试。
|
||||
- 记录缓存命中率、回源率、失效延迟和热键。
|
||||
|
||||
### 10.3 后台处理与多实例
|
||||
|
||||
- 所有任务领取继续使用 PostgreSQL 租约和 `FOR UPDATE SKIP LOCKED`。
|
||||
- 生命周期扫描和周期性调度增加跨实例互斥或唯一调度记录。
|
||||
- 任务处理器必须幂等,并区分可重试和永久失败。
|
||||
- 记录任务排队时间、执行时间、重试次数、租约过期和死任务数量。
|
||||
|
||||
### 10.4 业务性能测试
|
||||
|
||||
- 保留公共缓存读和依赖就绪场景。
|
||||
- 增加带 JWT、权限、租户解析和 DataScope 的认证分页读。
|
||||
- 增加创建练习、并发答题、交卷、报告和排行榜场景。
|
||||
- 增加下单、支付回调、退款和权益发放场景。
|
||||
- 增加导入、导出和提醒后台任务积压恢复场景。
|
||||
- 结果必须报告目标速率、实际 QPS、状态分布、错误率、p95、p99、丢弃迭代和依赖指标。
|
||||
- 稳态容量测试至少持续 30 分钟,并使用接近生产的数据量和独立发压端。
|
||||
|
||||
### 10.5 可观测性与 SLO
|
||||
|
||||
- 建立 API 延迟、错误率、数据库命令数、连接池等待和慢 SQL 指标。
|
||||
- 建立 Redis 延迟、命中率、连接失败和缓存失效指标。
|
||||
- 建立答题冲突、重复交卷、评分失败、支付回调失败和权益补偿指标。
|
||||
- 日志统一包含请求 ID、租户 ID、用户 ID、操作、任务 ID 和业务对象 ID,敏感值必须脱敏。
|
||||
- 为认证、练习、支付、后台任务和数据库连接池设置发布告警门槛。
|
||||
|
||||
完成标准:核心业务拥有可重复的容量基线,扩容不会导致重复任务、重复发放或状态竞争。
|
||||
|
||||
## 11. 每阶段统一交付物
|
||||
|
||||
每个阶段合并前必须同时交付:
|
||||
|
||||
- Domain、Application、Infrastructure 和 API 边界清晰的实现。
|
||||
- 可审查的 EF Core Migration;特殊 PostgreSQL 约束使用手写 SQL。
|
||||
- DTO、OpenAPI operation、错误码和生成的前端类型。
|
||||
- 单元测试、API 集成测试和真实 PostgreSQL 集成测试。
|
||||
- 跨租户、越权、重复请求、并发竞争和失败回滚负向测试。
|
||||
- 必要的指标、结构化日志和运维配置。
|
||||
- 对应平台端、租户端、教师端或学生端工作流。
|
||||
- 发布、回滚、数据兼容和缓存失效说明。
|
||||
|
||||
## 12. 统一发布门禁
|
||||
|
||||
发布候选必须满足:
|
||||
|
||||
```bash
|
||||
dotnet restore TIKU-BACKEND.slnx
|
||||
dotnet build TIKU-BACKEND.slnx --no-restore
|
||||
dotnet test TIKU-BACKEND.slnx --no-build
|
||||
dotnet format TIKU-BACKEND.slnx --verify-no-changes --no-restore
|
||||
dotnet ef migrations script --project Tiku.Infrastructure --startup-project Tiku.DbMigrator
|
||||
git diff --check
|
||||
```
|
||||
|
||||
涉及数据库或种子数据时还必须:
|
||||
|
||||
- 在 Development 环境运行真实 `Tiku.DbMigrator`。
|
||||
- 再运行一次迁移器验证幂等性。
|
||||
- 验证迁移前后关键数据兼容性。
|
||||
- 验证真实 PostgreSQL 下的租户隔离、约束和并发行为。
|
||||
|
||||
涉及性能敏感路径时还必须:
|
||||
|
||||
- 运行对应业务场景压测。
|
||||
- 对照最近一次有效基线检查吞吐、p95、p99、错误率和数据库命令数。
|
||||
- 性能退化未解释或超过阶段门槛时不得发布。
|
||||
|
||||
## 13. 建议执行顺序
|
||||
|
||||
严格按以下顺序启动开发:
|
||||
|
||||
1. 服务端评分与题目快照。
|
||||
2. 答题幂等、会话版本和客户端序列。
|
||||
3. 并发安全交卷与不可变报告。
|
||||
4. 内容生命周期、题集和练习蓝图。
|
||||
5. 班级、任务、提醒、学情和教师干预。
|
||||
6. 错题间隔复习、词汇学习和自适应练习。
|
||||
7. 套餐权益、教育商品、支付和激活码联动。
|
||||
8. 完成各角色产品界面及 OpenAPI 契约门禁。
|
||||
9. 聚合读模型、业务压测、多实例安全和 SLO 告警。
|
||||
|
||||
P0 未通过发布门禁前,不应上线积分奖励、排行榜或基于正确率的推荐;这些能力依赖可信评分结果。
|
||||
@@ -55,6 +55,8 @@ dotnet run --project Tiku.DbMigrator -- --bootstrap-platform-admin
|
||||
|
||||
Redis key 使用环境前缀;配置解析会强制 `AbortOnConnectFail=false`。Redis 不是用户、Session、权限、套餐或用量的权威数据源。
|
||||
|
||||
授权缓存由 `Security:AuthorizationCache` 配置,默认 `Mode` 为 `Disabled`。切换为 `Shadow` 或 `Active` 前,应先确认 API 与 Worker 使用同一 Redis 和 PostgreSQL,并观察 `Tiku.Security.AuthorizationCache` 的 mismatch、fallback 与 Redis 延迟指标。回滚只需切回 `Disabled`,不应回退授权版本或失效事件相关数据库结构。详细故障语义见[认证、授权与租户隔离](architecture/security-and-tenancy.md#redis-授权缓存模式)。
|
||||
|
||||
## Worker 与后台处理配置
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,47 +1,61 @@
|
||||
# 空数据库到租户建站
|
||||
# 本地开发快速上手
|
||||
|
||||
本页记录 Development 环境从全新 PostgreSQL 数据库完成平台管理员、租户、Owner 激活、网站发布和学生端访问的真实流程。数据库迁移只由 `Tiku.DbMigrator` 执行,API 不会自动更新 Schema。
|
||||
本页用于日常开发:初始化依赖、迁移数据库、启动 API/Worker/平台前端并完成基础验证。若要从空数据库验收“创建租户 → Owner 激活 → 发布站点”的完整流程,请改看[空数据库到租户建站验收](tenant-provisioning.md)。
|
||||
|
||||
## 1. 准备环境
|
||||
## 前置依赖
|
||||
|
||||
需要 .NET 10、Node.js 24+、npm 11+、PostgreSQL、Redis,以及 `psql`、`createdb`、`dropdb`。
|
||||
- .NET 10 SDK
|
||||
- Node.js 24+、npm 11+
|
||||
- PostgreSQL
|
||||
- Redis(Development 可不配置;涉及分布式缓存、频控或完整验收时应启动)
|
||||
|
||||
```bash
|
||||
dotnet --version
|
||||
node --version
|
||||
npm --version
|
||||
pg_isready -h 127.0.0.1 -p 5432
|
||||
redis-cli -h 127.0.0.1 -p 6379 ping
|
||||
```
|
||||
|
||||
首次拉取代码后安装依赖:
|
||||
首次拉取代码后恢复依赖:
|
||||
|
||||
```bash
|
||||
dotnet restore TIKU-BACKEND.slnx
|
||||
npm --prefix Tiku.PlatformAdmin.Web install
|
||||
npm --prefix /path/to/tiku-saas-web install
|
||||
```
|
||||
|
||||
## 2. 创建空数据库
|
||||
## 配置数据库
|
||||
|
||||
以下操作只针对本地数据库 `tiku`。如果它已经包含需要保留的数据,请先备份,不要执行清理命令。
|
||||
Development 未配置连接串时,默认使用当前系统用户连接本机 `tiku` 数据库:
|
||||
|
||||
```text
|
||||
Host=localhost;Database=tiku;Username=<当前系统用户>
|
||||
```
|
||||
|
||||
首次使用可创建数据库:
|
||||
|
||||
```bash
|
||||
dropdb --if-exists -h 127.0.0.1 -U <数据库用户> tiku
|
||||
createdb -h 127.0.0.1 -U <数据库用户> tiku
|
||||
```
|
||||
|
||||
Development 未配置连接串时默认使用当前系统用户连接本机 `tiku`。其他用户或端口应显式设置:
|
||||
使用其他地址、端口或账号时,通过环境变量覆盖:
|
||||
|
||||
```bash
|
||||
export ConnectionStrings__Database='Host=127.0.0.1;Port=5432;Database=tiku;Username=<数据库用户>;Password=<本地密码>'
|
||||
```
|
||||
|
||||
不要把连接串、密码或 Token 写入 `appsettings*.json`、README 或 Git。
|
||||
不要把连接串、密码、Token 或私钥写入仓库。
|
||||
|
||||
## 3. 初始化目录、starter 套餐和首个平台账号
|
||||
## 迁移与初始化
|
||||
|
||||
先设置一次性 Bootstrap 参数,再执行生产式空库初始化:
|
||||
API 不执行 Migration。数据库结构、内置权限/菜单目录和 `starter` 套餐统一由 `Tiku.DbMigrator` 初始化:
|
||||
|
||||
```bash
|
||||
ASPNETCORE_ENVIRONMENT=Development dotnet run --project Tiku.DbMigrator
|
||||
```
|
||||
|
||||
默认 Development seed 会在尚无平台角色绑定时创建 `admin@tiku.local`、演示租户和演示运营数据;随机临时密码只在首次创建时输出,首次登录必须改密。
|
||||
|
||||
若需要不含演示数据的空库,使用显式 Bootstrap 流程,不要运行默认 Development seed:
|
||||
|
||||
```bash
|
||||
export ASPNETCORE_ENVIRONMENT=Development
|
||||
@@ -54,157 +68,69 @@ dotnet run --project Tiku.DbMigrator -- \
|
||||
--bootstrap-platform-admin
|
||||
```
|
||||
|
||||
该命令按固定顺序执行:
|
||||
完整空库验收步骤见[空数据库到租户建站验收](tenant-provisioning.md)。
|
||||
|
||||
1. EF Core Migration;
|
||||
2. Feature、Permission、菜单和额度目录;
|
||||
3. 内置 `starter` 套餐及其已发布版本;
|
||||
4. 可选的平台超级管理员 Bootstrap。
|
||||
## 启动运行时
|
||||
|
||||
`starter` 是零元、CNY、已发布的建站基础套餐,包含 `core.backoffice` 和 `marketing.site_content`。首个平台账号使用临时密码,首次登录必须改密。
|
||||
|
||||
清除 Bootstrap 密码并再运行一次 Migrator,确认日常重复执行不会创建演示租户或重复目录:
|
||||
启动 API:
|
||||
|
||||
```bash
|
||||
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
|
||||
ASPNETCORE_ENVIRONMENT=Development dotnet run --project Tiku.Api --launch-profile http
|
||||
```
|
||||
|
||||
Migration 需要 `citext`、`ltree` 和 `pg_trgm` 扩展。执行用户必须可以创建这些扩展,或由数据库管理员预先安装。
|
||||
|
||||
## 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 服务:
|
||||
`Tiku.Api.csproj` 的 SPA Proxy 会在 Development 启动 `Tiku.PlatformAdmin.Web` 的 Vite 服务。默认入口:
|
||||
|
||||
- 平台管理端:<http://localhost:5173>
|
||||
- API:<http://localhost:5090>
|
||||
- Scalar:<http://localhost:5090/scalar/v1>
|
||||
- OpenAPI:<http://localhost:5090/openapi/v1.json>
|
||||
- Liveness:<http://localhost:5090/api/health>
|
||||
- Readiness:<http://localhost:5090/api/health/ready>
|
||||
|
||||
不要使用 `http://localhost:5090/platform-admin/` 作为开发入口;该路径受 API 授权保护,未登录访问返回 401。
|
||||
|
||||
在 `tiku-saas-web` 仓库另开终端,使用真实 API 模式启动租户前端:
|
||||
后台循环不在 API 内运行。需要处理域名、订阅、任务队列、授权缓存失效或商业账务时,另开终端启动 Worker:
|
||||
|
||||
```bash
|
||||
VITE_DATA_MODE=api \
|
||||
VITE_DEV_API_TARGET='http://localhost:5090' \
|
||||
npm run dev -- --host 0.0.0.0 --port 5180
|
||||
ASPNETCORE_ENVIRONMENT=Development dotnet run --project Tiku.Worker
|
||||
```
|
||||
|
||||
浏览器始终请求同源 `/api`,Vite 只在服务端把它代理到 `VITE_DEV_API_TARGET`。代理保留原始 Host,因此 `school.localhost` 能由后端解析到正确租户。
|
||||
|
||||
## 5. 浏览器建站流程
|
||||
|
||||
### 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`,开发代理目标使用:
|
||||
如需 Redis,API 与 Worker 应使用同一实例:
|
||||
|
||||
```bash
|
||||
VITE_DEV_API_TARGET=http://localhost:5090
|
||||
export ConnectionStrings__Redis='localhost:6379,abortConnect=false'
|
||||
```
|
||||
|
||||
确保旧的 `VITE_API_BASE_URL` 未注入进程,并重启 Vite。
|
||||
## 验证修改
|
||||
|
||||
### 激活失败后地址栏已没有 Token
|
||||
```bash
|
||||
dotnet build TIKU-BACKEND.slnx --no-restore
|
||||
dotnet test TIKU-BACKEND.slnx --no-build
|
||||
dotnet format TIKU-BACKEND.slnx --verify-no-changes --no-restore
|
||||
npm --prefix Tiku.PlatformAdmin.Web run check
|
||||
dotnet ef migrations has-pending-model-changes \
|
||||
--project Tiku.Infrastructure \
|
||||
--startup-project Tiku.DbMigrator \
|
||||
--no-build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
如果后端尚未消费 Token,可重新打开平台最初交付的完整链接。已经消费、过期或丢失明文时,必须由平台撤销并重新签发,不能从数据库或日志恢复。
|
||||
PostgreSQL 特有的 Migration、事务、约束和租户隔离行为必须由真实 PostgreSQL 集成测试验证,EF InMemory 不能替代。接口、DTO 和错误响应以当前运行时 OpenAPI/Scalar 为准。
|
||||
|
||||
### 发布后旧标签仍显示筹备页
|
||||
## 常见问题
|
||||
|
||||
草稿与已发布配置相互隔离。确认向导显示发布成功后,刷新学生端标签,让它重新请求 Runtime Bootstrap。
|
||||
### API 报数据库不可用
|
||||
|
||||
### Cookie 没有建立
|
||||
确认 PostgreSQL 已启动、数据库存在,并核对 `ConnectionStrings__Database` 或 `DATABASE_URL`。非 Development 环境没有本地默认连接串。
|
||||
|
||||
必须通过 `school.localhost:5180` 访问激活和后台,不能改用 `localhost:5180` 或把 `tenantId` 填进请求。浏览器 Session 使用 Secure/HttpOnly Cookie,写请求使用 CSRF 双提交 Token,前端不得降级保存 JWT。
|
||||
### 找不到平台管理员临时密码
|
||||
|
||||
默认 Development seed 和显式 Bootstrap 都只在创建账号时输出一次临时密码,后续运行不会重放。不要从日志或数据库恢复明文;应通过受控流程重置,或在确认无需保留本地数据后重建开发数据库。
|
||||
|
||||
### Readiness 返回 503
|
||||
|
||||
确认 PostgreSQL 和 Redis 均可访问。匿名 readiness 只返回总体状态;依赖详情需要具有 `platform:operations:view` 权限的平台账号。
|
||||
`/api/health/ready` 会检查 PostgreSQL 和已配置的 Redis。先验证数据库连接;配置 Redis 后还需确认 Redis 可访问。匿名响应不会暴露依赖详情。
|
||||
|
||||
## 8. 清理
|
||||
### API 启动了但后台任务不执行
|
||||
|
||||
先停止 API、平台 Vite 和租户 Vite,再只删除明确的本地数据库:
|
||||
Production 和常规 Development 都需要独立运行 `Tiku.Worker`。Development 仅额外在 API 中注册本地域名生命周期旁路,不代表 API 承载全部 Worker 循环。
|
||||
|
||||
```bash
|
||||
dropdb -h 127.0.0.1 -U <数据库用户> tiku
|
||||
```
|
||||
|
||||
该操作不可恢复,会删除本轮创建的平台账号、租户、Session、草稿和发布配置。
|
||||
|
||||
更多配置见[配置与后台任务](operations.md),安全边界见[认证、授权与租户隔离](architecture/security-and-tenancy.md)。
|
||||
下一步可阅读[系统架构与业务边界](architecture/overview.md)、[认证、授权与租户隔离](architecture/security-and-tenancy.md)和[配置与后台任务](operations.md)。
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Redis 认证授权缓存
|
||||
|
||||
## 请求链路
|
||||
|
||||
受保护请求先在本地完成 JWT 验签,再通过一次 Redis MGET 校验 Session、用户、租户、成员资格和持久化授权版本。权限快照使用 60 秒进程内缓存和 5 分钟 Redis 缓存;本地快照键包含授权版本,因此撤权后的下一请求不会继续使用旧权限。PostgreSQL 始终是事实源,Redis 读取失败时绕过本地快照并回退数据库。
|
||||
|
||||
缓存不保存 JWT、Refresh Token、手机号或邮箱。Redis key 使用环境隔离前缀,并区分 platform/tenant realm、tenant、user 和 session。
|
||||
|
||||
## 配置与故障语义
|
||||
|
||||
```json
|
||||
{
|
||||
"Security": {
|
||||
"AuthorizationCache": {
|
||||
"Mode": "Disabled",
|
||||
"LocalSnapshotSeconds": 60,
|
||||
"DistributedStateSeconds": 60,
|
||||
"DistributedSnapshotSeconds": 300,
|
||||
"JitterPercent": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `Disabled`:保持 PostgreSQL 权威读取,仅维护持久化授权版本。
|
||||
- `Shadow`:PostgreSQL 决策仍为准,同时读取、回填和比较 Redis 结果。
|
||||
- `Active`:Redis 为主要读取路径,缓存缺失或不可用时回退 PostgreSQL。
|
||||
- Redis 与 PostgreSQL 同时不可用时返回 `503`,错误码为 `auth_security_unavailable`。
|
||||
|
||||
生产环境的 API 和 Worker 都必须配置 `ConnectionStrings:Redis` 或 `REDIS_URL`。Worker 重试 `authorization_cache_invalidations` 中未完成的失效事件;Redis 版本写入是单调的,旧事件不会覆盖新版本。
|
||||
|
||||
## 发布与回滚
|
||||
|
||||
1. 先部署迁移,保持 `Disabled`。
|
||||
2. 切换 `Shadow`,观察 `Tiku.Security.AuthorizationCache` 指标中的 mismatch、fallback 和 Redis 延迟。
|
||||
3. 确认无非并发不一致后,对单实例启用 `Active`,再逐步扩容。
|
||||
4. 回滚时只把模式切回 `Disabled`,不回退数据库迁移。
|
||||
|
||||
RBAC 表和权限目录由 PostgreSQL 触发器在业务事务内推进授权版本并写入失效事件。任何新增的用户、成员、租户、Session 或角色权限写路径,也必须调用 `IAuthorizationStateInvalidator` 完成同步 Redis 失效。
|
||||
210
docs/tenant-provisioning.md
Normal file
210
docs/tenant-provisioning.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 空数据库到租户建站验收
|
||||
|
||||
本页记录 Development 环境从全新 PostgreSQL 数据库完成平台管理员、租户、Owner 激活、网站发布和学生端访问的真实流程。数据库迁移只由 `Tiku.DbMigrator` 执行,API 不会自动更新 Schema。
|
||||
|
||||
## 1. 准备环境
|
||||
|
||||
需要 .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
|
||||
redis-cli -h 127.0.0.1 -p 6379 ping
|
||||
```
|
||||
|
||||
首次拉取代码后安装依赖:
|
||||
|
||||
```bash
|
||||
dotnet restore TIKU-BACKEND.slnx
|
||||
npm --prefix Tiku.PlatformAdmin.Web install
|
||||
npm --prefix /path/to/tiku-saas-web install
|
||||
```
|
||||
|
||||
## 2. 创建空数据库
|
||||
|
||||
以下操作只针对本地数据库 `tiku`。如果它已经包含需要保留的数据,请先备份,不要执行清理命令。
|
||||
|
||||
```bash
|
||||
dropdb --if-exists -h 127.0.0.1 -U <数据库用户> tiku
|
||||
createdb -h 127.0.0.1 -U <数据库用户> tiku
|
||||
```
|
||||
|
||||
Development 未配置连接串时默认使用当前系统用户连接本机 `tiku`。其他用户或端口应显式设置:
|
||||
|
||||
```bash
|
||||
export ConnectionStrings__Database='Host=127.0.0.1;Port=5432;Database=tiku;Username=<数据库用户>;Password=<本地密码>'
|
||||
```
|
||||
|
||||
不要把连接串、密码或 Token 写入 `appsettings*.json`、README 或 Git。
|
||||
|
||||
## 3. 初始化目录、starter 套餐和首个平台账号
|
||||
|
||||
先设置一次性 Bootstrap 参数,再执行生产式空库初始化:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
该命令按固定顺序执行:
|
||||
|
||||
1. EF Core Migration;
|
||||
2. Feature、Permission、菜单和额度目录;
|
||||
3. 内置 `starter` 套餐及其已发布版本;
|
||||
4. 可选的平台超级管理员 Bootstrap。
|
||||
|
||||
`starter` 是零元、CNY、已发布的建站基础套餐,包含 `core.backoffice` 和 `marketing.site_content`。首个平台账号使用临时密码,首次登录必须改密。
|
||||
|
||||
清除 Bootstrap 密码并再运行一次 Migrator,确认日常重复执行不会创建演示租户或重复目录:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
Migration 需要 `citext`、`ltree` 和 `pg_trgm` 扩展。执行用户必须可以创建这些扩展,或由数据库管理员预先安装。
|
||||
|
||||
## 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:<http://localhost:5090/openapi/v1.json>
|
||||
- Readiness:<http://localhost:5090/api/health/ready>
|
||||
|
||||
不要使用 `http://localhost:5090/platform-admin/` 作为开发入口;该路径受 API 授权保护,未登录访问返回 401。
|
||||
|
||||
在 `tiku-saas-web` 仓库另开终端,使用真实 API 模式启动租户前端:
|
||||
|
||||
```bash
|
||||
VITE_DATA_MODE=api \
|
||||
VITE_DEV_API_TARGET='http://localhost:5090' \
|
||||
npm run dev -- --host 0.0.0.0 --port 5180
|
||||
```
|
||||
|
||||
浏览器始终请求同源 `/api`,Vite 只在服务端把它代理到 `VITE_DEV_API_TARGET`。代理保留原始 Host,因此 `school.localhost` 能由后端解析到正确租户。
|
||||
|
||||
## 5. 浏览器建站流程
|
||||
|
||||
### 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
|
||||
VITE_DEV_API_TARGET=http://localhost:5090
|
||||
```
|
||||
|
||||
确保旧的 `VITE_API_BASE_URL` 未注入进程,并重启 Vite。
|
||||
|
||||
### 激活失败后地址栏已没有 Token
|
||||
|
||||
如果后端尚未消费 Token,可重新打开平台最初交付的完整链接。已经消费、过期或丢失明文时,必须由平台撤销并重新签发,不能从数据库或日志恢复。
|
||||
|
||||
### 发布后旧标签仍显示筹备页
|
||||
|
||||
草稿与已发布配置相互隔离。确认向导显示发布成功后,刷新学生端标签,让它重新请求 Runtime Bootstrap。
|
||||
|
||||
### Cookie 没有建立
|
||||
|
||||
必须通过 `school.localhost:5180` 访问激活和后台,不能改用 `localhost:5180` 或把 `tenantId` 填进请求。浏览器 Session 使用 Secure/HttpOnly Cookie,写请求使用 CSRF 双提交 Token,前端不得降级保存 JWT。
|
||||
|
||||
### Readiness 返回 503
|
||||
|
||||
确认 PostgreSQL 和 Redis 均可访问。匿名 readiness 只返回总体状态;依赖详情需要具有 `platform:operations:view` 权限的平台账号。
|
||||
|
||||
## 8. 清理
|
||||
|
||||
先停止 API、平台 Vite 和租户 Vite,再只删除明确的本地数据库:
|
||||
|
||||
```bash
|
||||
dropdb -h 127.0.0.1 -U <数据库用户> tiku
|
||||
```
|
||||
|
||||
该操作不可恢复,会删除本轮创建的平台账号、租户、Session、草稿和发布配置。
|
||||
|
||||
日常开发启动见[本地开发快速上手](quickstart.md),更多配置见[配置与后台任务](operations.md),安全边界见[认证、授权与租户隔离](architecture/security-and-tenancy.md)。
|
||||
Reference in New Issue
Block a user