会员积分体系设计

前言

积分体系是对用户行为的一种反馈,本质是用户忠诚度计划的一部分。能够一定层面上解决用户的 “促活、留存”问题。当然,最重要的还是看公司有多少资源投入。下面讲下如何设计一个比较基础的通用会员积分体系。

需求场景分析

1,是否对所有用户(新用户和老用户)?
2,积分的使用。可否当现金?兑换商品?
3,荣誉徽章制度?
4,如何获得积分?



我们可以设计出以下基础表。
1,用户积分账户(不考虑在原有的用户增加字段,而设计新表。是因为这是一个比较独立的系统。后期可能会加一些其他需求)
2,积分记录表(记录用户积分变动流水)
3,用户签到表
4,奖品(商品)表
5,兑换(购买)订单表
6,任务表
7,完成任务记录表

    用户积分表,上一篇的用户钱包表有做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;
        }
    }
}

以上就是关于用户积分系统的设计了,不足的地方请评论区留言,共同探讨,一起进步。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 3年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 8

在上家和上上家公司都深度参与积分系统核心开发并上线,日活量级都在百万左右,自认为对于积分系统还是有些发言权的。就基础功能而言,楼主的这篇文章基本上是完成度很不错的了。但是实际商业应用上积分系统的重点其实还有很多可以讨论的空间。

  1. 任务系统和积分系统最好拆分开来。这个文章中已经有提现了,这么做的原因有几个,一来是因为,任务系统和积分系统未必属于同一个业务层,比如,任务系统的支持很可能是别的更偏向C端的团队在做,而积分系统更加偏向基础功能团队在做。即使是小项目,也最好分开来,毕竟积分的来源可能不止签到,还有可能是别的什么任务,签到后期的奖励也可能从积分改成其他。二是任务系统积分系统向来是用户和喜欢薅羊毛的地方,高峰期的服务器压力会比较大,处理任务和积分的逻辑太复杂的话会严重影响用户体验甚至宕机,所以我个人是比较推荐业务上拆开来的做法,任务系统单独处理逻辑,处理完成后塞一个消息进MQ/Redis,其他系统以异步队列的方式去消费队列,处理发放积分或者其他额外的操作的逻辑。

  2. 并发问题。这个问题应该是很多场景下要考虑的问题。有加积分就有消耗积分,要是这两个动作同时了怎么办?如果羊毛党水平比较高抓到了签到、任务的接口直接调接口一下子发送多个请求怎么办?这自然是要加分布式锁了,当然这也是个学问,锁的粒度上我们也吃过亏,任务上要加锁,积分上也是要加锁的,数据库上顺便也来个乐观锁吧。。。这个就看具体的业务需求上各自大显神通的,不过这个大体上的思路应该是没啥问题的。

  3. 对账问题。这个问题是基于第一个问题产生的,系统间服务调用的一个基本原则就是互不信任,任务系统和积分系统拆分后,如何保证任务系统期望发放的积分跟积分中心实际发放的积分是一致的呢?如果说这中间任意一个系统出问题了怎么办?那就对账吧,明细账得对,总账也得对,对账的周期可以安排每天对一次明细,每月对一次总账。也是看具体业务需求而定了,实现上也很灵活,我们之前的方式是每天生成前一天的明细CSV文件,rsync到积分系统的机器上,由积分系统对账并发送邮件报告对账结果,对账出问题了基本上是很高级别的故障等级了,双方基本上都是要第一时间找原因并修复的,补发或者扣除用户的积分。

  4. 反作弊问题。积分是薅羊毛重灾区大家应该都有所耳闻,那反作弊自然也是要的,提高任务和积分的门槛,让普通用户能够正常收益而屏蔽掉作弊账号。这一点一般的小公司就比较不太好做了,一来技术上没有接触过自然是没有思路,二来也没有足够的人力物力来维护一个这样的系统。

  5. 由具体业务引出的其他问题,比如:一个电商项目里如果积分可以用来抵扣订单的一部分费用,但是用户没有提交订单的时候这部分订单不算被消费,那么“锁定/冻结积分”这个操作应该怎么实现呢?如果需要实现积分的部分过期又怎么实现呢?如果在积分部分过期的基础上用户退款的积分需要返还这又怎么实现呢?

总之,积分系统往深了说其实挺挑战业务、技术以及一个开发思维的缜密性和沟通的逻辑性的,新手或者初中级工程师如果能够深度参与一个有质量的积分系统的研发,做完之后必定功力大增,钱途无量啊,美好的明天在等着你们!

3年前 评论
javalaw (作者) 3年前
reacti0n 3年前
0Robert0 3年前

赞一个:+1:

有个问题请教:签到和积分系统分开设计在会不会更灵活一些?签到除了得积分,可以有别的奖励。

3年前 评论
Euan (楼主) 3年前

先Mark 假设存在 “ 今年(2020) 获取的积分需要在明年2021.6.30过期,而在2021.1.1 - 2021.6.30之间获取的积分不过期 ” 这样的需求,貌似你这个没有考虑到吧 :smiley:

3年前 评论
Euan (楼主) 3年前
caijunduo (作者) 3年前
clyde-cn

Mark :+1:

3年前 评论

请问积分记录的 SourceID 是指?

3年前 评论
Euan (楼主) 3年前

在上家和上上家公司都深度参与积分系统核心开发并上线,日活量级都在百万左右,自认为对于积分系统还是有些发言权的。就基础功能而言,楼主的这篇文章基本上是完成度很不错的了。但是实际商业应用上积分系统的重点其实还有很多可以讨论的空间。

  1. 任务系统和积分系统最好拆分开来。这个文章中已经有提现了,这么做的原因有几个,一来是因为,任务系统和积分系统未必属于同一个业务层,比如,任务系统的支持很可能是别的更偏向C端的团队在做,而积分系统更加偏向基础功能团队在做。即使是小项目,也最好分开来,毕竟积分的来源可能不止签到,还有可能是别的什么任务,签到后期的奖励也可能从积分改成其他。二是任务系统积分系统向来是用户和喜欢薅羊毛的地方,高峰期的服务器压力会比较大,处理任务和积分的逻辑太复杂的话会严重影响用户体验甚至宕机,所以我个人是比较推荐业务上拆开来的做法,任务系统单独处理逻辑,处理完成后塞一个消息进MQ/Redis,其他系统以异步队列的方式去消费队列,处理发放积分或者其他额外的操作的逻辑。

  2. 并发问题。这个问题应该是很多场景下要考虑的问题。有加积分就有消耗积分,要是这两个动作同时了怎么办?如果羊毛党水平比较高抓到了签到、任务的接口直接调接口一下子发送多个请求怎么办?这自然是要加分布式锁了,当然这也是个学问,锁的粒度上我们也吃过亏,任务上要加锁,积分上也是要加锁的,数据库上顺便也来个乐观锁吧。。。这个就看具体的业务需求上各自大显神通的,不过这个大体上的思路应该是没啥问题的。

  3. 对账问题。这个问题是基于第一个问题产生的,系统间服务调用的一个基本原则就是互不信任,任务系统和积分系统拆分后,如何保证任务系统期望发放的积分跟积分中心实际发放的积分是一致的呢?如果说这中间任意一个系统出问题了怎么办?那就对账吧,明细账得对,总账也得对,对账的周期可以安排每天对一次明细,每月对一次总账。也是看具体业务需求而定了,实现上也很灵活,我们之前的方式是每天生成前一天的明细CSV文件,rsync到积分系统的机器上,由积分系统对账并发送邮件报告对账结果,对账出问题了基本上是很高级别的故障等级了,双方基本上都是要第一时间找原因并修复的,补发或者扣除用户的积分。

  4. 反作弊问题。积分是薅羊毛重灾区大家应该都有所耳闻,那反作弊自然也是要的,提高任务和积分的门槛,让普通用户能够正常收益而屏蔽掉作弊账号。这一点一般的小公司就比较不太好做了,一来技术上没有接触过自然是没有思路,二来也没有足够的人力物力来维护一个这样的系统。

  5. 由具体业务引出的其他问题,比如:一个电商项目里如果积分可以用来抵扣订单的一部分费用,但是用户没有提交订单的时候这部分订单不算被消费,那么“锁定/冻结积分”这个操作应该怎么实现呢?如果需要实现积分的部分过期又怎么实现呢?如果在积分部分过期的基础上用户退款的积分需要返还这又怎么实现呢?

总之,积分系统往深了说其实挺挑战业务、技术以及一个开发思维的缜密性和沟通的逻辑性的,新手或者初中级工程师如果能够深度参与一个有质量的积分系统的研发,做完之后必定功力大增,钱途无量啊,美好的明天在等着你们!

3年前 评论
javalaw (作者) 3年前
reacti0n 3年前
0Robert0 3年前

同楼上的问题

3年前 评论

continue_day smallint(6) NOT NULL DEFAULT '0' COMMENT '持续签到天数', period_day tinyint(3) NOT NULL DEFAULT '0' COMMENT '周期内签到天数',
这个值是不是 存的二进制值来表示?

3年前 评论

请问下任务链接参数的作用是什么

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!