天天看点

如何实现抢红包?

现在市面上很多聊天交友app,其中的佼佼者就是我们都在用的微信,微信的红包功能更是增加了我们

生活的便利,随份子可以不用到场,发红包就行,在群里发个广告,不发个红包都不好意思,母亲节、

父亲节、情人节,不再只是一句简单的问候和祝福,发个红包更能增进之间的感情。今天就来聊一下该

如何实现发红包、抢红包功能。

红包分为两种:

群红包和个人红包。

个人红包就比较简单了,就只有两个角色,发送者和接收者,类似我们经常说的生产者和消费者。

群红包相对复杂一点,分为普通红包和拼手气红包,一种是平均分配,另一种是完全随机。

表设计:

CREATE TABLE `redpacket` (
  `id` bigint(18) NOT NULL COMMENT 'id',
  `recordid` bigint(18) DEFAULT NULL COMMENT '乐观锁 防并发id',
  `userid` bigint(18) DEFAULT NULL COMMENT '用户id',
  `groupid` bigint(18) DEFAULT NULL COMMENT '群id',
  `redpacketmode` int(1) DEFAULT NULL COMMENT '红包模式:普通/0、拼手气/1',
  `redpackettype` int(1) DEFAULT NULL COMMENT '红包类型:个人/0、群红包1',
  `redpacketcopulation` decimal(18,2) DEFAULT NULL COMMENT '红包交子金额数',
  `surpluscopulation` decimal(18,2) DEFAULT NULL COMMENT '剩余交子金额数',
  `robpeoplenum` int(1) DEFAULT NULL COMMENT '设定的可抢人数',
  `robgrabitnum` int(1) DEFAULT NULL COMMENT '已经抢到的人数',
  `redpacketdesc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '红包描述',
  `redpacketstatus` int(1) DEFAULT NULL COMMENT '红包状态:0/未抢完、1/抢完、2红包过期',
  `redpackettimeslot` varchar(255) DEFAULT NULL COMMENT '抢红包时长:例如:5分钟抢完',
  `endtime` datetime DEFAULT NULL COMMENT '被抢完时间/过期时间',
  `createtime` datetime DEFAULT NULL COMMENT '红包创建时间',
  PRIMARY KEY (`redpacketid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
           

在这张表里就可以把红包类型和红包模式区分开来了,表设计好了,那么开始梳理逻辑。

首先是发红包。

  1. 个人红包:

    发送者发红包,发送者余额减少,生成红包记录,资金流水记录。

  2. 群红包:

    发送者发送红包,发送者余额减少,设定红包个数,设定发的红包是普通红包还是拼手气,生成红

    包记录,资金流水记录,设定24小时过期时间。

群红包的普通红包可以通过(发送的金额 / 红包个数)来实现平均分配,拼手气红包则需要做到随机。

拼手气红包算法奉上:

public class RedPacketUtil {

    /**
     * 生成红包最小值 1分
     */
    private static final int MIN_MONEY = 1;

    /**
     * 生成红包最大值 200人民币
     */
    private static final int MAX_MONEY = 200 * 100;

    /**
     * 小于最小值
     */
    private static final int LESS = -1;
    /**
     * 大于最大值
     */
    private static final int MORE = -2;

    /**
     * 正常值
     */
    private static final int OK = 1;

    /**
     * 最大的红包是平均值的 TIMES 倍,防止某一次分配红包较大
     */
    private static final double TIMES = 2.1F;

    private int recursiveCount = 0;

    public List<Integer> splitRedPacket(int money, int count) {
        List<Integer> moneys = new LinkedList<>();

        //金额检查,如果最大红包 * 个数 < 总金额;则需要调大最小红包 MAX_MONEY
        if (MAX_MONEY * count <= money) {
            return moneys ;
        }


        //计算出最大红包
        int max = (int) ((money / count) * TIMES);
        max = max > MAX_MONEY ? MAX_MONEY : max;

        for (int i = 0; i < count; i++) {
            //随机获取红包
            int redPacket = randomRedPacket(money, MIN_MONEY, max, count - i);
            moneys.add(redPacket);
            //总金额每次减少
            money -= redPacket;
        }

        return moneys;
    }

    private int randomRedPacket(int totalMoney, int minMoney, int maxMoney, int count) {
        //只有一个红包直接返回
        if (count == 1) {
            return totalMoney;
        }

        if (minMoney == maxMoney) {
            return minMoney;
        }

        //如果最大金额大于了剩余金额 则用剩余金额 因为这个 money 每分配一次都会减小
        maxMoney = maxMoney > totalMoney ? totalMoney : maxMoney;

        //在 minMoney到maxMoney 生成一个随机红包
        int redPacket = (int) (Math.random() * (maxMoney - minMoney) + minMoney);

        int lastMoney = totalMoney - redPacket;

        int status = checkMoney(lastMoney, count - 1);

        //正常金额
        if (OK == status) {
            return redPacket;
        }
        //如果生成的金额不合法 则递归重新生成
        if (LESS == status) {
            recursiveCount++;
            return randomRedPacket(totalMoney, minMoney, redPacket, count);
        }

        if (MORE == status) {
            recursiveCount++;
            return randomRedPacket(totalMoney, redPacket, maxMoney, count);
        }

        return redPacket;
    }

    /**
     * 校验剩余的金额的平均值是否在 最小值和最大值这个范围内
     *
     * @param lastMoney
     * @param count
     * @return
     */
    private int checkMoney(int lastMoney, int count) {
        double avg = lastMoney / count;
        if (avg < MIN_MONEY) {
            return LESS;
        }
        if (avg > MAX_MONEY) {
            return MORE;
        }
        return OK;
    }
}

           

抢红包:

  1. 个人红包:

    接收者余额增加,生成红包记录,生成资金流水记录。

  2. 群红包:

    接收者余额增加,生成红包记录,生成资金流水记录,红包抢完后,修改红包状态,计算红包抢完

    时长,拼手气红包需判断手气最佳。

群红包逻辑实现:

判断红包是否抢完,判断红包是否已经抢过了, 判断红包是否过期。根据第几位用户从redis中获取存

储的红包数据的索引位置拿到抢到的红包数额,就是该用户抢到的红包金额。如果该用户最后一个抢该

红包,判断是否手气最佳,余额增加,生成抢红包记录、资金流水记录,修改红包状态,清除redis缓

存。

抢红包很容易造成并发的情况,所以可以采用 Synchronized 加锁实现并发处理。

表设计:

CREATE TABLE `rob_redpacket` (
  `id` bigint(18) NOT NULL,
  `redpacketid` bigint(18) DEFAULT NULL COMMENT '红包id',
  `sendredpacketuserid` bigint(18) DEFAULT NULL COMMENT '发红包用户id',
  `robredpacketuserid` bigint(18) DEFAULT NULL COMMENT '收到红包用户id',
  `robcopulation` decimal(18,0) DEFAULT NULL COMMENT '抢到的交子金额',
  `isbesthand` int(1) DEFAULT NULL COMMENT '是否手气最佳:0/否、1/是',
  `createtime` datetime DEFAULT NULL COMMENT '抢红包时间',
  PRIMARY KEY (`robredpacketid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
           

红包过期处理:

在发红包的时候,我们设定了过期时间,也就是redis中红包数据的过期时间,那么触发红包过期退还,

就需要定时任务来实现了,关于红包过期处理,我们下期再聊。