zl程序教程

您现在的位置是:首页 >  其他

当前栏目

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

2023-03-07 09:15:43 时间

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

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

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

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

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

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

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

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

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

核心功能

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

  • 增加积分
  • 扣减积分
  • 查询用户当前积分
  • 查询积分明细列表

增加积分

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

积分还可以分类:

  • 换商品
  • 换优惠券
  • 换抽奖机会
  • ...

扣减积分

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

查询用户当前积分

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

查询积分明细列表

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

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

代码实现

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

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

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

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

CREATE TABLE `user_credit` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `credit` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id_indx_uniq` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `credit_detail` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `type` int NOT NULL,
  `number` int NOT NULL,
  `order_no` varchar(255) NOT NULL,
  `create_time` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `order_no_index` (`order_no`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

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

controller层代码:

/**
 * @author tianwc  公众号:java后端技术全栈、面试专栏
 * @version 1.0.0
 * @date 2022年11月27日 09:53
 * <p>
 * 用户积分
 */
@Slf4j
@Api(tags = "用户积分")
@Controller("/credit")
public class UserCreditController {
    @Resource
    private UserCreditService userCreditService;

    @ApiOperation(value = "通过用户id获取用户当前积分", notes = "通过用户id获取用户当前积分")
    @ApiImplicitParam(name = "userId", value = "用户id", dataType = "int ")
    @ArgsLogAnnotation(methodDescription = "通过用户id获取用户积分")
    @GetMapping("/user")
    @ResponseBody
    public CommonResult findByUserId(Integer userId) {
        return userCreditService.findByUserId(userId);
    }

    @ApiOperation(value = "新增用户积分信息(初始化)", notes = "新增用户积分信息")
    @ApiImplicitParam(name = "userCreditDto", value = "初始化用户积分账号", dataType = "object")
    @ArgsLogAnnotation(methodDescription = "通过用户id获取用户积分")
    @PostMapping("/init")
    @ResponseBody
    public CommonResult init(@RequestBody UserCreditDto userCreditDto) {
        return userCreditService.initUserCredit(userCreditDto);
    }


    @ApiOperation(value = "增加用户积分", notes = "增加用户积分")
    @ApiImplicitParam(name = "modifyCreditDto", value = "增加用户积分", dataType = "object ")
    @ArgsLogAnnotation(methodDescription = "增加用户积分")
    @PostMapping("/add")
    @ResponseBody
    public CommonResult addCredit(@RequestBody @Validated ModifyCreditDto modifyCreditDto) {
        try {
            return userCreditService.addCredit(modifyCreditDto);
        } catch (Exception e) {
            log.error("增加用户积分失败", e);
            return CommonResult.failed("增加用户积分失败");
        }
    }


    @ApiOperation(value = "扣减用户积分", notes = "扣减用户积分信息")
    @ApiImplicitParam(name = "modifyCreditDto", value = "扣减积分信息", dataType = "object")
    @ArgsLogAnnotation(methodDescription = "扣减用户积分")
    @PostMapping("/reduce")
    @ResponseBody
    public CommonResult reduceCredit(@RequestBody @Validated ModifyCreditDto modifyCreditDto) {
        try {
            return userCreditService.addCredit(modifyCreditDto);
        } catch (Exception e) {
            log.error("扣减用户积分失败", e);
            return CommonResult.failed("扣减用户积分失败");
        }
    }

    @ApiOperation(value = "积分明细", notes = "通过用户id获取用户积分明细")
    @ArgsLogAnnotation(methodDescription = "通过用户id获取用户积分")
    @GetMapping("/detail/list")
    @ResponseBody
    public CommonResult findCreditDetailListByUserId(Integer userId, Integer start, Integer pageSize) {
        return userCreditService.findUserCreditList(userId, start, pageSize);
    }
}

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

service层代码实现:

/**
 * @author tianwc  公众号:java后端技术全栈、面试专栏
 * @version 1.0.0
 * @date 2022年11月27日 11:05
 */
@Service
public class UserCreditServiceImpl implements UserCreditService {

    @Resource
    private UserCreditMapper userCreditMapper;
    @Resource
    private CreditDetailMapper creditDetailMapper;

    @Override
    public CommonResult<AddUserCreditDto> findByUserId(Integer userId) {
        UserCredit userCredit = userCreditMapper.selectByUserId(userId);
        if (userCredit != null) {
            AddUserCreditDto addUserCreditDto = new AddUserCreditDto();
            addUserCreditDto.setCredit(userCredit.getCredit());
            addUserCreditDto.setId(userCredit.getId());
            addUserCreditDto.setUserId(userCredit.getUserId());
            return CommonResult.success(addUserCreditDto);
        }
        return CommonResult.failed("查询用户积分失败");
    }

    @Override
    public CommonResult<String> initUserCredit(UserCreditDto userCreditDto) {
        UserCredit userCredit = userCreditMapper.selectByUserId(userCreditDto.getUserId());
        if (userCredit != null) {
            return CommonResult.success("添加成功");
        }
        userCredit = new UserCredit();
        userCredit.setUserId(userCreditDto.getUserId());
        userCredit.setCredit(userCreditDto.getCredit());
        int flag = userCreditMapper.insert(userCredit);
        if (flag == 1) {
            return CommonResult.success("添加成功");
        }
        return CommonResult.failed("添加失败");
    }

    /**
     * 1、修改积分信息
     * 2、新增明细
     *
     * @param modifyCreditDto 新增积分参数
     * @return 添加成功 返回最新积分
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public CommonResult<UserCreditDto> addCredit(ModifyCreditDto modifyCreditDto) throws Exception {
        UserCredit userCredit = userCreditMapper.selectByUserId(modifyCreditDto.getUserId());
        if (userCredit == null) {
            return CommonResult.failed("用户积分添加失败,userId=" + modifyCreditDto.getUserId() + " 有误");
        }
        userCredit.setCredit(userCredit.getCredit() + modifyCreditDto.getNumber());
        //更新用户积分
        if (checkUpdateCreditSuccess(userCredit)) {
            //新增明细
            if (checkAddCreditDetailSuccess(modifyCreditDto)) {
                throw new Exception("新增积分明细失败");
            }
        }
        UserCreditDto userCreditDto = new UserCreditDto();
        userCreditDto.setUserId(modifyCreditDto.getUserId());
        userCreditDto.setCredit(userCredit.getCredit());
        return CommonResult.success(userCreditDto);
    }

    @Override
    public CommonResult<UserCreditDto> reduceCredit(ModifyCreditDto modifyCreditDto) throws Exception {
        UserCredit userCredit = userCreditMapper.selectByUserId(modifyCreditDto.getUserId());
        if (userCredit == null) {
            return CommonResult.failed("用户积分扣减失败,userId=" + modifyCreditDto.getUserId() + " 有误");
        }

        if (userCredit.getCredit() < modifyCreditDto.getNumber()) {
            return CommonResult.failed("用户可用积分不足");
        }

        userCredit.setCredit(userCredit.getCredit() - modifyCreditDto.getNumber());
        //更新用户积分
        if (checkUpdateCreditSuccess(userCredit)) {
            //新增明细
            if (checkAddCreditDetailSuccess(modifyCreditDto)) {
                throw new Exception("新增积分明细失败");
            }
        }
        UserCreditDto userCreditDto = new UserCreditDto();
        userCreditDto.setUserId(modifyCreditDto.getUserId());
        userCreditDto.setCredit(userCredit.getCredit());
        return CommonResult.success(userCreditDto);
    } 

    @Override
    public CommonResult findUserCreditList(Integer userId, Integer start, Integer pageSize) {
        List<CreditDetail> creditDetailList = creditDetailMapper.selectByUserId(userId, start, pageSize);
        if (CollectionUtils.isEmpty(creditDetailList)) {
            return CommonResult.success(creditDetailList);
        }

        List<CreditDetailDto> creditDetailDtoList = new ArrayList<>();
        for (CreditDetail creditDetail : creditDetailList) {
            CreditDetailDto creditDetailDto = new CreditDetailDto();
            creditDetailDto.setCreateTime(creditDetail.getCreateTime());
            creditDetailDto.setNumber(creditDetail.getNumber());
            creditDetailDto.setType(creditDetail.getType());
            creditDetailDto.setOrderNo(creditDetail.getOrderNo());
            creditDetailDtoList.add(creditDetailDto);
        }
        return CommonResult.success(creditDetailDtoList);
    }
}

我们来测试一下:

测试

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

初始化用户积分

请求url:IP:PORT/credit/init

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

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

再看看数据库变化:

初始化成功了。

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

查询用户当前积分

请求url:IP:PORT/credit/user

响应参数:{"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}}

再来看看两个表数据:

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

扣减用户积分

前面可知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

响应参数:

{
 "code": 200,
 "message": "操作成功",
 "data": [{
  "ruleId": null,
  "type": 0,
  "number": 5,
  "orderNo": "ADD100001",
  "createTime": "2022-11-27T14:41:12.000+0000"
 }, {
  "ruleId": null,
  "type": 0,
  "number": 15,
  "orderNo": "REDUCE100001",
  "createTime": "2022-11-28T02:22:23.000+0000"
 }]
}

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

后记

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

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

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