补回 swagger(Swashbuckle 6.6.2 + XML 注释)、socket.html 入库、固化本机构建与服务器替换脚本
- Directory.Build.props: 生成 XML 注释供 Swagger 展示中文说明 - AdminHost/FishGameHost: 接入 Swashbuckle,路由 adminswagger / roomswagger - 后台前端 config.js 改为同源相对地址,适配反向代理 - socket.html(SignalR 协议文档)纳入仓库 - deploy/: 本机构建(容器内编译,规避杀软)、服务器替换脚本、Nginx 配置
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# Docker 构建上下文忽略项(上下文为仓库根目录,用于 deploy/Dockerfile.build 的容器内编译)
|
||||
.git
|
||||
.gitignore
|
||||
**/bin
|
||||
**/obj
|
||||
deploy/publish
|
||||
deploy/dist
|
||||
deploy/logs
|
||||
deploy/uploads
|
||||
*.tar
|
||||
+2
-1
@@ -365,4 +365,5 @@ FodyWeavers.xsd
|
||||
# ===== 部署产物(deploy/deploy.sh 运行时生成,不入库)=====
|
||||
deploy/publish/
|
||||
deploy/logs/
|
||||
deploy/uploads/
|
||||
deploy/uploads/
|
||||
deploy/dist/
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -18,6 +18,7 @@
|
||||
<PackageReference Include="DotNetCore.CAP.Dashboard" Version="8.3.4" />
|
||||
<PackageReference Include="DotNetCore.CAP.PostgreSql" Version="8.3.4" />
|
||||
<PackageReference Include="DotNetCore.CAP.RabbitMQ" Version="8.3.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -98,6 +98,24 @@ builder.Services.AddCronJob<DailyTaskJob>(c =>
|
||||
c.CronExpression = @"0 0 0 * * *";
|
||||
});
|
||||
|
||||
#region Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
|
||||
{
|
||||
Title = "AdminApi",
|
||||
Description = "游戏后台管理系统",
|
||||
Version = "v1"
|
||||
});
|
||||
// 加载各项目生成的 XML 注释,用于展示接口的中文说明
|
||||
foreach (var xml in System.IO.Directory.GetFiles(AppContext.BaseDirectory, "*.xml"))
|
||||
{
|
||||
try { c.IncludeXmlComments(xml, true); } catch { }
|
||||
}
|
||||
});
|
||||
#endregion
|
||||
|
||||
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
|
||||
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
|
||||
{
|
||||
@@ -113,6 +131,15 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
#region Swagger
|
||||
app.UseSwagger(c => c.RouteTemplate = "adminswagger/{documentName}/swagger.json");
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.RoutePrefix = "adminswagger";
|
||||
c.SwaggerEndpoint("v1/swagger.json", "AdminApi v1");
|
||||
});
|
||||
#endregion
|
||||
|
||||
app.UseCors("Cors");
|
||||
app.UseRouting();
|
||||
app.UseStaticFiles();
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
<!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</td><td><pre>{
|
||||
code: int, // 错误码(0=未知错误)
|
||||
message: string // 错误描述
|
||||
}</pre><br><b>错误码说明:</b><br>1001=房间已满<br>1002=金币不足<br>1003=房间不存在<br>1004=玩家不在房间<br>1005=签名错误<br>1006=请求已过期<br>1007=鱼配置不存在<br>1008=子弹参数无效<br>1009=金币参数超出范围<br>1011=用户不存在<br>1012=道具配置不存在<br>1013=道具数量不足<br>1014=钻石不足<br>1015=炮倍参数无效</td><td>调用者</td><td>错误通知。前端根据 code 处理业务逻辑</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> RoomInfo</td><td><pre>RedisGameRoom 扩展字段,包含首次及当前活跃鱼:
|
||||
{
|
||||
RoomId, RoomName, GameType, RoomLevel, RoomPartition,
|
||||
RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
|
||||
RobotCount, Players[], CreateUserId, SystemFactor,
|
||||
activeFishes: [{ // 首次进房/后续进房/重连/取消挂机时包含
|
||||
fishId: number,
|
||||
fishIds: number[],
|
||||
fishNumber: string,
|
||||
routeId: number,
|
||||
fishArrayNumber: string,
|
||||
createdAt: number, // 毫秒
|
||||
expireAt: number, // 毫秒
|
||||
remainingSeconds: int
|
||||
}]
|
||||
}</pre></td><td>调用者</td><td>加入房间/断线重连/取消挂机返回房间信息。先用 activeFishes 初始化鱼群;服务端约 300ms 后还会向当前玩家补发同一批 OnFishSpawn(init),前端须按 fishId 去重。</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>
|
||||
|
||||
<p style="padding:12px 20px;margin:0;background:#fff8e6;border-bottom:1px solid #f0d58a;"><strong>🏠 房间列表说明:</strong>大厅只返回后台预配置的固定数字房间号。服务端不会再创建随机 GUID 展示房间,并会过滤、清理“无房主、无玩家”的旧异常房间。前端收到 <code>GetRoomList</code>、<code>CreateRoomNotice</code>、<code>JoinRoomNotice</code> 或 <code>ExitRoomNotice</code> 后,应按 <code>roomId</code> 更新同一条房间数据,不要追加重复房间。</p>
|
||||
|
||||
<h3>📤 客户端 → 服务器</h3>
|
||||
<table>
|
||||
<tr><th style="width:120px">方法</th><th style="width:380px">参数</th><th>说明</th></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> CreateRoom</td><td><code>CreateRoomParam { roomId, userId, gameType(1=我方黑 2=我方红), password, gold }</code></td><td>创建房间</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> JoinGame</td><td><code>string gameId, string pwd, string userId</code></td><td>加入游戏</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> GameReady</td><td><code>string gameId, string userId</code></td><td>准备</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> EndGame</td><td><code>string gameId, string winUserId</code></td><td>直接结束(对方获胜)</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> RequestEndGame</td><td><code>string gameId, string userId, string winUserId</code></td><td>请求结束(需双方确认)</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> RestartGame</td><td><code>string gameId, string userId</code></td><td>对局结束后重新开始</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> GoldConfig</td><td><code>string gameId, long gold, string userId</code></td><td>房主设置金币</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> ExitRoom</td><td><code>string gameId, string userId</code></td><td>退出(房主退出清空房间,非房主保留房间)</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> Heartbeat</td><td><code>string gameId</code></td><td>心跳,更新活跃时间</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> CheckTimeout</td><td><code>string gameId</code></td><td>检查超时(60秒断线判负)</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> GetRoomList</td><td><code>int partition=0</code></td><td>房间列表;0=全部分区,正数=指定分区</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> GetRoomState</td><td><code>string gameId</code></td><td>获取当前房间完整状态,用于返回场景或重连后主动校准</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> KickPlayer</td><td><code>string gameId, string targetUserId</code></td><td>房主在开局前踢出指定玩家</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> SyncGameData</td><td><code>string gameId, string data</code></td><td>同步棋子移动</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> GameOperation</td><td><code>string gameId, int type, string data</code><br>type: 1=求和 2=认输(自动结束) 3=悔棋</td><td>游戏操作</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>📥 服务器 → 客户端</h3>
|
||||
<table>
|
||||
<tr><th style="width:170px">事件</th><th>数据</th><th>目标</th></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GameError</td><td><code>string</code></td><td>调用者</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> BeginGame</td><td><code>ChessGameRoom</code></td><td>调用者/房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> JoinRoom</td><td><code>ChessGameRoom</code></td><td>调用者</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> JoinRoomNotice</td><td><code>ChessGameRoom</code></td><td>象棋大厅组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> RoomStateNotice</td><td><code>ChessGameRoom</code></td><td>房间组/调用者,房间完整状态</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GoldConfigRequired</td><td><code>{ targetUserId, room }</code></td><td>房间组,两名真人到齐后提示指定红方设置金币</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> Reconnect</td><td><code>{ userId, userName }</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> Disconnect</td><td><code>{ userId, disconnectTime, timeoutSeconds }</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> ReadyNotice</td><td><code>string userId</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> AllBeginTime</td><td><code>ChessGameRoom</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GameEnd</td><td><code>{ winUserId, room }</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> EndGameRequestSent</td><td><code>string</code></td><td>调用者</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> OpponentRequestEnd</td><td><code>string userId</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> RestartGameSuccess</td><td><code>ChessGameRoom</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> RestartRoomNotice</td><td><code>ChessGameRoom</code></td><td>全部</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> ExitRoomNotice</td><td><code>ChessGameRoom</code></td><td>象棋大厅组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> ExitRoomSuccess</td><td><code>string userId</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> KickedFromRoom</td><td><code>{ gameId, userId }</code></td><td>被踢玩家</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> CreateRoomNotice</td><td><code>ChessGameRoom</code></td><td>象棋大厅组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GoldConfigSuccess</td><td><code>ChessGameRoom</code></td><td>房间组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GoldConfigRoomNotice</td><td><code>ChessGameRoom</code></td><td>全部</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GetRoomList</td><td><code>List<ChessGameRoom></code></td><td>调用者;仅固定房间,按 roomId 覆盖本地列表</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> SyncGameData</td><td><code>string</code></td><td>全部</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GameOperation</td><td><code>{ type, data }</code></td><td>全部</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== CustomerHub ==================== -->
|
||||
<div id="hub-customer" class="hub-section">
|
||||
<div class="hub-card">
|
||||
<div class="hub-header" style="background:#16a085;">
|
||||
<div class="path">CustomerHub — <code>/customerHub</code></div>
|
||||
<div class="detail">WS,客服即时通讯。客服端需 JWT,玩家端无需 Token。</div>
|
||||
</div>
|
||||
|
||||
<h3>📤 客户端 → 服务器</h3>
|
||||
<table>
|
||||
<tr><th style="width:140px">方法</th><th style="width:380px">参数</th><th>说明</th></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> JoinConversation</td><td><code>string conversationId</code></td><td>加入会话房间组</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> LeaveConversation</td><td><code>string conversationId</code></td><td>离开会话房间组</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> SendMessage</td><td><code>string conversationId, int senderType, string senderId, string senderName, string? senderAvatar, int contentType, string content, string guId=""</code><br>senderType: 1=用户 2=客服<br>contentType: 1=文字 2=图片<br>conversationId 为空时自动创建新会话</td><td>发送消息</td></tr>
|
||||
<tr><td><span class="tag tag-cs">C→S</span> StartConversation</td><td><code>string userId, string userName</code></td><td>主动发起新会话</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>📥 服务器 → 客户端</h3>
|
||||
<table>
|
||||
<tr><th style="width:170px">事件</th><th>数据</th><th>目标</th></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> OnMessageReceived</td>
|
||||
<td><pre>{
|
||||
GuId: string,
|
||||
ConversationId: string,
|
||||
SenderType: int,
|
||||
SenderId: string,
|
||||
SenderName: string,
|
||||
SenderAvatar: string?,
|
||||
ContentType: int,
|
||||
Content: string,
|
||||
CreateTime: DateTime
|
||||
}</pre></td><td>会话组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> OnConversationStarted</td><td><code>ConversationEntity</code></td><td>调用者</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> OnNewConversation</td><td><code>{ conversationId, customerId?, customerName?, userId?, userName?, status, createTime }</code></td><td>客服组</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> Exception</td><td><pre>{
|
||||
code: int,
|
||||
message: string
|
||||
}</pre></td><td>调用者</td><td>错误通知,code 同上表</td></tr>
|
||||
</table>
|
||||
<p style="padding:8px 12px;margin:10px 0 0;background:#f0faf0;border-radius:4px;"><strong>注意:</strong>客服连接时携带 JWT,服务端自动从 <code>uid</code> Claim 加入通知组,无需手动 JoinCustomerQueue。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 公共说明 ==================== -->
|
||||
<div id="hub-common" class="hub-section">
|
||||
<div class="info-box">
|
||||
<h3>🔗 Hub 路由总览</h3>
|
||||
<table>
|
||||
<tr><th>Hub</th><th>路由</th><th>认证</th></tr>
|
||||
<tr><td>LoginHub</td><td><code>/loginhub</code></td><td>JWT</td></tr>
|
||||
<tr><td>FishGameHub</td><td><code>/fishinghub</code></td><td>方法传参 userId</td></tr>
|
||||
<tr><td>FishRoomHub</td><td><code>/fishingroomhub</code></td><td>方法传参 userId</td></tr>
|
||||
<tr><td>ChessGameHub</td><td><code>/chessgamehub</code></td><td>方法传参 userId</td></tr>
|
||||
<tr><td>CustomerHub</td><td><code>/customerHub</code></td><td>客服需JWT,玩家无需</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>📦 通用数据结构</h3>
|
||||
<table>
|
||||
<tr><th style="width:180px">名称</th><th>字段</th></tr>
|
||||
<tr><td><code>RedisGameRoom</code></td>
|
||||
<td><pre>{
|
||||
RoomId, RoomName, GameType, RoomLevel, RoomPartition,
|
||||
RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
|
||||
RobotCount, Players[], CreateUserId,
|
||||
SystemFactor: double // 当前系统因子(根据当日盈利动态调整)
|
||||
}</pre></td></tr>
|
||||
<tr><td><code>RedisGamePlayer</code></td>
|
||||
<td><pre>{
|
||||
UserId, UserName, UserNumber, Avatar, IsRedName,
|
||||
Level, VipLevel, Gold, Diamond, Position,
|
||||
CannonId, WingsId, WarGodId, CannonMultiple,
|
||||
BeginCoin, MaxGold, BeginTime, State(0=正常 1=断线),
|
||||
CId, PlayerFactor, TotalRechargeGold, SafeBoxGold,
|
||||
IsBot, IsAfk
|
||||
}</pre></td></tr>
|
||||
<tr><td><code>ChessGameRoom</code></td>
|
||||
<td><pre>{
|
||||
RoomId, state(1=等待 2=游戏中 3=已结束), users[],
|
||||
gameType(1=我方黑 2=我方红), gold, password,
|
||||
createUserId, robotCount, isGameEnded,
|
||||
timeoutSeconds(默认60), endGameRequests, pendingWinUserId
|
||||
}</pre></td></tr>
|
||||
<tr><td><code>ConversationEntity</code></td>
|
||||
<td><pre>{
|
||||
UserId?, UserName?, CustomerId?, CustomerName?,
|
||||
Status(0=等待 1=进行中 2=已结束), CreateTime,
|
||||
EndTime?, LastReadTime?
|
||||
}</pre></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>📤 发送基础设施(IHubSend<T>)</h3>
|
||||
<table>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td><code>SendUserCId(cid, method, obj)</code></td><td>发送给指定连接</td></tr>
|
||||
<tr><td><code>SendUserUserId(userId, method, obj)</code></td><td>通过 userId 查找 CId 后发送</td></tr>
|
||||
<tr><td><code>SendGroup(groupId, method, obj)</code></td><td>发送给 SignalR 群组</td></tr>
|
||||
<tr><td><code>SendAll(method, obj)</code></td><td>广播给所有客户端</td></tr>
|
||||
<tr><td><code>GroupExcept(cid, groupId, method, obj)</code></td><td>发送给群组排除指定连接</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>流程说明</h3>
|
||||
|
||||
<h4>1. 登录 / Bind 流程 (LoginHub)</h4>
|
||||
<ol>
|
||||
<li>客户端连接 /loginhub (JWT)</li>
|
||||
<li>调用 Bind(userId),加入 userId 组</li>
|
||||
<li>Bind 读取 Redis LoginCId:
|
||||
<ul>
|
||||
<li>若存在旧 LoginCId 且不等于当前连接 -> 顶号:
|
||||
<ul>
|
||||
<li>发 AccountKicked 给旧 LoginHub 连接</li>
|
||||
<li>查旧 LoginHub 关联的游戏 Hub CId,发 KickToMainPage</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>更新 Redis LoginCId = 当前连接 CId</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>检测断线房间 (PendingRoomId):
|
||||
<ul>
|
||||
<li>有 -> 发 PendingRoomNotice(弹窗:返回房间 / 离开)</li>
|
||||
<li>无 -> 进入大厅</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h4>2. 顶号流程</h4>
|
||||
<ul>
|
||||
<li>设备 A 登录:Bind -> LoginCId = cid_A</li>
|
||||
<li>设备 B 登录:Bind -> LoginCId = cid_A != cid_B -> 发 AccountKicked 到 cid_A -> 更新 LoginCId = cid_B -> A 被顶</li>
|
||||
<li>设备 C 登录:Bind -> LoginCId = cid_B != cid_C -> 发 AccountKicked 到 cid_B -> 更新 LoginCId = cid_C -> B 被顶</li>
|
||||
<li>关键:仅 LoginHub.Bind 负责发 AccountKicked,FishGameHub 不再发送</li>
|
||||
</ul>
|
||||
|
||||
<h4>3. JoinRoom 流程 (FishGameHub)</h4>
|
||||
<ol>
|
||||
<li>客户端调 JoinRoom(roomId, userId, posituin)</li>
|
||||
<li>检查玩家是否已在房间 -> 是则返回</li>
|
||||
<li>获取/创建房间 roomInfo</li>
|
||||
<li>判断是否首个真人进房:
|
||||
<ul>
|
||||
<li>是 -> RegisterRoom(roomId, roomLevel) 注册出鱼服务</li>
|
||||
<li>否 -> GetActiveFishes(roomId) 获取当前活跃鱼列表</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>发 RoomInfo(cid, room, activeFishes) 给调用者</li>
|
||||
<li>发 OnSeatChange(join) 通知房间组</li>
|
||||
</ol>
|
||||
|
||||
<h4>4. Reconnect 流程 (FishGameHub)</h4>
|
||||
<ol>
|
||||
<li>客户端连接 FishGameHub 后立即调 Reconnect(userId)</li>
|
||||
<li>查 Redis 有无断线房间 (PendingRoomId):
|
||||
<ul>
|
||||
<li>有 -> 恢复玩家状态 State=0,走 JoinRoom 逻辑(含鱼同步)</li>
|
||||
<li>无 -> 无操作,等待后续指令</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>返回结果:RoomInfo(首个真人注册后的首次鱼群放入 activeFishes,约 300ms 后向当前玩家补发 OnFishSpawn(type=init))</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,14 @@
|
||||
<Project>
|
||||
|
||||
<!--
|
||||
全局构建属性
|
||||
- GenerateDocumentationFile:生成 XML 注释文件,供 Swagger 展示接口的中文说明
|
||||
- NoWarn 1591:抑制「公开成员缺少注释」的警告
|
||||
旧线上版本(dual-20261008)即按此方式产出各项目的 .xml,此处保持一致。
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -17,6 +17,7 @@
|
||||
<PackageReference Include="DotNetCore.CAP.Dashboard" Version="8.3.4" />
|
||||
<PackageReference Include="DotNetCore.CAP.PostgreSql" Version="8.3.4" />
|
||||
<PackageReference Include="DotNetCore.CAP.RabbitMQ" Version="8.3.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -138,6 +138,24 @@ builder.Services.AddRedisCacheSingleton(builder.Configuration);
|
||||
builder.Services.Configure<BulletConfig>(builder.Configuration.GetSection("BulletConfig"));
|
||||
builder.Services.AddSingleton<BulletConfig>(sp => sp.GetRequiredService<IOptions<BulletConfig>>().Value);
|
||||
|
||||
#region Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
|
||||
{
|
||||
Title = "FishGameHost",
|
||||
Description = "捕鱼游戏接口",
|
||||
Version = "v1"
|
||||
});
|
||||
// 加载各项目生成的 XML 注释,用于展示接口的中文说明
|
||||
foreach (var xml in System.IO.Directory.GetFiles(AppContext.BaseDirectory, "*.xml"))
|
||||
{
|
||||
try { c.IncludeXmlComments(xml, true); } catch { }
|
||||
}
|
||||
});
|
||||
#endregion
|
||||
|
||||
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
|
||||
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
|
||||
{
|
||||
@@ -165,6 +183,15 @@ if (app.Environment.IsDevelopment())
|
||||
{
|
||||
}
|
||||
|
||||
#region Swagger
|
||||
app.UseSwagger(c => c.RouteTemplate = "roomswagger/{documentName}/swagger.json");
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.RoutePrefix = "roomswagger";
|
||||
c.SwaggerEndpoint("v1/swagger.json", "FishGameHost v1");
|
||||
});
|
||||
#endregion
|
||||
|
||||
app.UseApiLogging();
|
||||
app.UseRouting();
|
||||
app.UseGlobalExceptionHandler();
|
||||
|
||||
@@ -175,12 +175,12 @@ namespace Service.Fishing
|
||||
roomId, cid, initialFishPayload.Count);
|
||||
}
|
||||
}
|
||||
public void RoomKillNotice(string roomId, string fishNumber, string userId, long gold, string fishId, long timespan, double beishu = 1,long skillsGold=0,int fishScore=1, object param = null, object drop = null)
|
||||
public void RoomKillNotice(string roomId, string fishNumber, string userId, long gold, string fishId, long timespan, double beishu = 1,long skillsGold=0,int fishScore=1, object param = null, object drop = null, long gainExp = 0, long exp = 0, int level = 0, long expMax = 0)
|
||||
{
|
||||
|
||||
var killtime=DateTime.Now.ToTimeStamp();
|
||||
|
||||
_hubSend.SendGroup(roomId, "RoomKillNotice", new { fishNumber, userId, gold, fishId, timespan, killtime, msg = $"时间:{killtime - timespan}秒", beishu, skillsGold, fishScore, param, drop });
|
||||
_hubSend.SendGroup(roomId, "RoomKillNotice", new { fishNumber, userId, gold, fishId, timespan, killtime, msg = $"时间:{killtime - timespan}秒", beishu, skillsGold, fishScore, param, drop, gainExp, exp, level, expMax });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -18,7 +18,7 @@ namespace FishGameService.GameSend
|
||||
|
||||
void SyncGameData(string roomId, object data);
|
||||
|
||||
void RoomKillNotice(string roomId, string fishNumber, string userId, long gold, string fishId, long timespan, double beishu = 1,long skillsGold = 0,int fishScore = 1, object param = null, object drop = null);
|
||||
void RoomKillNotice(string roomId, string fishNumber, string userId, long gold, string fishId, long timespan, double beishu = 1,long skillsGold = 0,int fishScore = 1, object param = null, object drop = null, long gainExp = 0, long exp = 0, int level = 0, long expMax = 0);
|
||||
|
||||
void RoomJoinNotice(string roomId, RedisGamePlayer player);
|
||||
|
||||
|
||||
@@ -1058,9 +1058,6 @@ namespace FishGameService.GameService
|
||||
}
|
||||
}
|
||||
|
||||
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan,res.beishu, gold,fishConfig.FishScore, res.res);
|
||||
await FishKillControl(cid, gameRoom, player, fishNumber, skillGold, fihsId,gold,timespan, killConfig);
|
||||
|
||||
var expGain = fishConfig.FishType is FishTypeEnum.Boss鱼 or FishTypeEnum.特殊鱼 or FishTypeEnum.特殊Boss
|
||||
? 1000 : fishConfig.FishScore;
|
||||
if (expGain > 0)
|
||||
@@ -1076,7 +1073,8 @@ namespace FishGameService.GameService
|
||||
Source = RewardSource.击杀鱼,
|
||||
Remark = "捕鱼经验",
|
||||
RoomInfo = roomId,
|
||||
IsAddRecord = false
|
||||
IsAddRecord = false,
|
||||
SyncToFrontend = false
|
||||
});
|
||||
expSaved = expResult.isok;
|
||||
}
|
||||
@@ -1097,6 +1095,9 @@ namespace FishGameService.GameService
|
||||
}
|
||||
}
|
||||
|
||||
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan,res.beishu, gold,fishConfig.FishScore, res.res, gainExp: killExpGain, exp: player.Exp, level: player.Level, expMax: player.MaxExp);
|
||||
await FishKillControl(cid, gameRoom, player, fishNumber, skillGold, fihsId,gold,timespan, killConfig);
|
||||
|
||||
if ((fishConfig.FishType == FishTypeEnum.Boss鱼 || fishConfig.FishType == FishTypeEnum.特殊Boss || fishConfig.FishType == FishTypeEnum.特殊鱼)
|
||||
&& fishNumber != "12028"
|
||||
&& killConfig.BossDropConfig != null
|
||||
@@ -1131,7 +1132,7 @@ namespace FishGameService.GameService
|
||||
Source = RewardSource.击杀Boss,
|
||||
IsAddRecord = false
|
||||
});
|
||||
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan, res.beishu, gold, fishConfig.FishScore, drop: new { wonItem.PropNumber, wonItem.PropName, wonItem.Count });
|
||||
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan, res.beishu, gold, fishConfig.FishScore, drop: new { wonItem.PropNumber, wonItem.PropName, wonItem.Count }, exp: player.Exp, level: player.Level, expMax: player.MaxExp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1156,7 +1157,7 @@ namespace FishGameService.GameService
|
||||
Source = RewardSource.击杀Boss,
|
||||
IsAddRecord = false
|
||||
});
|
||||
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan, res.beishu, gold, fishConfig.FishScore, drop: new { item.PropNumber, propName, item.Count });
|
||||
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan, res.beishu, gold, fishConfig.FishScore, drop: new { item.PropNumber, propName, item.Count }, exp: player.Exp, level: player.Level, expMax: player.MaxExp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# ============================================================
|
||||
# 容器内编译 + 打包镜像(多阶段)
|
||||
#
|
||||
# 用途:本机构建时使用。编译发生在 Docker 的 Linux 虚拟机内,
|
||||
# Windows 上的杀软(如火绒)无法拦截中间产物 AdminApi.dll 等,
|
||||
# 可规避「dll 生成后被删除导致 MSB3030」的问题。
|
||||
#
|
||||
# 由 deploy/build-local.ps1 调用,构建上下文 = 仓库根目录:
|
||||
# docker build -f deploy/Dockerfile.build \
|
||||
# --build-arg PROJECT=<项目csproj相对路径,正斜杠> \
|
||||
# --build-arg SVC_NAME=<服务名,如 admin> \
|
||||
# --build-arg APP_DLL=<入口dll,如 AdminHost.dll> \
|
||||
# --build-arg PUBLIC_HOST=<捕鱼对外地址> \
|
||||
# -t 0524<服务名> .
|
||||
# ============================================================
|
||||
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:8.0
|
||||
ARG RUNTIME_IMAGE=mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
# ---------- 阶段一:编译发布 ----------
|
||||
FROM ${SDK_IMAGE} AS build
|
||||
ARG PROJECT
|
||||
ARG SVC_NAME
|
||||
ARG PUBLIC_HOST=localhost
|
||||
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
|
||||
DOTNET_NOLOGO=1
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
# 编译(不依赖 PUBLIC_HOST,可复用构建缓存)
|
||||
RUN dotnet publish "${PROJECT}" -c Release -o /out --nologo
|
||||
|
||||
# 部署配置覆盖 + 对外地址替换(单独一层:日后只改地址可秒级重建,无需重编译)
|
||||
RUN set -eux; \
|
||||
# 部署版配置覆盖(各 Host 的 Config/system.json 默认不随发布输出)
|
||||
mkdir -p /out/Config; \
|
||||
cp "/src/deploy/config/${SVC_NAME}/system.json" /out/Config/system.json; \
|
||||
cp /src/nlog.config /out/nlog.config; \
|
||||
# 捕鱼对局层:替换客户端要访问的对外地址
|
||||
sed -i "s|__PUBLIC_HOST__|${PUBLIC_HOST}|g" /out/Config/system.json; \
|
||||
# 网关:ocelot 下游地址由 localhost:524x 改写为「容器名:80」
|
||||
if [ -f /out/ocelot.json ]; then \
|
||||
sed -i \
|
||||
-e 's|{ "Host": "localhost", "Port": 5241 }|{ "Host": "0524admin", "Port": 80 }|' \
|
||||
-e 's|{ "Host": "localhost", "Port": 5245 }|{ "Host": "0524user", "Port": 80 }|' \
|
||||
-e 's|{ "Host": "localhost", "Port": 5247 }|{ "Host": "0524customer", "Port": 80 }|' \
|
||||
-e 's|{ "Host": "localhost", "Port": 5242 }|{ "Host": "0524chess", "Port": 80 }|' \
|
||||
-e 's|{ "Host": "localhost", "Port": 5244 }|{ "Host": "0524fishroom", "Port": 80 }|' \
|
||||
-e 's|{ "Host": "localhost", "Port": 5248 }|{ "Host": "0524fishgame", "Port": 80 }|' \
|
||||
/out/ocelot.json; \
|
||||
fi
|
||||
|
||||
# ---------- 阶段二:运行时镜像 ----------
|
||||
FROM ${RUNTIME_IMAGE}
|
||||
ARG APP_DLL
|
||||
|
||||
ENV APP_DLL=${APP_DLL} \
|
||||
ASPNETCORE_URLS=http://+:80 \
|
||||
ASPNETCORE_ENVIRONMENT=Production \
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /out .
|
||||
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["sh", "-c", "exec dotnet ${APP_DLL}"]
|
||||
@@ -0,0 +1,157 @@
|
||||
<#
|
||||
本机构建镜像并导出 tar(编译在 Docker 容器内进行)
|
||||
------------------------------------------------------------------
|
||||
适用场景:服务器无法访问 mcr.microsoft.com;且本机杀软(如火绒)
|
||||
会拦截/删除编译产物(AdminApi.dll 等)。
|
||||
编译与镜像构建全部在容器内完成,Windows 杀软无法拦截中间产物。
|
||||
|
||||
用法:
|
||||
# 1) 全量构建(7 个服务)
|
||||
powershell -ExecutionPolicy Bypass -File deploy\build-local.ps1 -PublicHost game.xxx.com
|
||||
|
||||
# 2) 只构建/打包指定服务(多个逗号分隔)
|
||||
powershell -ExecutionPolicy Bypass -File deploy\build-local.ps1 -PublicHost game.xxx.com -Services admin,fishgame
|
||||
|
||||
# 3) 按「改动的子项目」自动推导需要重建的服务(推荐,避免漏编)
|
||||
powershell -ExecutionPolicy Bypass -File deploy\build-local.ps1 -PublicHost game.xxx.com -ForProject FishGameService
|
||||
|
||||
产物:deploy\dist\ 下的 tar(只含本次构建的镜像)
|
||||
#>
|
||||
param(
|
||||
[string]$PublicHost = "CHANGE_ME",
|
||||
[string]$Tag = (Get-Date -Format "yyyyMMdd-HHmm"),
|
||||
[string[]]$Services = @(),
|
||||
[string]$ForProject = "",
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$Deploy = Join-Path $Root "deploy"
|
||||
$Dist = Join-Path $Deploy "dist"
|
||||
|
||||
function Info($m) { Write-Host "[本地构建] $m" -ForegroundColor Green }
|
||||
function Warn($m) { Write-Host "[警告] $m" -ForegroundColor Yellow }
|
||||
|
||||
# ---------- 服务定义:服务名 -> 项目文件 / 入口 dll / Host 项目名 ----------
|
||||
$serviceMap = [ordered]@{
|
||||
gateway = @{ proj = "Gateway/Gateway.csproj"; dll = "Gateway.dll"; host = "Gateway" }
|
||||
admin = @{ proj = "AdminSrc/AdminHost/AdminHost.csproj"; dll = "AdminHost.dll"; host = "AdminHost" }
|
||||
user = @{ proj = "UserSrc/UserHost/UserHost.csproj"; dll = "UserHost.dll"; host = "UserHost" }
|
||||
customer = @{ proj = "CustomerSrc/CustomerHost/CustomerHost.csproj"; dll = "CustomerHost.dll"; host = "CustomerHost" }
|
||||
chess = @{ proj = "ChessGameHost/ChessGameHost.csproj"; dll = "ChessGameHost.dll"; host = "ChessGameHost" }
|
||||
fishroom = @{ proj = "FishGameSrc/FishRoomHost/FishRoomHost.csproj"; dll = "FishRoomHost.dll"; host = "FishRoomHost" }
|
||||
fishgame = @{ proj = "FishGameSrc/FishGameHost/FishGameHost.csproj"; dll = "FishGameHost.dll"; host = "FishGameHost" }
|
||||
}
|
||||
|
||||
# ---------- 依赖图:改动某项目 -> 需要重建的服务 ----------
|
||||
function Get-AffectedServices([string]$changedProject) {
|
||||
$refs = @{}
|
||||
Get-ChildItem -Path $Root -Recurse -Filter *.csproj -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$txt = [IO.File]::ReadAllText($_.FullName)
|
||||
$deps = @()
|
||||
foreach ($m in [regex]::Matches($txt, 'ProjectReference\s+Include="([^"]+)"')) {
|
||||
$deps += [IO.Path]::GetFileNameWithoutExtension($m.Groups[1].Value)
|
||||
}
|
||||
$refs[$_.BaseName] = $deps
|
||||
}
|
||||
$result = @()
|
||||
foreach ($name in $serviceMap.Keys) {
|
||||
$hostName = $serviceMap[$name].host
|
||||
if ($hostName -eq $changedProject) { $result += $name; continue }
|
||||
$seen = New-Object System.Collections.Generic.HashSet[string]
|
||||
$stack = New-Object System.Collections.Stack
|
||||
foreach ($d in @($refs[$hostName])) { if ($d) { [void]$stack.Push($d) } }
|
||||
while ($stack.Count -gt 0) {
|
||||
$cur = $stack.Pop()
|
||||
if (-not $seen.Add($cur)) { continue }
|
||||
foreach ($d in @($refs[$cur])) { if ($d) { [void]$stack.Push($d) } }
|
||||
}
|
||||
if ($seen.Contains($changedProject)) { $result += $name }
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
# ---------- 1) 决定要构建哪些服务 ----------
|
||||
if ($ForProject) {
|
||||
$targets = @(Get-AffectedServices $ForProject)
|
||||
if ($targets.Count -eq 0) {
|
||||
throw "没有任何 Host 引用项目 '$ForProject'(项目名不带 .csproj)。请检查拼写。"
|
||||
}
|
||||
Info ("按项目 '$ForProject' 推导出需重建的服务: " + ($targets -join ', '))
|
||||
} elseif ($Services.Count -gt 0) {
|
||||
$svcList = @()
|
||||
foreach ($s in $Services) { $svcList += ($s -split ',') }
|
||||
$svcList = @($svcList | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
foreach ($s in $svcList) {
|
||||
if (-not $serviceMap.Contains($s)) { throw "未知服务名 '$s'。可选: $($serviceMap.Keys -join ', ')" }
|
||||
}
|
||||
$targets = @($svcList)
|
||||
Info ("指定构建服务: " + ($targets -join ', '))
|
||||
} else {
|
||||
$targets = @($serviceMap.Keys)
|
||||
Info "全量构建 7 个服务"
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Info ("[DryRun] 将构建的服务: " + ($targets -join ', '))
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------- 2) 前置校验 + 准备基础镜像(经镜像站拉取并打回官方 tag)----------
|
||||
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw "未找到 docker,请先安装并启动 Docker Desktop" }
|
||||
$prevEap = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
docker info 2>&1 | Out-Null
|
||||
$dockerOk = ($LASTEXITCODE -eq 0)
|
||||
$ErrorActionPreference = $prevEap
|
||||
if (-not $dockerOk) { throw "Docker 引擎未就绪,请先启动 Docker Desktop 并等托盘图标变绿" }
|
||||
if ($PublicHost -eq "CHANGE_ME") {
|
||||
Warn "未指定 -PublicHost,捕鱼对局层地址会被写成 CHANGE_ME,客户端将连不上"
|
||||
}
|
||||
$baseImages = [ordered]@{
|
||||
"mcr.microsoft.com/dotnet/sdk:8.0" = "mcr.m.daocloud.io/dotnet/sdk:8.0"
|
||||
"mcr.microsoft.com/dotnet/aspnet:8.0" = "mcr.m.daocloud.io/dotnet/aspnet:8.0"
|
||||
}
|
||||
foreach ($official in $baseImages.Keys) {
|
||||
docker image inspect $official *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Info "本地缺少 $official,经镜像站拉取 ..."
|
||||
docker pull $baseImages[$official]
|
||||
if ($LASTEXITCODE -ne 0) { throw "拉取 $official 失败" }
|
||||
docker tag $baseImages[$official] $official
|
||||
}
|
||||
}
|
||||
|
||||
# ---------- 3) 构建镜像(编译在容器内)----------
|
||||
foreach ($name in $targets) {
|
||||
$s = $serviceMap[$name]
|
||||
Info "构建镜像 0524$name ..."
|
||||
docker build -f (Join-Path $Deploy "Dockerfile.build") `
|
||||
--build-arg "PROJECT=$($s.proj)" `
|
||||
--build-arg "SVC_NAME=$name" `
|
||||
--build-arg "APP_DLL=$($s.dll)" `
|
||||
--build-arg "PUBLIC_HOST=$PublicHost" `
|
||||
-t "0524$name" $Root
|
||||
if ($LASTEXITCODE -ne 0) { throw "构建 0524$name 失败" }
|
||||
}
|
||||
|
||||
# ---------- 4) 导出 tar(只含本次构建的镜像)----------
|
||||
New-Item -ItemType Directory -Path $Dist -Force | Out-Null
|
||||
$isAll = ($targets.Count -eq $serviceMap.Count)
|
||||
$suffix = if ($isAll) { "" } else { "-" + ($targets -join "-") }
|
||||
$tar = Join-Path $Dist "buyu-images$suffix-$Tag.tar"
|
||||
$tags = @($targets | ForEach-Object { "0524$_" })
|
||||
Info ("导出镜像到 $tar ...(含: " + ($tags -join ', ') + ")")
|
||||
docker save -o $tar @tags
|
||||
if ($LASTEXITCODE -ne 0) { throw "导出镜像失败" }
|
||||
$sizeMB = [math]::Round((Get-Item $tar).Length / 1MB, 1)
|
||||
|
||||
Write-Host ""
|
||||
Info "完成!镜像包: $tar ($sizeMB MB)"
|
||||
Write-Host "下一步 —— 在本机上传到服务器:" -ForegroundColor Cyan
|
||||
Write-Host " ssh root@<服务器IP> `"mkdir -p /opt/buyu/deploy/dist`"" -ForegroundColor Cyan
|
||||
Write-Host " scp `"$tar`" root@<服务器IP>:/opt/buyu/deploy/dist/" -ForegroundColor Cyan
|
||||
Write-Host "再登录服务器执行(只重启本次涉及的服务):" -ForegroundColor Cyan
|
||||
Write-Host " cd /opt/buyu" -ForegroundColor Cyan
|
||||
Write-Host (" bash deploy/load-and-up.sh deploy/dist/" + (Split-Path $tar -Leaf) + " " + ($targets -join ' ')) -ForegroundColor Cyan
|
||||
@@ -0,0 +1,70 @@
|
||||
<#
|
||||
本机拉取中间件镜像并导出 tar
|
||||
------------------------------------------------------------------
|
||||
适用场景:服务器访问不了 docker.io(拉 mongo/redis/postgres/rabbitmq 报
|
||||
"error from registry: unknown error"),改为在本机拉好后传过去。
|
||||
|
||||
用法:
|
||||
powershell -ExecutionPolicy Bypass -File deploy\build-middleware.ps1
|
||||
产物:
|
||||
deploy\dist\middleware.tar
|
||||
|
||||
随后在服务器执行:
|
||||
docker load -i middleware.tar
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
#>
|
||||
param(
|
||||
[string]$OutFile = "",
|
||||
[string[]]$Images = @("redis:7", "mongo:6.0", "postgres:16", "rabbitmq:3.13-management")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$Dist = Join-Path $Root "deploy\dist"
|
||||
if (-not $OutFile) { $OutFile = Join-Path $Dist "middleware.tar" }
|
||||
|
||||
$mirrors = @("docker.m.daocloud.io", "docker.1ms.run", "docker.xuanyuan.me", "hub.rat.dev")
|
||||
|
||||
function Info($m) { Write-Host "[中间件] $m" -ForegroundColor Green }
|
||||
|
||||
function Invoke-Quiet([scriptblock]$sb) {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
& $sb 2>&1 | Out-Null
|
||||
$code = $LASTEXITCODE
|
||||
$ErrorActionPreference = $prev
|
||||
return $code
|
||||
}
|
||||
|
||||
# ---------- 引擎检查 ----------
|
||||
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw "未找到 docker,请先安装并启动 Docker Desktop" }
|
||||
if ((Invoke-Quiet { docker info }) -ne 0) { throw "Docker 引擎未就绪,请先启动 Docker Desktop 并等托盘图标变绿" }
|
||||
|
||||
# ---------- 拉取镜像(逐个镜像站回退)----------
|
||||
foreach ($img in $Images) {
|
||||
if ((Invoke-Quiet { docker image inspect $img }) -eq 0) { Info "已存在,跳过: $img"; continue }
|
||||
|
||||
$done = $false
|
||||
foreach ($m in $mirrors) {
|
||||
Info "拉取 $img (经 $m)..."
|
||||
if ((Invoke-Quiet { docker pull "$m/library/$img" }) -eq 0) {
|
||||
Invoke-Quiet { docker tag "$m/library/$img" $img } | Out-Null
|
||||
Info "OK: $img <= $m"
|
||||
$done = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $done) { throw "镜像 $img 在所有镜像站都拉取失败,请稍后重试" }
|
||||
}
|
||||
|
||||
# ---------- 导出 tar ----------
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $OutFile) | Out-Null
|
||||
Info "导出到 $OutFile (体积较大,稍候)..."
|
||||
if ((Invoke-Quiet { docker save -o $OutFile @Images }) -ne 0) { throw "导出镜像失败" }
|
||||
$mb = [math]::Round((Get-Item $OutFile).Length / 1MB, 1)
|
||||
|
||||
Info "完成!$OutFile ($mb MB)"
|
||||
Write-Host "下一步 —— 本机上传:" -ForegroundColor Cyan
|
||||
Write-Host " scp `"$OutFile`" root@<服务器IP>:/opt/buyu/" -ForegroundColor Cyan
|
||||
Write-Host "服务器执行:" -ForegroundColor Cyan
|
||||
Write-Host " cd /opt/buyu && docker load -i middleware.tar && docker compose -f deploy/docker-compose.yml up -d" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# 服务器侧:加载镜像包并按需启动(无需联网、无需 sdk 镜像)
|
||||
#
|
||||
# 用法:
|
||||
# # 1) 加载完整包并启动全部
|
||||
# bash deploy/load-and-up.sh deploy/dist/buyu-images-<tag>.tar
|
||||
#
|
||||
# # 2) 只加载某个服务的包并只重启该服务(增量更新)
|
||||
# bash deploy/load-and-up.sh deploy/dist/buyu-images-admin-<tag>.tar admin
|
||||
# bash deploy/load-and-up.sh deploy/dist/buyu-images-admin-fishgame-<tag>.tar admin fishgame
|
||||
#
|
||||
# 说明:镜像内的配置(网关 ocelot 下游改写、捕鱼对外地址)已在构建时固化;
|
||||
# 服务器只做「加载镜像 + 按需重启」,不涉及编译。
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE="$ROOT/deploy/docker-compose.yml"
|
||||
TAR="${1:-}"
|
||||
shift || true
|
||||
SERVICES=("$@")
|
||||
|
||||
log() { echo -e "\033[1;32m[部署]\033[0m $*"; }
|
||||
|
||||
[ -n "$TAR" ] || { echo "用法: bash deploy/load-and-up.sh <镜像包.tar> [服务名...]"; exit 1; }
|
||||
[ -f "$TAR" ] || { echo "找不到镜像包: $TAR"; exit 1; }
|
||||
command -v docker >/dev/null 2>&1 || { echo "未检测到 docker,请先安装 Docker"; exit 1; }
|
||||
docker compose version >/dev/null 2>&1 || { echo "未检测到 docker compose v2"; exit 1; }
|
||||
|
||||
log "1/3 加载镜像(tar 较大,需要一点时间)..."
|
||||
docker load -i "$TAR"
|
||||
|
||||
log "2/3 启动容器..."
|
||||
mkdir -p "$ROOT/deploy"/logs/{gateway,admin,user,customer,chess,fishroom,fishgame}
|
||||
mkdir -p "$ROOT/deploy"/uploads/{admin,user,customer}
|
||||
|
||||
if [ ${#SERVICES[@]} -gt 0 ]; then
|
||||
log "仅重建/重启: ${SERVICES[*]}"
|
||||
docker compose -f "$COMPOSE" up -d --force-recreate "${SERVICES[@]}"
|
||||
CHECK=("${SERVICES[@]}")
|
||||
else
|
||||
docker compose -f "$COMPOSE" up -d
|
||||
CHECK=(gateway admin user customer chess fishroom fishgame)
|
||||
fi
|
||||
|
||||
log "3/3 健康检查..."
|
||||
sleep 6
|
||||
declare -A PORT=( [gateway]=8001 [admin]=5241 [user]=5245 [customer]=5247 [chess]=5242 [fishroom]=5244 [fishgame]=5248 )
|
||||
for name in "${CHECK[@]}"; do
|
||||
port="${PORT[$name]:-}"
|
||||
[ -n "$port" ] || continue
|
||||
ok=""
|
||||
for _ in $(seq 1 20); do
|
||||
if curl -fsS -o /dev/null --max-time 2 "http://localhost:$port/health"; then ok="✔"; break; fi
|
||||
sleep 2
|
||||
done
|
||||
echo " ${ok:-✘} $name http://localhost:$port/health"
|
||||
done
|
||||
|
||||
echo ""
|
||||
log "完成。网关统一入口: http://<服务器IP或域名>:8001"
|
||||
@@ -0,0 +1,49 @@
|
||||
# ============================================================
|
||||
# 捕鱼/棋牌平台 - Nginx 反向代理
|
||||
# 放到宝塔站点目录(该目录下的 *.conf 被全局 nginx.conf 的 http 块 include):
|
||||
# /www/server/panel/vhost/nginx/buyu.conf
|
||||
# 生效:nginx -t && systemctl reload nginx
|
||||
# 说明:所有后端都在本机(Docker 映射到 127.0.0.1),Nginx 只代理到本机端口。
|
||||
# ============================================================
|
||||
|
||||
# WebSocket 升级映射(名字加 buyu 前缀,避免与宝塔/其它站点冲突)
|
||||
map $http_upgrade $buyu_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name 47.109.132.12; # 换成你的域名或服务器公网 IP
|
||||
|
||||
client_max_body_size 50m;
|
||||
|
||||
# 管理后台静态页面(AdminHost 的 wwwroot,访问 http://<IP>/admin/index.html)
|
||||
location /admin/ {
|
||||
proxy_pass http://127.0.0.1:5241;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 上传的图片(公告图、客服聊天图等)
|
||||
location /uploads/ {
|
||||
proxy_pass http://127.0.0.1:5241;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# 其余全部走网关(HTTP 接口 + SignalR WebSocket)
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade; # SignalR 必需
|
||||
proxy_set_header Connection $buyu_upgrade; # SignalR 必需
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s; # 长连接不要被掐断
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 0524 平台:按现有约定替换/回滚单个(或多个)服务容器
|
||||
# 位置:服务器 /mnt/web/0524-replace.sh
|
||||
#
|
||||
# 用法:
|
||||
# bash 0524-replace.sh admin # 用 0524admin:latest 重建 admin
|
||||
# bash 0524-replace.sh fishgame admin # 一次替换多个
|
||||
# TAG=dual-20261008 bash 0524-replace.sh admin # 回滚 admin 到旧镜像
|
||||
#
|
||||
# 约定(与线上既有部署保持一致):
|
||||
# - 网络 webapi_backend
|
||||
# - 端口只绑 127.0.0.1
|
||||
# - 配置挂载 /mnt/web/config -> /app/Config(全服务共用)
|
||||
# - 日志挂载 /mnt/web/logs -> /app/logs
|
||||
# - ASPNETCORE_URLS 按各服务内部端口
|
||||
# ============================================================
|
||||
set -u
|
||||
|
||||
NET=webapi_backend
|
||||
CFG=/mnt/web/config
|
||||
LOGS=/mnt/web/logs
|
||||
UPLOADS=/mnt/web/uploads
|
||||
TAG="${TAG:-latest}"
|
||||
ROLLBACK_TAG="${ROLLBACK_TAG:-dual-20261008}"
|
||||
|
||||
ALL="gateway admin user customer chess fishroom fishgame"
|
||||
|
||||
# 容器内部监听端口
|
||||
internal_port() {
|
||||
case "$1" in
|
||||
gateway) echo 8001 ;;
|
||||
admin) echo 5241 ;;
|
||||
user) echo 5245 ;;
|
||||
customer) echo 5247 ;;
|
||||
chess) echo 5242 ;;
|
||||
fishroom) echo 5244 ;;
|
||||
fishgame) echo 8001 ;;
|
||||
esac
|
||||
}
|
||||
# 宿主机映射端口(只绑 127.0.0.1)
|
||||
host_port() {
|
||||
case "$1" in
|
||||
gateway) echo 8001 ;;
|
||||
admin) echo 5241 ;;
|
||||
user) echo 5245 ;;
|
||||
customer) echo 5247 ;;
|
||||
chess) echo 5242 ;;
|
||||
fishroom) echo 5244 ;;
|
||||
fishgame) echo 5248 ;;
|
||||
esac
|
||||
}
|
||||
# 额外的 -v/-e 参数
|
||||
extra_args() {
|
||||
case "$1" in
|
||||
gateway) echo "-v $CFG/ocelot.json:/app/ocelot.json -v $UPLOADS:/app/uploads" ;;
|
||||
admin) echo "-v $UPLOADS:/app/wwwroot/uploads" ;;
|
||||
customer) echo "-v $UPLOADS:/app/wwwroot/uploads" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
# INSTANCE_ID(日志文件名里用于区分实例)
|
||||
instance_id() {
|
||||
case "$1" in
|
||||
gateway) echo "" ;;
|
||||
fishgame) echo "1" ;;
|
||||
*) echo "Container_Port_$(host_port "$1")" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_container() {
|
||||
local svc=$1 tag=$2
|
||||
local ip hp inst extra
|
||||
ip=$(internal_port "$svc"); hp=$(host_port "$svc"); inst=$(instance_id "$svc"); extra=$(extra_args "$svc")
|
||||
|
||||
docker rm -f "0524$svc" >/dev/null 2>&1
|
||||
local envar="ASPNETCORE_URLS=http://+:$ip"
|
||||
[ -n "$inst" ] && envar="$envar -e INSTANCE_ID=$inst"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
docker run -d --name "0524$svc" --restart always --network "$NET" \
|
||||
-e $envar $extra \
|
||||
-p "127.0.0.1:$hp:$ip" \
|
||||
-v "$CFG:/app/Config" -v "$LOGS:/app/logs" -v "$CFG/nlog.config:/app/nlog.config" \
|
||||
"0524$svc:$tag" >/dev/null
|
||||
}
|
||||
|
||||
health() {
|
||||
local hp; hp=$(host_port "$1")
|
||||
for _ in $(seq 1 15); do
|
||||
local c; c=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$hp/health")
|
||||
[ "$c" = "200" ] && return 0
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
replace_one() {
|
||||
local svc=$1
|
||||
echo "=== 替换 $svc (0524$svc:$TAG)==="
|
||||
run_container "$svc" "$TAG"
|
||||
if health "$svc"; then
|
||||
echo " ✔ $svc 健康检查通过"
|
||||
else
|
||||
echo " ✘ $svc 未通过,输出日志并回滚到 $ROLLBACK_TAG"
|
||||
docker logs --tail 40 "0524$svc" 2>&1 | tail -20
|
||||
run_container "$svc" "$ROLLBACK_TAG"
|
||||
health "$svc" && echo " ↩ 已回滚,$svc 恢复正常" || echo " !! 回滚后仍不健康,请人工介入"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "用法: bash 0524-replace.sh <服务...> (可选服务: $ALL)"
|
||||
echo " TAG=dual-20261008 bash 0524-replace.sh <服务...> # 回滚"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for svc in "$@"; do
|
||||
case " $ALL " in
|
||||
*" $svc "*) replace_one "$svc" ;;
|
||||
*) echo "未知服务: $svc(可选: $ALL)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 当前业务容器 ==="
|
||||
docker ps --format '{{.Names}} => {{.Image}} => {{.Status}}' | grep 0524
|
||||
Reference in New Issue
Block a user