• 如何从0到1设计积分系统?


    比如:淘宝、京东等各大电商平台,都有积分系统,各大社区系统也有积分系统,就连想在大城市中小学读书,都有个积分的说法。

    在很多平台不叫积分,叫什么币,比如:金币、鱼币、喵币、京豆等。

    信用卡有信用积分、加油卡也有加油积分、......

    你也可以看看你用过的相关app、网站系统,基本上大多数都有这个积分的概念。

    很多游戏中,每天登陆也会送各种各样

    由此,我们能看出一个积分系统在各大平台中的重要性。

    积分体系连接用户与产品,能够有效引导用户成长,将新用户培养成高价值用户。

    前两天有同学和我聊,说电商项目中,能不能把营销系统和积分给拆分开。

    经过一番探讨后,觉得把积分系统单独出来。

    b73186c83a4b79394fc95223a42515cb.png

    核心功能

    但是再大再小,都必须有下面四个核心功能:

    • 增加积分

    • 扣减积分

    • 查询用户当前积分

    • 查询积分明细列表

    增加积分

    很多平台,通过各种各样的运营策略来给用户添加积分,比如:每天登录系统增加积分、购买商品增加积分、看短视频15秒增加积分等。

    积分还可以分类:

    • 换商品

    • 换优惠券

    • 换抽奖机会

    • ...

    扣减积分

    扣减积分,更多是说把积分兑换成商品、优惠券等,但也有积分到期了,需要扣减到期的积分。

    查询用户当前积分

    用户个人中心,通常都会展示自己当前积分。

    查询积分明细列表

    每次积分兑换商品了,会留下一条记录。每次积分兑换了抽奖,也会留下一条记录。....

    用户可以通过个人中心查看自己积分变化情况。

    代码实现

    为了演示,这里就把积分项目单独出来,成一个web项目(我的电商项目里只是一个module,服务间调用用的是dubbo+nacos),但是这个项目里就简单是一个单体项目,项目结构如下:

    02851782c77559e95d49878269350266.png

    整体业务实体关系(简单的演示):

    cdef2392608971e9939c436907a84e3e.png

    基于这个,我们来实现一版简单的积分系统。

    bf647c5b4ffc99d11c17813a559d115a.png

    建两张表:用户积分表和积分明细表

    1. CREATE TABLE `user_credit` (
    2.   `id` int NOT NULL AUTO_INCREMENT,
    3.   `user_id` int NOT NULL,
    4.   `credit` int NOT NULL DEFAULT '0',
    5.   PRIMARY KEY (`id`),
    6.   UNIQUE KEY `user_id_indx_uniq` (`user_id`)
    7. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    8. CREATE TABLE `credit_detail` (
    9.   `id` int NOT NULL AUTO_INCREMENT,
    10.   `user_id` int NOT NULL,
    11.   `type` int NOT NULL,
    12.   `number` int NOT NULL,
    13.   `order_no` varchar(255) NOT NULL,
    14.   `create_time` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP,
    15.   PRIMARY KEY (`id`),
    16.   UNIQUE KEY `order_no_index` (`order_no`)
    17. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

    mapper和entity这里就不展示了,这里我把controller和service贴出来:

    controller层代码:

    1. /**
    2.  * @author tianwc  公众号:java后端技术全栈、面试专栏
    3.  * @version 1.0.0
    4.  * @date 2022年11月27日 09:53
    5.  * 

    6.  * 用户积分
    7.  */
    8. @Slf4j
    9. @Api(tags = "用户积分")
    10. @Controller("/credit")
    11. public class UserCreditController {
    12.     @Resource
    13.     private UserCreditService userCreditService;
    14.     @ApiOperation(value = "通过用户id获取用户当前积分", notes = "通过用户id获取用户当前积分")
    15.     @ApiImplicitParam(name = "userId", value = "用户id", dataType = "int ")
    16.     @ArgsLogAnnotation(methodDescription = "通过用户id获取用户积分")
    17.     @GetMapping("/user")
    18.     @ResponseBody
    19.     public CommonResult findByUserId(Integer userId) {
    20.         return userCreditService.findByUserId(userId);
    21.     }
    22.     @ApiOperation(value = "新增用户积分信息(初始化)", notes = "新增用户积分信息")
    23.     @ApiImplicitParam(name = "userCreditDto", value = "初始化用户积分账号", dataType = "object")
    24.     @ArgsLogAnnotation(methodDescription = "通过用户id获取用户积分")
    25.     @PostMapping("/init")
    26.     @ResponseBody
    27.     public CommonResult init(@RequestBody UserCreditDto userCreditDto) {
    28.         return userCreditService.initUserCredit(userCreditDto);
    29.     }
    30.     @ApiOperation(value = "增加用户积分", notes = "增加用户积分")
    31.     @ApiImplicitParam(name = "modifyCreditDto", value = "增加用户积分", dataType = "object ")
    32.     @ArgsLogAnnotation(methodDescription = "增加用户积分")
    33.     @PostMapping("/add")
    34.     @ResponseBody
    35.     public CommonResult addCredit(@RequestBody @Validated ModifyCreditDto modifyCreditDto) {
    36.         try {
    37.             return userCreditService.addCredit(modifyCreditDto);
    38.         } catch (Exception e) {
    39.             log.error("增加用户积分失败", e);
    40.             return CommonResult.failed("增加用户积分失败");
    41.         }
    42.     }
    43.     @ApiOperation(value = "扣减用户积分", notes = "扣减用户积分信息")
    44.     @ApiImplicitParam(name = "modifyCreditDto", value = "扣减积分信息", dataType = "object")
    45.     @ArgsLogAnnotation(methodDescription = "扣减用户积分")
    46.     @PostMapping("/reduce")
    47.     @ResponseBody
    48.     public CommonResult reduceCredit(@RequestBody @Validated ModifyCreditDto modifyCreditDto) {
    49.         try {
    50.             return userCreditService.addCredit(modifyCreditDto);
    51.         } catch (Exception e) {
    52.             log.error("扣减用户积分失败", e);
    53.             return CommonResult.failed("扣减用户积分失败");
    54.         }
    55.     }
    56.     @ApiOperation(value = "积分明细", notes = "通过用户id获取用户积分明细")
    57.     @ArgsLogAnnotation(methodDescription = "通过用户id获取用户积分")
    58.     @GetMapping("/detail/list")
    59.     @ResponseBody
    60.     public CommonResult findCreditDetailListByUserId(Integer userId, Integer start, Integer pageSize) {
    61.         return userCreditService.findUserCreditList(userId, start, pageSize);
    62.     }
    63. }

    当用户信息创建时,同时初始化用户积分信息,用户后面的各种积分操作就可以展开了。

    service层代码实现:

    1. /**
    2.  * @author tianwc  公众号:java后端技术全栈、面试专栏
    3.  * @version 1.0.0
    4.  * @date 2022年11月27日 11:05
    5.  */
    6. @Service
    7. public class UserCreditServiceImpl implements UserCreditService {
    8.     @Resource
    9.     private UserCreditMapper userCreditMapper;
    10.     @Resource
    11.     private CreditDetailMapper creditDetailMapper;
    12.     @Override
    13.     public CommonResult findByUserId(Integer userId) {
    14.         UserCredit userCredit = userCreditMapper.selectByUserId(userId);
    15.         if (userCredit != null) {
    16.             AddUserCreditDto addUserCreditDto = new AddUserCreditDto();
    17.             addUserCreditDto.setCredit(userCredit.getCredit());
    18.             addUserCreditDto.setId(userCredit.getId());
    19.             addUserCreditDto.setUserId(userCredit.getUserId());
    20.             return CommonResult.success(addUserCreditDto);
    21.         }
    22.         return CommonResult.failed("查询用户积分失败");
    23.     }
    24.     @Override
    25.     public CommonResult initUserCredit(UserCreditDto userCreditDto) {
    26.         UserCredit userCredit = userCreditMapper.selectByUserId(userCreditDto.getUserId());
    27.         if (userCredit != null) {
    28.             return CommonResult.success("添加成功");
    29.         }
    30.         userCredit = new UserCredit();
    31.         userCredit.setUserId(userCreditDto.getUserId());
    32.         userCredit.setCredit(userCreditDto.getCredit());
    33.         int flag = userCreditMapper.insert(userCredit);
    34.         if (flag == 1) {
    35.             return CommonResult.success("添加成功");
    36.         }
    37.         return CommonResult.failed("添加失败");
    38.     }
    39.     /**
    40.      * 1、修改积分信息
    41.      * 2、新增明细
    42.      *
    43.      * @param modifyCreditDto 新增积分参数
    44.      * @return 添加成功 返回最新积分
    45.      */
    46.     @Transactional(rollbackFor = Exception.class)
    47.     @Override
    48.     public CommonResult addCredit(ModifyCreditDto modifyCreditDto) throws Exception {
    49.         UserCredit userCredit = userCreditMapper.selectByUserId(modifyCreditDto.getUserId());
    50.         if (userCredit == null) {
    51.             return CommonResult.failed("用户积分添加失败,userId=" + modifyCreditDto.getUserId() + " 有误");
    52.         }
    53.         userCredit.setCredit(userCredit.getCredit() + modifyCreditDto.getNumber());
    54.         //更新用户积分
    55.         if (checkUpdateCreditSuccess(userCredit)) {
    56.             //新增明细
    57.             if (checkAddCreditDetailSuccess(modifyCreditDto)) {
    58.                 throw new Exception("新增积分明细失败");
    59.             }
    60.         }
    61.         UserCreditDto userCreditDto = new UserCreditDto();
    62.         userCreditDto.setUserId(modifyCreditDto.getUserId());
    63.         userCreditDto.setCredit(userCredit.getCredit());
    64.         return CommonResult.success(userCreditDto);
    65.     }
    66.     @Override
    67.     public CommonResult reduceCredit(ModifyCreditDto modifyCreditDto) throws Exception {
    68.         UserCredit userCredit = userCreditMapper.selectByUserId(modifyCreditDto.getUserId());
    69.         if (userCredit == null) {
    70.             return CommonResult.failed("用户积分扣减失败,userId=" + modifyCreditDto.getUserId() + " 有误");
    71.         }
    72.         if (userCredit.getCredit() < modifyCreditDto.getNumber()) {
    73.             return CommonResult.failed("用户可用积分不足");
    74.         }
    75.         userCredit.setCredit(userCredit.getCredit() - modifyCreditDto.getNumber());
    76.         //更新用户积分
    77.         if (checkUpdateCreditSuccess(userCredit)) {
    78.             //新增明细
    79.             if (checkAddCreditDetailSuccess(modifyCreditDto)) {
    80.                 throw new Exception("新增积分明细失败");
    81.             }
    82.         }
    83.         UserCreditDto userCreditDto = new UserCreditDto();
    84.         userCreditDto.setUserId(modifyCreditDto.getUserId());
    85.         userCreditDto.setCredit(userCredit.getCredit());
    86.         return CommonResult.success(userCreditDto);
    87.     } 
    88.     @Override
    89.     public CommonResult findUserCreditList(Integer userId, Integer start, Integer pageSize) {
    90.         List creditDetailList = creditDetailMapper.selectByUserId(userId, start, pageSize);
    91.         if (CollectionUtils.isEmpty(creditDetailList)) {
    92.             return CommonResult.success(creditDetailList);
    93.         }
    94.         List creditDetailDtoList = new ArrayList<>();
    95.         for (CreditDetail creditDetail : creditDetailList) {
    96.             CreditDetailDto creditDetailDto = new CreditDetailDto();
    97.             creditDetailDto.setCreateTime(creditDetail.getCreateTime());
    98.             creditDetailDto.setNumber(creditDetail.getNumber());
    99.             creditDetailDto.setType(creditDetail.getType());
    100.             creditDetailDto.setOrderNo(creditDetail.getOrderNo());
    101.             creditDetailDtoList.add(creditDetailDto);
    102.         }
    103.         return CommonResult.success(creditDetailDtoList);
    104.     }
    105. }

    我们来测试一下:

    测试

    我们先来测试初始化用户积分,给用户初始化10个积分:

    初始化用户积分

    请求url:IP:PORT/credit/init

    请求参数:{"userId":2,"credit":10}

    相应参数:{"code":200,"message":"操作成功","data":"添加成功"}

    再看看数据库变化:

    11c810b8e98703086940fe7adcb00ac7.png

    初始化成功了。

    我们再来测试用户查看当前积分:

    查询用户当前积分

    请求url:IP:PORT/credit/user

    26acdad347045c35c4da97c8fe9d75c9.png

    响应参数:{"code":200,"message":"操作成功","data":{"id":2,"userId":2,"credit":10}}

    再来测试增加用户积分,给用户增加5个积分:

    增加用户积分

    请求url:IP:PORT/credit/add

    请求参数:{"userId":2,"number":5,"orderNo":"ADD100001"}

    响应参数:{"code":200,"message":"操作成功","data":{"userId":2,"credit":15}}

    再来看看两个表数据:

    44ffe47480d8a5b90500629f6fe41300.png

    6cc247a6c942319ca5bfc31c80252d3d.png


    接下来,我们来测试给用扣减积分(把积分用来兑换商品、兑换优惠券等)。

    扣减用户积分

    前面可知userId=2的用户积分是15个,此时如果我们扣减20个积分明细是不够的。

    我们来测试一把:

    请求url:IP:PORT/credit/reduce

    请求参数:{"userId":2,"number":20,"orderNo":"REDUCE100001"}

    响应参数:{"code":500,"message":"用户可用积分不足","data":null}

    既然不足,我们就把扣减积分减少:

    请求参数:{"userId":2,"number":15,"orderNo":"REDUCE100001"}

    响应参数:{"code":200,"message":"操作成功","data":{"userId":2,"credit":0}}

    从响应参数可知,此时用户积分已用完。

    我们再来查看用户积分明细:

    查询用户积分明细

    请求url:IP:PORT/credit/detail/list

    1a2416bb9fc9f1b8ba1f029ffca1d5aa.png

    响应参数:

    1. {
    2.  "code"200,
    3.  "message""操作成功",
    4.  "data": [{
    5.   "ruleId": null,
    6.   "type"0,
    7.   "number"5,
    8.   "orderNo""ADD100001",
    9.   "createTime""2022-11-27T14:41:12.000+0000"
    10.  }, {
    11.   "ruleId": null,
    12.   "type"0,
    13.   "number"15,
    14.   "orderNo""REDUCE100001",
    15.   "createTime""2022-11-28T02:22:23.000+0000"
    16.  }]
    17. }

    OK,到这里我们的用户积分明细查询也就搞定了。

    后记

    前期项目可能部署一台就够了,随着时间的增长,你会发现一台是应对不了,此时开始部署多台,既然部署多台,那就会面临很多分布式问题了。比如说:积分兑换商品时,此时需要扣减积分,可能正在扣除积分的同时又增加积分的业务出现,服务部署多台,那就会涉及到分布式锁了,不然这个积分很容易出现问题。

    另外,积分明细表到后期了,这数据量肯定会越来越大,还会涉及到分库分表,然后有可能面临分布式事务的问题。

    好了,今天的面经就分享到这里。

    在线背面试八股文:http://woaijava.cc/mianshi/index

    欢迎加入我的知识星球,做面试辅导、技术分享和技术指导,主要内容:

    1、免费 修改简历

    2、免费 模拟面试

    3、免费 《java后端面试小抄-v3.0》

    4、免费 使用刷题网站(已更新1046题)

    5、免费 无限次数技术提问

    6、共享 大量总结好的资料

    7、共享源码分析系列(mybatis源码分析、dubbo源码分析、手写Spring IOC、AOP、MVC、手写RPC框架... )、项目实战(电商项目、医院项目、个人博客、OA系统)等

    8、专门技术探讨群

    扫下方二维码即可加入:

    2f607776eadf2f7b9d76a9e5e0c0337e.jpeg

    如果学生经济压力较大的,加我weixin:tj20120622 。可以获取优惠。

    精彩文章

    资料:秒杀系统设计,文档、代码和视频

    用Spring Boot搞了个医院项目,附源码!

    海康 面试:说说MyBatis 插件机制

    QPS、TPS、并发用户数、吞吐量关系

    1000多道面试题,多久能背完?

    dubbo源码深度分析:62个文档+中文注释+流程图+思维导图

  • 相关阅读:
    当 xxl-job 遇上 docker → 它晕了,我也乱了!
    游戏服务端配置“热更”及“秒启动”终极方案(golang/ygluu/卢益贵)
    Aspectj与SpringAOP比较记录
    scrcpy macos 编译安装最新版 1.2.4
    华为笔记本电脑原装win10/win11系统恢复安装教程方法
    31.带有文本和渐变阴影的CSS图标悬停效果
    selenium元素定位 —— 提高篇 CSS定位元素
    淘宝、1688、拼多多、苏宁商品详情API接口(网络爬虫数据示例)
    计算建模之EM算法
    Jmeter常用参数化技巧总结
  • 原文地址:https://blog.csdn.net/o9109003234/article/details/128090046