前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微信抢红包模拟实现

微信抢红包模拟实现

作者头像
yuanshuai
发布2023-11-17 18:15:12
2590
发布2023-11-17 18:15:12
举报
文章被收录于专栏:一只程序原一只程序原

微信抢红包模拟实现

1、抢红包介绍

微信抢红包基本流程:

  1. 发红包(拼手气红包)

需要发红包用户输入红包总个数、总金额,然后发红包。

2.抢红包

需要满足规则:

  1. 所有人抢到金额之和要等于红包总金额
  2. 每个人至少抢到一分钱
  3. 要保证所有人抢到金额的几率相等

2、二倍均值法

目前市面上主流实现是二倍均值算法(听说微信的红包实现是用的这个,应该是改良过的)

设剩余红包金额为 M,剩余人数为 N,每次抢到的金额 = 随机区间(0,M / N * 2)

分析:这样保证了每个随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。

假设有 10 个人,红包总金额 100,第一个人的随机范围是(0,100/10 * 2)=(0,20),平均金额 = 10;

假设第一个人随机到 10 元,第二个人的随机范围就是(0,90/9 * 2)=(0,20),平均金额 = 10;

假设第二个人随机到 10 元,第三个人的随机范围就是(0,80/8 * 2)=(0,20),平均金额 = 10。

以此类推,每一次的随机范围都相同,平均值也相同。二倍均值法保证了抢红包的公平性,但不能保证真正的随机性。因为除了最后一个人,前面任何一个人抢到的金额都一定小于当前人均金额的两倍,并不是真正的随机。

算法核心逻辑实现(拆分红包)

代码语言:javascript
复制
    /**
     * 红包分割方法
     *
     * @param amount 总金额
     * @param min    每个红包最小值
     * @param num    红包数
     */
    public static List<BigDecimal> splitRedPackage(BigDecimal amount, BigDecimal min, BigDecimal num){
        List<BigDecimal> split = new ArrayList<>();
        BigDecimal remain = amount.subtract(min.multiply(num));
        final Random random = new Random();
        final BigDecimal hundred = new BigDecimal("100");
        final BigDecimal two = new BigDecimal("2");
        BigDecimal sum = BigDecimal.ZERO;
        BigDecimal redpeck;
        for (int i = 0; i < num.intValue(); i++) {
            final int nextInt = random.nextInt(100);
            if(i == num.intValue() -1){
                redpeck = remain;
            }else{
                //RoundingMode.CEILING:取右边最近的整数
                //RoundingMode.FLOOR:取左边最近的正数
                redpeck = new BigDecimal(nextInt).multiply(remain.multiply(two).divide(num.subtract(new BigDecimal(i)),2, RoundingMode.CEILING)).divide(hundred,2, RoundingMode.FLOOR);
            }
            if(remain.compareTo(redpeck) > 0){
                remain = remain.subtract(redpeck);
            }else{
                remain = BigDecimal.ZERO;
            }
            sum = sum.add(min.add(redpeck));
            // 添加到List
            split.add(min.add(redpeck));
        }
        Assert.isTrue(compare(amount, sum), "切分红包出现异常");
        return split;
    }

    private static boolean compare(BigDecimal a, BigDecimal b){
        if(a.compareTo(b) == 0){
            return true;
        }
        return false;
    }

3、流程模拟实现

3.1 发红包接口

  • SendRedPackageDto
代码语言:javascript
复制
/**
 * @program: red-package
 * @description: 发红包dto
 * @author: yuanshuai
 * @create: 2023-07-19 18:46
 **/
@Data
public class SendRedPackageDto {

    /**
     * 总金额
     */
    @NotBlank(message = "总金额不能为空")
    private String totalAmount;

    /**
     * 拆分红包个数
     */
    @NotBlank(message = "拆红包个数不能为空")
    private String redPackageNums;

    /**
     * 每个红包最小金额
     */
    @NotBlank(message = "红包最小金额不能为空")
    private String minAmount;

}
  • RedPackageV1Controller
代码语言:javascript
复制
    /**
     * 发红包
     *
     * @param dto DTO 发红包参数
     * @return {@link Result}<{@link String}>
     */
    @PostMapping(value = "/send")
    public Result sendRedPackage(@Valid @RequestBody SendRedPackageDto dto) {
        try {
            // 验证红包个数不能小于1
            int redNums = Integer.parseInt(dto.getRedPackageNums());
            if (redNums <= 0) {
                return Result.build(null, ResultCodeEnum.PARAMS_ERROR);
            }
            // 验证总金额 和 最小金额 不能小于0
            BigDecimal total = new BigDecimal(dto.getTotalAmount());
            BigDecimal min = new BigDecimal(dto.getMinAmount());
            if (total.compareTo(BigDecimal.ZERO) < 0 || min.compareTo(BigDecimal.ZERO) < 0) {
                return Result.build(null, ResultCodeEnum.PARAMS_ERROR);
            }
            log.info("开始发红包, redNums:{}, total:{}, min:{}", redNums, total, min);
            return redPackageV1Service.sendRedPackage(redNums, total, min);
        }catch (Exception e) {
            log.error("请求发红包接口发生异常, e:{}", e.toString());
            return Result.build(null, ResultCodeEnum.PARAMS_ERROR);
        }

    }
  • RedPackageV1Service
代码语言:javascript
复制
  /**
     * 发红包
     *
     * @param redNums 红包个数
     * @param total   总金额
     * @param min     每个红包最小金额
     * @return {@link Result}
     */
    public Result sendRedPackage(int redNums, BigDecimal total, BigDecimal min) {
        //1 拆红包,将总金额total拆分为redNums个子红包
        List<BigDecimal> splitRedPackages = RedPackageUtil.splitRedPackage(total, min, new BigDecimal(String.valueOf(redNums)));
        log.info("拆红包结果: {}", new Gson().toJson(splitRedPackages));
        //2 发红包并保存进list结构里面且设置过期时间
        String key = RedPackageUtil.generateUUID();
        // 设置红包拆分缓存 由于这里是发红包不存在并发 理论上可以写成两行
        redisTemplate.opsForList().leftPushAll(Constant.RED_PACKAGE_KEY + key, splitRedPackages);
        // 设置过期时间  默认24小时退回
        redisTemplate.expire(Constant.RED_PACKAGE_KEY + key, 1, TimeUnit.DAYS);

        //3 发红包OK,返回前台显示
        return Result.build(key, ResultCodeEnum.SUCCESS);
    }

3.2 抢红包

  • RedPackageV1Controller
代码语言:javascript
复制
    /**
     * 抢红包
     *
     * @param redPackageKey 红包密钥
     * @param token         token
     * @return {@link Result}
     */
    @GetMapping(value = "/rob")
    public Result robRedPackage(@RequestParam("redPackageKey") String redPackageKey, @RequestHeader("token") String token) {
        log.info("开始抢红包, redPackageKey:{}, token:{}", redPackageKey, token);
        return redPackageV1Service.robRedPackage(redPackageKey, token);
    }
  • RedPackageV1Service
代码语言:javascript
复制
    /**
     * 抢红包
     *
     * @param redPackageKey 红包密钥
     * @param token         token
     * @return {@link Result}
     */
    public Result robRedPackage(String redPackageKey, String token) {
        //1 验证某个用户是否抢过红包,不可以多抢
        Object redPackage = redisTemplate.opsForHash().get(Constant.RED_PACKAGE_CONSUME_KEY + redPackageKey, token);
        //2 没有抢过可以去抢红包,否则返回-2表示该用户抢过红包了
        if (null == redPackage) {
            //2.1 从红包池(list)里面出队一个作为该客户抢的红包,抢到了一个红包
            Object partRedPackage = redisTemplate.opsForList().leftPop(Constant.RED_PACKAGE_KEY + redPackageKey);
            if (partRedPackage != null) {
                //2.2 抢到红包后需要记录进入hash结构,表示谁抢到了多少钱的某个子红包
                redisTemplate.opsForHash().put(Constant.RED_PACKAGE_CONSUME_KEY + redPackageKey, token, partRedPackage);
                log.info("用户:{} 抢到了多少钱的红包:{}", token, partRedPackage);
                //TODO 后续异步进mysql或者MQ进一步做统计处理,每一年你发出多少红包,抢到了多少红包,年度总结
                return Result.build(partRedPackage, ResultCodeEnum.SUCCESS);
            }
            // 抢完了
            log.info("红包池已抢空,红包标识:{}", redPackageKey);
            return Result.build(null, ResultCodeEnum.RED_PACKAGE_FINISHED);
        }
        //3 某个用户抢过了,不可以作弊抢多次
        return Result.build(null, ResultCodeEnum.RED_PACKAGE_REAPT);

    }

3.3 模拟测试

3.3.1 先发红包 获取到一个uuid 作为本次红包的一个标识

发100块,分成三个包,每个包最低金额为0.01元

获取到uuid:f4860538-1bd6-4f83-bc4a-d0af8cf1ee9d

3.3.2 抢红包 注意一个用户是一个token 且只能抢一次

第一次抢(header的token一致就认为是一个用户)

同一个用户第二次抢

提示不能重复抢

当红包抢完后

这样基本模拟了简单的抢红包流程。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-04-192,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 微信抢红包模拟实现
    • 1、抢红包介绍
      • 2、二倍均值法
        • 3、流程模拟实现
          • 3.1 发红包接口
          • 3.2 抢红包
          • 3.3 模拟测试
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档