news 2026/9/10 16:37:23

Medusa 定时任务(Scheduled Jobs)编写与自动加载机制全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Medusa 定时任务(Scheduled Jobs)编写与自动加载机制全解析

Medusa 定时任务(Scheduled Jobs)编写与自动加载机制全解析

【免费下载链接】medusaThe world's most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa

导读

Medusa 框架内置了一套基于工作流(Workflow)的定时任务体系,开发者只需在任意模块、插件或自定义目录中放置一个导出了handlerconfig的 JS/TS 文件,框架启动时便会自动发现该文件、校验配置、注册为名为job-<name>的工作流,并按 Cron 表达式周期性执行。本文以框架 jobs 目录下的示例任务order-summary(见 fixtures 目录)为核心骨架,结合 JobLoader 实现 与其 单元测试,完整讲解任务文件格式、config 参数语义、加载过滤规则与调度执行原理,帮助你写出规范、可调试、可测试的 Medusa 定时任务。

一、一个最小定时任务长什么样

Medusa 中一个定时任务就是一个模块文件,默认导出异步处理函数handler,同时具名导出config配置对象。以框架自带的测试示例 order-summary.ts 为例:

import { MedusaContainer } from "@medusajs/types" export default async function handler(container: MedusaContainer) { console.log(`You have received 5 orders today`) } export const config = { name: "summarize-orders", schedule: "* * * * * *", numberOfExecutions: 2, }

这个文件只有两个关键导出:

  • default(handler):任务的实际执行逻辑,接收containerMedusaContainer类型的依赖注入容器),可以通过container.resolve(...)获取数据库连接、模块服务、Logger 等任意注册资源,并支持异步。
  • config:任务元信息,由nameschedulenumberOfExecutions等字段组成,决定任务注册名与调度策略。

值得注意的是,在框架的fixtures目录 中还放置了扩展名为.md.txt的同名文件,其内容虽然是合法的任务代码,但加载器会将其静默忽略——这正是下一节要讲的扩展名过滤规则。因此实际项目中的定时任务必须使用.js.ts扩展名。

二、config 配置项详解

根据 job-loader.ts 中的 CronJobConfig 类型定义 与加载器校验逻辑,config支持以下字段:

字段类型是否必填说明
namestring必填任务名称。注册时会自动拼接为工作流名job-${name},例如summarize-orders会生成工作流job-summarize-orders
schedulestring \| SchedulerOptions必填Cron 表达式字符串(如"* * * * * *",六段式);也可以直接传入一个包含cronnumberOfExecutions等字段的调度器选项对象
numberOfExecutionsnumber可选限定任务的总执行次数。示例中的2表示该任务只会被调度执行 2 次,之后不再触发;用于一次性/限期任务的场景

这三个字段并非可有可无——加载器在注册前会执行严格的配置校验(validateConfig):

if (!config) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, "Config is required for scheduled jobs." ) } if (!config.schedule) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, "Cron schedule definition is required for scheduled jobs." ) } if (!config.name) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, "Job name is required for scheduled jobs." ) }

从 源码 可以看到:configschedulename三者缺一不可,否则会抛出INVALID_ARGUMENT类型的MedusaError,且任一配置缺失都会导致任务无法注册。因此编写任务时请确保三个字段同时存在。

schedule 的两种传法

schedule既可以是字符串,也可以是对象。加载器在注册时会进行归一化处理:

const workflowConfig = { name: workflowName, schedule: isObject(config.schedule) ? config.schedule : { cron: config.schedule, numberOfExecutions: config.numberOfExecutions, }, }

即:当schedule传入的是对象时,直接透传;当传入字符串时,自动包装为{ cron, numberOfExecutions }。上面的示例任务经过转换后,其调度配置等价于:

{ cron: "* * * * * *", numberOfExecutions: 2, }

测试 register-jobs.spec.ts 恰好验证了这一结果——注册完成后,通过WorkflowManager.getWorkflow("job-summarize-orders")能取到工作流,且其options.schedule精确等于{ cron: "* * * * * *", numberOfExecutions: 2 }

三、任务是如何被自动发现的:JobLoader 加载链路

3.1 资源发现与文件过滤

定时任务的自动发现由 JobLoader 完成,它继承自 ResourceLoader 基类。JobLoader.load()会调用基类的discoverResources(),递归扫描传入的sourceDir,并对每个候选文件执行自定义过滤:

customFiltering = (entry: Dirent) => { const parsedName = parse(entry.name) return ( !entry.isDirectory() && (allowIndex || parsedName.name !== "index") && !parsedName.base.endsWith(".d.ts") && !entry.path.includes("__tests__") && [".js", ".ts"].includes(parsedName.ext) && !this.#excludes.some((exclude) => exclude.test(parsedName.base)) && !exclude.some((exclude) => exclude.test(parsedName.base)) ) }

也就是说,只有同时满足以下条件的文件才会被当作任务加载:

  • 扩展名必须是.js.ts——这是 fixtures 中的order-summary.mdorder-summary.txt不会被加载的根本原因;
  • 文件名不能是index
  • 不以.d.ts结尾;
  • 路径中不能包含__tests__目录;
  • 不以_开头(/^_[^/\\]*(\.[^/\\]+)?$/),也不以.spec.[jt]s.test.[jt]s结尾。

这套规则保证了 README、测试文件、辅助模块等不会误注册为定时任务。对应的行为在测试中得到了直接验证:

it("should not load non js/ts files", async () => { const jobLoader: JobLoader = new JobLoader( join(__dirname, "../__fixtures__/plugin/jobs-with-other-files"), container ) await jobLoader.load() const workflow = WorkflowManager.getWorkflow("job-summarize-orders") expect(workflow).toBeUndefined() })

由于jobs-with-other-files目录下只有.md.txt文件,加载后工作流job-summarize-orders未被注册(toBeUndefined()),从侧面印证了扩展名过滤的确定性。

3.2 校验、注册与工作流化

通过过滤的文件会进入onFileLoaded流程,其类型签名要求导出default(handler)与config两个成员:

protected async onFileLoaded( path: string, fileExports: { default: CronJobHandler config: CronJobConfig } ) { if (isFileSkipped(fileExports)) { return } this.validateConfig(fileExports.config) this.logger.debug(`Registering job from ${path}.`) this.register({ path, config: fileExports.config, handler: fileExports.default, }) }

其中isFileSkipped来自 define-file-config.ts,它会检查导出中是否带有框架约定的跳过标记MEDUSA_SKIP_FILE——若任务文件显式导出该标记,则会被跳过不注册,为开发者提供了一种"临时停用任务而不删除文件"的手段。

通过校验后进入register方法,加载器会将 handler 包装为一个Step,再用createWorkflow注册为工作流:

const workflowName = `job-${config.name}` const step = createStep( `${config.name}-as-step`, async (input: ScheduledJobWorkflowInput | undefined, stepContext) => { const { container } = stepContext const context: ScheduledJobContext = { scheduledFor: input?.scheduledFor ? new Date(input.scheduledFor) : new Date(), } try { const res = await handler(container, context) return new StepResponse(res, res) } catch (error) { this.logger.error( `Scheduled job ${config.name} failed with error: ${error.message}` ) throw error } } )

这段代码揭示了几个关键点:

  1. 每个定时任务最终都被注册为名为job-<name>的工作流(Workflow),复用 Medusa 的工作流调度与执行基础设施;
  2. handler 的执行发生在 step 内部,container由工作流上下文注入,因此任务内可以直接使用容器解析任何服务;
  3. 任务抛出的异常会被捕获并记录为Scheduled job <name> failed with error: ...,随后重新抛出,便于在日志中定位失败任务。

3.3 scheduledFor:任务触发时间上下文

从 types.ts 可以看到,handler 的完整签名是:

export type ScheduledJobHandler = ( container: MedusaContainer, context?: ScheduledJobContext ) => Promise<unknown> export type ScheduledJobContext = { scheduledFor: Date } export type ScheduledJobWorkflowInput = { scheduledFor: string }

handler 的第二个参数context.scheduledFor表示"本次任务计划执行的时间点"。加载器在调度执行时会从工作流输入中取出scheduledFor并转换为Date;若未提供,则默认取当前时间new Date()

这一机制的语义验证可见 fixtures 中的 scheduled-for.ts:它的 handler 直接把context.scheduledFor写入全局变量;对应的测试通过LocalWorkflow.run(ulid(), { scheduledFor }, { __type: MedusaContextType })手动触发工作流job-capture-scheduled-for,最终断言global.__medusaScheduledForTest等于传入时间戳转换后的Date对象(见 register-jobs.spec.ts)。如果你的业务需要"按计划时间戳"而非"实际运行时刻"来统计数据(例如补跑某天的订单汇总),务必使用context.scheduledFor

四、从插件加载:sourceDir 的传参方式

JobLoader的构造函数接受sourceDir: string | string[],支持同时从多个目录发现任务:

constructor(sourceDir: string | string[], container: MedusaContainer) { super(sourceDir, container) }

基类 ResourceLoader.discoverResources 会把目录统一规范化为数组,并对每个目录执行access(sourcePath)存在性检查——若目录不存在,会打印No job to load from <path>. skipped.并跳过,而不是报错。这意味着:

  • 插件可以把任务文件放在自己的src/jobs目录下,框架启动时即可自动发现;
  • 即使某个插件没有 jobs 目录,加载流程也不会中断;
  • 多个 sourceDir 之间互不影响,各自独立发现与注册。

测试should registers jobs from plugins即演示了以join(__dirname, "../__fixtures__/plugin/jobs")作为 sourceDir,从"插件形态"的目录中加载order-summaryscheduled-for两个任务,并成功注册工作流job-summarize-orders的过程。

五、任务编写与调试实战建议

结合上述机制,编写一个规范的 Medusa 定时任务可以参考以下实践:

1. 保持文件结构清晰建议将任务统一放在项目的src/jobs(或插件的同名字目录)下,一个文件一个任务,文件名与config.name保持对应关系(如order-summary.ts对应summarize-orders),便于排查与维护。

2. 使用完整的六段 Cron 表达式示例中使用的"* * * * * *"是六段式 Cron(秒 分 时 日 月 周),开发调试时可用它让任务每秒触发;生产环境请根据实际频率改写为合理的表达式,避免高频空转。

3. 善用 numberOfExecutions 控制执行次数对于一次性数据修复、迁移类任务,可以通过numberOfExecutions: 1让任务只执行一次,配合schedule即可实现"启动后立即执行且仅执行一次"的语义,无需额外的状态标记。

4. 通过容器获取服务,而非直接 importhandler 的第一个参数是MedusaContainer,应当通过container.resolve(...)获取订单服务、库存服务或 Logger 等资源,这样既能享受依赖注入与测试 mock 的便利,也符合框架的解耦设计。

5. 遵守扩展名与命名约束

  • 文件必须为.js/.ts
  • 不要命名为index.d.ts,不要放入__tests__目录,不要以_开头或以.spec.ts/.test.ts结尾,否则会被自动过滤;
  • 若希望临时停用某个任务,可以导出MEDUSA_SKIP_FILE标记(详见 define-file-config.ts),而不是删除文件或破坏扩展名。

6. 利用日志定位失败任务当 handler 抛出异常时,加载器会输出Scheduled job <name> failed with error: ...,建议在 handler 内部也使用注入的 Logger 记录关键业务步骤,便于区分"调度失败"与"业务失败"。

六、机制总结

回顾整条链路,一个 Medusa 定时任务的生命周期是:

  1. 发现JobLoader递归扫描 sourceDir,按扩展名与命名规则过滤出合法的.js/.ts任务文件(非 JS/TS 文件如.md.txt被忽略);
  2. 校验validateConfig强制要求configschedulename三者齐全,缺失即抛INVALID_ARGUMENT
  3. 注册:handler 被包装为 step,与归一化后的调度配置一起通过createWorkflow注册为job-<name>工作流;
  4. 执行:工作流调度器按 Cron 触发 step,向 handler 注入container与包含scheduledFor的上下文,numberOfExecutions控制累计执行次数;
  5. 失败处理:异常被记录日志后重新抛出,等待调度器与监控体系进一步处理。

这套设计让定时任务与 Medusa 的工作流引擎深度统一,开发者无需关心底层的调度实现,只需遵循"default handler + config"的约定编写任务文件,即可获得自动发现、自动注册、可单测(通过WorkflowManager.getWorkflow断言注册结果)的完整能力。

【免费下载链接】medusaThe world's most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 16:36:07

推荐:智能聊天助手——AskSusi Telegram Bot

推荐&#xff1a;智能聊天助手——AskSusi Telegram Bot 1、项目介绍 在数字时代&#xff0c;我们每天都会遇到各种问题&#xff0c;快速获取准确答案是关键。这就是AskSusi Telegram Bot的用武之地。这个开源项目将人工智能与即时通讯完美结合&#xff0c;为你提供一个通过T…

作者头像 李华
网站建设 2026/9/10 16:34:53

Zola结构化数据3步落地:让搜索结果展示文章摘要与作者

Zola结构化数据3步落地&#xff1a;让搜索结果展示文章摘要与作者 【免费下载链接】zola A fast static site generator in a single binary with everything built-in. https://www.getzola.org 项目地址: https://gitcode.com/GitHub_Trending/zo/zola 本文以 Zola 静…

作者头像 李华
网站建设 2026/9/10 16:33:18

MTProxy自动化部署脚本:从源码到服务的一键安装

MTProxy自动化部署脚本&#xff1a;从源码到服务的一键安装 MTProxy是一款高效的代理工具&#xff0c;通过自动化部署脚本可以实现从源码到服务的快速搭建。本文将详细介绍如何使用MTProxy的自动化部署功能&#xff0c;让你轻松完成代理服务的安装与配置。 准备工作&#xff…

作者头像 李华
网站建设 2026/9/10 16:31:38

SpringBoot宠物寄养系统:活体服务建模与强耦合调度实战

简介&#xff1a;本资源是一套已通过导师验收的高分本科毕业设计项目——基于SpringBoot开发的宠物医院寄养管理系统&#xff0c;面向计算机类专业本科生、Java初学者及课程设计实践者&#xff0c;解决宠物寄养业务中客户预约、宠物信息登记、订单管理、员工协同等核心场景的信…

作者头像 李华
网站建设 2026/9/10 16:30:24

MusicFree 免费音乐播放器上手指南:从装插件到日常听歌

MusicFree 免费音乐播放器上手指南&#xff1a;从装插件到日常听歌 【免费下载链接】MusicFree 插件化、定制化、无广告的免费音乐播放器 项目地址: https://gitcode.com/GitHub_Trending/mu/MusicFree 主流音乐 App 里广告、会员墙和强制登录几乎成了标配&#xff0c;想…

作者头像 李华