发布单与公开代次
发布单固定内容修订,构建成功后才允许激活。工作稿和审批记录位于编辑域,cms_contents 是当前公开投影及稳定身份;保存工作稿不会修改公开正文。
构建与激活
POST /api/cms/releases固定已批准的修订和撤下清单,并记录创建时的baseGenerationId。不指定内容时,发布单用于发布栏目、页面、导航、部件和站点公开配置。- 构建进入任务中心
cms-release-build。Worker 在 PostgreSQL 中创建由服务器数字 ID 命名的关系快照cms_generation_{id},复制本站公开定义及其关联,并在该快照应用目标修订。编辑、权限、会话、互动答卷与访问统计不在快照内。 - 模板和所有取数使用显式事务上下文,文件写入
CMS_STATIC_ROOT/{siteCode}/generation-{id}/。定义缓存按代次隔离或绕过;候选渲染不写公开缓存、访问统计或 CDN。生成后记录关系快照摘要与文件 SHA-256 清单。 - 激活前检查产物完整性、精确修订批准和素材授权。短事务在站点锁内比较当前代次与预期代次,更新公开投影、已发布修订引用和
cms_site_generations.active_generation_id。任何失败都保留旧代次。并发发布冲突返回 409,不能静默覆盖其他发布。 - 排期入参为所选
timeZone下的YYYY-MM-DD HH:mm:ss墙上时间,服务端转换为绝对时刻;不存在的夏令时时间被拒绝,响应仍按所选 IANA 时区显示。隐式发布继承内容修订的排期。回滚重新指向历史部署并恢复其公开投影,不修改历史修订。
内容发布与配置发布的自动化程度不同:
- 内容发布单(
source=content,含批量发布的编组)构建成功后自动激活 - 配置发布单(
source=configuration)默认只构建、待人工审阅后激活;带activateAt时由定时任务到点激活,两者都要求cms:publish:manage - 手工发布单可明确选择内容、页面、部件或整站配置
传统配置发布 outbox 仍然可靠保存;其 worker 会转交发布单,旧任务的完成含义是“发布单已提交”。内容是否上线以发布单激活结果为准。
候选构建的增量与续跑
候选部署的构建计划(buildPlan)把工作拆成有依赖关系的阶段与逐路径目标,构建过程可中断、可续跑、可跨部署复用:
| 机制 | 说明 |
|---|---|
| 目标指纹 | 每个目标由「运行时摘要(渲染器与主题源码)+ 依赖表摘要 + 页面/内容依赖 + 时钟边界与素材授权窗口」共同决定;任一输入变化即失效 |
| 产物复用 | 目标指纹命中且基代产物 SHA-256 校验通过时,从基代目录复制字节而不是重新渲染;HTML 复用会重绑发布标识 |
| 断点续跑 | 失败目标记入 failedTargetKey,恢复时从该目标继续;同一候选部署的并发执行轮次由 advisory lock 拒绝 |
| 构建指标 | buildMetrics 记录复用 / 续跑 / 新生成产物数、耗时与尝试次数,发布中心据此展示进度 |
构建在暂存 schema(cms_generation_{id}_build)中进行:冻结快照中的站点、内容与资源以视图指向代次数据,而「影响 HTML 但必须保持实时」的运行时表(评论、广告、表单、互动问卷、素材授权、字典)复制进暂存 schema。因此候选渲染看到的是实时运行时数据,却不会污染线上代次或公开缓存。部署激活或回滚上线后,该部署与被替换部署的暂存 schema 随即删除;失败的构建保留暂存 schema,以便从断点续跑。代次内的目标索引 cms_build_targets(主键即确定性渲染目标)是派生索引,不是任务队列。
配置任务在提交事务中固定配置快照与基代,后台构建不重新读取之后的工作配置。自动配置任务生成待人工激活的发布单。手工发布单可以明确选择页面、部件或整站配置;内容的隐式发布单不会携带未选择的页面或部件工作稿。选定页面的部件引用、部件的来源引用和素材版本一并固定;候选依赖缺失会阻断构建。
发布单的列表、详情、预览和操作复用全部所选内容的数据范围与栏目权限。只管理单栏目的编辑者可以发布自己的修订;配置发布及整站回滚需要完整栏目范围。配置快照不会出现在通用任务详情、列表、WS 推送或任务内容搜索中。
发布单创建后内容发生下线、合规锁等公开状态变化会阻止旧发布单激活;普通草稿继续编辑不影响已经固定的审批事实。回滚保留运行时撤下和锁定状态,不改写工作稿正文或 CAS 版本;按新公开修订重新计算一致性,较新的工作稿显示为有未发布修改,待审和审核决定保持不变。每次真正的激活和回滚都会追加 cms_release_activations 记录。
候选预览通过受权的 GET /api/cms/releases/{id}/preview?path=/... 返回 HTML,读取固定候选代次并内联候选主题 CSS;不加载脚本,不写访问统计。它用于逐路径审阅,不是可交互的生产交互沙箱。
公开读取
SSR、Headless 和检索在请求开始时捕获 active generation,使用短 REPEATABLE READ 事务和受控 search_path 读取同一关系快照。此上下文不包裹流式响应或交互写入。访问和搜索统计显式离开只读上下文。
封印后的代次目录只读。代次目录内的主题 CSS 和岛脚本只在冻结候选构建期间落盘,并纳入完整产物清单;公开请求和候选预览不会补写历史目录。运行时需要新版指纹资产时直接返回相应内容,不改变已封印文件。激活和回滚继续严格检查完整清单,缺失、额外文件或校验和变化均返回 409,并要求重新构建;不能跳过校验强行切换公开指针。
静态文件读取同代目录;不生成动态栏目或按受众过滤页面的公共静态文件。动态页面仍可读取冻结的内容和布局定义。Headless 响应带 X-Cms-Generation,游标绑定代次;继续翻页时代次变化返回 409,调用方重新拉取,避免两次请求混用不同公开集合。
紧急撤下、身份删除/归档/下线、素材撤权和授权过期是运行时可见性门禁,历史部署不能撤销这些门禁。受影响时聚合静态页和 Redis 页面缓存被绕过,SSR 与 Headless 将相应内容视为下线;增量同步仍保留对应的 delete 语义。解除门禁触碰公开更新时间,让同步客户端重新收到 upsert。
实现上,代次封印时把内容与资源表改名为投影表并建视图:内容视图把「已删除 / 非已发布 / 被撤下 / 固定素材已撤权或过期」的行输出为 offline,updated_at 取身份表、撤下、授权变更、到期与代次激活时间的最大值,回收 / 归档 / 锁定与浏览量、点赞、收藏计数取自实时身份表;资源视图直接过滤撤权与过期素材。因此正文、布局与关联来自冻结快照,而运营态(计数、锁定、撤下、授权)始终随实时数据变化。
站点快照的 settings 采用公开字段白名单;Webhook/CDN 凭据、推送令牌和验证码密钥不会复制到历史关系表。验证码公开开关与 IndexNow 校验值由实时站点配置读取。审批、Webhook/CDN 投递以及会话仍使用实时配置。
交付和运行边界
发布中心的“实际交付”记录将激活、缓存刷新请求和页面观测分开。站点可配置源站入口、读者实际访问的公开入口及关键路径;未配置源站时使用系统公开基址下的 /__cms/{siteCode}。配置采用版本比较,检测记录固定配置版本及激活身份,配置或公开身份变化后旧任务不能覆盖新记录。未配置公开入口时保留为未验证,Demo 模式不模拟真实 HTTP 成功。
HTML 与响应头独立输出代次、发布单和可见性版本,不依赖访问统计开关。下线、紧急撤下、恢复及素材授权变化推进可见性版本;与当前版本不一致的静态产物和页面缓存被绕过,直至新代次构建捕获该版本。交付任务通过正常页面地址读取 HTML 标记,分别保留源站与公开入口证据;撤权素材按实际文件 URL 检查访问是否被拒绝。缓存刷新返回成功只代表请求已接受,不代表页面已经生效。证据默认保留 365 天,当前记录和未结束记录不参与回收。
内容事件在业务事务内保存固定事件 ID、发生时间和最小快照。删除事件不再依赖已删除的实体;outbox 重放沿用同一个事件 ID。CDN 激活刷新在同一事务登记任务,失败由任务中心重试。数据库激活是站点内原子操作,CDN 收敛仍是异步过程,不承诺跨站或全球同时生效。
当前关系存储采用全站关系复制,文件为独立代次目录。这是有明确成本的实现:大站构建会增加数据库空间、I/O 和备份体积,尚未实现结构共享或对象存储适配。部署数据库角色需要创建派生 schema 的权限;备份必须包含 cms_generation_*(含 *_build 暂存 schema),共享文件存储必须包含各 generation 目录。历史代次用于回滚,不应手工删除,由站点保留策略统一回收:默认保留最近 10 个上线过的版本或 30 天内的部署,失败部署保留 7 天;系统任务 cms-deployment-retention 每日 03:30 自动提交回收(默认开启,可在发布中心「部署容量」按站点调整或关闭),回收删除代次 schema、*_build 暂存 schema 与 generation 目录,保留发布审计记录。在线、已设为重要、仍可激活的候选(发布单仍指向它且基于当前在线版本)、作为待处理发布单构建基础或被执行中任务引用的代次始终受保护;基础代次已变化、发布单已取消或已改用新部署的候选无法再上线,按普通部署参与回收。正式物理文件 URL 的撤权仍受存储与 CDN 缓存能力约束。