基于 Redis BitMap 的签到、打标与优惠券发放系统设计与实现

本文记录了一个完整的用户签到与优惠券发放系统的设计与实现。核心功能包括:

  1. 签到功能:基于 Redis BitMap 存储用户每日签到情况。
  2. 连续签到奖励:用户连续签到满 7 天自动发放奖励券(通过标签化发放)。
  3. 用户打标:基于近 30 日消费金额对用户打标签。
  4. 标签化发券:根据不同用户标签发放不同额度的优惠券。

本文不仅包含设计思路与实现代码,还附带了面试可能会被问到的深挖点及速答。


一、签到功能设计与实现

1.1 签到功能

  • 使用 Redis BitMap 结构,一个月对应一个 BitMap,每一位表示当月某一天是否签到。
  • 当用户签到时,将对应位置设为 1。
public Result sign() {
    Long userId = UserHolder.getUser().getId();
    LocalDateTime now = LocalDateTime.now();
    int dayOfMonth = now.getDayOfMonth();

    String key = USER_SIGN_KEY + userId + ":" + now.format(DateTimeFormatter.ofPattern("yyyyMM"));
    redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);

    // 统计连续签到并发券
    int count = countContinuedSignedDay(userId, now, key, dayOfMonth);

    return Result.ok(count);
}

1.2 统计连续签到与发放奖励

  • 从今天开始向前遍历 BitMap,直到遇到第一天未签到为止。
  • 如果连续签到 满 7 天,调用 issueTagCoupon 根据用户标签发放奖励券。
private int countContinuedSignedDay(Long userId, LocalDateTime now, String key, int dayOfMonth) {
    List<Long> bit = redisTemplate.opsForValue().bitField(
            key,
            BitFieldSubCommands.create()
                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                    .valueAt(0)
    );

    if (bit == null || bit.isEmpty() || bit.get(0) == null) return 0;

    long num = bit.get(0);
    if ((num & 1) == 0) return 0;

    int count = 0;
    while ((num & 1) == 1) {
        count++;
        num >>= 1;
    }

    // 连续签到满 7 天发券
    if (count >= 7) {
        issueTagCoupon(userId);
    }

    return count;
}

1.3 发放优惠券方法

private void issueCoupon(Long userId, String couponType) {
    // 防止重复发放
    boolean exists = couponMapper.existsByUserIdAndTypeAndStatus(userId, couponType, 0);
    if (exists) return;

    Coupon coupon = new Coupon();
    coupon.setUserId(userId);
    coupon.setType(couponType);
    coupon.setStatus(0); // 未使用
    coupon.setExpireTime(LocalDateTime.now().plusDays(7));
    couponMapper.insert(coupon);
}

二、用户打标与标签化发券

2.1 用户消费打标

  • 定义一个周期:近 30 日消费金额。
  • 查询数据库订单表,累计消费金额。
public String getUserTag(Long userId) {
    LocalDateTime begin = LocalDateTime.now().minusDays(30);
    BigDecimal total = orderMapper.selectTotalAmount(userId, begin);

    if (total == null) return "NEW_USER";

    if (total.compareTo(new BigDecimal("1000")) >= 0) return "VIP";
    if (total.compareTo(new BigDecimal("500")) >= 0) return "ACTIVE";
    return "NORMAL";
}

2.2 标签化发券

  • 根据不同标签类型发放不同额度的优惠券。
  • 由连续签到满 7 天触发。
public void issueTagCoupon(Long userId) {
    String tag = getUserTag(userId);
    String couponType;

    switch (tag) {
        case "VIP":
            couponType = "BIG_DISCOUNT";
            break;
        case "ACTIVE":
            couponType = "MEDIUM_DISCOUNT";
            break;
        case "NORMAL":
            couponType = "SMALL_DISCOUNT";
            break;
        default:
            couponType = "NEW_USER_GIFT";
    }

    issueCoupon(userId, couponType);
}

三、系统特点

  1. 高效存储:签到数据存入 Redis BitMap,每月只需 4 字节左右即可存储整个用户的签到情况。
  2. 实时奖励:用户每次签到时自动统计连续天数并发放奖励,而非依赖定时任务。
  3. 用户分层运营:通过打标与标签化发券,可实现精准营销。

四、面试可能的深挖点 Q&A

Q1:为什么选择 Redis BitMap 存储签到数据?

  • BitMap 存储极其高效,一个月 31 天只需 31 bit,大约 4 个字节。支持高效的位运算统计连续签到。

Q2:如何解决并发签到的幂等性问题?

  • setBit 操作天然幂等,重复签到不会影响结果。

Q3:如果用户跨月签到如何处理?

  • 不同月份使用不同 key,例如 sign:userId:202508,互不干扰。

Q4:用户打标为什么选择近 30 日消费金额?

  • 30 日周期能平衡用户活跃度和单次大额消费的影响。

Q5:优惠券的发放如何避免重复或超发?

  • 发券操作加唯一索引 (userId, type, status) 或分布式锁,保证同一奖励只发一次。

五、总结

本文通过 Redis BitMap 实现了用户签到与连续签到奖励功能,并结合用户近 30 日消费打标,实现了分层化发券的精准营销。整个系统架构简洁、存储高效,且具备良好的扩展性。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇