关于 medusa-development
Medusa Development 是面向使用 Medusa 无头电商框架构建功能的开发者技能。它围绕数据模型、模块服务和由步骤组成的工作流提供指引,并涵盖 API、定时任务及订阅器的调用方式。
主要功能
- 01数据模型与模块服务指引
- 02可组合的工作流步骤
- 03通过路由和任务调用工作流
- 04使用 Query 读取数据
- 05管理端定制规范
适用场景
- 01添加电商模块和数据模型
- 02将业务逻辑实现为工作流
- 03通过路由或定时任务调用工作流
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill medusa-development安装后在 Claude Code 中输入/medusa-development确认已加载。
详细介绍
Medusa 开发
Medusa 是一个以模块、数据模型和工作流为核心构建的无头电商框架;几乎所有业务逻辑——从 API 路由到定时任务——都应表示为由离散、可组合步骤组成的工作流。
构建 Medusa 功能的工作流
- 定义或扩展数据模型 — 使用
@medusajs/framework/utils中的model工具,在src/modules/<module>/models/下声明模块的数据模型。 - 编写模块服务 — 在
src/modules/<module>/service.ts中创建服务;如果模块有数据模型,则扩展MedusaService,并提供用于领域操作的异步方法。 - 注册模块 — 将模块添加到
medusa-config.ts,以便 Medusa 的容器能够解析该模块。 - 构建步骤 — 使用
@medusajs/framework/workflows-sdk中的createStep,将每个工作单元定义为一个步骤;对于失败时需要撤销的操作,还要包含补偿函数。 - 组合工作流 — 使用
createWorkflow将各步骤串联起来,并使用transform整理数据、使用when创建条件分支。 - 公开工作流 — 从 API 路由、定时任务或订阅器中调用工作流——切勿将业务逻辑直接写在路由、任务或订阅器处理函数中。
- 使用 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,导致其与架构其余部分不一致。
其他资源
- Medusa 文档:https://docs.medusajs.com/llms-full.txt
评论
支持 Markdown