Files
BuYuHouDuan/AdminSrc/AdminHost/wwwroot/admin/dist/views/gameconfig/socket.html
T

611 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: 100vh; }
.sidebar { width: 220px; background: #1e2a3a; color: #fff; overflow-y: auto; flex-shrink: 0; }
.sidebar .logo { padding: 20px; font-size: 18px; font-weight: bold; text-align: center; border-bottom: 1px solid #2a3a4a; }
.sidebar .nav-item { padding: 12px 20px; cursor: pointer; font-size: 14px; border-left: 3px 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: 10px; padding: 0 8px; font-size: 11px; line-height: 18px; }
.main { flex: 1; overflow-y: auto; padding: 20px 30px; }
.hub-section { display: none; }
.hub-section.active { display: block; }
.hub-card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 25px; overflow: hidden; }
.hub-header { padding: 18px 20px; color: #fff; }
.hub-header .path { font-size: 20px; font-weight: bold; }
.hub-header .detail { font-size: 13px; opacity: .9; margin-top: 6px; }
h2 { font-size: 18px; margin: 25px 0 12px; padding-left: 12px; border-left: 4px solid #3498db; }
h3 { font-size: 15px; margin: 20px 0 10px; color: #555; }
table { width: 100%; border-collapse: collapse; margin-bottom: 5px; }
th { background: #f0f4f8; text-align: left; padding: 10px 12px; font-size: 13px; color: #555; border-bottom: 2px solid #ddd; }
td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 13px; vertical-align: top; }
tr:hover td { background: #f8faff; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-right: 4px; }
.tag-cs { background: #e3f2fd; color: #1565c0; }
.tag-sc { background: #fce4ec; color: #c62828; }
.tag-new { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: bold; background: #e74c3c; color: #fff; margin-left: 4px; vertical-align: middle; }
tr.row-new { border-left: 4px solid #e74c3c; background: #fff5f5 !important; }
tr.row-new td { background: #fff5f5 !important; }
tr.row-new:hover td { background: #ffe8e8 !important; }
code { background: #f0f0f0; padding: 1px 5px; border-radius: 3px; font-size: 12px; color: #c7254e; }
pre { background: #f8f9fa; padding: 12px; border-radius: 4px; overflow-x: auto; font-size: 12px; line-height: 1.6; border: 1px solid #eee; }
.info-box { background: #eef9ff; border: 1px solid #b3d8ef; border-radius: 6px; padding: 18px 22px; margin-bottom: 20px; }
ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
.top-bar { background: #fff; border-radius: 8px; padding: 18px 22px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.top-bar h1 { font-size: 22px; color: #2c3e50; }
.top-bar p { color: #888; font-size: 13px; margin-top: 6px; }
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
</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 =&gt; { ... })</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 / ErrorMessage</td><td><code>{ code: int, message: string }</code></td><td>调用者</td><td>错误通知</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
}]
}</pre></td><td>调用者</td><td>加入房间/断线重连/取消挂机返回房间信息。先用 activeFishes 初始化完整鱼群;服务端约 300ms 后仅兼容补发普通散鱼的 OnFishSpawn(init),Boss、黄金鱼、特殊鱼和鱼阵不会重复补发。</td></tr>
<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>{
type: "init" | "spawn" | "sync", // init=进房补发,spawn=定时出鱼,sync=主动同步
timestamp: number, // 服务器当前时间戳(毫秒)
fish: [{
fishId: number, // 普通鱼ID;鱼阵时为首条普通鱼ID(兼容旧客户端)
fishNumber: string, // 普通鱼=鱼编号;鱼阵=""
routeId: number, // 普通鱼=路径编号;鱼阵=固定阵型编号(如71001)
fishArrayNumber: string,// 鱼阵编号(非鱼阵时为"")
fishIds: number[], // 所有出鱼必传;普通鱼=[fishId],鱼阵=阵内全部鱼ID
createdAt: number, // 鱼创建时间戳(毫秒),前端用它算路径进度
expireAt: number, // 鱼过期时间戳(毫秒)
remainingSeconds: int // 鱼剩余存活秒数
}]
}</pre></td><td>房间组(init/spawn)/调用者(sync)</td><td><code>fishIds</code> 在首次出鱼、定时出鱼、入房及重连同步中都必须提供。普通鱼为单元素集合 <code>[fishId]</code>;鱼阵按鱼阵配置的生成顺序使用 ID 集合。鱼阵的 <code>fishNumber</code> 为空,鱼信息由 <code>fishArrayNumber</code> 对应的本地配置确定。击中时把对应 ID 作为 KillFish 的 <code>fihsId</code> 原样传回。</td></tr>
<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&lt;GameRoomReponse&gt;</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>
<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> 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> 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> 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&lt;ChessGameRoom&gt;</code></td><td>调用者</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> ErrorMessage</td><td><code>string</code></td><td>调用者</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&lt;T&gt;)</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>
<li>返回结果:RoomInfo(首次完整鱼群放入 activeFishes;约 300ms 后仅兼容补发普通散鱼,Boss、黄金鱼、特殊鱼和鱼阵不重复补发)</li>
</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>
<li>后续真人进房/重连/取消挂机 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回,并约 300ms 后仅补发普通散鱼 OnFishSpawn(type=init)</li>
<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>