Medusa 自定义工作流开发实战:从 createStep 到 createWorkflow 的完整指南 Medusa 自定义工作流开发实战从 createStep 到 createWorkflow 的完整指南【免费下载链接】medusaThe worlds most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa导读本文以 Medusa当前仓库GitHub_Trending/me/medusaloyalty 插件中的工作流目录 packages/plugins/loyalty/src/workflows 及其配套文档 Custom Workflows README 为核心系统讲解如何在 Medusa 中通过createStep、createWorkflow、StepResponse、WorkflowResponse构建和编排自定义工作流并介绍transform、useQueryGraphStep、runAsStep、hooks 等进阶组合手段。读完本文你将掌握在工作流文件中定义多步骤任务的完整写法、在 API Route / Scheduled Job / Subscriber 中执行工作流的调用方式以及如何基于源码理解 Medusa 工作流引擎的底层编排与补偿机制。一、工作流是什么在 Medusa 中工作流Workflow是一系列完成某项任务的查询queries与动作actions的组合。它把多个业务步骤组织成一个可执行、可回滚、可复用的单元是 Medusa 框架中处理复杂业务逻辑的标准抽象。工作流文件是普通的 TypeScript 或 JavaScript 文件约定存放于项目的src/workflows目录下例如 loyalty 插件中packages/plugins/loyalty/src/workflows/gift-cards/workflows/create-gift-cards.tspackages/plugins/loyalty/src/workflows/gift-cards/workflows/redeem-gift-card.tspackages/plugins/loyalty/src/workflows/store-credit/workflows/credit-accounts.ts插件通过 index.ts 统一导出各业务子目录carts、gift-cards、store-credit下的工作流外部模块即可按需引用。一个工作流由两类核心构件组成构件用途定义来源createStep定义一个最小执行单元Step可携带补偿函数packages/core/workflows-sdk/src/utils/composer/create-step.tscreateWorkflow将多个 Step 编排为一个可执行的工作流packages/core/workflows-sdk/src/utils/composer/create-workflow.tsStepResponseStep 的返回包装携带输出与补偿数据packages/core/workflows-sdk/src/utils/composer/helpers/step-response.tsWorkflowResponse工作流构造器函数的返回包装声明最终输出packages/core/workflows-sdk/src/utils/composer/helpers/workflow-response.ts二、创建第一个自定义工作流2.1 基础示例在原文档 Custom Workflows README 中给出了最简化的Hello World工作流我们完整继承并加以注解import { createStep, createWorkflow, WorkflowResponse, StepResponse, } from medusajs/framework/workflows-sdk const step1 createStep(step-1, async () { return new StepResponse(Hello from step one!) }) type WorkflowInput { name: string } const step2 createStep( step-2, async ({ name }: WorkflowInput) { return new StepResponse(Hello ${name} from step two!) } ) type WorkflowOutput { message1: string message2: string } const helloWorldWorkflow createWorkflow( hello-world, (input: WorkflowInput) { const greeting1 step1() const greeting2 step2(input) return new WorkflowResponse({ message1: greeting1, message2: greeting2 }) } ) export default helloWorldWorkflow要点拆解createStep(step-1, async () {...})第一个参数是步骤名称在日志、追踪与补偿编排中作为唯一标识第二个参数是调用函数invoke function返回StepResponse。createWorkflow(hello-world, (input) {...})第一个参数是工作流名称第二个参数是构造器函数composer function它接收工作流输入input在其中声明步骤的调用顺序最后必须返回WorkflowResponse。注意构造器函数内部不能直接操作或读取运行时数据只能声明性地组合步骤若需要对步骤输出做数据变换必须使用transform见下文。文件默认导出工作流对象供 API Route、Scheduled Job、Subscriber 引用。2.2 执行工作流工作流定义后并不会立即执行需要在其他资源API Route、定时任务、订阅者中显式触发。原文档给出了在 API Route 中执行的示例import type { MedusaRequest, MedusaResponse, } from medusajs/framework import myWorkflow from ../../../workflows/hello-world export async function GET( req: MedusaRequest, res: MedusaResponse ) { const { result } await myWorkflow(req.scope) .run({ input: { name: req.query.name as string, }, }) res.send(result) }关键点myWorkflow(req.scope)传入请求级容器req.scope获得一个可执行的工作流实例.run({ input })提交输入并运行返回{ result, errors }result即WorkflowResponse中声明的输出。同样的模式可用于Scheduled Job定时任务与Subscriber事件订阅者——只需把req.scope换成各自的容器引用。三、深入 Step调用函数、补偿函数与 StepResponse3.1 调用函数Invoke Function与补偿函数Compensate Function从源码 create-step.ts 可以看出createStep的完整签名是export function createStepTInvokeInput, TInvokeResultOutput, TInvokeResultCompensateInput( nameOrConfig: string | ({ name: string } OmitTransactionStepsDefinition, next | uuid | action), invokeFn: InvokeFnTInvokeInput, TInvokeResultOutput, TInvokeResultCompensateInput, compensateFn?: CompensateFnTInvokeResultCompensateInput ): StepFunctionTInvokeInput, TInvokeResultOutputinvokeFn步骤执行时调用的函数接收input与StepExecutionContext内含container等必须返回StepResponse。compensateFn可选当工作流后续步骤出错时执行的回滚函数用于撤销本步骤的副作用即分布式事务中的补偿机制。若未提供补偿函数源码中会通过stepConfig.noCompensation !compensateFn标记该步骤不可补偿。一个带补偿的实际例子来自 SDK 注释create-step.ts 顶部示例export const createProductStep createStep( createProductStep, async function (input: CreateProductInput, { container }) { const productModuleService container.resolve(product) const product await productModuleService.createProducts(input) return new StepResponse( { product }, { product_id: product.id } // 传给补偿函数的参数 ) }, async function (input, { container }) { if (!input) return const productModuleService container.resolve(product) await productModuleService.deleteProducts([input.product_id]) } )3.2 StepResponse输出与补偿数据StepResponse构造函数接受两个参数见 step-response.tsoutput步骤的输出供后续步骤或工作流最终结果使用compensateInput可选传给补偿函数的参数。若省略则补偿函数默认接收output本身。此外StepResponse提供两个静态工具方法StepResponse.permanentFailure(message?)抛出PermanentStepFailureError表示步骤已永久失败、不再触发重试适合业务上不可重试的错误场景StepResponse.skip()返回SkipStepResponse用于条件不满足时跳过该步骤常与条件逻辑配合。3.3 从忠诚度插件看真实 Steployalty 插件中的 redeem-gift-card.ts 定义了一个真实的校验步骤export const validateGiftCardRedeemStep createStep( validate-gift-card-redeem, async function ({ giftCardStoreCreditAccount, giftCard }) { if (giftCard.status GiftCardStatus.REDEEMED) { throw new MedusaError( MedusaError.Types.INVALID_DATA, Gift card is already redeemed ) } if (giftCardStoreCreditAccount) { throw new MedusaError( MedusaError.Types.INVALID_DATA, Gift card already has a store credit account ) } } )该 Step 仅做前置校验礼品卡已兑换、或已关联店铺信用账户时直接抛出MedusaError从而阻止后续步骤执行——体现了Step 即业务原子操作的设计理念。四、组合与变换transform、查询与并行4.1 transform在工作流内部变换数据由于工作流构造器函数是声明式的无法直接读写运行时数据Medusa 提供了transform函数来访问步骤的运行时输出并加工源码见 transform.tsconst storeCreditAccontCurrencies transform( { giftCards }, ({ giftCards }) { return giftCards.map((giftCard) ({ currency_code: giftCard.currency_code, })) } )transform的第一个参数传入一个对象其属性是先前步骤的输出引用第二个参数是变换函数接收运行时已解析的值并返回新数据返回值同样可作为后续步骤的输入。4.2 useQueryGraphStep在工作流中查询数据loyalty 插件大量使用useQueryGraphStep来自medusajs/medusa/core-flows在工作流内部发起查询例如 redeem-gift-card.tsconst giftCardQuery useQueryGraphStep({ entity: gift_card, filters: { id: input.gift_card_id }, fields: [id, code, status, value, currency_code], options: { throwIfKeyNotFound: true }, }).config({ name: get-gift-card-query }) const giftCard transform({ giftCardQuery }, ({ giftCardQuery }) { return giftCardQuery.data[0] })entity/filters/fields分别指定查询实体、过滤条件与返回字段options.throwIfKeyNotFound: true表示查不到记录时抛错.config({ name })允许为步骤指定别名查询结果通过transform提取为普通对象后供后续步骤消费。4.3 runAsStep在工作流中嵌套执行另一个工作流Medusa 允许一个工作流作为另一个工作流的步骤运行即runAsStep实现在 create-workflow.ts 的mainFlow.runAsStep。它本质上会创建一个名为${name}-as-step的包装 Step并把内部工作流的run包装进 invoke 函数、把cancel包装进补偿函数——这样嵌套工作流同样可以参与外层的事务与补偿。典型的组合示例create-gift-cards.tscreateLinksWorkflow.runAsStep({ input: linkToCreate }) creditAccountsWorkflow.runAsStep({ input: creditAccountsInput, }) const redeemedGiftCards updateGiftCardsWorkflow.runAsStep({ input: updateGiftCardsInput, }) return new WorkflowResponse(redeemedGiftCards)这段代码串起了创建礼品卡 → 创建匿名店铺信用账户并建立关联 → 按礼品卡面额入账 → 标记礼品卡为已兑换的完整业务流程每一步都复用已有工作流充分体现工作流的可组合性。4.4 并行执行与条件分支workflows-sdk还提供两个常用组合工具见 composer/index.ts 的导出parallelize(...steps)将多个步骤声明为并行执行when(input, condition)根据条件动态构造分支执行路径。此外每个 Step 返回的引用对象还暴露了.if(input, condition)方法用于给该步骤附加条件判断源码位于 create-step.ts 的refRet.if。合理使用这些 API 可显著提升复杂业务流程的表达能力。五、Hooks扩展既有核心工作流除自建工作流外Medusa 还允许开发者为既有工作流注册 Hook从而在核心流程的特定节点注入自定义逻辑。loyalty 插件在 hooks/after-order-created.ts 中演示了完整用法import { StepResponse } from medusajs/framework/workflows-sdk import { completeCartWorkflow } from medusajs/medusa/core-flows import { confirmCartCreditLinesWorkflow } from ../carts/workflows/confirm-cart-credit-lines import { cloneCartGiftCardsToOrderWorkflow } from ../orders/workflows/link-gift-cards-to-order ;(completeCartWorkflow.hooks as any).orderCreated( async (data: { order_id: string; cart_id: string }, stepContext) { const { container, ...sharedContext } stepContext const { order_id, cart_id } data const transaction await cloneCartGiftCardsToOrderWorkflow.run({ input: { order_id, cart_id }, container, context: { ...sharedContext, preventReleaseEvents: true }, }) return new StepResponse(transaction.result, stepContext.transactionId) }, async (transactionId, stepContext) { if (!transactionId) return const { container, ...sharedContext } stepContext await confirmCartCreditLinesWorkflow(container).cancel({ transactionId, container, context: { ...sharedContext }, }) } )解读通过completeCartWorkflow.hooks.orderCreated(...)挂载订单创建完成后的回调回调函数签名与 Step 一致第一参数为 Hook 负载数据第二参数为stepContext含container回调返回StepResponse并同样提供补偿函数——当订单创建流程回滚时自动取消购物车信用额度确认流程保证数据一致性这正是工作流 Hook 补偿三者协同的经典场景。六、底层原理工作流引擎如何编排理解底层实现有助于排查问题与设计复杂流程。从源码看编排核心位于 create-workflow.ts注册createWorkflow调用WorkflowManager.register(name, undefined, handlers, options)注册工作流若已存在同名工作流则走register更新分支。组合上下文构造CreateWorkflowComposerContext包含workflowId、flow、handlers等并将其挂在全局SymbolMedusaWorkflowComposerContext上随后执行构造器函数使每个createStep的调用都能拿到上下文见 create-step.ts 中global[SymbolMedusaWorkflowComposerContext]的判断与报错createStep must be used inside a createWorkflow definition。构建执行图每个 Step 通过this.flow.addAction(stepName, stepConfig)追加到事务定义中stepConfig.uuid ulid()为每个步骤生成唯一 IDstepConfig.noCompensation !compensateFn标记是否可补偿详见 create-step.ts 的applyStep。惰性执行构造器函数内的所有代码包括数据访问并不会立即执行而是在调用.run()时由工作流引擎统一调度这也解释了为何必须用transform而非直接取值。执行与补偿.run()按执行图依次执行步骤的 invoke 函数任一步骤失败时引擎逆序调用已完成步骤的补偿函数实现事务式回滚。理解了这一机制就能明白 loyalty 插件中redeemGiftCardWorkflow为何能在一个编排中同时完成查询礼品卡 → 校验 → 创建账户 → 入账 → 更新状态 → 返回账户详情而无需手写任何事务代码。七、在忠诚度业务中的实践总结作为实践落点梳理 loyalty 插件工作流目录中的真实资产可作为你自建工作流的参考模板礼品卡gift-cards/workflows/create-gift-cards.ts创建并自动建立店铺信用账户、redeem-gift-card.ts兑换并返回账户详情、claim-gift-card.ts、update-gift-cards.ts、delete-gift-card.ts店铺信用store-credit/workflows/credit-accounts.ts、debit-accounts.ts、create-store-credit-accounts.ts、credit-store-credit-account.ts、claim-store-credit-account.ts购物车carts/workflows/add-gift-card-to-cart.ts、confirm-cart-credit-lines.ts、refresh-cart-gift-cards.ts、remove-gift-cart-from-cart.ts订单orders/workflows/link-gift-cards-to-order.ts、refund-credit-lines.tsHookshooks/after-order-created.ts、hooks/after-order-credit-lines-created.ts、hooks/before-payment-collection-refresh.ts、hooks/complete-cart-before-payment-authorization.ts。编写工作流的推荐步骤在src/workflows/domain/steps/下用createStep定义原子操作含补偿函数在src/workflows/domain/workflows/下用createWorkflow编排这些 Step需要时用transform变换数据、用useQueryGraphStep查询、用runAsStep复用其他工作流用WorkflowResponse声明工作流输出在 API Route / Scheduled Job / Subscriber 中通过workflow(container).run({ input })触发执行若需扩展核心流程通过someWorkflow.hooks.someHook(callback, compensateFn)挂载 Hook。八、常见问题与注意事项createStep must be used inside a createWorkflow definitioncreateStep返回的函数只能在createWorkflow构造器函数内部调用脱离组合上下文会抛出该错误见 create-step.ts 中的校验。不要直接操纵数据构造器函数是声明式的读写运行时数据必须使用transform否则数据访问不会按预期时机执行。异步步骤可通过createStep({ name, async: true }, ...)声明异步步骤引擎会将其标记为后台执行避免阻塞整个事务相关逻辑见 create-step.ts 的wrapAsyncHandler。补偿函数缺失未提供补偿函数的步骤在出错时无法回滚设计时请为有副作用的步骤创建、扣款、状态变更等补齐补偿逻辑。StepResponse 的两个参数第二个参数用于精确控制补偿函数拿到的数据省略时补偿函数接收输出本身业务上请确保输出中包含了补偿所需的最小信息如刚创建的记录 ID。结语从createStep的最小原子单元到createWorkflow的声明式编排再到transform、useQueryGraphStep、runAsStep与 Hooks 的组合拳Medusa 的工作流体系让复杂业务可以被拆解、复用、组合并天然具备补偿回滚能力。loyalty 插件 src/workflows 目录下的完整实现以及 workflows-sdk 源码 中的底层机制是理解这一体系的最佳参考。掌握本文的方法你就能在自己的 Medusa 项目中构建出可靠、可维护的自定义工作流。【免费下载链接】medusaThe worlds most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考