Skip to content
bitzorcas
中EN

Reference

Guidance 内容生命周期与持久化

深入解释 GuidanceContent 统一聚合、字段边界、Draft/Published 状态、同行修订、平台/租户存储、软删除、语言归一化、版本历史和并发缺口。

Last updated

GuidanceContent 同时是领域聚合与持久化模型。平台内容和租户内容共享一张表;TenantId 可空,因此不能使用会排除平台行的普通全局租户过滤,所有 Store 查询必须显式表达范围。

1. 表与索引

GuidanceContent
PlatformAggregateRoot

RouteKey + LanguageCode index

TenantId + RouteKey + ComponentId index

StatusName index

三个索引都是普通查询索引,没有业务唯一索引。数据库允许同一 TenantId/RouteKey/ComponentId/LanguageCode/RequiredRole 出现多个 Draft 或 Published,最终由排序隐式选胜者。

2. 字段合同

字段当前约束
ContentId必填,最长 36;"0" 占位由 Store 重分配
RouteKey必填,Trim,最长 300
ComponentId可空,Trim,最长 120
Title必填,Trim,最长 200
BodyMarkdown必填,Trim,大文本
RequiredRole可空,单角色,最长 120
TenantId可空,最长 36
LanguageCode最长 10,当前 zh-CN/en-US
ContentVersion正整数,Create=1,Revise+1
StatusNameDraft/Published 严格字符串

BodyMarkdown 没有内容长度上限,也没有 HTML、链接、图片或危险协议校验。服务端返回原文,安全渲染属于尚未固定的跨端合同。

3. 创建语义

直接构造经过校验的 Draft
// Create 会统一 Trim 字段并校验持久化列长度。
var created = GuidanceContent.Create(
contentId: "0",
routeKey: "/cases",
componentId: "case-form",
title: "案件录入",
bodyMarkdown: "请先填写委托人与案由。",
requiredRole: "Attorney",
tenantId: currentTenantId,
languageCode: LanguageCode.ZhCn,
clock);
// Store 将空占位 ID 替换为最终持久化 ID。
if (created.IsFailure)
return created.Error;

Command 对 TenantId 有特殊推导:普通租户省略时写当前 Tenant;平台租户省略时写 null;平台租户也能显式为其他 Tenant 创建。Guard 不验证目标租户真实存在。

4. 语言行为

LanguageCode.Normalize 只显式识别 en-US,其余任何输入都回退 zh-CN,包括拼写错误、空值和未来语言。创建与查询均沿用该行为。持久化 Restore 却只接受精确 zh-CN/en-US,损坏数据会失败关闭。

商业 API 更适合对未知语言返回 Validation,并把 fallback 链作为产品配置;否则 en-GB 或 zh-TW 会悄悄命中简体中文。

5. 状态机

Create version 1Publish same rowRevise version++Revise version++DeleteDelete

Draft

Published

Deleted

重复 Publish 返回 Guidance.Content.AlreadyPublished。Revise 的 Body 必填、Title 可空表示保留旧值;修订总是转 Draft。没有 Scheduled、Archived、Rejected 或 Unpublished。

6. 同行修订的产品后果

Published 内容被 Revise 后,同一行立即变成 Draft,Contextual Query 不再返回它。旧正文被覆盖,ContentVersion 只是当前行计数,不是可查询 Revision。系统无法回答谁在何时发布了什么,也不能 diff/rollback。

GA 目标应采用 Draft 与不可变 PublishedRevision 分离,或至少保留完整历史并维护 CurrentPublishedVersion 指针。修订工作区不得影响线上读。

7. 保存语义

Store 在 ContentId 为空占位时分配 ID,然后按全局 ID 判断 Add/Update。它不在保存时重新验证调用租户,也不接收 expected version;租户安全依赖 Handler Guard 与聚合 TenantId。

当前修订与发布调用顺序
// Handler 必须在修改同行聚合前完成对象租户检查。
var content = (await store.GetByIdAsync(contentId, cancellationToken)).Value;
var access = GuidanceApplicationGuards.EnsureCanMutate(currentUser, content!);
if (access.IsFailure)
return access.Error;
// Revise 修改当前聚合并转回 Draft;Publish 再修改同一聚合。
var revised = content!.Revise(newTitle, newBodyMarkdown, clock);
return revised.IsFailure
? revised.Error
: await store.SaveAsync(content, cancellationToken);

基础聚合可能带框架 Version,但 HTTP Command 没有 ETag/ExpectedVersion,文档不能承诺并发冲突一定被稳定映射。需要用真实双 ORM 并发更新证明。

8. 软删除

Delete Handler 先读取、做 Guard,再调用 DeleteWhereAsync(content.Id == contentId)。Store 不返回 affected count,也不把 TenantId 放入删除谓词。ID 被设计为全局主键,但 TOCTOU、重复删除和并发发布需要集成测试。

删除没有 Restore API、保留期或历史引用策略。已存在的 AI Conversation 仍可能保存删除前正文;删除内容不会关闭或刷新 Session。

9. 持久化恢复

Restore 严格检查必填字段、长度、语言、正版本、枚举和 IsDeleted/DeleteTime 一致性。StatusName 未登记时抛 InvalidOperationException,不会静默变 Draft。此 fail-closed 行为已有测试。

平台/租户内容共表的迁移还要求全局 ContentId 唯一。备份恢复要验证 null TenantId 语义、状态、软删除和框架 Version。

10. 自然键设计

目标自然键至少包含 Scope(platform/tenant)、TenantId、RouteKey、ComponentId、LanguageCode 与 AudiencePolicy。Draft 可允许每编辑分支一条,Current Published 必须唯一。若用 filtered unique index,需要验证所有目标数据库/ORM 迁移能力。

RequiredRole 未来升级为 Audience Policy 时,自然键应引用稳定 PolicyId/Hash,而不是把可变角色名直接塞入唯一键。

11. Markdown 安全

BodyMarkdown 可能包含原始 HTML、javascript: 链接、远程像素、超大 data URL、嵌套内容或提示词注入。存储层不应自称安全;管理端预览与用户端渲染必须共用 allowlist sanitizer、URL policy、CSP 和资源代理策略。

若正文进入 AI Prompt,还要执行另一套数据分类与敏感信息策略。对浏览器安全的 Markdown 不等于适合发送给外部 Provider。

12. 测试矩阵

场景当前证据GA 补充
字段必填/长度聚合实现HTTP 错误合同
未知持久化状态应用测试数据库迁移隔离
选择与软删除内存/双 ORM并发与 affected count
发布后修订实现存在线上版本持续可见
重复自然键未限制唯一冲突
并发修订/发布未覆盖ETag/无丢失更新
Markdown 攻击未覆盖渲染端到端安全
删除后会话未覆盖Prompt 撤销/清理

13. 检查命令

Terminal window
# 聚合字段、状态与同行修订。
rg -n "BitzTable|ContentVersion\+\+|Status = GuidanceStatus|BodyMarkdown" \
src/Platform/Guidance/BitzOrcas.Platform.Guidance.Contracts/Content -g '*.cs'
# 不可变发布、历史和并发 API 当前预期无命中。
rg -n "PublishedRevision|CurrentPublished|Rollback|ExpectedVersion|ETag" \
src/Platform/Guidance -g '*.cs'

Guidance 总览 · 上下文选择与授权 · 测试与 GA

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%