ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Medusa Loyalty 插件深度解析:Gift Cards 与 Store Credit 模块的实现与演进(@medusajs/loyalty-plugin)

Medusa Loyalty 插件深度解析:Gift Cards 与 Store Credit 模块的实现与演进(@medusajs/loyalty-plugin) Medusa Loyalty 插件深度解析Gift Cards 与 Store Credit 模块的实现与演进medusajs/loyalty-plugin【免费下载链接】medusaThe worlds most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa导读medusajs/loyalty-plugin是 Medusa v2 开源生态中的官方忠诚度插件为电商应用提供礼品卡Gift Cards与店铺余额Store Credit两大能力。本文以该插件的 CHANGELOG.md 为骨架结合其源码实现梳理插件的模块架构、配置方式、兑换码生成、购物车/下单扣款链路、并发防超扣机制、过期校验、Admin 集成与版本演进帮助你理解其内部原理并能在自己的 Medusa 项目中正确安装、配置与二次开发。插件概览与定位medusajs/loyalty-plugin在 package.json 中描述为 Medusa Plugin: Loyalty - Gift Cards当前版本 2.20.1与 Medusa 核心medusajs/medusa2.20.x 版本线保持同步发布。它不是一个单一模块而是打包了两个独立 Module加**一组工作流Workflows**的复合插件Loyalty 模块loyalty核心实体是GiftCard礼品卡Store Credit 模块store_credit核心实体是StoreCreditAccount店铺余额账户与AccountTransaction账户流水二者通过模块链接Module Links与订单、购物车、客户等核心模块关联。从 types/modules.ts 可以看到插件内部通过枚举注册了两个模块名LOVALTY loyalty与STORE_CREDIT store_credit后续所有工作流都通过PluginModule.LOYALTY/PluginModule.STORE_CREDIT在容器中解析对应服务。模块架构GiftCard 与 Store Credit 的双模块设计Loyalty 模块与 GiftCard 数据模型Loyalty 模块的服务定义在 modules/loyalty/service.ts它通过MedusaService({ GiftCard })自动获得基于GiftCard模型的 CRUD 能力并额外暴露getOptions()方法返回插件配置项。礼品卡模型定义在 modules/loyalty/models/gift-card.ts表名为loyalty_gift_card关键字段如下字段类型说明id主键前缀gcard礼品卡 IDstatus枚举默认pending可取pending/redeemed见 types/loyalty/module.ts 中的GiftCardStatusvaluebigNumber礼品卡面值codetext唯一且可搜索兑换码currency_codetext可搜索ISO 三位货币代码expires_atdateTime可空过期时间为空表示永不过期reference/reference_idtext可空来源资源类型与 ID如order与订单 IDline_item_idtext可空作为商品被购买时的行项目 IDnote/metadatatext / json备注与自定义元数据对应的类型定义ModuleGiftCard/ModuleCreateGiftCard/ModuleUpdateGiftCard位于 types/loyalty/module.ts其中ModuleCreateGiftCard的code注释明确说明未提供时将自动生成。Store Credit 模块账户 流水账本Store Credit 模块的服务实现在 modules/store-credit/service.ts是插件中业务逻辑最重的部分。它基于两个模型store-credit-account.ts表名store_credit_accountID 前缀sc_acc包含code、currency_code、customer_id、metadata并对customer_id currency_code建立了唯一索引仅当customer_id IS NOT NULL时生效保证同一客户同一币种只能有一个账户。account-transaction.ts流水表type取credit/debit见 types/store-credit/module.ts 的TransactionType枚举。值得注意的设计决策是账户余额并非存储字段而是由流水实时聚合得出。retrieveAccountStats方法通过 SQL 对store_credit_account_transaction表做条件聚合——SUM(CASE WHEN at.type credit THEN at.amount ELSE -at.amount END)计算余额分别统计credits与debits总额返回ModuleAccountStats含balance/credits/debits。这意味着每一笔变动都会留下不可篡改的流水痕迹余额永远是算出来的。服务还通过白名单机制限制可更新字段updateStoreCreditAccounts只允许更新id与metadata因为变更客户或货币代码会导致余额跟踪异常。此外由于MedusaService按模型名生成方法deleteAccountTransactions而公开接口约定为deleteTransactions服务中实现了转发方法避免运行时TypeError该问题在代码注释中有完整说明。安装与配置依赖与 peer 范围从 package.json 可以看到插件的运行时 peer 依赖包括medusajs/medusa、medusajs/framework、medusajs/dashboard等且除核心包外的 peer 依赖均标记为 optional。React 系依赖react、react-dom、react-router-dom也是 optional——只有需要加载插件自带的管理后台页面时才需要。CHANGELOG 中 2.19.0 版本将react-router-dom的可选 peer 范围放宽为^6.30.4 || ^7.0.0意味着无论宿主项目使用 react-router 6 还是 7插件都能干净安装2.16.0 版本则做过一次三个包draft-order、dashboard、loyalty-plugin之间的 react-router-dom 版本对齐。插件配置项prefix 与 sections插件接受两个配置项定义在 types/loyalty/module.ts 的LoyaltyPluginOptions中并在medusa-config中传给插件export type LoyaltyPluginOptions { /** * 生成的礼品卡兑换码前缀默认 GIFT * example GC → GC-XXXX-XXXX-XXXX-XXXX */ prefix?: string /** * 兑换码中 4 字符分组的段数默认 4 * example 3 → GIFT-XXXX-XXXX-XXXX */ sections?: number }在medusa-config.ts中启用插件的配置方式如下module.exports defineConfig({ plugins: [ { resolve: medusajs/loyalty-plugin, options: { prefix: GC, // 自定义前缀 sections: 4, // 4 组 × 4 字符 }, }, ], })createGiftCardsStep见 workflows/gift-cards/steps/create-gift-cards.ts会从容器中解析 Loyalty 模块并调用module.getOptions()拿到这两个配置值再传给兑换码生成器。兑换码生成与自定义 Code2.17.2 新能力自动生成算法默认兑换码由 utils/code-generator.ts 中的generateCode(prefix GIFT, sections 4)生成。核心要点字符集为ABCDEFGHJKLMNPQRSTUVWXYZ23456789刻意排除了易混淆的0/O、1/I用crypto.randomBytes生成密码学安全随机字节并按需取模映射到字符集代码总长度为sections * 4个字符按每 4 字符一组用连字符拼接带前缀时输出形如GIFT-XXXX-XXXX-XXXX-XXXX不带前缀则直接输出分组串。自定义 Code 支持2.17.2 版本新增礼品卡支持自定义 code能力。实现上createGiftCardsStep中先用isPresent(giftCard.code)判断调用方是否传入了自定义 code——只有未传入时才调用generateCode自动生成随后才调用module.createGiftCards(input)落库。因此调用方传的 code 会原样保留唯一性约束由模型的code.unique()保证。对应地Admin 创建礼品卡接口api/admin/gift-cards/validators.ts的AdminCreateGiftCardschema 中code为z.string().optional()即不填则自动生成、填了则使用自定义值。核心业务流程创建 → 兑换 → 加购 → 下单扣款 → 认领创建礼品卡AdminAdmin 侧POST /admin/gift-cardsapi/admin/gift-cards/route.ts将请求体交给createGiftCardsWorkflow创建后再通过 Query Graph 回读返回完整对象。创建 schema 要求currency_code、valuez.number().min(1)可选code、status默认pending、expires_at、reference、reference_id、line_item_id、note、metadata。兑换礼品卡Store兑换发生在客户侧。redeemGiftCardWorkflowworkflows/gift-cards/workflows/redeem-gift-card.ts的执行链路是按gift_card_id查询礼品卡查询是否已关联 store credit 账户validateGiftCardRedeemStep校验——已兑换status REDEEMED或已有关联账户则报错createStoreCreditAccountsStep按礼品卡币种创建匿名 store credit 账户createLinksWorkflow建立gift_card ↔ store_credit_account链接creditAccountsWorkflow以reference: gift_card、reference_id: 兑换码记入一笔 credit 流水note 为 Gift card redemptionupdateGiftCardsWorkflow将礼品卡状态置为redeemed返回带余额与流水明细的账户。添加到购物车Store 侧POST /store/carts/:id/gift-cardsapi/store/carts/[id]/gift-cards/route.ts调用addGiftCardToCartWorkflowworkflows/carts/workflows/add-gift-card-to-cart.ts。该工作流内置三层校验validateGiftCardStep礼品卡必须存在validateCartGiftCardStep礼品卡未重复加购、未过期isGiftCardExpired、币种与购物车一致validateGiftCardBalancesStep账户余额必须大于 0。通过校验后取min(账户余额, 购物车总额)创建购物车 credit linereference: gift-card建立cart ↔ gift_card链接并触发refreshCartItemsWorkflow刷新购物车。值得关注的是该工作流暴露了一个validate钩子createHook(validate, ...)允许开发者在不改动插件源码的前提下通过addGiftCardToCartWorkflow.hooks.validate(...)注入自定义校验逻辑——这正是 Medusa Workflows SDK 的扩展点设计。下单扣款与防超扣购物车完成时插件通过completeCartWorkflow.hooks.orderCreated钩子workflows/hooks/after-order-created.ts触发cloneCartGiftCardsToOrderWorkflow把购物车上的礼品卡链接克隆到订单上并调用confirmCartCreditLinesWorkflowworkflows/carts/workflows/confirm-cart-credit-lines.ts完成实际扣款。confirmCartCreditLinesWorkflow的逻辑校验购物车上所有礼品卡未过期validateGiftCardsNotExpiredStep防止过期余额在结算时被扣走通过gift_card_store_credit_account链接把 credit line 映射到 store credit 账户对reference为store-credit或gift-card的 credit line 调用debitAccountsWorkflow逐笔借记note 为 Gift card usage。2.20.0 的 lock account on debit 修复就落在这里。由于余额由流水聚合而来借记是典型的先查余额再插入流水check-then-insert操作。在 Postgres 默认的 READ COMMITTED 隔离级别下两个并发结账事务互相看不到对方未提交的借记可能双双通过余额校验造成超扣。修复实现在lockAccountsForUpdate_modules/store-credit/service.ts在同一事务内对涉及的所有账户执行SELECT ... FOR UPDATE行级锁把同一账户的借记串行化账户 ID 先去重再排序锁定避免跨账户借记时相互死锁若当前不在事务上下文中则主动抛错而不是静默回退到非事务连接——因为非事务连接会立即释放锁等于悄悄重新引入超扣竞态。认领礼品卡claimGiftCardWorkflowworkflows/gift-cards/workflows/claim-gift-card.ts允许注册客户把已兑换的匿名礼品卡余额转入自己的账户。前置校验validateClaimGiftCardInputStep要求礼品卡已有关联的 store credit 账户、账户有 code、且客户has_account trueOnly customers with an account can claim a gift card。校验通过后委托claimStoreCreditAccountWorkflow完成余额转移对应 Store 接口为POST /store/store-credit-accounts/claimapi/store/store-credit-accounts/claim/route.ts入参为code与当前登录客户 ID。过期时间校验2.20.0 完善2.20.0 的 validate gift card expiry dates 修复围绕isGiftCardExpired工具函数utils/gift-card.ts展开expires_at为空则永不过期否则将过期时间与当前时间按 UTC 时间戳比较expires_at now即视为过期。该函数被购物车添加校验、下单前校验等多个工作流复用并在 utils/tests/gift-card.spec.ts 与 workflows/carts/workflows/tests/validate-gift-card-expiry.spec.ts 中有对应单测覆盖。同版本还包含两个配套修复fix(loyalty-plugin): edit gift card product following global product options change跟随核心产品模块全局 Product Options 变更调整礼品卡商品编辑逻辑以及fix(loyalty-plugin): lock account on debit上文已述。Admin 集成与平台级兼容认证类型适配2.18.02.18.0 修复了fix(loyalty-plugin): honor the configured admin auth type in the admin SDK instead of hard-coding session auth, fixing 401s and empty pages under ADMIN_AUTH_TYPEjwt。此前插件的 Admin 客户端代码把认证方式硬编码为 session当宿主项目通过ADMIN_AUTH_TYPEjwt启用 JWT 认证时请求会返回 401、页面空白。修复后 Admin SDK 改为读取服务端实际配置的认证类型。这与 admin/lib/sdk.ts 中的 SDK 初始化逻辑对应。默认货币列表扩展2.18.0 / 2.16.0插件 Admin 端维护了一份硬编码货币映射表 admin/lib/currencies.ts。CHANGELOG 中三个版本分别向默认货币列表补充了新币种2.18.0伊朗里亚尔IRT、安哥拉宽扎AOA后者同时修复了 Admin 区域编辑器遇到未知货币 code 时的崩溃2.16.0冈比亚达拉西GMD——若缺失该条目Admin 中遍历store.supported_currencies做查找的页面会抛TypeError: Cannot read properties of undefined (reading code)。2.16.0 还修复了fix(loyalty-plugin): respect user locale in currency formatting即货币金额格式化遵循用户 locale而不是写死某种格式。Admin 界面扩展机制2.16.02.16.0 引入的LayoutComposer/ 插件注入区injection zones机制让插件可以将自己的页面礼品卡列表、礼品卡商品、Store Credit 账户注入 Admin 布局。插件的 Admin 路由位于 src/admin/routes 下包含gift-cards列表、创建、详情、过期/备注编辑与store-credit-accounts列表、创建、详情、充值两大板块另有sales-channel-gift-cards、customer-store-credit-widget、order-gift-cards-widget等注入式 widget见 src/admin/widgets。构建与工程化2.18.0 / 2.14.02.18.0 调整了插件构建流程以处理插件构建过程中循环依赖cyclic deps问题并顺带修复db命令在容器初始化失败时退出码不为 1 的问题2.14.0开源版本完成了若干工程化收尾迁移到Zod v4migrate to Zod v4、为服务方法补充 tsdocs 并新增index.ts模型导出、移除礼品卡删除操作并清理代码、修复礼品卡商品分区显示与过期日期错误提示2.14.2 移除未使用的参数并导出 step2.17.2 补充了 packagebugs元数据。数据关联Module Links插件通过模块链接把礼品卡、账户与核心模块打通链接文件集中在 src/links链接文件关联对象语义cart-gift-cards-link.tscart ↔ gift_card购物车应用了哪些礼品卡order-gift-cards-link.tsorder ↔ gift_card订单关联的礼品卡customer-store-credit-account-link.tsstore_credit_account ↔ customer客户拥有的账户只读、一对一gift-card-store-credit.tsgift_card ↔ store_credit_account礼品卡背后的余额账户order-line-item-gift-card-link.tsorder_line_item ↔ gift_card作为商品购买的礼品卡行项目这些链接使插件各工作流可以直接用 Query Graph 跨模块读取关联数据例如add-gift-card-to-cart工作流中读取cart.gift_cards.code而无需破坏模块隔离。版本演进速览2.14.0 → 2.20.1从 CHANGELOG.md 可以还原插件从开源到当前版本的演进主线版本关键变更2.14.0插件正式开源open source loyalty pluginZod v4 迁移tsdocs 补齐移除删除礼品卡操作2.15.x依赖滚动更新无独立功能变更2.16.0LayoutComposer 插件注入区、GMD 货币、locale 感知的货币格式化、react-router-dom 版本对齐2.17.2自定义兑换码支持Adding an option for custom codes in gift-cards、bugs 元数据2.18.0IRT / AOA 货币、Admin 认证类型适配ADMIN_AUTH_TYPEjwt、构建循环依赖修复2.19.0react-router-dompeer 范围放宽为^6.30.4 \|\| ^7.0.02.20.0借记时锁定账户防超扣、礼品卡商品跟随全局产品选项、过期日期验证2.20.1依赖版本对齐medusajs/ui4.2.3等总结medusajs/loyalty-plugin是一个小而完整的复合插件用GiftCard模型承载礼品卡语义用账户 流水账本的 Store Credit 模块承载真实余额通过模块链接打通购物车、订单与客户再用一组可扩展的工作流串联创建 → 兑换 → 加购 → 结算扣款 → 认领全链路。其核心设计值得借鉴余额不落库、流水即账本保证每一分钱的变动可追溯行级锁 确定性锁序在 READ COMMITTED 下消灭并发超扣createHook扩展点 模块链接让开发者不 fork 即可注入自定义逻辑。如果你正在 Medusa 项目中规划礼品卡或余额体系可以直接启用该插件并参考 src/workflows 下的实现在其基础上定制自己的忠诚度玩法。【免费下载链接】medusaThe worlds most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表