Files
tiku-backend.net/docs/architecture/content-domain-v2.md
xiong bb8a4deda5
Some checks failed
ci / release-gate (push) Has been cancelled
feat(content): 增加编辑树与在线查询投影的架构说明
2026-08-06 10:17:50 +08:00

28 KiB
Raw Blame History

内容领域 V2 与学习访问投影

状态AcceptedV2 交付主链路已落地,管理面继续补齐) 首次决策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 判断题目是否相同或是否可共享。
  • 题集是策划好的题目集合。发布态用 CollectionReleaseCollectionReleaseQuestion 锁定所含候选题及顺序; 它引用发布候选题,不复制规范题目。
  • 大纲是业务线下的教学结构。QuestionPlacement 将同一题目放入某个 CurriculumNode;同一题目可以在 不同大纲或节点有多个投放。
  • 知识点分为跨大纲语义 KnowledgeConcept 和具体大纲位置 CurriculumNode。二者允许关联,但不能互相替代。

编辑树与在线查询投影

租户录题和内容编辑界面仍应以树形结构展示大纲,例如“学科 → 章节 → 小节 → 知识点”,因为树适合人工定位、 拖动和维护内容位置。后端不会把完整路径复制到题目,而是用以下两种结构保存大纲:

  • CurriculumNode.ParentId 保存直接父子关系,是编辑树的事实来源;
  • CurriculumNodeClosure 保存 (AncestorNodeId, DescendantNodeId, Depth),用于高效查询祖先、后代和整棵子树, 避免每次递归遍历邻接表。

题目导入和题目投放是两个独立用例:

导入 / 录入题目
  → 查重、复用或创建 QuestionAsset
  → 创建不可变 QuestionRevision

选择大纲树节点
  → 创建 QuestionPlacement
  → 保存 CurriculumVersionId + CurriculumNodeId
  → 配置适用目标、评分政策和知识概念

因此前端可以把“上传 → 查重预览 → 选择树节点 → 完成导入”组织成一个连续流程,但后端不能把 QuestionAsset 的创建与某个树节点绑定为同一个不可拆分操作。同一道题可以导入一次,再通过多个 Placement 放入不同大纲或不同节点。

发布时,ContentReleaseCompiler 将树上的有效 Placement 编译为扁平的 ContentReleaseQuestion 候选行, 每行直接保存 CurriculumNodeIdQuestionRevisionIdAudienceSegmentId、评分政策和稳定 Ordinal。 学生在线组题读取这些发布候选,不再从题目向上或向下遍历大纲树,也不在线重新计算目标适用规则。

编辑视图:树形 CurriculumNode
        ↓ 选择节点并创建 Placement
发布阶段:编译适用规则和交付版本
        ↓
查询视图:扁平 ContentReleaseQuestion 候选投影

当前 ContentCandidateReaderCurriculumNodeId 使用精确匹配。请求父章节时不会自动包含所有子节点题目。 如果产品定义“点击父章节即练习整个子树”,必须显式选择并实现以下策略之一:

  1. 发布时把子树范围预展开到专用候选投影;
  2. 查询前通过 CurriculumNodeClosure 得到后代 NodeId再批量过滤发布候选
  3. 为章节策划并发布 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

关系图按中文可分为五组:

分组 主要实体 说明
题目事实 QuestionAssetQuestionRevisionQuestionPlacement 稳定身份、不可变内容、场景化投放
教学与目标 Curriculum*TargetDimensionDefinitionExamTargetProfile* 教学结构和可配置适用目标
发布投影 ContentReleaseAudienceSegment*ContentReleaseQuestion 将复杂规则编译为在线可直接过滤的数据
商品授权 ProductAccessManifest*StudentEntitlementClassAssignmentGrant 定义可卖内容与学生实际获得的 Grant
在线交付 EffectiveAccessProjectionPracticeSession* 学生级授权快照和会话级不可变交付证据

租户所有权规则

数据 所有权范围 跨租户访问规则
TargetDimensionDefinitionTargetNodeExamTargetProfile*BusinessTargetPolicy* 平台全局定义 普通租户只按已许可 Profile 使用,不创建同名业务枚举
QuestionAssetQuestionRevision 内容所有者租户 投放仅可引用当前租户私题或唯一平台内容租户的公共题
Curriculum*QuestionPlacementAssessmentPolicy*ContentRelease* 发布内容的租户 必须以明确 ContentOwnerTenantId 进入受审计的 ITenantExecutionScope 读取
ProductAccessManifest*StudentEntitlementClassAssignmentGrantEffectiveAccessProjection 消费租户 所有查询必须带当前 TenantId,不得从请求体接受任意租户切换
PracticeSession*、答案、错题、收藏和报告 学生所在租户 会话中另存题目所有者 ID用于读取已授权且锁定的 Revision

平台公共题不是“无租户数据”,而是归属于唯一 TenantMode.PlatformOwned 内容租户。跨租户读取必须同时满足:

  1. 当前操作已经从 Manifest、Release 或 Placement 得到明确的所有者 ID
  2. 使用受审计 System Scope 切换到该所有者;
  3. 查询再次带所有者 ID 和不可变资源 ID
  4. 不把 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事实库失败时不返回过期授权。

创建会话最终只能执行:

  1. 获取一个 EffectiveAccessProjection
  2. 验证请求资源被其中的 Manifest 允许。
  3. 按 Release、Segment、课程节点和稳定随机种子读取候选。
  4. 批量锁定交付版本和评分证据、原子扣减额度、创建会话。

答题期间不再查询套餐、班级、老师、目标层级或内容规则。强撤权版本仍在答题和交卷入口检查。

规则表达与发布编译

受限 DNF 适用规则

QuestionPlacementRuleGroup 之间是 OR任一组成立,同一组内的 QuestionPlacementRuleConditionAND全部成立。没有规则组表示该投放适用于当前业务线的全部已发布 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: 创建 ContentReleaseCompiling
    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 秒:

  1. 租户和 TenantBusinessLicense 必须有效;
  2. 学生必须有且仅有一个当前主目标,主/备目标都必须被租户许可;
  3. 有效 StudentEntitlement 或班级 ClassAssignmentGrant 必须关联已发布 Manifest
  4. Profile 角色必须满足 ProductManifestTarget,数量不得超过 Manifest 上限;
  5. Manifest 引用的 Release 必须存在、已发布且属于相同业务线;
  6. 当前 Profile 必须在 Release 的 AudienceSegmentMember 中命中至少一个 Segment
  7. 将 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 题,候选读取 服务自身把查询上限限制在 1500。

会话创建时必须锁定:

  • ContentReleaseQuestionId:证明候选来自哪次发布;
  • QuestionAssetOwnerTenantIdQuestionAssetIdQuestionRevisionId:证明题目身份和具体内容;
  • QuestionPlacementId:证明当时的大纲位置与适用语境;
  • AssessmentPolicyVersionIdGradingRulesSnapshot:证明当时如何评分;
  • GrantVersionStrongRevocationVersion 与访问快照:证明当时为什么有权创建会话。

因此发布新 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 → PublishedDraftFailedRetired 是生命周期状态 当前事务失败会整体回滚;若后续需要保留 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/* ContentV2AuthoringControllerContentV2AuthoringService
平台公共题录入与发布 api/platform/content-v2/* PlatformContentV2AuthoringControllerPlatformContentActorResolver
题目指纹与适用规则 Application 纯规则 QuestionDeliveryFingerprintQuestionApplicabilityEvaluator
Release 编译 IContentReleaseCompiler ContentReleaseCompiler
在线候选读取 IContentCandidateReader ContentCandidateReader
学生目标与认证目录 api/student/learning-contextlearning-targetscatalog/resources StudentLearningTargetServiceStudentLearningCatalogService
班级 V2 授权 api/tenant/classes/{classId}/learning-assignments V2LearningAccessAdministrationService
有效访问投影 IEffectiveLearningAccessService EffectiveLearningAccessService
创建与读取练习 api/student/learning/practice-sessions* PracticeSessionServiceV2PracticeSessionService
答题与强撤权 学习答题入口 AnsweringServiceLearningStrongRevocationService
平台包 Cell 就绪投影 IPlatformContentPackageProjectionService PlatformContentPackageProjectionService

领域实体集中在 Tiku.Domain/Content/ContentV2*.csTiku.Domain/Learning/LearningAccessV2Entities.csEF 约束集中在 Tiku.Infrastructure/Persistence/Configurations/ContentV2*.csLearningAccessV2Configurations.cs。关键回归测试位于 Tiku.UnitTests/ContentV2RulesTests.csTiku.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 后必须无待生成迁移。开发数据不迁移,种子按上述配置重新生成。