1
This commit is contained in:
@@ -361,3 +361,8 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# ===== 部署产物(deploy/deploy.sh 运行时生成,不入库)=====
|
||||
deploy/publish/
|
||||
deploy/logs/
|
||||
deploy/uploads/
|
||||
@@ -814,7 +814,7 @@ namespace FishGameService.GameService
|
||||
|
||||
}
|
||||
|
||||
public async Task<long> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
|
||||
public async Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
|
||||
{
|
||||
return await _distributedLock.TryExecuteAsync(
|
||||
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
|
||||
@@ -822,12 +822,12 @@ namespace FishGameService.GameService
|
||||
retry: true);
|
||||
}
|
||||
|
||||
private async Task<long> KillFishInternal(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
|
||||
private async Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFishInternal(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
|
||||
{
|
||||
if (!VerifySign(cid, $"{roomId}|{userId}|{fishNumber}|{fihsId}|{gold}|{timespan}|{bulletId}|{timestamp}", timestamp, sign))
|
||||
{
|
||||
_sendResult.ErrorMessage(cid, ErrorCodes.签名错误, "签名错误");
|
||||
return 0;
|
||||
return (0, 0, 0, 0, 0);
|
||||
}
|
||||
;
|
||||
|
||||
@@ -847,7 +847,7 @@ namespace FishGameService.GameService
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cid))
|
||||
_sendResult.ErrorMessage(cid, ErrorCodes.鱼配置不存在, "鱼配置不存在");
|
||||
return 0;
|
||||
return (0, 0, 0, 0, 0);
|
||||
}
|
||||
var gameRoom = _redisService.GetGameRoom(roomId);
|
||||
if (gameRoom == null)
|
||||
@@ -855,7 +855,7 @@ namespace FishGameService.GameService
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cid))
|
||||
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间不存在");
|
||||
return 0;
|
||||
return (0, 0, 0, 0, 0);
|
||||
}
|
||||
var player = gameRoom.Players.Where(c => c.UserId == userId).FirstOrDefault();
|
||||
if (player == null)
|
||||
@@ -863,31 +863,32 @@ namespace FishGameService.GameService
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cid))
|
||||
_sendResult.ErrorMessage(cid, ErrorCodes.玩家不在房间, "玩家不在房间");
|
||||
return 0;
|
||||
return (0, 0, 0, 0, 0);
|
||||
}
|
||||
if (player.State == 2)
|
||||
{
|
||||
|
||||
return player.Gold;
|
||||
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
|
||||
}
|
||||
if (!IsCallerPlayer(cid, gameRoom, player))
|
||||
{
|
||||
_hubGame.SendUserCId(cid, "KickToMainPage", new { userId });
|
||||
return player.Gold;
|
||||
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
|
||||
}
|
||||
if (gold <= 0 || string.IsNullOrWhiteSpace(bulletId))
|
||||
{
|
||||
_sendResult.ErrorMessage(cid, ErrorCodes.子弹参数无效, "子弹参数无效");
|
||||
return player.Gold;
|
||||
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
|
||||
}
|
||||
|
||||
if (!TryConsumeBullet(roomId, userId, bulletId))
|
||||
{
|
||||
|
||||
return player.Gold;
|
||||
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
|
||||
}
|
||||
|
||||
var isKill = false;
|
||||
long killExpGain = 0;
|
||||
var factor = 0.0;
|
||||
var playerFactor = await GetPlayerFactorAsync(roomId, userId);
|
||||
|
||||
@@ -897,7 +898,7 @@ namespace FishGameService.GameService
|
||||
serverFishId = fishIdLong;
|
||||
if (!_fishSpawnService.ValidateFishHit(roomId, fishIdLong))
|
||||
{
|
||||
return player.Gold;
|
||||
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1006,7 +1007,7 @@ namespace FishGameService.GameService
|
||||
catch (OverflowException)
|
||||
{
|
||||
_sendResult.ErrorMessage(cid, ErrorCodes.金币参数超出范围, "金币参数超出范围");
|
||||
return player.Gold;
|
||||
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
|
||||
}
|
||||
(long gold, double beishu, object res) res;
|
||||
if (isKill)
|
||||
@@ -1062,7 +1063,39 @@ namespace FishGameService.GameService
|
||||
|
||||
var expGain = fishConfig.FishType is FishTypeEnum.Boss鱼 or FishTypeEnum.特殊鱼 or FishTypeEnum.特殊Boss
|
||||
? 1000 : fishConfig.FishScore;
|
||||
_redisService.AddFishingExpProgress(roomId, player.UserId, expGain);
|
||||
if (expGain > 0)
|
||||
{
|
||||
var expSaved = false;
|
||||
try
|
||||
{
|
||||
var expResult = await _userService.UpdateAssets(new UserAssetParam
|
||||
{
|
||||
UserId = player.UserId,
|
||||
Exp = expGain,
|
||||
ChangeType = UserAssetChangeTypeEnum.增加,
|
||||
Source = RewardSource.击杀鱼,
|
||||
Remark = "捕鱼经验",
|
||||
RoomInfo = roomId,
|
||||
IsAddRecord = false
|
||||
});
|
||||
expSaved = expResult.isok;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "击杀经验实时入库异常 roomId={RoomId} userId={UserId} exp={Exp}", roomId, player.UserId, expGain);
|
||||
}
|
||||
|
||||
if (expSaved)
|
||||
{
|
||||
ApplyFishingExp(player, expGain);
|
||||
killExpGain = expGain;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("击杀经验实时入库未成功,转为 Redis 累计待离房结算 roomId={RoomId} userId={UserId} exp={Exp}", roomId, player.UserId, expGain);
|
||||
_redisService.AddFishingExpProgress(roomId, player.UserId, expGain);
|
||||
}
|
||||
}
|
||||
|
||||
if ((fishConfig.FishType == FishTypeEnum.Boss鱼 || fishConfig.FishType == FishTypeEnum.特殊Boss || fishConfig.FishType == FishTypeEnum.特殊鱼)
|
||||
&& fishNumber != "12028"
|
||||
@@ -1135,7 +1168,21 @@ namespace FishGameService.GameService
|
||||
}
|
||||
_redisService.AddSystemGoldDay(gold - (isKill ? skillGold : 0));
|
||||
|
||||
return player.Gold;
|
||||
return (player.Gold, killExpGain, player.Exp, player.Level, player.MaxExp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 击杀经验实时入库成功后,同步房间内玩家的经验/等级/经验上限(与 UserEntity.LevelVerify 的升级判定保持一致)。
|
||||
/// </summary>
|
||||
private void ApplyFishingExp(RedisGamePlayer player, long gainExp)
|
||||
{
|
||||
player.Exp += gainExp;
|
||||
var levelCfg = _gameConfig?.UserLevelConfigs?.FirstOrDefault(c => player.Exp >= c.ExpMin && player.Exp < c.ExpMax);
|
||||
if (levelCfg != null)
|
||||
{
|
||||
player.Level = levelCfg.Level;
|
||||
player.MaxExp = levelCfg.ExpMax;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBullets(string roomId, string userId, List<string> bulletIds)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -25,7 +25,7 @@ namespace FishGameService.GameService
|
||||
|
||||
Task<(List<string> bullets, long gold)> FireCannon(string cid, string roomId, string userId, long gold, int count, long timestamp, string sign, object extra = null);
|
||||
|
||||
Task<long> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign);
|
||||
Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign);
|
||||
|
||||
Task LeaveRoom(string cid, string roomId, string userId);
|
||||
|
||||
|
||||
@@ -313,8 +313,16 @@ namespace FishGameService.Hubs
|
||||
{
|
||||
string cid = Context.ConnectionId;
|
||||
|
||||
var currentGold = await _fishingService.KillFish(cid, roomId, userId, fishNumber, fihsId, gold, timespan, bulletId, timestamp, sign);
|
||||
return new { gold = currentGold, timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() };
|
||||
var result = await _fishingService.KillFish(cid, roomId, userId, fishNumber, fihsId, gold, timespan, bulletId, timestamp, sign);
|
||||
return new
|
||||
{
|
||||
gold = result.gold,
|
||||
gainExp = result.gainExp,
|
||||
exp = result.exp,
|
||||
level = result.level,
|
||||
expMax = result.expMax,
|
||||
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task LeaveRoom(string roomId, string userId)
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace FishGameService.Services
|
||||
RoomLevel = roomLevel,
|
||||
Config = config,
|
||||
};
|
||||
ResetBossSpawnState(state);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
lock (state.Lock)
|
||||
@@ -132,6 +133,7 @@ namespace FishGameService.Services
|
||||
|
||||
state.Config = newConfig;
|
||||
state.NextSpawnByType.Clear();
|
||||
ResetBossSpawnState(state);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -893,6 +895,12 @@ namespace FishGameService.Services
|
||||
return spawned;
|
||||
}
|
||||
|
||||
if (prob.FishType == (int)CommonDomain.Enum.FishTypeEnum.Boss鱼)
|
||||
return SpawnBossFishByQueue(state, candidates, spawnCount, now, random, isFirst);
|
||||
|
||||
if (prob.FishType == (int)CommonDomain.Enum.FishTypeEnum.特殊Boss)
|
||||
return SpawnSpecialBossByCycle(state, candidates, spawnCount, now, random, isFirst);
|
||||
|
||||
var maxAttempts = Math.Max(20, spawnCount * 10);
|
||||
for (int attempt = 0; spawned.Count < spawnCount && attempt < maxAttempts; attempt++)
|
||||
{
|
||||
@@ -921,7 +929,6 @@ namespace FishGameService.Services
|
||||
now < nextAllowedAt)
|
||||
continue;
|
||||
|
||||
var fishId = NextFishId();
|
||||
var routeId = PickRoute(fishCfg, random, requestedOdd);
|
||||
|
||||
if (alternateBySide && ((routeId & 1) == 1) != requestedOdd)
|
||||
@@ -962,49 +969,7 @@ namespace FishGameService.Services
|
||||
preferOdd = !preferOdd;
|
||||
}
|
||||
|
||||
var times = fishCfg.Times > 0 ? fishCfg.Times : 8;
|
||||
|
||||
DateTime createdAt;
|
||||
if (isFirst)
|
||||
{
|
||||
var pastOffset = times * (0.2 + random.NextDouble() * 0.6);
|
||||
createdAt = now.AddSeconds(-pastOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
createdAt = now;
|
||||
}
|
||||
|
||||
var activeFish = new ActiveFish
|
||||
{
|
||||
FishId = fishId,
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAt = createdAt,
|
||||
ExpireAt = createdAt.AddSeconds(times),
|
||||
IsKilled = false
|
||||
};
|
||||
|
||||
state.ActiveFishes[fishId] = activeFish;
|
||||
if (fishCfg.FishType == 2 || fishCfg.FishType == 5)
|
||||
{
|
||||
state.NextAllowedSpawnByFishNumber[fishCfg.FishNumber] =
|
||||
activeFish.ExpireAt.AddSeconds(RestrictedFishRespawnBufferSeconds);
|
||||
}
|
||||
state.FishCountByType.TryGetValue(fishCfg.FishType, out var fc);
|
||||
state.FishCountByType[fishCfg.FishType] = fc + 1;
|
||||
|
||||
spawned.Add(new SpawnFishItem
|
||||
{
|
||||
FishId = fishId,
|
||||
FishIds = new List<long> { fishId },
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAtMs = new DateTimeOffset(createdAt).ToUnixTimeMilliseconds(),
|
||||
ExpireAtMs = new DateTimeOffset(activeFish.ExpireAt).ToUnixTimeMilliseconds()
|
||||
});
|
||||
spawned.Add(AddActiveFish(state, fishCfg, routeId, isFirst, now, random));
|
||||
}
|
||||
}
|
||||
else if (prob.Type == 2 && config.FishArrays != null)
|
||||
@@ -1139,6 +1104,221 @@ namespace FishGameService.Services
|
||||
return spawned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置 Boss 排队状态:Boss鱼(FishType=2)队列、回合计数、特殊Boss(FishType=5)轮询列表
|
||||
/// </summary>
|
||||
private static void ResetBossSpawnState(RoomFishState state)
|
||||
{
|
||||
var fishes = state.Config?.Fishes ?? new List<FishSpawnFish>();
|
||||
|
||||
state.BossQueue = fishes
|
||||
.Where(f => f.FishType == (int)CommonDomain.Enum.FishTypeEnum.Boss鱼 &&
|
||||
f.FishWeight > 0 && !string.IsNullOrWhiteSpace(f.FishNumber))
|
||||
.Select(f => f.FishNumber)
|
||||
.Distinct()
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.ToList();
|
||||
state.BossQueueTotal = state.BossQueue.Count;
|
||||
state.BossSpawnedThisRound = new List<string>();
|
||||
state.BossRoundBatchCount = 0;
|
||||
|
||||
state.SpecialBossCycle = fishes
|
||||
.Where(f => f.FishType == (int)CommonDomain.Enum.FishTypeEnum.特殊Boss &&
|
||||
f.FishWeight > 0 && !string.IsNullOrWhiteSpace(f.FishNumber))
|
||||
.Select(f => f.FishNumber)
|
||||
.Distinct()
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boss鱼排队出鱼:按队列依次轮流出、不重复;每 maxBosscount/2 轮(批次)为一个回合,
|
||||
/// 回合结束后本回合出过的鱼重新添加到随机出 Boss 的列表中。
|
||||
/// </summary>
|
||||
private List<SpawnFishItem> SpawnBossFishByQueue(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
int spawnCount, DateTime now, Random random, bool isFirst)
|
||||
{
|
||||
var spawned = new List<SpawnFishItem>();
|
||||
if (state.BossQueueTotal == 0 && state.BossQueue.Count == 0 && state.BossSpawnedThisRound.Count == 0)
|
||||
ResetBossSpawnState(state);
|
||||
|
||||
while (spawned.Count < spawnCount)
|
||||
{
|
||||
var requestedOdd = state.PreferOddRestrictedRoute;
|
||||
var fishCfg = TakeNextEligibleBoss(state, candidates, requestedOdd, now);
|
||||
if (fishCfg == null)
|
||||
break;
|
||||
|
||||
var routeId = PickRoute(fishCfg, random, requestedOdd);
|
||||
state.PreferOddRestrictedRoute = (routeId & 1) == 0;
|
||||
spawned.Add(AddActiveFish(state, fishCfg, routeId, isFirst, now, random));
|
||||
}
|
||||
|
||||
state.BossRoundBatchCount++;
|
||||
var roundLength = Math.Max(1, state.BossQueueTotal / 2);
|
||||
if (state.BossRoundBatchCount >= roundLength)
|
||||
{
|
||||
if (state.BossSpawnedThisRound.Count > 0)
|
||||
state.BossQueue.AddRange(state.BossSpawnedThisRound);
|
||||
state.BossSpawnedThisRound.Clear();
|
||||
state.BossRoundBatchCount = 0;
|
||||
}
|
||||
|
||||
return spawned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 特殊Boss轮询出鱼:每轮按随机顺序把每个特殊Boss各出一次,出完一轮重新随机排序。
|
||||
/// </summary>
|
||||
private List<SpawnFishItem> SpawnSpecialBossByCycle(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
int spawnCount, DateTime now, Random random, bool isFirst)
|
||||
{
|
||||
var spawned = new List<SpawnFishItem>();
|
||||
|
||||
while (spawned.Count < spawnCount)
|
||||
{
|
||||
var requestedOdd = state.PreferOddRestrictedRoute;
|
||||
var fishCfg = TakeNextEligibleSpecialBoss(state, candidates, requestedOdd, now);
|
||||
if (fishCfg == null)
|
||||
break;
|
||||
|
||||
var routeId = PickRoute(fishCfg, random, requestedOdd);
|
||||
state.PreferOddRestrictedRoute = (routeId & 1) == 0;
|
||||
spawned.Add(AddActiveFish(state, fishCfg, routeId, isFirst, now, random));
|
||||
}
|
||||
|
||||
return spawned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Boss 队列依次取鱼:取队头,仍活跃或处于冷却期的鱼暂缓到队尾,取出的鱼记录到本回合已出列表。
|
||||
/// </summary>
|
||||
private FishSpawnFish? TakeNextEligibleBoss(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
bool requestedOdd, DateTime now)
|
||||
{
|
||||
var scanLimit = state.BossQueue.Count;
|
||||
for (var i = 0; i < scanLimit && state.BossQueue.Count > 0; i++)
|
||||
{
|
||||
var fishNumber = state.BossQueue[0];
|
||||
state.BossQueue.RemoveAt(0);
|
||||
|
||||
var fishCfg = candidates.FirstOrDefault(f =>
|
||||
string.Equals(f.FishNumber, fishNumber, StringComparison.OrdinalIgnoreCase));
|
||||
if (fishCfg == null)
|
||||
continue;
|
||||
|
||||
if (!HasRouteForSide(fishCfg, requestedOdd) || IsFishNumberActiveOrCooling(state, fishNumber, now))
|
||||
{
|
||||
state.BossQueue.Add(fishNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
state.BossSpawnedThisRound.Add(fishNumber);
|
||||
return fishCfg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从特殊Boss轮询列表依次取鱼:取队头,仍活跃或处于冷却期的鱼暂缓到队尾;列表取空后重新随机排序。
|
||||
/// </summary>
|
||||
private FishSpawnFish? TakeNextEligibleSpecialBoss(RoomFishState state, List<FishSpawnFish> candidates,
|
||||
bool requestedOdd, DateTime now)
|
||||
{
|
||||
if (state.SpecialBossCycle.Count == 0)
|
||||
{
|
||||
state.SpecialBossCycle = candidates
|
||||
.Select(f => f.FishNumber)
|
||||
.Distinct()
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var scanLimit = state.SpecialBossCycle.Count;
|
||||
for (var i = 0; i < scanLimit && state.SpecialBossCycle.Count > 0; i++)
|
||||
{
|
||||
var fishNumber = state.SpecialBossCycle[0];
|
||||
state.SpecialBossCycle.RemoveAt(0);
|
||||
|
||||
var fishCfg = candidates.FirstOrDefault(f =>
|
||||
string.Equals(f.FishNumber, fishNumber, StringComparison.OrdinalIgnoreCase));
|
||||
if (fishCfg == null)
|
||||
continue;
|
||||
|
||||
if (!HasRouteForSide(fishCfg, requestedOdd) || IsFishNumberActiveOrCooling(state, fishNumber, now))
|
||||
{
|
||||
state.SpecialBossCycle.Add(fishNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
return fishCfg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsFishNumberActiveOrCooling(RoomFishState state, string fishNumber, DateTime now)
|
||||
{
|
||||
if (state.ActiveFishes.Values.Any(f => !f.IsKilled && f.ExpireAt > now &&
|
||||
string.Equals(f.FishNumber, fishNumber, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
return state.NextAllowedSpawnByFishNumber.TryGetValue(fishNumber, out var nextAllowedAt) && now < nextAllowedAt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将鱼写入房间活跃鱼状态并返回出鱼项(首批鱼向前偏移创建时间,模拟已游动一段时间)。
|
||||
/// </summary>
|
||||
private SpawnFishItem AddActiveFish(RoomFishState state, FishSpawnFish fishCfg, int routeId,
|
||||
bool isFirst, DateTime now, Random random)
|
||||
{
|
||||
var times = fishCfg.Times > 0 ? fishCfg.Times : 8;
|
||||
|
||||
DateTime createdAt;
|
||||
if (isFirst)
|
||||
{
|
||||
var pastOffset = times * (0.2 + random.NextDouble() * 0.6);
|
||||
createdAt = now.AddSeconds(-pastOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
createdAt = now;
|
||||
}
|
||||
|
||||
var fishId = NextFishId();
|
||||
var activeFish = new ActiveFish
|
||||
{
|
||||
FishId = fishId,
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAt = createdAt,
|
||||
ExpireAt = createdAt.AddSeconds(times),
|
||||
IsKilled = false
|
||||
};
|
||||
|
||||
state.ActiveFishes[fishId] = activeFish;
|
||||
if (fishCfg.FishType == 2 || fishCfg.FishType == 5)
|
||||
{
|
||||
state.NextAllowedSpawnByFishNumber[fishCfg.FishNumber] =
|
||||
activeFish.ExpireAt.AddSeconds(RestrictedFishRespawnBufferSeconds);
|
||||
}
|
||||
state.FishCountByType.TryGetValue(fishCfg.FishType, out var fc);
|
||||
state.FishCountByType[fishCfg.FishType] = fc + 1;
|
||||
|
||||
return new SpawnFishItem
|
||||
{
|
||||
FishId = fishId,
|
||||
FishIds = new List<long> { fishId },
|
||||
FishNumber = fishCfg.FishNumber,
|
||||
FishType = fishCfg.FishType,
|
||||
RouteId = routeId,
|
||||
CreatedAtMs = new DateTimeOffset(createdAt).ToUnixTimeMilliseconds(),
|
||||
ExpireAtMs = new DateTimeOffset(activeFish.ExpireAt).ToUnixTimeMilliseconds()
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetSpawnLogTypeName(int fishType)
|
||||
{
|
||||
return fishType switch
|
||||
@@ -1271,6 +1451,16 @@ namespace FishGameService.Services
|
||||
|
||||
public Dictionary<string, int> ActiveArrayFishCount { get; set; } = new Dictionary<string, int>();
|
||||
|
||||
public List<string> BossQueue { get; set; } = new List<string>();
|
||||
|
||||
public List<string> BossSpawnedThisRound { get; set; } = new List<string>();
|
||||
|
||||
public int BossQueueTotal { get; set; }
|
||||
|
||||
public int BossRoundBatchCount { get; set; }
|
||||
|
||||
public List<string> SpecialBossCycle { get; set; } = new List<string>();
|
||||
|
||||
public bool PreferOddArrayNumber { get; set; } = true;
|
||||
|
||||
public bool PreferOddRestrictedRoute { get; set; } = true;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# ============================================================
|
||||
# 通用运行时镜像(7 个 Host 共用,由 deploy/deploy.sh 调用)
|
||||
#
|
||||
# 用法示例:
|
||||
# docker build -f deploy/Dockerfile --build-arg APP_DLL=AdminHost.dll -t 0524admin deploy/publish/admin
|
||||
#
|
||||
# 说明:
|
||||
# - 发布产物由 deploy.sh 在 dotnet/sdk 容器内编译生成,本镜像只负责运行
|
||||
# - 容器内统一监听 80 端口(与 docker-compose 的端口映射对应)
|
||||
# ============================================================
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
ARG APP_DLL
|
||||
ENV APP_DLL=${APP_DLL} \
|
||||
ASPNETCORE_URLS=http://+:80 \
|
||||
ASPNETCORE_ENVIRONMENT=Production \
|
||||
TZ=Asia/Shanghai \
|
||||
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||
|
||||
# 确保可写 /app/logs 日志目录与 /app/wwwroot/uploads 上传目录
|
||||
USER root
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["sh", "-c", "exec dotnet ${APP_DLL}"]
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"JwtConfig": {
|
||||
"TokenKey": "token",
|
||||
"Issuer": "admin",
|
||||
"Audience": "",
|
||||
"SecretKey": "henanhuashengchongwuyouxiangongsikejizhongxin",
|
||||
"ValidateIssuer": false,
|
||||
"ValidateAudience": false,
|
||||
"ValidateIssuerSigningKey": false,
|
||||
"ValidateLifetime": false
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PgSqlConnection": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"CapConfig": {
|
||||
"Host": "rabbitmq",
|
||||
"UserName": "admin",
|
||||
"Password": "123456",
|
||||
"VirtualHost": "/",
|
||||
"PGLogHost": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"RedisConfig": {
|
||||
"Host": "redis",
|
||||
"Port": 6379,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Database": 1,
|
||||
"ConnectionString": "redis:6379,password=123456,defaultDatabase=2,abortConnect=false"
|
||||
},
|
||||
"MongoDbSettings": {
|
||||
"username": "root",
|
||||
"password": "root23456",
|
||||
"ServerUrl": "mongodb://mongo:27017"
|
||||
},
|
||||
"WXConfig": {
|
||||
"AppId": "wxceab9ce5b4474219",
|
||||
"Secret": "5a291d7b295c76da579611da3f83e059",
|
||||
"Url": "https://api.weixin.qq.com"
|
||||
},
|
||||
"BulletConfig": {
|
||||
"SignKey": "63215f0afa3f5b25",
|
||||
"MaxBulletPerUser": 100,
|
||||
"TimeWindowSeconds": 500000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"JwtConfig": {
|
||||
"TokenKey": "token",
|
||||
"Issuer": "admin",
|
||||
"Audience": "",
|
||||
"SecretKey": "henanhuashengchongwuyouxiangongsikejizhongxin",
|
||||
"ValidateIssuer": false,
|
||||
"ValidateAudience": false,
|
||||
"ValidateIssuerSigningKey": false,
|
||||
"ValidateLifetime": false
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PgSqlConnection": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"CapConfig": {
|
||||
"Host": "rabbitmq",
|
||||
"UserName": "admin",
|
||||
"Password": "123456",
|
||||
"VirtualHost": "/",
|
||||
"PGLogHost": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"RedisConfig": {
|
||||
"Host": "redis",
|
||||
"Port": 6379,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Database": 1,
|
||||
"ConnectionString": "redis:6379,password=123456,defaultDatabase=2,abortConnect=false"
|
||||
},
|
||||
"MongoDbSettings": {
|
||||
"username": "root",
|
||||
"password": "root23456",
|
||||
"ServerUrl": "mongodb://mongo:27017"
|
||||
},
|
||||
"WXConfig": {
|
||||
"AppId": "wxceab9ce5b4474219",
|
||||
"Secret": "5a291d7b295c76da579611da3f83e059",
|
||||
"Url": "https://api.weixin.qq.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"JwtConfig": {
|
||||
"TokenKey": "token",
|
||||
"Issuer": "admin",
|
||||
"Audience": "",
|
||||
"SecretKey": "henanhuashengchongwuyouxiangongsikejizhongxin",
|
||||
"ValidateIssuer": false,
|
||||
"ValidateAudience": false,
|
||||
"ValidateIssuerSigningKey": false,
|
||||
"ValidateLifetime": false
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PgSqlConnection": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"CapConfig": {
|
||||
"Host": "rabbitmq",
|
||||
"UserName": "admin",
|
||||
"Password": "123456",
|
||||
"VirtualHost": "/",
|
||||
"PGLogHost": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"RedisConfig": {
|
||||
"Host": "redis",
|
||||
"Port": 6379,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Database": 1,
|
||||
"ConnectionString": "redis:6379,password=123456,defaultDatabase=2,abortConnect=false"
|
||||
},
|
||||
"MongoDbSettings": {
|
||||
"username": "root",
|
||||
"password": "root23456",
|
||||
"ServerUrl": "mongodb://mongo:27017"
|
||||
},
|
||||
"CustomerService": {
|
||||
"HubUrl": "/customerHub",
|
||||
"MaxMessageSize": 65536
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"JwtConfig": {
|
||||
"TokenKey": "token",
|
||||
"Issuer": "admin",
|
||||
"Audience": "",
|
||||
"SecretKey": "henanhuashengchongwuyouxiangongsikejizhongxin",
|
||||
"ValidateIssuer": false,
|
||||
"ValidateAudience": false,
|
||||
"ValidateIssuerSigningKey": false,
|
||||
"ValidateLifetime": false
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PgSqlConnection": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"CapConfig": {
|
||||
"Host": "rabbitmq",
|
||||
"UserName": "admin",
|
||||
"Password": "123456",
|
||||
"VirtualHost": "/",
|
||||
"PGLogHost": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"RedisConfig": {
|
||||
"Host": "redis",
|
||||
"Port": 6379,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Database": 1,
|
||||
"ConnectionString": "redis:6379,password=123456,defaultDatabase=2,abortConnect=false"
|
||||
},
|
||||
"MongoDbSettings": {
|
||||
"username": "root",
|
||||
"password": "root23456",
|
||||
"ServerUrl": "mongodb://mongo:27017"
|
||||
},
|
||||
"WXConfig": {
|
||||
"AppId": "wxceab9ce5b4474219",
|
||||
"Secret": "5a291d7b295c76da579611da3f83e059",
|
||||
"Url": "https://api.weixin.qq.com"
|
||||
},
|
||||
"BulletConfig": {
|
||||
"SignKey": "63215f0afa3f5b25",
|
||||
"MaxBulletPerUser": 100,
|
||||
"TimeWindowSeconds": 5
|
||||
},
|
||||
"GameServerInstances": [
|
||||
"http://__PUBLIC_HOST__/gamehub?instance=1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"JwtConfig": {
|
||||
"TokenKey": "token",
|
||||
"Issuer": "admin",
|
||||
"Audience": "",
|
||||
"SecretKey": "henanhuashengchongwuyouxiangongsikejizhongxin",
|
||||
"ValidateIssuer": false,
|
||||
"ValidateAudience": false,
|
||||
"ValidateIssuerSigningKey": false,
|
||||
"ValidateLifetime": false
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PgSqlConnection": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"CapConfig": {
|
||||
"Host": "rabbitmq",
|
||||
"UserName": "admin",
|
||||
"Password": "123456",
|
||||
"VirtualHost": "/",
|
||||
"PGLogHost": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"RedisConfig": {
|
||||
"Host": "redis",
|
||||
"Port": 6379,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Database": 1,
|
||||
"ConnectionString": "redis:6379,password=123456,defaultDatabase=2,abortConnect=false"
|
||||
},
|
||||
"MongoDbSettings": {
|
||||
"username": "root",
|
||||
"password": "root23456",
|
||||
"ServerUrl": "mongodb://mongo:27017"
|
||||
},
|
||||
"WXConfig": {
|
||||
"AppId": "wxceab9ce5b4474219",
|
||||
"Secret": "5a291d7b295c76da579611da3f83e059",
|
||||
"Url": "https://api.weixin.qq.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Ocelot": {
|
||||
"Config": "ocelot.json"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"RedisConfig": {
|
||||
"Host": "redis",
|
||||
"Port": 6379,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Database": 1,
|
||||
"ConnectionString": "redis:6379,password=123456,defaultDatabase=2,abortConnect=false"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"JwtConfig": {
|
||||
"TokenKey": "token",
|
||||
"Issuer": "admin",
|
||||
"Audience": "",
|
||||
"SecretKey": "henanhuashengchongwuyouxiangongsikejizhongxin",
|
||||
"ValidateIssuer": false,
|
||||
"ValidateAudience": false,
|
||||
"ValidateIssuerSigningKey": false,
|
||||
"ValidateLifetime": false
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PgSqlConnection": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"CapConfig": {
|
||||
"Host": "rabbitmq",
|
||||
"UserName": "admin",
|
||||
"Password": "123456",
|
||||
"VirtualHost": "/",
|
||||
"PGLogHost": "host=pgsql;database=data_0524;user id=data_0524;password=123456;"
|
||||
},
|
||||
"RedisConfig": {
|
||||
"Host": "redis",
|
||||
"Port": 6379,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Database": 1,
|
||||
"ConnectionString": "redis:6379,password=123456,defaultDatabase=2,abortConnect=false"
|
||||
},
|
||||
"MongoDbSettings": {
|
||||
"username": "root",
|
||||
"password": "root23456",
|
||||
"ServerUrl": "mongodb://mongo:27017"
|
||||
},
|
||||
"WXConfig": {
|
||||
"AppId": "wxceab9ce5b4474219",
|
||||
"Secret": "5a291d7b295c76da579611da3f83e059",
|
||||
"Url": "https://api.weixin.qq.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# 捕鱼/棋牌平台 - 一键部署脚本(Linux 服务器 + Docker)
|
||||
#
|
||||
# 用法:
|
||||
# bash deploy/deploy.sh
|
||||
# PUBLIC_HOST=1.2.3.4:8001 bash deploy/deploy.sh # 指定客户端访问地址(含端口)
|
||||
#
|
||||
# 前置要求:
|
||||
# - Linux 服务器已安装 Docker(含 docker compose v2 插件)
|
||||
# - 服务器可访问 nuget.org 与 docker hub(首次编译需要联网拉取依赖)
|
||||
# - 建议预留 5GB 以上磁盘空间
|
||||
#
|
||||
# 脚本做的事:
|
||||
# 1. 在 dotnet/sdk:8.0 容器内编译发布 7 个 Host(服务器无需安装 .NET SDK)
|
||||
# 2. 覆盖各服务的部署版 Config/system.json,并把网关下游地址改写为容器名
|
||||
# 3. 构建 7 个镜像(0524gateway / 0524admin / 0524user / 0524customer /
|
||||
# 0524chess / 0524fishroom / 0524fishgame)
|
||||
# 4. 启动全部容器(4 个中间件 + 7 个应用)并做健康检查
|
||||
#
|
||||
# 常用运维命令:
|
||||
# 查看状态:docker compose -f deploy/docker-compose.yml ps
|
||||
# 查看日志:docker compose -f deploy/docker-compose.yml logs -f <服务名>
|
||||
# 停止全部:docker compose -f deploy/docker-compose.yml down
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DEPLOY_DIR="$ROOT/deploy"
|
||||
PUB_DIR="$DEPLOY_DIR/publish"
|
||||
COMPOSE_FILE="$DEPLOY_DIR/docker-compose.yml"
|
||||
SDK_IMAGE="mcr.microsoft.com/dotnet/sdk:8.0"
|
||||
NUGET_VOLUME="buyu-nuget"
|
||||
|
||||
log() { echo -e "\033[1;32m[部署]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[警告]\033[0m $*"; }
|
||||
|
||||
# 客户端访问地址(用于捕鱼对局层返回给客户端的 WebSocket 地址);不传则自动取本机第一个 IP
|
||||
if [ -z "${PUBLIC_HOST:-}" ]; then
|
||||
SERVER_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
PUBLIC_HOST="${SERVER_IP:-localhost}:8001"
|
||||
fi
|
||||
|
||||
# ---------- 0. 环境检查 ----------
|
||||
command -v docker >/dev/null 2>&1 || { echo "未检测到 docker,请先安装 Docker"; exit 1; }
|
||||
docker compose version >/dev/null 2>&1 || { echo "未检测到 docker compose v2,请安装 docker-compose-plugin"; exit 1; }
|
||||
[ -f "$COMPOSE_FILE" ] || { echo "缺少编排文件:$COMPOSE_FILE"; exit 1; }
|
||||
|
||||
# ---------- 1. 容器内编译发布 + 准备部署配置 ----------
|
||||
log "1/4 编译发布(在容器内进行,首次需要拉取 SDK 镜像与 NuGet 包,耗时较长)..."
|
||||
docker run --rm \
|
||||
-v "$ROOT":/src \
|
||||
-w /src \
|
||||
-v "$NUGET_VOLUME":/root/.nuget/packages \
|
||||
-e PUBLIC_HOST="$PUBLIC_HOST" \
|
||||
"$SDK_IMAGE" bash -c '
|
||||
set -e
|
||||
rm -rf /src/deploy/publish
|
||||
mkdir -p /src/deploy/publish
|
||||
dotnet publish Gateway/Gateway.csproj -c Release -o /src/deploy/publish/gateway --nologo
|
||||
dotnet publish AdminSrc/AdminHost/AdminHost.csproj -c Release -o /src/deploy/publish/admin --nologo
|
||||
dotnet publish UserSrc/UserHost/UserHost.csproj -c Release -o /src/deploy/publish/user --nologo
|
||||
dotnet publish CustomerSrc/CustomerHost/CustomerHost.csproj -c Release -o /src/deploy/publish/customer --nologo
|
||||
dotnet publish ChessGameHost/ChessGameHost.csproj -c Release -o /src/deploy/publish/chess --nologo
|
||||
dotnet publish FishGameSrc/FishRoomHost/FishRoomHost.csproj -c Release -o /src/deploy/publish/fishroom --nologo
|
||||
dotnet publish FishGameSrc/FishGameHost/FishGameHost.csproj -c Release -o /src/deploy/publish/fishgame --nologo
|
||||
|
||||
# 覆盖部署版配置(各 Host 的 Config/system.json 默认不会随发布输出,必须显式拷贝)
|
||||
for name in gateway admin user customer chess fishroom fishgame; do
|
||||
mkdir -p /src/deploy/publish/$name/Config
|
||||
cp /src/deploy/config/$name/system.json /src/deploy/publish/$name/Config/system.json
|
||||
cp /src/nlog.config /src/deploy/publish/$name/nlog.config
|
||||
done
|
||||
|
||||
# 捕鱼对局层:替换客户端要访问的 WebSocket 地址(经网关 /gamehub 路由)
|
||||
sed -i "s|__PUBLIC_HOST__|$PUBLIC_HOST|g" /src/deploy/publish/fishgame/Config/system.json
|
||||
|
||||
# 网关:ocelot 下游地址由 localhost:524x 改写为「容器名:80」(容器间互访)
|
||||
OCELOT=/src/deploy/publish/gateway/ocelot.json
|
||||
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 }|" \
|
||||
"$OCELOT"
|
||||
'
|
||||
|
||||
# ---------- 2. 构建 7 个应用镜像 ----------
|
||||
log "2/4 构建镜像..."
|
||||
build_image() { # 镜像名 入口dll 发布目录名
|
||||
docker build -f "$DEPLOY_DIR/Dockerfile" --build-arg "APP_DLL=$2" -t "$1" "$PUB_DIR/$3"
|
||||
echo " ✔ $1"
|
||||
}
|
||||
build_image 0524gateway Gateway.dll gateway
|
||||
build_image 0524admin AdminHost.dll admin
|
||||
build_image 0524user UserHost.dll user
|
||||
build_image 0524customer CustomerHost.dll customer
|
||||
build_image 0524chess ChessGameHost.dll chess
|
||||
build_image 0524fishroom FishRoomHost.dll fishroom
|
||||
build_image 0524fishgame FishGameHost.dll fishgame
|
||||
|
||||
# ---------- 3. 启动全部容器 ----------
|
||||
log "3/4 启动容器(4 个中间件 + 7 个应用)..."
|
||||
mkdir -p "$DEPLOY_DIR"/logs/{gateway,admin,user,customer,chess,fishroom,fishgame}
|
||||
mkdir -p "$DEPLOY_DIR"/uploads/{admin,user,customer}
|
||||
docker compose -f "$COMPOSE_FILE" up -d
|
||||
|
||||
# ---------- 4. 健康检查 ----------
|
||||
log "4/4 等待服务就绪..."
|
||||
sleep 8
|
||||
check() { # 地址 服务名
|
||||
for _ in $(seq 1 15); do
|
||||
if curl -fsS -o /dev/null --max-time 2 "$1"; then
|
||||
echo " ✔ $2 $1"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
warn "$2 未就绪:$1(排查:docker compose -f deploy/docker-compose.yml logs $2)"
|
||||
}
|
||||
check "http://localhost:8001/health" gateway
|
||||
check "http://localhost:5241/health" admin
|
||||
check "http://localhost:5245/health" user
|
||||
check "http://localhost:5247/health" customer
|
||||
check "http://localhost:5242/health" chess
|
||||
check "http://localhost:5244/health" fishroom
|
||||
check "http://localhost:5248/health" fishgame
|
||||
|
||||
echo ""
|
||||
log "部署完成"
|
||||
echo "----------------------------------------------------------"
|
||||
echo " 统一入口(网关) : http://$PUBLIC_HOST"
|
||||
echo " RabbitMQ 控制台 : http://<服务器IP>:15672(admin / 123456)"
|
||||
echo " 日志目录 : deploy/logs/<服务名>/日期/"
|
||||
echo " 上传文件目录 : deploy/uploads/<服务名>/"
|
||||
echo " 停止全部 : docker compose -f deploy/docker-compose.yml down"
|
||||
echo "----------------------------------------------------------"
|
||||
@@ -0,0 +1,152 @@
|
||||
# ============================================================
|
||||
# 捕鱼/棋牌平台 - 一键部署编排(4 个中间件 + 7 个应用服务)
|
||||
#
|
||||
# 启动:docker compose -f deploy/docker-compose.yml up -d
|
||||
# 停止:docker compose -f deploy/docker-compose.yml down
|
||||
# 日志:docker compose -f deploy/docker-compose.yml logs -f <服务名>
|
||||
#
|
||||
# 说明:
|
||||
# 1. 所有容器在同一网络(webapi_default),相互通过容器名访问:
|
||||
# mongo / redis / rabbitmq / pgsql / 0524gateway / 0524admin / ...
|
||||
# 2. 应用容器内部监听 80 端口(见 deploy/Dockerfile),宿主机映射 524x / 8001
|
||||
# 3. 网关的 ocelot.json 由 deploy.sh 在发布产物中改写为「容器名:80」的下游地址,
|
||||
# 因此网关容器不需要 host 网络,跨平台可用
|
||||
# 4. 日志挂载到 deploy/logs/<服务名>,上传文件挂载到 deploy/uploads/<服务名>
|
||||
# ============================================================
|
||||
name: webapi
|
||||
|
||||
services:
|
||||
# ==================== 中间件 ====================
|
||||
mongo:
|
||||
image: mongo:6.0
|
||||
container_name: mongo
|
||||
restart: always
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: root
|
||||
MONGO_INITDB_ROOT_PASSWORD: root23456
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: redis
|
||||
restart: always
|
||||
command: ["redis-server", "--requirepass", "123456", "--appendonly", "yes"]
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.13-management
|
||||
container_name: rabbitmq
|
||||
restart: always
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: admin
|
||||
RABBITMQ_DEFAULT_PASS: "123456"
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
volumes:
|
||||
- rabbitmq-data:/var/lib/rabbitmq
|
||||
|
||||
pgsql:
|
||||
image: postgres:16
|
||||
container_name: pgsql
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: data_0524
|
||||
POSTGRES_USER: data_0524
|
||||
POSTGRES_PASSWORD: "123456"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgsql-data:/var/lib/postgresql/data
|
||||
|
||||
# ==================== 应用服务 ====================
|
||||
gateway:
|
||||
image: 0524gateway
|
||||
container_name: 0524gateway
|
||||
restart: always
|
||||
ports:
|
||||
- "8001:80"
|
||||
depends_on:
|
||||
- admin
|
||||
- user
|
||||
- customer
|
||||
- chess
|
||||
- fishroom
|
||||
- fishgame
|
||||
volumes:
|
||||
- ./logs/gateway:/app/logs
|
||||
|
||||
admin:
|
||||
image: 0524admin
|
||||
container_name: 0524admin
|
||||
restart: always
|
||||
ports:
|
||||
- "5241:80"
|
||||
depends_on: [mongo, redis, rabbitmq, pgsql]
|
||||
volumes:
|
||||
- ./logs/admin:/app/logs
|
||||
- ./uploads/admin:/app/wwwroot/uploads
|
||||
|
||||
user:
|
||||
image: 0524user
|
||||
container_name: 0524user
|
||||
restart: always
|
||||
ports:
|
||||
- "5245:80"
|
||||
depends_on: [mongo, redis, rabbitmq, pgsql]
|
||||
volumes:
|
||||
- ./logs/user:/app/logs
|
||||
- ./uploads/user:/app/wwwroot/uploads
|
||||
|
||||
customer:
|
||||
image: 0524customer
|
||||
container_name: 0524customer
|
||||
restart: always
|
||||
ports:
|
||||
- "5247:80"
|
||||
depends_on: [mongo, redis, rabbitmq, pgsql]
|
||||
volumes:
|
||||
- ./logs/customer:/app/logs
|
||||
- ./uploads/customer:/app/wwwroot/uploads
|
||||
|
||||
chess:
|
||||
image: 0524chess
|
||||
container_name: 0524chess
|
||||
restart: always
|
||||
ports:
|
||||
- "5242:80"
|
||||
depends_on: [mongo, redis, rabbitmq, pgsql]
|
||||
volumes:
|
||||
- ./logs/chess:/app/logs
|
||||
|
||||
fishroom:
|
||||
image: 0524fishroom
|
||||
container_name: 0524fishroom
|
||||
restart: always
|
||||
ports:
|
||||
- "5244:80"
|
||||
depends_on: [mongo, redis, rabbitmq, pgsql]
|
||||
volumes:
|
||||
- ./logs/fishroom:/app/logs
|
||||
|
||||
fishgame:
|
||||
image: 0524fishgame
|
||||
container_name: 0524fishgame
|
||||
restart: always
|
||||
ports:
|
||||
- "5248:80" # 与 Gateway/ocelot.json 保持一致(仓库内旧 compose 写的 5243 已废弃)
|
||||
depends_on: [mongo, redis, rabbitmq, pgsql]
|
||||
volumes:
|
||||
- ./logs/fishgame:/app/logs
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
redis-data:
|
||||
rabbitmq-data:
|
||||
pgsql-data:
|
||||
Reference in New Issue
Block a user