2026-10-18 17:28:45 +08:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > SignalR WebSocket 协议文档</ title >
< style >
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
html , body { height : 100 % ; font-family : - apple-system , "Microsoft YaHei" , sans-serif ; background : #f5f6fa ; color : #333 ; }
. layout { display : flex ; height : 100 vh ; }
. sidebar { width : 220 px ; background : #1e2a3a ; color : #fff ; overflow-y : auto ; flex-shrink : 0 ; }
. sidebar . logo { padding : 20 px ; font-size : 18 px ; font-weight : bold ; text-align : center ; border-bottom : 1 px solid #2a3a4a ; }
. sidebar . nav-item { padding : 12 px 20 px ; cursor : pointer ; font-size : 14 px ; border-left : 3 px solid transparent ; }
. sidebar . nav-item : hover { background : #2a3a4a ; }
. sidebar . nav-item . active { background : #2a3a4a ; border-left-color : #3498db ; color : #3498db ; }
. sidebar . nav-item . badge { float : right ; background : #3498db ; color : #fff ; border-radius : 10 px ; padding : 0 8 px ; font-size : 11 px ; line-height : 18 px ; }
. main { flex : 1 ; overflow-y : auto ; padding : 20 px 30 px ; }
. hub-section { display : none ; }
. hub-section . active { display : block ; }
. hub-card { background : #fff ; border-radius : 8 px ; box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 ); margin-bottom : 25 px ; overflow : hidden ; }
. hub-header { padding : 18 px 20 px ; color : #fff ; }
. hub-header . path { font-size : 20 px ; font-weight : bold ; }
. hub-header . detail { font-size : 13 px ; opacity : .9 ; margin-top : 6 px ; }
h2 { font-size : 18 px ; margin : 25 px 0 12 px ; padding-left : 12 px ; border-left : 4 px solid #3498db ; }
h3 { font-size : 15 px ; margin : 20 px 0 10 px ; color : #555 ; }
table { width : 100 % ; border-collapse : collapse ; margin-bottom : 5 px ; }
th { background : #f0f4f8 ; text-align : left ; padding : 10 px 12 px ; font-size : 13 px ; color : #555 ; border-bottom : 2 px solid #ddd ; }
td { padding : 10 px 12 px ; border-bottom : 1 px solid #eee ; font-size : 13 px ; vertical-align : top ; }
tr : hover td { background : #f8faff ; }
. tag { display : inline-block ; padding : 2 px 8 px ; border-radius : 3 px ; font-size : 11 px ; font-weight : bold ; margin-right : 4 px ; }
. tag-cs { background : #e3f2fd ; color : #1565c0 ; }
. tag-sc { background : #fce4ec ; color : #c62828 ; }
. tag-new { display : inline-block ; padding : 1 px 6 px ; border-radius : 3 px ; font-size : 10 px ; font-weight : bold ; background : #e74c3c ; color : #fff ; margin-left : 4 px ; vertical-align : middle ; }
tr . row-new { border-left : 4 px solid #e74c3c ; background : #fff5f5 !important ; }
tr . row-new td { background : #fff5f5 !important ; }
tr . row-new : hover td { background : #ffe8e8 !important ; }
code { background : #f0f0f0 ; padding : 1 px 5 px ; border-radius : 3 px ; font-size : 12 px ; color : #c7254e ; }
pre { background : #f8f9fa ; padding : 12 px ; border-radius : 4 px ; overflow-x : auto ; font-size : 12 px ; line-height : 1.6 ; border : 1 px solid #eee ; }
. info-box { background : #eef9ff ; border : 1 px solid #b3d8ef ; border-radius : 6 px ; padding : 18 px 22 px ; margin-bottom : 20 px ; }
ul { padding-left : 20 px ; } li { margin : 4 px 0 ; font-size : 13 px ; }
. top-bar { background : #fff ; border-radius : 8 px ; padding : 18 px 22 px ; margin-bottom : 20 px ; box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.06 ); }
. top-bar h1 { font-size : 22 px ; color : #2c3e50 ; }
. top-bar p { color : #888 ; font-size : 13 px ; margin-top : 6 px ; }
:: -webkit-scrollbar { width : 6 px ; } :: -webkit-scrollbar-track { background : transparent ; } :: -webkit-scrollbar-thumb { background : #ccc ; border-radius : 3 px ; }
</ style >
</ head >
< body >
< div class = "layout" >
< div class = "sidebar" >
< div class = "logo" > 📡 协议文档</ div >
< div class = "nav-item active" data-target = "hub-login" onclick = "switchTab(this)" > LoginHub < span class = "badge" > /loginhub</ span ></ div >
< div class = "nav-item" data-target = "hub-fishgame" onclick = "switchTab(this)" > FishGameHub < span class = "badge" > /fishinghub</ span ></ div >
< div class = "nav-item" data-target = "hub-fishroom" onclick = "switchTab(this)" > FishRoomHub < span class = "badge" > /fishingroomhub</ span ></ div >
< div class = "nav-item" data-target = "hub-chessgame" onclick = "switchTab(this)" > ChessGameHub < span class = "badge" > /chessgamehub</ span ></ div >
< div class = "nav-item" data-target = "hub-customer" onclick = "switchTab(this)" > CustomerHub < span class = "badge" > /customerHub</ span ></ div >
< div class = "nav-item" data-target = "hub-common" onclick = "switchTab(this)" > 公共说明</ div >
</ div >
< div class = "main" >
< div class = "top-bar" >
< h1 > SignalR WebSocket 协议文档</ h1 >
< p > 所有 Hub 支持 WebSocket 传输。LoginHub 和 CustomerHub 客服端需 JWT Token( < code > access_token</ code > 参数)。本轮不调整现有时间字段的单位和解析方式。</ p >
</ div >
<!-- ==================== LoginHub ==================== -->
< div id = "hub-login" class = "hub-section active" >
< div class = "hub-card" >
< div class = "hub-header" style = "background:#3498db;" >
< div class = "path" > LoginHub — < code > /loginhub</ code ></ div >
< div class = "detail" > WS + JWT,用于大厅资产同步、全服广播、邮件通知</ div >
</ div >
< h3 > 📤 客户端 → 服务器</ h3 >
< table >
< tr >< th style = "width:80px" > 方法</ th >< th style = "width:280px" > 参数</ th >< th > 说明</ th ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > Bind</ td >< td >< code > string userId</ code ></ td >< td > 必须:连接后立即调用,将连接与 userId 绑定。服务端加入 userId 命名的 SignalR 组,后续定向推送通过此组发送。</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-cs" > C→S</ span > DismissPendingRoom< span class = "tag-new" > NEW</ span ></ td >< td >< code > string userId</ code ></ td >< td > 用户拒绝返回断线房间。登录后若收到 PendingRoomNotice,用户选择"离开"时调用此方法,服务端清理旧房间残留。</ td ></ tr >
</ table >
< h3 > 📥 服务器 → 客户端</ h3 >
< table >
< tr >< th style = "width:150px" > 事件</ th >< th > 数据</ th >< th > 发送时机</ th ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > Broadcast</ td >
< td >< pre > {
UserName: string, // 昵称
AvatarUrl: string, // 头像URL
Level: int, // 等级
VipLevel: int, // VIP等级
Message: string, // 消息内容
Time: long // Unix时间戳(秒)
}</ pre ></ td >< td > 大喇叭全服广播</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > KillAnnouncement</ td >
< td >< pre > {
Text: string, // 公告文案: "恭喜玩家momo在风雷之渊高级场捕获 金龙*2000炮倍 100000000大奖"
UserName: string, // 玩家名: "momo"
GameTypeName: string, // 游戏类型: "风雷之渊"
RoomName: string, // 房间名: "高级场"
FishName: string, // 鱼名: "金龙"
Multiple: int, // 倍数: 2000
Gold: long, // 获得金币: 100000000
Time: long // Unix时间戳(秒)
}</ pre ></ td >< td > 全局击杀公告(特殊Boss击杀 / 机器人模拟击杀),走 LoginHub</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > AssetUpdated</ td >
< td >< pre > {
CurrencyType: int, // 货币类型: 1=金币 2=保险箱 3=钻石
ChangeType: int, // 变动类型: 1=增加 2=减少
Amount: long, // 变动数量
NewGold: long, // 最新金币
NewDiamond: int, // 最新钻石
Source: string, // 来源: "游戏金币结算"、"救济金"、"签到" 等
Remark: string // 备注(房间名等)
}</ pre ></ td >< td > 资产变更:退出房间结算 / 后台充值扣币 / 救济金 / 签到 / 捕鱼返利</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > LevelChanged< span class = "tag-new" > NEW</ span ></ td >
< td >< pre > {
NewLevel: int, // 最新等级
NewExp: long, // 最新经验
NewGold: long, // 最新金币
NewDiamond: int, // 最新钻石
Source: string, // 来源
Remark: string // 备注
}</ pre ></ td >< td > 后台修改玩家等级</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > BackpackUpdated</ td >
< td >< pre > {
Action: string, // "add" | "update" | "delete"
PropNumber: string, // 道具编号
Count: int // 数量
}</ pre ></ td >< td > 道具背包变更(后台操作)</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > NewEmail</ td >
< td >< pre > {
Title: string, // 邮件标题
EmailId: string // 邮件ID
}</ pre ></ td >< td > 收到新邮件通知</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > ChangeConfig< span class = "tag-new" > NEW</ span ></ td >
< td >< pre > changeType: int
1 = 活动配置更新
2 = 游戏配置更新</ pre ></ td >< td > 后台活动配置或游戏配置保存成功并发布 CAP 后,由 UserHost 通过 LoginHub 向所有在线玩家广播。前端收到后应按 changeType 重新请求对应配置。监听示例:< code > connection.on("ChangeConfig", changeType => { ... })</ code ></ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > AccountKicked< span class = "tag-new" > NEW</ span ></ td >
< td >< pre > {
userId: string // 被顶的玩家ID
}</ pre ></ td >< td > 账号被顶通知。该账号在其他设备登录时,旧连接收到此事件。前端应提示"账号已在其他地方登录"并清空房间 UI、断开游戏连接。</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > KickToMainPage< span class = "tag-new" > NEW</ span ></ td >
< td >< pre > {
userId: string // 被踢的玩家ID
}</ pre ></ td >< td > 强制退出到主页面。当前账号的连接已失效(被顶号/用户不匹配),前端收到后应立即清除所有游戏 UI 并导航到主页面/登录页。</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > PendingRoomNotice< span class = "tag-new" > NEW</ span ></ td >
< td >< pre > {
roomId: string, // 房间ID
gameType: string, // 游戏类型: "风雷之渊"
roomName: string // 房间名: "高级场"
}</ pre ></ td >< td > 登录后检测到有未完成的断线游戏。金币已结算至当前余额。前端应弹窗提示用户选择"返回房间"(调 Reconnect)或"离开"(调 DismissPendingRoom),60秒无操作自动离开。</ td ></ tr >
</ table >
</ div >
</ div >
<!-- ==================== FishGameHub ==================== -->
< div id = "hub-fishgame" class = "hub-section" >
< div class = "hub-card" >
< div class = "hub-header" style = "background:#27ae60;" >
< div class = "path" > FishGameHub — < code > /fishinghub</ code ></ div >
< div class = "detail" > WS,捕鱼游戏核心 Hub。连接后先调 Reconnect 绑定用户。</ div >
</ div >
< p style = "padding:12px 20px;margin:0;background:#f0faf0;border-bottom:1px solid #d0e8d0;" >< strong > 🤖 机器人说明:</ strong > 捕鱼房间中的机器人(< code > IsBot=true</ code > )由服务端完全模拟。客户端无需发送机器人指令,机器人在协议层面与真人一致——同样触发 RoomFireNotice、RoomKillNotice 等事件。</ p >
< h3 > 📤 客户端 → 服务器</ h3 >
< table >
< tr >< th style = "width:100px" > 方法</ th >< th style = "width:380px" > 参数</ th >< th > 返回</ th >< th > 说明</ th ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > Reconnect</ td >< td >< code > string userId</ code ></ td >< td > 无</ td >< td > 断线重连必须先调用。服务端返回 RoomInfo(含 activeFishes)并恢复房间状态</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > JoinRoom</ td >< td >< code > string roomId, string userId, int posituin</ code ></ td >< td > 无</ td >< td > 加入房间。posituin=-1 自动分配座位</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > FireCannon</ td >< td >< code > string roomId, string userId, long gold, int count, long timestamp, string sign, object? extra</ code ></ td >< td >< pre > { bullets: [{ bulletId: string, gold: long }], gold: long, timestamp: long }</ pre ></ td >< td > 发炮。每颗返回子弹携带本次发射炮倍;外层 gold 为玩家剩余金币。sign=MD5签名</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > KillFish</ td >< td >< code > string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign</ code ></ td >< td >< pre > { gold: long, timestamp: long }</ pre ></ td >< td > 击杀鱼</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > LeaveRoom</ td >< td >< code > string roomId, string userId</ code ></ td >< td > 无</ td >< td > 退出房间(金币结算后自动 Context.Abort 断开)</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > UseProp</ td >< td >< code > string roomId, string userId, string propNumber, int diamond=0</ code ></ td >< td >< code > int</ code > 剩余数量</ td >< td > 使用道具(钻石购买时传 diamond)。返回背包剩余数量,-1 表示失败。冰冻道具(40003)触发 FishFrozen 事件</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > ChangeGunMultiple</ td >< td >< code > string roomId, string userId, int multiple</ code ></ td >< td > 无</ td >< td > 更改炮倍</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GetRoomInfo</ td >< td >< code > string roomId</ code ></ td >< td > 无</ td >< td > 结果通过 GetRoomInfoResult 返回</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GetPlayerInfo</ td >< td >< code > string roomId, string userId</ code ></ td >< td > 无</ td >< td > 获取玩家信息</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > SyncGameData</ td >< td >< code > string roomId, object data</ code ></ td >< td > 无</ td >< td > 同步游戏数据</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-cs" > C→S</ span > SetPlayerAfk< span class = "tag-new" > NEW</ span ></ td >< td >< code > string roomId, string userId</ code ></ td >< td > 无</ td >< td > 玩家挂机,触发 PlayerAfkNotice</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-cs" > C→S</ span > CancelPlayerAfk< span class = "tag-new" > NEW</ span ></ td >< td >< code > string roomId, string userId</ code ></ td >< td > 无</ td >< td > 取消挂机,触发 PlayerCancelAfkNotice,返回 RoomInfo(含 activeFishes)同步当前鱼状态</ td ></ tr >
</ table >
< h3 > 📥 服务器 → 客户端</ h3 >
< table >
< tr >< th style = "width:170px" > 事件</ th >< th > 数据</ th >< th > 目标</ th >< th > 说明</ th ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > BindSuccess</ td >< td >< code > string</ code ></ td >< td > 调用者</ td >< td > 重连绑定成功</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > Exception</ td >< td >< pre > {
code: int, // 错误码(0=未知错误)
message: string // 错误描述
}</ pre >< br >< b > 错误码说明:</ b >< br > 1001=房间已满< br > 1002=金币不足< br > 1003=房间不存在< br > 1004=玩家不在房间< br > 1005=签名错误< br > 1006=请求已过期< br > 1007=鱼配置不存在< br > 1008=子弹参数无效< br > 1009=金币参数超出范围< br > 1011=用户不存在< br > 1012=道具配置不存在< br > 1013=道具数量不足< br > 1014=钻石不足< br > 1015=炮倍参数无效</ td >< td > 调用者</ td >< td > 错误通知。前端根据 code 处理业务逻辑</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomInfo</ td >< td >< pre > RedisGameRoom 扩展字段,包含首次及当前活跃鱼:
{
RoomId, RoomName, GameType, RoomLevel, RoomPartition,
RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
RobotCount, Players[], CreateUserId, SystemFactor,
activeFishes: [{ // 首次进房/后续进房/重连/取消挂机时包含
fishId: number,
fishIds: number[],
fishNumber: string,
routeId: number,
fishArrayNumber: string,
createdAt: number, // 毫秒
expireAt: number, // 毫秒
remainingSeconds: int
}]
2026-10-20 17:11:47 +08:00
}</ pre ></ td >< td > 调用者</ td >< td > 加入房间/断线重连/取消挂机返回房间信息,活跃鱼随本事件在 < code > activeFishes</ code > 中返回。< b > 注意</ b > :重连 / 取消挂机 / 挂起恢复场景会另行下发独立事件 < code > OnFishResync</ code > (全量替换语义),前端按该事件重建鱼群。</ td ></ tr >
2026-10-18 17:28:45 +08:00
< tr >< td >< span class = "tag tag-sc" > S→C</ span > SyncPlayerGold</ td >< td >< code > long</ code ></ td >< td > 调用者</ td >< td > 金币同步(金币≤0时触发)</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > SendPlayerInfoResult</ td >< td >< code > long?</ code ></ td >< td > 调用者</ td >< td > 玩家Gold值</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GetRoomInfoResult</ td >< td >< code > RedisGameRoom?</ code ></ td >< td > 调用者</ td >< td > 房间信息查询结果</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomKillNotice</ td >
< td >< pre > {
fishNumber: string, userId: string, gold: long,
fishId: string, timespan: long, killtime: long,
msg: string, // "时间:X秒"
beishu: double,
skillsGold: long, // 炮倍金币
fishScore: int,
param: object?, // Boss爆奖结果
drop: object? // Boss掉落 { propNumber, propName, count }
}</ pre ></ td >< td > 房间组</ td >< td > 击杀通知</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomFireNotice</ td >< td >< code > { userId, bulletIds[], gold, count, extra, isBot }</ code ></ td >< td > 房间组</ td >< td > 发炮通知</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomJoinNotice</ td >< td >< pre > {
userId: string, // 玩家ID
userName: string, // 玩家名字
userNumber: string, // 玩家编号
avatar: string, // 玩家头像
isRedName: bool, // 是否红名
level: int, // 玩家等级
exp: long, // 经验值
maxExp: long, // 当前等级最大经验值
vipLevel: int, // VIP等级
gold: long, // 金币
diamond: int, // 钻石
cannonId: string, // 炮台
wingsId: string, // 翅膀
warGodId: string, // 战神
cannonMultiple: int, // 炮倍
position: int, // 位置
beginCoin: long, // 进场金币
maxGold: long, // 最高金币
beginTime: long, // 进场时间
state: int, // 状态 0正常 1断线中
playerFactor: object, // 玩家因子
totalRechargeGold: long, // 累计充值
safeBoxGold: long, // 保险箱金币
isBot: bool, // 是否机器人
isAfk: bool // 是否挂机
}</ pre ></ td >< td > 房间组</ td >< td > 玩家加入</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomLeaveNotice</ td >< td >< code > string userId</ code ></ td >< td > 房间组</ td >< td > 玩家离开</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > BotLeaveRoomNotice< span class = "tag-new" > NEW</ span ></ td >< td >< pre > { userId: string }</ pre ></ td >< td > 房间组</ td >< td > 机器人退出房间</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > HostDisconnect< span class = "tag-new" > NEW</ span ></ td >< td > 无</ td >< td > 房间组</ td >< td > 房主断线,其他玩家可接受机器人控制权</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomDisconnectNotice</ td >< td >< code > string userId</ code ></ td >< td > 房间组</ td >< td > 玩家断线</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > ChangeGunMultipleNotice</ td >< td >< code > { userId, multiple }</ code ></ td >< td > 房间组</ td >< td > 炮倍变更</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > ChangeControllerUser</ td >< td >< code > string userId</ code ></ td >< td > 房间组</ td >< td > 房主控制权移交</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > UsePropNotice</ td >< td >< code > { userId, propNumber }</ code ></ td >< td > 房间组</ td >< td > 使用道具</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > SummonCardFishNotice< span class = "tag-new" > NEW</ span ></ td >< td >< pre > {
roomId: string, // 房间ID
userId: string, // 使用召唤卡的玩家ID
userNumber: string, // 玩家编号
userName: string, // 玩家昵称
fishType: int, // 2=Boss鱼,3=黄金鱼
fishTypeName: string, // "Boss鱼"或"黄金鱼"
fishNumber: string, // 召唤出的鱼配置编号
fishId: number // 本次生成的鱼实例ID
}</ pre ></ td >< td > 房间组</ td >< td > 召唤卡成功生成鱼后广播。鱼的路径和生命周期仍由 OnFishSpawn 处理;本事件用于显示“哪个玩家召唤了什么鱼”等提示。</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > PlayerAfkNotice< span class = "tag-new" > NEW</ span ></ td >< td >< code > { userId: string }</ code ></ td >< td > 房间组</ td >< td > 玩家挂机通知</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > PlayerCancelAfkNotice< span class = "tag-new" > NEW</ span ></ td >< td >< code > { userId: string }</ code ></ td >< td > 房间组</ td >< td > 取消挂机通知</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > TorpedoResultNotice< span class = "tag-new" > NEW</ span ></ td >< td >< pre > { userId: string, propNumber: string, gold: long, color: string }</ pre ></ td >< td > 房间组</ td >< td > 鱼雷使用结果通知</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > OnPlayerInfo< span class = "tag-new" > NEW</ span ></ td >< td >< pre > { userId, userName, gold, diamond, viplvl, cannonMultiple, cannonId, wingsId, position }</ pre ></ td >< td > 调用者</ td >< td > 玩家信息同步</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > SyncGameData< span class = "tag-new" > NEW</ span ></ td >< td >< code > object data</ code ></ td >< td > 房间组</ td >< td > 游戏数据同步(服务端主动推送)</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > FishFrozen< span class = "tag-new" > NEW</ span ></ td >< td > 无</ td >< td > 房间组</ td >< td > 冰冻道具(40003)生效,鱼存活时间延长 15 秒(客户端写死 15s 倒计时)</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > OnSeatChange</ td >< td >< pre > // type=join 时:
{ type: "join", playerId: string, position: int, playerName: string,
nickname: string, gold: long, diamond: int, vipLevel: int,
turretId: string, turretRate: int, turretLv: int, headId: string,
roomRate: int, bulletPower: int }
// type=leave 时:
{ type: "leave", userId: string, playerId: string,
position: int, showWaiting: bool? }</ pre ></ td >< td > 房间组</ td >< td > 座位变更(加入/离开)</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > OnFishSpawn< span class = "tag-new" > NEW</ span ></ td >< td >< pre > {
2026-10-20 17:11:47 +08:00
type: "init" | "spawn", // init=进房补发,spawn=定时出鱼(sync 已废弃:重连/恢复改走 OnFishResync)
2026-10-18 17:28:45 +08:00
timestamp: number, // 服务器当前时间戳(毫秒)
fish: [{
fishId: number, // 普通鱼ID;鱼阵时为首条普通鱼ID(兼容旧客户端)
fishNumber: string, // 普通鱼=鱼编号;鱼阵=""
routeId: number, // 普通鱼=路径编号;鱼阵=固定阵型编号(如71001)
fishArrayNumber: string,// 鱼阵编号(非鱼阵时为"")
fishIds: number[], // 所有出鱼必传;普通鱼=[fishId],鱼阵=阵内全部鱼ID
createdAt: number, // 鱼创建时间戳(毫秒),前端用它算路径进度
expireAt: number, // 鱼过期时间戳(毫秒)
remainingSeconds: int // 鱼剩余存活秒数
}]
2026-10-20 17:11:47 +08:00
}</ pre ></ td >< td > 房间组</ td >< td >< code > fishIds</ code > 在首次出鱼、定时出鱼、入房及重连同步中都必须提供。普通鱼为单元素集合 < code > [fishId]</ code > ;鱼阵按鱼阵配置的生成顺序使用 ID 集合。鱼阵的 < code > fishNumber</ code > 为空,鱼信息由 < code > fishArrayNumber</ code > 对应的本地配置确定。击中时把对应 ID 作为 KillFish 的 < code > fihsId</ code > 原样传回。</ td ></ tr >
< tr class = "row-new" >< td >< span class = "tag tag-sc" > S→C</ span > OnFishResync< span class = "tag-new" > NEW</ span ></ td >< td >< pre > {
roomId: string, // 房间ID
timestamp: number, // 服务器当前时间戳(毫秒)
serverTime: string, // "yyyy-MM-dd HH:mm:ss.fff"
count: number, // 鱼数量(鱼阵按阵型合并为一条)
fish: [{ // 与 OnFishSpawn 相同的鱼结构
fishId: number,
fishIds: number[],
fishNumber: string,
routeId: number,
fishArrayNumber: string,
createdAt: number, // 毫秒
expireAt: number, // 毫秒
remainingSeconds: int
}]
}</ pre ></ td >< td > 调用者</ td >< td >< b > 全量替换语义</ b > :客户端应< b > 先清空本地旧鱼</ b > ,再按 < code > fish</ code > 列表重建(不要把本事件当作增量补发)。用于断线重连、挂起恢复等场景,< b > 替代原 < code > OnFishSpawn(type=sync)</ code ></ b > ; < code > type=sync</ code > 已废弃。</ td ></ tr >
2026-10-18 17:28:45 +08:00
< tr >< td >< span class = "tag tag-sc" > S→C</ span > KillNotice</ td >< td >< pre > {
FishId: string, // 鱼编号
userName: string, // 玩家名
gold: long, // 金币
// 机器人模拟时额外字段:
fishName: string?, // 鱼名
multiplier: int? // 倍数
}</ pre ></ td >< td > 全部</ td >< td > 全局击杀公告(真人击杀/机器人模拟)</ td ></ tr >
</ table >
</ div >
</ div >
<!-- ==================== FishRoomHub ==================== -->
< div id = "hub-fishroom" class = "hub-section" >
< div class = "hub-card" >
< div class = "hub-header" style = "background:#e67e22;" >
< div class = "path" > FishRoomHub — < code > /fishingroomhub</ code ></ div >
< div class = "detail" > WS,房间列表</ div >
</ div >
< h3 > 📤 客户端 → 服务器</ h3 >
< table >
< tr >< th style = "width:110px" > 方法</ th >< th style = "width:380px" > 参数</ th >< th > 说明</ th ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GetRoomList</ td >< td >< code > string userId, int gameType, int roomLevel, int roomPartition</ code ></ td >< td > 获取房间列表。结果通过 GetRoomListResult 事件返回</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GetRoomInfo</ td >< td >< code > string roomId</ code ></ td >< td > 获取房间详情</ td ></ tr >
</ table >
< h3 > 📥 服务器 → 客户端</ h3 >
< table >
< tr >< th style = "width:170px" > 事件</ th >< th > 数据</ th >< th > 说明</ th ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GetRoomListResult</ td >< td >< code > List< GameRoomReponse> </ code >< br > GameRoomReponse: { RoomId, MaxPlayerCount, CurrentOnlinePlayerCount, RobotCount, RoomNumber }</ td >< td > 房间列表(仅统计真人)</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomChange</ td >< td >< code > RedisGameRoom</ code ></ td >< td > 房间变化通知(人数、状态变更)</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GetRoomInfoResult</ td >< td >< code > RedisGameRoom?</ code ></ td >< td > 房间详情</ td ></ tr >
</ table >
</ div >
</ div >
<!-- ==================== ChessGameHub ==================== -->
< div id = "hub-chessgame" class = "hub-section" >
< div class = "hub-card" >
< div class = "hub-header" style = "background:#8e44ad;" >
< div class = "path" > ChessGameHub — < code > /chessgamehub</ code ></ div >
< div class = "detail" > WS,中国象棋</ div >
</ div >
< p style = "padding:12px 20px;margin:0;background:#fff8e6;border-bottom:1px solid #f0d58a;" >< strong > 🏠 房间列表说明:</ strong > 大厅只返回后台预配置的固定数字房间号。服务端不会再创建随机 GUID 展示房间,并会过滤、清理“无房主、无玩家”的旧异常房间。前端收到 < code > GetRoomList</ code > 、< code > CreateRoomNotice</ code > 、< code > JoinRoomNotice</ code > 或 < code > ExitRoomNotice</ code > 后,应按 < code > roomId</ code > 更新同一条房间数据,不要追加重复房间。</ p >
< h3 > 📤 客户端 → 服务器</ h3 >
< table >
< tr >< th style = "width:120px" > 方法</ th >< th style = "width:380px" > 参数</ th >< th > 说明</ th ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > CreateRoom</ td >< td >< code > CreateRoomParam { roomId, userId, gameType(1=我方黑 2=我方红), password, gold }</ code ></ td >< td > 创建房间</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > JoinGame</ td >< td >< code > string gameId, string pwd, string userId</ code ></ td >< td > 加入游戏</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GameReady</ td >< td >< code > string gameId, string userId</ code ></ td >< td > 准备</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > EndGame</ td >< td >< code > string gameId, string winUserId</ code ></ td >< td > 直接结束(对方获胜)</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > RequestEndGame</ td >< td >< code > string gameId, string userId, string winUserId</ code ></ td >< td > 请求结束(需双方确认)</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > RestartGame</ td >< td >< code > string gameId, string userId</ code ></ td >< td > 对局结束后重新开始</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GoldConfig</ td >< td >< code > string gameId, long gold, string userId</ code ></ td >< td > 房主设置金币</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > ExitRoom</ td >< td >< code > string gameId, string userId</ code ></ td >< td > 退出(房主退出清空房间,非房主保留房间)</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > Heartbeat</ td >< td >< code > string gameId</ code ></ td >< td > 心跳,更新活跃时间</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > CheckTimeout</ td >< td >< code > string gameId</ code ></ td >< td > 检查超时(60秒断线判负)</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GetRoomList</ td >< td >< code > int partition=0</ code ></ td >< td > 房间列表;0=全部分区,正数=指定分区</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GetRoomState</ td >< td >< code > string gameId</ code ></ td >< td > 获取当前房间完整状态,用于返回场景或重连后主动校准</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > KickPlayer</ td >< td >< code > string gameId, string targetUserId</ code ></ td >< td > 房主在开局前踢出指定玩家</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > SyncGameData</ td >< td >< code > string gameId, string data</ code ></ td >< td > 同步棋子移动</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > GameOperation</ td >< td >< code > string gameId, int type, string data</ code >< br > type: 1=求和 2=认输(自动结束) 3=悔棋</ td >< td > 游戏操作</ td ></ tr >
</ table >
< h3 > 📥 服务器 → 客户端</ h3 >
< table >
< tr >< th style = "width:170px" > 事件</ th >< th > 数据</ th >< th > 目标</ th ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GameError</ td >< td >< code > string</ code ></ td >< td > 调用者</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > BeginGame</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 调用者/房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > JoinRoom</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 调用者</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > JoinRoomNotice</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 象棋大厅组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RoomStateNotice</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 房间组/调用者,房间完整状态</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GoldConfigRequired</ td >< td >< code > { targetUserId, room }</ code ></ td >< td > 房间组,两名真人到齐后提示指定红方设置金币</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > Reconnect</ td >< td >< code > { userId, userName }</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > Disconnect</ td >< td >< code > { userId, disconnectTime, timeoutSeconds }</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > ReadyNotice</ td >< td >< code > string userId</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > AllBeginTime</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GameEnd</ td >< td >< code > { winUserId, room }</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > EndGameRequestSent</ td >< td >< code > string</ code ></ td >< td > 调用者</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > OpponentRequestEnd</ td >< td >< code > string userId</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RestartGameSuccess</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > RestartRoomNotice</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 全部</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > ExitRoomNotice</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 象棋大厅组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > ExitRoomSuccess</ td >< td >< code > string userId</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > KickedFromRoom</ td >< td >< code > { gameId, userId }</ code ></ td >< td > 被踢玩家</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > CreateRoomNotice</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 象棋大厅组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GoldConfigSuccess</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GoldConfigRoomNotice</ td >< td >< code > ChessGameRoom</ code ></ td >< td > 全部</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GetRoomList</ td >< td >< code > List< ChessGameRoom> </ code ></ td >< td > 调用者;仅固定房间,按 roomId 覆盖本地列表</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > SyncGameData</ td >< td >< code > string</ code ></ td >< td > 全部</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > GameOperation</ td >< td >< code > { type, data }</ code ></ td >< td > 全部</ td ></ tr >
</ table >
</ div >
</ div >
<!-- ==================== CustomerHub ==================== -->
< div id = "hub-customer" class = "hub-section" >
< div class = "hub-card" >
< div class = "hub-header" style = "background:#16a085;" >
< div class = "path" > CustomerHub — < code > /customerHub</ code ></ div >
< div class = "detail" > WS,客服即时通讯。客服端需 JWT,玩家端无需 Token。</ div >
</ div >
< h3 > 📤 客户端 → 服务器</ h3 >
< table >
< tr >< th style = "width:140px" > 方法</ th >< th style = "width:380px" > 参数</ th >< th > 说明</ th ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > JoinConversation</ td >< td >< code > string conversationId</ code ></ td >< td > 加入会话房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > LeaveConversation</ td >< td >< code > string conversationId</ code ></ td >< td > 离开会话房间组</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > SendMessage</ td >< td >< code > string conversationId, int senderType, string senderId, string senderName, string? senderAvatar, int contentType, string content, string guId=""</ code >< br > senderType: 1=用户 2=客服< br > contentType: 1=文字 2=图片< br > conversationId 为空时自动创建新会话</ td >< td > 发送消息</ td ></ tr >
< tr >< td >< span class = "tag tag-cs" > C→S</ span > StartConversation</ td >< td >< code > string userId, string userName</ code ></ td >< td > 主动发起新会话</ td ></ tr >
</ table >
< h3 > 📥 服务器 → 客户端</ h3 >
< table >
< tr >< th style = "width:170px" > 事件</ th >< th > 数据</ th >< th > 目标</ th ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > OnMessageReceived</ td >
< td >< pre > {
GuId: string,
ConversationId: string,
SenderType: int,
SenderId: string,
SenderName: string,
SenderAvatar: string?,
ContentType: int,
Content: string,
CreateTime: DateTime
}</ pre ></ td >< td > 会话组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > OnConversationStarted</ td >< td >< code > ConversationEntity</ code ></ td >< td > 调用者</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > OnNewConversation</ td >< td >< code > { conversationId, customerId?, customerName?, userId?, userName?, status, createTime }</ code ></ td >< td > 客服组</ td ></ tr >
< tr >< td >< span class = "tag tag-sc" > S→C</ span > Exception</ td >< td >< pre > {
code: int,
message: string
}</ pre ></ td >< td > 调用者</ td >< td > 错误通知,code 同上表</ td ></ tr >
</ table >
< p style = "padding:8px 12px;margin:10px 0 0;background:#f0faf0;border-radius:4px;" >< strong > 注意:</ strong > 客服连接时携带 JWT,服务端自动从 < code > uid</ code > Claim 加入通知组,无需手动 JoinCustomerQueue。</ p >
</ div >
</ div >
<!-- ==================== 公共说明 ==================== -->
< div id = "hub-common" class = "hub-section" >
< div class = "info-box" >
< h3 > 🔗 Hub 路由总览</ h3 >
< table >
< tr >< th > Hub</ th >< th > 路由</ th >< th > 认证</ th ></ tr >
< tr >< td > LoginHub</ td >< td >< code > /loginhub</ code ></ td >< td > JWT</ td ></ tr >
< tr >< td > FishGameHub</ td >< td >< code > /fishinghub</ code ></ td >< td > 方法传参 userId</ td ></ tr >
< tr >< td > FishRoomHub</ td >< td >< code > /fishingroomhub</ code ></ td >< td > 方法传参 userId</ td ></ tr >
< tr >< td > ChessGameHub</ td >< td >< code > /chessgamehub</ code ></ td >< td > 方法传参 userId</ td ></ tr >
< tr >< td > CustomerHub</ td >< td >< code > /customerHub</ code ></ td >< td > 客服需JWT,玩家无需</ td ></ tr >
</ table >
</ div >
< div class = "info-box" >
< h3 > 📦 通用数据结构</ h3 >
< table >
< tr >< th style = "width:180px" > 名称</ th >< th > 字段</ th ></ tr >
< tr >< td >< code > RedisGameRoom</ code ></ td >
< td >< pre > {
RoomId, RoomName, GameType, RoomLevel, RoomPartition,
RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
RobotCount, Players[], CreateUserId,
SystemFactor: double // 当前系统因子(根据当日盈利动态调整)
}</ pre ></ td ></ tr >
< tr >< td >< code > RedisGamePlayer</ code ></ td >
< td >< pre > {
UserId, UserName, UserNumber, Avatar, IsRedName,
Level, VipLevel, Gold, Diamond, Position,
CannonId, WingsId, WarGodId, CannonMultiple,
BeginCoin, MaxGold, BeginTime, State(0=正常 1=断线),
CId, PlayerFactor, TotalRechargeGold, SafeBoxGold,
IsBot, IsAfk
}</ pre ></ td ></ tr >
< tr >< td >< code > ChessGameRoom</ code ></ td >
< td >< pre > {
RoomId, state(1=等待 2=游戏中 3=已结束), users[],
gameType(1=我方黑 2=我方红), gold, password,
createUserId, robotCount, isGameEnded,
timeoutSeconds(默认60), endGameRequests, pendingWinUserId
}</ pre ></ td ></ tr >
< tr >< td >< code > ConversationEntity</ code ></ td >
< td >< pre > {
UserId?, UserName?, CustomerId?, CustomerName?,
Status(0=等待 1=进行中 2=已结束), CreateTime,
EndTime?, LastReadTime?
}</ pre ></ td ></ tr >
</ table >
</ div >
< div class = "info-box" >
< h3 > 📤 发送基础设施(IHubSend< T> ) </ h3 >
< table >
< tr >< th > 方法</ th >< th > 说明</ th ></ tr >
< tr >< td >< code > SendUserCId(cid, method, obj)</ code ></ td >< td > 发送给指定连接</ td ></ tr >
< tr >< td >< code > SendUserUserId(userId, method, obj)</ code ></ td >< td > 通过 userId 查找 CId 后发送</ td ></ tr >
< tr >< td >< code > SendGroup(groupId, method, obj)</ code ></ td >< td > 发送给 SignalR 群组</ td ></ tr >
< tr >< td >< code > SendAll(method, obj)</ code ></ td >< td > 广播给所有客户端</ td ></ tr >
< tr >< td >< code > GroupExcept(cid, groupId, method, obj)</ code ></ td >< td > 发送给群组排除指定连接</ td ></ tr >
</ table >
</ div >
< div class = "info-box" >
< h3 > 流程说明</ h3 >
< h4 > 1. 登录 / Bind 流程 (LoginHub)</ h4 >
< ol >
< li > 客户端连接 /loginhub (JWT)</ li >
< li > 调用 Bind(userId),加入 userId 组</ li >
< li > Bind 读取 Redis LoginCId:
< ul >
< li > 若存在旧 LoginCId 且不等于当前连接 -> 顶号:
< ul >
< li > 发 AccountKicked 给旧 LoginHub 连接</ li >
< li > 查旧 LoginHub 关联的游戏 Hub CId,发 KickToMainPage</ li >
</ ul >
</ li >
< li > 更新 Redis LoginCId = 当前连接 CId</ li >
</ ul >
</ li >
< li > 检测断线房间 (PendingRoomId):
< ul >
< li > 有 -> 发 PendingRoomNotice(弹窗:返回房间 / 离开)</ li >
< li > 无 -> 进入大厅</ li >
</ ul >
</ li >
</ ol >
< h4 > 2. 顶号流程</ h4 >
< ul >
< li > 设备 A 登录:Bind -> LoginCId = cid_A</ li >
< li > 设备 B 登录:Bind -> LoginCId = cid_A != cid_B -> 发 AccountKicked 到 cid_A -> 更新 LoginCId = cid_B -> A 被顶</ li >
< li > 设备 C 登录:Bind -> LoginCId = cid_B != cid_C -> 发 AccountKicked 到 cid_B -> 更新 LoginCId = cid_C -> B 被顶</ li >
< li > 关键:仅 LoginHub.Bind 负责发 AccountKicked, FishGameHub 不再发送</ li >
</ ul >
< h4 > 3. JoinRoom 流程 (FishGameHub)</ h4 >
< ol >
< li > 客户端调 JoinRoom(roomId, userId, posituin)</ li >
< li > 检查玩家是否已在房间 -> 是则返回</ li >
< li > 获取/创建房间 roomInfo</ li >
< li > 判断是否首个真人进房:
< ul >
< li > 是 -> RegisterRoom(roomId, roomLevel) 注册出鱼服务</ li >
< li > 否 -> GetActiveFishes(roomId) 获取当前活跃鱼列表</ li >
</ ul >
</ li >
< li > 发 RoomInfo(cid, room, activeFishes) 给调用者</ li >
< li > 发 OnSeatChange(join) 通知房间组</ li >
</ ol >
< h4 > 4. Reconnect 流程 (FishGameHub)</ h4 >
< ol >
< li > 客户端连接 FishGameHub 后立即调 Reconnect(userId)</ li >
< li > 查 Redis 有无断线房间 (PendingRoomId):
< ul >
< li > 有 -> 恢复玩家状态 State=0,走 JoinRoom 逻辑(含鱼同步)</ li >
< li > 无 -> 无操作,等待后续指令</ li >
</ ul >
</ li >
2026-10-20 17:11:47 +08:00
< li > 返回结果:RoomInfo(首个真人注册后的首次鱼群放入 activeFishes;重连/恢复场景另行下发 OnFishResync 全量替换)</ li >
2026-10-18 17:28:45 +08:00
</ ol >
< h4 > 5. CancelPlayerAfk 流程</ h4 >
< ol >
< li > 客户端调 CancelPlayerAfk(roomId, userId)</ li >
< li > 清除玩家 IsAfk 标记</ li >
< li > GetActiveFishes(roomId) 获取当前活跃鱼</ li >
< li > 发 RoomInfo(cid, room, activeFishes) 给调用者(同步鱼状态)</ li >
< li > 发 PlayerCancelAfkNotice 通知房间组</ li >
</ ol >
< h4 > 6. LeaveRoom / 退出流程</ h4 >
< ol >
< li > 客户端调 LeaveRoom(roomId, userId)</ li >
< li > 金币结算,从房间移除玩家</ li >
< li > 发 RoomLeaveNotice 通知房间组</ li >
< li > 检查剩余真人:
< ul >
< li > 无真人 -> UnregisterRoom(roomId) 注销出鱼 + 清理房间</ li >
< li > 有真人 -> 房主转移(若原房主退出),发 ChangeControllerUser</ li >
</ ul >
</ li >
</ ol >
< h4 > 7. 房主断线 / 所有权转移</ h4 >
< ol >
< li > 房主断线触发 HandleDisconnect</ li >
< li > 查找下一个非挂机真人作为新房主:
< ul >
< li > 找到 -> 转移所有权,发 ChangeControllerUser(新房主 userId) 通知房间组</ li >
< li > 未找到 -> 发 HostDisconnect(前端禁用发炮/出鱼,显示等待提示)</ li >
</ ul >
</ li >
</ ol >
< h4 > 8. 出鱼服务 (FishSpawnService)</ h4 >
< ul >
< li > 首个真人进房 -> RegisterRoom -> 启动定时器持续出鱼</ li >
< li > 定时出鱼 -> OnFishSpawn(type=spawn) 广播房间组</ li >
2026-10-20 17:11:47 +08:00
< li > 后续真人进房 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回;重连 / 取消挂机 / 挂起恢复 -> 额外下发独立事件 OnFishResync(全量替换)</ li >
2026-10-18 17:28:45 +08:00
< li > 最后一个真人退出/断线 -> UnregisterRoom -> 停止定时器 + 清除活跃鱼</ li >
</ ul >
</ div >
</ div >
</ div > <!-- /main -->
</ div > <!-- /layout -->
< script >
function switchTab ( el ) {
document . querySelectorAll ( '.nav-item' ). forEach ( function ( n ) { n . classList . remove ( 'active' ); });
el . classList . add ( 'active' );
document . querySelectorAll ( '.hub-section' ). forEach ( function ( s ) { s . classList . remove ( 'active' ); });
var target = document . getElementById ( el . getAttribute ( 'data-target' ));
if ( target ) target . classList . add ( 'active' );
}
</ script >
</ body >
</ html >