28 KiB
内容领域 V2 与学习访问投影
状态:Accepted(V2 交付主链路已落地,管理面继续补齐) 首次决策:2026-08-05 最近核对:2026-08-06
文档用途与阅读约定
本文是 V2 题库的开发架构说明,覆盖“录题 → 投放 → 发布 → 授权 → 组题 → 答题”的主链路。 代码中的英文实体名保持不翻译,便于全文搜索;首次出现时附中文职责。本文不替代 OpenAPI、Migration 或代码,三者不一致时以当前代码和数据库模型为准,并回补本文。
文中状态含义:
- 事实模型:可编辑的题目、大纲、目标、评分和授权配置。
- 发布投影:由事实模型编译出的不可变 Release、Audience Segment 和候选题行。
- 在线快照:学生当前有效访问范围与已开始练习的交付证据。
- 目标设计:已接受但尚未全部自动化或完成基线重建的能力,会明确标注,不视为当前已完成。
决策
题目是可复用资产,不再通过题库、地区、章节或知识点字段确定唯一归属。题库仅是编辑文件夹;题集是策划集合。题目通过 QuestionPlacement 放入一个大纲节点,并用受限 DNF 规则声明适用目标;发布器把事实编译为不可变 Release、Audience Segment 和候选行。学生请求只读取编译访问快照与候选投影,不在线遍历租户、套餐、地区、班级、老师和作业关系链。
所有业务使用同一套可配置目标维度。新增考试业务时创建维度、目标 Profile、业务策略、大纲和产品 Manifest,不增加业务枚举或在刷题代码中加入分支。
总体架构
flowchart LR
subgraph Authoring["内容生产面(低频写)"]
A1["题目资产 QuestionAsset\n规范题目的稳定身份"]
A2["不可变修订 QuestionRevision\n题干、选项、答案和解析"]
A3["大纲 CurriculumVersion\n章节与知识点结构"]
A4["题目投放 QuestionPlacement\n大纲位置、适用规则、评分政策"]
A1 --> A2
A1 --> A4
A3 --> A4
end
subgraph Publishing["发布编译面(离线展开)"]
P1["Release 编译器\n校验发布事实并计算适用人群"]
P2["内容版本 ContentRelease\n一次不可变发布"]
P3["人群分段 AudienceSegment\n共享相同 Profile 集合"]
P4["候选题 ContentReleaseQuestion\n锁定 Revision、Placement、评分和顺序"]
P1 --> P2
P1 --> P3
P1 --> P4
end
subgraph Access["学习授权面(事实合并与投影)"]
G1["租户业务许可 TenantBusinessLicense"]
G2["商品清单 ProductAccessManifestVersion"]
G3["学生权益 / 班级授权"]
G4["有效访问快照 EffectiveAccessProjection\nRelease、Segment、Manifest 及版本号"]
G1 --> G4
G2 --> G4
G3 --> G4
end
subgraph Delivery["在线交付面(高频读写)"]
D1["认证学习目录\n只展示 Manifest 允许的资源"]
D2["候选读取 ContentCandidateReader\n稳定种子 + 顺序回绕"]
D3["练习会话 PracticeSession"]
D4["会话题快照 PracticeSessionQuestion\n锁定题目与评分证据"]
D5["答题、交卷、错题/报告投影"]
D1 --> D2 --> D3 --> D4 --> D5
end
A2 --> P1
A4 --> P1
P2 --> G2
P3 --> G4
P4 --> D2
G4 --> D1
G4 --> D2
架构的核心是把“内容是什么”“适用于谁”“谁买到了”“本次实际交付了什么”拆成四套证据, 不让地区、院校、套餐或班级字段反向污染规范题目身份。
所有权和版本边界
QuestionAsset.TenantId是资产所有者。平台公共内容使用平台内容所有者;租户私题只能由相同租户搜索、放置和发布。QuestionRevision创建后不可修改。完整交付指纹包含题型、题干、选项、答案、子题和代码内容;Unicode 和空白规范化不会忽略可见字符大小写。- 模糊相似仅由
pg_trgm产生人工复核候选,不自动合并。 - Profile、业务目标策略、大纲、试卷规范、评分政策和 Product Manifest 可编辑 Draft;发布后不可修改。
- Release 内容行不可修改。撤回只改变 Release 生命周期;升级必须创建新 Release。
- 已开始会话锁定 Revision、Placement、Assessment Policy Version 和分值证据。
核心概念职责
| 中文概念 | 代码实体 | 稳定性与职责 | 不应承担的职责 |
|---|---|---|---|
| 规范题目 | QuestionAsset |
一道可持续修订、可跨场景复用的稳定身份;由 TenantId 标识所有者 |
不保存地区、章节、套餐或学生权限 |
| 题目修订 | QuestionRevision |
不可变交付内容;新改动创建递增 RevisionNo |
不原地覆盖历史会话使用的版本 |
| 相似题族 | QuestionFamily |
显式关联变体或同源题,辅助人工治理 | 不代表重复题自动合并结果 |
| 大纲 | Curriculum / CurriculumVersion |
某业务线的一版教学结构 | 不作为题目所有者 |
| 大纲节点 | CurriculumNode / CurriculumNodeClosure |
表达学科、章节、知识点等树结构及祖先后代查询 | 不直接表达学生权益 |
| 知识概念 | KnowledgeConcept |
跨大纲复用的知识语义 | 不等同于某版大纲节点 |
| 题目投放 | QuestionPlacement |
将题目放到某版大纲节点,并绑定评分政策、有效期和适用规则 | 不复制题干或答案 |
| 评分政策 | AssessmentPolicyVersion |
锁定评分模式、默认分值、舍入及客观题规则 | 不决定题目适用地区 |
| 目标 Profile | ExamTargetProfileVersion |
一组维度值形成的可发布考试目标,例如年份、地区、院校、专业 | 不拥有题目 |
| 内容发布 | ContentRelease |
某版大纲的一次不可变发布批次 | 发布后不接受原地更新 |
| 人群分段 | AudienceSegment |
将适用 Profile 集合相同的投放合并,减少重复候选投影 | 不代表用户或租户群组 |
| 发布候选题 | ContentReleaseQuestion |
锁定 Release 中实际可交付的 Revision、Placement、评分政策和稳定顺序 | 不保存学生作答状态 |
| 商品访问清单 | ProductAccessManifestVersion |
声明商品允许的 Release、目标、目录资源和题量限制 | 不直接判断当前学生是否有效 |
| 有效访问投影 | EffectiveAccessProjection |
合并租户许可、目标选择、商品权益和班级授权后的学生级结果 | 不替代 PostgreSQL 中的授权事实 |
| 会话题快照 | PracticeSessionQuestion |
锁定一次练习实际交付的题目与评分证据 | 不随新 Revision 或新 Release 漂移 |
“题库、题集、大纲、知识点”如何区分
- 题库是运营和录题视角的工作空间或筛选入口,不参与 V2 规范题目的唯一身份。当前 V2 核心链路直接围绕
QuestionAsset工作,不能再用QuestionBankId判断题目是否相同或是否可共享。 - 题集是策划好的题目集合。发布态用
CollectionRelease和CollectionReleaseQuestion锁定所含候选题及顺序; 它引用发布候选题,不复制规范题目。 - 大纲是业务线下的教学结构。
QuestionPlacement将同一题目放入某个CurriculumNode;同一题目可以在 不同大纲或节点有多个投放。 - 知识点分为跨大纲语义
KnowledgeConcept和具体大纲位置CurriculumNode。二者允许关联,但不能互相替代。
编辑树与在线查询投影
租户录题和内容编辑界面仍应以树形结构展示大纲,例如“学科 → 章节 → 小节 → 知识点”,因为树适合人工定位、 拖动和维护内容位置。后端不会把完整路径复制到题目,而是用以下两种结构保存大纲:
CurriculumNode.ParentId保存直接父子关系,是编辑树的事实来源;CurriculumNodeClosure保存(AncestorNodeId, DescendantNodeId, Depth),用于高效查询祖先、后代和整棵子树, 避免每次递归遍历邻接表。
题目导入和题目投放是两个独立用例:
导入 / 录入题目
→ 查重、复用或创建 QuestionAsset
→ 创建不可变 QuestionRevision
选择大纲树节点
→ 创建 QuestionPlacement
→ 保存 CurriculumVersionId + CurriculumNodeId
→ 配置适用目标、评分政策和知识概念
因此前端可以把“上传 → 查重预览 → 选择树节点 → 完成导入”组织成一个连续流程,但后端不能把
QuestionAsset 的创建与某个树节点绑定为同一个不可拆分操作。同一道题可以导入一次,再通过多个 Placement
放入不同大纲或不同节点。
发布时,ContentReleaseCompiler 将树上的有效 Placement 编译为扁平的 ContentReleaseQuestion 候选行,
每行直接保存 CurriculumNodeId、QuestionRevisionId、AudienceSegmentId、评分政策和稳定 Ordinal。
学生在线组题读取这些发布候选,不再从题目向上或向下遍历大纲树,也不在线重新计算目标适用规则。
编辑视图:树形 CurriculumNode
↓ 选择节点并创建 Placement
发布阶段:编译适用规则和交付版本
↓
查询视图:扁平 ContentReleaseQuestion 候选投影
当前 ContentCandidateReader 对 CurriculumNodeId 使用精确匹配。请求父章节时不会自动包含所有子节点题目。
如果产品定义“点击父章节即练习整个子树”,必须显式选择并实现以下策略之一:
- 发布时把子树范围预展开到专用候选投影;
- 查询前通过
CurriculumNodeClosure得到后代 NodeId,再批量过滤发布候选; - 为章节策划并发布
CollectionRelease,在线按题集成员读取。
在方案确定前,不得默认父节点查询已经包含子节点,也不要在高频路径临时加入递归逐层查询。
关系图
erDiagram
QUESTION_ASSET ||--o{ QUESTION_REVISION : versions
QUESTION_ASSET ||--o{ QUESTION_PLACEMENT : placed_as
CURRICULUM ||--o{ CURRICULUM_VERSION : versions
CURRICULUM_VERSION ||--o{ CURRICULUM_NODE : contains
CURRICULUM_NODE ||--o{ QUESTION_PLACEMENT : receives
QUESTION_PLACEMENT ||--o{ PLACEMENT_RULE_GROUP : OR
PLACEMENT_RULE_GROUP ||--o{ PLACEMENT_RULE_CONDITION : AND
EXAM_TARGET_PROFILE ||--o{ EXAM_TARGET_PROFILE_VERSION : versions
EXAM_TARGET_PROFILE_VERSION ||--o{ EXAM_TARGET_PROFILE_VALUE : dimensions
CONTENT_RELEASE ||--o{ AUDIENCE_SEGMENT : compiles
AUDIENCE_SEGMENT ||--o{ AUDIENCE_SEGMENT_MEMBER : profiles
CONTENT_RELEASE ||--o{ CONTENT_RELEASE_QUESTION : candidates
PRODUCT_ACCESS_MANIFEST_VERSION ||--o{ PRODUCT_MANIFEST_RELEASE : allows
PRODUCT_ACCESS_MANIFEST_VERSION ||--o{ PRODUCT_MANIFEST_TARGET : allows
STUDENT_ENTITLEMENT }o--|| PRODUCT_ACCESS_MANIFEST_VERSION : grants
TENANT_BUSINESS_LICENSE ||--o{ TENANT_LICENSED_TARGET : bounds
EFFECTIVE_ACCESS_PROJECTION }o--o{ CONTENT_RELEASE : ids
EFFECTIVE_ACCESS_PROJECTION }o--o{ AUDIENCE_SEGMENT : ids
关系图按中文可分为五组:
| 分组 | 主要实体 | 说明 |
|---|---|---|
| 题目事实 | QuestionAsset、QuestionRevision、QuestionPlacement |
稳定身份、不可变内容、场景化投放 |
| 教学与目标 | Curriculum*、TargetDimensionDefinition、ExamTargetProfile* |
教学结构和可配置适用目标 |
| 发布投影 | ContentRelease、AudienceSegment*、ContentReleaseQuestion |
将复杂规则编译为在线可直接过滤的数据 |
| 商品授权 | ProductAccessManifest*、StudentEntitlement、ClassAssignmentGrant |
定义可卖内容与学生实际获得的 Grant |
| 在线交付 | EffectiveAccessProjection、PracticeSession* |
学生级授权快照和会话级不可变交付证据 |
租户所有权规则
| 数据 | 所有权范围 | 跨租户访问规则 |
|---|---|---|
TargetDimensionDefinition、TargetNode、ExamTargetProfile*、BusinessTargetPolicy* |
平台全局定义 | 普通租户只按已许可 Profile 使用,不创建同名业务枚举 |
QuestionAsset、QuestionRevision |
内容所有者租户 | 投放仅可引用当前租户私题或唯一平台内容租户的公共题 |
Curriculum*、QuestionPlacement、AssessmentPolicy*、ContentRelease* |
发布内容的租户 | 必须以明确 ContentOwnerTenantId 进入受审计的 ITenantExecutionScope 读取 |
ProductAccessManifest*、StudentEntitlement、ClassAssignmentGrant、EffectiveAccessProjection |
消费租户 | 所有查询必须带当前 TenantId,不得从请求体接受任意租户切换 |
PracticeSession*、答案、错题、收藏和报告 |
学生所在租户 | 会话中另存题目所有者 ID,用于读取已授权且锁定的 Revision |
平台公共题不是“无租户数据”,而是归属于唯一 TenantMode.PlatformOwned 内容租户。跨租户读取必须同时满足:
- 当前操作已经从 Manifest、Release 或 Placement 得到明确的所有者 ID;
- 使用受审计 System Scope 切换到该所有者;
- 查询再次带所有者 ID 和不可变资源 ID;
- 不把 System Scope 暴露为通用控制器参数。
目标配置样例
以下是种子/管理接口应表达的配置意图,不是在线授权脚本。
[
{
"business": "upgrade_education",
"selectionMode": "single_from_tenant_license",
"maxActiveTargets": 1,
"maxPrimaryTargets": 1,
"maxAlternateTargets": 0,
"cooldownDays": 30,
"requiredDimensions": ["exam_year", "exam_system", "administrative_area", "subject_track"]
},
{
"business": "civil_service",
"selectionMode": "base_plus_entitled_targets",
"includesBaseContent": true,
"maxActiveTargets": 16,
"maxPrimaryTargets": 1,
"requiredDimensions": ["exam_year", "exam_system", "paper_category", "administrative_area"]
},
{
"business": "postgraduate",
"selectionMode": "base_plus_primary_and_entitled_alternates",
"includesBaseContent": true,
"maxActiveTargets": 16,
"maxPrimaryTargets": 1,
"requiredDimensions": ["exam_year", "exam_system", "institution", "school", "major", "subject_code"]
}
]
示例 Profile:
2027 河南统招专升本 / 理科 / 高等数学2027 国考 / 行政执法类2027 江苏省考 / A 类2027 四川大学 / 计算机学院 / 软件工程 / 自主命题 874
发布和在线不变量
发布在 Repeatable Read 事务内执行,并按“所有者 + 大纲版本”取得 PostgreSQL 事务级 advisory lock。发布器验证 Revision、评分政策和 Profile,使用慢速 DNF 判定器生成 Segment,在同一事务写 Release、候选行和 Outbox。相同 Profile 集合共享 Segment;候选 ordinal 在发布时稳定生成。
访问编译器只接受当前租户和登录用户,从有效租户业务许可证、许可 Profile、V2 学生权益、Product Manifest、当前主/备目标和班级 Grant 生成 EffectiveAccessProjection。Redis/FusionCache 缓存 30 秒且关闭 fail-safe;缓存未命中回源 PostgreSQL,事实库失败时不返回过期授权。
创建会话最终只能执行:
- 获取一个
EffectiveAccessProjection。 - 验证请求资源被其中的 Manifest 允许。
- 按 Release、Segment、课程节点和稳定随机种子读取候选。
- 批量锁定交付版本和评分证据、原子扣减额度、创建会话。
答题期间不再查询套餐、班级、老师、目标层级或内容规则。强撤权版本仍在答题和交卷入口检查。
规则表达与发布编译
受限 DNF 适用规则
QuestionPlacementRuleGroup 之间是 OR(任一组成立),同一组内的
QuestionPlacementRuleCondition 是 AND(全部成立)。没有规则组表示该投放适用于当前业务线的全部已发布
Profile;空规则组不匹配任何 Profile。支持的操作符为:
| 操作符 | 中文含义 | 判定方式 |
|---|---|---|
Exact |
精确等于 | Profile 在该维度恰好包含目标节点 |
DescendantOf |
等于或属于其后代 | Profile 节点等于目标节点,或祖先集合包含目标节点 |
NotExact |
不精确等于 | 对 Exact 结果取反 |
NotDescendantOf |
不属于该节点及后代 | 对 DescendantOf 结果取反 |
示例:“(河南 且 理科)或(山东 且 不属于艺术类)”应建成两个规则组,而不是把四个条件放在同一组。 发布前必须验证维度和节点匹配、每个投放至少命中一个已发布 Profile,避免生成永远不可达的内容。
发布编译时序
sequenceDiagram
autonumber
participant U as "内容管理员"
participant API as "V2 录题 API"
participant C as "ContentReleaseCompiler(发布编译器)"
participant DB as "PostgreSQL(事实与发布投影)"
participant O as "LearningOutbox(学习事件箱)"
U->>API: 发布某个 CurriculumVersion(大纲版本)
API->>C: PublishContentReleaseCommand
C->>DB: 开启 Repeatable Read 事务
C->>DB: 获取“内容所有者 + 大纲版本”事务级 advisory lock
C->>DB: 校验已发布大纲、有效 Placement、题目 Revision 和评分政策
C->>DB: 读取已发布 Profile 与目标层级,执行受限 DNF 匹配
C->>DB: 创建 ContentRelease(Compiling)
C->>DB: 写 AudienceSegment / Member / ContentReleaseQuestion
C->>DB: 将 Release 改为 Published
C->>O: 同事务写 content_release_published 事件
C->>DB: 提交事务
C-->>API: 返回 ReleaseNo、候选数、Segment 数和 SourceFingerprint
SourceFingerprint 用来证明本次发布输入;ContentReleaseQuestion.Ordinal 在
“Segment + 大纲节点”范围内稳定生成。新 Revision 只影响下一次 Release,不能回写旧 Release。
学习授权编译
授权事实链
租户是否可经营该业务
TenantBusinessLicense + TenantLicensedTarget
↓
商品卖什么、允许哪些目标和目录资源
ProductAccessManifestVersion
├─ ProductManifestRelease
├─ ProductManifestTarget
└─ ProductManifestResource
↓
学生通过什么渠道获得
StudentEntitlement 或 ClassAssignmentGrant
↓
学生当前选择哪个主目标 / 备选目标
StudentTargetSelectionHistory
↓
编译后的学生级访问结果
EffectiveAccessProjection / EffectiveLearningAccessSnapshot
EffectiveLearningAccessService 按 (TenantId, UserId, BusinessLineId) 编译并缓存 30 秒:
- 租户和
TenantBusinessLicense必须有效; - 学生必须有且仅有一个当前主目标,主/备目标都必须被租户许可;
- 有效
StudentEntitlement或班级ClassAssignmentGrant必须关联已发布 Manifest; - Profile 角色必须满足
ProductManifestTarget,数量不得超过 Manifest 上限; - Manifest 引用的 Release 必须存在、已发布且属于相同业务线;
- 当前 Profile 必须在 Release 的
AudienceSegmentMember中命中至少一个 Segment; - 将 Release、Segment、Manifest ID 和三个版本号写入
EffectiveAccessProjection并返回快照。
缓存关闭 fail-safe。Redis/FusionCache 未命中可回源 PostgreSQL,但授权事实库失败时不能返回过期授权。
GrantVersion 表示权益变化,ContentVersion 表示内容/许可变化,StrongRevocationVersion 用于立即阻断已开始会话。
普通撤回默认只影响新建会话;账号停用、租户停用或明确强撤权时,提高强撤权版本。答题和交卷入口通过
一秒、无 fail-safe 的强撤权缓存核对版本,不一致即返回 practice_access_revoked。
在线组题与答题时序
sequenceDiagram
autonumber
participant S as "学生端"
participant L as "学习服务"
participant A as "有效访问服务"
participant R as "候选读取器"
participant DB as "PostgreSQL"
S->>L: 选择目录资源并创建练习
L->>DB: 根据 ProductManifestResource 确认唯一业务线
L->>A: 读取当前学生 EffectiveLearningAccessSnapshot
A-->>L: Release / Segment / Manifest / 版本号
L->>DB: 验证商品权益或班级 Grant,并限制到请求资源
L->>R: 按内容所有者、Release、Segment、节点/题集、稳定种子取候选
R-->>L: ContentReleaseQuestion 候选行
L->>DB: 批量加载锁定 Revision 与 AssessmentPolicyVersion
L->>DB: 原子预留总题量和每日题量
L->>DB: 创建 PracticeSession 与 PracticeSessionQuestion 快照
L-->>S: 返回不含答案和解析的会话题目
S->>L: 答题 / 交卷
L->>DB: 校验会话版本、幂等键与 StrongRevocationVersion
L->>DB: 使用会话内 GradingRulesSnapshot 评分并写答案事实
L->>DB: 同事务写 LearningOutbox,异步更新错题等投影
候选读取不使用 ORDER BY random()。系统以 (UserId, ResourceType, ResourceId) 计算稳定种子,在按
Release、Segment、节点、Ordinal、ID 排序的候选集合上选择起点并回绕;当前单次会话上限为 100 题,候选读取
服务自身把查询上限限制在 1~500。
会话创建时必须锁定:
ContentReleaseQuestionId:证明候选来自哪次发布;QuestionAssetOwnerTenantId、QuestionAssetId、QuestionRevisionId:证明题目身份和具体内容;QuestionPlacementId:证明当时的大纲位置与适用语境;AssessmentPolicyVersionId和GradingRulesSnapshot:证明当时如何评分;GrantVersion、StrongRevocationVersion与访问快照:证明当时为什么有权创建会话。
因此发布新 Revision、撤回旧 Placement 或发布新 Release 都不能改变已开始会话的题目和评分结果。
状态与变更规则
| 聚合 | 状态流 | 变更规则 |
|---|---|---|
QuestionAsset |
Draft → Published → Archived |
内容变化创建新 QuestionRevision;切换 CurrentRevisionId 只影响以后发布 |
QuestionPlacement |
Draft → Active → Retired |
激活后可被下一次 Release 编译;退役不删除历史发布证据 |
CurriculumVersion / AssessmentPolicyVersion / Profile Version |
Draft → Published → Retired |
Release 只接受 Published 版本 |
ContentRelease |
当前发布器执行 Compiling → Published;Draft、Failed、Retired 是生命周期状态 |
当前事务失败会整体回滚;若后续需要保留 Failed 记录,必须由显式失败处理流程写入。Published 内容行不可修改,升级创建新 ReleaseNo |
ProductAccessManifestVersion |
Draft → Published → Retired |
权益必须钉住明确版本,不跟随可编辑 Definition 漂移 |
StudentEntitlement |
Active → Expired / Revoked / Cancelled |
普通撤回阻止新会话;强撤权另行提高版本 |
ClassAssignmentGrant |
Active → Revoked / Expired |
只授权 Manifest 中明确列出的资源 |
数据库配置和服务校验共同维护不变量。不要只在前端隐藏非法状态,也不要通过物理删除回收已经被 Release、会话或 答题记录引用的版本。
平台公共内容与 Cell 投影
PlatformContentPackageVersion 将平台公共 ContentRelease 封装为不可变内容包;消费租户的
ProductManifestRelease 可以同时钉住内容包版本。PlatformContentPackageCellProjection 记录某 Cell 已就绪的包版本。
应用内容包时按“平台内容所有者 + PackageCode + CellId”加 advisory lock,并用 EventSequence 保证:
- 更小序号被忽略为乱序事件;
- 相同序号和相同版本/哈希是幂等重复;
- 相同序号但版本或哈希不同是冲突;
- 只有 Published 且哈希一致的不可变包可变为
Ready。
创建练习时,若 Manifest 链接了平台内容包,只有当前租户所在 Cell 的投影为 Ready 才能使用;这为后续 Cell
架构的数据分发提供接缝,但不代表跨 Cell 发布、传输和恢复流程已全部自动化。
代码导航
| 开发目的 | 主要入口 | 关键实现 |
|---|---|---|
| 租户私题录入、查重、Revision、Placement、导入、发布 | api/tenant/content-v2/* |
ContentV2AuthoringController、ContentV2AuthoringService |
| 平台公共题录入与发布 | api/platform/content-v2/* |
PlatformContentV2AuthoringController、PlatformContentActorResolver |
| 题目指纹与适用规则 | Application 纯规则 | QuestionDeliveryFingerprint、QuestionApplicabilityEvaluator |
| Release 编译 | IContentReleaseCompiler |
ContentReleaseCompiler |
| 在线候选读取 | IContentCandidateReader |
ContentCandidateReader |
| 学生目标与认证目录 | api/student/learning-context、learning-targets、catalog/resources |
StudentLearningTargetService、StudentLearningCatalogService |
| 班级 V2 授权 | api/tenant/classes/{classId}/learning-assignments |
V2LearningAccessAdministrationService |
| 有效访问投影 | IEffectiveLearningAccessService |
EffectiveLearningAccessService |
| 创建与读取练习 | api/student/learning/practice-sessions* |
PracticeSessionService、V2PracticeSessionService |
| 答题与强撤权 | 学习答题入口 | AnsweringService、LearningStrongRevocationService |
| 平台包 Cell 就绪投影 | IPlatformContentPackageProjectionService |
PlatformContentPackageProjectionService |
领域实体集中在 Tiku.Domain/Content/ContentV2*.cs 和
Tiku.Domain/Learning/LearningAccessV2Entities.cs;EF 约束集中在
Tiku.Infrastructure/Persistence/Configurations/ContentV2*.cs 与
LearningAccessV2Configurations.cs。关键回归测试位于 Tiku.UnitTests/ContentV2RulesTests.cs、
Tiku.IntegrationTests/ContentV2/ 和 Tiku.IntegrationTests/Api/LearningEndpointTests.cs。
当前实现边界
截至 2026-08-06,代码已经具备 V2 题目查重/复用、不可变 Revision、Placement、Release 编译、 Audience Segment、稳定候选读取、Manifest 授权投影、V2 会话快照、强撤权检查和平台内容包 Cell 就绪投影。
以下仍应视为后续开发工作,不应从领域实体存在推断为完整产品能力:
- Curriculum、Profile、BusinessTargetPolicy、AssessmentPolicy、Manifest、CollectionRelease 和 BlueprintRelease 的 完整管理 API、审核发布体验与批量运维工具仍需逐项核对和补齐;
- Excel/Word 文件解析不属于
ContentV2AuthoringService,该服务接收解析后的结构化行并执行逐行决策; - Release 发布事件已写入 Outbox,但所有下游内容版本失效、跨 Cell 分发和失败恢复链路仍需按 Worker 实现核验;
- 当前有效访问快照保存 ID 数组,适合基础主链路;全国规模下的基数、索引、缓存失效和 PostgreSQL 查询计划仍需真实数据压测;
- 旧 V1 实体或迁移残留不代表 V2 运行时可以继续依赖。新代码不得重新建立
QuestionBankId、地区字段或旧ContentSlice到 V2 规范题目身份的耦合。
PostgreSQL 选择
ltree:行政区、院校等单父层级目标节点及祖先/后代匹配。- 邻接表 + 闭包表:大纲节点;支持同一知识概念出现在不同大纲。
pg_trgm:所有者范围内的疑似重复题检索。- 原生月分区:答案尝试、访问事件和 Outbox。
- 不使用
ORDER BY random();按稳定 ordinal、种子起点和区间回绕采样。
首版不引入 Citus、图数据库、Roaring Bitmap 或 pgvector。它们不会参与权限判定或在线刷题。
基线重建要求
本节是破坏式开发阶段的目标设计与验收门槛,不是对当前 Migration 历史已经完成重建的声明。
删除 V1 内容/地区授权模型和旧迁移后,新的单一 InitialSchema 必须重新包含租户隔离函数与触发器、SaaS 防护对象、特殊 GIN/GiST/部分索引和月分区对象。空 PostgreSQL 执行 DbMigrator 后必须无待生成迁移。开发数据不迁移,种子按上述配置重新生成。