电商后端前端【免费下载链接】solidus Solidus, the open-source eCommerce framework for industry trailblazers.项目地址https://gitcode.com/gh_mirrors/so/solidus点击查看免费下载导读Solidus Legacy Promotions 是一个将经典 Spree/Solidus 促销系统从 Solidus 各 Gem 中抽取出来的独立 Rails Engine为订单、行项目与运费提供一套规则 动作驱动的灵活折扣能力。本文将先从四大核心概念促销处理器、促销规则、促销动作、调整项拆解其运行原理再以一个使用指定支付方式即可享受 50% 运费折扣的真实需求为线索完整演示自定义 handler、rule、action 并接入后台管理的全部步骤让你既能看懂现有促销代码也能照此模式扩展出属于自己的促销逻辑。一、认识 Legacy Promotions安装方式与项目定位Solidus Legacy PromotionsGem 名为solidus_legacy_promotions承载的是 Solidus 历史上经典的那套基于规则rule与动作action的促销系统。正如 legacy_promotions/README.md 所述它被从 Solidus 其他 Gem 中抽取出来作为一个独立的 Rails Engine 维护。1.1 安装安装方式取决于你的 Gemfile 结构如果你的 Gemfile 中包含gem solidus这一行本 Gem 会随 Solidus 全套自动安装无需额外操作如果你是按需引入 Solidus 套件中的各个独立 Gem则需要显式添加gem solidus_legacy_promotions1.2 定位与演进方向该 Gem 的命名已经暗示了它的处境——它计划被弃用slated for deprecation。Solidus 团队正在推进一套全新的促销实现并计划在同一年内随版本发布。在本仓库中这套新实现对应 promotions/ 目录下的solidus_promotionsGem。从引擎的初始化代码可以印证两者的衔接逻辑在 engine.rb 中只有当Spree::Config.promotions还是空的NullPromotionConfiguration即商店没有使用新的solidus_promotionsGem时引擎才会将配置切换为SolidusLegacyPromotions::Configuration并注入order_contents_class、adjustment_promotion_source_types等相关设置。也就是说两套促销系统通过配置层隔离互不抢占。另外该引擎还负责向后端注入促销管理入口在SolidusSupport.backend_available?时向后台菜单插入Promotions / Promotion Categories菜单项见 engine.rb在SolidusSupport.admin_available?时向 Solidus Admin 注册对应的菜单与订单列表组件engine.rb。二、架构全景四大核心概念如何协同工作Legacy Promotions 之所以能覆盖整单折扣、单行项目/多行项目折扣、运费折扣等多种场景是因为它把促销能力拆解为四个相互协作的概念概念职责仓库内代表实现Promotion handlers促销处理器在顾客旅程的合适节点激活促销legacy_promotions/app/models/spree/promotion_handlerPromotion rules促销规则校验订单是否满足促销条件legacy_promotions/app/models/spree/promotion/rulesPromotion actions促销动作定义对合格订单施加的具体折扣legacy_promotions/app/models/spree/promotion/actionsAdjustments调整项持久化折扣信息并在订单每次更新时重算legacy_promotions/app/models/spree/promotion/order_adjustments_recalculator.rb[!NOTE] 调整项并不只服务于促销。任何会修改订单金额的概念例如税费都会使用调整项机制促销只是它的典型应用之一。2.1 免费运费示例一次完整的促销生命周期原文档用订单总额达到 100 美元即免运费这个例子展示了 Solidus 处理一次促销的完整流程顾客填写配送信息时Shipping 促销处理器 在订单上激活促销。从源码看它先合并已关联订单的促销与自动应用apply_automatically: true的促销逐一调用promotion.eligible?(order)与promotion.activate(order:)激活时促销先执行基础资格检查使用次数上限、有效日期区间等再校验定义的规则是否全部满足。这两步分别对应 promotion.rb 中的 eligible? 方法先依次检查inactive?、blacklisted?、usage_limit_exceeded?、promotion_code.usage_limit_exceeded?最后调用eligible_rules逐条验证规则调用规则时ItemTotal 规则 确认订单item_total是否达到 100 美元其默认preferred_amount正是100.00且支持gt/gte两种运算符订单合格后FreeShipping 动作 被应用到订单的 shipments 上——它遍历每个 shipment为尚未被该动作优惠过的 shipment 创建一条amount shipment.cost * -1的调整项从而抵消运费顾客获得免费运费。2.2 激活的底层调用链促销激活的核心逻辑集中在 promotion.rb 中的 activate 方法先通过self.class.order_activatable?(order)排除complete、awaiting_return、returned等不可再激活的订单状态组装 payload含order、promotion、line_item、user、path、promotion_code依次调用每个动作的action.perform(payload)只要任何一个动作返回true就通过order.order_promotions.find_or_create_by!建立订单与促销的关联并返回action_taken。2.3 调整项重算促销资格的动态保持文档特别强调促销调整项在订单每次更新时都会重算以确认订单状态变化后资格是否仍然成立。这一机制由 OrderAdjustmentsRecalculator 实现遍历所有行项目与 shipments 上的促销调整项逐个调用recalculaterecalculate会跳过已 finalize 的调整项并根据调整项的 source即对应的 PromotionAction重新调用compute_amount算出金额再通过adjustment.source.promotion.eligible?重新判断资格计算完成后由 PromotionChooser 在同一组促销调整项中挑选金额最低优惠最大的一条作为最佳促销其余标记为 ineligible——这正是整单只能叠加一个最优整单促销的规则来源该重算器在Spree::OrderUpdater中于税费计算之前被调用从而保证税费始终尊重促销结果。[!TIP] 文档提到可以对重算行为做定制。对应的扩展点是Spree::Config.promotions.order_adjuster_class默认指向Spree::Promotion::OrderAdjustmentsRecalculator详见下文配置化扩展点一节。2.4 开箱即用的 handler、rule 与 action 清单Legacy Promotions 自带一批实用的内置实现可以从两个层面确认处理器4 个目录 legacy_promotions/app/models/spree/promotion_handler 下有Cart商品入购物车时激活兼容行项目级促销、Coupon优惠码应用/移除负责处理coupon_code_not_found、coupon_code_max_usage等状态码、Page通过促销的path字段按访问路径激活、Shipping配送环节激活运费类促销。其中 Coupon 处理器 的apply/remove方法完整演示了校验 → 激活 → 重算以及错误状态码的设定流程规则13 个ItemTotal、Product、User、FirstOrder、UserLoggedIn、OneUsePerUser、Taxon、MinimumQuantity、NthOrder、OptionValue、FirstRepeatPurchaseSince、UserRole、Store默认注册于 configuration.rb 中的 rules 类集合动作4 个CreateAdjustment整单折扣基于 calculator、CreateItemAdjustments按行项目折扣、CreateQuantityAdjustments按数量折扣、FreeShipping免运费默认注册于 configuration.rb 中的 actions 与 shipping_actions 类集合。其中shipping_actions专门标识运费类动作被 Shipping 处理器 的active_shipping_promotions查询所使用。三、实战三步实现指定支付方式享 50% 运费折扣文档用一个极具代表性的需求串联起整套自定义流程我们想与新的支付平台建立合作关系顾客在结账时使用该支付方式即可享受 50% 的运费折扣。为此需要分别实现我们自己的 handler、rule 和 action。下面按原文档的路线完整走一遍。3.1 实现一个新的 handler促销处理器促销处理器在技术上没什么特别它们就是普通的 Ruby 对象在结账流程的正确位置被创建并调用。Solidus 并没有为 handler 定义统一 API但可以参考 现有处理器 的格式来写。原文档给出的实现如下# frozen_string_literal: true module AmazingStore module PromotionHandler class Payment RULES_TYPE AmazingStore::Promotion::Rules::Payment attr_reader :order def initialize(order) order order end def activate promotions.each do |promotion| promotion.activate(order: order) if promotion.eligible?(order) end end private def promotions ::Spree::Promotion. active. joins(:promotion_rules). where(promotion_rules.type RULES_TYPE) end end end end这个 handler 先按promotion_rules.type筛选出包含我们特定规则类型的、处于激活状态的促销子集再逐个调用promotion.eligible?(order)与promotion.activate(order:)激活合格的促销。为什么需要自己的 handler文档点出了一个关键事实其他内置 handler 虽然也可能捡到我们的促销但如果在支付步骤之前运行它们根本无法激活它比如Cart处理器在商品加入购物车时就运行了。所以我们要确保促销在支付方式被选中之后的节点被激活。文档给出的做法是利用Spree::Order的状态机在结账流程从:payment状态迁出之后通过prepend注入一个回调# frozen_string_literal: true module AmazingStore module LoadPaymentPromotionHandler def self.prepended(base) base.state_machine.after_transition(from: :payment) do |order| AmazingStore::PromotionHandler::Payment.new(order).activate end end ::Spree::Order.prepend(self) end end这与 Solidus 自身的做法一脉相承订单状态机定义在 core/app/models/spree/core/state_machines/order/class_methods.rb其中已有after_transition from: any - :cart, to: any - [:confirm, :complete] do |order| order.recalculate end之类的全局回调而 Legacy Promotions 自己也通过 spree_order_state_machine_patch.rb 用同样的prepend手法在:delivery状态迁出前调用apply_shipping_promotions。可见prepend 状态机模块 挂载 transition 回调正是 Solidus 扩展结账行为的标准姿势。3.2 实现一个新的 rule促销规则有了 handler接下来实现检查顾客是否使用了被推广的支付方式的促销规则。为了让商店管理员能在后台编辑哪个支付方式享受折扣最优雅的方式是为规则本身创建 preference# frozen_string_literal: true module AmazingStore module Promotion module Rules class Payment ::Spree::PromotionRule DEFAULT_PREFERRED_PAYMENT_TYPE AmazingStore::AmazingPaymentPlatform ALLOWED_PAYMENT_TYPES [ DEFAULT_PREFERRED_PAYMENT_TYPE, Spree::PaymentMethod::Check, Spree::PaymentMethod::CreditCard ].freeze preference :payment_type, :string, default: DEFAULT_PREFERRED_PAYMENT_TYPE validates :preferred_payment_type, inclusion: { in: ALLOWED_PAYMENT_TYPES, allow_blank: true }, on: :update def applicable?(promotable) promotable.is_a?(::Spree::Order) end def eligible?(order, _options {}) order.payments.any? do |payment| payment.payment_method.type preferred_payment_type end end end end end end对照 PromotionRule 基类 可以理解这套契约基类通过raise NotImplementedError强制子类实现applicable?和eligible?并默认提供actionable?返回true表示可应用于任意行项目子类可按需覆写与eligibility_errors用ActiveModel::Errors记录不合格原因供后台展示错误码。此外基类还带有一个值得注意的校验unique_per_promotion同一个促销内不允许出现两条同类型的规则。[!CAUTION] 你可能注意到我们允许支付类型在创建时为空。这是因为促销规则在创建之初是不带任何 preference 的——这样后台才能先展示正确的表单给管理员配置。如果从一开始就强制要求支付类型必填Solidus 将无法创建这条规则管理员会直接看到报错。这也是为什么inclusion校验放在on: :update上的原因。为规则提供后台 UI。规则名是Payment因此需要创建对应的 partial其中可以通过局部变量promotion_rule访问当前规则实例div classrow div classcol-6 div classfield % promotion_rule.class.human_attribute_name(:payment_type) % /div /div div classcol-6 div classfield % select_tag #{param_prefix}[preferred_payment_type], options_for_select(promotion_rule.class::ALLOWED_PAYMENT_TYPES, promotion_rule.preferred_payment_type), class: fullwidth % /div /div /divpartial 的命名与存放位置并非随意——PromotionRule#to_partial_path 返回spree/admin/promotions/rules/#{model_name.element}即后台渲染规则表单时正是按规则类名去app/views/spree/admin/promotions/rules/下寻找同名的_xxx.html.erb。仓库内置规则的视图也遵循同一约定例如 _item_total.html.erb 通过promotion_rule.class.operator_options渲染运算符下拉框、通过fields_for param_prefix, promotion_rule渲染金额与币种输入。注册规则。最后在 initializer 中把规则注册进spree.promotions.rules类集合# ... Rails.application.config.spree.promotions.rules AmazingStore::Promotion::Rules::Payment这套注册机制的后台消费逻辑在 PromotionRulesControllervalidate_promotion_rule_type会从Spree::Config.promotions.rules中按type名称查找对应类找不到就提示invalid_promotion_rule。也就是说只有注册过的规则才会出现在后台的规则选择列表中。补充友好描述。为了让规则表单更易读可以关联一段描述文本en: # ... activerecord: attributes: amazing_store/promotion/rules/payment: description: Must use the specified payment method完成以上步骤后在后台新建促销时规则列表里就会出现Payment规则并且描述会随表单一起渲染。3.3 实现一个新的 action促销动作最后实现50% 运费折扣这个促销动作。文档建议直接参考现有的 FreeShipping 动作——它的perform遍历 shipments 建调整项、compute_amount返回shipment.cost * -1、remove_from清理自己创建的调整项结构完全可以照搬# frozen_string_literal: true module AmazingStore module Promotion module Actions class HalfShipping ::Spree::PromotionAction # The perform method is called when an action is applied to an order or line # item. The payload contains a lot of useful context: # 参见 [promotion.rb 中的 activate 方法](https://link.gitcode.com/i/eb073f4ad5bc77d914d694ccc00b6a06) # payload 包含 order、promotion、line_item、user、path、promotion_code 等键。 def perform(payload {}) order payload[:order] promotion_code payload[:promotion_code] results order.shipments.map do |shipment| # 如果该 shipment 已经被本促销动作优惠过则跳过。 next false if shipment.adjustments.where(source: self).exists? # 否则创建一条调整项为 shipment 施加 50% 折扣。 shipment.adjustments.create!( order: shipment.order, amount: compute_amount(shipment), source: self, promotion_code: promotion_code, label: promotion.name, ) # 返回 true 标记该 shipment 已被优惠。 true end # perform 需要在该促销动作实际应用了任何调整项时返回 true否则返回 false。 results.any? { |result| result true } end def compute_amount(shipment) shipment.cost * -0.5 end # remove_from 应撤销 perform 所做的一切。当订单不再满足某促销 # 的资格、促销需要被移除时该方法会被调用。 def remove_from(order) order.shipments.each do |shipment| shipment.adjustments.each do |adjustment| if adjustment.source self # 这里只需删除本促销动作在订单 shipments 上创建的调整项。 shipment.adjustments.destroy!(adjustment) end end end end end end end end这里体现了 PromotionAction 基类 定义的完整契约perform必须实现基类直接raise返回值语义是本次是否真正施加了调整项——promotion.rb 的 activate 正是通过results.include?(true)判断action_taken只有返回true才会建立OrderPromotion关联remove_from必须实现用于促销失效时回滚副作用Spree::Promotion#remove_from会依次调用所有动作的remove_from再销毁 join 记录与FreeShipping一样perform中需要幂等检查shipment.adjustments.where(source: self).exists?避免重复施加折扣。为动作提供空表单。和规则类似动作也需要一个 partial——即使本例不需要任何可配置参数Solidus 仍会去查找它。根据 PromotionAction#to_partial_path路径应为spree/admin/promotions/actions/#{model_name.element}!-- Intentionally empty --[!TIP] 如果你的动作需要可配置参数可以参照 CreateQuantityAdjustments 动作 及其 后台视图 的做法动作通过available_calculators来自Spree::Config.promotions.calculators[self.class]暴露可选的 calculator 集合视图则渲染promotion_calculators_with_custom_fields局部模板让管理员挑选计算器并配置其参数。这类动作通常继承Spree::CalculatedAdjustments例如 CreateAdjustment 与 CreateItemAdjustments 都 include 了它并在before_validation中兜底创建默认 calculator。注册动作。同样在 initializer 中追加注册# ... Rails.application.config.spree.promotions.actions AmazingStore::Promotion::Actions::HalfShipping注册的消费端在 PromotionActionsControllerpromotion_action_types Spree::Config.promotions.actions。重启服务器后后台动作列表中就会出现这个新动作。补充友好描述en: # ... activerecord: attributes: amazing_store/promotion/actions/half_shipping: description: Applies 50% discount in shipping四、端到端验证从后台配置到前台生效按文档给出的验收路径走一遍进入后台Promotions板块点击New Promotion本例的促销不需要优惠码勾选Apply to all orders对应模型上的apply_automatically字段apply_automatically: true的促销会被 Cart 处理器 等自动纳入激活候选注意apply_automatically与path互斥——见 promotion.rb 中的 apply_automatically_disallowed_with_paths 校验给促销添加Payment规则与Half shipping动作并选择配置好的支付方式回到前台选择该支付方式后即可看到运费被削减 50%。整个链路的工作方式顾客选完支付方式 → 订单状态从:payment迁出 → 我们 prepend 的回调调用AmazingStore::PromotionHandler::Payment→ 它找到并激活带Payment规则的促销 →HalfShipping#perform为各 shipment 创建-50%的调整项 → 订单重算后promo_total体现折扣最终反映在运费金额上。五、进阶扩展点与运维要点5.1 配置化扩展点一览除了向rules/actions类集合追加自定义类Configuration 还暴露了一批可整体替换的类名配置供深度定制配置项默认值用途promotion_chooser_classSpree::PromotionChooser决定同一组促销调整项中谁是最佳促销的选择器order_adjuster_classSpree::Promotion::OrderAdjustmentsRecalculator自定义促销调整项的重算行为promotion_finder_classSpree::PromotionFinder通过优惠码或 ID 查找促销的查找器shipping_promotion_handler_classSpree::PromotionHandler::Shipping配送环节的促销处理器coupon_code_handler_classSpree::PromotionHandler::Coupon优惠码处理逻辑advertiser_classSpree::PromotionAdvertiser促销推广advertise行为promotion_code_batch_mailer_classSpree::PromotionCodeBatchMailer优惠码批次异步生成结果的通知邮件此外还有promotions_per_page后台每页展示的促销数量默认 15与promotion_api_attributesAPI 返回促销时暴露的属性白名单两个普通 preference。5.2 动作与计算器的组合矩阵calculators是一个嵌套类集合定义了哪些计算器可用于哪些动作默认配置见 configuration.rb如下CreateAdjustment整单折扣FlatPercentItemTotal、FlatRate、FlexiRate、TieredPercent、TieredFlatRateCreateItemAdjustments行项目折扣DistributedAmount、FlatRate、FlexiRate、PercentOnLineItem、TieredPercentCreateQuantityAdjustments数量折扣PercentOnLineItem、FlatRate。对应计算器实现位于 legacy_promotions/app/models/spree/calculator例如tiered_flat_rate.rb、tiered_percent.rb、distributed_amount.rb等。5.3 运维任务仓库附带一个实用的维护任务solidus_legacy_promotions:delete_ineligible_adjustments定义于 delete_ineligible_adjustments.rake用于直接删除数据库中所有eligible: false的调整项。由于促销重算会不断产生失效的调整记录定期执行该任务可以控制数据膨胀。5.4 需要注意的几个约束规则/动作的唯一性PromotionRule基类的unique_per_promotion校验保证同一个促销内不会重复出现同一类型的规则订单状态的激活禁区promotion.rb 定义了UNACTIVATABLE_ORDER_STATES [complete, awaiting_return, returned]这些状态的订单不再接受促销激活apply_automatically与path互斥同时设置会触发apply_automatically_disallowed_with_paths校验见 promotion.rb新促销系统并存若项目已启用solidus_promotions新实现本 Gem 的配置注入会被跳过engine.rb两者由Spree::Config.promotions区分迁移时需注意不要同时配置两套。结语Solidus Legacy Promotions 的价值在于用四个清晰的抽象覆盖所有促销形态处理器决定何时激活、规则决定是否合格、动作决定折扣内容、调整项负责持久化与动态重算。通过本文的实战你已经掌握了自定义 handler / rule / action 并注册到后台的完整套路——这套模式同样适用于整单折扣、行项目折扣、运费折扣乃至任意你想象得到的促销场景。在正式项目中建议持续关注 Solidus 官方对 promotions/ 新实现的推进为后续从 Legacy Promotions 平滑迁移做好准备。赞分享电商后端前端【免费下载链接】solidus Solidus, the open-source eCommerce framework for industry trailblazers.项目地址https://gitcode.com/gh_mirrors/so/solidus点击查看免费下载相关推荐Solidus Promotions 促销系统深度指南架构、配置与迁移实战Solidus Promotions 促销系统深度指南架构、配置与迁移实战 Solidus Promotions solidus_promotions 是电商后端前端企业级RE引擎Mod框架架构解析高性能模块化开发平台的核心设计企业级RE引擎Mod框架架构解析高性能模块化开发平台的核心设计 REFramework作为专为RE引擎游戏打造的企业级Mod框架和脚本平台为《生化危机》、《游戏开发VReShop促销系统优惠券、折扣码与促销活动eShop促销系统优惠券、折扣码与促销活动 引言电商促销的挑战与机遇 在当今竞争激烈的电商环境中一个强大的促销系统是企业成功的关键因素。eShop作为基于后端电商前端微服务上一篇ncmdump NCM文件解密完整指南3步还原网易云音乐下一篇如何让 Windows 11 任务栏拖放回来Windows11DragAndDropToTaskbarFix 完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考