Skip to content

内容管线 ​

从创建到发布的完整内容生命周期。

内容状态机 ​

内容同时存在工作稿审核状态(editorialStatus)与线上投影状态(status),不能把保存、审核通过和上线视为同一步。已发布内容可以同时拥有正在编辑或审核中的新工作稿;编辑器分别展示这两个状态,并单独提示当前输入是否已保存。

操作工作稿 / 修订变化线上变化权限
保存更新工作稿并递增 CAS 版本保留当前公开投影cms:content:create / cms:content:update
提交审核冻结提交修订,工作稿标记 pending保留当前公开投影cms:content:update
批准待发布批准精确修订,供发布单选择保留当前公开投影cms:content:publish
申请发布批准修订并提交发布单发布单激活后更新投影为 publishedcms:content:publish
驳回(带原因)驳回当前提交修订;后续已编辑的新工作稿仍保留保留当前公开投影cms:content:audit
下线保留稿件与历史投影变为 offlinecms:content:publish
移入回收站记录 deletedAt停止公开cms:content:delete
恢复恢复为草稿仍需重新审核和发布cms:content:delete
彻底删除仅回收站内容可硬删除—cms:content:delete

回收站保留天数由站点内容策略 recycleKeepDays 控制,默认 30 天;设为 0 时永久保留,不自动清理。恢复只清除 deletedAt 并回到 draft,不会自动清除 archivedAt;归档内容恢复后要先取消归档才能提审或发布。

修订与工作稿 ​

编辑域与公开域分离:编辑器改的是工作稿,前台读的是公开投影,两者由发布流程衔接。

位置表语义
编辑域cms_content_working_copies每条内容唯一的工作稿(完整快照 + version CAS 版本 + 编辑状态)
编辑域cms_content_revisions不可变修订快照(checkpoint / submission / publication / restore / preview),读取时校验摘要
编辑域cms_content_review_revisions / cms_content_revision_approvals审批轮次固定的修订主体与批准事实(批准与修订摘要绑定)
公开域cms_contents 及标签/副栏目/相关文章关联表当前公开投影,按已批准修订在发布单激活时写入
  • 编辑状态(editorialStatus):draft 工作稿 / pending 审核中 / rejected 已驳回 / approved 已批准 / clean 与线上一致。发布后工作稿若与线上修订相同自动回到 clean,否则停留 draft,列表以「有未发布修改」标识
  • 修订内容:不只字段,还含标签、副栏目、相关文章、结构化正文、固化后的模型版本,以及按素材 id 固定的素材版本;冻结时同步占用模型唯一值、派生检索向量
  • 冻结强度:checkpoint(保存 / 自动保存留档)宽松,submission 与 publication 严格(执行发布必填、数值长度与唯一性校验)
  • 并发:写入一律带 expectedVersion,与工作稿版本不一致返回 409;批量操作逐条携带版本号
  • 发布:只接受已批准且摘要匹配的修订;发布事务写公开投影(status=published、publishedAt、清空 scheduledAt)并递增 cms_contents.version,工作稿保持不变
  • 紧急撤下(cms_content_suppressions,POST /api/cms/releases/content/{id}/suppress 与 /unsuppress):登记原因后作为运行时可见性门禁即时生效,详情页、聚合、Headless 与检索同时收敛,且历史部署无法撤销该门禁

审核双轨制 ​

站点「内容审核」设置支持两种模式:

  • 简单模式(默认):提交后进入「待审核」Tab,审核人直接发布/驳回。
  • 工作流模式:settings.auditMode = 'workflow'。提交审核自动发起工作流实例(bizType=cms_content),流程定义取站点配置或按名称「CMS 内容审核」回退。流程审核期间禁止手动发布/驳回;流程通过后批准该轮冻结修订并提交发布单,发布单激活后才改变线上内容。回调会复验提交修订仍属于当前审核轮次;驳回和撤回作用于工作稿审核状态,不覆盖审核后继续编辑的新稿。

工作流模式的内容编辑页保留完整富文本布局,页头分别显示工作稿保存状态、线上状态和工作稿审核状态,并展示审批流程名称、当前节点与「查看流程」入口。切换「内容 / 审批流程」等页签保留未保存的编辑内容。草稿/驳回内容默认展示本次提审预览,站点、栏目、标题变化后重新解析预览;预览不保存内容、不创建流程实例。主操作为「保存并提交审核」,提交成功后留在编辑页并切到实际审批流程。简单模式使用「保存并申请发布」,成功提示说明发布任务已提交,最终生效结果在发布中心查看;「保存并批准待发布」只批准修订,供组合发布单选取。

内容列表「查看审批」打开与普通流程一致的两栏侧边抽屉,左侧该轮冻结修订、右侧实际审批链,可查看流转记录和流程图。「审批轮次」支持选择过去记录;编辑页的当前预览与列表查看的最近实际轮次分别服务编辑和查看场景。过去轮次通过 approvalDetail 读取该轮绑定的不可变修订,正文与模型字段按当时快照展示。

业务编辑页使用内容权限读取流程上下文;工作流审批里的 ContentApprovalView 使用携带必填 instanceId 的 approvalDetail,服务端校验内容与实例关联及流程参与者可见性。定义不可用时在流程区显示错误,不能自动降级成简单审核。契约与公共容器接法见 业务模块接入工作流。

编辑体验 ​

内容形态(多形态内容类型) ​

每条内容具有形态(contentType,创建后不可变更):

形态说明发布前置校验
article 图文默认形态,富文本正文,支持正文多页分页—
album 图集编辑页多图管理(上传/媒体库/排序/图片说明),前台九宫格展示、点击看大图;正文降级为可选图文说明至少 1 张图片
media 音视频媒体地址(可上传)+ 海报 + 时长,前台原生 <video>/<audio> 播放器媒体地址必填
link 外链前台列表点击标题新窗口直达外链(rel="noopener nofollow"),不生成详情页外链地址必填

图集/音视频的结构化数据存 mediaData JSONB;图集说明纳入全文检索;列表页与后台列表均展示形态角标,后台支持按形态筛选。

正文存储模型 ​

正文以编辑器无关的结构化文档为主存储(bodyDocument:schemaVersion + 带稳定 id 的节点树,标签取自固定白名单),HTML 只作为导入 / 导出边界:

  • 写入时 HTML 先经 sanitizeCmsHtml 净化,再归一为文档树;未变动节点沿用原节点 id,因此保存、合并与三方差异能保持结构位置身份
  • 读取时由文档树序列化并再次净化,正式主题、审核预览与开放 API 读到同一份值
  • 富文本模型字段、单页正文与页面区块仍以 HTML 存储,同样在写入前净化

正文多页分页 ​

图文正文在编辑器点击「插入分页符」(写入 [分页] 标记;服务端同时识别 <!-- pagebreak -->)即可拆分多页:

  • 前台 URL:第 1 页 /{channelPath}/{idOrSlug}.html,第 n 页 /{channelPath}/{idOrSlug}_{n}.html(slug 不含下划线,无歧义)
  • 页内分页导航自动渲染;SEO 标题自动附「(第 n 页)」,canonical 指向当前分页
  • 静态化逐页生成全部分页,页数收缩时自动清理多余 _n.html(窗口 5 页);草稿预览始终展示第 1 页

内容字段 ​

除标题/摘要/正文外,支持副标题、短标题(列表窄位展示)、作者、责任编辑、来源、来源链接、原创标记。原创内容在列表以「原创」标识展示。

标题样式 ​

内容可单独设置标题加粗与标题颜色(cms_contents.title_style JSONB,{ bold?, color? }),颜色取自预置色板(CMS_TITLE_STYLE_COLORS)。后台内容列表与前台列表/详情页标题同步生效;两项都为空时回落主题默认外观。

正文附件 ​

正文之外可挂载结构化附件列表(cms_contents.attachments JSONB,单条含 name/url/size/ext/sort,上限 50 个)。编辑页「附件」区支持上传、改名、上下移动与删除,通用素材上传走 POST /api/cms/resources/upload?siteId={siteId}(图片快捷上传也可用 POST /api/cms/upload-image?siteId={siteId}),文件进入素材库后 url 归一为 cms-res://{id} 句柄(见「媒体库与素材句柄」)。

  • 附件非空时自动置位 hasAttachment,不再仅依赖正文链接正则推断
  • 前台详情页在正文下方渲染「附件下载」区(含类型徽标与体积)

自定义静态路径 ​

内容可指定 staticPath 覆盖默认的「栏目路径 + slug/id」命名,形如 news/2026/hello.html。规范只接受小写字母、数字、中划线、下划线、斜杠和 .html 扩展名,并拒绝 ..、连续斜杠及系统保留路径;路径站内唯一(部分唯一索引 cms_contents_site_static_path_uq,忽略回收站内容),正文分页在扩展名前追加 _N。留空时按栏目 detailPathRule 生成。

内容列表和详情接口同时返回服务端计算的 canonicalUrl 与 previewUrl:前者是站内规范地址(外链内容为外链地址),已发布的站内内容的 previewUrl 使用 /__cms/{siteCode} 前缀;外链型内容直接返回其外链地址。后台和主题不得自行拼接栏目路径、slug、id 或扩展名;草稿、待审和下线内容使用签名预览接口。

媒体处理(压缩 / 响应式图片 / 海报 / 字幕) ​

站点设置 →「图片处理」对素材中心上传的图片统一应用(存 cms_sites.settings):超宽等比压缩(imageMaxWidth,默认 1600px,0 = 不限制)、文字水印(九宫格定位 + 透明度 + 字号,默认关闭)、缩略图(thumbWidth 默认 400px,默认关闭)。素材列表另支持非破坏裁剪(POST /api/cms/resources/{id}/crop,另存为新素材,不影响原图引用)。

媒体处理(POST /api/cms/resources/{id}/media/process,任务中心任务 cms-media-processing)针对指定素材版本产出结构化结果:

能力说明
元信息宽高、时长、容器格式与音视频编码(图片经 sharp,音视频经 ffprobe,只处理本地临时文件)
响应式图片按 320 / 768 / 1440 三档宽度生成 WebP 变体,前台以 srcset 输出
焦点focalPoint(0–1 坐标)随变体一起冻结,前台按 object-position 应用
视频海报在指定秒数抽帧生成 WebP 封面
字幕关联本站 WebVTT 素材(校验 WEBVTT 头与时间轴、大小上限 5MB),前台输出 <track kind="subtitles">

处理结果按文件版本冻结进内容修订快照,因此候选部署与线上渲染一致:换文件只影响新修订,历史修订仍按当时的变体、焦点与海报渲染。未安装 ffmpeg / ffprobe 时音视频处理返回不可重试的明确错误。

开启缩略图后,上传封面/图集图片自动生成缩略图并记在素材行上,前台列表与图集九宫格优先使用缩略图。coverThumb 不是数据库列,而是读取时由封面素材派生:从媒体库选图同样有缩略图,替换素材后缩略图自动跟随;封面填站外 URL(未登记素材)时无变体,回退原图。

素材版本与授权 ​

  • 不可变文件版本:素材文件落在 cms_asset_versions;「替换」(POST /api/cms/resources/{id}/replace)创建新版本而不是覆盖旧文件,已发布修订与固定发布配置仍指向原文件
  • 删除保护:仍被内容修订(snapshot.assetVersions)或固定发布配置引用的素材版本不能删除(409),引用清单见 GET /api/cms/resources/{id}/references
  • 来源与版权(cms_asset_rights):来源、许可、到期时间、撤权标记、标签与替代文本 alt,经 GET/PUT /api/cms/resources/{id}/rights 维护
  • 撤权即时且 fail-closed:撤权或到期后素材立即停止交付(公开投影、静态交付与附件下载均复检),且历史部署不能恢复它;选定待发布修订或配置包含已撤权素材时,发布选择直接 409 拒绝。解除门禁会触碰公开更新时间,让 Headless 增量同步重新收到 upsert

写入安全边界 ​

  • 正文和单页正文在保存前经过 sanitizeCmsHtml;敏感词/易错词替换只作用于安全 HTML,再次净化后才入库。正式主题、审核预览和开放 API 读取同一份正文值;读取接口不修复已存脏值,导入/更新必须重新走净化管线。
  • externalLink、栏目链接、来源链接、重定向、友情链接、内链词、广告、主题参数、页面区块和页面部件的 URL 字段均在写入层执行共享 CMS URL policy;按字段用途接受 entity:、安全站内路径、http/https 或明确允许的 mailto/tel,素材字段另限站内绝对路径、http(s) 或 cms-res://。协议相对地址、反斜杠、点路径、控制字符及 javascript/data/vbscript/ftp 等协议会被拒绝。
  • 资源句柄必须是当前站点的 cms-res://{id}。写入或导入时发现其他站点句柄会失败;读取解析按 siteId 限定,未知句柄解析为空,不把跨站资源带回页面。
  • 主题参数和可视化区块的 URL 字段属于配置输入,仍应使用上述安全 URL 约定;区块 richtext 的 HTML 由服务端净化,JSON-LD/内联脚本动态值使用脚本安全 JSON 序列化。

并发保护(编辑锁 + 乐观锁) ​

  • 编辑锁(软锁):打开编辑页抢占 Redis 锁(cms:edit-lock:{id},TTL 120s,前端每 30s 心跳续期,离开自动释放)。他人持锁时页面顶部展示「xx 正在编辑」警示,但不阻断操作。
  • 乐观锁(硬保护):编辑、提审、发布申请、驳回等写入携带工作稿 expectedVersion,服务端版本不一致返回 409。编辑器保留本地修改并提供基线、本地与服务端工作稿的冲突对照,处理后再保存;公开投影版本在发布激活等公开状态变更时更新。软锁降低冲突概率,工作稿 CAS 防止静默覆盖。
  • 持久化合规锁:拥有 cms:content:lock 的管理员可填写原因锁定内容。锁记录 lockedAt/lockedBy/lockReason,会取消待执行 scheduledAt,并阻止编辑、审核/发布/下线、回收/删除、移动/分发、批量标记及工作流/周期任务回写;读取、预览、历史与操作日志仍可用。该锁必须显式解锁,不受 Redis TTL 影响。

自动保存 ​

有编辑权限且未被持久锁定的内容,编辑有改动时每 5s 静默自动保存工作稿;发生版本冲突或尚待处理本地恢复副本时暂停。新建内容选择栏目后同样会自动创建工作稿,标题暂未填写时以「未命名内容」保存。手动保存会执行表单校验,发布前仍须通过正式标题和发布必填检查。页头显示工作稿保存状态与最近自动保存时间;浏览器恢复副本用于找回尚未保存的修改。

草稿预览链接 ​

编辑页「工作稿预览」打开站内预览工作台;「生成分享预览」冻结当前工作稿并生成绑定修订与可撤销授权的签名临时链接(HMAC-SHA256,默认 2 小时有效):

text
/__cms/{siteCode}/preview/{contentId}?rid={revisionId}&gid={grantId}&exp={unix}&sig={hmac}

分享链接免登录访问,可直接发给审核人;页面顶部注入预览提示条,不缓存、不回写静态文件。预览前尝试保存未保存的改动,保存失败时明确提示展示最近一次保存的内容。分享预览固定到生成时修订,后续编辑不会改变链接内容;编辑页显示修订号、过期时间,并可撤销该链接。

修订历史 / 对比 / 回滚 ​

  • 「历史版本」抽屉列出内容的不可变修订(分页摘要,不含正文,显示标题、留档人与留档类型);修订是追加型里程碑,自动保存的 checkpoint 不会挤掉已批准或已发布的修订
  • 对比:GET /api/cms/contents/{id}/versions/{versionId}/diff 把该修订与当前工作稿逐字段比较,只返回变更字段,前端双栏红绿高亮
  • 回滚:POST /api/cms/contents/{id}/versions/{versionId}/restore 用该修订完整覆盖工作稿(连它固定的模型版本与素材版本一起恢复)并留档一条 restore 修订;回滚只改工作稿,仍需按当前审核模式提审 / 发布

媒体库与素材句柄 ​

封面图、图集、附件、模型 image/file 字段与页面区块统一引用素材中心(cms_resources)。从文件中心(managed_files)选取的文件在保存时自动登记为本站素材(复用同一 file_id,不复制物理文件),因此不存在「游离在素材中心之外、治理看不见」的媒资。组件:MediaPickerModal。

句柄化存储 ​

素材在正文 HTML、JSONB 与标量列中统一以 cms-res://{id} 句柄存储,而不是裸 URL:

  • 写入时归一:服务端把本站素材库中已登记的 URL 精确替换为句柄,未登记的外链地址原样保留。编辑器仍收发真实 URL,前端无感。归一化与引用索引重建在同一事务内完成(含文件中心引用的补登记),回滚不会留下孤儿素材行。
  • 读取时解析:实体映射(service mapXxx())、前台 SSR、静态化与 Headless API 输出前统一按站点把句柄还原为真实 URL(批量取数 + 进程内 60s 缓存,无 N+1)。
  • HTML 净化白名单:cms-res 已列入 sanitizeCmsHtml 的 scheme 白名单。站点导入、映射物化、分发同步等链路会把已落库的正文再次送进净化器,若不放行会整个丢掉 src/href 属性,等于永久删除正文中的全部图片。
  • 地址安全校验:素材地址在读取时被直接拼进已净化的 HTML(句柄替换发生在净化之后),因此 cms_resources.url / thumb_url 只允许站内绝对路径或 http(s) 绝对 URL,且不含引号、尖括号、反斜杠、空白,也不接受协议相对地址。后台、开放 API、会员/公开提交、站点导入和跨站分发等所有外来写入口都在入库或物化前强制校验。
  • 站点隔离:每个站点只引用自己的 cms_resources 行。跨站复制(站群分发、站点导入)会先把来源素材登记到目标站(按 URL 去重、复用同一 file_id 不复制物理文件),再改写句柄;普通写入遇到跨站句柄直接拒绝。

带来的能力:

能力说明
替换素材保留素材 id 并创建新文件版本;重新选取素材写入新工作稿时采用新版本,已冻结修订与发布配置继续使用原版本
迁移不断链换存储、换 CDN、改文件名只改素材行,不产生死图
精确引用引用关系落在索引表上,不再靠 URL 子串匹配(a.jpg 不会再命中 a.jpg.bak)
站点导入导出导出包含素材库,导入时重建素材并把包内句柄改写为新站 id,不会跨站引用来源站素材
跨站分发站群分发把来源素材登记到目标站后再改写句柄,两侧引用各自独立,来源站删除不影响目标站

引用索引与孤立治理 ​

cms_resource_refs(resource_id + owner_type + owner_id + field 唯一)记录每个素材被谁引用。owner 每次写入都在同一事务内整体重建自己的引用行,因此索引与业务数据强一致。

  • 覆盖 owner:站点、栏目、内容、内容版本快照、友情链接、广告、搭建页面、页面部件、表单
  • 素材列表展示「引用数」列,0 即孤立素材
  • 孤立判定是一条 NOT EXISTS 索引查询,治理任务复杂度 O(素材数),不依赖对业务表的全表 LIKE 扫描
  • 存在引用的素材禁止删除
  • 素材行带 owns_file 标记:false 表示文件由文件中心或来源站点持有(从文件中心选图自动登记、站点导入/站群分发复制出的素材都属此类),删除这类素材只删登记行、不动物理文件,避免把其他模块或其他站点正在用的文件删掉;true 时还要确认没有别的素材行共用同一 file_id 才会联动删除
  • 素材中心「重建引用索引」提交任务中心任务(cms-resource-ref-rebuild),逐 owner 类型分阶段执行,用于存量回填与索引修复;正常运行不需要

词库检查(敏感词 + 易错词) ​

编辑页「内容检查」按钮对标题/副标题/摘要/正文做单次 Aho-Corasick 扫描(POST /api/cms/contents/check-text),同时报告:

  • 敏感词命中:区分拦截词(须删除)与替换词(提交时自动替换),仅提示不拦截保存
  • 易错词命中:来自「易错词库」(/cms/error-prone-words,权限 cms:word:list|manage),支持单个/全部一键替换为正确写法,替换作用于标题/副标题/摘要/正文

站点还可开启保存时自动替换(站点设置 → 内容策略):

开关行为
autoReplaceSensitiveWords保存时按敏感词库替换标题/摘要/正文;命中拦截词(未配置替换文本)仍抛 400 拒绝保存
autoReplaceErrorProneWords保存时按易错词库把常见错词替换为正确写法

两者共用带 60s 缓存的 AC 自动机,关闭时不加载词库,无额外开销。

站点内容策略 ​

站点设置 →「内容策略」标签页统一维护以下开关,全部存 cms_sites.settings JSONB(缺项回落 CMS_SITE_OPS_DEFAULTS,无需数据迁移):

键默认作用
publishedContentEditabletrue关闭后编辑已发布内容直接返回 400,须先下线
recycleKeepDays30回收站保留天数,超期由每日周期任务彻底删除;0 = 永久保留
maxPageOnContentPublish0单条内容发布时最多重建所属栏目前 N 页列表;0 = 全部重建
autoReplaceSensitiveWordsfalse见上
autoReplaceErrorProneWordsfalse见上
autoCoverFromBodyfalse未填封面时,保存自动提取正文第一张图片作为封面(跳过 data: URI)
openApiPublishEnabledfalse允许开放 API 直接发布的站点级开关;仍需应用持有 cms:publish scope 且开放授权行开启 canPublish

操作日志时间线 ​

每条内容的关键操作(创建/更新/提审/发布/驳回/下线/回收/恢复/回滚/归档/移动等)自动落 cms_content_op_logs(随内容级联删除),编辑页「操作记录」抽屉按时间线展示操作人与详情。

编辑协作(批注、质量与转换) ​

内容编辑页的「协作与质量」面板把审稿、校对与多语言工作集中在一处。

审稿批注 ​

  • 批注可挂在字段或某个修订上(cms_editorial_notes),支持 @ 提及协作人,处理后可重新打开
  • 内容列表支持「仅有未处理批注」筛选,工作台提供「未解决批注」队列
  • 批注只是协作记录,不修改正文也不占用内容版本

质量检查与切换内容模型 ​

  • 质量检查:按已保存工作稿与模型规则输出问题,显示字段中文名称;「定位字段」切回内容页并打开对应属性页签或正文、媒体、附件与模型字段区。复合字段使用冻结模型中的子字段名称,正文图片问题定位到正文编辑区。见内容模型 → 质量检查
  • 切换内容模型(POST /api/cms/editorial/{id}/conversion-preview 与 /convert-type):指定目标模型与扩展字段映射,预检给出移除字段与质量清单,应用时必须携带 expectedVersion 并显式确认影响;结果只写工作稿。图文 / 图集 / 音视频 / 外链属于内容形态,创建后不可修改,该操作不在形态之间迁移。

多语言人工翻译稿 ​

  • 从源稿创建语言变体(POST /api/cms/editorial/{id}/translations,指定 locale、目标栏目与译文标题),变体是独立内容:拥有自己的工作稿、扩展字段与审核发布链路;同一源稿的同一语言只允许一条
  • 源稿变化时变体列表标记「源稿已更新」(按源修订对比),但不会自动跟随源稿改写
  • 前台经站点「多语言站点关联」输出 hreflang 与语言切换,与语言变体一一对应

编辑事项与读者反馈 ​

  • 编辑事项(cms_editorial_tasks):来源为手工事项、无结果搜索或表单来信,可指定负责人与期限并关联稿件;列表同时展示关联稿件的状态与「有未发布修改」
  • 读者反馈办理(cms_feedback_cases + cms_feedback_history):来信按状态流转(new / processing / resolved / closed),可配置默认负责人与办理审批流程,按版本 CAS 操作,办理历史不可变
  • 内容生产指标:GET /api/cms/editorial/metrics?siteId= 返回总量、进行中、待审、逾期、已排期、有未发布修改与未解决批注

内容组织 ​

内容模型(自定义字段) ​

内容模型提供 12 种类型的自定义字段(值存 extend JSONB),支持选项绑定系统字典、默认值自动填充、 「草稿宽松 / 发布严格」的必填校验分层、列表与详情展示配置,以及站群归属治理(平台共享 / 站点专属)。 完整说明见 内容模型与扩展字段。

绑定关系速览:站点(cms_sites.model_id + 自身 extend)、栏目(决定栏目下内容的扩展字段)、 内容(随主栏目继承,换栏目时按目标模型解释)。

标签 ​

站点级标签(/cms/tags):新建时输入名称自动生成拼音 slug(手改后不再覆盖),支持分组管理; 内容打标后前台输出 /tag/{slug}/ 聚合页,页面搭建的内容列表区块也可按标签跨栏目聚合取数。

一文多栏目 ​

内容除主栏目(channel_id)外可挂多个副栏目(cms_content_channels),栏目列表页自动聚合展示主栏目与副栏目内容。副栏目须为本站点列表型栏目。

相关文章 ​

编辑页可手动指定相关文章(cms_content_relations,按选择顺序排序);前台详情页「相关阅读」区块展示手动关联,不足 5 条时按共同标签自动补齐。

定时发布 / 过期下线 / 置顶到期 ​

  • scheduledAt:到期自动发布。计划时间保存在工作稿快照,每分钟的调度任务挑选「已批准修订的计划时间已到且尚未发布」的内容,以系统身份发布并写入实际发布时间(主表 publishedAt,同时清空计划时间),因此直接发布的内容不会残留计划;创建内容时设置计划、修改计划或清除已有计划均要求 cms:content:publish
  • expireAt:到期自动下线(同一周期任务处理),适合活动/公告类时效内容;请求时也会按 expireAt 立即判定不可见,不等任务执行
  • topExpireAt:置顶到期自动取消,配合置顶权重 topWeight(数值越大越靠前)实现多条置顶精确排序
  • 调度任务同时负责激活到点的发布单与排空转化投递 outbox,全程由 Redis 排他锁防止多实例重复执行

后台内容列表对两类计划显性可见:未发布且带计划的内容在状态列显示「定时」徽章、发布时间列显示计划时间(如「定时 08-16 09:00」);已发布且带过期时间的显示「限时」徽章,悬停提示到期时刻。

三者都会自动刷新静态页;发布/下线还会触发 Webhook。

内容日历 ​

内容管理页的「内容日历」Tab 是月视图:格子显示当日事件件数,悬浮列出当天明细并可点击进编辑页。数据来自按月聚合接口 GET /api/cms/contents/calendar?siteId=&month=(不翻页,counts 是精确值,明细每天最多 20 条)。

事件口径分两类,缺一不可:

  • 实际发布:cms_contents.published_at(主表列)。发布激活时会把 scheduledAt 清空,只留实际发布时间,因此直接发布的内容也一定会出现在日历里——只用快照里的三类日程字段查询是看不到它们的。
  • 待办日程:工作副本快照里的 scheduledAt(计划发布)/ dueAt(审稿截止)/ expireAt(过期下线)。

回收站与已归档内容不进入日历;栏目数据权限与系统数据范围照常生效。

内容成功发布时,发布事务除静态发布 outbox 外还会原子创建 cms-subscription-notify 任务。任务按发布时固化的订阅 cutoff 分批匹配站点、主栏目与标准化作者订阅,复用会员站内通知服务并按 content:{id}:version:{version} 去重;通知失败可在任务中心重试,不回滚也不阻塞内容发布。

归档 ​

已发布/已下线内容可归档(archivedAt):前台详情页保留可访问,但不再出现在栏目列表、首页区块、标签页、相关阅读、上下篇等聚合位。后台「归档」Tab 独立管理,可批量归档/取消归档;归档中的内容禁止状态流转。适合大量历史内容瘦身而不产生死链。

批量操作与导入导出 ​

  • 批量状态流转:勾选多条后可批量提交审核 / 发布 / 驳回(带原因)/ 下线(POST /api/cms/contents/batch-status)。逐条独立事务、按动作校验对应权限,部分成功——失败项返回行级原因(如状态不允许、被锁定),不整批回滚;需要构建的发布动作会返回生成/复用的发布单列表
  • 批量属性:移动栏目 / 追加标签 / 设置属性(置顶/推荐/热门/原创)/ 归档、取消归档 / 站群分发 / 回收 / 恢复 / 彻底删除,全部事务保护;批量写操作统一要求携带所选内容的 expectedVersions(CAS),返回逐条成功与失败原因。
  • 单条复制:操作列「复制」在原栏目创建草稿副本;「复制到其他栏目」可选本站任意栏目(跨站请用站群分发)。两者均置空 slug 与 staticPath 规避唯一约束,标签一并复制,isTop 重置;换栏目时 modelId 跟随目标栏目,扩展字段按目标模型解释
  • 站群分发双模式:
    • 独立复制(默认):完整拷贝正文与扩展字段,分发后独立编辑,操作日志记录来源
    • 映射:目标内容保存完整的目标站点本地快照,mappingSourceId 只保留来源关系供治理与同步;来源变更由带规则 revision、来源 version 和幂等键的异步任务同步,任务完成后目标才更新,期间继续提供上一份快照。映射内容禁止独立编辑正文(编辑页只读展示 + 服务端双重拦截);来源不再满足规则时,未锁定且已发布的目标会下线,其他状态的目标保持原状态,均物化最后快照并解除映射;已锁定目标则保留映射并记录冲突;删除规则时解除映射。
  • 导入:Excel 批量导入(首行表头:标题(必填)/摘要/正文/作者/来源),走任务中心异步执行,行级明细 + 断点续跑 + 幂等提交。入口:内容列表「导入」(需先在栏目树选择目标栏目)
  • 导出:接入导出中心(entity cms.contents),权限为 cms:content:export。导出复用站点、栏目 ACL、部门数据范围、状态、形态、归档/回收、属性、关键词和创建时间筛选,格式为 xlsx/csv。

栏目运维 ​

栏目管理页采用左侧栏目树 + 右侧编辑区的 Master-Detail 结构:左树点击节点即在右侧内联编辑(无需弹窗), 节点右侧「⋯」菜单提供访问前台 / 添加子栏目 / 授权用户 / 清空栏目 / 删除,节点上直接标注单页、外链、隐藏、停用状态。

栏目管理页提供四项运维能力:

操作说明权限
移动编辑栏目改「父栏目」即移动,防环校验 + 子树 path 级联重算cms:channel:update
合并多个来源栏目内容(含副栏目绑定)并入目标栏目后删除来源;要求同站点列表栏目且无子栏目cms:channel:update
清空栏目下全部内容移入回收站(不含子栏目)cms:channel:update
批量新增按名称列表一次创建多个栏目,slug 生成策略可选首字母缩写(默认,政务公开 → zwgk)或逐字全拼(→ zheng-wu-gong-kai),每行也可用 名称|slug 显式指定;路径冲突自动加序号。单个新增时名称失焦同样自动填充拼音 slugcms:channel:create

栏目标识(code)与 URL 标识(slug) ​

栏目有两个标识,职责不同,不要混用:

字段用途唯一性可变性
slugURL 片段,参与 path 级联(/news/notice/)同级唯一(经 path 全站唯一)随时可改,改了前台 URL 就变(需配 301)
code程序引用标识:内链、页面搭建区块、开放 API、主题模板站点内唯一一经设定不建议再改

code 的价值在于稳定:移动栏目、改 slug、站点复制/导入后它都不变,因此按 code 建立的引用不会失效。 新建栏目时留空会自动取 slug,站内冲突自动追加 -2 / -3;显式填写且撞车时直接报错,不会静默改写。

引用方式:

  • 内部链接:entity:channel@news(按 code 引用);entity:channel/45 与 entity:content/123 按站内 id 引用。站点导入时合法唯一的 code 引用保持不变,数值 id 按映射表改写;任一实体目标未随包提供、重复或无法安全改写时导入返回 400 并整体回滚。
  • 页面搭建:content-list 区块按 props.channelCode 取数;props.channelId 也可作为站内数值引用,但保存时应优先迁移为 code。
  • 开放 API:GET /api/open/v1/cms/contents?siteCode=&channel=news;开放写入只支持图文基础字段,详见开放能力。

会员投稿 ​

前台会员可通过投稿接口提交内容(memberId 标记来源),提交后直接进入审核流;被驳回可修改后重新提交。

Built with VitePress for local documentation preview.