docs: simplify migration documentation

This commit is contained in:
2026-07-28 16:22:50 +08:00
parent b8d14e8a7e
commit 5732df8886
13 changed files with 484 additions and 2016 deletions

View File

@@ -2,11 +2,11 @@
`operation-inventory.csv` 是旧 NestJS 与当前 .NET 运行时 OpenAPI 的机械比较结果。
状态说明
状态:
- `exact_match`HTTP 方法和路径完全一致仍需核对 DTO、响应、权限和业务错误。
- `legacy_only`:只存在于旧 NestJS后续决定迁移、替代或删除。
- `target_only`:只存在于 .NET通常是新 REST 设计、诊断接口或路径调整。
- `exact_match`HTTP 方法和路径完全一致仍需核对 DTO、响应、权限和业务错误。
- `legacy_only`:只存在于旧 NestJS后续决定迁移、替代或删除。
- `target_only`:只存在于 .NET通常是新 REST 设计、诊断接口或路径调整。
重新生成:
@@ -17,4 +17,4 @@ python3 scripts/compare_openapi.py \
--output docs/migration/contracts/operation-inventory.csv
```
旧 NestJS 开发环境文档默认是 `/openapi.json`,当前 .NET 开发环境文档是 `/openapi/v1.json`原始 OpenAPI 文件可能较大且会频繁变化,不提交仓库;提交归一化后的 CSV 基线。
原始 OpenAPI 文件较大且变化频繁,不提交仓库;提交归一化后的 CSV 基线。

View File

@@ -1,75 +1,35 @@
# 第一阶段:仓库转正基线
基线日期2026-07-27
状态:已完成
## 仓库身份
## 目标
- 本仓库是 ASP.NET Core 目标后端,后续新功能只在这里开发
- 旧 NestJS 后端只用于核对业务行为、接口契约和数据迁移
- 保留现有 Git 历史,不重新初始化仓库
- 正式远程使用 `https://git.gongxue100.com/xiongyuxing/tiku-backend.net.git`
- 默认分支统一为 `main`,远程创建后再设置保护规则。
- 确认 ASP.NET Core + EF Core + PostgreSQL 仓库为唯一目标后端。
- 建立旧 NestJS OpenAPI 与当前 .NET OpenAPI 的机械比较基线
- 接入自建 Git 上游
## 当前可验证基线
## 结果
| 项目 | 结果 |
- 旧 NestJS 仅作为行为、接口和迁移参考。
- 新功能在 .NET 仓库开发。
- 旧 URL 不要求逐字兼容。
- API 差距记录在 `docs/migration/contracts/operation-inventory.csv`
基线快照:
| 项 | 数量 |
| --- | ---: |
| 旧 NestJS OpenAPI 路径 | 283 |
| 旧 NestJS OpenAPI 操作 | 342 |
| 当前 .NET OpenAPI 路径 | 192 |
| 当前 .NET OpenAPI 操作 | 237 |
| 方法和路径完全一致 | 165 |
| 仅旧 NestJS 存在 | 177 |
| 仅当前 .NET 存在 | 72 |
| 当前 .NET Controller 文件 | 20 |
| 当前 EF Core Migration | 7 |
OpenAPI 数字来自开发环境运行时文档,不以 Controller 特性数量或 README 手工统计为准。详细清单见 [operation-inventory.csv](contracts/operation-inventory.csv)。
## 权威来源
| 内容 | 权威来源 |
| --- | --- |
| 目标架构 | `docs/adr/0001-authoritative-dotnet-backend.md` |
| API 契约 | 运行时 `/openapi/v1.json` |
| 数据库模型 | EF Core 实体和 Fluent Configuration |
| 数据库结构历史 | `Tiku.Infrastructure/Persistence/Migrations/` |
| 生产迁移入口 | `Tiku.DbMigrator` |
| 迁移范围与取舍 | `docs/migration/contracts/operation-inventory.csv` 及后续决策记录 |
## 自建 Git 接入
远程仓库创建完成后,在仓库根目录执行:
## 验收
```bash
git remote add origin https://git.gongxue100.com/xiongyuxing/tiku-backend.net.git
git push -u origin main
dotnet build TIKU-BACKEND.slnx --no-restore
dotnet test Tiku.UnitTests/Tiku.UnitTests.csproj --no-restore
dotnet test Tiku.IntegrationTests/Tiku.IntegrationTests.csproj --no-restore
dotnet ef migrations script --project Tiku.Infrastructure --startup-project Tiku.DbMigrator
git diff --check
```
如果服务端已经自动创建了 README 或初始提交,不要强制推送;先拉取并确认如何合并历史。
## 基线验证
2026-07-27 在 macOS、.NET SDK 10.0.301 上完成:
```text
dotnet restore通过
dotnet build通过0 warning / 0 error
dotnet test通过261/261
dotnet format --verify-no-changes通过
EF Core migration script通过生成 4166 行 SQL
API 契约清单校验通过414 个唯一操作
```
首次 restore 曾因 `api.nuget.org` TLS EOF 和下载超时失败;串行重试成功。这是依赖源网络故障,不是源码或项目路径问题。
## 第一阶段退出条件
- [x] 明确 .NET 是唯一目标后端。
- [x] 保留并接续现有 Git 历史。
- [x] 仓库命令不依赖开发者机器的绝对路径。
- [x] 建立旧 NestJS 与当前 .NET 的 OpenAPI 操作基线。
- [x] 固定 EF Core Migration 的权威地位。
- [x] 配置自建 Git remote。
- [x] 首次推送 `main`
- [ ] 在自建 Git 上启用 `main` 分支保护和 CI等待远程仓库

View File

@@ -1,87 +1,27 @@
# 第二阶段:.NET 工程底座
完成日期2026-07-27
状态:已完成。
本阶段按 greenfield 项目建设,不承担旧 Supabase / NestJS 数据兼容。数据库结构以 EF Core 实体、Fluent Configuration 和 Migration 为唯一权威来源API 运行时不自动修改数据库。
## 目标
## 数据库开发与发布边界
- 建立 ASP.NET Core / EF Core / PostgreSQL 工程底座。
- 固定数据库迁移边界API 不自动改库,迁移由 `Tiku.DbMigrator` 执行。
- 使用真实 PostgreSQL 验证 schema、事务、JSONB、约束和扩展。
- 开发采用 code first修改实体或 Fluent Configuration 后生成 EF Core Migration。
- Migration 统一保存在 `Tiku.Infrastructure/Persistence/Migrations/`
- `Tiku.DbMigrator` 是执行 Migration 的独立入口。
- `Tiku.Api``Tiku.Worker` 不调用 `Database.Migrate()`,避免多实例启动时争抢 DDL也避免应用进程持有结构变更权限。
- 发布前生成并审查 SQL生产环境由部署流程显式执行 DbMigrator 或审核后的 SQL。
- Development 未配置连接串时默认连接本机 `tiku` 数据库,并使用当前系统用户名,不内置密码。
- 非 Development 环境必须显式配置 `ConnectionStrings:Database``DATABASE_URL`,缺失时启动失败。
## 结果
常用命令:
- Development 未配置连接串时默认连接本机 `tiku` 数据库并使用当前系统用户。
- EF Core 使用 Npgsql 与 PostgreSQL 扩展。
- Secret payload 进入加密字段;不提交本地连接串和密钥。
- 真实 PostgreSQL 集成测试成为数据库能力验收入口。
## 验收
```bash
dotnet ef migrations add <MigrationName> \
--project Tiku.Infrastructure \
--startup-project Tiku.DbMigrator
dotnet ef migrations script \
--project Tiku.Infrastructure \
--startup-project Tiku.DbMigrator
dotnet build TIKU-BACKEND.slnx --no-restore
dotnet test Tiku.UnitTests/Tiku.UnitTests.csproj --no-restore
dotnet test Tiku.IntegrationTests/Tiku.IntegrationTests.csproj --no-restore
dotnet ef migrations script --project Tiku.Infrastructure --startup-project Tiku.DbMigrator
dotnet run --project Tiku.DbMigrator
git diff --check
```
## 真实 PostgreSQL 测试基线
`Tiku.IntegrationTests` 已移除 EF Core InMemory provider全部集成测试连接真实 PostgreSQL
- 管理连接可通过 `TIKU_TEST_POSTGRES_ADMIN` 覆盖。
- 默认管理连接为 `Host=localhost;Database=postgres;Username=<当前系统用户>`
- 测试进程创建一次已执行完整 Migration 的模板数据库。
- 每个 `ApiTestFactory` 从模板克隆独立 `tiku_it_*` 数据库,测试之间不共享业务数据。
- Factory 释放时终止连接并删除克隆库;进程退出时删除模板库。
- 测试只允许管理 `tiku_it_*` 命名空间,不访问或清理已有 `tiku` 业务数据库。
真实关系型测试暴露并修正了以下 InMemory 无法可靠验证的问题:
- 测试夹具缺失的 Region、School、Subject、Category、Question 等外键实体。
- `Question``QuestionVersion` 当前版本引用形成的插入循环,改为事务内分阶段保存。
- `ReferralLead.FirstTrackId``ReferralTrack.LeadId` 形成的插入循环,改为事务内分阶段保存。
- PostgreSQL JSON 映射不能将未定义的 `default(JsonElement)` 生成为 SQL literal缺省数组改为有效的 `[]`
- 徽章发放不再伪造超长 legacy ID通知使用新记录 ID 生成独立、稳定的去重键。
## 配置与密钥安全
- 根配置不再提交可用于生产的 JWT signing key。
- Development 只使用明确标识的开发 JWT keyProduction 拒绝该 key。
- 租户第三方凭据使用 AES-256-GCM envelope 保存。
- 每条密文使用 12-byte nonce、16-byte authentication tag。
- AAD 绑定 `tenantId``secretRef``keyId`,密文不能跨租户或跨引用替换。
- 主密钥必须是 Base64 编码的 32 字节值Production 拒绝仓库内的开发测试 key。
- 运行时配置键为 `Security:TenantSecrets:KeyId``Security:TenantSecrets:MasterKey`;环境变量可使用 `TIKU_TENANT_SECRET_KEY_ID``TIKU_TENANT_SECRET_MASTER_KEY`
- 新 schema 只保留 `encryption_key_id``encrypted_payload``encryption_nonce``encryption_tag`,不保留明文字段或明文回退读取路径。
本阶段的 `EncryptTenantSecretPayloads` Migration 会直接删除旧 `secret_payload` 字段。这是 greenfield 决策,不提供旧数据转换或兼容窗口。
## 验证结果
2026-07-27 在本机 PostgreSQL 18.4、.NET SDK 10.0.301 上完成:
```text
dotnet build通过0 warning / 0 error
dotnet test通过265/265
UnitTests16/16
IntegrationTests真实 PostgreSQL249/249
dotnet format --verify-no-changes通过
git diff --check通过
EF Core migration script通过生成 4184 行 SQL
测试数据库清理:通过,无 tiku_it_* 残留
```
## 第二阶段退出条件
- [x] EF Core Migration 成为 code-first schema 的唯一版本历史。
- [x] API 与 Worker 不在启动时自动执行 Migration。
- [x] 生产数据库、JWT 和租户密钥配置缺失时 fail fast。
- [x] 租户第三方凭据只保存 AES-GCM 密文 envelope。
- [x] 集成测试完全切换到隔离的真实 PostgreSQL 数据库。
- [x] 修复真实数据库暴露的外键、循环依赖、JSON 和字段长度问题。
- [x] 全量构建、测试、格式与 Migration SQL 验证通过。

View File

@@ -1,221 +1,52 @@
# 第三阶段:租户隔离、共享题库与租户前端运行时
状态:已实施并通过真实 PostgreSQL 验收2026-07-27
本阶段按 greenfield 项目实施,不兼容现有错误模型,也不承担正式业务数据迁移。阶段目标不是简单给现有查询补 `TenantId`而是同时建立请求、EF Core、写入和 PostgreSQL 四层租户边界,并让公共题库能够完整参与租户学生的组卷、答题、收藏和错题闭环。
原先预计的 35 个工作日只覆盖基础租户过滤。加入共享题库、版本锁定、域名入口和租户前端运行时后,完整阶段预计 1014 个工作日
## 已确认的产品规则
- 系统只存在一个 `TenantMode.PlatformOwned` 平台主体,平台主体拥有全部公共题库
- `Trial``Active` 且未过期的租户自动拥有全部公共题库访问权,不逐库授权。
- 租户可以上传自己的私有题库,私有题只对本租户学生和管理员可见。
- 公共题和租户私有题可以混合进入同一试卷、章节练习或题单。
- 平台分类是公共主干,租户可以在公共节点下增加仅本租户可见的扩展节点
- 新练习使用题目当前已发布版本;已开始练习锁定原版本,历史答题永久按原版本回放
- `PastDue``Cancelled` 租户不能浏览公共题答案或创建新的公共题练习;未过期的既有会话可以完成,历史答题、收藏、错题和统计继续可见
- 租户自定义域名通过 CNAME 指向统一前端和网关,浏览器使用同域 `/api` 访问后端
- 第一版前端配置覆盖品牌、主题令牌、功能开关、导航和首页模块,不实现任意低代码页面或脚本注入
- 本阶段不引入 PostgreSQL RLS。隔离由请求上下文、EF Core Query Filter、写入拦截器、数据库约束和真实 PostgreSQL 测试共同保证
## 3A请求租户与持久化边界
### 请求上下文
使用只读 `ITenantContext` 替换可由业务代码调用 `Load``ICurrentTenant`。公开属性固定为:
- `TenantId`
- `TenantCode`
- `ResolutionSource`
- `IsResolved`
- `IsSystem`
只有 API 入口的内部初始化器可以设置请求租户。普通 Controller 和 Service 只能读取上下文,不能切换租户。
平台任务、Worker 和公共题库基础设施通过 `ITenantExecutionScope` 创建新的 DI scope。System scope 必须提供调用方和原因,记录结构化审计;不允许在现有请求 scope 内临时改写租户。
### EF Core 防护
- 生产和测试均从 `AddDbContextPool<TikuDbContext>` 改为 scoped `AddDbContext<TikuDbContext>`
- 所有租户实体实现统一 marker interface
- 全局 Query Filter 自动添加当前 `TenantId`;未解析租户时查询默认返回空集合
- 普通租户 Query Filter 永远不自动包含平台主体数据,公共内容只能通过专用服务访问
- `SaveChangesInterceptor` 对 Added 自动写入当前租户,并拒绝伪造租户、跨租户修改或删除以及修改已有记录的租户归属
- 未解析租户的普通写入直接失败System scope 也不能把已有记录改归其他租户。
模型启动审计必须拒绝以下情况:
- 实体存在 `TenantId` 却没有明确分类。
- 租户范围内的唯一索引未包含 `TenantId`
- 租户实体之间的外键未包含组合租户键。
- 新增租户实体却没有对应 Query Filter。
普通业务代码禁止调用 `IgnoreQueryFilters()``FromSql``ExecuteSql` 或直接创建 `NpgsqlCommand`。确有需要的实现只能位于集中基础设施边界,并由架构测试维护允许列表。
## 3B域名入口与租户解析
租户解析必须在认证和任何租户数据库查询之前完成:
1. 网关移除客户端提供的 `Forwarded``X-Forwarded-Host` 等头。
2. 只接收受信代理重写的 Host 信息。
3. `TenantResolutionMiddleware` 通过只读 `ITenantDirectory` 查询 Active 域名和 Active 租户。
4. 未知、未验证、已禁用域名在认证前返回 404。
5. JWT tenant claim 与 Host 解析结果不一致时返回 403。
自定义域名请求不得通过 `host``tenantCode` 查询参数或 `x-tenant-code` 覆盖租户。显式 tenant code 只允许平台控制域名上的登录引导接口使用,多来源冲突必须拒绝。
域名生命周期固定为:
1. 创建 Pending 域名并生成验证令牌。
2. 租户设置 CNAME 和 TXT 所有权记录。
3. Worker 验证 DNS。
4. 网关完成 TLS 配置。
5. 域名变为 Active 后才参与请求解析。
`TenantDomain.Host` 全局唯一;每个租户最多一个 Active 主域名。解析结果使用短 TTL 缓存,域名或租户状态变化时主动失效。
## 3C共享题库、租户私库与学习闭环
### 内容所有权
- 公共题库、题目和题目版本的 `TenantId` 为平台主体 ID。
- 私有题库、题目和题目版本的 `TenantId` 为上传租户 ID。
- 删除容易与真实所有权冲突的 `QuestionBank.SourceScope`
- 删除 `QuestionBankGrant``TenantQuestionBankAdoption` 作为访问控制主模型。
- 新增 `TenantQuestionBankPreference`,只保存租户对公共题库的显示/隐藏、别名、排序和导航位置。
- `IPublicQuestionAccessPolicy` 根据租户状态和订阅状态统一判断能否开始新的公共题练习。
数据库对平台主体增加唯一约束,保证只能存在一个 `PlatformOwned` 租户。
### 公共分类主干与租户扩展
分类节点具有明确的 `OwnerTenantId`。父节点引用保存 `ParentOwnerTenantId + ParentId`
- 平台节点只能组成公共主干。
- 租户扩展节点可以挂到平台节点或本租户节点。
- 不允许租户 A 的节点挂到租户 B 的节点。
- 公共题只能关联平台分类。
- 租户私题可以关联平台分类或本租户扩展分类。
所有分类、题库和题目关联使用包含所有者 ID 的组合外键,并通过 PostgreSQL 约束或集中触发器验证“平台或本租户”规则。
### 受控题目引用
新增 `TenantQuestionReference`
- `TenantId`:消费题目的租户。
- `QuestionOwnerTenantId`:平台主体或当前租户。
- `QuestionId`:实际题目。
三列建立唯一约束和组合外键。引用按需创建,不预生成“所有租户 × 全部公共题”。数据库只允许当前租户引用平台公共题或本租户私题,永远拒绝引用其他租户私题。
公开查询返回 `QuestionLocator { source, questionId }`,其中 `source` 只允许 `platform``tenant`。客户端不能提交任意 owner tenant ID写操作由专用服务把 Locator 解析为受控题目引用。
### 组卷、练习和历史版本
- `QuestionCollectionItem` 改为引用 `TenantQuestionReference`,允许公共题和私题混合组卷。
- 新增规范化 `PracticeSessionQuestion`,删除 `PracticeSession.QuestionIds` JSON。
- SessionQuestion 保存题目顺序、分值、题目所有者、题目 ID 和创建会话时锁定的版本 ID。
- 答题接口只接受 `sessionQuestionId`,不再接受裸 `QuestionId`;单题练习也创建轻量会话。
- `AnswerRecord` 关联 SessionQuestion。
- `FavoriteQuestion``WrongQuestion` 关联受控题目引用,不再假设学习租户和题目所有者相同。
- 旧题目版本只能归档,不能物理删除。
## 3D租户前端运行时配置
整合现有 Branding、Theme 和 PublicConfig形成版本化 `TenantFrontendConfig`,配置范围包括:
- 品牌名称、Logo、Favicon 和客服信息。
- 颜色、字体、间距和圆角等主题令牌。
- 学生端功能开关。
- 导航菜单。
- 首页模块及排序。
配置采用 Draft → Preview → Publish。发布使用乐观并发版本号服务端在发布前验证结构不接受任意 HTML、JavaScript、外部脚本或管理端功能开关进入公开配置。
新增 `GET /api/runtime/bootstrap`
- 只根据当前可信 Host 返回配置。
- 返回 `schemaVersion``configVersion`、租户代码、品牌、主题、功能、导航和首页模块。
- 不返回内部租户 ID、密钥或管理端配置。
- 支持 ETag / 304。
- 配置发布、域名停用或租户暂停时主动失效缓存。
现有 `tenant/resolve?host=` 不再作为自定义域名的公开切租户入口,只保留给受控平台登录引导流程。
## 3E重建迁移与验收
项目尚无正式业务数据,因此不编写兼容迁移或双写逻辑:
- 完成模型重构后删除现有 Migration 历史和 Snapshot。
- 重新生成单一 Initial Schema。
- 重建本地开发数据库。
- 使用 `Tiku.DbMigrator` 从空 PostgreSQL 执行完整迁移。
- API 和 Worker 继续禁止启动时自动执行 Migration。
数据库必须落地以下硬约束:
- 唯一平台主体。
- 域名全局唯一。
- 租户唯一索引包含租户键。
- 题目、版本和分类使用所有者组合键。
- 题目引用只能指向平台或本租户。
- SessionQuestion 的版本属于对应题目。
- 学习记录与当前租户、学生和会话一致。
### EF Core 与 PostgreSQL 约束分工
EF Core 负责实体、Fluent Configuration、Migration 生成和迁移执行入口,迁移仍然是 code-first 管理,不允许去生产库手工补结构。
但以下跨表、跨租户不变量不能指望 ORM 自动推导:
- `TenantQuestionReference` 只能引用平台主体公共题或当前租户私题,不能引用其他租户私题。
- `TaxonomyNode` 的父节点只能属于平台主体或当前租户,不能挂到其他租户节点。
- 需要读取 `tenants.mode` 或比较多列 owner 关系的规则。
原因是 PostgreSQL `CHECK` 不能跨表查询,普通 FK 只能证明目标记录存在,不能表达“目标 owner 必须是平台主体或本租户”。这类规则必须落到 PostgreSQL trigger / constraint trigger。
实现要求:
- 触发器 SQL 必须集中在基础设施层,例如 `Tiku.Infrastructure/Persistence/PostgreSqlTenantConstraintSql.cs`
- Migration 只调用集中 helper`migrationBuilder.EnsureTenantIsolationGuards()` / `migrationBuilder.DropTenantIsolationGuards()`
- 重建或压缩 `InitialSchema` 后,必须在 `Up()` 末尾调用 `EnsureTenantIsolationGuards()`,在 `Down()` 开头调用 `DropTenantIsolationGuards()`
- 不允许把触发器 SQL 零散复制到多个 migration。
- Migration script 测试必须断言 guard function 和 trigger 存在,避免重建基线时漏掉数据库硬防线。
- 每个触发器必须有真实 PostgreSQL 集成测试覆盖允许路径和拒绝路径。
- 如果将来新增类似“平台或本租户”的 owner 规则,优先补集中 SQL helper 和模型/集成测试,不要只靠 Service 手写校验。
## 测试矩阵
真实 PostgreSQL 测试创建平台主体、租户 A、租户 B 及三套内容,至少覆盖:
- A 默认不能读取、修改、删除或 Attach 伪造 B 的租户数据。
- A 可以访问公共题和 A 私题,不能构造 B 私题引用。
- 公共题和私题可以混合组卷,并完成答题、收藏、错题和统计。
- 公共题 V1 创建会话后发布 V2旧会话仍用 V1新会话使用 V2历史答案可回放。
- Trial/Active 可以创建公共题练习PastDue/Cancelled 只能完成未过期会话并查看历史。
- 域名 A 携带租户 B JWT 时被拒绝;伪造 Host、tenantCode 和转发头不能切租户。
- Pending 域名、未知域名和 Suspended 租户无法获取运行时配置。
- 配置草稿不影响线上;发布后 ETag 和缓存版本改变。
- 新增租户实体漏写 Filter、组合外键或租户索引时模型测试失败。
- 架构测试发现未经允许的 Query Filter 绕过或原生 SQL 时失败。
- 从空 PostgreSQL 执行 Initial Schema、启动 API 并通过全量测试。
## 实施顺序和退出条件
实施顺序固定为:
1. 3A只读租户上下文、Query Filter、写入拦截器。
2. 3BHost 解析、域名生命周期和 JWT 租户一致性。
3. 3C公私内容所有权、分类扩展、题目引用和学习闭环。
4. 3D前端运行时配置、发布和缓存。
5. 3E重建 Migration、真实 PostgreSQL 隔离测试和全量验证。
第三阶段只有在以下条件全部满足后才能结束:
- 新增普通租户实体即使开发者忘记手写 `TenantId` 条件也不会跨租户读取。
- API 无法通过 DTO、Host、Header、JWT 或 Attach 伪造其他租户数据。
- 公共题库和租户私库能够混合完成组卷、答题、收藏和错题闭环。
- 题目发布新版本不会改变进行中会话或历史答案。
- 租户自定义域名能够安全获得已发布的前端运行时配置。
# 第三阶段:租户隔离、共享题库与前端运行时
状态:已完成主线设计和实现
## 目标
- 普通业务代码默认只能读取和写入当前租户数据
- 公共题库由平台主体拥有,有效租户可访问。
- 租户私题只属于本租户。
- 公共题和私题可混合组卷、答题、收藏、错题和统计。
- 租户自定义域名安全解析到统一前端运行时配置
## 结果
- `ITenantContext` 只读化,请求租户由中间件解析
- EF Core Query Filter 自动按租户过滤
- SaveChanges 拦截器自动写入当前租户并拒绝跨租户写入
- DbContext 使用 scoped `AddDbContext`,避免池化串租户状态
- `TenantResolutionMiddleware` 在认证前按可信 Host 解析租户
- JWT tenant claim 与 Host 解析不一致时返回 403
- 未知、Pending、禁用域名在业务前返回 404。
- 唯一 `PlatformOwned` 租户拥有公共题库、公共题、公共题版本和公共分类主干。
- `TenantQuestionReference` 作为公共题/私题消费引用。
- `PracticeSessionQuestion` 锁定题目版本,历史答题按原版本回放。
- `TenantFrontendConfig` 支持 Draft / Preview / Publish。
- `GET /api/runtime/bootstrap` 仅根据当前 Host 返回公开配置。
## 数据库边界
EF Core 负责实体、索引、外键和普通约束。以下跨表租户不变量由集中 PostgreSQL guard 管理:
- `TenantQuestionReference` 只能引用平台公共题或当前租户私题。
- `TaxonomyNode` 父节点只能属于平台主体或当前租户。
维护规则见根目录 README 的“数据库与 ORM 分工”。
## 验收
- A 租户不能读取、修改、删除或 Attach B 租户数据。
- A 可访问公共题和 A 私题,不能构造 B 私题引用
- 公共题 V1 创建会话后发布 V2旧会话仍使用 V1新会话使用 V2
- Host A 携带 Tenant B JWT 返回 403
- Pending/未知域名和 Suspended 租户无法获取 runtime bootstrap
- 新增租户实体漏写 filter、组合外键或租户索引时模型测试失败
```bash
dotnet build TIKU-BACKEND.slnx --no-restore
dotnet test Tiku.UnitTests/Tiku.UnitTests.csproj --no-build
dotnet test Tiku.IntegrationTests/Tiku.IntegrationTests.csproj --no-build
dotnet ef migrations script --project Tiku.Infrastructure --startup-project Tiku.DbMigrator
git diff --check
```

View File

@@ -1,141 +1,42 @@
# 第四阶段:外部服务解耦与租户级 Provider 模块化
# 第四阶段:外部服务解耦
状态:已实施并通过构建、单元测试、真实 PostgreSQL 集成测试和空库迁移验收2026-07-27
状态:已完成
本阶段按“完全不用 Supabase、无正式业务数据”的前提实施。不保留 Supabase Auth、Supabase Storage、旧 Provider 配置或旧 DTO 兼容层。目标是让业务层只表达身份、短信、对象存储、支付和通知的业务意图,第三方 SDK、账号、bucket、密钥和 claim 结构都收敛到 Infrastructure provider 边界。
## 目标
## 设计结论
- 完全移除 Supabase Auth / Storage 兼容层。
- 业务层只依赖身份、短信、对象存储、支付和通知抽象。
- 第三方 SDK、账号、bucket、密钥和 claim 结构只出现在 Infrastructure provider 边界。
- Identity 默认使用自有 JWT、数据库 Session、密码、短信验证码和微信认证。
- Object Storage 默认使用阿里云 OSS`local_dev` 只允许本地测试。
- Database 固定为 EF Core + Npgsql + PostgreSQL。
- Provider 配置统一使用 `TenantExternalProvider` + `TenantSecret`
- 不实现 Supabase Auth 兼容层。
- 不实现 Supabase Storage provider。
- 不继续扩散 `TenantAuthProvider``TenantPaymentAccount` 这类专用配置表。
## 结果
## 统一 Provider 配置
- Provider 配置统一为 `TenantExternalProvider` + `TenantSecret`
- `Capability` 覆盖 Identity、ObjectStorage、Sms、Payment、Notification。
- 同一租户内 `Capability + Provider` 唯一。
- `ConfigPublic` 只保存公开配置;敏感字段必须进入 `TenantSecret`
- 删除旧 `TenantAuthProvider``TenantPaymentAccount` 和 Supabase storage provider 路径。
- 阿里云 OSS、阿里云短信、微信、支付宝 SDK 只允许在 Infrastructure 使用。
新增统一租户外部服务配置模型 `TenantExternalProvider`,核心字段包括:
## 接口边界
- `TenantId`
- `Capability`
- `Provider`
- `Status`
- `DisplayName`
- `ConfigPublic`
- `SecretRef`
- `Priority`
- `Metadata`
- 审计字段
- `IIdentityProvider`:封装 password、sms、wechat_web、wechat_miniapp 身份解析,不签发 JWT。
- `ISmsProvider`:只负责发送,验证码生成、哈希、频控和校验归业务服务。
- `IObjectStorageService`bucket/provider 从租户配置解析,业务输入不得任意覆盖。
- `IPaymentProvider`:支付账户和密钥从统一 Provider 配置加载。
- `INotificationProvider`:默认站内通知持久化,后续外发通道按 provider 扩展。
`Capability` 固定为:
## 验收
- `Identity`
- `ObjectStorage`
- `Sms`
- `Payment`
- `Notification`
- 租户 A/B Provider 配置和密钥互不读取。
- `ConfigPublic` 拒绝 `secret``token``key``privateKey` 等敏感字段。
- 资产上传不能伪造 bucket/provider。
- 业务层不引用第三方 SDK namespace。
- 生产代码不回流 Supabase provider 或旧专用配置表。
同一租户内 `Capability + Provider` 唯一。默认 Provider 选择规则是 `Status = Active` 且优先级最高;需要指定 provider code 的入口必须仍然受当前租户上下文约束。
`ConfigPublic` 只允许公开配置,例如:
- identity`appId`
- payment`merchantId`
- object_storage`region``endpoint``bucketAlias`
- sms / notification`templateCode`
密钥一律进入 `TenantSecret`,由 `SecretRef` 关联。公开配置写入路径必须拒绝 `secret``token``key``privateKey` 等敏感字段,避免把第三方凭据落到普通 JSON 配置里。
## Application 层接口边界
### `IIdentityProvider`
封装 password、sms、wechat_web、wechat_miniapp 等身份解析,不直接把第三方 claim 结构散落在业务服务里,也不负责绕过统一 Session 签发流程。
微信登录通过当前租户的 `TenantExternalProvider(Capability = Identity)` 读取 `appId``SecretRef`,再由基础设施层完成第三方交互。`UserIdentity.Provider` 只保存标准 provider code。
### `ISmsProvider`
只负责发送验证码或模板短信。验证码生成、哈希、频控、过期、校验和登录 Session 仍归自有业务服务负责。发送失败时记录失败状态,不能误判验证码可用。
### `IObjectStorageService`
业务输入不接受任意 bucket。对象存储 provider、bucket alias、endpoint、region 由当前租户的 `TenantExternalProvider(Capability = ObjectStorage)` 解析。
资产表可以保存 `StorageProvider``Bucket``ObjectKey` 作为落库事实,但这些字段只允许由存储服务写入,不对业务 DTO 暴露成任意可写参数。
### `IPaymentProvider`
支付账户配置从统一 Provider 配置加载。支付回调仍按 provider code 路由,但必须校验签名、租户、订单号和幂等事件。
`TenantPaymentAccount` 表和旧支付专用配置模型已删除,管理侧应用模型只保留面向租户后台的 provider 视图。
### `INotificationProvider`
默认实现为站内通知持久化。普通业务不直接跨模块 new `UserNotification`,后续 email、企微、短信模板等外发能力都应作为 Notification provider 扩展。
## 存储和认证的具体变化
- 删除生产代码中的 `SupabaseStorage` provider 枚举值。
- 删除应用层所有 Supabase provider 配置路径。
- 阿里云 OSS SDK 细节只保留在 Infrastructure 实现中。
- 上传签名和上传确认通过当前租户 object storage provider 解析目标位置。
- 微信登录不再从公开 JSON 直接读取密钥。
- 短信登录继续复用自有验证码表、频控和 Session 体系,发送动作改由 `ISmsProvider` 执行。
## 数据库基线
项目尚无正式业务数据,因此本阶段继续采用 greenfield migration
- 删除旧 `TenantAuthProvider` 表。
- 删除旧 `TenantPaymentAccount` 表。
- 新增 `TenantExternalProvider` 表。
- 保留并扩展 `TenantSecret` 用途。
- 重建唯一 `InitialSchema` migration。
- 使用空 PostgreSQL 通过 `Tiku.DbMigrator` 验证完整建库。
## 架构防线
架构测试需要阻止业务代码重新引入以下依赖:
- `Supabase`
- `supabase_storage`
- `SUPABASE_`
- `TenantAuthProvider`
- `TenantPaymentAccount`
- 业务层直接引用阿里云 OSS、微信、支付或短信 SDK namespace
- 业务层直接读取第三方密钥
- 业务 DTO 暴露任意 bucket / provider 细节
允许例外只应存在于:
- Infrastructure provider 实现
- EF Migration
- 测试 fake provider
- 明确说明历史迁移背景的文档
## 验收结果
本阶段完成时的验收口径:
- `dotnet build TIKU-BACKEND.slnx --no-restore`
- `dotnet test Tiku.UnitTests/Tiku.UnitTests.csproj --no-restore`
- `dotnet test Tiku.IntegrationTests/Tiku.IntegrationTests.csproj --no-restore`
- 空 PostgreSQL 数据库执行 `Tiku.DbMigrator`
- `git diff --check`
这些命令的目标不是证明“没有 Supabase 字符串”,而是证明新 Provider 模型、租户边界、迁移链和集成测试能共同支撑后续开发。
## 后续扩展规则
新增外部服务 provider 时,必须先判断它属于哪个 `Capability`,再补 Infrastructure provider 实现和租户配置解析。不要为每个第三方服务新建一套业务专用账号表;除非该能力已经形成独立领域模型,否则默认进入 `TenantExternalProvider`
Provider 扩展时还必须同时补:
- 租户 A/B 配置隔离测试。
- 密钥只通过 `TenantSecret` 读取的测试。
- `ConfigPublic` 敏感字段拒绝测试。
- 架构扫描允许列表。
- 空库 migration 验证。
```bash
dotnet build TIKU-BACKEND.slnx --no-restore
dotnet test Tiku.UnitTests/Tiku.UnitTests.csproj --no-build
dotnet test Tiku.IntegrationTests/Tiku.IntegrationTests.csproj --no-build
dotnet ef migrations script --project Tiku.Infrastructure --startup-project Tiku.DbMigrator
git diff --check
```

View File

@@ -1,194 +1,47 @@
# 阶段:后台能力底座、Worker 基座与运营闭环
# 第五阶段:后台能力Worker 基座
状态:已完成第一轮底座实现并通过本地验收2026-07-28
状态:已完成第一轮底座实现。
本阶段从核心 SaaS 架构重构进入后台运营能力补齐。目标不是逐字兼容旧 NestJS而是在当前 ASP.NET Core + EF Core + PostgreSQL 架构下重建后台权限、菜单、审计、交易运营和 Worker 基座,并参考 yudao 的后台能力清单补齐系统底座。
## 目标
## 固定 SDK 选型
- 参考 yudao 后台能力,重建权限、菜单、审计、交易运营和 Worker 基座。
- 微信生态统一使用 `Senparc.Weixin.*`
- 业务层继续只依赖 Application 接口,不直接引用第三方 SDK。
微信生态统一使用 Senparc
## 结果
- `Senparc.Weixin`
- `Senparc.Weixin.MP`
- `Senparc.Weixin.WxOpen`
- `Senparc.Weixin.TenPayV3`
- 微信支付切换到 `Senparc.Weixin.TenPayV3`
- 生产代码禁止 `SKIT.FlurlHttpClient.Wechat.*`
- 新增平台/租户后台权限、菜单、角色和用户角色绑定模型。
- 菜单只控制 UI 展示,不作为 API 鉴权依据。
- 角色、权限、菜单和用户角色绑定写操作落 `AuditLog`
- 租户交易运营补齐退款、对账批次、对账 issue 和事件记录。
- 新增 `BackgroundJob` 统一任务模型。
- Worker 基于 `Microsoft.Extensions.Hosting` + `BackgroundService`
- 任务处理器骨架覆盖 `content_export``content_import``asset_security_scan``statistics_aggregation``commerce_reconciliation``tenant_domain_recheck`
- PostgreSQL tenant guard 统一放入集中 SQL helper由 migration 调用。
已移除并禁止:
- `SKIT.FlurlHttpClient.Wechat.*`
其他外部服务:
## 固定 SDK
- OSS`AlibabaCloud.OSS.V2`
- 阿里云短信:`AlibabaCloud.SDK.Dysmsapi20170525`
- 支付宝:`AlipaySDKNet.Standard`
- Worker`Microsoft.Extensions.Hosting` + `BackgroundService`
- 微信公众号:`Senparc.Weixin.MP`
- 微信小程序:`Senparc.Weixin.WxOpen`
- 微信支付 V3`Senparc.Weixin.TenPayV3`
业务层仍然只依赖 Application 接口:
## 验收
- `IIdentityProvider`
- `IObjectStorageService`
- `ISmsProvider`
- `IPaymentProvider`
- `INotificationProvider`
第三方 SDK namespace 只允许出现在 Infrastructure provider 实现中。
## 5A微信 Provider 收敛
`WechatPayProvider` 已切换到 `Senparc.Weixin.TenPayV3`
- JSAPI / H5 下单参数生成走 Senparc TenPayV3。
- JSAPI 前端支付参数使用 Senparc 签名 helper 生成。
- 支付 provider 继续通过 `IPaymentProvider` 暴露,不向 Application 或 API 泄漏 Senparc 类型。
- 架构测试禁止生产代码引用 `SKIT.FlurlHttpClient.Wechat`
说明:当前 `IPaymentProvider` 的回调入口抽象为 body + headers不直接暴露 `HttpContext`。Senparc 官方推荐的 `TenPayNotifyHandler(HttpContext)` 回调验签/解密模式后续可以在 Infrastructure 的 HTTP 适配层补强,但不能把 Senparc 类型扩散到业务层。
## 5B后台权限、菜单、角色与审计底座
新增后台基础模型:
- `BackendPermission`
- `BackendMenu`
- `TenantBackendRole`
- `TenantBackendRolePermission`
- `TenantBackendRoleMenu`
- `TenantBackendUserRole`
- `PlatformBackendRole`
- `PlatformBackendRolePermission`
- `PlatformBackendRoleMenu`
- `PlatformBackendUserRole`
设计边界:
- 权限点是稳定字符串 code。
- 菜单只控制后台 UI 展示,不作为唯一 API 鉴权来源。
- 平台角色和租户角色分表。
- 租户角色绑定包含 `TenantId`,平台角色绑定不带租户键。
- 角色、权限、菜单、用户角色绑定写操作落 `AuditLog`
新增接口:
- `GET /api/backoffice/tenant/bootstrap`
- `POST /api/backoffice/tenant/roles`
- `PUT /api/backoffice/tenant/roles/{roleId}/bindings`
- `PUT /api/backoffice/tenant/users/{userId}/roles`
- `GET /api/backoffice/platform/bootstrap`
- `POST /api/backoffice/platform/roles`
- `PUT /api/backoffice/platform/roles/{roleId}/bindings`
- `PUT /api/backoffice/platform/users/{userId}/roles`
`TenantAdminDirect` 继续作为过渡入口;新增后台能力使用 `backoffice` 模块命名。
## 5E交易运营底座
在现有 commerce 模型上补齐租户后台运营服务:
- 退款申请。
- 退款状态流转。
- 退款事件记录。
- 对账批次创建。
- 对账 issue 查询与状态流转。
- 退款、对账写操作落审计。
退款状态流转由服务控制,不能任意跳转。支付、回调、退款后续仍统一走 `IPaymentProvider`,初期不默认开启真实自动退款。
新增租户交易运营接口:
- `GET /api/tenant-commerce/refunds`
- `POST /api/tenant-commerce/refunds`
- `POST /api/tenant-commerce/refunds/status`
- `GET /api/tenant-commerce/refunds/{refundRequestId}/events`
- `GET /api/tenant-commerce/reconciliation/batches`
- `POST /api/tenant-commerce/reconciliation/batches`
- `GET /api/tenant-commerce/reconciliation/issues`
- `POST /api/tenant-commerce/reconciliation/issues/status`
## 5FWorker 与后台任务基座
新增统一任务模型 `BackgroundJob`
- `JobType`
- `TenantId`
- `Payload`
- `Status`
- `RetryCount`
- `MaxRetries`
- `LockedBy`
- `LockExpiresAt`
- `RunAfter`
- `StartedAt`
- `CompletedAt`
- `LastError`
- `OutputAssetId`
- `Result`
Worker 基于 `Microsoft.Extensions.Hosting` + `BackgroundService`,暂不引入 Hangfire。后续需要复杂 cron 时再评估 `Quartz.Extensions.Hosting`
当前任务处理器建立了幂等租户 Scope 入口和状态机骨架,覆盖以下 job type
- `content_export`
- `content_import`
- `asset_security_scan`
- `statistics_aggregation`
- `commerce_reconciliation`
- `tenant_domain_recheck`
新增租户后台任务接口:
- `GET /api/backoffice/tenant/jobs`
- `POST /api/backoffice/tenant/jobs`
API 只负责创建任务和查询任务Worker 必须通过 `ITenantExecutionScope` 初始化执行上下文。
## 阿里云短信 Provider
`ISmsProvider` 默认接入租户 Provider 配置:
- 未配置短信 provider 时,本地/测试降级为 `noop`
- 配置 `aliyun_sms` 时,从 `TenantExternalProvider(capability=sms)` 读取公开配置。
- `signName``templateCode``endpoint``regionId` 等公开字段放 `ConfigPublic`
- `accessKeyId``accessKeySecret` 只允许通过 `TenantSecret` 解密获得。
- 发送失败会记录 `SmsVerificationStatus.Failed`,不会留下可验证验证码。
## 数据库约束补强
阶段五重建 `InitialSchema` 时同步补齐阶段三遗漏的两个 PostgreSQL 硬约束:
- `TenantQuestionReference` 只能引用平台主体公共题或当前租户私题。
- `TaxonomyNode` 父节点只能属于平台主体或当前租户。
这类跨表租户不变量不能靠 EF Core FK / check constraint 自动表达,统一放在 `PostgreSqlTenantConstraintSql`,由 migration 调用,并由真实 PostgreSQL 集成测试覆盖。
## 架构测试
新增或强化禁止项:
- 生产代码不得引用 `SKIT.FlurlHttpClient.Wechat`
- 业务层不得直接引用阿里云 OSS、阿里云短信、Senparc、支付宝 SDK namespace。
- 生产代码不得回流 Supabase provider、Supabase Storage 或旧专用 provider 表模型。
- 普通业务目录继续禁止 `IgnoreQueryFilters``FromSql``ExecuteSql` 和直接 `NpgsqlCommand`
## 验收结果
本阶段本地验收:
- platform token / tenant token 后台权限不能串用。
- 高风险写操作都有审计。
- 租户 A 不能查询或处理租户 B 交易数据。
- Worker 必须通过 `ITenantExecutionScope` 初始化租户或 System Scope。
- 架构扫描禁止 Supabase、SKIT 微信支付、业务层第三方 SDK、`IgnoreQueryFilters``FromSql``ExecuteSql`、直接 `NpgsqlCommand`
```bash
dotnet restore TIKU-BACKEND.slnx
dotnet build TIKU-BACKEND.slnx --no-restore
dotnet test Tiku.UnitTests/Tiku.UnitTests.csproj --no-build
dotnet test Tiku.IntegrationTests/Tiku.IntegrationTests.csproj --no-build
dotnet ef migrations script --project Tiku.Infrastructure --startup-project Tiku.DbMigrator
git diff --check
```
结果:
- build0 警告0 错误。
- unit tests18/18 通过。
- integration tests264/264 通过。
- migration script生成成功。
- 空 PostgreSQL 通过 `Tiku.DbMigrator` 建库成功。
- PostgreSQL 验证两个租户约束 trigger 已创建。

View File

@@ -1,12 +1,15 @@
# 第七阶段:学生端体验与内容消费闭环
第七阶段只补齐学生端和内容消费闭环,不实现 AI 推荐报告。AI 后续单独进入 Semantic Kernel 阶段,租户自己的模型 API Key 通过 `TenantExternalProvider` + `TenantSecret` 配置,业务 DTO、Controller 和普通 Service 不直接接触密钥或 `Microsoft.SemanticKernel` namespace
状态:已完成
## 已实现范围
## 目标
### 视频消费
- 补齐学生端视频消费、Profile 签到、积分流水和内容导入异步化。
- 不实现 AI 业务功能AI 进入第八阶段。
新增学生端视频接口:
## 结果
学生端视频接口:
- `GET /api/videos/search`
- `POST /api/videos/play`
@@ -14,62 +17,38 @@
- `GET /api/questions/videos`
- `POST /api/questions/videos/batch`
设计边界
- 播放接口只返回当前租户可访问的视频播放信息。
- API 不暴露 OSS bucket、真实 object key 或 provider 细节。
- 公共题关联视频和租户私题关联视频都必须先通过当前租户可见性校验。
- 播放行为写入 `ContentAssetAccessEvent`
- 播放进度写入 `VideoPlaybackProgress`,同一租户、用户、视频、题目维度幂等更新。
### Profile 与积分
新增学生侧体验接口:
Profile 与积分接口
- `POST /api/profile/check-in`
- `GET /api/profile/score-events`
设计边界
- 签到复用积分任务与积分流水,不另起一套奖励体系。
- 同一用户、同一租户、同一天只能成功签到一次。
- 积分任务领取和积分兑换会同步产生 `UserScoreEvent`,学生端统一从 `score-events` 查询积分变化。
-`/api/profile/activity-tasks``/api/profile/exchange-items` 不恢复为主接口;对应能力由 `/api/points/tasks``/api/points/exchange-items``/api/points/exchange-orders` 替代。
### 内容导入异步化
保留统一导入入口:
内容导入入口
- `POST /api/tenant-content/imports/preview/{importType}`
- `POST /api/tenant-content/imports/{importType}`
- `GET /api/tenant-content/imports/detail`
设计边界
## 边界
- `questions``vocabulary``handbook``scoreline``videos` 这些旧专用导入语义统一映射为 `importType`
- 小批量可以同步执行
- 请求显式 `async=true` 或大批量导入时创建 `content_import` 后台任务
- Worker 通过 `ITenantExecutionScope` 初始化租户 scope 后执行导入,不在请求线程内跑重任务
- 导入结果写回 import job可通过 detail 接口查询
- 播放接口不暴露 OSS bucket、真实 object key 或 provider 细节
- 公共题和租户私题关联视频都必须按当前租户可见性校验
- 播放进度按租户、用户、视频、题目维度幂等更新
- 签到复用积分任务和积分流水;同一用户、同一租户、同一天只能成功一次
- `questions``vocabulary``handbook``scoreline``videos` 导入语义统一映射为 `importType`
- 大批量导入创建 `content_import` 后台任务Worker 使用 `ITenantExecutionScope` 执行。
- 本阶段未实现 `/api/ai/**`。SK 包和 AI provider 边界在第八阶段引入。
## AI 延后约定
## 旧接口替代
本阶段不新增 `/api/ai/**`,不引入 `Microsoft.SemanticKernel` NuGet 包也不实现真实模型调用、prompt、RAG、导出或推荐算法。
- `/api/profile/activity-tasks` -> `/api/points/tasks`
- `/api/profile/exchange-items` -> `/api/points/exchange-items`
- `/api/profile/exchange-items/redeem` -> `/api/points/exchange-orders`
后续 AI 阶段默认方向:
- AI Provider 使用 `TenantExternalProvider(capability=ai)`
- 租户 API Key 存入 `TenantSecret`,通过 `SecretRef` 关联。
- Semantic Kernel SDK 只允许出现在 Infrastructure AI provider 实现中。
- Application 层只暴露 `IAiRecommendationProvider``IAiKernelFactory` 等业务抽象。
- Controller 和业务 Service 不直接读取 API Key不直接引用 SK namespace。
## 验收重点
## 验收
- 租户 A 不能播放租户 B 视频。
- 公共题关联视频和租户私题关联视频都按当前租户权限返回。
- 播放进度重复上报幂等更新
- 公共题和租户私题解析视频都按权限返回。
- 播放进度重复上报不产生重复记录
- 每日签到同一天只能成功一次。
- 签到、积分任务和兑换产生可查询积分流水。
- 签到、积分任务和兑换产生可查询积分流水。
- 异步导入创建 `content_import` jobWorker 成功写入结果。
- 本阶段不得新增 `Microsoft.SemanticKernel` 引用。

View File

@@ -1,144 +1,61 @@
# 第八阶段AI 底座与教师端对话
第八阶段从“只预留 AI”进入 AI 底座建设。AI 当前只面向租户教师和后台运营,不开放给学生端
状态:基础包和边界已引入,业务接口待实现
## 当前 AI 使用场景
## 使用场景
### 1. 租户教师 AI 对话
第一批先做基础对话能力:
### 租户教师 AI 对话
- 教师在租户后台发起对话。
- AI 根据当前租户配置调用模型。
- AI 根据当前租户 Provider 配置调用模型。
- 对话历史按租户和教师隔离保存。
- 响应不暴露模型 API Key、Provider 原始响应密钥、内部租户 ID 或对象存储细节
- 后续可追加 function calling用于读取或操作受控业务能力
- 后续预留 function calling但只能调用受审计的后端业务函数
- function 有写操作时必须复用 RBAC、DataScope、Tenant Scope 和 AuditLog
预留 function call 的原则:
### AI 审核题目反馈
- Controller 不直接暴露任意 tool/function 名称给客户端调用
- Application 层定义可用业务函数目录,例如题目检索、题目草稿生成、班级学习概览、学生跟进建议。
- Infrastructure 用 Semantic Kernel 把受审计的业务函数注册为 plugin。
- 每个 function call 都必须记录租户、教师、会话、函数名、输入摘要、结果状态和耗时。
- 有写操作的 function 必须复用现有 RBAC、DataScope、Tenant Scope 和 AuditLog不允许 AI 绕过后台权限。
### 对话存储模型
不要把 Semantic Kernel 的 `ChatHistory``ChatMessageContent``KernelContent`、tool call object graph 或 provider 原始 response 直接作为 EF Core 持久化模型。原因:
- SK 的对象模型适合运行时编排,不适合作为长期数据库 schema。
- OpenAI-compatible provider 的消息格式并不完全等价DeepSeek 这类接口对 `role``content``tool_calls``tool_call_id` 的结构要求更严格。
- 如果把 SK metadata、内部 content item 或历史 tool 结构原样回放给 DeepSeek容易触发请求参数错误。
- 后续换 provider、增加 function call 或做消息压缩时,直接持久化 SK 对象会变成强耦合。
数据库只保存 provider-neutral 的规范化消息:
- `AiConversation`
- `TenantId`
- `TeacherUserId`
- `Title`
- `Scenario``teacher_chat`、后续可扩展。
- `ProviderCode`
- `Model`
- `Status`
- `Metadata`
- `AiConversationMessage`
- `TenantId`
- `ConversationId`
- `Sequence`
- `Role`:固定为 `system``user``assistant``tool`
- `ContentText`
- `ToolCallId`
- `ToolName`
- `ToolArguments`
- `ToolResultSummary`
- `ProviderMessageId`
- `TokenInput`
- `TokenOutput`
- `Metadata`
- `AiToolCallLog`
- `TenantId`
- `ConversationId`
- `MessageId`
- `ToolCallId`
- `ToolName`
- `InputSummary`
- `ResultStatus`
- `DurationMs`
- `ErrorCode`
运行时转换规则:
1. Application 层读取规范化消息,不产生 SK 类型。
2. Infrastructure adapter 把规范化消息转换成 Semantic Kernel `ChatHistory`
3. DeepSeek/OpenAI-compatible adapter 只发送 provider 接受的字段:
- 普通消息:`role + content`
- assistant tool call`role=assistant + tool_calls`
- tool 结果:`role=tool + tool_call_id + content`
4. Provider 原始响应只保存必要摘要和可审计 ID不作为下一轮请求的直接输入。
5. 任何无法被目标 provider 表达的 SK metadata 都必须丢弃或写入内部 `Metadata`,不得回放给模型 API。
### 2. AI 审核题目反馈
这个场景保持简单,不做复杂扩展:
- 输入:题目反馈内容、题目基本信息、反馈类型、提交用户上下文摘要。
- 输入:题目反馈、题目摘要、反馈类型、提交用户上下文摘要
- 输出:审核建议、风险等级、归类标签、是否建议人工复核。
- 不做 function calling。
- 不直接修改题目、反馈状态或用户数据。
- 只生成建议结果,最终状态变更仍由教师或运营人员确认。
## 存储模型
不要把 Semantic Kernel 的 `ChatHistory``ChatMessageContent``KernelContent`、tool call object graph 或 provider 原始 response 作为 EF Core 持久化模型。
数据库保存 provider-neutral 消息:
- `AiConversation`租户、教师、标题、场景、Provider、模型、状态、metadata。
- `AiConversationMessage`租户、会话、序号、role、文本、tool call id/name/arguments/result summary、token、metadata。
- `AiToolCallLog`:租户、会话、消息、函数名、输入摘要、结果、耗时、错误码。
- `AiFeedbackReview`租户、题目反馈、建议、风险、标签、人工复核标记、metadata。
运行时由 Infrastructure adapter 把规范化消息转换为 SK / OpenAI-compatible 请求。DeepSeek 等 provider 只接收目标接口允许的 `role``content``tool_calls``tool_call_id` 字段SK metadata 不得原样回放给模型 API。
## Provider 与密钥边界
- AI Provider 使用 `TenantExternalProvider(capability=ai)`
- 租户自己的模型 API Key 存 `TenantSecret`,通过 `SecretRef` 关联。
- `ConfigPublic` 只允许保存公开配置,例如 provider、model、endpoint、deployment、temperature 默认值、max token 限制
- `ConfigPublic` 禁止出现 `secret``token``apiKey``key``privateKey` 等敏感字段。
- `Microsoft.SemanticKernel` NuGet 包只引用在 `Tiku.Infrastructure`
- 租户 API Key 存 `TenantSecret`,通过 `SecretRef` 关联。
- `ConfigPublic` 只允许 provider、model、endpoint、deployment、temperature、max token 等公开配置
- `ConfigPublic` 禁止 `secret``token``apiKey``key``privateKey` 等敏感字段。
- `Microsoft.SemanticKernel` 只引用在 `Tiku.Infrastructure`
- `Tiku.Api``Tiku.Application``Tiku.Domain` 不直接引用 Semantic Kernel namespace。
## 建议模块边界
## 第一批接口
Application 层后续只放业务抽象:
- `POST /api/tenant-admin/ai/conversations`
- `GET /api/tenant-admin/ai/conversations`
- `GET /api/tenant-admin/ai/conversations/{conversationId}`
- `POST /api/tenant-admin/ai/conversations/{conversationId}/messages`
- `POST /api/tenant-admin/ai/question-feedback/review`
- `IAiConversationService`
- `IAiFeedbackReviewService`
- `IAiKernelFactory`
- `IAiProviderConfigService`
Infrastructure 层负责:
- 根据当前租户 Provider 配置和 `TenantSecret` 创建 Kernel。
- 注册受审计 plugin。
- 调用 chat completion。
- 处理 provider 错误、超时、重试和调用日志。
Domain 层可增加持久化模型:
- `AiConversation`
- `AiConversationMessage`
- `AiToolCallLog`
- `AiFeedbackReview`
## 第一批实施顺序
1. 增加 AI Provider capability、Semantic Kernel 包和架构测试边界。
2. 增加 AI 配置服务测试:租户 A/B 不能互读模型配置和密钥。
3. 增加教师对话数据模型和最小 API
- `POST /api/tenant-admin/ai/conversations`
- `GET /api/tenant-admin/ai/conversations`
- `GET /api/tenant-admin/ai/conversations/{conversationId}`
- `POST /api/tenant-admin/ai/conversations/{conversationId}/messages`
4. 增加 fake AI provider先跑通对话和日志不接真实模型。
5. 增加题目反馈审核最小 API
- `POST /api/tenant-admin/ai/question-feedback/review`
6. 最后再接真实模型 Provider。
第一批先接 fake/local AI provider 跑通对话、日志和隔离,再接真实模型。
## 暂不做
- 学生端 AI。
- 复杂 RAG。
- 自动改题自动发布题目。
- 自动改题自动发布题目。
- 自动处理反馈状态。
- 让客户端指定任意 function call。
- 生产环境使用明文 API Key 配置。
- 明文 API Key 配置。