feat(deployments): Update local environment configuration for IAM service and Redis integration
- Modified local `.env` and `.env.local` files to include external Redis configuration and IAM service database connection details. - Updated `docker-compose.yml` to disable local Redis service in favor of an external Redis instance. - Added JWT configuration parameters for the IAM service, enhancing security and token management. - Revised example environment file to reflect new configuration options for external services. - Enhanced documentation to clarify setup instructions for local development with external dependencies.
This commit is contained in:
@@ -15,12 +15,24 @@ JWT_ID_EXPIRES_IN=1h
|
||||
# ENCRYPTION
|
||||
ENCRYPTION_KEY='460d261122522a6da8df4b9116a55d97432102a524cf055c04118265f0e51693'
|
||||
|
||||
# INFRA
|
||||
REDIS_HOST=redis
|
||||
# INFRA - External Redis
|
||||
REDIS_HOST=167.114.174.113
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=Velik@2026
|
||||
DATABASE_URL='postgresql://neondb_owner:npg_Ssfy6HKO0cXI@ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech/iam-service?sslmode=require&channel_binding=require'
|
||||
|
||||
# OBSERVABILITY
|
||||
TRACING_ENABLED=false
|
||||
JAEGER_ENDPOINT=http://jaeger:14268/api/traces
|
||||
METRICS_ENABLED=true
|
||||
|
||||
# IAM SERVICE .NET
|
||||
IAM_NET_DATABASE_URL='Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Port=5432;Database=iam_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require'
|
||||
REDIS_EXTERNAL_HOST=167.114.174.113
|
||||
REDIS_EXTERNAL_PORT=6379
|
||||
REDIS_EXTERNAL_PASSWORD=Velik@2026
|
||||
REDIS_EXTERNAL_DATABASE=0
|
||||
JWT_ISSUER=goodgo-platform
|
||||
JWT_AUDIENCE=goodgo-services
|
||||
JWT_ACCESS_TOKEN_EXPIRY_MINUTES=15
|
||||
JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
|
||||
|
||||
@@ -59,3 +59,22 @@ METRICS_ENABLED=true
|
||||
EMAIL_FROM=noreply@goodgo.vn
|
||||
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# =============================================================================
|
||||
# IAM SERVICE .NET CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Neon PostgreSQL for IAM .NET Service
|
||||
IAM_NET_DATABASE_URL="Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Port=5432;Database=iam_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require"
|
||||
|
||||
# External Redis
|
||||
REDIS_EXTERNAL_HOST=167.114.174.113
|
||||
REDIS_EXTERNAL_PORT=6379
|
||||
REDIS_EXTERNAL_PASSWORD=Velik@2026
|
||||
REDIS_EXTERNAL_DATABASE=0
|
||||
|
||||
# JWT Configuration for .NET Service
|
||||
JWT_ISSUER=goodgo-platform
|
||||
JWT_AUDIENCE=goodgo-services
|
||||
JWT_ACCESS_TOKEN_EXPIRY_MINUTES=15
|
||||
JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
|
||||
|
||||
@@ -27,24 +27,24 @@ services:
|
||||
# SHARED INFRASTRUCTURE
|
||||
# ===========================================================================
|
||||
|
||||
# Redis - Shared cache and session store
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: redis-cache-local
|
||||
command: redis-server /etc/redis/redis.conf
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ../../infra/databases/redis/redis.conf:/etc/redis/redis.conf
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- microservices-network
|
||||
restart: unless-stopped
|
||||
# Redis - DISABLED (using external Redis 167.114.174.113)
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: redis-cache-local
|
||||
# command: redis-server /etc/redis/redis.conf
|
||||
# ports:
|
||||
# - "${REDIS_PORT:-6379}:6379"
|
||||
# volumes:
|
||||
# - redis_data:/data
|
||||
# - ../../infra/databases/redis/redis.conf:/etc/redis/redis.conf
|
||||
# healthcheck:
|
||||
# test: ["CMD", "redis-cli", "ping"]
|
||||
# interval: 5s
|
||||
# timeout: 3s
|
||||
# retries: 5
|
||||
# networks:
|
||||
# - microservices-network
|
||||
# restart: unless-stopped
|
||||
|
||||
# Traefik - API Gateway and Reverse Proxy
|
||||
traefik:
|
||||
@@ -81,59 +81,6 @@ services:
|
||||
# BACKEND SERVICES
|
||||
# ===========================================================================
|
||||
|
||||
# IAM Service - Identity and Access Management
|
||||
iam-service:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/iam-service/Dockerfile
|
||||
container_name: iam-service-local
|
||||
env_file:
|
||||
- .env.local
|
||||
environment:
|
||||
# Service-specific
|
||||
- PORT=5001
|
||||
- SERVICE_NAME=iam-service
|
||||
- API_VERSION=${API_VERSION:-v1}
|
||||
|
||||
# Shared from .env.local (explicit for clarity)
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-debug}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- REDIS_HOST=${REDIS_HOST:-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-15m}
|
||||
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
|
||||
- JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-7d}
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||
- CORS_ORIGIN=${CORS_ORIGIN}
|
||||
- TRACING_ENABLED=${TRACING_ENABLED:-false}
|
||||
- JAEGER_ENDPOINT=${JAEGER_ENDPOINT}
|
||||
ports:
|
||||
- "5001:5001"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
traefik:
|
||||
condition: service_started
|
||||
networks:
|
||||
- microservices-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5001/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
# Traefik service discovery
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.iam-service.rule=PathPrefix(`/api/v1/auth`) || PathPrefix(`/api/v1/users`) || PathPrefix(`/api/v1/identity`) || PathPrefix(`/api/v1/access`) || PathPrefix(`/api/v1/governance`) || PathPrefix(`/api/v1/rbac`) || PathPrefix(`/api/v1/mfa`) || PathPrefix(`/api/v1/sessions`)"
|
||||
- "traefik.http.routers.iam-service.entrypoints=web"
|
||||
- "traefik.http.services.iam-service.loadbalancer.server.port=5001"
|
||||
- "traefik.http.services.iam-service.loadbalancer.healthcheck.path=/health"
|
||||
- "traefik.http.services.iam-service.loadbalancer.healthcheck.interval=10s"
|
||||
|
||||
# Storage Service .NET - File Storage Management
|
||||
storage-service:
|
||||
build:
|
||||
@@ -154,8 +101,6 @@ services:
|
||||
ports:
|
||||
- "5002:8080"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
traefik:
|
||||
@@ -213,8 +158,6 @@ services:
|
||||
ports:
|
||||
- "5003:8080"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
traefik:
|
||||
condition: service_started
|
||||
networks:
|
||||
@@ -234,6 +177,57 @@ services:
|
||||
- "traefik.http.services.social-service.loadbalancer.healthcheck.path=/health/live"
|
||||
- "traefik.http.services.social-service.loadbalancer.healthcheck.interval=10s"
|
||||
|
||||
# IAM Service .NET - Identity and Access Management (Duende IdentityServer)
|
||||
iam-service-net:
|
||||
image: goodgo/iam-service-net:latest
|
||||
container_name: iam-service-net-local
|
||||
env_file:
|
||||
- .env.local
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_URLS=http://+:8080
|
||||
# EN: Database - Neon PostgreSQL (from .env)
|
||||
# VI: Cơ sở dữ liệu - Neon PostgreSQL (từ .env)
|
||||
- ConnectionStrings__DefaultConnection=${IAM_NET_DATABASE_URL}
|
||||
# EN: Redis Cache (external)
|
||||
# VI: Cache Redis (bên ngoài)
|
||||
- Redis__Host=${REDIS_EXTERNAL_HOST}
|
||||
- Redis__Port=${REDIS_EXTERNAL_PORT}
|
||||
- Redis__Password=${REDIS_EXTERNAL_PASSWORD}
|
||||
- Redis__Database=${REDIS_EXTERNAL_DATABASE}
|
||||
# EN: JWT Configuration
|
||||
# VI: Cấu hình JWT
|
||||
- Jwt__Secret=${JWT_SECRET}
|
||||
- Jwt__Issuer=${JWT_ISSUER:-goodgo-platform}
|
||||
- Jwt__Audience=${JWT_AUDIENCE:-goodgo-services}
|
||||
- Jwt__AccessTokenExpiryMinutes=${JWT_ACCESS_TOKEN_EXPIRY_MINUTES:-15}
|
||||
- Jwt__RefreshTokenExpiryDays=${JWT_REFRESH_TOKEN_EXPIRY_DAYS:-7}
|
||||
# EN: Features
|
||||
# VI: Tính năng
|
||||
- Features__SwaggerEnabled=true
|
||||
- Features__DetailedErrors=true
|
||||
ports:
|
||||
- "5001:8080"
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: service_started
|
||||
networks:
|
||||
- microservices-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.iam-service-net.rule=PathPrefix(`/api/v1/iam`) || PathPrefix(`/api/v1/auth`) || PathPrefix(`/api/v1/users`)"
|
||||
- "traefik.http.routers.iam-service-net.entrypoints=web"
|
||||
- "traefik.http.services.iam-service-net.loadbalancer.server.port=8080"
|
||||
- "traefik.http.services.iam-service-net.loadbalancer.healthcheck.path=/health/live"
|
||||
- "traefik.http.services.iam-service-net.loadbalancer.healthcheck.interval=10s"
|
||||
|
||||
# ===========================================================================
|
||||
# FRONTEND APPLICATIONS (Temporarily disabled)
|
||||
# ===========================================================================
|
||||
@@ -335,8 +329,8 @@ services:
|
||||
# VOLUMES
|
||||
# =============================================================================
|
||||
volumes:
|
||||
redis_data:
|
||||
driver: local
|
||||
# redis_data:
|
||||
# driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
# prometheus_data:
|
||||
|
||||
@@ -66,6 +66,26 @@ JAEGER_ENDPOINT=http://jaeger:14268/api/traces
|
||||
# Prometheus Metrics (exposed by each service at /metrics)
|
||||
METRICS_ENABLED=true
|
||||
|
||||
# =============================================================================
|
||||
# IAM SERVICE .NET CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Neon PostgreSQL for IAM .NET Service
|
||||
# Get from https://console.neon.tech
|
||||
IAM_NET_DATABASE_URL=Host=your-neon-host.neon.tech;Port=5432;Database=iam_service;Username=your-user;Password=your-password;SSL Mode=Require
|
||||
|
||||
# External Redis (if using external Redis instead of local container)
|
||||
REDIS_EXTERNAL_HOST=redis
|
||||
REDIS_EXTERNAL_PORT=6379
|
||||
REDIS_EXTERNAL_PASSWORD=
|
||||
REDIS_EXTERNAL_DATABASE=0
|
||||
|
||||
# JWT Configuration for .NET Service
|
||||
JWT_ISSUER=goodgo-platform
|
||||
JWT_AUDIENCE=goodgo-services
|
||||
JWT_ACCESS_TOKEN_EXPIRY_MINUTES=15
|
||||
JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
|
||||
|
||||
# =============================================================================
|
||||
# EXTERNAL SERVICES (Optional)
|
||||
# =============================================================================
|
||||
|
||||
@@ -20,11 +20,11 @@ COPY src/ ./src/
|
||||
# EN: Build the application
|
||||
# VI: Build ứng dụng
|
||||
WORKDIR "/src/src/IamService.API"
|
||||
RUN dotnet build "IamService.API.csproj" -c Release -o /app/build --no-restore
|
||||
RUN dotnet build "IamService.API.csproj" -c Release -o /app/build
|
||||
|
||||
# Publish stage / Giai đoạn publish
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "IamService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
RUN dotnet publish "IamService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Runtime stage / Giai đoạn runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.8'
|
||||
|
||||
# EN: Docker Compose for local development
|
||||
# VI: Docker Compose cho phát triển local
|
||||
# EN: Docker Compose for local development with external services
|
||||
# VI: Docker Compose cho phát triển local với dịch vụ bên ngoài
|
||||
|
||||
services:
|
||||
iamservice-api:
|
||||
@@ -10,16 +10,34 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: iamservice-api
|
||||
ports:
|
||||
- "5001:8080"
|
||||
- "${API_PORT:-5001}:8080"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=iamservice_db;Username=postgres;Password=postgres
|
||||
- REDIS_URL=redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
# EN: Override for container environment
|
||||
# VI: Ghi đè cho môi trường container
|
||||
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Development}
|
||||
- ASPNETCORE_URLS=http://+:8080
|
||||
# EN: Database - Neon PostgreSQL
|
||||
# VI: Cơ sở dữ liệu - Neon PostgreSQL
|
||||
- ConnectionStrings__DefaultConnection=${DATABASE_URL}
|
||||
# EN: Redis Cache
|
||||
# VI: Cache Redis
|
||||
- Redis__Host=${REDIS_HOST}
|
||||
- Redis__Port=${REDIS_PORT}
|
||||
- Redis__Password=${REDIS_PASSWORD}
|
||||
- Redis__Database=${REDIS_DATABASE:-0}
|
||||
# EN: JWT Configuration
|
||||
# VI: Cấu hình JWT
|
||||
- Jwt__Secret=${JWT_SECRET}
|
||||
- Jwt__Issuer=${JWT_ISSUER}
|
||||
- Jwt__Audience=${JWT_AUDIENCE}
|
||||
- Jwt__AccessTokenExpiryMinutes=${JWT_ACCESS_TOKEN_EXPIRY_MINUTES:-15}
|
||||
- Jwt__RefreshTokenExpiryDays=${JWT_REFRESH_TOKEN_EXPIRY_DAYS:-7}
|
||||
# EN: Features
|
||||
# VI: Tính năng
|
||||
- Features__SwaggerEnabled=${FEATURE_SWAGGER_ENABLED:-true}
|
||||
- Features__DetailedErrors=${FEATURE_DETAILED_ERRORS:-true}
|
||||
networks:
|
||||
- iamservice-network
|
||||
healthcheck:
|
||||
@@ -27,45 +45,8 @@ services:
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: iamservice-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: iamservice_db
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- iamservice-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: iamservice-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- iamservice-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
start_period: 15s
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
iamservice-network:
|
||||
|
||||
@@ -1,271 +1,230 @@
|
||||
# Architecture Documentation
|
||||
# Wallet Service Architecture
|
||||
|
||||
> Detailed architecture documentation for the .NET 10 Microservice Template.
|
||||
## Overview
|
||||
|
||||
## Architecture Overview
|
||||
The Wallet Service manages digital wallets and loyalty point accounts for the GoodGo Platform.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "API Layer"
|
||||
C[Controllers]
|
||||
CMD[Commands]
|
||||
Q[Queries]
|
||||
B[Behaviors]
|
||||
V[Validations]
|
||||
end
|
||||
|
||||
subgraph "Domain Layer"
|
||||
AR[Aggregate Roots]
|
||||
E[Entities]
|
||||
VO[Value Objects]
|
||||
DE[Domain Events]
|
||||
DX[Domain Exceptions]
|
||||
end
|
||||
|
||||
subgraph "Infrastructure Layer"
|
||||
DB[(PostgreSQL)]
|
||||
R[Repositories]
|
||||
CTX[DbContext]
|
||||
ID[Idempotency]
|
||||
end
|
||||
|
||||
C --> CMD
|
||||
C --> Q
|
||||
CMD --> B --> V
|
||||
CMD --> AR
|
||||
Q --> R
|
||||
R --> CTX --> DB
|
||||
AR --> DE
|
||||
R --> AR
|
||||
|
||||
style C fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Wallet Service │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ API Layer (Controllers, CQRS) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Domain Layer (Wallet, PointAccount Aggregates) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer (EF Core, Repositories) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ PostgreSQL Database │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Layer Responsibilities
|
||||
## Architecture Patterns
|
||||
|
||||
### 1. Domain Layer (WalletService.Domain)
|
||||
### Domain-Driven Design (DDD)
|
||||
|
||||
The heart of the application containing pure business logic. This layer:
|
||||
- Has **ZERO** external dependencies (except MediatR.Contracts for events)
|
||||
- Contains only POCO classes
|
||||
- Implements DDD tactical patterns
|
||||
- **Aggregates**: Wallet, PointAccount
|
||||
- **Entities**: WalletTransaction, PointTransaction
|
||||
- **Value Objects**: Money, PointBalance
|
||||
- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned
|
||||
|
||||
#### Components
|
||||
### CQRS Pattern
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
|
||||
| **AggregatesModel** | Aggregate roots with their entities and value objects |
|
||||
| **Events** | Domain events for cross-aggregate communication |
|
||||
| **Exceptions** | Domain-specific exceptions for business rule violations |
|
||||
|
||||
### 2. Infrastructure Layer (WalletService.Infrastructure)
|
||||
|
||||
Technical implementations and external concerns:
|
||||
- Database access (EF Core)
|
||||
- Repository implementations
|
||||
- External service integrations
|
||||
|
||||
### 3. API Layer (WalletService.API)
|
||||
|
||||
Application entry point and CQRS implementation:
|
||||
- Controllers for HTTP handling
|
||||
- Commands for write operations
|
||||
- Queries for read operations
|
||||
- MediatR behaviors for cross-cutting concerns
|
||||
|
||||
## CQRS Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Controller
|
||||
participant MediatR
|
||||
participant LoggingBehavior
|
||||
participant ValidatorBehavior
|
||||
participant TransactionBehavior
|
||||
participant CommandHandler
|
||||
participant Repository
|
||||
participant DbContext
|
||||
|
||||
Client->>Controller: HTTP Request
|
||||
Controller->>MediatR: Send(Command)
|
||||
MediatR->>LoggingBehavior: Handle
|
||||
LoggingBehavior->>ValidatorBehavior: Next()
|
||||
ValidatorBehavior->>TransactionBehavior: Next()
|
||||
TransactionBehavior->>CommandHandler: Next()
|
||||
CommandHandler->>Repository: Add/Update/Delete
|
||||
Repository->>DbContext: SaveEntitiesAsync()
|
||||
DbContext-->>Repository: Success
|
||||
Repository-->>CommandHandler: Result
|
||||
CommandHandler-->>Controller: Response
|
||||
Controller-->>Client: HTTP Response
|
||||
```
|
||||
Commands (Write) Queries (Read)
|
||||
│ │
|
||||
▼ ▼
|
||||
CreateWallet GetWalletQuery
|
||||
DepositCommand GetTransactionsQuery
|
||||
WithdrawCommand GetPointAccountQuery
|
||||
TransferCommand GetBalanceSummaryQuery
|
||||
EarnPointsCommand
|
||||
SpendPointsCommand
|
||||
```
|
||||
|
||||
## Domain Events
|
||||
## Domain Model
|
||||
|
||||
### Wallet Aggregate
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
AR[Aggregate Root] -->|Raises| DE[Domain Event]
|
||||
DE -->|Dispatched by| CTX[DbContext]
|
||||
CTX -->|Publishes to| M[MediatR]
|
||||
M -->|Handled by| H1[Handler 1]
|
||||
M -->|Handled by| H2[Handler 2]
|
||||
classDiagram
|
||||
class Wallet {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Money Balance
|
||||
+WalletStatus Status
|
||||
+List~WalletTransaction~ Transactions
|
||||
+Deposit(Money, description, ref)
|
||||
+Withdraw(Money, description, ref)
|
||||
+Freeze()
|
||||
+Unfreeze()
|
||||
+Close()
|
||||
}
|
||||
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DE fill:#f39c12,stroke:#d68910,color:#fff
|
||||
style M fill:#9b59b6,stroke:#7d3c98,color:#fff
|
||||
class WalletTransaction {
|
||||
+Guid Id
|
||||
+TransactionType Type
|
||||
+Money Amount
|
||||
+Money BalanceAfter
|
||||
+string Description
|
||||
+DateTime CreatedAt
|
||||
}
|
||||
|
||||
class Money {
|
||||
+decimal Amount
|
||||
+string Currency
|
||||
}
|
||||
|
||||
Wallet "1" --> "*" WalletTransaction
|
||||
Wallet --> Money
|
||||
```
|
||||
|
||||
### PointAccount Aggregate
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class PointAccount {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+int TotalPoints
|
||||
+int AvailablePoints
|
||||
+int PendingPoints
|
||||
+EarnPoints(points, source, desc, expires)
|
||||
+SpendPoints(points, source, desc)
|
||||
+AdjustPoints(points, source, desc)
|
||||
}
|
||||
|
||||
class PointTransaction {
|
||||
+Guid Id
|
||||
+PointTransactionType Type
|
||||
+int Points
|
||||
+int BalanceAfter
|
||||
+DateTime? ExpiresAt
|
||||
}
|
||||
|
||||
PointAccount "1" --> "*" PointTransaction
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Sample Aggregate
|
||||
### Tables
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
samples {
|
||||
uuid id PK
|
||||
varchar(200) name
|
||||
varchar(1000) description
|
||||
int status_id FK
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
sample_statuses {
|
||||
int id PK
|
||||
varchar(50) name
|
||||
}
|
||||
|
||||
samples ||--o{ sample_statuses : has
|
||||
```
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `Wallets` | User wallet accounts |
|
||||
| `WalletTransactions` | Wallet transaction history |
|
||||
| `PointAccounts` | User point accounts |
|
||||
| `PointTransactions` | Point transaction history |
|
||||
|
||||
## MediatR Pipeline
|
||||
### Key Indexes
|
||||
|
||||
- `IX_Wallets_UserId` - Fast lookup by user
|
||||
- `IX_WalletTransactions_WalletId` - Transaction history
|
||||
- `IX_PointAccounts_UserId` - Fast lookup by user
|
||||
|
||||
## API Flow
|
||||
|
||||
### Deposit Flow
|
||||
|
||||
```
|
||||
Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Log start/end Validate Begin/Commit
|
||||
+ timing with Transaction
|
||||
FluentValidation
|
||||
1. Client → POST /api/v1/wallets/deposit
|
||||
2. Controller → DepositCommand (MediatR)
|
||||
3. CommandHandler → Validate amount
|
||||
4. Handler → wallet.Deposit(amount)
|
||||
5. Domain → Create WalletTransaction + Domain Event
|
||||
6. Infrastructure → Save to database
|
||||
7. Response → Updated balance
|
||||
```
|
||||
|
||||
### Behavior Order
|
||||
|
||||
1. **LoggingBehavior** - Logs request handling with timing
|
||||
2. **ValidatorBehavior** - Validates request using FluentValidation
|
||||
3. **TransactionBehavior** - Wraps command handlers in database transactions
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Exception Hierarchy
|
||||
### Transfer Flow
|
||||
|
||||
```
|
||||
Exception
|
||||
└── DomainException
|
||||
└── SampleDomainException
|
||||
1. Client → POST /api/v1/wallets/transfer
|
||||
2. Controller → TransferCommand
|
||||
3. Handler → Get source wallet
|
||||
4. Handler → Get target wallet
|
||||
5. Domain → source.Withdraw() + target.Deposit()
|
||||
6. Infrastructure → Transactional save
|
||||
7. Response → Transfer confirmation
|
||||
```
|
||||
|
||||
### Problem Details (RFC 7807)
|
||||
## Inter-Service Communication
|
||||
|
||||
All errors are returned in Problem Details format:
|
||||
### IAM Service Integration
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7807",
|
||||
"title": "Validation Error",
|
||||
"status": 400,
|
||||
"detail": "One or more validation errors occurred.",
|
||||
"errors": {
|
||||
"Name": ["Name is required"]
|
||||
}
|
||||
}
|
||||
```
|
||||
Wallet Service ──────► IAM Service
|
||||
│
|
||||
▼
|
||||
User Validation
|
||||
JWT Verification
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
### Authentication Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
HC[Health Check Endpoint]
|
||||
HC --> |/health/live| L[Liveness]
|
||||
HC --> |/health/ready| R[Readiness]
|
||||
HC --> |/health| F[Full Status]
|
||||
|
||||
R --> PG[(PostgreSQL)]
|
||||
R --> RD[(Redis)]
|
||||
|
||||
style HC fill:#3498db,stroke:#2980b9,color:#fff
|
||||
style L fill:#2ecc71,stroke:#27ae60,color:#fff
|
||||
style R fill:#f39c12,stroke:#d68910,color:#fff
|
||||
```
|
||||
1. Client sends JWT token in Authorization header
|
||||
2. Wallet Service validates JWT with IAM Service
|
||||
3. Extract userId from JWT claims
|
||||
4. Process wallet operation for user
|
||||
|
||||
## Deployment Architecture
|
||||
## Deployment
|
||||
|
||||
### Docker Compose (Local)
|
||||
### Docker Compose Configuration
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myservice-api:
|
||||
build: .
|
||||
ports: ["5000:8080"]
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
wallet-service:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/wallet-service-net/Dockerfile
|
||||
environment:
|
||||
- DATABASE_URL=${WALLET_DATABASE_URL}
|
||||
- JWT_AUTHORITY=${IAM_SERVICE_URL}
|
||||
labels:
|
||||
- traefik.http.routers.wallet.rule=PathPrefix(`/api/v1/wallets`)
|
||||
```
|
||||
|
||||
### Kubernetes (Production)
|
||||
### Health Checks
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: myservice-api
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: api
|
||||
image: myservice:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/live
|
||||
port: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 8080
|
||||
```
|
||||
| Endpoint | Check |
|
||||
|----------|-------|
|
||||
| `/health/live` | Service is running |
|
||||
| `/health/ready` | Database connected |
|
||||
| `/health` | Full status |
|
||||
|
||||
## Security Considerations
|
||||
## Security
|
||||
|
||||
1. **Authentication**: JWT Bearer token (configure in production)
|
||||
2. **Authorization**: Role-based access control
|
||||
3. **Input Validation**: FluentValidation on all requests
|
||||
4. **SQL Injection**: EF Core parameterized queries
|
||||
5. **Secrets**: Environment variables, never in code
|
||||
### Authentication
|
||||
- JWT Bearer token validation
|
||||
- IAM Service integration
|
||||
|
||||
## Performance Optimization
|
||||
### Authorization
|
||||
- User can only access own wallet
|
||||
- Admin endpoints for system operations
|
||||
|
||||
1. **Connection Pooling**: EF Core with Npgsql connection resilience
|
||||
2. **Async/Await**: All I/O operations are async
|
||||
3. **Response Caching**: Add caching headers for queries
|
||||
4. **Database Indexes**: Configure in EntityConfigurations
|
||||
### Data Protection
|
||||
- All amounts stored with precision
|
||||
- Transaction audit trail
|
||||
- Soft delete for wallets
|
||||
|
||||
## References
|
||||
## Performance
|
||||
|
||||
- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
|
||||
- [.NET Microservices Architecture Guide](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
|
||||
- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
|
||||
- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
|
||||
### Optimizations
|
||||
- Connection pooling (EF Core)
|
||||
- Index on frequent queries
|
||||
- Pagination for transaction history
|
||||
|
||||
### Scaling
|
||||
- Horizontal scaling with load balancer
|
||||
- Read replicas for queries
|
||||
- Redis caching (future)
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Metrics
|
||||
- Request duration
|
||||
- Transaction counts
|
||||
- Error rates
|
||||
|
||||
### Logging
|
||||
- Serilog structured logging
|
||||
- Correlation IDs for tracing
|
||||
- Seq/Loki integration
|
||||
|
||||
@@ -1,193 +1,161 @@
|
||||
# .NET 10 Microservice Template
|
||||
# Wallet Service .NET
|
||||
|
||||
> Enterprise-grade .NET 10 microservice template following DDD, CQRS, and Clean Architecture patterns.
|
||||
> **EN**: Wallet and Point Account management service for GoodGo Platform.
|
||||
> **VI**: Dịch vụ quản lý Ví và Tài khoản Điểm cho GoodGo Platform.
|
||||
|
||||
## Overview
|
||||
|
||||
This template provides a production-ready structure for .NET microservices based on the eShopOnContainers reference architecture with:
|
||||
The Wallet Service provides comprehensive wallet and loyalty points management with:
|
||||
|
||||
- **Domain-Driven Design (DDD)** - Aggregates, Entities, Value Objects, Domain Events
|
||||
- **CQRS Pattern** - Separate Commands (write) and Queries (read) with MediatR
|
||||
- **Clean Architecture** - Domain, Infrastructure, API layered separation
|
||||
- **EF Core 10** - PostgreSQL with connection resilience
|
||||
- **FluentValidation** - Request validation
|
||||
- **API Versioning** - URL segment versioning
|
||||
- **Health Checks** - Kubernetes-ready probes
|
||||
- **Structured Logging** - Serilog with console and Seq
|
||||
- **Wallet Management** - Create, deposit, withdraw, transfer funds
|
||||
- **Point Account** - Earn, spend, and track loyalty points
|
||||
- **Transaction History** - Full audit trail of all transactions
|
||||
- **Multi-Currency Support** - Default VND with currency support
|
||||
- **Domain-Driven Design** - Clean Architecture with CQRS pattern
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|------------|
|
||||
| Framework | .NET 10 |
|
||||
| Database | PostgreSQL (EF Core) |
|
||||
| CQRS | MediatR |
|
||||
| Validation | FluentValidation |
|
||||
| API Docs | Swagger/OpenAPI |
|
||||
| Logging | Serilog |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Version |
|
||||
|-------------|---------|
|
||||
| .NET SDK | 10.0.101+ |
|
||||
| Docker | 24.0+ |
|
||||
| PostgreSQL | 15+ (or use Docker) |
|
||||
|
||||
```bash
|
||||
# Check .NET version
|
||||
dotnet --version
|
||||
# Should output: 10.0.xxx
|
||||
dotnet --version # Should be 10.0.x
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create New Service
|
||||
### 1. Configure Environment
|
||||
|
||||
```bash
|
||||
# Copy template to new service
|
||||
cp -r services/_template_dot_net services/your-service-name
|
||||
|
||||
# Navigate to service directory
|
||||
cd services/your-service-name
|
||||
|
||||
# Rename all occurrences of "WalletService" to "YourService"
|
||||
find . -type f -name "*.cs" -exec sed -i '' 's/WalletService/YourService/g' {} +
|
||||
find . -type f -name "*.csproj" -exec sed -i '' 's/WalletService/YourService/g' {} +
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
```bash
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your configuration
|
||||
nano .env
|
||||
# Edit .env with your database connection
|
||||
```
|
||||
|
||||
### 3. Run with Docker
|
||||
### 2. Run with Docker
|
||||
|
||||
```bash
|
||||
# Start all services (API + PostgreSQL + Redis)
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f myservice-api
|
||||
cd deployments/local
|
||||
docker-compose up wallet-service -d
|
||||
```
|
||||
|
||||
### 4. Run Locally
|
||||
### 3. Run Locally
|
||||
|
||||
```bash
|
||||
# Restore dependencies
|
||||
cd services/wallet-service-net
|
||||
dotnet restore
|
||||
|
||||
# Build all projects
|
||||
dotnet build
|
||||
|
||||
# Run the API
|
||||
dotnet run --project src/WalletService.API
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
_template_dot_net/
|
||||
├── src/
|
||||
│ ├── WalletService.API/ # Presentation Layer (Controllers, CQRS)
|
||||
│ │ ├── Controllers/ # API endpoints
|
||||
│ │ ├── Application/ # CQRS Implementation
|
||||
│ │ │ ├── Commands/ # Write operations (MediatR)
|
||||
│ │ │ ├── Queries/ # Read operations
|
||||
│ │ │ ├── Behaviors/ # MediatR pipeline behaviors
|
||||
│ │ │ └── Validations/ # FluentValidation validators
|
||||
│ │ ├── Middleware/ # Custom middleware
|
||||
│ │ └── Program.cs # Application entry point
|
||||
│ │
|
||||
│ ├── WalletService.Domain/ # Domain Layer (Pure business logic)
|
||||
│ │ ├── AggregatesModel/ # Aggregate roots and entities
|
||||
│ │ ├── Events/ # Domain events
|
||||
│ │ ├── Exceptions/ # Domain exceptions
|
||||
│ │ └── SeedWork/ # Base classes (Entity, ValueObject, etc.)
|
||||
│ │
|
||||
│ └── WalletService.Infrastructure/ # Infrastructure Layer (Data access)
|
||||
│ ├── EntityConfigurations/ # EF Core Fluent API configurations
|
||||
│ ├── Repositories/ # Repository implementations
|
||||
│ ├── Idempotency/ # Request idempotency handling
|
||||
│ └── WalletServiceContext.cs # DbContext with Unit of Work
|
||||
│
|
||||
├── tests/
|
||||
│ ├── WalletService.UnitTests/ # Unit tests (Domain, Application)
|
||||
│ └── WalletService.FunctionalTests/ # Integration tests (API endpoints)
|
||||
│
|
||||
├── Dockerfile # Multi-stage Docker build
|
||||
├── docker-compose.yml # Local development setup
|
||||
├── global.json # .NET SDK version pinning
|
||||
└── Directory.Build.props # Common MSBuild properties
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Wallet APIs
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/v1/samples` | Get all samples |
|
||||
| `GET` | `/api/v1/samples/{id}` | Get sample by ID |
|
||||
| `POST` | `/api/v1/samples` | Create new sample |
|
||||
| `PUT` | `/api/v1/samples/{id}` | Update sample |
|
||||
| `DELETE` | `/api/v1/samples/{id}` | Delete sample |
|
||||
| `PATCH` | `/api/v1/samples/{id}/status` | Change status |
|
||||
| `POST` | `/api/v1/wallets` | Create new wallet |
|
||||
| `GET` | `/api/v1/wallets/me` | Get current user's wallet |
|
||||
| `GET` | `/api/v1/wallets/{id}` | Get wallet by ID |
|
||||
| `POST` | `/api/v1/wallets/deposit` | Deposit funds |
|
||||
| `POST` | `/api/v1/wallets/withdraw` | Withdraw funds |
|
||||
| `POST` | `/api/v1/wallets/transfer` | Transfer between wallets |
|
||||
| `POST` | `/api/v1/wallets/{id}/freeze` | Freeze wallet |
|
||||
| `POST` | `/api/v1/wallets/{id}/unfreeze` | Unfreeze wallet |
|
||||
| `GET` | `/api/v1/wallets/transactions` | Get transaction history |
|
||||
|
||||
### Points APIs
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/v1/points/me` | Get current user's points |
|
||||
| `POST` | `/api/v1/points/earn` | Earn points |
|
||||
| `POST` | `/api/v1/points/spend` | Spend points |
|
||||
| `GET` | `/api/v1/points/transactions` | Get point transactions |
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `/health` | Full health status |
|
||||
| `/health/live` | Liveness probe |
|
||||
| `/health/ready` | Readiness probe |
|
||||
| `/health/live` | Liveness probe (K8s) |
|
||||
| `/health/ready` | Readiness probe (K8s) |
|
||||
|
||||
## CQRS Pattern
|
||||
## Project Structure
|
||||
|
||||
### Commands (Write Operations)
|
||||
|
||||
```csharp
|
||||
// Define command
|
||||
public record CreateSampleCommand(string Name, string? Description)
|
||||
: IRequest<CreateSampleCommandResult>;
|
||||
|
||||
// Handle command
|
||||
public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
|
||||
{
|
||||
public async Task<CreateSampleCommandResult> Handle(CreateSampleCommand request, CancellationToken ct)
|
||||
{
|
||||
var sample = new Sample(request.Name, request.Description);
|
||||
_repository.Add(sample);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(ct);
|
||||
return new CreateSampleCommandResult(sample.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Queries (Read Operations)
|
||||
|
||||
```csharp
|
||||
// Define query
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
wallet-service-net/
|
||||
├── src/
|
||||
│ ├── WalletService.API/ # API Layer
|
||||
│ │ ├── Controllers/ # REST endpoints
|
||||
│ │ └── Application/ # Commands & Queries
|
||||
│ │ ├── Commands/ # Write operations
|
||||
│ │ └── Queries/ # Read operations
|
||||
│ │
|
||||
│ ├── WalletService.Domain/ # Domain Layer
|
||||
│ │ ├── AggregatesModel/
|
||||
│ │ │ ├── WalletAggregate/ # Wallet, Transaction, Money
|
||||
│ │ │ └── PointAccountAggregate/ # Points, PointTransaction
|
||||
│ │ ├── Events/ # Domain events
|
||||
│ │ └── Exceptions/ # Domain exceptions
|
||||
│ │
|
||||
│ └── WalletService.Infrastructure/ # Infrastructure Layer
|
||||
│ ├── EntityConfigurations/ # EF Core mappings
|
||||
│ ├── Repositories/ # Data access
|
||||
│ └── WalletServiceContext.cs # DbContext
|
||||
│
|
||||
├── tests/
|
||||
│ ├── WalletService.UnitTests/ # Domain & Logic tests
|
||||
│ └── WalletService.FunctionalTests/ # API integration tests
|
||||
│
|
||||
├── docs/
|
||||
│ ├── en/ # English docs
|
||||
│ └── vi/ # Vietnamese docs
|
||||
│
|
||||
└── Dockerfile
|
||||
```
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Aggregate Root
|
||||
### Wallet Aggregate
|
||||
|
||||
```csharp
|
||||
public class Sample : Entity, IAggregateRoot
|
||||
{
|
||||
public string Name => _name;
|
||||
public SampleStatus Status => _status;
|
||||
|
||||
public Sample(string name, string? description) {
|
||||
// Business logic validation
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new SampleDomainException("Sample name cannot be empty");
|
||||
|
||||
// Domain event
|
||||
AddDomainEvent(new SampleCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
public void Activate() {
|
||||
if (_status != SampleStatus.Draft)
|
||||
throw new SampleDomainException("Only draft samples can be activated");
|
||||
// State transition
|
||||
}
|
||||
}
|
||||
// Create wallet
|
||||
var wallet = new Wallet(userId, "VND");
|
||||
|
||||
// Deposit funds
|
||||
wallet.Deposit(new Money(1000000m, "VND"), "Salary", "REF001");
|
||||
|
||||
// Withdraw funds
|
||||
wallet.Withdraw(new Money(500000m, "VND"), "Shopping", "REF002");
|
||||
|
||||
// Freeze/Unfreeze
|
||||
wallet.Freeze();
|
||||
wallet.Unfreeze();
|
||||
```
|
||||
|
||||
### Point Account Aggregate
|
||||
|
||||
```csharp
|
||||
// Create account
|
||||
var account = new PointAccount(userId);
|
||||
|
||||
// Earn points
|
||||
account.EarnPoints(100, "ORDER001", "Purchase reward", expiresAt);
|
||||
|
||||
// Spend points
|
||||
account.SpendPoints(50, "ORDER002", "Redeem discount");
|
||||
|
||||
// Adjust points (admin)
|
||||
account.AdjustPoints(10, "ADMIN", "Bonus adjustment");
|
||||
```
|
||||
|
||||
## Testing
|
||||
@@ -197,68 +165,75 @@ public class Sample : Entity, IAggregateRoot
|
||||
dotnet test
|
||||
|
||||
# Run with coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
|
||||
dotnet test /p:CollectCoverage=true
|
||||
|
||||
# Run specific test project
|
||||
# Unit tests only
|
||||
dotnet test tests/WalletService.UnitTests
|
||||
|
||||
# Functional tests only
|
||||
dotnet test tests/WalletService.FunctionalTests
|
||||
```
|
||||
|
||||
### Test Results
|
||||
- **23 tests** total
|
||||
- 20 unit tests (Wallet, PointAccount domain)
|
||||
- 3 functional tests (Health endpoints)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `ASPNETCORE_ENVIRONMENT` | Environment name | `Development` |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | - |
|
||||
| `REDIS_URL` | Redis connection string | - |
|
||||
| `JWT_SECRET` | JWT signing secret (min 32 chars) | - |
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `DATABASE_URL` | PostgreSQL connection | Yes |
|
||||
| `ASPNETCORE_ENVIRONMENT` | Environment | No (default: Development) |
|
||||
| `JWT_AUTHORITY` | JWT issuer URL | Yes (for auth) |
|
||||
|
||||
### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Database=myservice;Username=postgres;Password=postgres"
|
||||
"DefaultConnection": "Host=localhost;Database=wallet_db;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": "Information"
|
||||
"Jwt": {
|
||||
"Authority": "http://localhost:5001",
|
||||
"Issuer": "http://localhost:5001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
```bash
|
||||
# Create migration
|
||||
dotnet ef migrations add InitialCreate \
|
||||
--project src/WalletService.Infrastructure \
|
||||
--startup-project src/WalletService.API
|
||||
|
||||
# Apply migration
|
||||
dotnet ef database update \
|
||||
--project src/WalletService.Infrastructure \
|
||||
--startup-project src/WalletService.API
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Build
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
docker build -t myservice:latest .
|
||||
|
||||
# Run container
|
||||
docker run -p 5000:8080 --env-file .env myservice:latest
|
||||
docker build -t goodgo/wallet-service:latest .
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
### Docker Compose
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for Kubernetes deployment manifests.
|
||||
|
||||
## What's New in .NET 10
|
||||
|
||||
- **C# 14** language features
|
||||
- Improved **Native AOT** support
|
||||
- Better **async/await** performance
|
||||
- Enhanced **JSON serialization**
|
||||
- Performance improvements across the board
|
||||
- 3-year **LTS** support (until November 2028)
|
||||
Service is registered in `deployments/local/docker-compose.yml` with Traefik routing.
|
||||
|
||||
## Resources
|
||||
|
||||
- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) - Reference architecture
|
||||
- [.NET 10 Documentation](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10)
|
||||
- [DDD with .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)
|
||||
- [MediatR](https://github.com/jbogard/MediatR) - CQRS library
|
||||
- [FluentValidation](https://docs.fluentvalidation.net/) - Validation library
|
||||
- [Architecture Documentation](./ARCHITECTURE.md)
|
||||
- [GoodGo Platform Docs](../../docs/)
|
||||
- [eShopOnContainers Reference](https://github.com/dotnet-architecture/eShopOnContainers)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,271 +1,230 @@
|
||||
# Tài Liệu Kiến Trúc
|
||||
# Kiến Trúc Wallet Service
|
||||
|
||||
> Tài liệu kiến trúc chi tiết cho Template Microservice .NET 10.
|
||||
## Tổng Quan
|
||||
|
||||
## Tổng Quan Kiến Trúc
|
||||
Wallet Service quản lý ví điện tử và tài khoản điểm thưởng cho GoodGo Platform.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Lớp API"
|
||||
C[Controllers]
|
||||
CMD[Commands]
|
||||
Q[Queries]
|
||||
B[Behaviors]
|
||||
V[Validations]
|
||||
end
|
||||
|
||||
subgraph "Lớp Domain"
|
||||
AR[Aggregate Roots]
|
||||
E[Entities]
|
||||
VO[Value Objects]
|
||||
DE[Domain Events]
|
||||
DX[Domain Exceptions]
|
||||
end
|
||||
|
||||
subgraph "Lớp Infrastructure"
|
||||
DB[(PostgreSQL)]
|
||||
R[Repositories]
|
||||
CTX[DbContext]
|
||||
ID[Idempotency]
|
||||
end
|
||||
|
||||
C --> CMD
|
||||
C --> Q
|
||||
CMD --> B --> V
|
||||
CMD --> AR
|
||||
Q --> R
|
||||
R --> CTX --> DB
|
||||
AR --> DE
|
||||
R --> AR
|
||||
|
||||
style C fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Wallet Service │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ API Layer (Controllers, CQRS) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Domain Layer (Wallet, PointAccount Aggregates) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer (EF Core, Repositories) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ PostgreSQL Database │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Trách Nhiệm Các Lớp
|
||||
## Các Pattern Kiến Trúc
|
||||
|
||||
### 1. Lớp Domain (WalletService.Domain)
|
||||
### Domain-Driven Design (DDD)
|
||||
|
||||
Trái tim của ứng dụng chứa business logic thuần túy. Lớp này:
|
||||
- Có **ZERO** phụ thuộc bên ngoài (ngoại trừ MediatR.Contracts cho events)
|
||||
- Chỉ chứa các class POCO
|
||||
- Triển khai các tactical patterns của DDD
|
||||
- **Aggregates**: Wallet, PointAccount
|
||||
- **Entities**: WalletTransaction, PointTransaction
|
||||
- **Value Objects**: Money, PointBalance
|
||||
- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned
|
||||
|
||||
#### Thành Phần
|
||||
### CQRS Pattern
|
||||
|
||||
| Thành phần | Mục Đích |
|
||||
|------------|----------|
|
||||
| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
|
||||
| **AggregatesModel** | Aggregate roots với entities và value objects |
|
||||
| **Events** | Domain events cho giao tiếp cross-aggregate |
|
||||
| **Exceptions** | Domain exceptions cho vi phạm business rules |
|
||||
|
||||
### 2. Lớp Infrastructure (WalletService.Infrastructure)
|
||||
|
||||
Triển khai kỹ thuật và các mối quan tâm bên ngoài:
|
||||
- Truy cập database (EF Core)
|
||||
- Triển khai repositories
|
||||
- Tích hợp external services
|
||||
|
||||
### 3. Lớp API (WalletService.API)
|
||||
|
||||
Điểm vào ứng dụng và triển khai CQRS:
|
||||
- Controllers để xử lý HTTP
|
||||
- Commands cho các thao tác ghi
|
||||
- Queries cho các thao tác đọc
|
||||
- MediatR behaviors cho cross-cutting concerns
|
||||
|
||||
## Luồng CQRS
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Controller
|
||||
participant MediatR
|
||||
participant LoggingBehavior
|
||||
participant ValidatorBehavior
|
||||
participant TransactionBehavior
|
||||
participant CommandHandler
|
||||
participant Repository
|
||||
participant DbContext
|
||||
|
||||
Client->>Controller: HTTP Request
|
||||
Controller->>MediatR: Send(Command)
|
||||
MediatR->>LoggingBehavior: Handle
|
||||
LoggingBehavior->>ValidatorBehavior: Next()
|
||||
ValidatorBehavior->>TransactionBehavior: Next()
|
||||
TransactionBehavior->>CommandHandler: Next()
|
||||
CommandHandler->>Repository: Add/Update/Delete
|
||||
Repository->>DbContext: SaveEntitiesAsync()
|
||||
DbContext-->>Repository: Success
|
||||
Repository-->>CommandHandler: Result
|
||||
CommandHandler-->>Controller: Response
|
||||
Controller-->>Client: HTTP Response
|
||||
```
|
||||
Commands (Ghi) Queries (Đọc)
|
||||
│ │
|
||||
▼ ▼
|
||||
CreateWallet GetWalletQuery
|
||||
DepositCommand GetTransactionsQuery
|
||||
WithdrawCommand GetPointAccountQuery
|
||||
TransferCommand GetBalanceSummaryQuery
|
||||
EarnPointsCommand
|
||||
SpendPointsCommand
|
||||
```
|
||||
|
||||
## Domain Events
|
||||
## Domain Model
|
||||
|
||||
### Wallet Aggregate
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
AR[Aggregate Root] -->|Phát sinh| DE[Domain Event]
|
||||
DE -->|Dispatch bởi| CTX[DbContext]
|
||||
CTX -->|Publish tới| M[MediatR]
|
||||
M -->|Xử lý bởi| H1[Handler 1]
|
||||
M -->|Xử lý bởi| H2[Handler 2]
|
||||
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DE fill:#f39c12,stroke:#d68910,color:#fff
|
||||
style M fill:#9b59b6,stroke:#7d3c98,color:#fff
|
||||
```
|
||||
|
||||
## Schema Database
|
||||
|
||||
### Sample Aggregate
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
samples {
|
||||
uuid id PK
|
||||
varchar(200) name
|
||||
varchar(1000) description
|
||||
int status_id FK
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
classDiagram
|
||||
class Wallet {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Money Balance
|
||||
+WalletStatus Status
|
||||
+List~WalletTransaction~ Transactions
|
||||
+Deposit(Money, description, ref)
|
||||
+Withdraw(Money, description, ref)
|
||||
+Freeze()
|
||||
+Unfreeze()
|
||||
+Close()
|
||||
}
|
||||
|
||||
sample_statuses {
|
||||
int id PK
|
||||
varchar(50) name
|
||||
class WalletTransaction {
|
||||
+Guid Id
|
||||
+TransactionType Type
|
||||
+Money Amount
|
||||
+Money BalanceAfter
|
||||
+string Description
|
||||
+DateTime CreatedAt
|
||||
}
|
||||
|
||||
samples ||--o{ sample_statuses : has
|
||||
class Money {
|
||||
+decimal Amount
|
||||
+string Currency
|
||||
}
|
||||
|
||||
Wallet "1" --> "*" WalletTransaction
|
||||
Wallet --> Money
|
||||
```
|
||||
|
||||
## Pipeline MediatR
|
||||
|
||||
```
|
||||
Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Log start/end Validate Begin/Commit
|
||||
+ timing với Transaction
|
||||
FluentValidation
|
||||
```
|
||||
|
||||
### Thứ Tự Behaviors
|
||||
|
||||
1. **LoggingBehavior** - Ghi log xử lý request với timing
|
||||
2. **ValidatorBehavior** - Validate request sử dụng FluentValidation
|
||||
3. **TransactionBehavior** - Bao bọc command handlers trong database transactions
|
||||
|
||||
## Xử Lý Lỗi
|
||||
|
||||
### Phân Cấp Exceptions
|
||||
|
||||
```
|
||||
Exception
|
||||
└── DomainException
|
||||
└── SampleDomainException
|
||||
```
|
||||
|
||||
### Problem Details (RFC 7807)
|
||||
|
||||
Tất cả lỗi được trả về theo định dạng Problem Details:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7807",
|
||||
"title": "Lỗi Validation",
|
||||
"status": 400,
|
||||
"detail": "Một hoặc nhiều lỗi validation đã xảy ra.",
|
||||
"errors": {
|
||||
"Name": ["Tên là bắt buộc"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
### PointAccount Aggregate
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
HC[Health Check Endpoint]
|
||||
HC --> |/health/live| L[Liveness]
|
||||
HC --> |/health/ready| R[Readiness]
|
||||
HC --> |/health| F[Full Status]
|
||||
classDiagram
|
||||
class PointAccount {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+int TotalPoints
|
||||
+int AvailablePoints
|
||||
+int PendingPoints
|
||||
+EarnPoints(points, source, desc, expires)
|
||||
+SpendPoints(points, source, desc)
|
||||
+AdjustPoints(points, source, desc)
|
||||
}
|
||||
|
||||
R --> PG[(PostgreSQL)]
|
||||
R --> RD[(Redis)]
|
||||
class PointTransaction {
|
||||
+Guid Id
|
||||
+PointTransactionType Type
|
||||
+int Points
|
||||
+int BalanceAfter
|
||||
+DateTime? ExpiresAt
|
||||
}
|
||||
|
||||
style HC fill:#3498db,stroke:#2980b9,color:#fff
|
||||
style L fill:#2ecc71,stroke:#27ae60,color:#fff
|
||||
style R fill:#f39c12,stroke:#d68910,color:#fff
|
||||
PointAccount "1" --> "*" PointTransaction
|
||||
```
|
||||
|
||||
## Kiến Trúc Deployment
|
||||
## Database Schema
|
||||
|
||||
### Docker Compose (Local)
|
||||
### Các Bảng
|
||||
|
||||
| Bảng | Mô Tả |
|
||||
|------|-------|
|
||||
| `Wallets` | Tài khoản ví người dùng |
|
||||
| `WalletTransactions` | Lịch sử giao dịch ví |
|
||||
| `PointAccounts` | Tài khoản điểm người dùng |
|
||||
| `PointTransactions` | Lịch sử giao dịch điểm |
|
||||
|
||||
### Indexes Chính
|
||||
|
||||
- `IX_Wallets_UserId` - Tra cứu theo user
|
||||
- `IX_WalletTransactions_WalletId` - Lịch sử giao dịch
|
||||
- `IX_PointAccounts_UserId` - Tra cứu theo user
|
||||
|
||||
## API Flow
|
||||
|
||||
### Luồng Nạp Tiền
|
||||
|
||||
```
|
||||
1. Client → POST /api/v1/wallets/deposit
|
||||
2. Controller → DepositCommand (MediatR)
|
||||
3. CommandHandler → Validate số tiền
|
||||
4. Handler → wallet.Deposit(amount)
|
||||
5. Domain → Tạo WalletTransaction + Domain Event
|
||||
6. Infrastructure → Lưu database
|
||||
7. Response → Số dư cập nhật
|
||||
```
|
||||
|
||||
### Luồng Chuyển Khoản
|
||||
|
||||
```
|
||||
1. Client → POST /api/v1/wallets/transfer
|
||||
2. Controller → TransferCommand
|
||||
3. Handler → Lấy ví nguồn
|
||||
4. Handler → Lấy ví đích
|
||||
5. Domain → source.Withdraw() + target.Deposit()
|
||||
6. Infrastructure → Lưu transactional
|
||||
7. Response → Xác nhận chuyển khoản
|
||||
```
|
||||
|
||||
## Tích Hợp Giữa Các Service
|
||||
|
||||
### Tích Hợp IAM Service
|
||||
|
||||
```
|
||||
Wallet Service ──────► IAM Service
|
||||
│
|
||||
▼
|
||||
Xác thực User
|
||||
Kiểm tra JWT
|
||||
```
|
||||
|
||||
### Luồng Xác Thực
|
||||
|
||||
1. Client gửi JWT token trong Authorization header
|
||||
2. Wallet Service xác thực JWT với IAM Service
|
||||
3. Trích xuất userId từ JWT claims
|
||||
4. Xử lý thao tác ví cho user
|
||||
|
||||
## Triển Khai
|
||||
|
||||
### Cấu Hình Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myservice-api:
|
||||
build: .
|
||||
ports: ["5000:8080"]
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
wallet-service:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/wallet-service-net/Dockerfile
|
||||
environment:
|
||||
- DATABASE_URL=${WALLET_DATABASE_URL}
|
||||
- JWT_AUTHORITY=${IAM_SERVICE_URL}
|
||||
labels:
|
||||
- traefik.http.routers.wallet.rule=PathPrefix(`/api/v1/wallets`)
|
||||
```
|
||||
|
||||
### Kubernetes (Production)
|
||||
### Health Checks
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: myservice-api
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: api
|
||||
image: myservice:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/live
|
||||
port: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 8080
|
||||
```
|
||||
| Endpoint | Kiểm Tra |
|
||||
|----------|----------|
|
||||
| `/health/live` | Service đang chạy |
|
||||
| `/health/ready` | Database kết nối |
|
||||
| `/health` | Trạng thái đầy đủ |
|
||||
|
||||
## Cân Nhắc Bảo Mật
|
||||
## Bảo Mật
|
||||
|
||||
1. **Authentication**: JWT Bearer token (cấu hình trong production)
|
||||
2. **Authorization**: Role-based access control
|
||||
3. **Input Validation**: FluentValidation trên tất cả requests
|
||||
4. **SQL Injection**: EF Core parameterized queries
|
||||
5. **Secrets**: Biến môi trường, không bao giờ trong code
|
||||
### Xác Thực
|
||||
- Xác thực JWT Bearer token
|
||||
- Tích hợp IAM Service
|
||||
|
||||
## Tối Ưu Hiệu Năng
|
||||
### Phân Quyền
|
||||
- User chỉ truy cập được ví của mình
|
||||
- Admin endpoints cho system operations
|
||||
|
||||
1. **Connection Pooling**: EF Core với Npgsql connection resilience
|
||||
2. **Async/Await**: Tất cả I/O operations đều async
|
||||
3. **Response Caching**: Thêm caching headers cho queries
|
||||
4. **Database Indexes**: Cấu hình trong EntityConfigurations
|
||||
### Bảo Vệ Dữ Liệu
|
||||
- Lưu số tiền với độ chính xác cao
|
||||
- Audit trail giao dịch
|
||||
- Soft delete cho ví
|
||||
|
||||
## Tài Liệu Tham Khảo
|
||||
## Hiệu Năng
|
||||
|
||||
- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
|
||||
- [Hướng dẫn Kiến trúc .NET Microservices](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
|
||||
- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
|
||||
- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
|
||||
### Tối Ưu
|
||||
- Connection pooling (EF Core)
|
||||
- Index cho các queries thường xuyên
|
||||
- Phân trang cho lịch sử giao dịch
|
||||
|
||||
### Mở Rộng
|
||||
- Horizontal scaling với load balancer
|
||||
- Read replicas cho queries
|
||||
- Redis caching (tương lai)
|
||||
|
||||
## Giám Sát
|
||||
|
||||
### Metrics
|
||||
- Thời gian request
|
||||
- Số lượng giao dịch
|
||||
- Tỷ lệ lỗi
|
||||
|
||||
### Logging
|
||||
- Serilog structured logging
|
||||
- Correlation IDs cho tracing
|
||||
- Tích hợp Seq/Loki
|
||||
|
||||
@@ -1,265 +1,240 @@
|
||||
# Template Microservice .NET 10
|
||||
# Wallet Service .NET
|
||||
|
||||
> Template microservice .NET 10 cấp doanh nghiệp theo các pattern DDD, CQRS và Clean Architecture.
|
||||
> **EN**: Wallet and Point Account management service for GoodGo Platform.
|
||||
> **VI**: Dịch vụ quản lý Ví và Tài khoản Điểm cho GoodGo Platform.
|
||||
|
||||
## Tổng Quan
|
||||
|
||||
Template này cung cấp cấu trúc sẵn sàng production cho microservices .NET dựa trên kiến trúc tham chiếu eShopOnContainers với:
|
||||
Wallet Service cung cấp quản lý ví và điểm thưởng toàn diện:
|
||||
|
||||
- **Domain-Driven Design (DDD)** - Aggregates, Entities, Value Objects, Domain Events
|
||||
- **CQRS Pattern** - Tách biệt Commands (ghi) và Queries (đọc) với MediatR
|
||||
- **Clean Architecture** - Phân tầng Domain, Infrastructure, API
|
||||
- **EF Core 10** - PostgreSQL với connection resilience
|
||||
- **FluentValidation** - Validation request
|
||||
- **API Versioning** - Versioning theo URL segment
|
||||
- **Health Checks** - Probes sẵn sàng cho Kubernetes
|
||||
- **Structured Logging** - Serilog với console và Seq
|
||||
- **Quản Lý Ví** - Tạo, nạp tiền, rút tiền, chuyển khoản
|
||||
- **Tài Khoản Điểm** - Tích, tiêu và theo dõi điểm thưởng
|
||||
- **Lịch Sử Giao Dịch** - Audit trail đầy đủ các giao dịch
|
||||
- **Hỗ Trợ Đa Tiền Tệ** - Mặc định VND với hỗ trợ đa tiền tệ
|
||||
- **Domain-Driven Design** - Clean Architecture với CQRS pattern
|
||||
|
||||
## Yêu Cầu
|
||||
## Tech Stack
|
||||
|
||||
| Yêu cầu | Phiên bản |
|
||||
|---------|-----------|
|
||||
| .NET SDK | 10.0.101+ |
|
||||
| Docker | 24.0+ |
|
||||
| PostgreSQL | 15+ (hoặc dùng Docker) |
|
||||
| Thành Phần | Công Nghệ |
|
||||
|------------|-----------|
|
||||
| Framework | .NET 10 |
|
||||
| Database | PostgreSQL (EF Core) |
|
||||
| CQRS | MediatR |
|
||||
| Validation | FluentValidation |
|
||||
| API Docs | Swagger/OpenAPI |
|
||||
| Logging | Serilog |
|
||||
|
||||
## Yêu Cầu Hệ Thống
|
||||
|
||||
```bash
|
||||
# Kiểm tra phiên bản .NET
|
||||
dotnet --version
|
||||
# Kết quả nên là: 10.0.xxx
|
||||
dotnet --version # Phải là 10.0.x
|
||||
```
|
||||
|
||||
## Bắt Đầu Nhanh
|
||||
|
||||
### 1. Tạo Service Mới
|
||||
### 1. Cấu Hình Môi Trường
|
||||
|
||||
```bash
|
||||
# Sao chép template sang service mới
|
||||
cp -r services/_template_dot_net services/your-service-name
|
||||
|
||||
# Di chuyển đến thư mục service
|
||||
cd services/your-service-name
|
||||
|
||||
# Đổi tên tất cả "WalletService" thành "YourService"
|
||||
find . -type f -name "*.cs" -exec sed -i '' 's/WalletService/YourService/g' {} +
|
||||
find . -type f -name "*.csproj" -exec sed -i '' 's/WalletService/YourService/g' {} +
|
||||
```
|
||||
|
||||
### 2. Cấu Hình Môi Trường
|
||||
|
||||
```bash
|
||||
# Sao chép template môi trường
|
||||
cp .env.example .env
|
||||
|
||||
# Chỉnh sửa với cấu hình của bạn
|
||||
nano .env
|
||||
# Chỉnh sửa .env với connection database của bạn
|
||||
```
|
||||
|
||||
### 3. Chạy với Docker
|
||||
### 2. Chạy với Docker
|
||||
|
||||
```bash
|
||||
# Khởi động tất cả services (API + PostgreSQL + Redis)
|
||||
docker-compose up -d
|
||||
|
||||
# Xem logs
|
||||
docker-compose logs -f myservice-api
|
||||
cd deployments/local
|
||||
docker-compose up wallet-service -d
|
||||
```
|
||||
|
||||
### 4. Chạy Local
|
||||
### 3. Chạy Local
|
||||
|
||||
```bash
|
||||
# Khôi phục dependencies
|
||||
cd services/wallet-service-net
|
||||
dotnet restore
|
||||
|
||||
# Build tất cả projects
|
||||
dotnet build
|
||||
|
||||
# Chạy API
|
||||
dotnet run --project src/WalletService.API
|
||||
```
|
||||
|
||||
## Cấu Trúc Dự Án
|
||||
## API Endpoints
|
||||
|
||||
```
|
||||
_template_dot_net/
|
||||
├── src/
|
||||
│ ├── WalletService.API/ # Lớp Presentation (Controllers, CQRS)
|
||||
│ │ ├── Controllers/ # Các API endpoints
|
||||
│ │ ├── Application/ # Triển khai CQRS
|
||||
│ │ │ ├── Commands/ # Thao tác ghi (MediatR)
|
||||
│ │ │ ├── Queries/ # Thao tác đọc
|
||||
│ │ │ ├── Behaviors/ # MediatR pipeline behaviors
|
||||
│ │ │ └── Validations/ # FluentValidation validators
|
||||
│ │ ├── Middleware/ # Custom middleware
|
||||
│ │ └── Program.cs # Điểm vào ứng dụng
|
||||
│ │
|
||||
│ ├── WalletService.Domain/ # Lớp Domain (Business logic thuần túy)
|
||||
│ │ ├── AggregatesModel/ # Aggregate roots và entities
|
||||
│ │ ├── Events/ # Domain events
|
||||
│ │ ├── Exceptions/ # Domain exceptions
|
||||
│ │ └── SeedWork/ # Base classes (Entity, ValueObject, etc.)
|
||||
│ │
|
||||
│ └── WalletService.Infrastructure/ # Lớp Infrastructure (Truy cập dữ liệu)
|
||||
│ ├── EntityConfigurations/ # Cấu hình EF Core Fluent API
|
||||
│ ├── Repositories/ # Triển khai repositories
|
||||
│ ├── Idempotency/ # Xử lý idempotency request
|
||||
│ └── WalletServiceContext.cs # DbContext với Unit of Work
|
||||
│
|
||||
├── tests/
|
||||
│ ├── WalletService.UnitTests/ # Unit tests (Domain, Application)
|
||||
│ └── WalletService.FunctionalTests/ # Integration tests (API endpoints)
|
||||
│
|
||||
├── Dockerfile # Multi-stage Docker build
|
||||
├── docker-compose.yml # Thiết lập phát triển local
|
||||
├── global.json # Pin phiên bản .NET SDK
|
||||
└── Directory.Build.props # Thuộc tính MSBuild chung
|
||||
```
|
||||
|
||||
## Các Endpoint API
|
||||
### Wallet APIs
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
|--------|----------|-------|
|
||||
| `GET` | `/api/v1/samples` | Lấy tất cả samples |
|
||||
| `GET` | `/api/v1/samples/{id}` | Lấy sample theo ID |
|
||||
| `POST` | `/api/v1/samples` | Tạo sample mới |
|
||||
| `PUT` | `/api/v1/samples/{id}` | Cập nhật sample |
|
||||
| `DELETE` | `/api/v1/samples/{id}` | Xóa sample |
|
||||
| `PATCH` | `/api/v1/samples/{id}/status` | Thay đổi trạng thái |
|
||||
| `POST` | `/api/v1/wallets` | Tạo ví mới |
|
||||
| `GET` | `/api/v1/wallets/me` | Lấy ví của người dùng hiện tại |
|
||||
| `GET` | `/api/v1/wallets/{id}` | Lấy ví theo ID |
|
||||
| `POST` | `/api/v1/wallets/deposit` | Nạp tiền vào ví |
|
||||
| `POST` | `/api/v1/wallets/withdraw` | Rút tiền từ ví |
|
||||
| `POST` | `/api/v1/wallets/transfer` | Chuyển tiền giữa các ví |
|
||||
| `POST` | `/api/v1/wallets/{id}/freeze` | Đóng băng ví |
|
||||
| `POST` | `/api/v1/wallets/{id}/unfreeze` | Mở đóng băng ví |
|
||||
| `GET` | `/api/v1/wallets/transactions` | Lấy lịch sử giao dịch |
|
||||
|
||||
### Points APIs
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
|--------|----------|-------|
|
||||
| `GET` | `/api/v1/points/me` | Lấy điểm của người dùng |
|
||||
| `POST` | `/api/v1/points/earn` | Tích điểm |
|
||||
| `POST` | `/api/v1/points/spend` | Tiêu điểm |
|
||||
| `GET` | `/api/v1/points/transactions` | Lấy lịch sử điểm |
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
| Endpoint | Mục Đích |
|
||||
|----------|----------|
|
||||
| `/health` | Trạng thái health đầy đủ |
|
||||
| `/health/live` | Kiểm tra sống |
|
||||
| `/health/ready` | Kiểm tra sẵn sàng |
|
||||
| `/health` | Trạng thái sức khỏe đầy đủ |
|
||||
| `/health/live` | Liveness probe (K8s) |
|
||||
| `/health/ready` | Readiness probe (K8s) |
|
||||
|
||||
## Pattern CQRS
|
||||
## Cấu Trúc Dự Án
|
||||
|
||||
### Commands (Thao Tác Ghi)
|
||||
|
||||
```csharp
|
||||
// Định nghĩa command
|
||||
public record CreateSampleCommand(string Name, string? Description)
|
||||
: IRequest<CreateSampleCommandResult>;
|
||||
|
||||
// Xử lý command
|
||||
public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
|
||||
{
|
||||
public async Task<CreateSampleCommandResult> Handle(CreateSampleCommand request, CancellationToken ct)
|
||||
{
|
||||
var sample = new Sample(request.Name, request.Description);
|
||||
_repository.Add(sample);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(ct);
|
||||
return new CreateSampleCommandResult(sample.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Queries (Thao Tác Đọc)
|
||||
|
||||
```csharp
|
||||
// Định nghĩa query
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
wallet-service-net/
|
||||
├── src/
|
||||
│ ├── WalletService.API/ # API Layer
|
||||
│ │ ├── Controllers/ # REST endpoints
|
||||
│ │ └── Application/ # Commands & Queries
|
||||
│ │ ├── Commands/ # Thao tác ghi
|
||||
│ │ └── Queries/ # Thao tác đọc
|
||||
│ │
|
||||
│ ├── WalletService.Domain/ # Domain Layer
|
||||
│ │ ├── AggregatesModel/
|
||||
│ │ │ ├── WalletAggregate/ # Wallet, Transaction, Money
|
||||
│ │ │ └── PointAccountAggregate/ # Points, PointTransaction
|
||||
│ │ ├── Events/ # Domain events
|
||||
│ │ └── Exceptions/ # Domain exceptions
|
||||
│ │
|
||||
│ └── WalletService.Infrastructure/ # Infrastructure Layer
|
||||
│ ├── EntityConfigurations/ # EF Core mappings
|
||||
│ ├── Repositories/ # Data access
|
||||
│ └── WalletServiceContext.cs # DbContext
|
||||
│
|
||||
├── tests/
|
||||
│ ├── WalletService.UnitTests/ # Domain & Logic tests
|
||||
│ └── WalletService.FunctionalTests/ # API integration tests
|
||||
│
|
||||
├── docs/
|
||||
│ ├── en/ # Tài liệu tiếng Anh
|
||||
│ └── vi/ # Tài liệu tiếng Việt
|
||||
│
|
||||
└── Dockerfile
|
||||
```
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Aggregate Root
|
||||
### Wallet Aggregate
|
||||
|
||||
```csharp
|
||||
public class Sample : Entity, IAggregateRoot
|
||||
{
|
||||
public string Name => _name;
|
||||
public SampleStatus Status => _status;
|
||||
|
||||
public Sample(string name, string? description) {
|
||||
// Validation business logic
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new SampleDomainException("Tên sample không được để trống");
|
||||
|
||||
// Domain event
|
||||
AddDomainEvent(new SampleCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
public void Activate() {
|
||||
if (_status != SampleStatus.Draft)
|
||||
throw new SampleDomainException("Chỉ sample draft mới có thể kích hoạt");
|
||||
// Chuyển đổi trạng thái
|
||||
}
|
||||
}
|
||||
// Tạo ví
|
||||
var wallet = new Wallet(userId, "VND");
|
||||
|
||||
// Nạp tiền
|
||||
wallet.Deposit(new Money(1000000m, "VND"), "Lương", "REF001");
|
||||
|
||||
// Rút tiền
|
||||
wallet.Withdraw(new Money(500000m, "VND"), "Mua sắm", "REF002");
|
||||
|
||||
// Đóng băng/Mở đóng băng
|
||||
wallet.Freeze();
|
||||
wallet.Unfreeze();
|
||||
```
|
||||
|
||||
## Kiểm Thử
|
||||
### Point Account Aggregate
|
||||
|
||||
```csharp
|
||||
// Tạo tài khoản điểm
|
||||
var account = new PointAccount(userId);
|
||||
|
||||
// Tích điểm
|
||||
account.EarnPoints(100, "ORDER001", "Thưởng mua hàng", expiresAt);
|
||||
|
||||
// Tiêu điểm
|
||||
account.SpendPoints(50, "ORDER002", "Đổi giảm giá");
|
||||
|
||||
// Điều chỉnh điểm (admin)
|
||||
account.AdjustPoints(10, "ADMIN", "Điều chỉnh thưởng");
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Chạy tất cả tests
|
||||
dotnet test
|
||||
|
||||
# Chạy với coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
|
||||
dotnet test /p:CollectCoverage=true
|
||||
|
||||
# Chạy project test cụ thể
|
||||
# Chỉ unit tests
|
||||
dotnet test tests/WalletService.UnitTests
|
||||
|
||||
# Chỉ functional tests
|
||||
dotnet test tests/WalletService.FunctionalTests
|
||||
```
|
||||
|
||||
### Kết Quả Test
|
||||
- **23 tests** tổng cộng
|
||||
- 20 unit tests (Wallet, PointAccount domain)
|
||||
- 3 functional tests (Health endpoints)
|
||||
|
||||
## Cấu Hình
|
||||
|
||||
### Biến Môi Trường
|
||||
|
||||
| Biến | Mô Tả | Mặc định |
|
||||
| Biến | Mô Tả | Bắt Buộc |
|
||||
|------|-------|----------|
|
||||
| `ASPNETCORE_ENVIRONMENT` | Tên môi trường | `Development` |
|
||||
| `DATABASE_URL` | Connection string PostgreSQL | - |
|
||||
| `REDIS_URL` | Connection string Redis | - |
|
||||
| `JWT_SECRET` | Secret ký JWT (tối thiểu 32 ký tự) | - |
|
||||
| `DATABASE_URL` | Connection PostgreSQL | Có |
|
||||
| `ASPNETCORE_ENVIRONMENT` | Môi trường | Không (mặc định: Development) |
|
||||
| `JWT_AUTHORITY` | URL JWT issuer | Có (cho auth) |
|
||||
|
||||
### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Database=myservice;Username=postgres;Password=postgres"
|
||||
"DefaultConnection": "Host=localhost;Database=wallet_db;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": "Information"
|
||||
"Jwt": {
|
||||
"Authority": "http://localhost:5001",
|
||||
"Issuer": "http://localhost:5001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
```bash
|
||||
# Tạo migration
|
||||
dotnet ef migrations add InitialCreate \
|
||||
--project src/WalletService.Infrastructure \
|
||||
--startup-project src/WalletService.API
|
||||
|
||||
# Áp dụng migration
|
||||
dotnet ef database update \
|
||||
--project src/WalletService.Infrastructure \
|
||||
--startup-project src/WalletService.API
|
||||
```
|
||||
|
||||
## Triển Khai
|
||||
|
||||
### Docker Build
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
docker build -t myservice:latest .
|
||||
|
||||
# Chạy container
|
||||
docker run -p 5000:8080 --env-file .env myservice:latest
|
||||
docker build -t goodgo/wallet-service:latest .
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
### Docker Compose
|
||||
|
||||
Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để biết manifests triển khai Kubernetes.
|
||||
|
||||
## Có Gì Mới Trong .NET 10
|
||||
|
||||
- Tính năng ngôn ngữ **C# 14**
|
||||
- Hỗ trợ **Native AOT** được cải thiện
|
||||
- Hiệu suất **async/await** tốt hơn
|
||||
- **JSON serialization** được nâng cao
|
||||
- Cải thiện hiệu suất toàn diện
|
||||
- Hỗ trợ **LTS** 3 năm (đến tháng 11/2028)
|
||||
Service được đăng ký trong `deployments/local/docker-compose.yml` với Traefik routing.
|
||||
|
||||
## Tài Nguyên
|
||||
|
||||
- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) - Kiến trúc tham chiếu
|
||||
- [Tài liệu .NET 10](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10)
|
||||
- [DDD với .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)
|
||||
- [MediatR](https://github.com/jbogard/MediatR) - Thư viện CQRS
|
||||
- [FluentValidation](https://docs.fluentvalidation.net/) - Thư viện validation
|
||||
- [Tài liệu Kiến trúc](./ARCHITECTURE.md)
|
||||
- [Tài liệu GoodGo Platform](../../docs/)
|
||||
- [eShopOnContainers Reference](https://github.com/dotnet-architecture/eShopOnContainers)
|
||||
|
||||
## Giấy Phép
|
||||
|
||||
Độc quyền - GoodGo Platform
|
||||
Proprietary - GoodGo Platform
|
||||
|
||||
Reference in New Issue
Block a user