项目场景
实现一个大转盘抽奖的功能,能后台自定义奖项,各奖项中奖概率,奖品数量,当日抽奖最大次数等。
一、设计思路
抽奖规则
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