java实现一个简单的转盘抽奖(简易版,后端篇)

140 阅读3分钟

项目场景

实现一个大转盘抽奖的功能,能后台自定义奖项,各奖项中奖概率,奖品数量,当日抽奖最大次数等。

一、设计思路

抽奖规则

1.用户通过手机号码注册可以获得抽奖次数
2.每个用户最多只能中奖一次
3.中奖人数最多n人

抽奖概率

每次抽奖,中奖的概率统一为7%,中奖了,根据奖品的权重分配奖品。

二、数据库设计

1.注册用户表

CREATE TABLE `draw_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `phone` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
  `create_time` datetime NOT NULL,
  `draw_count` int(10) NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

2.奖池表

  CREATE TABLE `draw_drop` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `weight` double NOT NULL,
      `drop_desc` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
      `num` int(10) NOT NULL,
      `img_url` varchar(500) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

3.中奖记录表

  CREATE TABLE `draw_reward_record` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `phone` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
      `drop_id` bigint(20) NOT NULL,
      `ip` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
      `create_time` datetime NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `uinx_idx` (`phone`,`drop_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=171 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

三、Java代码

1.引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.example</groupId>
    <artifactId>draw-sdk-common</artifactId>
    <version>1.0-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
</dependency>

2.配置

spring:
  redis:
    host: localhost
    port: 6379
  datasource:
    url: jdbc:mysql://localhost:3306/db_draw_sdk?useSSL=false&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  type-aliases-package: com.demo.mp.domain.po # 别名扫描包
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值
  global-config:
    db-config:
      id-type: auto # 指定 id 的生成方式
  configuration:
    map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
    cache-enabled: false # 是否开启二级缓存

3.抽奖核心代码

controller
@RestController
@RequestMapping("/draw")
@RequiredArgsConstructor
@Slf4j
public class DrawController {

    private final IDrawService drawService;

    /**
     * 抽奖接口
     */
    @PostMapping
    public R luckDraw(@RequestBody JSONObject param, HttpServletRequest request) {
        log.info("luckDraw request: {}", param.toString());
        param.put("ip", IpUtil.getIpAddr(request));
        return drawService.luckDraw(param);
    }

}
service
@Service
@Slf4j
@RequiredArgsConstructor
public class DrawServiceImpl implements IDrawService {

    private final RedissonClient redisClient;

    private final IDrawDropService drawDropService;

    private final IDrawRewardRecordService drawRewardRecordService;

    private final IUserService userService;

    private final TransactionTemplate transactionTemplate;


    @Override
    public R luckDraw(JSONObject param) {

        String phone = param.getString("phone");
        String ip = param.getString("ip");
        //查询用户是否注册了
        User user = userService.queryUserByPhone(phone);
        if (user == null) {
            return R.fail("用户未注册");
        }
        //查询是否存在奖池,不存则提示功能未开放
        List<DrawDrop> drops = drawDropService.list();
        if (CollectionUtils.isEmpty(drops)) {
            return R.fail("功能未开放");
        }
        //判断用户抽奖次数,这里我把次数保存在了用户表
        if (user.getDrawCount() <= 0) { //实际上做redis缓存即可
            return R.fail("抽奖次数不足");
        }
        //每个手机号码只能中奖一次
        DrawRewardRecord drawRewardRecord = drawRewardRecordService.queryByPhone(user.getPhone());
        if (Objects.nonNull(drawRewardRecord)) {
            return R.fail("未中奖", -1); //未中奖
        }
 
        //计算是否中奖
        if (!winning()) {
            return R.fail("未中奖", -1); //未中奖
        }

        //根据权重抽奖
        WeightRandom.WeightObj[] weightObjs = new WeightRandom.WeightObj[drops.size()];
        for (int i = 0; i < drops.size(); i++) {
            DrawDrop drawDrop = drops.get(i);
            WeightRandom.WeightObj weightObj = new WeightRandom.WeightObj(drawDrop, drawDrop.getWeight());
            weightObjs[i] = weightObj;
        }
        WeightRandom<DrawDrop> weightRandom = new WeightRandom<>(weightObjs);
        DrawDrop drawDrop = weightRandom.next();
        if (drawDrop.getNum() <= 0) {  // 奖品库存不足
            return R.fail("未中奖", -1); //未中奖
        }
        user.setDrawCount(user.getDrawCount() - 1); //扣除用户抽奖次数
        userService.updateByUserId(user); //扣除抽奖次数
        RLock lock = redisClient.getLock("draw:" + drawDrop.getId());
        AtomicReference<Boolean> success = new AtomicReference<>(false);
        try {
            lock.lock(5, TimeUnit.SECONDS);
            //获取中奖人数
            RBucket<Integer> drawReward = redisClient.getBucket("drawReward");
            Integer count = Optional.ofNullable(drawReward.get()).orElse(0);
            log.info("drop count: {}", count);
            //中奖人数上限2人,也可以根据日期设定每日中奖2人
            if (count != null && count >= 2) {
                return R.fail("未中奖", -1); //奖品已抽完
            }
            Boolean execute = transactionTemplate.execute(status -> {
                Boolean falg = false;
                try {
                    //updateNum,修改奖池中奖品的数量
                    if (drawDropService.updateNum(drawDrop.getId())) { //乐观锁机制,防止超卖
                        //添加一条中奖记录
                        falg = drawRewardRecordService.insertRecord(drawDrop.getId(), phone, ip);
                        success.set(falg);
                    }
                } catch (Exception e) {
                    status.setRollbackOnly(); //事务回滚
                }
                return falg;
            });
            if (execute) {
                drawReward.set(count + 1); //更新中奖人数,限制最多中奖两人
            }
        } catch (Exception e){
            //e.printStackTrace();
        } finally {
            lock.unlock();
        }

        return success.get() ? R.success(drawDrop.getId()) : R.fail("未中奖", -1);
    }


}

计算是否中奖的方法

private static Boolean winning() {
    int winningProbability = 70; // 中奖概率为7%
    Random random = new Random();
    int v = random.nextInt(1000)+1; //1-1000的随机数,
    if (v <= winningProbability) { //小于等于70则视为中奖
        return true;
    }
    return false;
}

ps: 粘贴一下修改奖品数量的sql

UPDATE draw_drop SET num = num - 1 WHERE id = #{id} and num > 0