ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

游戏奖励系统完整示例:3步搞定项目级代码,告别教程焦虑

游戏奖励系统完整示例:3步搞定项目级代码,告别教程焦虑 游戏奖励系统完整示例:3步搞定项目级代码,告别教程焦虑 看了一堆教程还是不会写项目?别怪你,是那些碎片化文章没给你完整示例。今天不扯虚的,直接上代码,从零搭建一个生产级的游戏奖励系统。 项目目标:从玩具到生产 很多新手写的奖励系统,发个金币就完事了。但真实场景下,你要处理并发领取、防重复刷取、奖励过期、库存扣减。本文目标是实现一个具备以下能力的核心模块:原子性操作:防止高并发下超发。 幂等性设计:同一请求多次调用结果一致。 审计日志:所有变动可追溯,符合合规要求。 配置化奖励:支持不同活动、不同用户的差异化奖励。这不是一个简单的if-else,而是一个涉及数据库事务、分布式锁、消息队列的完整闭环。 目录结构:清晰分层是关键 项目采用标准分层架构,避免业务逻辑耦合。 game-reward-system/ ├── src/ │ ├── main/ │ │ ├── java/com/game/reward/ │ │ │ ├── config/ # 配置类 │ │ │ ├── controller/ # 接口层 │ │ │ ├── service/ # 业务逻辑层 │ │ │ ├── repository/ # 数据访问层 │ │ │ ├── entity/ # 数据库实体 │ │ │ ├── dto/ # 数据传输对象 │ │ │ └── exception/ # 自定义异常 │ │ └── resources/ │ │ ├── application.yml # 配置文件 │ │ └── db/migration/ # 数据库迁移脚本 │ └── test/ # 单元测试 ├── pom.xml # Maven依赖 └── README.md关键点:repository层只负责CRUD,service层处理所有业务规则。这种分离让后续替换Redis或数据库时,改动范围可控。 核心代码实现:逐行拆解 1. 数据库设计:别只存金币数 奖励系统最忌“黑盒”。我们需要记录每一笔变动。 -- 奖励记录表 CREATE TABLE reward_record (id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',user_id BIGINT NOT NULL COMMENT '用户ID',reward_type VARCHAR(50) NOT NULL COMMENT '奖励类型: COIN, EXP, ITEM',quantity INT NOT NULL COMMENT '数量',biz_code VARCHAR(100) NOT NULL COMMENT '业务唯一码,用于幂等',status TINYINT DEFAULT 0 COMMENT '0-待发放, 1-已发放, 2-已过期',created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_biz_code (biz_code), -- 关键:唯一索引防重INDEX idx_user_id (user_id) ) COMMENT='奖励发放记录';注意:biz_code是幂等性的核心。它由用户ID + 活动ID + 时间戳生成,确保同一业务场景下只生成一条记录。 2. Service层:事务与幂等 这是最容易出Bug的地方。很多教程直接insert,并发下就炸了。 @Service public class RewardService {@Autowiredprivate RewardRepository rewardRepository;@Autowiredprivate StringRedisTemplate redisTemplate;/*** 发放奖励 - 核心方法* @param userId 用户ID* @param rewardType 奖励类型* @param quantity 数量* @param bizCode 业务唯一码*/@Transactional(rollbackFor = Exception.class)public boolean grantReward(Long userId, String rewardType, int quantity, String bizCode) {// 1. 幂等检查:先查库,再查Redisif (rewardRepository.existsByBizCode(bizCode)) {log.info(重复请求,忽略: {}, bizCode);return false;}// 2. Redis预占位,防止数据库唯一索引冲突(可选优化)String lockKey = reward:lock: + bizCode;Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS);if (Boolean.FALSE.equals(locked)) {throw new BusinessException(并发冲突,请重试);}try {// 3. 插入记录RewardRecord record = new RewardRecord();record.setUserId(userId);record.setRewardType(rewardType);record.setQuantity(quantity);record.setBizCode(bizCode);record.setStatus(0); // 待发放rewardRepository.save(record);// 4. 实际发放逻辑(此处省略,调用积分中心/背包服务)doActualGrant(record);// 5. 更新状态record.setStatus(1);rewardRepository.save(record);return true;} catch (Exception e) {// 6. 异常回滚,释放Redis锁redisTemplate.delete(lockKey);throw e;}} }逐行解析:@Transactional:确保数据库操作原子性。 setIfAbsent:利用Redis原子操作做初步防重,减轻DB压力。 try-catch:无论成功失败,必须释放Redis锁,防止死锁。 existsByBizCode:数据库层面的最终防线。即使Redis挂了,DB的唯一索引也能兜底。3. 配置化奖励:告别硬编码 不同活动奖励不同,不能写死在代码里。 # application.yml game:reward:config:- activityId: newbierewardType: COINquantity: 100expireHours: 24- activityId: vip_weeklyrewardType: ITEMquantity: 1itemId: sword_001expireHours: 168通过@ConfigurationProperties绑定到对象,动态加载配置。这样运营改奖励,不需要发版。 运行与测试:MockMvc实战 不要只靠Postman点按钮。用JUnit+MockMvc写接口测试。 @SpringBootTest @AutoConfigureMockMvc class RewardControllerTest {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Testvoid testGrantReward_Success() throws Exception {// 1. 准备数据RewardRequest request = new RewardRequest();request.setUserId(1001L);request.setActivityId(newbie);request.setBizCode(test_biz_001);// 2. 执行请求mockMvc.perform(post(/api/reward/grant).contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(request))).andExpect(status().isOk()).andExpect(jsonPath($.code).value(200)).andExpect(jsonPath($.data.success).value(true));// 3. 验证数据库// 可注入Repository进行断言} }测试要点:模拟并发请求,验证幂等性。 模拟Redis宕机,验证DB唯一索引兜底。 模拟奖励过期,验证状态流转。优化扩展:生产级考量异步发放:高并发下,同步发放会拖慢主流程。将doActualGrant改为发送MQ消息,由消费者异步处理。 缓存策略:用户当前余额/背包数据缓存在Redis,DB仅做持久化。注意双写一致性。 限流降级:使用Sentinel对grantReward接口限流,防止恶意刷接口。 监控告警:埋点监控bizCode重复率、发放失败率。当失败率1%时触发告警。避坑指南:不要用System.currentTimeMillis()做bizCode的一部分,高并发下可能重复。建议加UUID或雪花算法ID。 事务中不要做远程调用:doActualGrant如果调用了远程服务,会导致事务长时间持有,连接池耗尽。务必改为异步或本地消息表。小结:从代码到思维 这个完整示例不是让你抄代码,而是让你理解:幂等性是分布式系统的生命线。 数据库唯一索引是最后的防线,不要过度依赖缓存。 分层架构让你能从容应对技术栈变更。游戏奖励系统只是冰山一角。类似的思路适用于订单支付、库存扣减、积分兑换等几乎所有涉及“资源变动”的场景。 当你下次遇到“并发超卖”或“重复发奖”的Bug时,想想今天的bizCode和唯一索引。 还有什么不懂的?评论区留言挨个回。 特别是关于Redis锁与DB事务一致性的高阶问题,欢迎探讨。
返回列表