Skip to content

内容管线

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

内容状态机

内容共 5 个状态:draft(草稿)→ pending(待审核)→ published(已发布)/ rejected(已驳回)/ offline(已下线)。

操作允许的源状态目标状态权限
提交审核draft / rejectedpendingcms:content:update
发布draft / pending / rejected / offlinepublishedcms:content:publish
驳回(带原因)pendingrejectedcms:content:audit
下线publishedofflinecms:content:publish
移入回收站任意offline + deletedAtcms:content:delete
恢复回收站draftcms:content:delete
彻底删除仅回收站—(硬删除)cms:content:delete

回收站内容超过 30 天由周期任务自动清理。

审核双轨制

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

  • 简单模式(默认):提交后进入「待审核」Tab,审核人直接发布/驳回。
  • 工作流模式settings.auditMode = 'workflow'。提交审核自动发起工作流实例(bizType=cms_content),流程定义取站点配置或按名称「CMS 内容审核」回退。流程审核期间禁止手动发布/驳回;流程通过 → 自动发布 + 静态化 + 推送(回调前会复验内容仍为待审状态,防止长周期流程覆盖人工操作);驳回 → rejected;撤回 → draft。

编辑体验

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

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

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

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

正文多页分页

图文正文在编辑器点击「插入分页符」(插入 [分页] 标记,兼容 <!-- pagebreak -->)即可拆分多页:

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

内容字段

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

标题样式

内容可单独设置标题加粗标题颜色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 入素材库(非图片类型原样入库);url 落库时归一为 cms-res://{id} 句柄(见「媒体库与素材句柄」)。

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

自定义静态路径

内容可指定 staticPath 覆盖默认的「栏目路径 + slug/id」命名,形如 news/2026/hello.html(支持 .html/.htm/.shtml/.json)。站内唯一(部分唯一索引 cms_contents_site_static_path_uq,忽略回收站内容);正文分页在扩展名前追加 _N。留空时行为不变。

封面缩略图

站点开启缩略图(站点设置 → 图片处理)后,上传封面/图集图片自动生成缩略图并记在素材行上。前台列表与图集九宫格优先使用缩略图,节省流量。

coverThumb 不是数据库列,而是读取时由封面素材派生:因此从媒体库选图同样有缩略图,且替换素材后缩略图自动跟随。封面填的是站外 URL(未登记素材)时无缩略图,回退原图。

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

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

自动保存

草稿/驳回状态的既有内容,编辑有改动时每 30s 静默自动保存一次,标题栏展示「已自动保存 HH:mm:ss」。新建内容(未落库)不自动保存,避免误创建。

草稿预览链接

编辑页「预览」按钮生成签名临时链接(HMAC-SHA256,默认 2 小时有效):

text
/__cms/{siteCode}/preview/{contentId}?exp={unix}&sig={hmac}

免登录访问,可直接分享给审核人;页面顶部注入预览提示条,不缓存、不回写静态文件。预览前有未保存改动会自动落库(保证预览即所见)。

版本快照 / 对比 / 回滚

  • 每次更新前自动留档版本快照(每内容保留最近 20 版)
  • 「历史版本」抽屉支持对比GET /{id}/versions/{versionId}/diff 返回字段级差异,前端双栏红绿高亮)与回滚(回滚前自动为当前状态留档)

媒体库与素材句柄

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

句柄化存储

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

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

带来的能力:

能力说明
替换素材素材列表「替换」上传新文件,保留素材 id,站内所有引用位置自动指向新文件
迁移不断链换存储、换 CDN、改文件名只改素材行,不产生死图
精确引用引用关系落在索引表上,不再靠 URL 子串匹配(a.jpg 不会再命中 a.jpg.bak
站点导入导出导出包含素材库,导入时重建素材并把包内句柄改写为新站 id,不会跨站引用来源站素材
跨站分发站群分发把来源素材登记到目标站后再改写句柄,两侧引用各自独立,来源站删除不影响目标站

引用索引与孤立治理

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

  • 覆盖 owner:站点、栏目、内容、内容版本快照、友情链接、广告、搭建页面、表单
  • 素材列表展示「引用数」列,0 即孤立素材
  • 孤立判定是一条 NOT EXISTS 索引查询;治理任务从原先「逐素材对 9 张表做全表 LIKE 扫描」的 O(N×M) 降为 O(N)
  • 存在引用的素材禁止删除
  • 素材行带 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)

操作日志时间线

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

内容组织

内容模型(自定义字段)

模型定义字段元数据(12 种类型:text/textarea/richtext/number/date/datetime/image/file/select/radio/checkbox/switch),值存入 extend JSONB。字段可配置必填、纳入检索(searchable)、列表显示。

模型可绑定到三级对象,均通过各自的 model_id + extend 列承载:

绑定级绑定位置值存放用途
站点cms_sites.model_idcms_sites.extend站点级运营元数据(备案号、客服电话、App 下载地址等),主题上下文通过 site.extend 读取
栏目cms_channels.model_id栏目下内容的 extend决定该栏目下内容编辑页动态渲染哪些扩展字段
内容cms_contents.model_idcms_contents.extend由所属栏目继承,换栏目时跟随目标栏目

选项来源select / radio / checkbox 三类字段的选项支持两种来源:

  • 手动设置(默认):在模型字段编辑区直接维护 {label, value}[]
  • 系统字典:只填字典编码(如 content_status),选项在读取模型时实时解析自 dict_items(仅取启用项,按 sort 排序)。字典维护一处,所有引用它的模型字段自动同步,无需逐个模型改选项

解析结果通过 resolvedOptions 返回给前端,内容编辑页与站点扩展字段区统一按它渲染;options 保留手动来源的原始值。字典编码不存在时 resolvedOptions 为空数组,不影响其他字段渲染。

一文多栏目

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

相关文章

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

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

  • scheduledAt:到期自动发布(每分钟检查,Redis 排他锁防多实例重复执行);创建内容时设置计划、修改计划或清除已有计划均要求 cms:content:publish
  • expireAt:到期自动下线(同一周期任务处理),适合活动/公告类时效内容
  • topExpireAt置顶到期自动取消(v1.8.0+),配合置顶权重 topWeight(数值越大越靠前)实现多条置顶精确排序

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

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

归档

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

批量操作与导入导出

  • 批量:移动栏目 / 追加标签 / 设置属性(置顶/推荐/热门)/ 归档、取消归档 / 站群分发 / 回收 / 恢复 / 彻底删除,全部事务保护
  • 单条复制:操作列「复制」在原栏目创建草稿副本;「复制到其他栏目」可选本站任意栏目(跨站请用站群分发)。两者均置空 slugstaticPath 规避唯一约束,标签一并复制,isTop 重置;换栏目时 modelId 跟随目标栏目,扩展字段按目标模型解释
  • 站群分发双模式(v1.8.0+):
    • 独立复制(默认):完整拷贝正文与扩展字段,分发后独立编辑,操作日志记录来源
    • 映射:仅拷贝标题等元数据(mappingSourceId 指向来源),正文/扩展字段运行时透传来源内容,源改动即时生效;映射内容禁止独立编辑正文(编辑页只读展示 + 服务端双重拦截);来源被彻底删除时自动物化为独立内容防失源;映射的映射自动指向原始来源,不产生解析链
  • 导入:Excel 批量导入(首行表头:标题(必填)/摘要/正文/作者/来源),走任务中心异步执行,行级明细 + 断点续跑 + 幂等提交。入口:内容列表「导入」(需先在栏目树选择目标栏目)
  • 导出:接入导出中心(entity cms.contents),按当前筛选条件导出 xlsx/csv

栏目运维

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

栏目管理页提供四项运维能力(v1.8.0+):

操作说明权限
移动编辑栏目改「父栏目」即移动,防环校验 + 子树 path 级联重算cms:channel:update
合并多个来源栏目内容(含副栏目绑定)并入目标栏目后删除来源;要求同站点列表栏目且无子栏目cms:channel:update
清空栏目下全部内容移入回收站(不含子栏目)cms:channel:update
批量新增按名称列表一次创建多个栏目,slug 自动取拼音(pinyin-pro),路径冲突自动加序号;单个新增时名称失焦也会自动填充拼音 slugcms:channel:create

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

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

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

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

引用方式:

  • 内部链接entity:channel@news(推荐);entity:channel/45 为历史 id 写法,仍然兼容。站点导入时前者无需重映射,后者按 id 映射表改写,映射缺失则置空
  • 页面搭建content-list 区块存 props.channelCode;旧页面存的 props.channelId 继续生效
  • 开放 APIGET /api/open/v1/cms/contents?siteCode=&channel=newschannelId=45 保留兼容

会员投稿

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

Built with VitePress for local documentation preview.