mcpField
M
medusa-development

medusa-development

github.com/mindrally开发29775 次安装4 周前更新
代码仓库

关于 medusa-development

Medusa Development 是面向使用 Medusa 无头电商框架构建功能的开发者技能。它围绕数据模型、模块服务和由步骤组成的工作流提供指引,并涵盖 API、定时任务及订阅器的调用方式。

主要功能

  1. 01数据模型与模块服务指引
  2. 02可组合的工作流步骤
  3. 03通过路由和任务调用工作流
  4. 04使用 Query 读取数据
  5. 05管理端定制规范

适用场景

  1. 01添加电商模块和数据模型
  2. 02将业务逻辑实现为工作流
  3. 03通过路由或定时任务调用工作流

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mindrally/skills --skill medusa-development

安装后在 Claude Code 中输入/medusa-development确认已加载。

详细介绍

Medusa 开发

Medusa 是一个以模块、数据模型和工作流为核心构建的无头电商框架;几乎所有业务逻辑——从 API 路由到定时任务——都应表示为由离散、可组合步骤组成的工作流。

构建 Medusa 功能的工作流

  1. 定义或扩展数据模型 — 使用 @medusajs/framework/utils 中的 model 工具,在 src/modules/<module>/models/ 下声明模块的数据模型。
  2. 编写模块服务 — 在 src/modules/<module>/service.ts 中创建服务;如果模块有数据模型,则扩展 MedusaService,并提供用于领域操作的异步方法。
  3. 注册模块 — 将模块添加到 medusa-config.ts,以便 Medusa 的容器能够解析该模块。
  4. 构建步骤 — 使用 @medusajs/framework/workflows-sdk 中的 createStep,将每个工作单元定义为一个步骤;对于失败时需要撤销的操作,还要包含补偿函数。
  5. 组合工作流 — 使用 createWorkflow 将各步骤串联起来,并使用 transform 整理数据、使用 when 创建条件分支。
  6. 公开工作流 — 从 API 路由、定时任务或订阅器中调用工作流——切勿将业务逻辑直接写在路由、任务或订阅器处理函数中。
  7. 使用 Query 读取数据 — 使用 Medusa 的 Query(req.scope.resolve("query") 或工作流级别的 useQueryGraphStep)获取数据,不要直接调用模块服务进行读取。

通用规则

  • 导入文件时不要使用类型别名——直接从源模块导入类型和值,不要通过本地别名重新导出。
  • 抛出错误时,始终抛出 MedusaError(来自 @medusajs/framework/utils),而不是普通的 Error,这样 API 层才能将其映射为正确的 HTTP 状态和错误代码。
  • 始终使用 Query 获取数据,不要直接调用模块的服务方法进行读取——Query 能理解模块链接,并可在一次调用中跨模块联接数据。
import { MedusaError } from "@medusajs/framework/utils"

if (!product) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    `Product with id "${productId}" was not found`
  )
}

数据模型规则

  • 使用 @medusajs/framework/utils 中的 model 工具定义数据模型。
  • 数据模型变量应采用 camelCase;传递给 model.define 的名称应采用 snake_case。
  • 向数据模型添加 id 字段时,始终使用 .primaryKey() 将其设为主键。
  • 数据模型只能有一个 id 字段——其他标识符应使用 text 字段。
  • 数据模型字段应采用 snake_case。
// src/modules/loyalty/models/loyalty-account.ts
import { model } from "@medusajs/framework/utils"

const LoyaltyAccount = model.define("loyalty_account", {
  id: model.id().primaryKey(),
  customer_id: model.text(),
  points_balance: model.number().default(0),
  tier: model.enum(["bronze", "silver", "gold"]).default("bronze"),
})

export default LoyaltyAccount

服务规则

  • 创建服务时,始终将其方法设为异步方法。
  • 如果模块有数据模型,则让服务扩展 MedusaService,以便继承每个模型自动生成的 CRUD 方法。
// src/modules/loyalty/service.ts
import { MedusaService } from "@medusajs/framework/utils"
import LoyaltyAccount from "./models/loyalty-account"

class LoyaltyModuleService extends MedusaService({
  LoyaltyAccount,
}) {
  async addPoints(accountId: string, points: number) {
    const account = await this.retrieveLoyaltyAccount(accountId)
    return await this.updateLoyaltyAccounts({
      id: account.id,
      points_balance: account.points_balance + points,
    })
  }
}

export default LoyaltyModuleService

工作流规则

  • 创建工作流或步骤时,始终使用 Medusa 的 Workflow SDK(@medusajs/framework/workflows-sdk)来定义。
  • 在 API 路由、定时任务或订阅器中创建功能时,始终为其创建工作流,而不是将逻辑直接写在处理程序中。
  • 创建工作流时,始终为其中每个独立的工作单元创建一个步骤。
  • 在工作流中,使用 transform 在步骤之间转换数据——不要直接在工作流函数体中操作步骤输出。
  • 在工作流中,使用 when 定义条件分支,而不是围绕步骤调用编写普通的 if 语句。
  • 在工作流中调用步骤时不要使用 await——步骤调用会返回一个由工作流引擎解析的特殊引用,使用 await 会破坏工作流协调补偿和重试的能力。
  • 不要将工作流函数本身声明为 async——函数体只描述步骤图,不会以命令式方式执行。
  • 不要为补偿函数的输入添加类型——补偿函数接收步骤的 invoke 函数返回的内容,Medusa 会自动推断类型。
  • 工作流中只能使用步骤——不要直接在工作流函数中调用服务、Query 或其他有副作用的代码;应将这些逻辑放入步骤中。
// src/workflows/redeem-loyalty-points.ts
import {
  createStep,
  createWorkflow,
  StepResponse,
  transform,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"
import { LOYALTY_MODULE } from "../modules/loyalty"
import LoyaltyModuleService from "../modules/loyalty/service"

type RedeemPointsInput = {
  accountId: string
  points: number
}

const deductPointsStep = createStep(
  "deduct-points-step",
  async (input: RedeemPointsInput, { container }) => {
    const loyaltyService: LoyaltyModuleService = container.resolve(LOYALTY_MODULE)
    const account = await loyaltyService.retrieveLoyaltyAccount(input.accountId)

    if (account.points_balance < input.points) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Insufficient points balance"
      )
    }

    const previousBalance = account.points_balance
    const updated = await loyaltyService.updateLoyaltyAccounts({
      id: account.id,
      points_balance: previousBalance - input.points,
    })

    return new StepResponse(updated, { accountId: account.id, previousBalance })
  },
  async (compensationInput, { container }) => {
    if (!compensationInput) return
    const loyaltyService: LoyaltyModuleService = container.resolve(LOYALTY_MODULE)
    await loyaltyService.updateLoyaltyAccounts({
      id: compensationInput.accountId,
      points_balance: compensationInput.previousBalance,
    })
  }
)

export const redeemLoyaltyPointsWorkflow = createWorkflow(
  "redeem-loyalty-points",
  (input: RedeemPointsInput) => {
    const account = deductPointsStep(input)

    const tierDowngrade = when(account, (acc) => acc.points_balance === 0)
      .then(() => {
        return transform({ account }, (data) => ({
          ...data.account,
          tier: "bronze" as const,
        }))
      })

    return new WorkflowResponse(account)
  }
)

API 路由与数据读取

  • 通过 src/api/ 下的 API 路由公开工作流;路由处理程序应验证输入、调用工作流并构造 HTTP 响应,仅此而已。
  • 读取数据(列表/详情端点)时,始终使用 Query,而不是直接解析模块服务。
// src/api/store/loyalty/[id]/route.ts
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const query = req.scope.resolve("query")

  const { data: accounts } = await query.graph({
    entity: "loyalty_account",
    fields: ["id", "points_balance", "tier"],
    filters: { id: req.params.id },
  })

  res.json({ loyalty_account: accounts[0] })
}

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { result } = await redeemLoyaltyPointsWorkflow(req.scope).run({
    input: req.validatedBody as { accountId: string; points: number },
  })

  res.json({ loyalty_account: result })
}

订阅器与定时任务

  • 订阅器和定时任务应调用工作流,与 API 路由完全相同——它们只是同一业务逻辑的不同触发方式。
// src/subscribers/order-placed.ts
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { redeemLoyaltyPointsWorkflow } from "../workflows/redeem-loyalty-points"

export default async function orderPlacedHandler({ event, container }: SubscriberArgs<{ id: string }>) {
  await redeemLoyaltyPointsWorkflow(container).run({
    input: { accountId: event.data.id, points: 0 },
  })
}

export const config: SubscriberConfig = { event: "order.placed" }

管理后台自定义规则

  • 从管理后台自定义内容(小组件、自定义页面)发送请求时,始终使用 Medusa 的 JS SDK(@medusajs/js-sdk),而不是原始 fetch。
  • 使用 TailwindCSS 为管理后台自定义内容设置样式,并遵循 Medusa Admin 自身 UI 的惯例。
// src/admin/widgets/loyalty-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

const LoyaltyWidget = ({ data }: { data: { id: string } }) => {
  const { data: result } = useQuery({
    queryFn: () => sdk.admin.customer.retrieve(data.id),
    queryKey: ["customer", data.id],
  })
  return <p className="text-ui-fg-subtle">{result?.customer.email}</p>
}

export const config = defineWidgetConfig({ zone: "customer.details.after" })
export default LoyaltyWidget

常见错误

  • 在 API 路由处理程序中直接调用模块服务,而不是通过工作流调用——这会跳过重试、补偿以及工作流提供的标准事件/可观测性钩子。
  • 抛出普通的 Error,而不是 MedusaError,这会导致 API 响应丢失映射后的 HTTP 状态码和结构化错误类型。
  • 在工作流函数中对步骤调用使用 await,这会破坏工作流引擎构建步骤图的能力。
  • 通过解析模块服务而不是使用 Query 读取数据,这会遗漏 Query 自动解析的跨模块联接和关联。
  • 将数据模型字段或 model.define 名称命名为 camelCase,而不是 snake_case,导致其与架构其余部分不一致。

其他资源

评论

支持 Markdown