一、为什么做这个项目
狼人杀是一个强实时、强状态、强信息隔离的多人博弈游戏。白天要发言、夜晚各角色要按顺序秘密行动、狼人要内部商量刀谁、女巫手握一瓶解药一瓶毒药、守卫不能连续守同一个人……这些规则天然对技术架构提出了苛刻的要求:
实时性:任何人的行动都要毫秒级同步给全房间,倒计时必须全端一致;
状态一致性:一个房间就是一台"状态机",多人在同一时间并发操作,不能出现重复结算、卡死、越权;
信息隔离:预言家只能看到自己的查验结果,狼人只能看到狼队友,出局者的身份要等游戏结束才揭晓——错误推送等于作弊;
真人不够怎么办:凑不齐 12 个人?让 AI 来当玩家,甚至让 AI 托管掉线的真人。
于是我用前后端分离的方式实现了这样一个「AI 狼人杀」全栈项目。本文不聊 UI 有多炫,只聊下面四个我觉得真正"硬核"的点:
双通道实时通信架构(Spring WebSocket + Netty 分工与降级);
AI 玩家决策的工程化(Spring AI Alibaba + 通义/DashScope,怎么让它"会玩、不剧透、不卡顿");
内存 + Redis 双层的游戏状态设计(并发正确性与缓存生命周期);
一整套"做大项目才需要"的工程化基建(无状态 JWT、AOP 限流、国际化、链路追踪、成就补偿……)。
技术栈先亮出来:

二、系统整体架构
先给一张"逻辑拓扑"(不是部署拓扑,是模块协作关系):
┌──────────────────────────────┐
│ Vue 3 前端 (SPA) │
│ PC/移动端自适应 · Pinia · i18n │
└──────┬───────────┬───────────┘
HTTP/JSON│ │WebSocket(wss)
▼ ▼
┌────────────────────────────────────────────────────────────┐
│ Spring Boot 3.5 (8080) │
│ ┌──────────┐ ┌────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ REST API │ │ WS: /ws/game│ │ WS: /ws/chat│ │ /ws/netty │ │
│ │ 13 模块 │ │ /{roomCode} │ │ (IM) │ │ (降级) │ │
│ └──────────┘ └────────────┘ └─────────────┘ └───────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GameService / GameFlowService (状态机·结算·超时) │ │
│ │ AiBehaviorService (AI 决策) · EventSourcing │ │
│ │ AchievementService · ImService · OnlineUserService │ │
│ └──────────────┬───────────────────────┬──────────────┘ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Netty :9000 │ │ Redis 7 │ │
│ │ 全局事件推送 │ │ 房间快照/在线/限流│ │
│ └──────────────┘ └─────────────────┘ │
└────────────────────────────────────────────────────────────┘
│ │
▼ ▼
MySQL 8 (元数据/战绩/IM/成就) DashScope 通义千问/deepseek2.1 "双通道"到底指什么
很多人一听到"WebSocket + Netty"会以为是灾备冗余,但这里其实是按消息类型分工 + 断线降级,共三条实时链路:
三个端点全部注册在 WebSocketConfig 里,并用统一的握手拦截器做 JWT 认证:
registry.addHandler(gameWebSocketHandler, "/ws/game/{roomCode}")
.addInterceptors(webSocketAuthInterceptor);
registry.addHandler(chatWebSocketHandler, "/ws/chat")
.addInterceptors(webSocketAuthInterceptor);
registry.addHandler(nettyWebSocketHandler, "/ws/netty")
.addInterceptors(webSocketAuthInterceptor);会话由 WebSocketSessionManager 用三张 ConcurrentHashMap 维护,游戏会话与聊天会话刻意分离,避免同一用户同时开房间 + 聊天时互相覆盖:
gameSessions: userId → sessionchatSessions: userId → sessionroomUsers: roomCode → Set<userId>
为什么游戏/聊天要分离? 如果共用一个会话,用户一边挂在大厅/房间里、一边和好友聊天,后连接的一方会把前一个连接顶掉。分开之后各自独立,前端可以同时维持两条长连接。
2.2 消息协议长什么样
聊天与游戏共用一个带版本号的 JSON 协议,核心常量在 WsProtocol:
WsProtocol {
version: "1.0.0"
字段: version / type / data / timestamp
/ roomCode / senderId / senderName / requestId
}WsMessageCodec.encode() 会自动补全 version 与 timestamp,序列化后下发;decode() 则会:
先做协议版本校验(只比较主版本号,向后兼容),不支持直接抛
WsProtocolException;校验
type非空;对
data做归一化——兼容"平铺字段"和"data 对象"两种形态,老客户端也能用;可选 AES-GCM 加密:密文以
ENC:头开头,IV 前置拼接,用于敏感消息。
消息类型集中在 WsMessageType,按域分组并统一 snake_case:
game_start / night_start / day_start / vote_start
seer_action / witch_action / guard_action / wolf_action
player_speak / hunter_shoot / game_end
chat_* / friend_* / group_* / voice_* / system_*
2.3 Netty 为什么要"另起炉灶"
Netty 通道解决两个 WebSocket 不方便的问题:
原生 TCP 客户端可接入(未来可做桌面/硬件客户端),通过 4 字节长度帧 +
LengthFieldBasedFrameDecoder解决粘包/拆包;全局事件推送的卸载——WebSocket 端点都带
@Scope语义,不适合做"给全平台广播在线人数"这类事情。
同时 Netty 做了很强的推拉结合:
sendEvent会判断消息类型——聊天/群聊事件优先走 Chat WebSocket,WS 在线就不走 Netty 重复推,离线才降级,避免双通道重复消息;sendEventToAll广播时只预序列化一次 JSON,而不是对每个用户重复序列化;broadcastOnlineCountUpdate用 3 秒节流 + CAS 标记,防止连接抖动引发 O(N²) 的重复广播风暴。
三、难点一:让 AI 学会"打狼人杀"
这是整个项目最有意思的部分。狼人杀对 LLM 的要求不是"能聊",而是在有限信息下做决策、遵守回合、不剧透、不卡局。
3.1 AI 到底扮演什么角色
先说结论(避免过度吹嘘):AI 不做法官、不做主持人。发牌、天黑天亮播报、胜负判定全部是引擎的确定性逻辑 + 预置国际化系统消息(i18nKey),保证公平可审计。LLM 只出现在两处:
AI 玩家:真人不足时加入(
addAiPlayer),开局随机中文名、自动 ready、自动行动;掉线托管(更妙的设计):真人中途掉线 → 后端标记
isEntrusted=true→ 复用和 AI 玩家完全相同的自动决策管线(isAiOrEntrusted统一判断)。真人回来可取消托管继续玩。
这意味着我只需要把"AI 行为"写一遍,就同时服务了"AI 玩家"和"真人托管"两个场景。
3.2 决策点全覆盖
AiBehaviorService 覆盖了狼人杀里 AI 需要的全部决策,且每个决策都有确定性兜底,保证 AI 永远不会因为"模型超时/输出不合法"而卡住整局游戏:
行动还会模拟"人类思考时间"(守卫 2.5~4.5s、狼 3~5.5s、预言家 3.5~5.5s、女巫 4.5~6.5s、发言 1~2.5s、投票 2~5s),让节奏更真实。
3.3 统一调用 + 超时重试
所有决策收敛到同一个 callLLM(),用 CompletableFuture + 15 秒硬超时包住同步调用,失败最多重试 2 次并指数退避——这是"在线游戏里调用 LLM"和"聊天框里调用 LLM"最大的区别:游戏等不起无限长的流式响应:
private String callLLM(String prompt) {
try {
return CompletableFuture
.supplyAsync(() -> chatClient.prompt().user(prompt).call().content())
.get(LLM_TIMEOUT_SECONDS, TimeUnit.SECONDS); // 15s 硬超时
} catch (Exception e) {
log.warn("LLM 调用失败,第 {} 次重试...", retry);
// MAX_RETRIES=2,指数退避后重试
}
return null; // 返回 null 由上层走兜底策略
}3.4 提示词工程:怎么把"一局游戏"喂给模型
游戏状态要传给模型,最直接的方式是拼一个大 JSON,但 token 很快就爆了。这里做了三层压缩:
第一层:Markdown 表格化上下文。 AiPromptBuilder.buildGameContext 把玩家列表(座号/昵称/存活/死因)、存活统计、已亮身份、历史发言摘要、投票记录整理成紧凑的 Markdown 表格。
第二层:摘要式记忆(处理长对局的关键)。 每晚结束后用 generateDaySummary 生成当日摘要写入 gameState.daySummaries[round],再增量合并成一份"全局总摘要"(80~200 字)。AI 决策时只喂"总摘要 + 当日摘要 + 最近 3 条压缩发言",而不是把整局所有发言都塞进去。老轮次的发言会用 SpeechCompressor.compress() 压成一句话——这就是 LLM 打长对局不爆 token、不失忆的实用方案。
第三层:按角色注入私有记忆。 buildAiIdentity 会告诉狼"你的队友是 3 号、5 号,你们这晚刀过 7 号",告诉预言家"你验过 2 号是狼、6 号是好人",让 AI 具备人类玩家的"记忆对齐"。
3.5 结构化输出 + 输出安全(防注入、防剧透)
我没有用 function-calling 或强制 JSON 输出,而是让模型"只输出一个数字(座号),无法决定输出 -1",然后用正则抽取,最稳、最省钱:
private Integer extractSeatNumber(String llmOutput) {
if (llmOutput == null) return null;
Matcher m = Pattern.compile("\\d+").matcher(llmOutput);
return m.find() ? Integer.parseInt(m.group()) : null;
}抽取后还有一道合法性校验 isValidNightTarget:狼不能刀狼队友/自己、女巫不能毒自己等,任何越权结果都会被拦下并走兜底。
而真正让我觉得"专业"的是 filterSpeechContent 输出安全过滤(对发言内容):它会拦截
代码块 / 链接 / URL;
AI 身份泄露词:
作为AI、我是语言模型、llm/gpt/deepseek等 18 个词;指令注入残留:
忽略之前、ignore previous、你现在是、system:等;敏感词(密码 / 转账 / 炸弹等)。
并且每个 prompt 都以"最高优先级安全约束"结尾,要求模型忽略玩家注入的指令、绝不透露自己 AI 的身份——因为狼人杀里如果 AI 玩家说出"我是大模型",这局游戏的信息博弈就崩了。
3.6 房间级动态模型选择
模型配置是数据库驱动的(llm_model_config 表),开局时房间随机选一个聊天模型(randomSelectChatModel),支持 deepseek-r1 / deepseek-v3 / qwen-max / qwen-turbo 等,还支持运行时切换(POST /api/llm/config/switch/{modelCode}),可以对比不同模型的"狼人杀智商"。语音识别则单独用 paraformer-v1(ASR),玩家发言原文会异步落库到 game_speech,用于合规取证与复盘。
四、难点二:实时状态同步与一致性
游戏页面是"一个会被很多人同时改动的状态机",前端 room store 有 50+ 个状态字段。最怕三件事:重复结算、断线丢状态、倒计时漂移。
4.1 服务端:防重复结算的三板斧
房间 GameRoom 是纯内存对象,玩家座位用 ConcurrentHashMap<seat, GamePlayer> 存。并发正确性靠三层:
class GameRoom {
final ReentrantLock lock; // ① 回合操作串行化
volatile boolean wolfKillResolved; // ② 幂等标记:狼刀结果只结算一次
volatile boolean voteResolved; // ② 幂等标记:投票结果只结算一次
ScheduledFuture<?> timeoutFuture; // ③ 单飞超时任务,推进时取消防重复
}举个实际例子——狼人投票结束的推进逻辑:
if (votedCount >= aliveCount && !room.voteResolved) {
room.voteResolved = true; // 先置幂等标记再结算
executeVoteResult(room);
}配合 @Sharable 的 Netty handler 与多线程的 AI 调度器,这类"谁都能触发推进"的地方全靠幂等标记兜底,注释里也明确写了防并发竞态的设计动机。
4.2 断线重连:状态全量 + 事件增量 + 消息补偿
对"中途刷新/断网"的用户,做了三层保护:
全量状态同步:重连后前端主动发
request_state_sync,服务端把当前房间完整状态下发;事件增量同步:旁路的
EventStore(Redis 有序事件流)为每局维护offset,前端通过GET /room/{roomCode}/events/incremental按自己的可见性过滤(玩家只能拿到自己该知道的事件,走的是服务端过滤而非前端过滤,避免"抓包作弊")拿到增量事件;lastEventOffset就是断点续传的游标;离线消息补偿:
sendToUser在用户不在线时把消息写入pendingMessages(每人上限 50 条),重连握手成功后flush补发。
4.3 倒计时漂移:时钟同步
狼人杀全靠倒计时推进,而"浏览器 setTimeout 不准 + 服务端判定有延迟"会导致客户端倒计时和服务端真实截止时间不一致(最典型:本地显示还有 3 秒,服务端其实已经超时结算了)。解法是连接建立时做一次时钟同步:
前端发 time_sync_request(clientTime=T1)
后端回 time_sync_response(serverTime=T2, clientTime=T1)
前端算出 clockOffset = serverTime - (本地接收时刻 + T1)/2
所有倒计时 deadline 用 getAdjustedTime() 校正后渲染前端 useGameWebSocket 里还专门为"预言家查验结果最少展示 3 秒,防止 night_start 过早把结果清掉"写了注释和定时器——这类为体验兜的细节,往往是最值钱的代码。
4.4 前端:指数退避重连
前端 useWebSocket.js 对断线做了非常完整的处理,直接看延迟策略就懂它的严谨:
// 第 1 次固定 3s,之后指数增长,上限 30s
function getReconnectDelay(attempt) {
if (attempt <= 1) return reconnectBaseDelay; // 3000ms
const delay = reconnectBaseDelay * Math.pow(2, attempt - 1);
return Math.min(delay, reconnectMaxDelay); // 30000ms 封顶
}还有几个细节值得抄作业:
最大重连 20 次,第 3 次提示"网络已断开",第 10 次提示"网络不稳定",耗尽则跳回大厅;
被踢下线的识别:服务端强制断开时 close reason 带
kicked,此时不重连、清 token、跳登录,避免"被踢了还在疯狂重连";游戏进行中(非 WAITING/ENDED)重连成功后主动发
request_state_sync,保证刷新后状态一致。
五、难点三:内存 + Redis 双层游戏状态
房间状态分两层:运行时全在内存(快),Redis 负责跨实例共享 / 崩溃恢复,MySQL 只存"元数据"。这是单机也能跑、未来想横向扩容也能平滑演进的设计。
5.1 三层各有分工
5.2 Redis Key 设计
注意:房间存的是 String(JSON) 而非 Hash——因为
GameRoom结构复杂且要保留@class类型信息(RedisConfig里用activateDefaultTyping(NON_FINAL)),JSON 整体读写最简单可靠。
5.3 Lua 脚本:把"并发正确性"下沉到 Redis
跨实例的原子操作全部用 Lua 脚本一次搞定,最典型的是玩家进房:必须"校验此人不在别的房间 + 写入房间 + 写入玩家映射"三件事原子完成,否则会出现同一玩家同时进两个房间的脏数据:
-- PLAYER_JOIN_ROOM_SCRIPT(语义示意)
-- 1) 若 field 已存在则返回冲突(防一玩家两房)
-- 2) HSET wolf:game:player:room userId roomCode
-- 3) 加入房间的玩家集合另一个有意思的是在线用户过期清理:以前是"先按分数 range 取出过期用户、再 zrem 删除",两步之间用户可能刚好刷新心跳被误删;改成一条 Lua:ZRANGEBYSCORE + ZREMRANGEBYSCORE 一步原子取出并删除,从根上消除竞态。
5.4 缓存生命周期的"三管齐下"
防雪崩/穿透/击穿之外,这个项目最值得学的是缓存生命周期管理:
CacheRestoreRunner(启动恢复):应用启动时先从 Redis 恢复完整房间;对 Redis 里没有、但 DB 里仍是 WAITING/PLAYING 的房间,按 DB 元数据重建并回写 Redis;GameRoomCacheBackupTask(运行中兜底):每 30s 全量syncAllToRedis();CacheShutdownHook(停机兜底):监听ContextClosedEvent,优雅停机前再全量落一次 Redis;加上
CacheWarmupService延迟预热、GameRoomCleanupTask定时清理 ENDED/DISMISSED 房间、扫"僵尸房"(房主离线 5 分钟自动转让、全员离线 15 分钟解散)……
一套组合拳下来,"进程随便崩、重启不丢房"基本做到了。另外还实现了纯单机版游戏的所有复杂规则都跑在确定性引擎里,AI 只是锦上添花——这也是为什么这个系统稳定。
六、游戏状态机与规则引擎
6.1 一局游戏的生命周期
WAITING ──startGame──▶ 第一夜 NIGHT
NIGHT ──endNightPhase──▶ DAY(死亡公告 → 发言 → 投票 → 猎人或平安)
DAY ──proceedToNextNight──▶ NIGHT(round+1)
任意阶段 ──checkGameEnd(屠边)──▶ ENDED ──5min后──▶ 清理房间6.2 角色与夜晚行动顺序
游戏支持 6 种角色:狼人 / 村民 / 预言家 / 女巫 / 守卫 / 猎人(狼阵营走 isWolf=true),模式从 6 人到 12 人可配。夜晚采用固定链式推进 advanceNightStep:
GUARD(守卫) → WOLF(狼人投票刀人) → SEER(预言家验人)
→ WITCH(女巫用药) → HUNTER(猎人夜检) → 天亮结算每一步会先 hasAliveRole 判断该角色是否还有人存活,没人就自动跳过。其中值得讲的规则细节:
守卫:不能连续两晚守同一人(
prevGuardedPlayer),第一晚额外 +5s 思考时间;狼人:多狼要内部投票选出刀谁(
wolfKillVotes+wolf_vote_update广播),投完统一确认,模拟"狼队商量";女巫:一晚只能用一瓶药,会收到"今晚谁被刀"的提示,且守卫行动在女巫之前结算,所以女巫能看到守卫奶没奶上;
猎人:夜里只做"是否中毒"检查——被毒死的猎人不能开枪。
6.3 夜晚结算的"奶穿"规则
天亮结算 endNightPhase 是规则密集区,刀杀判定优先级如下(狼刀 vs 守卫/解药):
毒药则不可解(守护/解药都不能抵消被毒者)。死亡公告后有个顺序细节很关键:代码注释强调必须先 checkGameEnd 再推 day_start,保证前端在进入白天前先收到胜负判定,避免"游戏已结束前端还停在白天"的状态错乱。
6.4 胜负与"屠边"
checkGameEnd 用经典屠边规则:
好人阵营赢:存活狼人 = 0;
狼人阵营赢:屠神(存活神职 = 0,神=非狼非民)或屠民(存活村民 = 0)。
结算时把全部玩家身份揭晓推给所有人(game_end),异步写入 game_record / game_record_player,再触发成就判定,5 分钟后清理房间给玩家留足看结果的时间。
七、把项目"做正规":一整套工程化基建
除了游戏玩法,这个项目让我觉得最"值钱"的是把下面这些做大项目才需要的基建都补全了。
7.1 无状态 JWT:自动续期 + 多端踢人
SecurityConfig 用白名单 + denyAll() 兜底(显式放行 /api/auth/**、/ws/** 等),JWT 有效期 2 小时,剩余不足 30 分钟自动续期——通过响应头 X-New-Token 带回新 token,前端静默更新,用户无感:
请求剩余 <30min → refreshToken
→ 响应头 X-New-Token: <新token>
→ 响应头 X-Token-Remaining-Time: <剩余秒数>多端互踢的实现很有意思:新登录后,旧 token 被写进 Redis 黑名单 wolf:token:blacklist:*,旧 token 的下一次请求在过滤器层直接 401;如果旧端还挂着 WebSocket,服务端会主动推 {"type":"kicked"} 并断开——前端识别到这个原因就不重连,直接跳登录,体验干净利落。
7.2 注解 + AOP 的横切能力
@OperateLog:通过OperateLogAspect环绕记录到sys_operate_log(含耗时、入参/出参 JSON、成功失败),审计不留死角;@RateLimiter:RateLimiterAspect用固定窗口SET key 1 EX window NX(首请求)+INCR(递增)实现 IP/用户/路径维度的限流,还处理了"key 在 setIfAbsent 与 increment 之间过期重建"的极端情况。
7.3 统一错误码 + 三级国际化
错误码按段规划:10xxx 认证 / 20xxx 游戏 / 21xxx IM / 22xxx 群聊……且每条错误码都绑定一个 messageKey,如 error.user.not_found。GlobalExceptionHandler 按 messageKey → 原生 message → 中文硬编码 三级降级取文案,后端抛出的所有异常在前端都能直接显示成用户语言——后端负责翻译、前端零硬编码,中英切换对错误提示也生效。
后端消息结构则统一为:
public class Result<T> { int code; String message; T data; }所有 WS 系统消息也带 i18nKey,前端统一走 translateMsg():有 i18nKey 用翻译,否则回落 message——这个约定让"系统消息"天然双语。
7.4 MDC 链路追踪 + 结构化日志
MdcInterceptor为每个请求生成 16 位requestId,并把当前userId / username塞进 SLF4J MDC,请求结束清理;logback-spring.xml控制台输出带[%X{requestId:-}];文件用 Logstash JSON Encoder 输出结构化日志,customFields 携带app/environment,并把requestId/userId/username/roomCode导出——丢给日志平台可直接按链路/用户/房间检索;游戏 / AI / WS 走独立
game.log,dev 开 DEBUG、非 dev 只 INFO,避免生产被 AI 决策日志刷爆。
7.5 成就系统:实时 Redis + 每日 DB 补偿"双保险"
成就走数据驱动(achievement 表定义 key/类型/条件/奖杯/积分),覆盖首胜、连胜(3~20 连胜)、角色胜(狼王/预言家/女巫/猎人/守卫/平民英雄)、局数(10~500 局)、以及"完美胜利"(全程存活且胜利)等。
它的设计亮点是计数可靠性:实时用 Redis 计数器累加(响应快),但 Redis 有丢数据风险,于是 AchievementCompensationTask 每天凌晨 3 点从 game_record_player 拉取战绩重算补偿,初始化缺失记录——防 Redis 计数器过期导致进度丢失,双保险。
7.6 即时通讯子系统
好友 / 私聊 / 群聊一应俱全:好友搜索与申请、备注、群聊含群已读回执(im_group_message_read)、禁言/转让/管理员/群昵称。群消息发送后会区分"已读/未读",是相对完整的一个 IM 子系统。


八、前端:赛博朋克设计系统与体验细节
前端最抢眼的是设计系统——它不是随便用用 Element Plus,而是定义了一整套"赛博朋克" Design Tokens:
// variables.scss 摘录
// 主色:青蓝 | 辅色:品红 | 强调:翠绿
$--color-primary: #00e5ff; // 青蓝(主按钮/链接/发光)
$--color-secondary: #ff00e5; // 品红(强调)
$--color-accent: #00ffa3; // 翠绿(成功/加分)
// 角色专属色
$--color-wolf: #ff3344; // 狼人·红
$--color-seer: #aa66ff; // 预言家·紫
$--color-witch: #66ffaa; // 女巫·绿
$--color-hunter: #ff8833; // 猎人·橙
$--color-guard: #3388ff; // 守卫·蓝
// 深空背景
$--bg-color: #0a0a12;
$--bg-color-card: #14142a;配合首页的粒子漂浮背景、房间的网格光晕、按钮的霓虹发光,整体是"暗夜 × 霓虹"的狼人杀氛围。角色色贯穿全程:头像高亮、发言标识、死亡变灰、出局原因都用语义色区分,玩家扫一眼就能读懂局面。
多端适配方面,src/pc/、src/mobile/ 目前是"独立视图的规划目录",当前实际策略是共享一套 src/views + CSS 媒体查询做响应式:桌面是三栏布局,移动端则用抽屉收起来,顶部只留分享/离开/解散按钮。这样一套代码同时覆盖 PC 和手机,且为将来"拆独立视图"留好了扩展位。
其他值得一提的前端细节:
VirtualList虚拟列表 +MessageList:房间聊天/群聊消息多了也不卡(配合useMessageStore做增量存储、maxMessages截断);语音:
VoiceChannel/VoiceRecorder/VoiceMessage实现了房间内"语音房"式体验(静音/闭麦信令只广播给房间其他人,不经服务端转发音频,压低延迟);阶段动画:
PhaseTransition(天黑天亮转场)、PhaseSummaryCard(回合小结卡片先播完再弹结果,避免跳变)、RoleRevealModal(身份揭晓)、GameTimer(颜色随剩余秒数渐变、读秒紧迫感);音效
useGameSound、新手引导OnboardingGuide、主题/语言切换(ThemeToggle/LanguageSelector)一应俱全;构建优化:Vite 手动分包(vue/element/utils 独立 chunk)、Element Plus 全自动按需引入。

九、部署与多环境
后端三套环境(dev/test/prod)通过 spring.profiles.active 切换,生产域名走 HTTPS。前端通过 VITE_API_BASE_URL / VITE_WS_URL 区分:
# 前端 .env.development
VITE_API_BASE_URL=/api
VITE_WS_URL=ws://localhost:8080
# 生产环境留空 → 走 Nginx 同源反向代理,wss 自动跟随 https后端部署脚本 start.sh 也写得相当工程化:
JVM 参数:
-Xms512m -Xmx1024m -XX:+UseG1GC(G1 + 目标停顿 200ms);显式开启 GC 日志文件 + OOM 自动堆转储(
-XX:+HeapDumpOnOutOfMemoryError)——线上出问题不用等现场;支持
start / stop / restart / status,PID 文件管理,优雅停机 30s 内 kill、超时再kill -9。
另外为了配置安全还引入了 Jasypt(ENC(...) 加密串),虽然当前配置文件仍是明文、尚未完全启用,但工具链(JasyptUtil,PBEWITHHMACSHA512ANDAES_256)已经就位,属于"能力先行、随时可落地"。
十、踩过的坑与设计取舍(复盘)
写博客我最喜欢诚实地记录"当时纠结过什么":
浏览器 WebSocket 无法自定义 Header → 认证只能走 URL
?token=。后端其实写好了一套 HMAC-SHA256 signature/timestamp/nonce 反重放校验,但因为浏览器限制最终注释掉了。如果未来做原生客户端(能自由设 Header/签名),可以重新启用。这就是"协议设计要为客户端能力妥协"。LLM 不能无限等 → 坚决不用流式响应做游戏决策,15s 超时 + 重试 + 兜底三步走,宁可 AI 说一句模板话,也不让整局卡 30 秒。
Redis 反序列化会丢东西 →
GameRoom反序列化可能丢失锁/类型信息,所以RedisConfig必须开activateDefaultTyping保留@class,业务代码里多处"从 DB 重新 select 恢复 gameConfig"的兜底正是为了治这个。发言历史不能无限喂给模型 → 摘要 + 压缩 + 只保留最近 3 条,是 token 成本与"AI 记忆"的平衡点。
大厅广播不能太频繁 → 在线人数广播做了 3 秒节流 + CAS,防"每人每秒上下线波动"把广播打成 O(N²)。
被踢和断线要区分 → 前端通过 close reason 区分"被踢下线"和"网络抖动",前者不重连直接跳登录,体验完全不同。
十一、项目结构与跑起来
完整代码结构(后端 + 前端)可供查阅:后端 game-wolf(Spring Boot),前端 game-wolf-ui(Vue 3)。本地快速体验只需准备 JDK 17 / MySQL 8 / Redis 7:
# 后端
CREATE DATABASE game_wolf DEFAULT CHARACTER SET utf8mb4;
# 依次执行 sql/schema.sql(表结构)、sql/data.sql(模式/AI模型/成就)
mvn spring-boot:run
# 前端
npm i
npm run dev # 默认连 ws://localhost:8080进大厅建房 → 人不够就 添加 AI 玩家 → 开局,就能亲眼看到 AI 玩家发言、投票、甚至在你掉线时接管你的身份了。
十二、总结
一句话总结这个项目带给我的启发:在线博弈游戏的技术难点不在"某个算法",而在"把状态机做对 + 把实时消息做稳 + 把 AI 做成不会卡局的参与者",最后再用工程化基建把这些能力兜住。
实时:WebSocket / Netty 双通道按消息类型分工、断线降级、消息补偿;
正确:内存 + Redis + MySQL 三层、Lua 原子脚本、幂等标记、事件增量同步;
智能:Spring AI 让 AI 会玩不剧透,还顺带解决了真人掉线托管;
正规:JWT 续期/多端踢人、AOP 限流与审计、统一错误码国际化、MDC 结构化日志、成就双保险。
如果你也在做"强实时多人对局"或"LLM 与规则引擎结合"的项目,希望这些取舍能给你一些参考。有问题欢迎评论交流~
项目体验地址:https://www.shturl.online