后台界面同步:以线上 /mnt/web/admin 为准,补入仓库缺失/更新的页面(mall、socket、琉璃树等)
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
<!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 => { ... })</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<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>
|
||||
|
||||
<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<ChessGameRoom></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<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>
|
||||
<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>
|
||||
@@ -0,0 +1,125 @@
|
||||
|
||||
<title>钻石充值列表</title>
|
||||
|
||||
<div class="layui-card layadmin-header">
|
||||
<div class="layui-breadcrumb" lay-filter="breadcrumb">
|
||||
<a lay-href="">主页</a>
|
||||
<a><cite>商城</cite></a>
|
||||
<a><cite>钻石充值列表</cite></a>
|
||||
</div>
|
||||
</div>
|
||||
<table id="playerconfig" lay-filter="playerconfig"></table>
|
||||
<script type="text/html" id="table-admin">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit"><i class="layui-icon layui-icon-edit"></i>修改</a>
|
||||
</script>
|
||||
<script>
|
||||
layui.use(['table', 'admin'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, view = layui.view
|
||||
, table = layui.table
|
||||
, setter = layui.setter
|
||||
, form = layui.form;
|
||||
|
||||
|
||||
//第一个实例
|
||||
|
||||
|
||||
table.render({
|
||||
elem: '#playerconfig',
|
||||
url: setter.baseurl + "adminapi/mall/GetOrderList",
|
||||
method: "post",
|
||||
contentType: "application/json",
|
||||
page: true,
|
||||
where: {
|
||||
keyWord: "",
|
||||
state: -1
|
||||
},
|
||||
request: {
|
||||
pageName: 'pageIndex' //页码的参数名称,默认:page
|
||||
, limitName: 'pageSize' //每页数据量的参数名,默认:limit
|
||||
},
|
||||
limit: 10,
|
||||
headers: {
|
||||
'token': layui.data(setter.tableName)[setter.request.tokenName]
|
||||
},
|
||||
cols: [
|
||||
[
|
||||
{
|
||||
field: "name",
|
||||
title: "用户名",
|
||||
templet: function (d) {
|
||||
|
||||
return d.createInfo.userName
|
||||
}
|
||||
}, {
|
||||
field: "productName",
|
||||
title: "商品",
|
||||
templet: function (d) {
|
||||
return d.productInfo.name
|
||||
}
|
||||
}, {
|
||||
field: "price",
|
||||
title: "单价"
|
||||
},
|
||||
{
|
||||
field: "payCount",
|
||||
title: "购买数量"
|
||||
},
|
||||
{
|
||||
field: "payMoney",
|
||||
title: "支付总额"
|
||||
}
|
||||
, {
|
||||
field: "registerTime",
|
||||
title: "购买时间",
|
||||
templet: function (d) {
|
||||
return formatTimestamp(d.createInfo.optionTime);
|
||||
}
|
||||
}]
|
||||
],
|
||||
text: "对不起,加载出现异常!",
|
||||
response: {
|
||||
statusName: 'code', //数据状态的字段名称,默认:code
|
||||
statusCode: 0, //成功的状态码,默认:0
|
||||
msgName: 'info', //状态信息的字段名称,默认:msg
|
||||
countName: 'count', //数据总数的字段名称,默认:count
|
||||
dataName: 'data' //数据列表的字段名称,默认:data
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
// 将时间戳(秒)转换为指定格式的时间
|
||||
function formatTimestamp(timestamp, format = 'yyyy-MM-dd hh:mm:ss') {
|
||||
if (!timestamp) return '';
|
||||
|
||||
// 时间戳是秒数,需要转为毫秒
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
||||
const o = {
|
||||
'M+': date.getMonth() + 1, // 月份
|
||||
'd+': date.getDate(), // 日
|
||||
'h+': date.getHours(), // 小时
|
||||
'm+': date.getMinutes(), // 分
|
||||
's+': date.getSeconds(), // 秒
|
||||
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
|
||||
'S': date.getMilliseconds() // 毫秒
|
||||
};
|
||||
|
||||
if (/(y+)/.test(format)) {
|
||||
format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
|
||||
}
|
||||
|
||||
for (const k in o) {
|
||||
if (new RegExp('(' + k + ')').test(format)) {
|
||||
format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ?
|
||||
(o[k]) : (('00' + o[k]).substr(('' + o[k]).length)));
|
||||
}
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,251 @@
|
||||
|
||||
|
||||
<div class="layui-form" lay-filter="layuiadmin-form-admin" style="padding: 20px 30px 0 0;">
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">产品名称</label>
|
||||
<div class="layui-input-inline">
|
||||
<script type="text/html" template>
|
||||
<input type="text" name="id" value="{{ d.params.id}}" class="layui-input" style="display:none">
|
||||
</script>
|
||||
<script type="text/html" template>
|
||||
<input type="text" name="name" value="{{ d.params.name || '' }}" lay-verify="required" placeholder="请输入产品名称" autocomplete="off" class="layui-input">
|
||||
</script>
|
||||
|
||||
<script type="text/html" lay-done="sendParams(d.params)" template>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">图片</label>
|
||||
<div class="layui-input-inline">
|
||||
<script type="text/html" template>
|
||||
<button type="button" class="layui-btn" id="layuiadmin-upload-admin">
|
||||
<i class="layui-icon layui-icon-upload"></i> 图片上传
|
||||
</button>
|
||||
<div style="width: 132px;">
|
||||
<div class="layui-upload-list">
|
||||
<img class="layui-upload-img" id="ID-upload-demo-img" src="{{ d.params.imageUrl || '' }}" style="width: 100%; height: 92px;">
|
||||
<div id="ID-upload-demo-text"></div>
|
||||
<input type="text" name="imageUrl" id="ID-upload-demo-img-v" value="{{ d.params.imageUrl || '' }}" style="display:none" />
|
||||
</div>
|
||||
<div class="layui-progress layui-progress-big" lay-showPercent="yes" lay-filter="filter-demo">
|
||||
<div class="layui-progress-bar" lay-percent=""></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">道具</label>
|
||||
<div class="layui-input-inline">
|
||||
<script type="text/html" template>
|
||||
<select name="baseProduct" id="baseProduct" class="layui-select" lay-filter="baseProduct">
|
||||
|
||||
<option value="" {{ d.params.detail?.productInfo.number==''?"selected":"" }}> 未选择</option>
|
||||
</select>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" template>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">现金</label>
|
||||
<div class="layui-input-inline">
|
||||
|
||||
<input type="number" name="moneyPrice" value="{{d.params.moneySaleInfo?.price??0}}" placeholder="金额" autocomplete="off" step="0" class="layui-input">
|
||||
|
||||
|
||||
</div>
|
||||
<label class="layui-form-label">现金限购</label>
|
||||
<div class="layui-input-inline">
|
||||
|
||||
|
||||
<input type="number" name="moneyLimitCount" value="{{d.params.moneySaleInfo?.limitCount??0}}" placeholder="限购" autocomplete="off" step="0" class="layui-input">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">钻石</label>
|
||||
<div class="layui-input-inline">
|
||||
|
||||
<input type="number" name="diamondPrice" value="{{d.params.diamondSaleInfo?.price??0}}" placeholder="金额" autocomplete="off" step="0" class="layui-input">
|
||||
|
||||
|
||||
</div>
|
||||
<label class="layui-form-label">钻石限购</label>
|
||||
<div class="layui-input-inline">
|
||||
|
||||
<input type="number" name="diamondLimitCount" value="{{d.params.diamondSaleInfo?.limitCount??0}}" placeholder="限购" autocomplete="off" step="0" class="layui-input">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">积分</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="number" name="scorePrice" value="{{d.params.scoreSaleInfo?.price??0}}" placeholder="金额" autocomplete="off" step="0" class="layui-input">
|
||||
|
||||
|
||||
</div>
|
||||
<label class="layui-form-label">积分限购</label>
|
||||
<div class="layui-input-inline">
|
||||
|
||||
|
||||
<input type="number" name="scoreLimitCount" value="{{d.params.scoreSaleInfo?.limitCount??0}}" placeholder="限购" autocomplete="off" step="0" class="layui-input">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label"></label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="button" lay-submit lay-filter="LAY-product-back-submit" value="确认" class="layui-btn">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
var productId = '';
|
||||
var isLayUI = false;
|
||||
|
||||
function sendParams(params) {
|
||||
|
||||
productId = params.productInfo.number;
|
||||
if (isLayUI) {
|
||||
layuiLoad();
|
||||
}
|
||||
|
||||
}
|
||||
layuiLoad();
|
||||
function layuiLoad() {
|
||||
|
||||
layui.use(['admin', 'form', 'upload'], function () {
|
||||
var $ = layui.$
|
||||
, form = layui.form
|
||||
, admin = layui.admin
|
||||
, setter = layui.setter
|
||||
, upload = layui.upload
|
||||
, element = layui.element;
|
||||
bindSelect();
|
||||
|
||||
function bindSelect() {
|
||||
admin.req({
|
||||
method: "get",
|
||||
contentType: "application/json;charset=UTF-8",
|
||||
dataType: "json",
|
||||
|
||||
url: setter.baseurl + "adminapi/GameConfig",//实际使用请改成服务端真实接口
|
||||
async: false,
|
||||
done: function (res) {
|
||||
|
||||
$('#baseProduct').append("<optgroup label='宝箱' selected>");
|
||||
res.data.boxConfig.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
|
||||
$('#baseProduct').append("<optgroup label='钻石'>");
|
||||
res.data.diamondRecharge.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
|
||||
$('#baseProduct').append("<optgroup label='鱼食'>");
|
||||
res.data.fishFood.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
$('#baseProduct').append("<optgroup label='鱼苗'>");
|
||||
res.data.fishSeeds.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
|
||||
$('#baseProduct').append("<optgroup label='锦鲤碎片'>");
|
||||
res.data.fishFragments.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
|
||||
|
||||
$('#baseProduct').append("<optgroup label='养护道具'>");
|
||||
res.data.careProp.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
$('#baseProduct').append("<optgroup label='装饰道具'>");
|
||||
res.data.decorations.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
$('#baseProduct').append("<optgroup label='锦鲤'>");
|
||||
res.data.fishConfig.forEach(function (option) {
|
||||
$('#baseProduct').append("<option value='" + option.number + "'" + (option.number == productId ?"selected":"") + " >" + option.name + "</option>");
|
||||
|
||||
});
|
||||
$('#baseProduct').append("</optgroup>");
|
||||
form.render('select');
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
form.on('select(baseProduct)', function (data) {
|
||||
//console.log(data.elem); //得到select原始DOM对象
|
||||
//console.log(data.value); //得到被选中的值
|
||||
console.log(data.othis); //得到美化后的DOM对象
|
||||
});
|
||||
var uploadInst = upload.render({
|
||||
elem: '#layuiadmin-upload-admin',
|
||||
url: setter.baseurl + "adminapi/ImageUpload/upload", // 实际使用时改成您自己的上传接口即可。
|
||||
before: function (obj) {
|
||||
obj.preview(function (index, file, result) {
|
||||
$('#ID-upload-demo-img').attr('src', result); // 图片链接(base64)
|
||||
});
|
||||
|
||||
|
||||
element.progress('filter-demo', '0%'); // 进度条复位
|
||||
layer.msg('上传中', { icon: 16, time: 0 });
|
||||
},
|
||||
done: function (res) {
|
||||
// 若上传失败
|
||||
if (res.code > 0) {
|
||||
return layer.msg('上传失败');
|
||||
}
|
||||
|
||||
console.log($('#ID-upload-demo-img-v').val())
|
||||
// 上传成功的一些操作
|
||||
$('#ID-upload-demo-img-v').val(setter.baseurl + res.path);
|
||||
$('#ID-upload-demo-text').html(res.path); // 置空上传失败的状态
|
||||
},
|
||||
error: function () {
|
||||
// 演示失败状态,并实现重传
|
||||
var demoText = $('#ID-upload-demo-text');
|
||||
demoText.html('<span style="color: #FF5722;">上传失败</span> <a class="layui-btn layui-btn-xs demo-reload">重试</a>');
|
||||
demoText.find('.demo-reload').on('click', function () {
|
||||
uploadInst.upload();
|
||||
});
|
||||
},
|
||||
// 进度条
|
||||
progress: function (n, elem, e) {
|
||||
element.progress('filter-demo', n + '%'); // 可配合 layui 进度条元素使用
|
||||
if (n == 100) {
|
||||
layer.msg('上传完毕', { icon: 1 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
<title>商品列表</title>
|
||||
|
||||
<div class="layui-card layadmin-header">
|
||||
<div class="layui-breadcrumb" lay-filter="breadcrumb">
|
||||
<a lay-href="">主页</a>
|
||||
<a><cite>商城</cite></a>
|
||||
<a><cite>商品列表</cite></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
<div style="padding-bottom: 10px;">
|
||||
<button class="layui-btn layuiadmin-btn-admin" data-type="add" >添加</button>
|
||||
</div>
|
||||
|
||||
<table id="LAY-user-back-manage" lay-filter="LAY-user-back-manage"></table>
|
||||
|
||||
<script type="text/html" id="table-product-admin">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit"><i class="layui-icon layui-icon-edit"></i>编辑</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del"><i class="layui-icon layui-icon-delete"></i>删除</a>
|
||||
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="state"><i class="layui-icon layui-icon-edit"></i>上下架</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
layui.use('productlistjs', layui.factory('productlistjs')).use(['admin', 'table'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, view = layui.view
|
||||
, table = layui.table
|
||||
, setter = layui.setter
|
||||
, form = layui.form;
|
||||
|
||||
form.render(null, 'layadmin-userfront-formlist');
|
||||
|
||||
|
||||
//事件
|
||||
var active = {
|
||||
add: function () {
|
||||
|
||||
admin.popup({
|
||||
title: '添加商品'
|
||||
, area: ["500px", "800px"]
|
||||
, id: 'LAY-popup-product-add'
|
||||
, success: function (layero, index) {
|
||||
view(this.id).render('mall/productform').done(function () {
|
||||
form.render(null, 'layuiadmin-form-admin');
|
||||
|
||||
|
||||
//监听提交
|
||||
form.on('submit(LAY-product-back-submit)', function (data) {
|
||||
var aa = $('#baseProduct').val();
|
||||
console.log(aa)
|
||||
console.log(data)
|
||||
var param = {
|
||||
|
||||
name: data.field.name,
|
||||
imageUrl: data.field.imageUrl,
|
||||
detailType: parseInt(data.field.detailType),
|
||||
|
||||
number: data.field.baseProduct
|
||||
};
|
||||
|
||||
param.moneySaleInfo = { limitCount: data.field.moneyLimitCount,price: parseFloat(data.field.moneyPrice) }
|
||||
param.diamondSaleInfo = { limitCount: data.field.diamondLimitCount, price: parseFloat(data.field.diamondPrice) }
|
||||
param.scoreSaleInfo = { limitCount: data.field.scoreLimitCount, price: parseFloat(data.field.scorePrice) }
|
||||
|
||||
//提交数据
|
||||
admin.req({
|
||||
method: "post",
|
||||
contentType: "application/json;charset=UTF-8",
|
||||
dataType: "json",
|
||||
headers: {
|
||||
'token': layui.data(setter.tableName)[setter.request.tokenName]
|
||||
},
|
||||
url: setter.baseurl + "adminapi/mall/AddProduct",//实际使用请改成服务端真实接口
|
||||
async: false,
|
||||
data: JSON.stringify(param),
|
||||
done: function (res) {
|
||||
|
||||
//$.ajax({});
|
||||
layui.table.reload('LAY-user-back-manage'); //重载表格
|
||||
layer.close(index); //执行关闭
|
||||
}
|
||||
})
|
||||
//提交 Ajax 成功后,关闭当前弹层并重载表格
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$('.layui-btn.layuiadmin-btn-admin').on('click', function () {
|
||||
|
||||
var type = $(this).data('type');
|
||||
active[type] ? active[type].call(this) : '';
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
该目录存放【设置】的视图文件
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
该目录存放【用户】的视图文件
|
||||
Reference in New Issue
Block a user