首页
/ Strapi EE Review Workflows 技术设计详解:Workflow/Stage 数据模型、Admin API 路由与阶段权限实现

Strapi EE Review Workflows 技术设计详解:Workflow/Stage 数据模型、Admin API 路由与阶段权限实现

2026-09-05 19:56:51作者:尤峻淳Whitney

本篇基于 Strapi 仓库中的官方技术设计文档 01-review-workflows.md,结合当前仓库中 packages/core/review-workflows 包的真实源码,完整讲解 Enterprise Edition 专属的 Review Workflows(内容审核工作流)功能:包括它为什么与企业版代码解耦、两个核心内容类型(strapi_workflowsstrapi_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_workflowsstrapi_workflows_stages workflow/index.tsworkflow-stage/index.ts
Controllers(workflows / stages / assignees) workflows.tsstages.tsassignees.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.tsstages.tsassignees.tsstage-permissions.tsvalidation.tsmetrics/weekly-metrics.ts
Utils utils/review-workflows.ts
Bootstrap / Register bootstrap.tsregister.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::permissionmanyToMany 关系——这正是设计文档中描述的“每个阶段条目与 admin_permissions 持有多对多关系”,用来决定哪些角色可以把条目变更(transition)到该阶段。

模型 UID 为 plugin::review-workflows.workflow-stage,表名 strapi_workflows_stages

注入业务实体的 stage 与 assignee 属性

Review Workflows 之所以能“给实体打阶段标签”,靠的是在 register.tsextendReviewWorkflowContentTypes() 中,为每一个启用的可见内容类型动态扩展两个隐藏的关系属性:

// 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_stagestrapi_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 = 200MAX_STAGES_PER_WORKFLOW = 200:默认上限;
  • 错误信息常量 ERRORS:工作流至少需要一个阶段(WORKFLOW_WITHOUT_STAGES)、达到工作流数量上限(WORKFLOWS_LIMIT)、达到阶段数量上限(STAGES_LIMIT)、阶段名必须唯一(DUPLICATED_STAGE_NAME);
  • WORKFLOW_POPULATE:查询工作流时的标准 populate 结构,会带出 stages.permissions(含 actionactionParameters 及关联 role 的 idname)和 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/ 目录一一对应:

  1. review-workflows:在 Strapi 的 bootstrap / register 阶段使用,主要职责是按需迁移实体数据、并给实体 Schema 添加 stage 字段。现行代码中这部分能力由 register.tsmigrations/ 下的迁移脚本共同承担,包括 set-stages-default-color(阶段默认色)、set-stages-rolessetup-stage-transfer-to-roles(阶段转移权限迁移到角色体系)、set-workflow-default-namemultiple-workflows(支持多工作流)、handle-deleted-ct-in-workflows(清理工作流中已删除的内容类型)。
  2. workflowsworkflows.ts):操作 workflow 实体,提供创建、读取、更新能力,以及供中间件使用的 getAssignedWorkflow(uid)(查询内容类型绑定的工作流)与 count()(bootstrap 中判断是否初始化默认工作流)。
  3. stagesstages.ts):操作 stage 实体并更新其他实体上的阶段,提供 stage 的创建、读取、更新、删除能力。
  4. metricsmetrics/index.ts):遥测服务,采集功能使用情况,包括创建的工作流/阶段数量以及实体阶段更新的频率。
  5. weekly-metricsweekly-metrics.ts):每周上报一次统计。bootstrap.ts 中通过 getService('workflow-weekly-metrics').registerCron() 注册定时任务,统计内容包括:活跃工作流数量、每个工作流的平均阶段数、所有工作流中的最大阶段数,以及启用了 Review Workflows 的内容类型。
  6. assigneesassignees.ts):操作启用 Review Workflows 的内容类型上与 admin user 的 assignee 关系,提供查找实体处理人 ID、更新处理人、移除处理人(unassign)三种能力。
  7. stage-permissionsstage-permissions.ts):为阶段启用 RBAC。其实现细节见下一节。
  8. validationvalidation.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_UIDadmin::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

按执行顺序:

  1. 注册数据迁移:向 strapi::content-types.afterSync 钩子依次注册 persistRWOnDowngrade 与 6 个迁移函数(降级 join 表持久化 + 阶段默认色、阶段角色、阶段转移权限、工作流默认名、多工作流、已删除内容类型清理);
  2. 中间件:执行 reviewWorkflowsMiddlewares.contentTypeMiddleware(strapi),即文档中标注 DEPRECATEDreviewWorkflows 选项归位中间件;
  3. Schema 扩展extendReviewWorkflowContentTypes() 为启用的内容类型注入 strapi_stagestrapi_assignee 两个隐藏 oneToOne 关系;
  4. 许可限制:读取 strapi.ee.features.get('review-workflows'),以默认值(200 工作流 / 每工作流 200 阶段)合并后注册进 validation 服务。

bootstrap 阶段(bootstrap.ts

  1. 注册 RBAC actions:通过 admin 包 permission 服务的 actionProvider.registerMany(actions.reviewWorkflows),把 config/actions.ts 中定义的动作注册进权限体系(如第五节路由表中引用的 admin::review-workflows.create/read/update/delete);
  2. 注册 Webhook 事件registerWebhookEvents() 遍历 constants/webhook-events.ts 中的事件,逐一调用 strapi.get('webhookStore').addAllowedEvent(...)。源码注释明确说明:Webhook store 会限制可触发的事件集合,此函数用于把 Review Workflows 能触发的事件扩展进去;
  3. 注册每周指标 cronregisterCron()
  4. 初始化默认工作流initDefaultWorkflow() 先统计数据库中的 workflow 与 stage 数量,若两者均为 0,则用 constants/default-workflow.jsonconstants/default-stages.json 创建初始工作流(contentTypes: [])——这解释了为什么全新安装的 EE 项目打开设置页时总是已有一个默认工作流;
  5. 挂载文档服务中间件
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/cloneupdatepublish 动作:

  1. assignStageOnCreate:实体创建(或克隆)时,查询该内容类型绑定的工作流(getAssignedWorkflow),若请求数据中没有显式指定 strapi_stage,则自动赋值为该工作流的第一个阶段workflow.stages[0])。这保证新建草稿一出生就处于工作流的起点;
  2. 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 能感知“阶段变更”的底层来源
  3. 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 包中逐一定位验证。

登录后查看全文
热门项目推荐
相关项目推荐