分享 Spree 源码导读

zhaozijie · 2015年03月05日 · 最后由 1c7 回复于 2018年03月14日 · 11995 次阅读
本帖已被设为精华帖!

最近在研究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
  • spree_core: 基础功能(模型、必不可少的核心代码)
  • spree_api: RESTful API接口实现
  • spree_backend: 后台管理模块
  • spree_frontend: 前台展现
  • spree_sample: 数据样例
  • spree_cmd: 一系列命令行工具

开发人员可以根据需要选择使用这些不同的组件,比如自己来构建frontend展现,但spree_core是必选的。

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的设计有如下优势:

  • 支持多区域:有Zone的概念,这样便引申出了一系列的差异化服务,比方说针对不同国家、地区、城市的定价,运费,以及仓储管理(可以参考京东商城的就近发货功能);
  • 促销功能比较灵活:促销抽象出了规则跟动作两个概念,前者用于检查某张订单是否满足促销条件,后者用来决定应用优惠的时机,这样两者组合起来就能实现很丰富的玩法了;比方说,你可以创建一个促销算法,在新用户下第一张订单,并购买了指定商品的时候给予优惠。
  • 订单功能比较完善:spree不仅支持正常的用户下单操作,还能由管理员在后台直接生成订单;另外就是考虑到了线上、线下两种支付情况。

缺点的话,个人主观认为,如果想在天朝使用,有些功能不太符合国情,比如支付方式,默认支持的网关都是国外的,针对支付宝、财付通、微信支付等都没有相应的实现;另外,他的某些概念就算实际用不上,比如区域仓储功能,你还是得老老实实的创建一个默认的仓库,然后才能创建产品(这个属性是必须的);还有产品的套餐属性是全局设定的,虽然说某些情况下可以重用,但我总觉得这个信息应该是独立于每个产品更加合适(或者说,独立于产品类型),灵活度会更高。

共收到 38 条回复

赞,期待楼主继续贡献

Spree 是个不错的项目,有不少学习价值

我们后续会增加对其他模块的源码分析,以及整个gem是如何组装起来,实现模块化架构的原理,敬请期待。

要好好学习下才行!

这个一定要顶,Spree的代码的质量感觉还是很高的!

我只想问,楼主用的什么画图工具?

最近流行源码分析。。。

和ecshop相比,spree有啥优势?

楼主为何不研究ruby-china源码呢?

我帮LZ答: 1 分析spree可以学习rails这是绝对优势 2 spree和ruby-china是不同类型的项目,分析spree可以学习到很多关于电子商务的知识 BTW,谢谢分析,之前我粗略学习过,等待其他部分!

@stephen 没错,我再来加一点最关键,也是最吸引我的:

3、spree采用rails engines来拆分子模块,开发者可以根据需要灵活选择组合实现业务功能,这是社区目前比较流行的架构方式(Modular Architecture,也有人称之为Component-based Application),基于这种方式,每个子模块都是独立的gem包,可以重用到不同的场合。还记得大明湖畔的devise么?它也是个rails engine gem,用户登录基本上是每个web应用必备的功能,每次新开发项目都要重复一遍是很蛋疼的事儿,devise通过将自己打包为mountable的engine提供给程序员,做到的最大的可重用性,但是spree则更进一步,将一个具体的业务领域(电子商务)用同样的模块化架构来实现,并开源,多么美好……

目前我在自己的项目中也在学习运用这种方式来组织功能模块,目前遇到的问题是,这样会打破很多rails约定俗成的惯例,也就是不符合COC原则,确实有点蛋疼,大概需要更高层次的架构水平,以及对整个开发过程有深入的理解,提高熟练程度才能得心应手吧。换句话说,新手请绕道,日后再说吧:)

@mogodb @zhaozijie

@mogod 没有看过ecshop的源码,貌似是php的?期待了解的朋友分享感受:)

spree 有学习过他们支付流程和代码模块组织的东西,不过运用起来还是有些复杂- -

我们目前的项目正在用Spree。确实如你所说,它的默认支付是用支付网关的方式,是个局限。我前段时间主要就是完成了接入支付宝、财付通、微信支付等支付方式流程,以及QQ、微博联合登陆。

还有一点,spree_auth_devise默认必须要求一个邮件地址,这个很蛋疼,费了好大劲儿才搞明白怎么绕过去。

#16楼 @dapic 国内自己搞网店的很少吧,你们是国内电商?

#17楼 @mogodb 是的。网站还没上线。电商只是销售手段,我们在研发自有品牌的产品(也还没上市)。

#18楼 @dapic 为何不通过电商平台,而要自建站点呢?

#19楼 @mogodb 希望能够超越这些大平台的局限,提升用户体验。就像苹果自建旗舰店往往是独立建筑,而不是大商场中的店面。

发Spree发过MR,参考过其订单的架构。

#20楼 @dapic 我们的产品,最早就是基于spree开发,但实在不适合国内用,后来放弃,按国内主流电商业务,重新开发了,不如去试试,能帮你省不少心。

#22楼 @chrisloong

提示:你的浏览器当前处于放大状态,可能导致页面显示不正常,可按“ctrl+数字0”组合键恢复初始状态。 我知道了

#23楼 @ruby_sky 遇到问题了?请问是用什么设备和浏览器呢?

@scriptfans @chrisloong option_values 和 variants的关系应该是多对多吧?

#22楼 @chrisloong 谢谢,看过了,不错啊。但是我们希望使用SaaS平台作为业务平台。你们目前没有把平台主要框架开源的计划吧?

#24楼 @chrisloong 我也遇到ruby_sky同样的问题。MacBookPro Retina, Chrome V41

#28楼 @dapic 感谢反馈。这个功能已改进,预计下周更新。

#27楼 @dapic 对于SaaS,开放API来定制业务流程,比开源代码更重要。😄

#25楼 @stephen 是的,图画错了

#28楼 @dapic 可以给个联系方式呗,关于spree的一些事情跟你聊一下

#22楼 @chrisloong 你们放弃spree,改用什么啦?

#7楼 @zhaozijie 想找你帮忙,能给个联系方式吗?我的邮箱caiqinghua@126.com

#16楼 @dapic 能解释一下怎么绕过去吗? preference :send_core_emails, :boolean, default: true,配置里面可以关闭发email

#13楼 @scriptfans 请问能否教一下,spree这个gem是如何组合的,我发现在安装spree这个gem后,查看他们源码的文件夹,下面居然只有lib文件夹,spree.gemspec文件也没有,而spree_api这个gem的Gemfile中spree_core的位置也是指向‘../core’,而这些gem也能正常运行,请问这是怎么实现的。

文章最后的连接进不去了, 能否提供新连接? @scriptfans @zhaozijie

@dapic 后来进行的怎么样了,有上线了么

不错不错,多谢文章,现在是2018年3月,我在了解 Spree 的整体情况。是否合适需求,能否拿来用。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册