本文记录了一个完整的用户签到与优惠券发放系统的设计与实现。核心功能包括:
- 签到功能:基于 Redis BitMap 存储用户每日签到情况。
- 连续签到奖励:用户连续签到满 7 天自动发放奖励券(通过标签化发放)。
- 用户打标:基于近 30 日消费金额对用户打标签。
- 标签化发券:根据不同用户标签发放不同额度的优惠券。
本文不仅包含设计思路与实现代码,还附带了面试可能会被问到的深挖点及速答。
一、签到功能设计与实现
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);
}
三、系统特点
- 高效存储:签到数据存入 Redis BitMap,每月只需 4 字节左右即可存储整个用户的签到情况。
- 实时奖励:用户每次签到时自动统计连续天数并发放奖励,而非依赖定时任务。
- 用户分层运营:通过打标与标签化发券,可实现精准营销。
四、面试可能的深挖点 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 日消费打标,实现了分层化发券的精准营销。整个系统架构简洁、存储高效,且具备良好的扩展性。