近期看了idilent的文章《使用面向对象技术解决商品打折问题》,文后有读者提出要求:如果不同商品的折扣不同怎么办? 或者有买一百送五十这种方式,或不同会员等级的折扣不同。 怎么处理?”idilent认为打折这个问题并不是能够通过一个数据库的字段就可以解决的。有不同的会员,不同的产品,不同的销售计划,而这些也是在不停的不变化和增加的。而会员和产品的打折,以及店庆等打折,虽然都是折扣,但是很难抽象成数据库中的一个字段或者几个字段,不使用程序解决,而希望只是通过改变数据库中的数据,在目前阶段实现起来可能还比较困难。
之前我曾参与过一个影碟出租销售管理系统的项目开发,负责其中的架构设计和数据建模工作,尽管最后该项目由于某些客观原因而被放弃,但是该项目中也有打折优惠这方面的功能需求,我也思考过这一块的数据建模。其实,我们可以把商品销售打折这样的商务规则分解成几个部分,分析各个部分之间的关系,从中找出关键点,再将其泛化数据建模,即可实现让用户自己定义打折规则。下面开始分析商品销售打折的商业规则:
一套商品销售管理信息系统,必定存在下面两个实体:顾客,商品,打折这种商业规则一定是围绕着这两个实体以及相互间的关系而制定的。回顾我们的购物经历,打折的需求应该可以分为三种:
1)对特定商品的折扣一般有如下几种情况:按售价进行一定百分比的打折;原价->特价(某个时间段内进行的)的打折;捆绑优惠销售(如购买某一(几)种商品后即可按较低的价格或折扣购买另一(几)种商品)。
2)对顾客的的打折方式一般采用会员制,即是按会员等级在交费时直接给与一定的折扣优惠,或者在会员累积消费一定金额后给以一定比例的返点优惠,该方式需要顾客办理会员卡之类的身份标识卡。
3)对总额的打折方式一般是顾客消费后送出的限定最后使用期限的代金券。
注意到上面三大类折扣方式分解开来,都离不开商品,所以这三种打折方式都是商品的共有属性,应该归入到商品表中。另外,一般大型超市多拥有多个分店,而且可能出现各个分店的打折规则略有不同的情况,上面的三种打折规则得同相应的分店一一对应。最后,数据建模大致如下:
店铺表(shop)
名称 类型 约束条件 说明
shop_id int 无重复 店铺标识,主键
shop_name varchar(40) 不允许为空 店铺名称
shop_addr varchar(80) 不允许为空 店铺地址
……
商品类别表(ware_type)
名称 类型 约束条件 说明
type_id int 无重复 商品类别标识,主键
type_name varchar(20) 不允许为空 商品类别名称
father int 不允许为空 该类别的父类别标识,如果是顶节点的话设定为某个唯一值
layer char(6) 限定3层,初始值为000000 类别的先序遍历,主要为减少检索数据库的次数
商品表(ware)
名称 类型 约束条件 说明
ware_id int 无重复 商品标识,主键
ware_type int 不允许为空 所属商品类别,和ware_type.type_id关联
ware_name varchar(40) 不允许为空 商品名称
buy_price float 不允许为空 进货价
sell_price float 不允许为空 销售价
d_type char(1) 不允许为空 商品打折方式
m_type char(1) 不允许为空 会员打折方式
has_coupon bit 默认值为0 是否有代金券
……
说明:
①d_type用来辨别该商品的商品打折方式。”0″表示该商品无商品折扣方式;”1″表示该商品采用百分比打折方式;”2″表示该商品采用特价打折方式;”3″表示该商品采用捆绑打折方式,是捆绑打折规则中的必购商品;”4″表示该商品采用捆绑打折方式,是捆绑打折规则中的允购商品。
②m_type用来辨别该商品的会员打折方式。”0″表示该商品不参与会员折扣计算;”1″表示该商品采取会员百分比折扣方式;”2″表示该商品采取会员卡累积消费返点折扣方式。
③has_coupon用来指明该商品是否有代金券。”0″表示该商品无代金券;”1″反之。
商品库存表(store_table)
名称 类型 约束条件 说明
store_id int 无重复 库存标识,主键
shop_id int 不允许为空 店铺标识,和shop.shop_id关联
ware_id int 不允许为空 商品标识,和ware.ware_id关联
number int 默认值为0 店铺库存数量
unit varchar(10) 不允许为空 销售单位
商品折扣规则表(discount)
名称 类型 约束条件 说明
id int 无重复 折扣规则标识,主键
s_id int 不允许为空 店铺标识,和shop.shop_id关联
w_id int 不允许为空 商品标识,和ware.ware_id关联
d_value float 不允许为空 打折数值,用来记录百分比或特价
enddate datetime 不允许为空 该规则的终止日期
number int 允许为空 该规则所允许的最大销量
unit varchar(10) 允许为空 销售单位
商品捆绑打折表(bind_discount)
名称 类型 约束条件 说明
b_id int 无重复 捆绑打折规则标识,主键
shop_id int 不允许为空 店铺标识,和shop.shop_id关联
1st_ware int 不允许为空 必购的商品标识的集合,和ware.ware_id关联
min_req int 默认值为1 最小必购数量
2nd_ware int 不允许为空 允购的商品标识的集合,和ware.ware_id关联
max_buy int 默认值为1 最大允购数量
d_type char(1) 不允许为空 打折方式,是百分比方式还是特价方式
d_value float 不允许为空 打折数值,用来记录百分比或特价
enddate datetime 不允许为空 该规则的终止日期
number int 允许为空 该规则所允许的最大销量
unit varchar(10) 允许为空 销售单位
说明:1st_ware用来记录必购商品的集合,min_req表示在必购商品集合内的最小购买数量。2nd_ware用来记录允购商品的集合,max_buy表示达到必购商品的最小购买数量后,所允许购买的允购商品的最大允购数量。举例说明:某捆绑销售规定,凡是购买了某系列商品中的任意1件,即可按特价购买允购商品中的任意1款1件。这种促销方式大家都见过吧?买二送一不过是其中的特例罢了。
会员等级表(member_type)
名称 类型 约束条件 说明
type_id int 无重复 会员等级标识,主键
s_id int 不允许为空 店铺标识,和shop.shop_id关联
type_name varchar(10) 不允许为空 会员等级名称
t_value float 不允许为空 打折百分比或累积消费返点数
condition float 不允许为空 达到该等级所需累积的消费额
会员表(member)
名称 类型 约束条件 说明
m_id int 无重复 会员标识,主键
m_name varchar(10) 不允许为空 会员姓名
type_id int 不允许为空 会员等级标识,和member_type.type_id关联
score float 默认值为0 会员累积的消费积分
……
代金券表(coupon)
名称 类型 约束条件 说明
c_id int 无重复 代金券标识,主键
c_name varchra(20) 不允许为空 代金券姓名
c_value float 不允许为空 代金数额
condition float 不允许为空 所需现金消费
enddate datetime 不允许为空 代金券的终止日期
当然,由于本人所认知的打折方式并不全面,也没有和相关的业务人士深入讨论过这方面的问题。所以,上面的数据建模并不能保证覆盖现实商品销售中的的所有打折方式。不过,我相信,采用上面的数据建模来定义打折规则,覆盖率还是在90%以上的。根据95/5规则,只要给我足够的时间,再加上专业人士的协助,不计开发成本的话,100%的覆盖率是可以达到的^-^
最后,由于每张购物清单都是由商品组成,而每种商品的折扣的计算规则并不一定完全相同,所以我认为在用面向对象的设计方法,设计计算折扣的组件时,采用装饰(decorator)模式比较适合。