积分系统的设计

需求分析

  • 积分赚取和兑换规则
    • 在电商平台消费可获得相应比例的积分
    • 假设比例为1:1(即每消费1元获得1积分)
    • 用户每日签到可获得一定积分
    • 评论上热门可获得一定积分
    • 平台可能不定期举办活动:消费获得额外积分,提高积分比例等
    • 积分有效期为31天
  • 积分消费和兑换规则
    • 在平台消费时可消费积分
    • 1积分可当作1分钱来使用(即100积分1块钱)
    • 也可直接通过积分换购优惠券
  • 积分及其明细查询
    • 用户可通过账号查询积分明细
    • 积分系统通过查询数据库中的信息来汇总返回给用户

系统设计

采用第二种划分方式:积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护,分散在各个相关业务系统中,比如订单系统、评论系统、签到系统、换购商城、优惠券系统等

用户下订单成功之后,订单系统根据商品对应的积分兑换比例,计算所能兑换的积分数量,然后直接调用积分系统给用户增加积分

上下层系统之间的调用通过接口实现,这种划分方式具有高内聚,低耦合的特点

数据库设计

积分明细表

Attribute Value
id 明细ID
user_id 用户ID
channel_id 赚取或消费渠道ID
event_id 相关事件ID,比如订单ID、评论ID、优惠券换购交易ID、平台活动ID
credit 积分(赚取为正值,消费为负值)
create_time 积分赚取或消费时间
expired_time 积分过期时间
1
2
3
4
5
6
7
8
9
create table credit_transaction(
id int PRIMARY KEY,
user_id int,
channel_id int,
event_id int,
credit int,
create_time date,
expired_time date
);

除此之外,还需要用户信息表和事件表

用户信息表除存放用户的基本信息外,还有用户当前可使用的积分

事件表中以事件ID为主键,存放事件名称,具体活动介绍,对积分兑换比例的影响,用户参与要求等

系统接口设计

系统结构

积分系统大致结构如下所示

1

分为Controller层、Service层和Repository层,每层都会定义相应的数据对象分别为VO、BO、Entity

每一层都由业务实体类,接口和接口实现类组成

整个系统由积分系统和多个子系统组成,如刚才所提到的订单系统、评论系统、签到系统、换购系统和优惠券系统等等。每个系统都有自己独立的功能,均可独立运行。各个系统通过调用积分系统的接口给用户增加积分

积分系统接口设计

接口设计符合单一职责原则,粒度越小通用性就越好。每个接口只负责实现一个功能,如下所示

  1. 赚取积分

其它系统通过调用此接口来实现给用户增加积分。给用户增加积分,并增加一条积分明细的记录

传入积分明细所需要的所有参数,返回积分明细的ID

1
2
3
4
5
6
7
8
9
10
11
12
public int addCredit(int userId,
int channelId,
int eventId,
int credit,
String expiredTime) {
/*
生成一个积分明细的id当做唯一标识
将传入的信息加上积分明细id存在数据库中
重新计算该用户当前可用的积分保存在用户信息中
保存成功返回积分明细的ID
*/
}
  1. 消费积分

用户在平台使用积分时,系统通过调用此接口来减少用户相应的积分,并增肌一条积分明细的记录

传入的参数与赚取积分接口相同,成功时同样返回积分明细的ID

1
2
3
4
5
6
7
8
9
10
11
12
13
public int subtractCredit(int userId,
int channelId,
int eventId,
int credit,
String expiredTime) {
/*
生成一个积分明细的id当做唯一标识
将传入的信息加上积分明细id存在数据库中
重新计算该用户当前可用的积分保存在用户信息中
保存成功返回积分明细的ID
优先减少即将过期的积分
*/
}
  1. 查询积分

用户可查询自己的积分余额,系统将调用此接口来查询该用户的积分余额。传入的参数是用户ID(userId),返回该用户当前总可用积分

1
2
3
4
5
6
7
8
public int getAvailableCredit(int userId) {
/*
通过用户ID查询用户当前总可用积分
在需求分析时规定积分有效期为31天
所以查询31天前到今天的所有积分明细记录
返回所得的总可用积分数量
*/
}
  1. 查询总/赚取/消费积分明细

除积分余额外,用户还可查询自己的总积分明细,也可仅查询赚取/消费的积分明细。系统会通过调用此接口来进行操作。传入参数用户ID(userId)以及分页数,返回若干条积分明细记录

1
2
3
4
5
6
7
8
9
public List<CreditVo> getAllCredit(int userId,int pageNumber);
public List<CreditVo> getExpenseCredit(int userId,int pageNumber);
public List<CreditVo> getIncomeCredit(int userId,int pageNumber);
/*
CreditVo为Controller层的业务实体类
用于向Controller层提供Entity实体类相应的功能及服务
getAllCredit: 查询总积分明细 返回符合要求的积分明细的列表 由前端接收并渲染为页面提供给用户
getExpenseCredit和getIncomeCredit同理,查询时增加对积分的约束,正值为赚取,负值为消费
*/

以上为积分系统接口的设计,这部分接口仅对外提供赚取积分、消费积分、查询积分或积分明细的功能

像通过一个订单的具体信息,按照比例得到积分,给哪一个用户增加积分是由订单系统完成

同理,签到系统、评论系统、换购系统等都有自己独立的一套代码逻辑,完成相应的功能

这样就具备了高内聚、低耦合的特点,并且维护了单一职责原则

同时也保证了此系统是由面向对象的编码风格的代码所实现的

订单系统接口设计

其它系统调用积分系统的接口原理相同,以订单系统为例

交易成功后生成订单,订单系统通过调用接口增加用户的积分

传入订单的信息,按照比例计算获得的积分,再调用积分系统开放的的接口

1
2
3
4
5
6
7
public class Bill {} // 存放订单的具体信息 其中主要包括用户ID、交易额、厂商ID、时间等信息
public addCreditAfterTransaction(Bill bill) {
// 按比例计算用户获得的积分
int credit=bill.amount*0.1;
// 调用接口 传入相应参数
CreditController.addCredit(bill.userId,bill.channelId ... );
}

评论、签到、换购等活动获得积分时同理

不过使用接口也会有一些问题,例如

  • 多次远程接口调用会影响性能
  • 多个小接口可能会涉及分布式事务的数据一致性的问题
  • 网络波动可能会导致信息的传输错误,延迟等问题

业务实体类

以积分明细为例,数据库中存放积分明细的表为credit_transaction

在积分系统各层中对应的实体类为VO(Controller中的CreditVo),BO(Service层中的CreditBo),Entity(Repository层中的CreditEntity)

每一层只会对各层自己的业务实体类进行操作,并不会影响其他层

各层的业务实体类除基本属性外还包括一些方法,更方便地与其他层进行交互,实现功能

为防止代码重复,可使用继承或组合来解决代码重复问题

设计时采用充血模型更为方便和高效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CreditBase {
/*
创建CreditBase基类来解决代码重复问题
各层需要的方法在各层对应的业务实体类中定义即可
CreditBase类中仅存放公共属性
*/
private int id; // Primary Key
private int userId;
private int channelId;
private int eventId;
private int credit;
private Date createTime;
private Date expiredTime;
}
/*
业务实体类中除基本属性外 还需要定义一些方法
例如Repository层将对象传给Service层时,需要将Entity对象转换为Bo对象,这个方法应定义在Entity类中
其他层之间的交互同理
*/
public class CreditEntity extends CreditBase {
private CreditBo Entity2Bo() {}
}
public class CreditBo extends CreditBase {
private CreditVo Bo2Vo() {}
}
public class CreditVo extends CreditBase {}

模块划分

系统模块划分如下图所示

2

分层设计说明

上面也提到过

积分系统分为Controller层、Service层和Repository层,每层都会定义相应的数据对象分别为VO、BO、Entity

每一层都由业务实体类,接口和接口实现类组成

总系统由积分系统和多个子系统组成,如刚才所提到的订单系统、评论系统、签到系统、换购系统和优惠券系统等等。每个系统都有自己独立的功能,均可独立运行。各个系统通过调用积分系统的接口给用户增加积分

分层能起到代码复用、隔离变化、隔离关注点的作用,同时提高了代码的可测试性以及能够应对系统的复杂性

3

设计原则

  1. 将系统划分为多个部分(积分系统以及其余子系统),符合高内聚、低耦合的原则。不同模块内部高内聚,模块之间低耦合,进而实现整个系统功能的正常运行,且系统分开时也可独立运行。
  2. 接口设计时符合单一职责原则,每一个接口的设计要尽量职责单一。同时分层的目的也是为了更加符合单一职责原则。可以在职责单一的细粒度接口之上,再封装一层粗粒度的接口给外部使用。这样从外部看起来使用了较少的接口,但实际上在系统内部仍是调用多个接口实现,所以仍然符合单一职责原则。
  3. 每一层都有接口及其实现类,用户可以使用相关功能,但不需要知道功能如何实现,即符合封装和抽象的原则,实现功能,封装细节。
  4. 使用面向对象语言Java以及面向对象的编码风格来实现积分系统,关注每个对象的功能而非整体的过程。每个对象仅关注自己的属性和方法,符合面向对象设计的原则。
  5. 各层业务实体类VO、BO、Entity存在代码重复,但实际上功能语义不同,可以通过继承或组合的方法,解决三者之间的代码重复问题,符合DRY原则,不造成语句或语义上的重复。