diff --git a/README.md b/README.md index 979f6c9b14a7a5c3c8549950089e53365f4030c9..461c7ccc1a3ff630d3cd0ff4d899c290eb6ad520 100644 --- a/README.md +++ b/README.md @@ -66,72 +66,55 @@ territory-paint-game/ │ └── vite.config.js ├── backend/ # .NET 8后端项目 (DDD架构) │ ├── src/ -│ │ ├── TerritoryGame.API/ # 接口层 -│ │ │ ├── Controllers/ # RESTful API控制器 -│ │ │ │ ├── CommandController.cs # 命令处理 (CQRS) -│ │ │ │ └── QueryController.cs # 查询处理 (CQRS) -│ │ │ ├── Hubs/ # SignalR实时通信 +│ │ ├── TerritoryGame.API/ # 接口层 (ASP.NET Core Web API) +│ │ │ ├── Controllers/ # API控制器 +│ │ │ │ ├── CommandController.cs +│ │ │ │ └── QueryController.cs +│ │ │ ├── Hubs/ # SignalR Hubs │ │ │ │ └── GameHub.cs -│ │ │ ├── Program.cs # 启动配置 +│ │ │ ├── Program.cs │ │ │ └── appsettings.json -│ │ ├── TerritoryGame.Domain/ # 领域层 (核心业务逻辑) -│ │ │ ├── Entities/ # 实体 -│ │ │ │ ├── Game.cs # 游戏聚合根 -│ │ │ │ ├── Player.cs # 玩家实体 -│ │ │ │ └── GameRoom.cs # 房间实体 -│ │ │ ├── ValueObjects/ # 值对象 -│ │ │ │ ├── Position.cs # 位置坐标 -│ │ │ │ ├── Color.cs # 玩家颜色 -│ │ │ │ └── GameScore.cs # 游戏分数 -│ │ │ ├── Events/ # 领域事件 -│ │ │ │ ├── GameStartedEvent.cs -│ │ │ │ ├── AreaCapturedEvent.cs -│ │ │ │ └── GameEndedEvent.cs -│ │ │ ├── Services/ # 领域服务 -│ │ │ │ ├── IGameLogicService.cs -│ │ │ │ └── GameLogicService.cs # 游戏规则引擎 -│ │ │ └── Repositories/ # 仓储接口 -│ │ │ ├── IGameRepository.cs -│ │ │ └── IPlayerRepository.cs -│ │ ├── TerritoryGame.Application/ # 应用层 (用例编排) -│ │ │ ├── Commands/ # 命令模式 +│ │ ├── TerritoryGame.Application/ # 应用层 +│ │ │ ├── Commands/ # 命令定义 │ │ │ │ ├── CreateRoomCommand.cs │ │ │ │ ├── JoinRoomCommand.cs │ │ │ │ └── PaintActionCommand.cs -│ │ │ ├── Queries/ # 查询模式 +│ │ │ ├── Queries/ # 查询定义 │ │ │ │ ├── GetRoomQuery.cs │ │ │ │ └── GetLeaderboardQuery.cs -│ │ │ ├── Handlers/ # CQRS处理器 +│ │ │ ├── Handlers/ # 命令和查询处理器 │ │ │ │ ├── CommandHandlers/ │ │ │ │ └── QueryHandlers/ │ │ │ └── Services/ # 应用服务 -│ │ │ └── GameOrchestrator.cs # 游戏编排服务 +│ │ │ └── GameOrchestrator.cs +│ │ ├── TerritoryGame.Domain/ # 领域层 +│ │ │ ├── Entities/ # 聚合根和实体 +│ │ │ │ ├── Game.cs +│ │ │ │ ├── Player.cs +│ │ │ │ └── GameRoom.cs +│ │ │ ├── Events/ # 领域事件 +│ │ │ │ ├── GameStartedEvent.cs +│ │ │ │ ├── AreaCapturedEvent.cs +│ │ │ │ └── GameEndedEvent.cs +│ │ │ ├── Repositories/ # 仓储接口 +│ │ │ │ ├── IGameRepository.cs +│ │ │ │ └── IPlayerRepository.cs +│ │ │ ├── Services/ # 领域服务 +│ │ │ │ └── IGameLogicService.cs +│ │ │ └── ValueObjects/ # 值对象 +│ │ │ ├── Position.cs +│ │ │ ├── Color.cs +│ │ │ └── GameScore.cs │ │ ├── TerritoryGame.Infrastructure/ # 基础设施层 -│ │ │ ├── Persistence/ # 数据持久化 +│ │ │ ├── Persistence/ # 数据持久化 (EF Core) │ │ │ │ ├── GameContext.cs # EF Core上下文 │ │ │ │ └── Repositories/ # 仓储实现 │ │ │ │ ├── GameRepository.cs │ │ │ │ └── PlayerRepository.cs -│ │ │ ├── Redis/ # 缓存服务 +│ │ │ ├── Caching/ # 缓存 (Redis) │ │ │ │ └── GameCacheService.cs -│ │ │ └── Messaging/ # 事件总线 +│ │ │ └── Messaging/ # 消息/事件总线 │ │ │ └── EventBus.cs -│ │ │ ├── Entities/ # 实体类 -│ │ │ │ ├── GameRoom.cs -│ │ │ │ ├── Player.cs -│ │ │ │ ├── GameSession.cs -│ │ │ │ └── PaintAction.cs -│ │ │ └── Interfaces/ -│ │ ├── TerritoryGame.Infrastructure/ -│ │ │ ├── Data/ -│ │ │ │ └── GameDbContext.cs -│ │ │ └── Repositories/ -│ │ └── TerritoryGame.Application/ -│ │ ├── DTOs/ -│ │ ├── Services/ -│ │ │ ├── GameService.cs -│ │ │ └── AreaCalculationService.cs -│ │ └── Hubs/ │ └── TerritoryGame.sln ├── paint-demo.html # 游戏演示页面 ├── docker-compose.yml # Docker编排文件 diff --git a/backend/src/TerritoryGame.Application/Commands/CreatePlayerCommand.cs b/backend/src/TerritoryGame.Application/Commands/CreatePlayerCommand.cs new file mode 100644 index 0000000000000000000000000000000000000000..e6d1801927764267a1fe00cc8d7a7f8dd8a09826 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Commands/CreatePlayerCommand.cs @@ -0,0 +1,8 @@ +namespace TerritoryGame.Application.Commands; + +/// +/// 创建新玩家的命令 +/// +/// 玩家昵称 +/// 玩家头像的URL(可选) +public record CreatePlayerCommand(string NickName, string? Avatar) : IRequest; \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Commands/CreateRoomCommand.cs b/backend/src/TerritoryGame.Application/Commands/CreateRoomCommand.cs new file mode 100644 index 0000000000000000000000000000000000000000..34d22aaf2bb26cd48302ae5bb90312079d7061bf --- /dev/null +++ b/backend/src/TerritoryGame.Application/Commands/CreateRoomCommand.cs @@ -0,0 +1,10 @@ +using MediatR; +using TerritoryGame.Application.DTOs; + +namespace TerritoryGame.Application.Commands; + +/// +/// 创建新游戏房间的命令 +/// +/// 房间名称 +public record CreateRoomCommand(string Name) : IRequest; \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Commands/JoinRoomCommand.cs b/backend/src/TerritoryGame.Application/Commands/JoinRoomCommand.cs new file mode 100644 index 0000000000000000000000000000000000000000..a46235245040b19f78767cd48be3f8effcb0401f --- /dev/null +++ b/backend/src/TerritoryGame.Application/Commands/JoinRoomCommand.cs @@ -0,0 +1,11 @@ +using MediatR; +using TerritoryGame.Application.DTOs; + +namespace TerritoryGame.Application.Commands; + +/// +/// 玩家加入游戏房间的命令 +/// +/// 房间ID +/// 玩家ID +public record JoinRoomCommand(Guid RoomId, Guid PlayerId) : IRequest; \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Commands/PaintActionCommand.cs b/backend/src/TerritoryGame.Application/Commands/PaintActionCommand.cs new file mode 100644 index 0000000000000000000000000000000000000000..9991424ac22049f6d2319bd14829c1ff0c1fcaaf --- /dev/null +++ b/backend/src/TerritoryGame.Application/Commands/PaintActionCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace TerritoryGame.Application.Commands; + +/// +/// 玩家执行涂色动作的命令 +/// +/// 房间ID +/// 玩家ID +/// X坐标 +/// Y坐标 +/// 笔刷大小 +public record PaintActionCommand(Guid RoomId, Guid PlayerId, int X, int Y, int BrushSize) : IRequest; \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/DTOs/GameRoomDto.cs b/backend/src/TerritoryGame.Application/DTOs/GameRoomDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..631029cc700ead17c8a68a0476159484ebea9535 --- /dev/null +++ b/backend/src/TerritoryGame.Application/DTOs/GameRoomDto.cs @@ -0,0 +1,27 @@ +namespace TerritoryGame.Application.DTOs; + +/// +/// 游戏房间信息的数据传输对象 +/// +public class GameRoomDto +{ + /// + /// 房间ID + /// + public Guid Id { get; set; } + + /// + /// 房间名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 房间内的玩家列表 + /// + public List Players { get; set; } = new(); + + /// + /// 游戏状态 + /// + public string Status { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/DTOs/LeaderboardDto.cs b/backend/src/TerritoryGame.Application/DTOs/LeaderboardDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..86df4808eef35b18248ab8ba427a4b04acccd688 --- /dev/null +++ b/backend/src/TerritoryGame.Application/DTOs/LeaderboardDto.cs @@ -0,0 +1,12 @@ +namespace TerritoryGame.Application.DTOs; + +/// +/// 排行榜信息的数据传输对象 +/// +public class LeaderboardDto +{ + /// + /// 排行榜条目列表 + /// + public List Items { get; set; } = new(); +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/DTOs/LeaderboardItemDto.cs b/backend/src/TerritoryGame.Application/DTOs/LeaderboardItemDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..e3b98ff2eba1e964c5f8c9d3231e178f53f8cf99 --- /dev/null +++ b/backend/src/TerritoryGame.Application/DTOs/LeaderboardItemDto.cs @@ -0,0 +1,22 @@ +namespace TerritoryGame.Application.DTOs; + +/// +/// 排行榜条目的数据传输对象 +/// +public class LeaderboardItemDto +{ + /// + /// 玩家ID + /// + public Guid PlayerId { get; set; } + + /// + /// 玩家昵称 + /// + public string PlayerName { get; set; } = string.Empty; + + /// + /// 分数 + /// + public int Score { get; set; } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/DTOs/PlayerDto.cs b/backend/src/TerritoryGame.Application/DTOs/PlayerDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..59a0114c48d020d64c3004ac37fd5308173a7326 --- /dev/null +++ b/backend/src/TerritoryGame.Application/DTOs/PlayerDto.cs @@ -0,0 +1,27 @@ +namespace TerritoryGame.Application.DTOs; + +/// +/// 玩家信息的数据传输对象 +/// +public class PlayerDto +{ + /// + /// 玩家ID + /// + public Guid Id { get; set; } + + /// + /// 玩家昵称 + /// + public string NickName { get; set; } = string.Empty; + + /// + /// 玩家颜色 + /// + public string Color { get; set; } = string.Empty; + + /// + /// 玩家头像 + /// + public string Avatar { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/CreatePlayerCommandHandler.cs b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/CreatePlayerCommandHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..ed02b99139b5e67c3711cec6511cc2b3093c37ae --- /dev/null +++ b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/CreatePlayerCommandHandler.cs @@ -0,0 +1,35 @@ +using MediatR; +using TerritoryGame.Application.Commands; +using TerritoryGame.Application.DTOs; +using TerritoryGame.Domain.Entities; +using TerritoryGame.Domain.Repositories; + +namespace TerritoryGame.Application.Handlers.CommandHandlers; + +public class CreatePlayerCommandHandler : IRequestHandler +{ + private readonly IPlayerRepository _playerRepository; + + public CreatePlayerCommandHandler(IPlayerRepository playerRepository) + { + _playerRepository = playerRepository; + } + + public async Task Handle(CreatePlayerCommand request, CancellationToken cancellationToken) + { + // 使用充血模型的构造函数创建实体 + var player = new Player(request.NickName, request.Avatar); + + await _playerRepository.AddAsync(player, cancellationToken); + + // 返回一个DTO,而不是领域实体 + return new PlayerDto + { + Id = player.Id, + NickName = player.NickName, + Avatar = player.Avatar, + TotalGames = player.TotalGames, + WinCount = player.WinCount + }; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/CreateRoomCommandHandler.cs b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/CreateRoomCommandHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce9bab19145fee9b83eeab24a93a077dd70e2bb0 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/CreateRoomCommandHandler.cs @@ -0,0 +1,32 @@ +using MediatR; +using TerritoryGame.Application.Commands; +using TerritoryGame.Application.DTOs; +using TerritoryGame.Domain.Entities; +using TerritoryGame.Domain.Repositories; + +namespace TerritoryGame.Application.Handlers.CommandHandlers; + +public class CreateRoomCommandHandler : IRequestHandler +{ + private readonly IGameRoomRepository _gameRoomRepository; + + public CreateRoomCommandHandler(IGameRoomRepository gameRoomRepository) + { + _gameRoomRepository = gameRoomRepository; + } + + public async Task Handle(CreateRoomCommand request, CancellationToken cancellationToken) + { + var gameRoom = new GameRoom(request.Name); + + await _gameRoomRepository.AddAsync(gameRoom, cancellationToken); + + return new GameRoomDto + { + Id = gameRoom.Id, + Name = gameRoom.Name, + Status = gameRoom.Status.ToString(), + Players = new List() // Initially empty + }; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/JoinRoomCommandHandler.cs b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/JoinRoomCommandHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..a3ee0aead7751811361df91ed763a51c318aa664 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/JoinRoomCommandHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using TerritoryGame.Application.Commands; +using TerritoryGame.Application.DTOs; +using TerritoryGame.Domain.Repositories; + +namespace TerritoryGame.Application.Handlers.CommandHandlers; + +public class JoinRoomCommandHandler : IRequestHandler +{ + private readonly IGameRoomRepository _gameRoomRepository; + private readonly IPlayerRepository _playerRepository; + + public JoinRoomCommandHandler(IGameRoomRepository gameRoomRepository, IPlayerRepository playerRepository) + { + _gameRoomRepository = gameRoomRepository; + _playerRepository = playerRepository; + } + + public async Task Handle(JoinRoomCommand request, CancellationToken cancellationToken) + { + var gameRoom = await _gameRoomRepository.GetByIdAsync(request.RoomId, cancellationToken); + if (gameRoom == null) + { + // Or throw a custom exception + throw new Exception("Game room not found."); + } + + var player = await _playerRepository.GetByIdAsync(request.PlayerId, cancellationToken); + if (player == null) + { + // Or throw a custom exception + throw new Exception("Player not found."); + } + + gameRoom.AddPlayer(player); + + await _gameRoomRepository.UpdateAsync(gameRoom, cancellationToken); + + return new GameRoomDto + { + Id = gameRoom.Id, + Name = gameRoom.Name, + Status = gameRoom.Status.ToString(), + Players = gameRoom.Players.Select(p => new PlayerDto { Id = p.Id, NickName = p.NickName, Avatar = p.Avatar }).ToList() + }; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/PaintActionCommandHandler.cs b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/PaintActionCommandHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..1d2da52412a0e89faff84cbc2e0eeec8273071f9 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Handlers/CommandHandlers/PaintActionCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using TerritoryGame.Application.Commands; +using TerritoryGame.Domain.Repositories; +using TerritoryGame.Domain.Services; + +namespace TerritoryGame.Application.Handlers.CommandHandlers; + +public class PaintActionCommandHandler : IRequestHandler +{ + private readonly IGameRoomRepository _gameRoomRepository; + private readonly IPlayerRepository _playerRepository; + private readonly IGameLogicService _gameLogicService; + + public PaintActionCommandHandler( + IGameRoomRepository gameRoomRepository, + IPlayerRepository playerRepository, + IGameLogicService gameLogicService) + { + _gameRoomRepository = gameRoomRepository; + _playerRepository = playerRepository; + _gameLogicService = gameLogicService; + } + + public async Task Handle(PaintActionCommand request, CancellationToken cancellationToken) + { + var gameRoom = await _gameRoomRepository.GetByIdAsync(request.RoomId, cancellationToken); + if (gameRoom == null) + { + throw new Exception("Game room not found."); + } + + var player = await _playerRepository.GetByIdAsync(request.PlayerId, cancellationToken); + if (player == null) + { + throw new Exception("Player not found."); + } + + _gameLogicService.HandlePaintAction(gameRoom, player, request.X, request.Y, request.BrushSize); + + await _gameRoomRepository.UpdateAsync(gameRoom, cancellationToken); + + return Unit.Value; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetLeaderboardQueryHandler.cs b/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetLeaderboardQueryHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..7aac7c77a7210f035bd66bfabb65262e82d486b4 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetLeaderboardQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using TerritoryGame.Application.DTOs; +using TerritoryGame.Application.Queries; +using TerritoryGame.Domain.Repositories; + +namespace TerritoryGame.Application.Handlers.QueryHandlers; + +public class GetLeaderboardQueryHandler : IRequestHandler +{ + private readonly IPlayerRepository _playerRepository; + + public GetLeaderboardQueryHandler(IPlayerRepository playerRepository) + { + _playerRepository = playerRepository; + } + + public async Task Handle(GetLeaderboardQuery request, CancellationToken cancellationToken) + { + // This is a simplified implementation. A real-world leaderboard might involve more complex logic + // and potentially a separate, optimized data store for leaderboard data. + var players = await _playerRepository.GetAllAsync(cancellationToken); + + var leaderboardItems = players + .OrderByDescending(p => p.WinCount) // Or any other metric + .Select(p => new LeaderboardItemDto + { + PlayerId = p.Id, + PlayerName = p.NickName, + Score = p.WinCount // Or a calculated score + }) + .ToList(); + + return new LeaderboardDto + { + Items = leaderboardItems + }; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetPlayerQueryHandler.cs b/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetPlayerQueryHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..611b68dc0b4f77ffc8d2ad5609bb7a4870007783 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetPlayerQueryHandler.cs @@ -0,0 +1,35 @@ +using MediatR; +using TerritoryGame.Application.DTOs; +using TerritoryGame.Application.Queries; +using TerritoryGame.Domain.Repositories; + +namespace TerritoryGame.Application.Handlers.QueryHandlers; + +public class GetPlayerQueryHandler : IRequestHandler +{ + private readonly IPlayerRepository _playerRepository; + + public GetPlayerQueryHandler(IPlayerRepository playerRepository) + { + _playerRepository = playerRepository; + } + + public async Task Handle(GetPlayerQuery request, CancellationToken cancellationToken) + { + var player = await _playerRepository.GetByIdAsync(request.Id, cancellationToken); + + if (player == null) + { + return null; + } + + return new PlayerDto + { + Id = player.Id, + NickName = player.NickName, + Avatar = player.Avatar, + TotalGames = player.TotalGames, + WinCount = player.WinCount + }; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetRoomQueryHandler.cs b/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetRoomQueryHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..8d33a4238b789381a9132109f084f0bb79546ced --- /dev/null +++ b/backend/src/TerritoryGame.Application/Handlers/QueryHandlers/GetRoomQueryHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using TerritoryGame.Application.DTOs; +using TerritoryGame.Application.Queries; +using TerritoryGame.Domain.Repositories; + +namespace TerritoryGame.Application.Handlers.QueryHandlers; + +public class GetRoomQueryHandler : IRequestHandler +{ + private readonly IGameRoomRepository _gameRoomRepository; + + public GetRoomQueryHandler(IGameRoomRepository gameRoomRepository) + { + _gameRoomRepository = gameRoomRepository; + } + + public async Task Handle(GetRoomQuery request, CancellationToken cancellationToken) + { + var gameRoom = await _gameRoomRepository.GetByIdAsync(request.RoomId, cancellationToken); + + if (gameRoom == null) + { + return null; + } + + return new GameRoomDto + { + Id = gameRoom.Id, + Name = gameRoom.Name, + Status = gameRoom.Status.ToString(), + Players = gameRoom.Players.Select(p => new PlayerDto { Id = p.Id, NickName = p.NickName, Avatar = p.Avatar }).ToList() + }; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Queries/GetLeaderboardQuery.cs b/backend/src/TerritoryGame.Application/Queries/GetLeaderboardQuery.cs new file mode 100644 index 0000000000000000000000000000000000000000..d13edc8598c9fa9e3fd333f3f3cb6d6963630ced --- /dev/null +++ b/backend/src/TerritoryGame.Application/Queries/GetLeaderboardQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using TerritoryGame.Application.DTOs; + +namespace TerritoryGame.Application.Queries; + +/// +/// 获取排行榜信息的查询 +/// +public record GetLeaderboardQuery : IRequest; \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Queries/GetPlayerQuery.cs b/backend/src/TerritoryGame.Application/Queries/GetPlayerQuery.cs new file mode 100644 index 0000000000000000000000000000000000000000..e9b36097fbdfa2e3b0f55cb5e4c520e4aa629fbe --- /dev/null +++ b/backend/src/TerritoryGame.Application/Queries/GetPlayerQuery.cs @@ -0,0 +1,10 @@ +using MediatR; +using TerritoryGame.Application.DTOs; + +namespace TerritoryGame.Application.Queries; + +/// +/// 获取单个玩家信息的查询 +/// +/// 玩家ID +public record GetPlayerQuery(Guid Id) : IRequest; \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Queries/GetRoomQuery.cs b/backend/src/TerritoryGame.Application/Queries/GetRoomQuery.cs new file mode 100644 index 0000000000000000000000000000000000000000..3dd0e424302589afd788a4073c8ffc21a42dc0dc --- /dev/null +++ b/backend/src/TerritoryGame.Application/Queries/GetRoomQuery.cs @@ -0,0 +1,10 @@ +using MediatR; +using TerritoryGame.Application.DTOs; + +namespace TerritoryGame.Application.Queries; + +/// +/// 获取单个游戏房间信息的查询 +/// +/// 房间ID +public record GetRoomQuery(Guid RoomId) : IRequest; \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Entities/GameRoom.cs b/backend/src/TerritoryGame.Domain/Entities/GameRoom.cs new file mode 100644 index 0000000000000000000000000000000000000000..9cb2c3811f8b3e3495156815761ab0d1420790ac --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Entities/GameRoom.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using TerritoryGame.Domain.Enums; + +namespace TerritoryGame.Domain.Entities; + +/// +/// 游戏房间 +/// +public class GameRoom +{ + /// + /// 房间ID + /// + public Guid Id { get; private set; } + + /// + /// 房间名称 + /// + public string Name { get; private set; } + + /// + /// 游戏状态 + /// + public GameStatus Status { get; private set; } + + /// + /// 房间内的玩家列表 + /// + private readonly List _players = new(); + public IReadOnlyCollection Players => _players.AsReadOnly(); + + /// + /// 游戏会话列表 + /// + public ICollection GameSessions { get; private set; } = new List(); + + /// + /// 涂色动作列表 + /// + public ICollection PaintActions { get; private set; } = new List(); + + // EF Core 构造函数 + private GameRoom() { } + + public GameRoom(string name) + { + Id = Guid.NewGuid(); + Name = name; + Status = GameStatus.Waiting; + } + + /// + /// 添加玩家到房间 + /// + /// 要添加的玩家 + public void AddPlayer(Player player) + { + if (Status != GameStatus.Waiting) + throw new InvalidOperationException("游戏已经开始或结束,不能加入玩家。"); + + if (_players.Count >= 6) // 假设最大玩家数为6 + throw new InvalidOperationException("房间已满。"); + + if (_players.Any(p => p.Id == player.Id)) + throw new InvalidOperationException("玩家已在房间内。"); + + _players.Add(player); + } + + /// + /// 开始游戏 + /// + public void StartGame() + { + if (Status != GameStatus.Waiting) + throw new InvalidOperationException("游戏已经开始或结束。"); + + if (_players.Count < 2) // 假设最少需要2个玩家才能开始 + throw new InvalidOperationException("玩家人数不足,无法开始游戏。"); + + Status = GameStatus.InProgress; + // 在这里可以发布一个领域事件,例如 GameStartedEvent + } + + /// + /// 结束游戏 + /// + public void EndGame() + { + if (Status != GameStatus.InProgress) + throw new InvalidOperationException("游戏尚未开始,不能结束。"); + + Status = GameStatus.Finished; + // 在这里可以发布一个领域事件,例如 GameEndedEvent + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Entities/GameSession.cs b/backend/src/TerritoryGame.Domain/Entities/GameSession.cs new file mode 100644 index 0000000000000000000000000000000000000000..e751a680274c7bc2ff27d439238bb3be64f9a67b --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Entities/GameSession.cs @@ -0,0 +1,54 @@ +using System; + +namespace TerritoryGame.Domain.Entities; + +/// +/// 表示玩家的游戏会话。 +/// +public class GameSession +{ + /// + /// 游戏会话的唯一标识符。 + /// + public Guid Id { get; set; } + + /// + /// 游戏房间的ID。 + /// + public Guid RoomId { get; set; } + + /// + /// 玩家的ID。 + /// + public Guid PlayerId { get; set; } + + /// + /// 玩家在游戏中的颜色。 + /// + public string PlayerColor { get; set; } = string.Empty; + + /// + /// 玩家最终占领的区域面积。 + /// + public int FinalArea { get; set; } + + /// + /// 玩家的排名。 + /// + public int? Rank { get; set; } + + /// + /// 玩家加入游戏的时间戳。 + /// + public DateTime JoinedAt { get; set; } + + /// + /// 导航属性,指向游戏房间。 + /// + public GameRoom? GameRoom { get; set; } + + /// + /// 导航属性,指向玩家。 + /// + public Player? Player { get; set; } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Entities/PaintAction.cs b/backend/src/TerritoryGame.Domain/Entities/PaintAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..31f484152b4626d92c40f31d89648f16ef83c5ec --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Entities/PaintAction.cs @@ -0,0 +1,54 @@ +using System; + +namespace TerritoryGame.Domain.Entities; + +/// +/// 表示玩家的涂色动作。 +/// +public class PaintAction +{ + /// + /// 涂色动作的唯一标识符。 + /// + public Guid Id { get; set; } + + /// + /// 游戏房间的ID。 + /// + public Guid RoomId { get; set; } + + /// + /// 玩家的ID。 + /// + public Guid PlayerId { get; set; } + + /// + /// 涂色点的X坐标。 + /// + public int X { get; set; } + + /// + /// 涂色点的Y坐标。 + /// + public int Y { get; set; } + + /// + /// 笔刷的大小。 + /// + public int BrushSize { get; set; } + + /// + /// 动作发生时的时间戳。 + /// + public DateTime Timestamp { get; set; } + + /// + /// 导航属性,指向游戏房间。 + /// + public GameRoom? GameRoom { get; set; } + + /// + /// 导航属性,指向玩家。 + /// + public Player? Player { get; set; } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Entities/Player.cs b/backend/src/TerritoryGame.Domain/Entities/Player.cs new file mode 100644 index 0000000000000000000000000000000000000000..1e12d2fd48a63099a24c28aba4fe2e059ca097f0 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Entities/Player.cs @@ -0,0 +1,87 @@ +namespace TerritoryGame.Domain.Entities; + +/// +/// 玩家实体 +/// +public class Player +{ + /// + /// 玩家ID + /// + public Guid Id { get; private set; } + + /// + /// 玩家昵称 + /// + public string NickName { get; private set; } + + /// + /// 玩家头像的URL + /// + public string? Avatar { get; private set; } + + /// + /// 玩家进行的游戏总数 + /// + public int TotalGames { get; private set; } + + /// + /// 玩家获胜的游戏数 + /// + public int WinCount { get; private set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; private set; } + + /// + /// 游戏会话列表 + /// + public ICollection GameSessions { get; private set; } = new List(); + + /// + /// 涂色动作列表 + /// + public ICollection PaintActions { get; private set; } = new List(); + + // EF Core 构造函数 + private Player() { } + + public Player(string nickName, string? avatar = null) + { + Id = Guid.NewGuid(); + NickName = nickName; + Avatar = avatar; + TotalGames = 0; + WinCount = 0; + CreatedAt = DateTime.UtcNow; + } + + /// + /// 更新玩家资料 + /// + /// 新昵称 + /// 新头像 + public void UpdateProfile(string newNickName, string? newAvatar) + { + if (string.IsNullOrWhiteSpace(newNickName)) + throw new ArgumentException("昵称不能为空。", nameof(newNickName)); + + NickName = newNickName; + Avatar = newAvatar; + } + + /// + /// 记录一次游戏结果 + /// + /// 是否获胜 + public void RecordGamePlayed(bool hasWon) + { + TotalGames++; + if (hasWon) + { + WinCount++; + } + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Enums/GameStatus.cs b/backend/src/TerritoryGame.Domain/Enums/GameStatus.cs new file mode 100644 index 0000000000000000000000000000000000000000..9d759f3f05050fe47258ffde19ca5907dc7570cf --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Enums/GameStatus.cs @@ -0,0 +1,22 @@ +namespace TerritoryGame.Domain.Enums; + +/// +/// 游戏状态 +/// +public enum GameStatus +{ + /// + /// 等待中 + /// + Waiting, + + /// + /// 进行中 + /// + InProgress, + + /// + /// 已结束 + /// + Finished +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Repositories/IPlayerRepository.cs b/backend/src/TerritoryGame.Domain/Repositories/IPlayerRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..d6e47a5f2762685d9abf5b43a19d3d6c54908980 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Repositories/IPlayerRepository.cs @@ -0,0 +1,11 @@ +using TerritoryGame.Domain.Entities; + +namespace TerritoryGame.Domain.Repositories; + +public interface IPlayerRepository +{ + Task GetByIdAsync(Guid id); + Task AddAsync(Player player); + Task UpdateAsync(Player player); + Task DeleteAsync(Player player); +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Services/GameLogicService.cs b/backend/src/TerritoryGame.Domain/Services/GameLogicService.cs new file mode 100644 index 0000000000000000000000000000000000000000..71c305a93fb0480b4e0b56be7633fd257212eb65 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Services/GameLogicService.cs @@ -0,0 +1,46 @@ +using TerritoryGame.Domain.Entities; + +namespace TerritoryGame.Domain.Services; + +/// +/// 游戏核心逻辑的服务 +/// +public class GameLogicService : IGameLogicService +{ + /// + /// 处理玩家的涂色动作 + /// + /// 当前游戏房间 + /// 执行动作的玩家 + /// X坐标 + /// Y坐标 + /// 笔刷大小 + public void HandlePaintAction(GameRoom gameRoom, Player player, int x, int y, int brushSize) + { + // 1. 验证游戏是否正在进行中 + if (gameRoom.Status != Enums.GameStatus.InProgress) + { + // 可以抛出异常或直接返回 + return; + } + + // 2. 验证玩家是否在房间内 + if (!gameRoom.Players.Any(p => p.Id == player.Id)) + { + // 玩家不在房间内,非法操作 + return; + } + + // 3. 创建并记录涂色动作(此处只是示例,具体实现可能需要更复杂的逻辑) + var paintAction = new PaintAction(player.Id, gameRoom.Id, x, y, brushSize, DateTime.UtcNow); + gameRoom.PaintActions.Add(paintAction); + + // 4. TODO: 在此实现更复杂的逻辑,例如: + // - 检查涂色区域是否合法(例如,是否在画布边界内) + // - 判断是否覆盖了其他玩家的颜色 + // - 计算新占领的面积 + // - 发布领域事件(如 AreaPaintedEvent),通知其他部分(如应用层)更新状态或通知客户端 + + Console.WriteLine($"Player {player.NickName} painted at ({x}, {y}) in room {gameRoom.Name}"); + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Services/IGameLogicService.cs b/backend/src/TerritoryGame.Domain/Services/IGameLogicService.cs new file mode 100644 index 0000000000000000000000000000000000000000..7cac3ad83aecbaebb9c3fc01fad5c475c1feb3e3 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Services/IGameLogicService.cs @@ -0,0 +1,19 @@ +using TerritoryGame.Domain.Entities; + +namespace TerritoryGame.Domain.Services; + +/// +/// 定义游戏核心逻辑的服务接口 +/// +public interface IGameLogicService +{ + /// + /// 处理玩家的涂色动作 + /// + /// 当前游戏房间 + /// 执行动作的玩家 + /// X坐标 + /// Y坐标 + /// 笔刷大小 + void HandlePaintAction(GameRoom gameRoom, Player player, int x, int y, int brushSize); +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Infrastructure/Persistence/Repositories/PlayerRepository.cs b/backend/src/TerritoryGame.Infrastructure/Persistence/Repositories/PlayerRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..d61bbd19c89c7ae6bf58fc0285a64d3690e2dccd --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Persistence/Repositories/PlayerRepository.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Domain.Entities; +using TerritoryGame.Domain.Repositories; +using TerritoryGame.Infrastructure.Persistence; + +namespace TerritoryGame.Infrastructure.Persistence.Repositories; + +public class PlayerRepository : IPlayerRepository +{ + private readonly TerritoryGameDbContext _context; + + public PlayerRepository(TerritoryGameDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid id) + { + return await _context.Players.FindAsync(id); + } + + public async Task AddAsync(Player player) + { + await _context.Players.AddAsync(player); + await _context.SaveChangesAsync(); + } + + public async Task UpdateAsync(Player player) + { + _context.Players.Update(player); + await _context.SaveChangesAsync(); + } + + public async Task DeleteAsync(Player player) + { + _context.Players.Remove(player); + await _context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Infrastructure/Persistence/TerritoryGameDbContext.cs b/backend/src/TerritoryGame.Infrastructure/Persistence/TerritoryGameDbContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf3981effa96e2b91c42bb26b7077fc5d5d49f98 --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Persistence/TerritoryGameDbContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Domain.Entities; + +namespace TerritoryGame.Infrastructure.Persistence; + +public class TerritoryGameDbContext : DbContext +{ + public TerritoryGameDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Players { get; set; } + public DbSet GameRooms { get; set; } + public DbSet GameSessions { get; set; } + public DbSet PaintActions { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasOne(gs => gs.GameRoom) + .WithMany(gr => gr.GameSessions) + .HasForeignKey(gs => gs.RoomId); + + modelBuilder.Entity() + .HasOne(gs => gs.Player) + .WithMany(p => p.GameSessions) + .HasForeignKey(gs => gs.PlayerId); + + modelBuilder.Entity() + .HasOne(pa => pa.GameRoom) + .WithMany(gr => gr.PaintActions) + .HasForeignKey(pa => pa.RoomId); + + modelBuilder.Entity() + .HasOne(pa => pa.Player) + .WithMany(p => p.PaintActions) + .HasForeignKey(pa => pa.PlayerId); + } +} \ No newline at end of file diff --git a/docs/BACKEND_DEVELOPMENT_PROCESS.md b/docs/BACKEND_DEVELOPMENT_PROCESS.md new file mode 100644 index 0000000000000000000000000000000000000000..8ccbe7e78cbd9f81b791db0603e626204717965a --- /dev/null +++ b/docs/BACKEND_DEVELOPMENT_PROCESS.md @@ -0,0 +1,207 @@ +# 后端开发全流程指南 + +本文档旨在为后端开发团队提供一个标准化的、端到端的开发流程,涵盖从需求分析到最终部署和长期维护的每一个环节。遵循此流程有助于提高开发效率、保证代码质量、降低沟通成本,并确保项目成功交付。 + +## 目录 +1. [阶段一:需求分析 (Requirements Analysis)](#阶段一需求分析-requirements-analysis) +2. [阶段二:系统设计 (System Design)](#阶段二系统设计-system-design) +3. [阶段三:开发实现 (Development)](#阶段三开发实现-development) +4. [阶段四:测试验证 (Testing)](#阶段四测试验证-testing) +5. [阶段五:部署发布 (Deployment)](#阶段五部署发布-deployment) +6. [阶段六:运维与监控 (Operations & Monitoring)](#阶段六运维与监控-operations--monitoring) +7. [阶段七:维护与迭代 (Maintenance & Iteration)](#阶段七维护与迭代-maintenance--iteration) + +--- + +## 阶段一:需求分析 (Requirements Analysis) + +此阶段的目标是清晰、完整地理解要做什么,并确保所有项目相关方对需求有统一的认识。 + +- **关键任务**: + - 与产品经理(PM)、项目经理和客户进行深入沟通,理解业务目标和用户场景。 + - 明确功能性需求(如:用户注册、商品下单、数据查询等)。 + - 明确非功能性需求(如:性能指标、安全标准、可扩展性、数据一致性要求等)。 + - 参与需求评审会议,从技术角度评估需求的可行性、复杂度和潜在风险。 + +- **参与人员**: + - `主要`:后端开发负责人、产品经理 + - `次要`:架构师、测试工程师、项目经理 + +- **交付物**: + - **需求规格说明书 (SRS)**:由产品经理主导,技术团队评审确认。 + - **用户故事 (User Stories)** 和 **用例 (Use Cases)**:清晰描述每个功能点。 + - **技术可行性分析报告**:针对复杂或不明确的需求。 + +- **最佳实践**: + - **刨根问底**:不要只满足于表面需求,要理解其背后的商业逻辑。 + - **量化指标**:尽可能量化非功能性需求,例如“接口响应时间应在200ms以内”、“系统需支持1000 QPS的并发量”。 + - **早期介入**:开发人员应尽早参与需求讨论,以便提前识别技术挑战。 + - **文档化**:所有需求和变更都应记录在案,避免口头约定。 + +--- + +## 阶段二:系统设计 (System Design) + +此阶段将需求转化为详细的技术方案和蓝图,是后续开发工作的指导方针。 + +- **关键任务**: + - **架构设计**:根据需求选择合适的架构模式(如:微服务、单体、Serverless),划分服务模块。 + - **技术选型**:选择合适的编程语言、框架、中间件(如:数据库、缓存、消息队列)。 + - **数据库设计**:设计数据库表结构(ER图)、索引和数据字典。 + - **API 设计**:设计清晰、一致的 API 接口(如:RESTful、GraphQL),并撰写接口文档。 + - **安全设计**:规划认证授权方案(如:OAuth2, JWT)、数据加密、防范常见攻击(如:SQL注入, XSS)。 + +- **参与人员**: + - `主要`:架构师、高级后端工程师 + - `次要`:后端开发团队、DBA、安全专家 + +- **交付物**: + - **架构设计文档**:包含系统架构图、模块划分、核心流程图。 + - **数据库设计文档**:包含 ER 图、表结构详情、数据字典。 + - **API 接口文档**:使用 OpenAPI/Swagger 等工具生成,包含详细的请求/响应格式、参数说明和错误码。 + - **技术选型报告**:说明选型原因和优缺点对比。 + +- **最佳实践**: + - **KISS, DRY, SOLID**:遵循高内聚、低耦合等基本设计原则。 + - **面向未来设计**:充分考虑系统的可扩展性、可维护性和弹性。 + - **评审机制**:组织设计评审会议,让团队成员充分讨论,凝聚共识。 + - **文档先行**:在编码前完成关键设计文档的编写。 + +--- + +## 阶段三:开发实现 (Development) + +此阶段是根据设计文档将技术方案转化为实际可运行的代码。 + +- **关键任务**: + - 搭建和配置本地开发环境。 + - 根据任务分配,实现业务逻辑、数据访问层和 API 接口。 + - 编写单元测试,确保代码模块的正确性。 + - 代码审查(Code Review),保证代码质量和风格统一。 + - 与前端、测试等团队保持密切沟通,及时解决联调和集成问题。 + - 配置并实践持续集成(CI),自动化构建和测试流程。 + +- **参与人员**: + - `主要`:后端开发工程师 + +- **交付物**: + - **源代码**:托管于版本控制系统(如 Git)。 + - **单元测试用例**及测试报告。 + - **Code Review 记录**。 + - **构建脚本**(如:Dockerfile, Maven/Gradle 配置)。 + +- **最佳实践**: + - **编码规范**:遵循团队统一的编码规范和代码风格。 + - **版本控制**:使用 Git Flow 或类似的分支管理策略,保持提交信息清晰明了。 + - **TDD/BDD**:鼓励测试驱动开发或行为驱动开发。 + - **小步快跑**:频繁提交、小批量集成,尽早发现和解决冲突。 + - **注释与文档**:对复杂逻辑或公共模块编写必要的注释和开发者文档。 + +--- + +## 阶段四:测试验证 (Testing) + +此阶段通过系统化的测试来保证软件质量,确保其符合需求并能稳定运行。 + +- **关键任务**: + - **集成测试**:测试不同模块或服务协同工作的正确性。 + - **API 功能测试**:使用 Postman、Insomnia 等工具对所有 API 接口进行黑盒测试。 + - **性能测试**:对核心接口进行压力测试和负载测试,评估系统性能瓶颈。 + - **安全测试**:进行漏洞扫描和渗透测试。 + - **回归测试**:在新版本发布前,确保原有功能未受影响。 + - 缺陷管理:记录、跟踪和验证 Bug 的修复。 + +- **参与人员**: + - `主要`:测试工程师(QA) + - `次要`:后端开发工程师、产品经理 + +- **交付物**: + - **测试计划**和**测试用例**。 + - **测试报告**:包含功能、性能、安全等维度的测试结果。 + - **缺陷报告列表**(Bug List)。 + +- **最佳实践**: + - **测试自动化**:将回归测试和核心功能的 API 测试自动化,提高效率。 + - **分层测试**:建立单元测试、集成测试、端到端测试的金字塔模型。 + - **隔离的测试环境**:搭建与生产环境尽可能一致的独立测试环境。 + - **有效的缺陷管理**:使用 Jira 等工具进行系统化的缺陷跟踪。 + +--- + +## 阶段五:部署发布 (Deployment) + +此阶段将经过测试验证的应用程序安全、平稳地发布到生产环境。 + +- **关键任务**: + - 准备生产环境,包括服务器、网络、数据库等基础设施。 + - 编写和测试部署脚本(如:Ansible, Terraform)。 + - 构建最终的部署产物(如:Docker 镜像)。 + - 执行部署流程,并进行部署后验证(Smoke Test)。 + - 制定回滚计划,以应对部署失败的情况。 + +- **参与人员**: + - `主要`:运维工程师(DevOps/Ops) + - `次要`:后端开发工程师 + +- **交付物**: + - **持续部署(CD)流水线**。 + - **部署操作手册**和**回滚方案**。 + - **版本发布说明(Release Notes)**。 + +- **最佳实践**: + - **IaC(基础设施即代码)**:使用代码来管理和配置基础设施,实现自动化和可重复性。 + - **蓝绿部署/金丝雀发布**:采用先进的发布策略,实现零停机或灰度发布,降低发布风险。 + - **自动化**:尽可能自动化整个部署流程,减少人为错误。 + - **配置中心**:将应用配置与代码分离,实现动态配置管理。 + +--- + +## 阶段六:运维与监控 (Operations & Monitoring) + +应用上线后,需要持续监控其运行状态,保障服务的稳定性和可用性。 + +- **关键任务**: + - **日志管理**:集中收集、存储和分析应用日志(ELK/EFK Stack)。 + - **性能监控**:监控系统关键指标(CPU、内存、磁盘、网络、JVM/CLR 等)。 + - **业务监控**:监控核心业务指标(如:订单量、用户注册数)。 + - **告警系统**:设置合理的告警阈值,在问题发生时通过短信、电话、邮件等方式及时通知相关人员。 + - **备份与恢复**:制定并定期演练数据备份和灾难恢复计划。 + +- **参与人员**: + - `主要`:运维工程师(DevOps/SRE) + - `次要`:后端开发工程师 + +- **交付物**: + - **监控仪表盘(Dashboard)**:如 Grafana, Prometheus。 + - **告警规则和通知策略**。 + - **应急预案(On-Call Playbook)**和**灾备演练报告**。 + +- **最佳实践**: + - **可观测性(Observability)**:建立涵盖日志(Logging)、指标(Metrics)和追踪(Tracing)的全面监控体系。 + - **定义SLO/SLA**:明确服务水平目标和服务水平协议,作为衡量服务质量的标准。 + - **自动化运维**:利用脚本和工具实现自动化故障发现和自愈。 + - **定期演练**:定期进行故障注入和灾备演练,确保预案的有效性。 + +--- + +## 阶段七:维护与迭代 (Maintenance & Iteration) + +此阶段是应用的长期生命周期管理,包括修复问题、优化系统和响应新的业务需求。 + +- **关键任务**: + - **问题修复**:快速响应和修复线上发现的 Bug。 + - **技术优化**:进行代码重构、性能优化、升级老旧依赖,偿还技术债务。 + - **需求迭代**:根据用户反馈和新的业务规划,进入新的开发循环(重复阶段一至六)。 + - **文档更新**:同步更新架构、设计、API 等文档,确保其与代码一致。 + +- **参与人员**: + - 所有项目相关方 + +- **交付物**: + - **软件补丁**和**新版本**。 + - **更新后的各类文档**。 + +- **最佳实践**: + - **敏捷迭代**:采用 Scrum 或 Kanban 等敏捷方法,快速响应变化。 + - **建立知识库**:沉淀问题解决方案和最佳实践,供团队共享。 + - **定期复盘**:定期召开项目复盘会议,总结经验教训,持续改进流程。 \ No newline at end of file