分享 Spree 源码导读

zhaozijie · 2015年03月05日 · 最后由 1c7 回复于 2018年03月14日 · 19088 次阅读
本帖已被管理员设置为精华贴

最近在研究 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 不仅支持正常的用户下单操作,还能由管理员在后台直接生成订单;另外就是考虑到了线上、线下两种支付情况。

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

赞,期待楼主继续贡献

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 想找你帮忙,能给个联系方式吗?我的邮箱 [email protected]

#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 的整体情况。是否合适需求,能否拿来用。

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