实战篇:社交系统完整实现
概述
本篇把第 6 周的五个模块合成一个综合案例:一个"好友对战 + 公会冲榜"的社交小游戏,把好友(关系与在线状态)、聊天(频道与敏感词)、公会(成员与活跃)、邮件(奖励送达)、排行榜(公会战力榜)串成一条完整链路。通过代码展示社交系统的工程结构与关键实现。
一、案例设计与架构
1.1 玩法定义
好友对战 + 公会冲榜:
好友对战:好友间发起切磋,胜者获得积分
积分同时计入:个人成就、公会活跃、公会战力榜
聊天:世界/公会/私聊三个频道
排行榜:公会战力周榜,周期结束发邮件奖励
链路演示:
好友对战结算 → 加积分 → 更新公会活跃与周榜
→ 公会榜结算 → 邮件发奖励 → 成就/红点通知1.2 模块划分
com.game.social
├── friend
│ ├── FriendService.java // 好友申请/添加/删除
│ ├── FriendRepository.java // 关系存取
│ └── RecommendService.java // 推荐好友
├── chat
│ ├── ChatService.java // 频道消息
│ ├── SensitiveWordFilter.java // 敏感词过滤
│ └── ChatRateLimiter.java // 频率限制
├── guild
│ ├── GuildService.java // 公会生命周期
│ └── GuildAuthService.java // 权限体系
├── mail
│ ├── MailService.java // 收发/附件
│ └── MailScheduler.java // 过期清理
├── leaderboard
│ └── RankService.java // 周榜维护与结算
└── event
└── SocialEvent.java // 事件定义二、好友与在线状态
2.1 关系存储
java
@Service
public class FriendRepository {
// 双向存储:A 加 B 成功,写 (A,B) 与 (B,A) 两行
public void addFriend(long a, long b) {
friendDao.insert(new FriendRow(a, b, FriendStatus.FRIEND));
friendDao.insert(new FriendRow(b, a, FriendStatus.FRIEND));
}
public List<Long> listFriends(long playerId) {
return friendDao.selectByPlayerId(playerId);
}
}2.2 在线状态批量查询
java
// 用 Redis MGET 批量查在线,避免逐玩家多次网络往返
public Map<Long, Boolean> queryOnline(List<Long> playerIds) {
String[] keys = playerIds.stream()
.map(id -> "online:" + id).toArray(String[]::new);
List<String> values = redis.mget(keys);
Map<Long, Boolean> result = new HashMap<>();
for (int i = 0; i < playerIds.size(); i++) {
result.put(playerIds.get(i), values.get(i) != null);
}
return result;
}三、聊天频道与敏感词
3.1 频道路由
java
@Service
public class ChatService {
// 不同频道走不同 Topic(分区键保序)
public void send(int channel, long from, String content) {
// 敏感词过滤
FilterResult r = sensitiveWordFilter.filter(content);
if (r.isReject()) {
throw new ChatException(ChatCode.SENSITIVE);
}
String masked = r.getMasked();
String topic = switch (channel) {
case WORLD -> "chat-world"; // 分区键 = 频道
case GUILD -> "chat-guild"; // 分区键 = 公会ID
case PRIVATE -> "chat-private"; // 分区键 = 会话ID
default -> throw new IllegalStateException();
};
// 限流(每分钟 20 条)
if (!chatRateLimiter.allow(from, channel)) {
throw new ChatException(ChatCode.TOO_FAST);
}
mqProducer.send(topic, buildMessage(from, masked));
}
}3.2 敏感词过滤(AC 自动机)
java
// 高敏词直接拒绝,一般词替换为 *
public FilterResult filter(String text) {
List<String> hits = acMachine.match(text);
if (hits.stream().anyMatch(SensitiveWordConfig::isHighRisk)) {
return FilterResult.reject(hits);
}
return FilterResult.mask(text, hits);
}四、公会活跃与权限
4.1 活跃度写入
java
@Service
public class GuildService {
// 好友对战结束 → 公会活跃 + 战力分
public void onBattleEnd(GuildBattleEvent event) {
long guildId = memberDao.findGuild(event.getPlayerId());
if (guildId == 0) {
return; // 无公会
}
memberDao.addActivity(guildId, event.getPlayerId(),
event.getScore());
// 同步到战力周榜
rankService.addScore("guild:power", guildId, event.getScore());
// 公会活跃驱动升级
maybeLevelUp(guildId);
}
}4.2 权限校验
java
// ROLE_PERMS:LEADER 全权限,ELDER 部分权限,MEMBER 最小权限
public boolean hasPermission(GuildMember member, GuildPerm perm) {
EnumSet<GuildPerm> perms = ROLE_PERMS.get(member.getRole());
return perms.contains(perm);
}五、排行榜结算与邮件
5.1 周榜结算
java
@Component
public class RankScheduler {
// 每周一 0 点结算公会战力周榜
@Scheduled(cron = "0 0 0 * * MON")
public void settleWeekly() {
String lockKey = "lock:rank:guild:power";
if (!redis.setIfAbsent(lockKey, "1", Duration.ofMinutes(5))) {
return; // 防重复结算
}
// 冻结榜 → 读前 100 → 写入结算记录
// 按名次区间发邮件奖励
List<GuildRank> top = rankService.freezeAndTop("guild:power", 100);
for (int i = 0; i < top.size(); i++) {
mailService.sendGuildReward(top.get(i).getGuildId(),
rewardOfRank(i));
}
redis.delete(lockKey);
}
}5.2 邮件附件领取
java
@Transactional
public ClaimResult claim(long playerId, long mailId) {
Mail mail = mailDao.selectByPk(mailId);
if (mail == null || mail.getReceiverId() != playerId) {
return ClaimResult.fail(NOT_EXIST);
}
if (mail.getClaimedFlag() == 1) {
return ClaimResult.fail(ALREADY_CLAIMED); // 幂等
}
// 发放附件(统一发放接口)
economyService.grant(playerId, mail.getAttachments());
mailDao.updateClaimed(mailId);
return ClaimResult.ok();
}六、消息入口与事件
6.1 协议入口
Opcode 规划:
C2S_FRIEND_APPLY → FriendService.apply
C2S_CHAT_SEND → ChatService.send
C2S_GUILD_APPLY → GuildService.apply
C2S_MAIL_CLAIM → MailService.claim
C2S_RANK_QUERY → RankService.queryBoard
C2S_TITLE_EQUIP → TitleService.equip处理方式:
Spring 消息分发(见消息分发章节)
同玩家串行(EventExecutorGroup 绑定)
跨服务协作统一走事件6.2 事件串联
社交链路事件流:
BattleEndEvent → GuildService(活跃) + RankService(战力)
RankSettleEvent → MailService(发奖) + Notification(红点)
MailClaimEvent → EconomyService(发道具) + Achievement(进度)七、关键要点回顾
工程要点:
好友双向存储 → 查询单表索引即可
在线状态 MGET 批量 → 减少 RTT
聊天 MQ 分区键 → 同会话保序
敏感词先过滤后广播 → 服务端权威
周榜结算加分布式锁 → 防重复发奖
附件领取 UPDATE 条件状态 → 幂等
性能注意:
排行榜异步写(ZINCRBY 合并)
邮件批量清理(分批 DELETE)
红点计数 Redis INCR/DECR验证清单:
好友双向写入一致
黑名单阻止聊天/申请
公会权限边界(MEMBER 不能踢人)
邮件重复领取被拒
周榜结算只发一次奖
异常恢复后进度不丢