实战篇:经济系统完整实现
概述
本篇把第 7 周的六个模块合成一个综合案例:一个"卡牌收集 + 日常养成"的经济闭环,把货币(钻石/金币)、道具(卡牌/材料)、商城(限购商品)、抽卡(概率与保底)、任务(日常)、签到(月签)串成完整链路。通过代码展示经济系统的工程结构,重点演示统一变动入口与幂等。
一、案例设计与架构
1.1 玩法闭环
卡牌收集 + 日常养成:
充值/对战 → 获得钻石/金币(货币)
钻石抽卡 → 获得卡牌/碎片(道具)
金币买材料 → 卡牌升级(消耗道具)
日常任务 + 签到 → 产出小额资源(循环)
商城限购 → 补充稀缺资源
链路演示:
对局结算 → 加金币 → 任务进度 +1
钻石抽卡 → 扣钻石 → 概率产出 + 保底
卡牌升级 → 扣材料 → 战力变化
每日刷新 → 任务重置 + 签到可签1.2 模块划分
com.game.economy
├── currency
│ ├── CurrencyService.java // 货币统一变动
│ └── CurrencyFlowDao.java // 货币流水
├── item
│ ├── ItemService.java // 道具统一变动
│ ├── ItemBag.java // 背包容量
│ └── EffectExecutor.java // 使用效果
├── shop
│ ├── ShopService.java // 商品与购买
│ └── ShopStock.java // 库存/限购
├── gacha
│ ├── GachaService.java // 抽卡
│ ├── GachaPool.java // 概率表
│ └── PityCounter.java // 保底
├── quest
│ ├── QuestService.java // 任务进度
│ └── ActivityService.java // 活跃度
└── signin
└── SigninService.java // 签到二、货币统一变动
java
@Service
public class CurrencyService {
// 唯一货币入口:所有模块调用此方法
@Transactional
public CurrencyResult change(long playerId, int currencyId,
long delta, String reason, String bizId) {
// 幂等:同 bizId 不重复执行
if (flowDao.exists(bizId)) {
return CurrencyResult.dup();
}
// 扣减时校验余额(条件更新)
int rows = currencyDao.atomicChange(playerId, currencyId, delta);
if (rows == 0) {
throw new NotEnoughException(currencyId);
}
// 写流水
flowDao.insert(playerId, currencyId, delta, reason, bizId);
return CurrencyResult.ok();
}
}sql
-- 原子扣减:余额不足时影响行数为 0
UPDATE player_currency
SET amount = amount - #{delta}
WHERE player_id = #{playerId}
AND currency_id = #{currencyId}
AND amount >= #{delta}三、抽卡与保底
java
@Service
public class GachaService {
// 单抽:扣费 → 概率结果 → 发奖 → 记录
public GachaResult gachaOnce(long playerId, int poolId) {
GachaPool pool = poolCache.get(poolId);
// 1. 扣货币(统一入口,幂等键 = 抽取批次)
currencyService.change(playerId, pool.getCostCurrency(),
-pool.getCostAmount(), "GACHA", nextBizId(playerId));
// 2. 概率 + 保底(服务端随机,可复现)
PityCounter pity = pityCounter.of(playerId, poolId);
int rarity = pool.pickRarity(pity, new Random(seedOf(playerId)));
ItemDef reward = pool.pickItem(rarity, new Random(seedOf(playerId)));
// 3. 发奖(统一入口)
itemService.addItem(playerId, reward.getId(), 1, "GACHA", bizId);
// 4. 保底计数更新 + 抽取记录
pity.increase(reward.isTop());
gachaLogDao.insert(buildLog(playerId, poolId, reward, seed));
return GachaResult.of(reward, pity.current());
}
}四、商城购买与限购
java
@Service
public class ShopService {
public BuyResult buy(long playerId, int shopId, int count) {
ShopItem item = shopCache.get(shopId);
checkSelling(item); // 在售/限时
checkLimit(playerId, item, count); // Redis 原子限购
// 扣货币
currencyService.change(playerId, item.getCurrencyId(),
-item.getPrice() * count, "SHOP", orderBizId(playerId));
// 发货
itemService.addItem(playerId, item.getItemId(),
item.getCount() * count, "SHOP", orderBizId(playerId));
return BuyResult.ok();
}
// Redis Lua 校验 + 扣减库存/限购计数(原子)
private void checkLimit(long playerId, ShopItem item, int count) {
int code = shopStock.decrIfEnough(item.getShopId(),
item.getPerLimit(), playerId, count);
if (code != 0) {
throw new BuyLimitException();
}
}
}五、任务与签到联动
java
// 对局结束事件 → 任务进度 + 货币奖励
@EventListener
public void onBattleEnd(BattleEndEvent e) {
long playerId = e.getPlayerId();
// 任务进度(同玩家串行)
questService.onEvent(playerId, QuestTarget.BATTLE, 1);
// 货币奖励
currencyService.change(playerId, CurrencyId.GOLD,
e.isWin() ? 100 : 50, "BATTLE", "battle:" + e.getBattleId());
}
// 签到 + 活跃度
public SignResult signIn(long playerId) {
int day = signinService.doSign(playerId); // 防重签到
currencyService.change(playerId, CurrencyId.GOLD, 20, "SIGN", bizId);
activityService.add(playerId, 10, "SIGN"); // 活跃度 +10
return SignResult.of(day);
}六、每日刷新调度
java
@Component
public class EconomyResetJob {
// 每日 0 点:任务刷新 + 活跃度清零 + 连续签到结算
@Scheduled(cron = "0 0 0 * * ?")
public void dailyReset() {
if (!redis.setIfAbsent("lock:daily:reset", "1",
Duration.ofMinutes(10))) {
return; // 防多节点重复执行
}
questService.refreshDaily(); // 生成当日任务
activityService.resetDaily(); // 活跃度清零
signinService.settleStreak(); // 连续签到结算(断签清零)
redis.delete("lock:daily:reset");
}
}七、关键要点回顾
工程要点:
货币/道具统一变动入口 + bizId 幂等
抽卡结果完全服务端生成(种子可复现)
商城限购 Lua 原子(防超卖)
任务/签到事件驱动(不轮询)
每日刷新统一调度 + 分布式锁
所有变动留流水(审计可追溯)
验证清单:
余额不足扣款被拒(条件更新)
重复回调/重复请求幂等
抽卡保底到点必出
限购商品超量被拒
每日刷新后任务/签到状态正确
跨月签到状态隔离