- 后端
- 电商
【免费下载链接】reaction
Project has been discontinued ////// Mailchimp Open Commerce is an API-first, headless commerce platform built using Node.js, React, GraphQL. Deployed via Docker and Kubernetes.
本篇围绕仓库中 packages/api-plugin-fulfillment/README.md 文档展开,讲解 Mailchimp Open Commerce(Reaction)中履约(Fulfillment)基础插件的定位与实现:它本身不实现任何具体履约方式,而是为 shipping、pickup、digital 等履约类型插件,以及 flatRate、store pickup 等履约方式插件提供统一的注册机制、数据模型、GraphQL API 与报价(quotes)流程。读完本文,你可以理解如何基于这套框架编写自己的履约类型插件和履约方式插件,并清楚从「购物车请求报价」到「用户选中履约方式」的完整调用链。
一、基础插件的角色:README 核心观点的源码印证
README 的核心论点可以归纳为三句话:
- 这是一个基础履约插件,其作用是让其他履约类型(shipping、pickup、digital 等)与履约方式(shipping、store、download 等)能够以各自独立的插件形式被引入系统;
- 它提供实现履约所需的通用能力(collections、GraphQL schema、queries/mutations、schema 扩展、报价分发等);
- 每个新引入的履约类型插件,其对应的具体履约方式也必须作为独立的插件添加。例如
pickup类型下可以有store pickup和curb-side pickup两种方式,每一种都是一个单独插件。
仓库的目录结构与这一设计完全对应:
| 层级 | 插件包 | 职责 |
|---|---|---|
| 基础层 | api-plugin-fulfillment | 通用模型、API、注册机制 |
| 类型层 | api-plugin-fulfillment-type-shipping、api-plugin-fulfillment-type-pickup | 注册履约类型枚举值,并在 startup 时创建类型根记录 |
| 方式层 | api-plugin-fulfillment-method-shipping-flat-rate、api-plugin-fulfillment-method-shipping-dynamic-rate、api-plugin-fulfillment-method-pickup-store | 实现具体报价逻辑,向类型下注入方法记录 |
应用侧的插件装配关系可以直接在 apps/reaction/plugins.json 中看到:fulfillment、fulfillmentTypeShipping、fulfillmentTypePickup、fulfillmentMethodShippingFlatRate、fulfillmentMethodShippingDynamicRate、fulfillmentMethodPickupStore被分别独立列出并加载。
二、插件注册:一个插件声明了什么
入口是 src/index.js 中导出的register(app)函数,它通过app.registerPlugin声明了基础插件的全部能力,逐项来看:
- collections:注册
Fulfillment与FulfillmentRestrictions两个 MongoDB 集合,并声明索引:Fulfillment:name: 1、shopId: 1—— 履约类型按店铺隔离,一条文档代表一个店铺下的一种履约类型,其methods数组内嵌该类型下的所有履约方式;FulfillmentRestrictions:methodIds: 1—— 履约限制规则集合(按方法 ID 索引)。
- graphQL:注入 schema.graphql 与 resolvers。
- queries / mutations:注入内部查询与变更函数(见第五节)。
- functionsByType:
registerPluginHandler: [registerPluginHandlerForFulfillmentTypes]—— 供子插件注册履约类型名;preStartup/startup—— 启动钩子(见第七节)。
- contextAdditions:向请求上下文注入
fulfillment对象,见下节。 - simpleSchemas:注册
MethodEmptyData。 - shopSettingsConfig:注册店铺设置项
baseFulfillmentTypesForShop(字符串数组,可选),控制「每个新建商品默认附加哪些履约类型」,编辑权限为reaction:legacy:fulfillmentTypes/update:settings。
2.1 注册处理器:子插件如何「登记」履约类型
src/registration.js 维护一个全局对象:
export const fulfillment = { registeredFulfillmentTypes: ["undecided"] }; export function registerPluginHandlerForFulfillmentTypes({ registeredFulfillmentTypes }) { if (registeredFulfillmentTypes) { fulfillment.registeredFulfillmentTypes = fulfillment.registeredFulfillmentTypes.concat(registeredFulfillmentTypes); } FulfillmentTypeDeclaration.validate(fulfillment); }基础插件自身只预置undecided这一兜底类型(对应 GraphQL 枚举中的默认值)。类型插件则通过插件描述里的registeredFulfillmentTypes字段向该数组追加自己的类型。以 api-plugin-fulfillment-type-pickup/src/index.js 为例:
await app.registerPlugin({ label: "Fulfillment Type Pickup", name: "fulfillment-type-pickup", ... functionsByType: { startup: [startup] }, registeredFulfillmentTypes: ["pickup"] });registerPluginHandlerForFulfillmentTypes正是框架在加载插件时按functionsByType.registerPluginHandler类型回调的钩子,完成「类型名登记」这一动作。
三、数据模型:SimpleSchema 定义与跨插件 Schema 扩展
3.1 核心 Schema
src/simpleSchemas.js 定义了本插件的文档模型:
FulfillmentTypeSchema(Fulfillment集合的文档结构):
| 字段 | 类型 | 说明 |
|---|---|---|
name | String | 履约类型显示名,用户可编辑 |
shopId | String | 所属店铺 ID |
provider | ProviderSchema | 内嵌对象:enabled(Boolean)、label(String)、name(String) |
fulfillmentType | String | 类型标识(如shipping、pickup),应用使用、用户不可编辑 |
displayMessageType | String? | 特定于该类型的 UI 附加提示 |
methods | FulfillmentMethodSchema[]? | 该类型下的履约方式数组 |
createdAt/updatedAt | Date | 时间戳,必填 |
FulfillmentMethodSchema(内嵌于类型的methods数组):
| 字段 | 类型 | 说明 |
|---|---|---|
cost | Number? | 可选成本 |
fulfillmentTypes | String[] (minCount 1) | 适用类型(向后兼容字段) |
group | String | 方法所属分组 |
handling | Number | 手续费 |
enabled | Boolean | 是否启用 |
label | String | UI 显示名 |
name | String | 方法唯一标识(用户可编辑名) |
fulfillmentMethod | String? | 应用内部用的方法名(如flatRate),用户不可编辑 |
displayMessageMethod | String? | 特定于该方法的 UI 附加提示 |
rate | Number | 费率 |
UserEditableFulfillmentMethodSchema只保留了cost、handling、enabled、label、displayMessageMethod、rate六个可编辑字段,用于updateFulfillmentMethod的入参校验。
MethodEmptyData定义了一个空占位对象(gqlType: String+ 可选emptyData: Boolean),作为methodAdditionalData的默认形态。
3.2 extendFulfillmentSchemas:把「类型白名单」注入其他插件
extendFulfillmentSchemas(schemas, allFulfillmentTypesArray)是本插件preStartup阶段调用的关键函数。它用SimpleSchema.extend把「所有已注册履约类型组成的白名单数组」写入其他插件的 schema 定义中,涉及:
Product/CatalogProduct的supportedFulfillmentTypes.$(allowedValues);Shipment的type;CartItem的selectedFulfillmentType与supportedFulfillmentTypes.$;CommonOrder的fulfillmentType;orderFulfillmentGroupInputSchema/OrderFulfillmentGroup的type;- 同时向
SelectedFulfillmentOption和ShippingMethod追加methodAdditionalData字段(SimpleSchema.oneOf(MethodEmptyData),可选),为方式插件扩展自己的附加数据结构预留扩展点。
也就是说,基础插件是全局履约字段校验规则的中枢:类型插件只负责登记类型名,商品、购物车条目、发货单上的履约类型字段合法性则由这里统一收口。
3.3 GraphQL 层的数据类型
schemas/schema.graphql 定义了完整的对外模型:
enum FulfillmentType:基础插件只声明undecided一个值,其余枚举值(如shipping、pickup)由类型插件的 schema 追加扩展(源码注释写明 "extended by fulfillment-type plugins");type EmptyData+union AdditionalData = EmptyData:方法附加数据的多态容器,resolvers/index.js 中的AdditionalData.__resolveType依据obj.gqlType做类型解析;方式插件会以相同模式追加 union 成员。例如 api-plugin-fulfillment-method-pickup-store/src/preStartup.js 会在preStartup阶段把自己的 MethodStoreData(含storeId、storeAddress、storeTiming等门店字段)extend 进ShippingMethod与SelectedFulfillmentOption的methodAdditionalData定义;type FulfillmentMethodObj:方法根对象(cost、handling、rate、enabled、label、name、fulfillmentMethod、group、fulfillmentTypes);type CustomFulfillmentMethodObj:在方法对象上额外带出fulfillmentTypeId与shopId,供按类型分页查询方法时使用;type FulfillmentTypeObj/ProviderInfo:类型根对象与供应商信息;- Relay 风格的 Connection 分页类型:
FulfillmentTypeObjConnection、FulfillmentMethodObjConnection及对应 Edge。
四、GraphQL 查询与变更 API
4.1 Query
schema.graphql 暴露三个查询,Resolver 实现分别位于:
fulfillmentType(fulfillmentTypeId, shopId):src/queries/fulfillmentType.js,先校验reaction:legacy:fulfillmentTypes/read权限,再按_id + shopId查询;fulfillmentTypes(shopId, after/before/first/last/offset):src/queries/fulfillmentTypes.js,返回Fulfillment.find({ shopId })游标;fulfillmentMethods(shopId, fulfillmentTypeId, after/before/first/last/offset):src/queries/fulfillmentMethods.js 使用聚合管道先把匹配的Fulfillment文档$unwind: "$methods",再$replaceRoot合并出fulfillmentTypeId与shopId——因为方法是以数组形式内嵌在类型文档里的,聚合是把它「拍平」为独立节点的必要手段。
另外,ShopSettings被扩展了baseFulfillmentTypesForShop: [FulfillmentType]字段(同时扩展了ShopSettingsUpdatesinput),语义是「将添加到每个新建商品上的默认履约类型列表」。
4.2 Mutation
GraphQL 层只暴露两个面向管理端的变更(schema.graphql),而内部 mutations 更完整(src/mutations/index.js):
| Mutation | 说明 |
|---|---|
updateFulfillmentType(input) | 更新类型的name、enabled、label、displayMessageType;实现见 src/mutations/updateFulfillmentType.js,校验reaction:legacy:fulfillmentTypes/update权限后按_id + shopId执行$set |
updateFulfillmentMethod(input) | 更新方法的可编辑字段,入参UpdateFulfillmentMethodInfo需给出shopId、fulfillmentTypeId、methodId与UserEditableMethodInput(label/displayMessageMethod/cost/handling/rate/enabled均可选) |
selectFulfillmentOptionForGroup(内部 mutation,由 GraphQL resolver 层调用) | 从购物车的shipmentQuotes中选出指定fulfillmentMethodId的报价,写入该组的shipmentMethod并saveCart;见 src/mutations/selectFulfillmentOptionForGroup.js |
updateFulfillmentOptionsForGroup(内部 mutation) | 重新拉取某履约组的报价(quotes)并回写购物车;见 src/mutations/updateFulfillmentOptionsForGroup.js |
4.3 关键 mutation 的实现细节
createFulfillmentType(src/mutations/createFulfillmentType.js):
- 用
FulfillmentTypeSchema.clean(input)清洗入参,并把provider.name强制对齐为name; - 明确「虽然 schema 允许
methods,但创建类型时不允许同时携带方法」——方法应由方式插件通过createFulfillmentMethod注入,这一点直接呼应了 README 中「方式必须是独立插件」的设计; - 幂等处理:若
{ shopId, fulfillmentType }已存在则仅Logger.warn并直接返回,不抛错; - 新插入前校验
reaction:legacy:fulfillmentTypes/create权限,用Random.id()生成_id。
对应的测试 src/mutations/createFulfillmentType.test.js 覆盖了三个路径:缺少必填字段报Name is required、类型已存在时跳过权限校验直接返回、以及正常插入。
createFulfillmentMethod(src/mutations/createFulfillmentMethod.js):校验reaction:legacy:fulfillmentMethods/create权限后,先确认目标类型存在;若该类型下已有同fulfillmentMethod名的方法则幂等返回;否则为方法生成_id、自动把fulfillmentTypes设为[该类型的 fulfillmentType],再用$addToSet: { methods: method }追加到类型文档的methods数组。
五、报价流程:fulfillmentMethodsWithQuotes 的分发机制
这是基础插件「通用能力」中最核心的一环:src/queries/fulfillmentMethodsWithQuotes.js。
流程可以拆解为:
- 前置检查:
commonOrder.items为空直接返回空数组;调用 extendCommonOrder 把订单条目用 Catalog 中真实的商品/变体数据(而非客户端数据)合并补全,并串联执行所有addShippingRestrictionCustomAttributes类型的函数以收集限制所需的标签与自定义属性; - 按类型分发:
commonOrder.fulfillmentType指定本次要报价的履约类型,基础插件从context.getFunctionsOfType("fulfillmentMethodsWithQuotes")中筛选func.key === fulfillmentTypeInGroup的所有 handler 并行执行(Promise.all); - 失败重试一次:各方式插件会把处理失败的请求登记进
retrialTargets,首轮结束后若非空则整体重试一轮,仍失败则Logger.warn记录出错的 package; - 返回:所有方式插件写入
rates的报价数组。
方式插件正是通过functionsByType.fulfillmentMethodsWithQuotes挂载自己的 handler。以 api-plugin-fulfillment-method-shipping-flat-rate/src/index.js 为例:
functionsByType: { preStartup: [fulfillmentMethodShippingFlatRatePreStartup], validateOrderMethods: [{ key: "flatRate", handler: validateOrderMethodsFlatRate, name: "validateOrderMethodsFlatRate" }], fulfillmentMethodsWithQuotes: [ { key: "shipping", handler: fulfillmentMethodsWithQuotesShippingFlatRate, name: "fulfillmentMethodsWithQuotesShippingFlatRate" } ] }key: "shipping"即声明「我负责 shipping 类型下的报价」,与类型插件登记的registeredFulfillmentTypes形成闭环。此外 src/queries/getFulfillmentMethodsWithQuotes.js 是 5.0 起标记为 deprecated 的别名,直接转发给fulfillmentMethodsWithQuotes,仅为向后兼容保留。
购物车侧的完整链路在 src/mutations/updateFulfillmentOptionsForGroup.js 中体现:取出购物车 →removeMissingItemsFromCart清理失效商品 →getCommonOrderForCartGroup构造该履约组的 CommonOrder → 调用fulfillmentMethodsWithQuotes拿到 rates → 用getShipmentQuotesQueryStatus归一化出shipmentQuotes与shipmentQuotesQueryStatus(区分pending/success/error三种状态)→ 仅当报价或状态有变化时saveCart。随后用户在结算页做选择,走selectFulfillmentOptionForGroup:校验所选fulfillmentMethodId必须存在于当前组的shipmentQuotes中(防止选择已被刷新掉的报价),再把对应option.method写入组的shipmentMethod。
六、启动流程与数据库迁移
6.1 preStartup:版本检查 + Schema 扩展
src/preStartup.js 做两件事:
- 通过
@reactioncommerce/db-version-check校验reaction_api_plugin_fulfillment命名空间的数据库版本是否为期望的2;setToExpectedIfMissing回调中,只有当Fulfillment与FulfillmentRestrictions两个集合均为空(或不存在)时才把版本直接置为期望值,避免对已有数据误判; - 从
context.fulfillment.registeredFulfillmentTypes(此时已由各类型插件填好)取值,调用extendFulfillmentSchemas完成上一节所述的白名单注入。
6.2 startup:undecided 兜底类型
src/startup.js 监听appEvents的afterShopCreate事件:每个新店铺创建后,若Fulfillment集合中不存在{ fulfillmentType: "undecided", shopId }记录,则自动插入一条name: "Undecided Type"、provider 为{ enabled: true, label: "Undecided", name: "undecided" }的根记录,作为用户在尚未选定履约类型时的占位。
类型插件遵循同一模式:api-plugin-fulfillment-type-pickup/src/checkAndCreateFulfillmentType.js 在 startup 时为每个店铺幂等创建pickup类型记录;方式插件同理,api-plugin-fulfillment-method-pickup-store/src/startup.js 遍历所有店铺(每个店铺一个事务)确保门店自提方法存在,并监听afterShopCreate覆盖新店铺。
6.3 迁移:从 Shipping 到 Fulfillment 的更名
migrations/2.js(注册于 migrations/index.js)记录了这套模型从「shipping 专用」到「通用履约」的演进:
up:用聚合把旧Shipping集合整体$out复制为新Fulfillment集合、FlatRateFulfillmentRestrictions复制为FulfillmentRestrictions;为缺失fulfillmentType的文档补写fulfillmentType: "shipping";用arrayFilters为已有methods批量补写fulfillmentMethod: "flatRate";并把 Groups 中的旧权限reaction:legacy:shippingMethods/*、shippingRestrictions/*映射替换为reaction:legacy:fulfillmentTypes/*、fulfillmentMethods/*、fulfillmentRestrictions/*;down:从受影响 Groups 中pullAll新权限并 drop 两个新集合。
这也解释了为何 GraphQL schema 中仍保留Shipment、ShippingMethod等命名(源码注释中多处提到 "retained for backward compatibility")。
七、不透明 ID 与工具
src/xforms/id.js 定义了reaction/shop、reaction/cart、reaction/fulfillmentGroup、reaction/fulfillmentMethod四个命名空间的 opaque id 编解码函数,其中FulfillmentMethod._id的 resolver(src/resolvers/FulfillmentMethod/index.js)即用encodeFulfillmentMethodOpaqueId把内部 ID 包装为不透明 ID 输出给客户端。
八、本地开发与测试
按 package.json 的声明,该包要求 Node>=14.18.1、npm>=7,依赖simpl-schema、lodash与同仓库的api-utils、reaction-error、random、db-version-check等包。常用脚本:
# 在 packages/api-plugin-fulfillment 目录下 npm test # 运行 jest npm run lint # eslint测试集中在每个 mutation 同目录的*.test.js(如 createFulfillmentType.test.js、selectFulfillmentOptionForGroup.test.js),通过@reactioncommerce/api-utils/tests/mockContext.js与mockCollection构造 mock 上下文与集合,分别验证必填校验、幂等返回、正常写入三类行为;src/tests/factory.js 则提供了测试用的工厂数据。
九、小结:三层插件架构的完整图景
把 README 的设计主张与源码对照,可以得到这张扩展地图:
- 基础插件(本文主角):提供
Fulfillment/FulfillmentRestrictions集合、SimpleSchema 模型与全局白名单扩展、GraphQL 查询/变更、registerPluginHandlerForFulfillmentTypes登记机制、fulfillmentMethodsWithQuotes报价分发、购物车选择/刷新报价 mutation,以及undecided兜底类型的 startup 逻辑; - 类型插件:如 fulfillment-type-shipping / fulfillment-type-pickup,只需通过
registeredFulfillmentTypes登记类型名、追加 GraphQL 枚举值,并在 startup 时为店铺创建类型根记录; - 方式插件:如 fulfillment-method-shipping-flat-rate / fulfillment-method-pickup-store,通过
functionsByType.fulfillmentMethodsWithQuotes(按类型 key)挂载报价实现、通过createFulfillmentMethod注入方法记录、通过preStartup把自己的MethodXxxData扩展进methodAdditionalData,并可追加validateOrderMethods校验器。
对要扩展新履约能力(例如 digital 下载方式或 curb-side pickup)的开发者而言,正确的落点正是 README 所强调的:新建类型插件登记类型名 + 新建方式插件实现报价与附加数据,而不是修改基础插件本身。
- 后端
- 电商
【免费下载链接】reaction
Project has been discontinued ////// Mailchimp Open Commerce is an API-first, headless commerce platform built using Node.js, React, GraphQL. Deployed via Docker and Kubernetes.
相关推荐
Reaction(Mailchimp Open Commerce)中 shipping 履约类型的注册与初始化机制:api-plugin-fulfillment-type-shipping 源码解析
Reaction(Mailchimp Open Commerce)中 shipping 履约类型的注册与初始化机制:api plugin fulfillment
后端电商Reaction Commerce pickup 履约类型插件:api-plugin-fulfillment-type-pickup 的实现与运行机制解析
Reaction Commerce pickup 履约类型插件:api plugin fulfillment type pickup 的实现与运行机制解析 本文
后端电商Reaction 门店自提履约方法插件实现解析:api-plugin-fulfillment-method-pickup-store
Reaction 门店自提履约方法插件实现解析:api plugin fulfillment method pickup store 本篇技术指南围绕 Reac
后端电商
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考