积分体系设计的必要性
互联网平台积分体系是一个独立、完整的系统模块。
主要用于激励和回馈用户在平台的消费行为和活动行为,通过积分体系可以激发与引导用户在平台的活跃行为,逐步形成用户对平台的依赖性和习惯性,提升用户对平台的黏度,提高用户和平台的交易频率。
积分体系在保持系统独立性的同时,又与平台会员系统、商品系统、订单系统等具有紧密的关联性,因此积分体系的规划设计需与平台其他系统模块同时设计开发。
如何进行积分体系设计
已知积分体系在互联网的必要性,那么该如何设计积分体系呢?
需求分析
以我目前设计的智慧停车管理系统为例,有下列原始业务:
- 用户能查询到积分的赚取/消费明细,像下面这样:
- 用户可查询自己的可用积分。
用户每次会消费最快到期的积分。
- 用户能使用积分兑换优惠券、洗车券和活动商品,比如:
- 用户预约车位成功,系统奖励积分。
- 用户成功注册账户后,系统奖励积分。
- 用户充值成功付费会员后,系统奖励积分。
- 用户使用积分进行活动抽奖。
- 用户每日签到,系统奖励积分。
基于这些原始业务,大致可以分为两类:赚取积分和消费积分。并且根据用户行为应该给予多少积分奖励、行为次数是否有上限、下单金额需要满多少、积分抵扣金额时需要满多少以及积分的有效期等等,这些一律称为积分规则。比如:用户成功注册账号,系统奖励积分 100,积分有效期 7 天。
系统设计
已知积分体系与会员系统、商品系统、订单系统等其他系统具有紧密的关联性,那么该如何做积分体系的系统设计呢?
所谓系统设计实际上就是将合适的功能放到合适的模块中,并确定合适的模块交互关系。合理地划分模块也可以做到模块层面的高内聚、低耦合,架构整洁清晰。
以上面提出的原始需求为根据主要分为下列功能:
- 积分交易明细管理,包括赚取和消费。
- 用户行为管理,依据运营目标的不同,对不同的用户行为有不同的积分规则。
- 积分规则管理,包含赚取规则和消费规则。
具体的划分和模块之间的关系如下:
积分规则和用户行为的管理和维护不划分到积分系统中,而是放到更上层的营销系统中。这样积分系统就会变得非常简单,能够进行 CRUD 管理就够了。在这种方式下系统的架构大致如下:
最终营销系统通过上层服务传入的参数:
- 行为ID用与获取积分规则
- 事件ID可用于获取交易额(有些可能仅仅起到追溯和记录的作用)然后计算出赚取/消费的积分交给积分系统进行管理。
当然还有其他方式,但都不如这种方式优秀。
数据库设计和面向对象设计实现
下面给出整个积分体系的表设计:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
-- 库表通用字段
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新日期',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`version` int(4) DEFAULT '0' COMMENT '版本',
`state` int(4) DEFAULT '1' COMMENT '状态'
-- ----------------------------
-- Table structure for cre_transaction
-- ----------------------------
DROP TABLE IF EXISTS `cre_transaction`;
CREATE TABLE `cre_transaction` (
`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户id',
`channel_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户行为id',
`event_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '事件id(如订单id、评论id、优惠券换购交易id)',
`credit` decimal(7, 2) DEFAULT NULL COMMENT '积分(赚取为正,消费为负)',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '积分赚取/消费时间',
`create_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '创建人',
`version` int(4) NOT NULL DEFAULT 1,
`state` int(4) NOT NULL DEFAULT 1,
`expired_time` timestamp(0) DEFAULT NULL COMMENT '积分过期时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '积分明细表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for pro_points_rule
-- ----------------------------
DROP TABLE IF EXISTS `pro_points_rule`;
CREATE TABLE `pro_points_rule` (
`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
`behavior_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '行为ID',
`points` decimal(7, 2) DEFAULT NULL COMMENT '积分面额',
`exchange_ratio` decimal(4, 3) DEFAULT NULL COMMENT '兑换比例(有正负)',
`formula_mode` int(4) DEFAULT 1 COMMENT '计算模式(1面额、2兑换比例,默认面额)',
`spend_lower_limit` decimal(7, 2) DEFAULT NULL COMMENT '消费金额下限(积分赚取规则)',
`balance_lower_limit` decimal(7, 2) DEFAULT NULL COMMENT '积分余额下限(积分消费规则)',
`expired` int(4) DEFAULT NULL COMMENT '约定过期时间',
`time_upper_limit` int(4) DEFAULT NULL COMMENT '每日行为次数上限',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '积分规则表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for pro_user_behavior
-- ----------------------------
DROP TABLE IF EXISTS `pro_user_behavior`;
CREATE TABLE `pro_user_behavior` (
`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
`type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '行为类别',
`behavior` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '具体行为',
`points_type` int(4) DEFAULT NULL COMMENT '积分类型(1赚取,2消费,默认赚取)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户行为表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for cre_available_points
-- ----------------------------
DROP TABLE IF EXISTS `cre_available_points`;
CREATE TABLE `cre_available_points` (
`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
`user_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户ID',
`add_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '赚取积分ID',
`points` decimal(7, 2) DEFAULT NULL COMMENT '积分',
`expired_time` timestamp(0) DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '可用积分表' ROW_FORMAT = Dynamic;
我为该积分体系总共设计了四张表:
- cre_transaction,积分交易明细表,用与记录和查询用户的积分交易流水。
- cre_available,可用积分表,用于查询用户可用积分。
- pro_user_behavior,用户行为表,该表主要用于穷举业务系统中用户可能存在的行为。
- pro_points_rule,积分规则表,计入积分赚取/消费的规则。
下面是我在设计以上四张表的经验:
经验1
通过 cre_transaction,也可以计算用户的积分可用余额,那么为什么还要增加 cre_available 呢?
在实际的业务系统开发中仅有一张积分交易明细表是远远不够的,比如在没有可用积分表之前,查询用户可用积分是下面这样的:
1
2
3
4
5
6
7
SELECT
sum( credit )
FROM
cre_transaction
WHERE
user_id = #{userId}
AND (expired_time > CURRENT_TIME or expired_time is null);
expired_time > CURRENT_TIME 是查询未过期得赚取积分,而 expired_time 查询的是所有得消费的积分,照这种算法,过期的积分就相当于没被抵扣过,所以可用积分肯定被多扣了。实际上一张表本就很难模拟积分的增减情况。
所以新增一张可用积分表用于记录积分的增减情况是很有必要的,该表是如何操作的呢?
每次赚取积分同时往 cre_transaction 和 cre_available 中添加一条记录,每次消费积分时除了记录交易明细,同时需要更新 cre_available 中的最快过期积分(最快过期积分 - 消费积分),直到消费积分被扣完。这样就能实现查询用户可用积分和每次消费最快过期积分两个功能。
经验2
积分规则存在消费/赚取规则,实际上对于更加复杂的积分体系,每种商品,每种活动,每个用户都会有对应的个性化的积分规则,那么这样将所有规则记录在同一张表中合适吗?
实际上最开始我是设计了两张表的:赚取规则和消费规则,但因为在编码实现当中比较冗余,实现起来有点麻烦,因为分为两张表后在进行积分操作时,营销系统需要调用两个不同的接口,entity 也是不同的。这就造成营销系统需要两套针对的实现。或多或少有些麻烦。同时因为我的业务系统本身没有那么复杂,所以更不会有个性化的积分规则。因此这样设计是最符合实际业务的。
经验3
实际上只有一张积分规则表就可以进行积分的兑换操作了,为什么还要增加用户行为表进行辅助?
假设只有 pro_points_rule 表,那么对于用户的不同操作(在代码中则是一个个接口),我们需要将规则ID硬编码在实现当中,但是与用户行为不同的是规则是会发生改变的,一旦规则改变就需要修改代码。而增加 user_behavior,由于用户行为对于业务系统是不变的,在用户行为中关联规则ID,能方便规则的维护,也方便规则发生改变。例如下面是我的营销系统入口代码实现:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public String promoteEntryOfPoints(String userId, String channelId, String eventId) {
//查询用户行为
ProUserBehavior userBehavior = findById(channelId);
//查询用户行为对应的积分规则
ProPointsRule rule = pointsRuleService.getOneByBehaviorID(channelId);
//添加积分记录
CreTransaction trade = new CreTransaction();
if (userBehavior.getPointsType() == POINTS_TYPE_EARN){
if (rule.getFormulaMode() == FORMULA_MODE_FACE_AMOUNT){
// TODO: 2023/3/1 赚取面额有次数上限,如果 timeUpperLimit != null,应该进行限制,其中用户行为次数可以用 Redis 进行统计
if (rule.getTimeUpperLimit() != null){
int times = 5;
if (times > rule.getTimeUpperLimit()){
//不提供积分服务,返回空字符串
return "";
}
}
trade.setCredit(rule.getPoints());
}else if (rule.getFormulaMode() == FORMULA_MODE_RATIO){
// TODO: 2023/3/1 赚取比例=消费额度*比例,消费额度 >= spendLowerLimit
BigDecimal spend = new BigDecimal(100);
if (spend.compareTo(rule.getSpendLowerLimit()) < 0){
//不提供积分服务,返回空字符串
return "";
}
trade.setCredit(rule.getExchangeRatio().multiply(spend));
}
trade.setExpiredTime(LocalDateTime.now().plusDays(rule.getExpired()));
}else {
if (rule.getFormulaMode() == FORMULA_MODE_FACE_AMOUNT){
trade.setCredit(rule.getPoints());
}else if (rule.getFormulaMode() == FORMULA_MODE_RATIO){
BigDecimal available = availablePointsService.getAvailable(userId);
if (available.compareTo(rule.getBalanceLowerLimit()) < 0){
//不提供积分服务,返回空字符串
return "";
}
trade.setCredit(available.multiply(rule.getExchangeRatio()));
}
}
trade.setUserId(userId);
trade.setChannelId(channelId);
trade.setEventId(eventId);
transactionService.create(trade);
return trade.getId();
}
当上层服务调用营销系统时,由此函数进行处理:
- 首先判断是赚取行为/消费行为
- 在判断积分计算公式:面额相加/比例相乘
- 然后根据具体的规则计算产生的积分交易
- 由积分系统生成积分交易
当然目前的实现不易扩展,如果存在业务变更代码绝对需要重构,甚至表结构也需要重新设计,但这是目前我能想出的最佳设计了。
积分体系设计本身是非常复杂的,要设计出一个完整的有效的高大上的体系牵涉很多相关的考虑。但是就我当前的业务系统的体量,这样设计就够了。实际上真要说满足业务需求,我现在的设计已经算是过度设计了。