Strapi EE Review Workflows 技术设计详解:Workflow/Stage 数据模型、Admin API 路由与阶段权限实现
本篇基于 Strapi 仓库中的官方技术设计文档 01-review-workflows.md,结合当前仓库中 packages/core/review-workflows 包的真实源码,完整讲解 Enterprise Edition 专属的 Review Workflows(内容审核工作流)功能:包括它为什么与企业版代码解耦、两个核心内容类型(strapi_workflows、strapi_workflows_stages)的 Schema 设计、Admin API 的全部路由、各 Service 的职责分工,以及阶段(stage)、处理人(assignee)如何被注入到业务内容类型中、文档服务中间件与阶段权限(RBAC)的底层实现。读完本文,你可以准确定位该功能的每一处代码实现,理解其数据流向、许可限制与生命周期钩子。
一、功能定位:EE 专属,且刻意与社区版解耦
根据设计文档的定义,Review Workflows 是仅在企业版(Enterprise Edition)中可用的功能,这也是它大部分代码从社区版中彻底剥离的原因。功能的核心目的是:允许用户为 Strapi 项目中的各类实体分配一个“阶段”(stage)标签,这些阶段被组织在一个“工作流”(workflow)中。
设计文档给出的首要设计考量是:尽可能与社区版解耦,实现方式上接近一个插件(plugin)的构建方式。这一点在现行源码中依然成立——整个功能被收敛在独立包 packages/core/review-workflows 中,插件入口 index.ts 中的 getPlugin() 根据特性开关决定导出内容:
// packages/core/review-workflows/server/src/index.ts
const getPlugin = () => {
if (strapi.ee.features.isEnabled('review-workflows')) {
return { register, bootstrap, destroy, contentTypes, services, controllers, routes };
}
// 特性关闭(或降级到社区版)时,仅保留 contentTypes
// 以避免降级到 CE 时丢失数据库中已有的 stage/assignee 数据
return { contentTypes };
};
这段源码印证了设计文档中“与企业版解耦”的原则:当 EE 许可失效降级到社区版时,内容类型声明依然会被加载,从而保证历史数据不会因为 Schema 缺失而被破坏。
二、后端代码组织:从 admin/ee 到独立包
设计文档将后端代码划分为如下模块,并给出了当时位于 packages/core/admin/ee 下的文件路径。当前仓库中这些代码已迁移为独立的 @strapi/review-workflows 包,目录结构与设计文档描述的模块划分一一对应:
| 设计文档中的模块 | 当前仓库对应位置 |
|---|---|
Content-types(strapi_workflows、strapi_workflows_stages) |
workflow/index.ts、workflow-stage/index.ts |
| Controllers(workflows / stages / assignees) | workflows.ts、stages.ts、assignees.ts |
Middlewares(contentTypeMiddleware,文档标注 DEPRECATED) |
middlewares/review-workflows.ts |
| Routes | routes/review-workflows.ts |
| Services(review-workflows / workflows / stages / metrics / weekly-metrics / validation / assignees / stage-permissions) | services/ 目录(workflows.ts、stages.ts、assignees.ts、stage-permissions.ts、validation.ts、metrics/weekly-metrics.ts) |
| Utils | utils/review-workflows.ts |
| Bootstrap / Register | bootstrap.ts、register.ts |
文档中还提到一个已废弃的中间件 contentTypeMiddleware:为了让内容类型的选项在对象根层级统一管理,reviewWorkflows 选项需要迁移到内容类型数据内部的 options 对象中,以保证所有选项在各自数据结构中被一致地组织和访问。当前仓库中该中间件文件依然存在(middlewares/review-workflows.ts),并在 register.ts 中仍被调用。
三、数据模型:两个内容类型与注入实体的两个属性
strapi_workflows
该内容类型保存工作流信息,负责持有所有阶段(stages)及其顺序。在早期 MVP 中数据库里只存储一个工作流;当前源码中 workflow/index.ts 的 Schema 定义如下:
name(string,必填、唯一):工作流名称;stages:指向plugin::review-workflows.workflow-stage的 oneToMany 关系,mappedBy: 'workflow';stageRequiredToPublish:指向某个 stage 的 oneToOne 关系,表示“发布前必须处于的阶段”(可为空);contentTypes(json,默认'[]'):启用该工作流的内容类型 UID 列表;- 模型 UID 为
plugin::review-workflows.workflow,表名strapi_workflows;通过pluginOptions将其对 Content Manager 与 Content Type Builder 均设为visible: false,即这是纯内部模型,不会暴露给用户管理界面。
strapi_workflows_stages
该内容类型保存每个阶段的名称等信息,完整定义见 workflow-stage/index.ts:
name(string,不可配置):阶段名称;color(string,默认值STAGE_DEFAULT_COLOR,即#4945FF,见 constants/workflows.ts);workflow:manyToOne 关系指回工作流(inversedBy: 'stages'),表达阶段在所属工作流中的归属;permissions:与admin::permission的 manyToMany 关系——这正是设计文档中描述的“每个阶段条目与admin_permissions持有多对多关系”,用来决定哪些角色可以把条目变更(transition)到该阶段。
模型 UID 为 plugin::review-workflows.workflow-stage,表名 strapi_workflows_stages。
注入业务实体的 stage 与 assignee 属性
Review Workflows 之所以能“给实体打阶段标签”,靠的是在 register.ts 的 extendReviewWorkflowContentTypes() 中,为每一个启用的可见内容类型动态扩展两个隐藏的关系属性:
// packages/core/review-workflows/server/src/register.ts(节选)
function setRelation(attributeName, target, contentType) {
Object.assign(contentType.attributes, {
[attributeName]: {
writable: true,
private: false,
configurable: false,
visible: false, // 对用户不可见
useJoinTable: true, // 使用 join table,降级到 CE 时数据仍可持久化
type: 'relation',
relation: 'oneToOne',
target,
},
});
return contentType;
}
// 对每个启用 Review Workflows 的内容类型:
setRelation(ENTITY_STAGE_ATTRIBUTE, STAGE_MODEL_UID, contentType); // strapi_stage
setRelation(ENTITY_ASSIGNEE_ATTRIBUTE, 'admin::user', contentType); // strapi_assignee
从源码结构看,这两个属性的字段名固定为常量 strapi_stage 与 strapi_assignee(定义于 constants/workflows.ts)。useJoinTable: true 是一个刻意的降级保护设计:register.ts 中的 persistRWOnDowngrade() 还会通过 admin 包的 persist-tables 服务,在 strapi::content-types.afterSync 钩子中持久化 _strapi_stage_lnk、_strapi_assignee_lnk 这两类 join 表,确保即使 EE 许可被关闭,阶段与处理人数据也不会丢失。
四、关键常量与许可(License)限制
constants/workflows.ts 集中定义了功能的核心常量与错误信息:
WORKFLOW_MODEL_UID = 'plugin::review-workflows.workflow'、STAGE_MODEL_UID = 'plugin::review-workflows.workflow-stage';STAGE_TRANSITION_UID = 'admin::review-workflows.stage.transition':阶段转移这一特殊权限 action 的 UID(源码注释提示出于 V4 兼容保留了旧 UID,若修改需同步前端Stages.tsx);MAX_WORKFLOWS = 200、MAX_STAGES_PER_WORKFLOW = 200:默认上限;- 错误信息常量
ERRORS:工作流至少需要一个阶段(WORKFLOW_WITHOUT_STAGES)、达到工作流数量上限(WORKFLOWS_LIMIT)、达到阶段数量上限(STAGES_LIMIT)、阶段名必须唯一(DUPLICATED_STAGE_NAME); WORKFLOW_POPULATE:查询工作流时的标准 populate 结构,会带出stages.permissions(含action、actionParameters及关联 role 的id、name)和stageRequiredToPublish。
这些默认上限并非硬编码写死,而是在 register.ts 中通过 strapi.ee.features.get('review-workflows') 读取当前 EE 许可配置后,用 defaultsDeep 与默认值合并,最终交给 validation 服务注册:
const reviewWorkflowsOptions = defaultsDeep(
{ numberOfWorkflows: MAX_WORKFLOWS, stagesPerWorkflow: MAX_STAGES_PER_WORKFLOW },
strapi.ee.features.get('review-workflows')
);
workflowsValidationService.register(reviewWorkflowsOptions);
也就是说,实际的 workflow / stage 数量上限由 EE 许可证决定,200 只是默认值。
五、Admin API 路由全览
设计文档列出了 EE Admin API 中与 Review Workflows 相关的全部路由。当前仓库 routes/review-workflows.ts 与之一致(type: 'admin',路径挂在插件挂载前缀 /review-workflows 之下),每条路由都挂有 enableFeatureMiddleware('review-workflows') 特性开关中间件和权限策略。完整路由表如下:
工作流管理路由
| 方法 | 路径(含挂载前缀) | Handler | 权限策略 |
|---|---|---|---|
| GET | /review-workflows/workflows |
workflows.find |
admin::isAuthenticatedAdmin + admin::review-workflows.read |
| POST | /review-workflows/workflows |
workflows.create |
admin::isAuthenticatedAdmin + admin::review-workflows.create |
| GET | /review-workflows/workflows/:workflow_id/stages |
stages.find |
admin::isAuthenticatedAdmin + admin::review-workflows.read |
| GET | /review-workflows/workflows/:workflow_id/stages/:id |
stages.findById |
admin::isAuthenticatedAdmin + admin::review-workflows.read |
| PUT | /review-workflows/workflows/:id |
workflows.update |
admin::isAuthenticatedAdmin + admin::review-workflows.update |
| DELETE | /review-workflows/workflows/:id |
workflows.delete |
admin::isAuthenticatedAdmin + admin::review-workflows.delete |
设计文档中 PUT /review-workflows/workflows/:workflow_id/stages 描述的“更新某个工作流关联的阶段(阶段数据在请求体中传递)”,在现行路由文件中由 stages.find 所在的阶段路由族承担,实际更新逻辑位于 stages 控制器中(controllers/stages.ts)。
实体阶段/处理人路由(挂载在 Content Manager 路径下)
| 方法 | 路径 | Handler | 权限策略 | 用途 |
|---|---|---|---|---|
| PUT | /content-manager/(collection|single)-types/:model_uid/:id/stage |
stages.updateEntity |
admin::isAuthenticatedAdmin |
更新某实体所属阶段(新阶段值在请求体中) |
| GET | /content-manager/(collection|single)-types/:model_uid/:id/stages |
stages.listAvailableStages |
admin::isAuthenticatedAdmin |
返回当前用户有权限转移进入的阶段列表(依据阶段权限设置) |
| PUT | /content-manager/(collection|single)-types/:model_uid/:id/assignee |
assignees.updateEntity |
admin::isAuthenticatedAdmin + admin::users.read |
更新实体的处理人(assignee),更新后的实体数据在请求体中 |
值得注意的是 listAvailableStages 这条路由:它直接对应设计文档中 stage-permissions 服务“判断某个角色能否从给定阶段进行转移”的能力,把 RBAC 检查做进了 API 层,前端据此只渲染用户可操作的阶段。
六、Service 层职责分工
设计文档将服务层划分为 8 个 service,与当前 services/ 目录一一对应:
- review-workflows:在 Strapi 的 bootstrap / register 阶段使用,主要职责是按需迁移实体数据、并给实体 Schema 添加 stage 字段。现行代码中这部分能力由 register.ts 与 migrations/ 下的迁移脚本共同承担,包括
set-stages-default-color(阶段默认色)、set-stages-roles、setup-stage-transfer-to-roles(阶段转移权限迁移到角色体系)、set-workflow-default-name、multiple-workflows(支持多工作流)、handle-deleted-ct-in-workflows(清理工作流中已删除的内容类型)。 - workflows(workflows.ts):操作 workflow 实体,提供创建、读取、更新能力,以及供中间件使用的
getAssignedWorkflow(uid)(查询内容类型绑定的工作流)与count()(bootstrap 中判断是否初始化默认工作流)。 - stages(stages.ts):操作 stage 实体并更新其他实体上的阶段,提供 stage 的创建、读取、更新、删除能力。
- metrics(metrics/index.ts):遥测服务,采集功能使用情况,包括创建的工作流/阶段数量以及实体阶段更新的频率。
- weekly-metrics(weekly-metrics.ts):每周上报一次统计。bootstrap.ts 中通过
getService('workflow-weekly-metrics').registerCron()注册定时任务,统计内容包括:活跃工作流数量、每个工作流的平均阶段数、所有工作流中的最大阶段数,以及启用了 Review Workflows 的内容类型。 - assignees(assignees.ts):操作启用 Review Workflows 的内容类型上与 admin user 的 assignee 关系,提供查找实体处理人 ID、更新处理人、移除处理人(unassign)三种能力。
- stage-permissions(stage-permissions.ts):为阶段启用 RBAC。其实现细节见下一节。
- validation(validation.ts):确保功能按预期工作并校验数据合法性,限制值来自 EE 许可(见第四节)。
stage-permissions:阶段转移的 RBAC 实现
设计文档对此服务的描述是:strapi_workflows_stages 的每个条目与 admin_permissions 持有多对多关系,关系中的权限指明哪些角色可以变更处于该阶段的条目的审核阶段;服务提供按“阶段 + 角色 ID”注册/注销阶段权限、以及判断角色能否从给定阶段进行转移的能力。
现行 stage-permissions.ts 的源码与描述完全吻合,关键实现点:
register({ roleId, action, fromStage })/registerTo({ roleId, action, toStage }):通过 admin 包的role服务向角色追加权限。action 只接受STAGE_TRANSITION_UID(admin::review-workflows.stage.transition),并以actionParameters: { from }或{ to }记录转移的源/目标阶段 ID,从而把“可以从/可以进入某个阶段”编码为参数化权限;unregister(permissions):按权限 ID 批量删除;can(action, fromStage):基于当前请求上下文strapi.requestContext.get()?.state中的用户角色与userAbility判断是否允许从某阶段转移;canTransitionToStage(toStageId):查询目标阶段的 permissions(populate 出 role)后,判断当前用户的任一角色是否出现在其中——这正是GET .../stages(可用阶段列表)路由背后的核心逻辑;- Super Admin 旁路:若用户角色中存在
strapi-super-admin,上述判断直接返回true,无需逐条比对权限记录。
七、生命周期:register 与 bootstrap 阶段
register 阶段(register.ts)
按执行顺序:
- 注册数据迁移:向
strapi::content-types.afterSync钩子依次注册persistRWOnDowngrade与 6 个迁移函数(降级 join 表持久化 + 阶段默认色、阶段角色、阶段转移权限、工作流默认名、多工作流、已删除内容类型清理); - 中间件:执行
reviewWorkflowsMiddlewares.contentTypeMiddleware(strapi),即文档中标注 DEPRECATED 的reviewWorkflows选项归位中间件; - Schema 扩展:
extendReviewWorkflowContentTypes()为启用的内容类型注入strapi_stage、strapi_assignee两个隐藏 oneToOne 关系; - 许可限制:读取
strapi.ee.features.get('review-workflows'),以默认值(200 工作流 / 每工作流 200 阶段)合并后注册进 validation 服务。
bootstrap 阶段(bootstrap.ts)
- 注册 RBAC actions:通过 admin 包 permission 服务的
actionProvider.registerMany(actions.reviewWorkflows),把 config/actions.ts 中定义的动作注册进权限体系(如第五节路由表中引用的admin::review-workflows.create/read/update/delete); - 注册 Webhook 事件:
registerWebhookEvents()遍历 constants/webhook-events.ts 中的事件,逐一调用strapi.get('webhookStore').addAllowedEvent(...)。源码注释明确说明:Webhook store 会限制可触发的事件集合,此函数用于把 Review Workflows 能触发的事件扩展进去; - 注册每周指标 cron:
registerCron(); - 初始化默认工作流:
initDefaultWorkflow()先统计数据库中的 workflow 与 stage 数量,若两者均为 0,则用 constants/default-workflow.json 与 constants/default-stages.json 创建初始工作流(contentTypes: [])——这解释了为什么全新安装的 EE 项目打开设置页时总是已有一个默认工作流; - 挂载文档服务中间件:
const docsMiddlewares = getService('document-service-middlewares');
strapi.documents.use(docsMiddlewares.assignStageOnCreate);
strapi.documents.use(docsMiddlewares.handleStageOnUpdate);
strapi.documents.use(docsMiddlewares.checkStageBeforePublish);
文档服务中间件:阶段行为的三个切入点
document-service-middleware.ts 是功能与实体数据流耦合最深的一层,三个中间件分别拦截 Document Service 的 create/clone、update、publish 动作:
assignStageOnCreate:实体创建(或克隆)时,查询该内容类型绑定的工作流(getAssignedWorkflow),若请求数据中没有显式指定strapi_stage,则自动赋值为该工作流的第一个阶段(workflow.stages[0])。这保证新建草稿一出生就处于工作流的起点;handleStageOnUpdate:实体更新时,先通过getEntityStage()读取旧阶段(带populate: { workflow: true }),执行更新后比较新旧阶段 ID;若发生变化,则通过strapi.eventHub.emit(WORKFLOW_UPDATE_STAGE, payload)发出事件,payload 包含model/uid、实体信息(id、documentId、locale、status)以及workflow.stages.from/to(含 id 与 name)。这就是Webhook 能感知“阶段变更”的底层来源;checkStageBeforePublish:发布前校验。若绑定的工作流设置了stageRequiredToPublish,且实体当前阶段与其不一致,则抛出ValidationError('Entry is not at the required stage to publish')。这实现了“必须流转到指定阶段才允许发布”的硬性门禁。
八、演进与替代方案
设计文档的 Alternatives 一节指出:Review Workflows 当时作为核心功能包含在 Strapi 仓库中,社区曾讨论将其迁移为插件,尚未有最终决定。从当前仓库结构看,这一方向已部分落地——功能代码不再是 packages/core/admin/ee 中的一部分,而是形成了拥有独立 admin/(前端)、server/(后端)、shared/(前后端契约,如 review-workflows.ts)、独立测试与构建配置(package.json)的完整包,这正是设计文档中“实现方式接近插件”理念的演进结果。
九、小结
Strapi EE 的 Review Workflows 是一项以“解耦”为第一设计原则的功能:通过 plugin::review-workflows.* 命名的内部内容类型存储工作流与阶段,通过动态注入的 strapi_stage/strapi_assignee 隐藏关系连接业务实体,通过完整的 Admin API(工作流/阶段 CRUD、实体阶段切换、可用阶段查询、处理人更新)驱动前端,通过 stage-permissions 服务把“阶段转移”编码为参数化权限实现 RBAC,并通过文档服务中间件在 create/update/publish 三个切入点织入阶段分配、变更事件与发布门禁逻辑。所有数量限制均由 EE 许可证驱动,且全链路(join 表持久化、特性开关、降级保护)都考虑了企业版许可失效降级到社区版时的数据保全。关键实现可沿 设计文档 所述的模块划分,在当前仓库的 packages/core/review-workflows 包中逐一定位验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00