会员积分体系设计
前言
积分体系是对用户行为的一种反馈,本质是用户忠诚度计划的一部分。能够一定层面上解决用户的 “促活、留存” 问题。当然,最重要的还是看公司有多少资源投入。下面讲下如何设计一个比较基础的通用会员积分体系。
需求场景分析
- 是否对所有用户(新用户和老用户)?
- 积分的使用。可否当现金?兑换商品?
- 荣誉徽章制度?
- 如何获得积分?
……
我们可以设计出以下基础表。
- 用户积分账户(不考虑在原有的用户增加字段,而设计新表。是因为这是一个比较独立的系统。后期可能会加一些其他需求)
- 积分记录表(记录用户积分变动流水)
- 用户签到表
- 奖品(商品)表
- 兑换 (购买) 订单表
- 任务表
- 完成任务记录表
用户积分表,上一篇的用户钱包表有做sign验证,这里就不验证了。因为积分相对于金钱来说,安全要求没那么高。
CREATE TABLE `user_score` (
`user_uuid` char(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户id',
`level` tinyint(3) NOT NULL DEFAULT '1' COMMENT '等级',
`total_score` int(11) NOT NULL DEFAULT '0' COMMENT '总积分',
`usable_score` int(11) NOT NULL DEFAULT '0' COMMENT '可用积分',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='用户积分账户表'
记录积分流水信息。这里可以用一个字段的正负值来表示获得和使用积分。也可以用一个类型字段表示获得和支出
CREATE TABLE `score_record` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'auto id',
`user_uuid` char(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户id',
`source_id` int(11) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '来源id',
`source_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1 任务 2 系统赠送 3 取消订单 4 签到 5 兑换奖品 6过期',
`exprie_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否过期0 没过期 1 过期',
`remark` varchar(64) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注',
`symbol` varchar(3) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'in 收入 out 支出',
`score` int(11) NOT NULL DEFAULT '0' COMMENT '分值',
`expire_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '过期时间',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `UCID` (`user_uuid`,`create_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='积分记录表'
前端纪录表。
CREATE TABLE `user_sign_in` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'auto id',
`user_uuid` char(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户id',
`continue_day` smallint(6) NOT NULL DEFAULT '0' COMMENT '持续签到天数',
`period_day` tinyint(3) NOT NULL DEFAULT '0' COMMENT '周期内签到天数',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `UCID` (`user_uuid`,`create_at`),
KEY `create_at` (`create_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='用户签到表'
奖品表。
CREATE TABLE `prize` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'auto id',
`score` int(11) NOT NULL DEFAULT '0' COMMENT '所需积分',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
`name` char(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '名称',
`img_url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片url',
`introduction` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '简介',
`content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0 草稿 1 发布 2 停用',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`action_user` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作人',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='奖品表'
奖品订单表。这里的状态值。可以按系统其它表中的设计。
CREATE TABLE `prize_order` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'auto id',
`user_uuid` char(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户id',
`prize_id` int(11) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '奖品id',
`deduct_score` int(11) NOT NULL DEFAULT '0' COMMENT '扣除积分',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 待审核 1 经审核 2 不通过,3取消',
`audit_at` datetime DEFAULT NULL COMMENT '审核时间',
`remark` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注',
`action_user` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作人',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `user_uuid` (`user_uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='奖品兑换订单表'
获取积分的方式有很多种。所以增加了一张任务表,
CREATE TABLE `task` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'auto id',
`name` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '简体名称',
`score` int(11) NOT NULL DEFAULT '0' COMMENT '分值',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0 草稿 1 发布 2 停用',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`is_pro` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 pro专属',
`is_once` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 一次性任务',
`link_params` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '任务链接参数',
`page_url` varchar(64) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '页面',
`thumb_url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图标',
`identify` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标识符',
`remark` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注',
`action_user` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作人',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `identify` (`identify`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='任务表'
任务完成表
CREATE TABLE `task_finish` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'auto id',
`user_uuid` char(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户id',
`task_id` int(11) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '任务Id',
`score_uuid` int(11) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '积分记录id',
`finish_score` int(11) NOT NULL DEFAULT '0' COMMENT '完成获得积分',
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `UCID` (`user_uuid`,`create_at`),
KEY `UTCID` (`user_uuid`,`task_id`,`create_at`),
KEY `create_at` (`create_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='完成记录表'
以上表就是基础表了。可以根据自己的业务特点增删一些字段,或增加一些其它表
其它
用户等级积分关系通常不会改变,因此这里就不放在数据库中了。直接写在 php 文件里
//等级与积分映射。可根据实际情况填写
public $levelScoreMap = [
'1' => 0,
'2' => 100,
'3' => 300,
'4' => 400,
'5' => 500,
'6' => 750,
'7' => 1200,
'8' => 1800,
'9' => 2500,
'10' => 3500,
'11' => 5000,
'12' => 8000,
'13' => 15000,
'14' => 20000,
'15' => 30000,
'16' => 45000,
];
//七天连续签到天数与获得积分映射。可根据实际情况填写
public $signInScoreMap = [
'1' => 5,
'2' => 5,
'3' => 5,
'4' => 15,
'5' => 5,
'6' => 5,
'7' => 30,
];
/**根据用户的积分获取用户等级
* @param $levelScoreMap
* @param $levelArray
* @param $totalScore
* @return int
*/
public function getLevel($totalScore){
$levelScore = $this->levelScoreMap;
krsort($levelScore);
foreach ($levelScore as $level => $score) {
if ($totalScore >= $score) {
return $level;
}
}
}
思考
任务系统和积分系统最好拆分开来。
这么做的原因有几个,一来是因为,任务系统和积分系统未必属于同一个业务层,比如,任务系统的支持很可能是别的更偏向 C 端的团队在做,而积分系统更加偏向基础功能团队在做。即使是小项目,也最好分开来,毕竟积分的来源可能不止签到,还有可能是别的什么任务,签到后期的奖励也可能从积分改成其他。二是任务系统积分系统向来是用户和喜欢薅羊毛的地方,高峰期的服务器压力会比较大,处理任务和积分的逻辑太复杂的话会严重影响用户体验甚至宕机,所以我个人是比较推荐业务上拆开来的做法,任务系统单独处理逻辑,处理完成后塞一个消息进 MQ/Redis,其他系统以异步队列的方式去消费队列,处理发放积分或者其他额外的操作的逻辑。并发问题。
这个问题应该是很多场景下要考虑的问题。有加积分就有消耗积分,要是这两个动作同时了怎么办?如果羊毛党水平比较高抓到了签到、任务的接口直接调接口一下子发送多个请求怎么办?这自然是要加分布式锁了,当然这也是个学问,锁的粒度上我们也吃过亏,任务上要加锁,积分上也是要加锁的,数据库上顺便也来个乐观锁吧。。。这个就看具体的业务需求上各自大显神通的,不过这个大体上的思路应该是没啥问题的。对账问题。
这个问题是基于第一个问题产生的,系统间服务调用的一个基本原则就是互不信任,任务系统和积分系统拆分后,如何保证任务系统期望发放的积分跟积分中心实际发放的积分是一致的呢?如果说这中间任意一个系统出问题了怎么办?那就对账吧,明细账得对,总账也得对,对账的周期可以安排每天对一次明细,每月对一次总账。也是看具体业务需求而定了,实现上也很灵活,我们之前的方式是每天生成前一天的明细 CSV 文件,rsync 到积分系统的机器上,由积分系统对账并发送邮件报告对账结果,对账出问题了基本上是很高级别的故障等级了,双方基本上都是要第一时间找原因并修复的,补发或者扣除用户的积分。反作弊问题。
积分是薅羊毛重灾区大家应该都有所耳闻,那反作弊自然也是要的,提高任务和积分的门槛,让普通用户能够正常收益而屏蔽掉作弊账号。这一点一般的小公司就比较不太好做了,一来技术上没有接触过自然是没有思路,二来也没有足够的人力物力来维护一个这样的系统。由具体业务引出的其他问题。
比如:一个电商项目里如果积分可以用来抵扣订单的一部分费用,但是用户没有提交订单的时候这部分订单不算被消费,那么 “锁定 / 冻结积分” 这个操作应该怎么实现呢?如果需要实现积分的部分过期又怎么实现呢?如果在积分部分过期的基础上用户退款的积分需要返还这又怎么实现呢?
欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 ljheiseberg@163.com
文章标题:会员积分体系设计
文章字数:2.9k
本文作者:LJHeisenberg
发布时间:2021-02-28, 00:54:44
最后更新:2021-04-27, 10:55:27
原始链接:https://ljheisenberg1072.github.io/2021/02/28/design-of-membership-points-system/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。