出鱼协议改为 OnFishResync 全量替换(废弃 OnFishSpawn(type=sync));socket.html 同步更新;后台 user/list.html 更新;version 2.11.11

This commit is contained in:
2026-10-20 17:11:47 +08:00
parent 2babb26fca
commit 69c264468f
6 changed files with 112 additions and 118 deletions
@@ -88,9 +88,9 @@
<button type="button" class="layui-btn layui-btn-sm layui-btn-primary" id="btn-reset">
<i class="layui-icon layui-icon-refresh"></i> 重置
</button>
<button type="button" class="layui-btn layui-btn-sm layui-btn-warm" id="btn-sync-level">
<!--<button type="button" class="layui-btn layui-btn-sm layui-btn-warm" id="btn-sync-level">
<i class="layui-icon layui-icon-refresh-3"></i> 同步等级
</button>
</button>-->
</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
+1 -1
View File
@@ -20,7 +20,7 @@
<script>
layui.config({
base: './dist/' //指定 layuiAdmin 项目路径
, version: '2.11.10'
, version: '2.11.11'
}).use('index', function () {
var layer = layui.layer, admin = layui.admin;
+21 -5
View File
@@ -212,7 +212,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
expireAt: number, // 毫秒
remainingSeconds: int
}]
}</pre></td><td>调用者</td><td>加入房间/断线重连/取消挂机返回房间信息。先用 activeFishes 初始化鱼群;服务端约 300ms 后还会向当前玩家补发同一批 OnFishSpawn(init),前端须按 fishId 去重。</td></tr>
}</pre></td><td>调用者</td><td>加入房间/断线重连/取消挂机返回房间信息,活跃鱼随本事件在 <code>activeFishes</code> 中返回。<b>注意</b>:重连 / 取消挂机 / 挂起恢复场景会另行下发独立事件 <code>OnFishResync</code>(全量替换语义),前端按该事件重建鱼群。</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>
@@ -288,7 +288,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
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=主动同步
type: "init" | "spawn", // init=进房补发,spawn=定时出鱼(sync 已废弃:重连/恢复改走 OnFishResync)
timestamp: number, // 服务器当前时间戳(毫秒)
fish: [{
fishId: number, // 普通鱼ID;鱼阵时为首条普通鱼ID(兼容旧客户端)
@@ -300,7 +300,23 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
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>
}</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>
<tr><td><span class="tag tag-sc">S→C</span> KillNotice</td><td><pre>{
FishId: string, // 鱼编号
userName: string, // 玩家名
@@ -562,7 +578,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
<li>无 -> 无操作,等待后续指令</li>
</ul>
</li>
<li>返回结果:RoomInfo(首个真人注册后的首次鱼群放入 activeFishes,约 300ms 后向当前玩家补发 OnFishSpawn(type=init))</li>
<li>返回结果:RoomInfo(首个真人注册后的首次鱼群放入 activeFishes;重连/恢复场景另行下发 OnFishResync 全量替换)</li>
</ol>
<h4>5. CancelPlayerAfk 流程</h4>
@@ -602,7 +618,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
<ul>
<li>首个真人进房 -> RegisterRoom -> 启动定时器持续出鱼</li>
<li>定时出鱼 -> OnFishSpawn(type=spawn) 广播房间组</li>
<li>后续真人进房/重连/取消挂机 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回,并约 300ms 后补发 OnFishSpawn(type=init)</li>
<li>后续真人进房 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回;重连 / 取消挂机 / 挂起恢复 -> 额外下发独立事件 OnFishResync(全量替换)</li>
<li>最后一个真人退出/断线 -> UnregisterRoom -> 停止定时器 + 清除活跃鱼</li>
</ul>
</div>
@@ -63,65 +63,13 @@ namespace Service.Fishing
_subscriber.Publish("fishgame:global", msg);
}
public async Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null)
public async Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null, bool sendInitialFish = true)
{
object payload;
List<object> compatibilityFishPayload = null;
List<object> initialFishPayload = null;
if (activeFishes != null && activeFishes.Count > 0)
{
var now = DateTimeOffset.UtcNow;
var activeFishPayload = activeFishes
.Where(f => string.IsNullOrWhiteSpace(f.FishArrayNumber))
.Select(f => (object)new
{
fishId = f.FishId,
fishIds = new[] { f.FishId },
fishNumber = f.FishNumber,
routeId = f.ClientRouteId,
fishArrayNumber = string.Empty,
createdAt = new DateTimeOffset(f.CreatedAt).ToUnixTimeMilliseconds(),
createdTime = f.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
expireAt = new DateTimeOffset(f.ExpireAt).ToUnixTimeMilliseconds(),
expireTime = f.ExpireAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(f.ExpireAt) - now).TotalSeconds)
})
.Concat(
activeFishes
.Where(f => !string.IsNullOrWhiteSpace(f.FishArrayNumber))
.GroupBy(f => f.FishArrayNumber)
.Select(group =>
{
var first = group.First();
return (object)new
{
fishId = first.FishId,
fishIds = group.Select(f => f.FishId).ToList(),
fishNumber = string.Empty,
routeId = first.ClientRouteId,
fishArrayNumber = first.FishArrayNumber,
createdAt = new DateTimeOffset(first.CreatedAt).ToUnixTimeMilliseconds(),
createdTime = first.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
expireAt = new DateTimeOffset(group.Min(f => f.ExpireAt)).ToUnixTimeMilliseconds(),
expireTime = group.Min(f => f.ExpireAt).ToString("yyyy-MM-dd HH:mm:ss.fff"),
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(group.Min(f => f.ExpireAt)) - now).TotalSeconds)
};
}))
.ToList();
compatibilityFishPayload = activeFishes
.Where(f => string.IsNullOrWhiteSpace(f.FishArrayNumber) && f.FishType == 1)
.Select(f => (object)new
{
fishId = f.FishId,
fishIds = new[] { f.FishId },
fishNumber = f.FishNumber,
routeId = f.ClientRouteId,
fishArrayNumber = string.Empty,
createdAt = new DateTimeOffset(f.CreatedAt).ToUnixTimeMilliseconds(),
createdTime = f.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
expireAt = new DateTimeOffset(f.ExpireAt).ToUnixTimeMilliseconds(),
expireTime = f.ExpireAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(f.ExpireAt) - now).TotalSeconds)
}).ToList();
initialFishPayload = BuildActiveFishPayload(activeFishes);
payload = new
{
@@ -137,7 +85,7 @@ namespace Service.Fishing
roomInfo.Players,
roomInfo.CreateUserId,
roomInfo.SystemFactor,
activeFishes = activeFishPayload
activeFishes = initialFishPayload
};
@@ -147,12 +95,80 @@ namespace Service.Fishing
payload = roomInfo;
}
await _hubSend.SendUserCIdAsync(cid, "RoomInfo", payload);
if (compatibilityFishPayload != null && compatibilityFishPayload.Count > 0)
if (sendInitialFish && initialFishPayload != null && initialFishPayload.Count > 0)
{
_ = SendInitialFishAfterSceneReady(cid, roomInfo.RoomId, compatibilityFishPayload);
_ = SendInitialFishAfterSceneReady(cid, roomInfo.RoomId, initialFishPayload);
}
}
/// <summary>
/// 补发场上活鱼(挂起恢复 / 重连专用独立协议):
/// 发到独立事件 OnFishResync,客户端按"全量替换"处理(先清本地旧鱼,再按本列表重建)。
/// fish 列表口径与 RoomInfo.activeFishes / 首屏 init 完全一致,都带服务端原始的 createdAt。
/// </summary>
public async Task FishResyncNoticeAsync(string cid, string roomId, List<ActiveFish> activeFishes)
{
var fish = activeFishes == null || activeFishes.Count == 0
? new List<object>()
: BuildActiveFishPayload(activeFishes);
var now = DateTimeOffset.UtcNow;
await _hubSend.SendUserCIdAsync(cid, "OnFishResync", new
{
roomId,
timestamp = now.ToUnixTimeMilliseconds(),
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
count = fish.Count,
fish
});
}
/// <summary>
/// 把场上活鱼转成下发 payload:单体鱼逐条、鱼阵按 FishArrayNumber 分组(鱼阵用首条 fishId 作主键)。
/// createdAt / expireAt 一律取服务端原始值,客户端据此按 (当前时间 - createdAt) / 路径总时长 推算位置。
/// </summary>
private static List<object> BuildActiveFishPayload(List<ActiveFish> activeFishes)
{
var now = DateTimeOffset.UtcNow;
return activeFishes
.Where(f => f != null && string.IsNullOrWhiteSpace(f.FishArrayNumber))
.Select(f => (object)new
{
fishId = f.FishId,
fishIds = new[] { f.FishId },
fishNumber = f.FishNumber,
routeId = f.ClientRouteId,
fishArrayNumber = string.Empty,
createdAt = new DateTimeOffset(f.CreatedAt).ToUnixTimeMilliseconds(),
createdTime = f.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
expireAt = new DateTimeOffset(f.ExpireAt).ToUnixTimeMilliseconds(),
expireTime = f.ExpireAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(f.ExpireAt) - now).TotalSeconds)
})
.Concat(
activeFishes
.Where(f => f != null && !string.IsNullOrWhiteSpace(f.FishArrayNumber))
.GroupBy(f => f.FishArrayNumber)
.Select(group =>
{
var first = group.First();
return (object)new
{
fishId = first.FishId,
fishIds = group.Select(f => f.FishId).ToList(),
fishNumber = string.Empty,
routeId = first.ClientRouteId,
fishArrayNumber = first.FishArrayNumber,
createdAt = new DateTimeOffset(first.CreatedAt).ToUnixTimeMilliseconds(),
createdTime = first.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
expireAt = new DateTimeOffset(group.Min(f => f.ExpireAt)).ToUnixTimeMilliseconds(),
expireTime = group.Min(f => f.ExpireAt).ToString("yyyy-MM-dd HH:mm:ss.fff"),
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(group.Min(f => f.ExpireAt)) - now).TotalSeconds)
};
}))
.ToList();
}
private async Task SendInitialFishAfterSceneReady(string cid, string roomId, List<object> initialFishPayload)
{
try
@@ -22,7 +22,12 @@ namespace FishGameService.GameSend
void RoomJoinNotice(string roomId, RedisGamePlayer player);
Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null);
Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null, bool sendInitialFish = true);
/// <summary>
/// 补发场上活鱼(挂起恢复 / 重连专用独立协议,事件名 OnFishResync,全量替换语义)。
/// </summary>
Task FishResyncNoticeAsync(string cid, string roomId, List<ActiveFish> activeFishes);
void RoomLeaveNotice(string roomId, string userId);
@@ -155,7 +155,7 @@ namespace FishGameService.Hubs
await Groups.AddToGroupAsync(cid, room.RoomId);
var activeFishes = _fishingService.GetActiveFishes(room.RoomId);
await _send.SendRoomInfo(cid, room, activeFishes);
await _send.SendRoomInfo(cid, room, activeFishes, sendInitialFish: false);
_send.RoomJoinNotice(room.RoomId, player);
await Clients.Caller.SendAsync("OnPlayerInfo", new
{
@@ -191,9 +191,8 @@ namespace FishGameService.Hubs
await _fishingService.SendRoomChange(room);
await SendActiveFishSync(room.RoomId);
// 重连补发:走独立协议 OnFishResync(全量替换),不再走 OnFishSpawn(type=sync)
await _send.FishResyncNoticeAsync(cid, room.RoomId, activeFishes);
try
{
@@ -210,16 +209,6 @@ namespace FishGameService.Hubs
_logger.LogWarning(ex, "FishHub重连后更新用户状态失败 userId={UserId} roomId={RoomId}", userId, room.RoomId);
}
try
{
await SendActiveFishSync(room.RoomId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "FishHub重连同服务端步活跃鱼失败 userId={UserId} roomId={RoomId}", userId, room.RoomId);
}
return;
}
}
@@ -378,12 +367,14 @@ namespace FishGameService.Hubs
try
{
var room = _redisService.GetGameRoom(roomId);
var activeFishes = _fishingService.GetActiveFishes(roomId);
if (room != null)
{
var activeFishes = _fishingService.GetActiveFishes(roomId);
await _send.SendRoomInfo(cid, room, activeFishes);
// 关掉 RoomInfo 搭车的延迟 init,补发统一走独立协议
await _send.SendRoomInfo(cid, room, activeFishes, sendInitialFish: false);
}
// 挂起恢复补发:独立协议 OnFishResync(全量替换)
await _send.FishResyncNoticeAsync(cid, roomId, activeFishes);
}
catch (Exception ex)
{
@@ -391,40 +382,6 @@ namespace FishGameService.Hubs
}
}
private async Task SendActiveFishSync(string roomId)
{
var activeFishes = _fishingService.GetActiveFishes(roomId);
if (activeFishes == null || activeFishes.Count == 0)
return;
var now = DateTimeOffset.UtcNow;
var syncFish = activeFishes.Select(f => new
{
fishId = f.FishId,
fishIds = new[] { f.FishId },
fishNumber = string.IsNullOrWhiteSpace(f.FishArrayNumber) ? f.FishNumber : string.Empty,
routeId = f.ClientRouteId,
fishArrayNumber = f.FishArrayNumber ?? string.Empty,
createdAt = new DateTimeOffset(f.CreatedAt).ToUnixTimeMilliseconds(),
createdTime = f.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
expireAt = new DateTimeOffset(f.ExpireAt).ToUnixTimeMilliseconds(),
expireTime = f.ExpireAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(f.ExpireAt) - now).TotalSeconds)
}).ToList();
var payload = new
{
type = "sync",
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
fish = syncFish
};
await Clients.Caller.SendAsync("OnFishSpawn", payload);
}
public async Task<List<string>> BotFireCannon(string roomId, string botUserId, long gold, int count)
{
string cid = Context.ConnectionId;