实战篇:运营系统完整实现
概述
本篇把第 8 周的五个模块合成一个综合案例:一个"周年庆限时活动"的运营落地,用活动框架管理活动生命周期,每日刷新驱动节奏,公告触达玩家,礼包码分发福利,定时任务承载全部调度。通过代码展示运营系统的工程结构,重点演示配置化与幂等。
一、案例设计与架构
1.1 活动定义
周年庆限时活动(组合式):
冲榜赛:活动期战力冲榜,结束发奖
累充返利:累计充值达档领奖
节日签到:活动期双倍签到奖励
礼包码:主播/渠道发码,兑换福利
公告:开服/预热/结束三级公告
时间轴:
预热公告(D-3)→ 活动开启(D0)→ 冲榜
→ 冲榜结算(D+7)→ 领取窗口(+3 天)→ 清理1.2 模块划分
com.game.ops
├── activity
│ ├── ActivityService.java // 活动生命周期
│ ├── ActivityConfig.java // 配置化活动
│ └── RankRushTemplate.java // 冲榜模板
├── job
│ ├── DailyResetJob.java // 每日重置
│ └── ActivityJob.java // 活动调度
├── announce
│ ├── AnnounceService.java // 公告发布/推送
│ └── BroadcastHub.java // 在线广播
├── cdk
│ ├── CdkService.java // 码生成/兑换
│ └── CdkRiskService.java // 黑产风控
└── common
└── LockUtil.java // 分布式锁二、活动框架落地
2.1 配置化活动
java
@Component
public class ActivityService {
// 活动配置加载(热更新)
public void reloadConfig() {
for (ActivityConfig cfg : configCenter.getActivities()) {
activities.put(cfg.getActivityId(), cfg);
}
}
// 按时间判定活动状态(不依赖定时器精度)
public ActivityStatus getStatus(long activityId) {
ActivityConfig cfg = activities.get(activityId);
if (cfg == null) {
return ActivityStatus.NONE;
}
long now = System.currentTimeMillis();
if (now < cfg.getStartTime()) return ActivityStatus.UPCOMING;
if (now < cfg.getEndTime()) return ActivityStatus.RUNNING;
if (now < cfg.getEndTime() + cfg.getClaimWindow()) {
return ActivityStatus.CLAIMABLE; // 领取窗口
}
return ActivityStatus.ENDED;
}
}2.2 冲榜模板结算
java
// 冲榜赛:结束瞬间锁榜 + 幂等结算
public void settleRankRush(long activityId) {
if (!lockUtil.tryLock("lock:act:settle:" + activityId,
Duration.ofMinutes(10))) {
return; // 防多节点重复结算
}
// 冻结榜单 → 读前 N 名 → 按名次发邮件奖励
rankService.freezeBoard("rank:rush:" + activityId);
List<RankEntry> top = rankService.top("rank:rush:" + activityId, 100);
for (int i = 0; i < top.size(); i++) {
mailService.sendRankReward(top.get(i).getPlayerId(),
activityId, i + 1, rewardOf(activityId, i + 1));
}
lockUtil.unlock("lock:act:settle:" + activityId);
}三、每日刷新调度
java
@Component
public class DailyResetJob {
// 每日 0 点:重置 + 活动切换检查
@XxlJob("dailyReset")
public void execute() {
if (!lockUtil.tryLock("lock:daily:reset", Duration.ofMinutes(10))) {
return;
}
questService.refreshDaily(); // 每日任务刷新
activityService.resetDaily(); // 活跃度清零
signinService.settleStreak(); // 签到连续结算
activityService.switchByTime(); // 活动到点启停(懒检查)
lockUtil.unlock("lock:daily:reset");
}
}四、公告发布与广播
java
@Service
public class AnnounceService {
// 发布公告:持久化 + 在线广播 + 离线登录拉取
public void publish(Announcement ann) {
announceDao.insert(ann); // 持久化
broadcastHub.pushMarquee(ann); // 在线玩家滚动推送
if (ann.getLevel() == Level.SYSTEM) {
// 重要公告同时发邮件(离线必达)
mailService.sendSystemMail(ann.getTitle(), ann.getContent());
}
}
}java
// 在线广播(Redis Pub/Sub → 各逻辑服 → WebSocket)
public void pushMarquee(Announcement ann) {
redis.publish("channel:announce", Json.toJson(ann));
}五、礼包码兑换
java
@Service
public class CdkService {
// 兑换:风控 → 占码(原子)→ 发奖
@Transactional
public RedeemResult redeem(long playerId, String code,
String channel, String ip, String deviceId) {
// 1. 校验位(无效码快速拒绝,不查库)
if (!CdkCode.checkValid(code)) {
return fail(CODE_INVALID);
}
// 2. 风控(限频/设备/IP)
if (!cdkRiskService.pass(playerId, ip, deviceId)) {
return fail(RISK_BLOCKED);
}
// 3. 原子占用(并发防重复)
int rows = cdkDao.occupy(code, playerId);
if (rows == 0) {
return fail(CODE_USED);
}
// 4. 发奖(幂等:以兑换流水为凭)
grantBatchReward(playerId, code);
return ok(code);
}
}六、定时任务总览
活动期任务清单:
每小时:活动状态检查(到点启停/预热触发)
每日 0 点:每日重置 + 活动日结算
活动结束时:冲榜锁榜 + 结算发奖
领取窗口结束后:活动数据清理
每周:周榜结算(如有)
调度选型:XXL-Job(多节点 + 分片 + 失败重试)
boss 分发:rank:rush 数据量大时按分片处理七、关键要点回顾
工程要点:
活动配置化(运营不改代码上线活动)
活动状态按时间判定(幂等可恢复)
每日重置加分布式锁(防多节点重复)
公告持久化 + 在线广播 + 离线补发
礼包码原子占用 + 风控
所有发奖走统一接口(流水可审计)
验证清单:
活动到点自动开启/关闭
冲榜结算只发一次奖
每日 0 点重置正确(在线玩家无感)
公告在线实时收到、离线登录补发
同一礼包码重复兑换被拒
后台操作全量留痕