EVGA — Tài liệu kỹ thuật
Tài liệu kỹ thuật hệ thống

EVGA

Hệ thống Quản lý Golf
Phiên bản
1.0
Ngày lập
02 / 2026
Phân loại
Nội bộ
Biên soạn bởi Quoc Toan TranITViet.cz

Chương 01Tổng quan hệ thống

EVGA (European Vietnamese Golf Association) là nền tảng quản lý golf toàn diện, phục vụ hiệp hội golf Việt Nam tại Châu Âu. Hệ thống quản lý giải đấu, trận đấu, điểm chấp (handicap), câu lạc bộ, sân golfngười chơi.

Kiến trúc Clean Architecture với pattern CQRS qua MediatR. Backend ASP.NET Core triển khai hoàn toàn trên Azure.

Các project trong Solution

ProjectNền tảngVai tròPhụ thuộc
EVGA.Web.NET 7+API layer — Controllers, Auth, Middleware, SignalRASP.NET Core, Swagger, Hangfire, FluentValidation
EVGA.Core.NET 7+Business logic — DbContext, Repos, CQRS handlersEF Core 6+, MediatR, BulkExtensions, Azure.Storage
EVGA.Modelsnetstandard2.0DTOs, Enums, Validation models dùng chungSystem.Text.Json
EVGA.CommonLogicnetstandard2.0Bộ tính Handicap và ScoreChỉ tham chiếu Models
EVGA.Functions.NET 7Azure Functions — Queue triggers xử lý nềnAzure WebJobs, Firebase
Tests.NET 7Unit + Integration testsNUnit, FluentAssertions, Moq

Pattern CQRS

Toàn bộ business logic nằm trong Command/Query handlers. Controllers chỉ dispatch qua _mediator.Send().

LoạiQuy ướcVí dụTrả về
Query*Query / *QueryHandlerGetGameDetailQueryDữ liệu (chỉ đọc)
Command*Command / *CommandHandlerCreateGameCommandResult<T> hoặc Unit

Chương 02Hạ tầng Azure

Tổng cộng 51 resources trong subscription sub-evga, vùng chính West Europe. 3 môi trường độc lập.

Ba môi trường

EnvRGBackendAdminDBFunctionsStorage
DEVEVGA-devevga-devevga-admin-static-devevga-devevga-dev-functionsevgadevstorage
UATEVGA-uatevga-uatevga-admin-static-uatevga-uatevga-uat-functionsevgauatstorage
PRODEVGAevgaevga-admin-staticevga-prodevga-functionsevgasblobtorage

Thành phần

⚙️
Backend API
App Service — ASP.NET Core, JWT, Hangfire + SignalR cùng process
App Service
🖥️
Admin Web
React SPA. Đang migrate từ App Service → Static Web App
SWA
🗄️
CSDL
Azure SQL Server. Primary: West EU, Failover: North EU (geo-rep)
SQL
Functions
Azure Functions v3. Queue-triggered: stats, tournament games
Serverless
📨
Queues
Storage Queues: update-stats, notifications, tournament-games
Queue
🖼️
Blob Storage
Ảnh profile: container profile-images, {userId}.jpeg
Blob
🌐
API Gateway
APIM: api-evga (prod), evga-uat-api. DEV không có
APIM
🔀
Traffic Manager
evga-prod — DNS-level failover cho production
DNS LB
📊
Monitoring
App Insights + Log Analytics + Failure Anomalies mỗi env
Insights

Chương 03Sơ đồ kiến trúc

Dựa trên sơ đồ gốc từ developer Jakub bàn giao, kết hợp phân tích source code.

Luồng request chính

📱 Mobile App
🔀 Traffic Mgr
🌐 APIM
⚙️ App Service
🗄️ SQL Server
🖥️ Admin Panel
→ REST →
⚙️ App Service
🖼️ Blob Storage

Luồng xử lý nền

⚙️ Backend
📨 Queue
⚡ Functions
⚙️ API (system)
⚙️ Backend
🔔 Firebase
📱 Push

SQL Server topology

evga (West EU)
⇄ geo-rep ⇄
evga-failover (North EU)
evga-prod
evga-uat
evga-dev
dev replica
backup 2024-07
⚠️ Lưu ý: Sơ đồ gốc ghi "serverless lambdas / AWS lambdas" — thực tế là Azure Functions, không phải AWS.

Chương 04Cơ sở dữ liệu

EF Core 6+ Code-First migrations. DateTime tự động UTC qua UtcDateAnnotation.

Bảng chính

EntityTrường chínhQuan hệGhi chú
UserId (gán sẵn), Name, HandicapIndex, Gender, PasswordHash, FirebaseToken, AdminOfClubs, Friends, Stats, InitialScoresId không auto-increment
GameId, TimeLocal, GameStatus, Type, CourseId, TournamentId?PlayersScore[], CourseDraft → Saved → Sent → Confirmed
PlayerScoreId, UserId, HitsJson (JSON), tất cả loại điểmGame, UserGross, Net, Stb Netto/Brutto, ScoreDiff
CourseId, Name, HolesCount, GolfClubId, TimeZoneTees[], Holes[]TimeZone cho giờ local
TeeId, TeeColor, Gender, SlopeRating, CourseRatingCourseColor + Gender duy nhất/sân
TournamentId, Name, Status, StartTime, nhiều configFlights[], Players[], Categories[]Draft → Published → Started → Finished
TournamentFlightId, TimeLocal, Hole, MaxPlayersPlayers[], CourseNhóm tee time
TournamentPlayerId, UserId, FlightId?, TeeColorUser, Flight?, Score?Liên kết player ↔ tournament
ClubId, Name, Country, CountryParentId?Parent hierarchyEVGA → Quốc gia → CLB
MessageId, ForUserId, Content, GameId?ForUserThông báo in-app
YearlyStatisticsUserId + Year (composite key)Best games, coursesTính lại qua Functions

Migration commands

  • Tạo: dotnet ef migrations add {Tên} --startup-project EVGA.Web --project EVGA.Core
  • Chạy: dotnet ef database update --startup-project EVGA.Web --project EVGA.Core
  • Xuất SQL: dotnet ef migrations script {from} --startup-project EVGA.Web --project EVGA.Core
⛔ PROD: LUÔN xuất SQL script và review. Không dùng database update trực tiếp.

Chương 05API Endpoints

JWT Bearer required (trừ login). Swagger tại /swagger.

/api/users

MethodEndpointMô tả
POST/api/users/loginĐăng nhập → JWT token
GET/api/users/profileProfile bản thân
GET/api/users/handicapChi tiết handicap + score differentials
GET/api/users/statistics/{year?}Thống kê năm
POST/api/users/firebase/tokenĐăng ký push token
PUT/api/users/logoutĐăng xuất

/api/games

MethodEndpointMô tả
POST/api/games/dataTính dữ liệu game trước khi tạo
POST/api/games/Tạo + xác nhận game, tính lại handicap
PUT/api/games/{id}/scoreSửa điểm
GET/api/games/{id}Chi tiết game

/api/tournaments

MethodEndpointMô tả
GET/api/tournamentsDanh sách giải đấu
POST/api/tournaments/{id}/signupĐăng ký tham gia
POST/api/tournaments/{id}/publish-startlistCông bố start list (admin)
POST/api/tournaments/{id}/startBắt đầu giải (admin)
POST/api/tournaments/{id}/endKết thúc giải (admin)
POST/api/tournaments/{id}/generate-gamesTạo game từ kết quả

Chương 06Logic nghiệp vụ

Hệ thống Handicap

  • Score Differential = (AGS − Course Rating) × 113 / Slope Rating
  • Game Handicap = Player HCP × (Slope / 113) + (Course Rating − Par)
  • Player HCP = Trung bình N tốt nhất trong 20 game + hệ số
  • Chưa đủ 20 game → dùng InitialGameScore
  • Tính lại sau mỗi game qua update-stats-queue

Code: IHandicapCalculator (CommonLogic), HandicapService (Core)

Các loại điểm

LoạiCông thứcMục đích
GrossScoreTổng hitsTổng gốc
NetScoreGross − GameHandicapSau điều chỉnh HCP
AdjustedGrossHits cap par+2 mỗi hốĐầu vào tính HCP
StbNettoStableford nettoXếp hạng giải (netto)
StbBruttoStableford bruttoXếp hạng giải (brutto)
ScoreDifferential(AGS − CR) × 113 / SRTính Handicap Index

Vòng đời giải đấu

  1. Draft — Admin cấu hình categories, tees, flights
  2. Đăng ký — Users signup, admin có thể thêm
  3. Phân flight — Gán vào tee time
  4. Publish start list — Khóa danh sách
  5. Start — Nhập điểm
  6. End — Kiểm tra điểm, đẩy queue
  7. Generate games — Functions tạo Game records, tính lại HCP

Luồng tạo game — CreateGameCommandHandler

  1. Validate người chơi, tee, điểm đầy đủ, thứ tự ngày
  2. Tạo PlayerScore qua PlayerScoreFactory
  3. Tính tất cả loại điểm + handicap mới
  4. Cập nhật LastGameId, LastGameDate
  5. Lưu DB trong transaction
  6. Đẩy UpdateStatsMessage vào queue
  7. Gửi push notification + tin nhắn in-app

Chương 07Xác thực & Phân quyền

Login bằng userId (số) + password → JWT token chứa UserId, Name, Email, HomeClubId, AdminOf.

Tham sốGiá trị
IssuerEvgaApi
AudienceEvgaMobileApp
System UseruserId = 1, dùng cho Functions gọi API

Chính sách phân quyền

PolicyMô tảDùng cho
AdminOnlyAdminOf chứa clubId liên quanQuản lý CLB/user
ParentAdminOnlyAdmin CLB chaXuyên CLB
EvgaAdminOnlyAdmin EVGA top-levelToàn hệ thống
SystemOnlyuserId=1API nội bộ từ Functions

Chương 08Xử lý nền & Thông báo

FunctionQueueHành động
update-statisticsupdate-stats-queueRecalculate personal + course stats per user
generate-tournament-gamestournament-games-queueTạo Game records từ tournament

Functions đăng nhập system user → JWT → gọi API. Firebase FCM gửi push qua topics (all-dev, all-uat, all-prod). Lỗi notification không crash (catch + log warning).

Hangfire: SQL storage, dashboard /hangfire.  SignalR: Hub tại /pubsub, live scores.


Chương 09Cấu hình

SectionKeysGhi chú
ConnectionStringsEvgaContextPROD: Azure App Settings (không trong file)
AuthenticationConfigSecret, SystemHash, SystemUserDEV/UAT chung Secret, PROD Hash khác
BlobStorageConfigConnectionString, ContainerNameprofile-images container
QueueClientConfigStatsQueue, NotificationsQueue, TournamentGamesQueueCùng storage account với Blob
FirebaseKey (JSON), AllTopicNameFCM config

Chương 10CI/CD & Triển khai

PipelineConfigĐặc biệt
azure-pipelines-dev.ymlDevelopmentStandard publish
azure-pipelines-uat.ymlUATStandard publish
azure-pipelines-prod.ymlPRODSelf-contained, win-x86

Steps: Restore → Build → Test → Publish → Artifacts. Auth bằng Managed Identity.

Docker

  • Build: docker build --force-rm -f EVGA.Web/Dockerfile -t evgaweb .
  • Run: docker run -it --rm -p 5010:80 -e ASPNETCORE_ENVIRONMENT=Development evga-web:1.0.0

Chương 11Hướng dẫn phát triển

Thêm tính năng

  1. Model — DTO trong EVGA.Models/{Feature}/
  2. Query/CommandEVGA.Core/{Feature}/, implement IRequest<T>
  3. Handler — implement IRequestHandler, dùng EvgaContext
  4. Controller — endpoint trong EVGA.Web/API/, gọi _mediator.Send()
  5. Test — integration test trong Tests/EVGA.Web.IntegrationTests/

Vấn đề thường gặp

Kết thúc giải đấu chậm

Handler load toàn bộ flights/players/scores vào RAM → tạo Game tuần tự. Fix: batch + BulkInsert.

N+1 queries khi thêm players vào flight

AddPlayerToFlightCommandHandler Include() mỗi lần. Fix: batch assignment hoặc stored procedure.

Handicap > 54

Thêm Math.Min(result, 54m) trong HandicapCalculator.CalculatePlayerHandicap().


Chương 12Vấn đề bảo mật

⛔ NGHIÊM TRỌNG: Credentials nhạy cảm được commit trực tiếp vào source code. Xử lý TRƯỚC khi bàn giao.
Vấn đềChi tiếtGiải pháp
SQL password trong codeappsettings.Development/UAT.json chứa password rõ ràngAzure Key Vault hoặc Managed Identity
Firebase key trong repofirebase-config.json chứa private keyRotate key, Key Vault, xóa git history
Storage account keyAccountKey trong appsettingsManaged Identity
JWT Secret chungDEV/UAT dùng cùng SecretSecret riêng mỗi env
SQL user chungjakubjenis@evga cho DEV + UATUser riêng, quyền tối thiểu
Không rate limit/api/users/login không giới hạnRate limit qua APIM/middleware

Chương 13TODO & Tối ưu

Ưu tiên cao

  • Giải đấu EVGA luôn hiển thị đầu tiên
  • Chỉ start tournament vào đúng ngày
  • HCP tối đa 54
  • GPS cho holes

Ưu tiên trung bình

  • Sửa reserved IDs
  • Export tournament → Excel

Tối ưu hiệu suất

  • Cache course queries
  • Cache user detail
  • Tối ưu flight assignment + tournament finish cho giải lớn

Chương 14Bàn giao hệ thống

Checklist các thông tin cần nhận bàn giao từ developer gốc (Jakub).

14.1  Tài khoản & Quyền truy cập

  • Azure Portal — Subscription sub-evgaOwner/Contributor trên subscription. 3 resource groups: EVGA, EVGA-uat, EVGA-dev
  • Azure SQL Server — Tài khoản adminThay thế jakubjenis@evga. Password hiện tại cần ĐỔI NGAY
  • Azure DevOps — Pipeline accessAdmin project DevOps. Service Connections & Managed Identities
  • Firebase Console — evga-f8306Owner trên Firebase project. Cloud Messaging, rotate key
  • Source code repositoryAdmin trên Git repo. Branch strategy, protection rules

14.2  Credentials & Secrets

  • JWT SecretRotate và chuyển vào Key Vault
  • System User credentialsuserId=1, SystemHash. Password cho Functions config
  • Storage Account Keys3 accounts — nên chuyển sang Managed Identity
  • Firebase Service Account KeyCẦN ROTATE — đã commit vào repo
  • APIM Subscription KeysKeys cho API Management nếu client apps cần
  • Azure App Settings PRODConnection string PROD nằm trong Azure — xác nhận đầy đủ

14.3  Tài liệu & Source code

  • Backend source + Git historyRepo, branch chính, release strategy
  • Mobile app source codeRepo riêng iOS/Android? Framework?
  • Admin web source codeReact repo, build & deploy process
  • Database backup mới nhấtBackup hiện có: 2024-07-01. Cần bản mới
  • Deploy processCI/CD: commit → pipeline → auto hay manual?
  • Monitoring setupAi nhận alerts? Cần update Action Group
  • Admin users listAi có quyền gì trong hệ thống

14.4  Hành động SAU bàn giao

⚠️ Thực hiện NGAY sau khi nhận bàn giao:
  1. Đổi SQL password — thay password cho tất cả environments
  2. Rotate Firebase key — tạo key mới, cập nhật Azure App Settings
  3. Rotate JWT Secret — users phải đăng nhập lại
  4. Chuyển secrets → Azure Key Vault
  5. Tạo SQL users riêng mỗi env, quyền tối thiểu
  6. Thu hồi quyền dev cũ — Azure, DevOps, Firebase, repo
  7. Cập nhật alert recipients
  8. Full backup PROD + test restore trên failover

Chương 15Tra cứu nhanh

Chạy local

  • dotnet run --project EVGA.Web → https://localhost:5001
  • Swagger: /swagger · Hangfire: /hangfire · SignalR: /pubsub
  • Env: ASPNETCORE_ENVIRONMENT=Development

File quan trọng

Ở đâu
Solutionsrc/EVGA.sln
Startupsrc/EVGA.Web/Startup.cs
DIsrc/EVGA.Web/DI/ContainerExtensions.cs
DbContextsrc/EVGA.Core/Database/EvgaContext.cs
Migrationssrc/EVGA.Core/Migrations/
Controllerssrc/EVGA.Web/API/
CQRS Handlerssrc/EVGA.Core/{Feature}/Commands/ & Queries/
Calculatorssrc/EVGA.CommonLogic/Calculators/
Functionssrc/EVGA.Functions/
Pipelinesazure-pipelines-{env}.yml
Configsrc/EVGA.Web/appsettings.{Env}.json
Middlewaresrc/EVGA.Web/Middlewares/ApiExceptionHandlingMiddleware.cs
SignalR Hubsrc/EVGA.Web/Hubs/PubSubHub.cs
QT
ITViet.cz

Quoc Toan Tran

Phân tích hệ thống từ source code, Azure resources & sơ đồ bàn giao — Tháng 2, 2026