首页 积分体系设计
文章
取消

积分体系设计

积分体系设计的必要性

互联网平台积分体系是一个独立、完整的系统模块。

主要用于激励和回馈用户在平台的消费行为和活动行为,通过积分体系可以激发与引导用户在平台的活跃行为,逐步形成用户对平台的依赖性和习惯性,提升用户对平台的黏度,提高用户和平台的交易频率。

积分体系在保持系统独立性的同时,又与平台会员系统、商品系统、订单系统等具有紧密的关联性,因此积分体系的规划设计需与平台其他系统模块同时设计开发。

如何进行积分体系设计

已知积分体系在互联网的必要性,那么该如何设计积分体系呢?

需求分析

以我目前设计的智慧停车管理系统为例,有下列原始业务:

  1. 用户能查询到积分的赚取/消费明细,像下面这样:

  1. 用户可查询自己的可用积分。
  2. 用户每次会消费最快到期的积分。

  3. 用户能使用积分兑换优惠券、洗车券和活动商品,比如:

  1. 用户预约车位成功,系统奖励积分。
  2. 用户成功注册账户后,系统奖励积分。
  3. 用户充值成功付费会员后,系统奖励积分。
  4. 用户使用积分进行活动抽奖。
  5. 用户每日签到,系统奖励积分。

基于这些原始业务,大致可以分为两类:赚取积分和消费积分。并且根据用户行为应该给予多少积分奖励、行为次数是否有上限、下单金额需要满多少、积分抵扣金额时需要满多少以及积分的有效期等等,这些一律称为积分规则。比如:用户成功注册账号,系统奖励积分 100,积分有效期 7 天。

系统设计

已知积分体系与会员系统、商品系统、订单系统等其他系统具有紧密的关联性,那么该如何做积分体系的系统设计呢?

所谓系统设计实际上就是将合适的功能放到合适的模块中,并确定合适的模块交互关系。合理地划分模块也可以做到模块层面的高内聚、低耦合,架构整洁清晰。

以上面提出的原始需求为根据主要分为下列功能:

  1. 积分交易明细管理,包括赚取和消费。
  2. 用户行为管理,依据运营目标的不同,对不同的用户行为有不同的积分规则。
  3. 积分规则管理,包含赚取规则和消费规则。

具体的划分和模块之间的关系如下:

积分规则和用户行为的管理和维护不划分到积分系统中,而是放到更上层的营销系统中。这样积分系统就会变得非常简单,能够进行 CRUD 管理就够了。在这种方式下系统的架构大致如下:

最终营销系统通过上层服务传入的参数:

  1. 行为ID用与获取积分规则
  2. 事件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;

我为该积分体系总共设计了四张表:

  1. cre_transaction,积分交易明细表,用与记录和查询用户的积分交易流水。
  2. cre_available,可用积分表,用于查询用户可用积分。
  3. pro_user_behavior,用户行为表,该表主要用于穷举业务系统中用户可能存在的行为。
  4. 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();
    }

当上层服务调用营销系统时,由此函数进行处理:

  1. 首先判断是赚取行为/消费行为
  2. 在判断积分计算公式:面额相加/比例相乘
  3. 然后根据具体的规则计算产生的积分交易
  4. 由积分系统生成积分交易

当然目前的实现不易扩展,如果存在业务变更代码绝对需要重构,甚至表结构也需要重新设计,但这是目前我能想出的最佳设计了。

积分体系设计本身是非常复杂的,要设计出一个完整的有效的高大上的体系牵涉很多相关的考虑。但是就我当前的业务系统的体量,这样设计就够了。实际上真要说满足业务需求,我现在的设计已经算是过度设计了。

参考链接

  1. 电商平台-会员积分系统的设计与架构
  2. 带过期时间的积分系统表设计
本文由作者按照 CC BY 4.0 进行授权

职责链模式

字典项自动翻译器