最近在研究 Spree 的代码,写了系列的分析文章。
Spree 是 rails 社区比较有名的开源电商平台,这里是一些个人在阅读其源码时所做的笔记(本篇主要基于 spree_core 模块,后续会增加其他模块)。
spree 采用 rails engines 来实现模块化,将功能划分成以下几个独立的组件(可以通过源码根目录下的 spress.gemspec 证实):
s.add_dependency 'spree_core', version
s.add_dependency 'spree_api', version
s.add_dependency 'spree_backend', version
s.add_dependency 'spree_frontend', version
s.add_dependency 'spree_sample', version
s.add_dependency 'spree_cmd', version
开发人员可以根据需要选择使用这些不同的组件,比如自己来构建 frontend 展现,但 spree_core 是必选的。
spree_core 核心模块是一个 rails engine,通过查看其入口文件 lib/spree_core.rb 得知,初始化代码的逻辑是由 lib/spree/core.rb 来完成的,在这个文件里面定义了 Spree 命名空间模块,以及一些全局的配置方法,比如通过 Spree.user_class=来设置用户模型使用哪一个类(也就是说 spree 并不提供默认的用户模型实现)。
当开发者提供了具体的用户模型之后,spree 是如何做到扩展的呢?奥秘就在初始化时执行了 config/initializers/user_class_extensions.rb:
Spree::Core::Engine.config.to_prepare do
if Spree.user_class
Spree.user_class.class_eval do
include Spree::UserApiAuthentication
include Spree::UserReporting
#用户-角色 多对多关联
has_and_belongs_to_many :spree_roles,
join_table: 'spree_roles_users',
foreign_key: "user_id",
class_name: "Spree::Role"
#订单
has_many :orders, foreign_key: :user_id, class_name: "Spree::Order"
#发货地址、账单地址
belongs_to :ship_address, class_name: 'Spree::Address'
belongs_to :bill_address, class_name: 'Spree::Address'
# has_spree_role? simply needs to return true or false whether a user has a role or not.
def has_spree_role?(role_in_question)
spree_roles.where(name: role_in_question.to_s).any?
end
def last_incomplete_spree_order
orders.incomplete.order('created_at DESC').first
end
end
end
end
首先,我们围绕电商的核心模型 Product 来探索 spree 的 model 设计,部分关联如下图所示:
产品表存储了最基本的信息,使用 Property 来扩展额外的键值对属性。
采用 Variant 来实现同一产品的不同套餐型号(每个型号的名称来自于全局设定的 OptionType,值采用 OptionValue 存储),关联多个价格是因为它支持针对不同地域定制不同的价格。
基于不同的 Zone,可以预先设置好特定于区域的发货方式,以便针对多国用户提供差异化的服务;不同的区域可以设置不同的运输方式,然后每个产品的所有版本都需要关联运输方式,在用户下单时系统会根据这个来计算运费。如果一张订单中所购买的商品运输方式不是同一种的话,在执行发货操作的时候,还会自动生成多条对应的发货记录(以便支持类似京东的拆单发货功能)。
运费作为订单调整金额计算入总价,同时还可以针对订单项进行价格微调,他有个“价格调整”的概念。
打折促销抽象出了独立的模型,可以设置规则(涵盖了优惠码等情况),通过触发动作来实现具体的金额调整。
支付功能基于 activemerchant 来集成支付网关,需要在后台维护好支付方式;另外他也支持线下支付的功能,也就是说可以在收到用户的钱之后,由管理员在后台输入相关信息完成支付。
很多模型都使用 acts_as_paranoid 这个 gem 包来做假删除,另外订单的状态维护是通过 state_machines-activerecord 基于有限状态机原理来实现的。
关于库存方面,模型关系如下图所示:
需要配置好仓库位置信息(类似京东那种多仓库就近发货概念),然后在维护产品不同版本的时候,可以选择一个位置将其加入到仓库,库存数据是自动计算的,库存动态表记录了变化信息;库存管理还支持不同仓库之间的移货操作,通过 StockTransfer 来完成。
再来看看实现退货功能的模型:
spree 的退货流程,需要管理员手动创建一条退货许可(ReturnAuthorization),然后将需要退回的货物关联到退货项;在实际收到用户寄回的货品时,还要手工创建对应的用户退货(CustomerReturn)记录。
通过这几天的源码阅读,对 spree 的 core 模块有了大体的了解,具体的 API 细节还需要结合 frontend 以及 backend 这两个上层模块的实现来看才行。从目前理解到的模型层面的知识,对比 D3 的实现之前个人开发的网店应用,可以看到 spree 的设计有如下优势:
缺点的话,个人主观认为,如果想在天朝使用,有些功能不太符合国情,比如支付方式,默认支持的网关都是国外的,针对支付宝、财付通、微信支付等都没有相应的实现;另外,他的某些概念就算实际用不上,比如区域仓储功能,你还是得老老实实的创建一个默认的仓库,然后才能创建产品(这个属性是必须的);还有产品的套餐属性是全局设定的,虽然说某些情况下可以重用,但我总觉得这个信息应该是独立于每个产品更加合适(或者说,独立于产品类型),灵活度会更高。