feat: Triển khai hệ thống cấp độ và kinh nghiệm (EXP) cho thành viên, bao gồm LevelDefinition aggregate và tích hợp sự kiện lên cấp với Wallet Service.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
## System Overview
|
||||
|
||||
Membership Service manages user membership data. Profile information is stored in IAM Service.
|
||||
Membership Service manages user membership data including **Level**, **Experience (EXP)**, and **Preferences**. Profile information is stored in IAM Service.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
@@ -17,8 +17,9 @@ graph TB
|
||||
|
||||
subgraph Services["Microservices"]
|
||||
IAM["🔐 IAM Service<br/>(User & Profile)"]
|
||||
MEMBER["👥 Membership Service<br/>(Membership Data)"]
|
||||
MEMBER["👥 Membership Service<br/>(Level, EXP, Preferences)"]
|
||||
STORAGE["📦 Storage Service"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
end
|
||||
|
||||
subgraph Data["Data Layer"]
|
||||
@@ -34,17 +35,20 @@ graph TB
|
||||
IAM --> |"JWT Token"| MEMBER
|
||||
MEMBER --> |"HTTP Client"| IAM
|
||||
MEMBER --> PG
|
||||
MEMBER -.-> |"Level Up Event"| WALLET
|
||||
|
||||
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style STORAGE fill:#eab308,stroke:#a16207,color:#000
|
||||
style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
|
||||
style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Responsibility Separation:**
|
||||
> - **IAM Service**: Manages User identity and UserProfile (avatar, phone, address, DOB)
|
||||
> - **Membership Service**: Manages membership data (membership level, preferences, country code, gender)
|
||||
> - **Membership Service**: Manages membership data (level, experience, preferences, country code, gender)
|
||||
> - **Wallet Service**: Receives Level Up events to process rewards
|
||||
|
||||
## Clean Architecture Layers
|
||||
|
||||
@@ -55,6 +59,7 @@ graph TB
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Domain Layer (Core) │
|
||||
│ Entities, Aggregates, Value Objects, Domain Events │
|
||||
│ Member Aggregate, LevelDefinition Aggregate │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer │
|
||||
│ DbContext, Repositories, Entity Configurations │
|
||||
@@ -64,7 +69,7 @@ graph TB
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Aggregate Structure
|
||||
### Aggregates Overview
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
@@ -73,37 +78,115 @@ classDiagram
|
||||
+Guid UserId
|
||||
+string CountryCode
|
||||
+string? Gender
|
||||
+int MembershipLevelId
|
||||
+MembershipLevel MembershipLevel
|
||||
+int CurrentLevel
|
||||
+int CurrentExp
|
||||
+int TotalExpEarned
|
||||
+string? Preferences
|
||||
+bool IsDeleted
|
||||
+DateTime CreatedAt
|
||||
+DateTime UpdatedAt
|
||||
+AddExperience(points, levelRules)
|
||||
+UpdateGender()
|
||||
+UpdateCountryCode()
|
||||
+UpdatePreferences()
|
||||
+ChangeMembershipLevel()
|
||||
+Delete()
|
||||
+Restore()
|
||||
}
|
||||
|
||||
class MembershipLevel {
|
||||
<<enumeration>>
|
||||
+int Id
|
||||
class LevelDefinition {
|
||||
+Guid Id
|
||||
+int LevelNumber
|
||||
+string Name
|
||||
+Free = 1
|
||||
+Basic = 2
|
||||
+Premium = 3
|
||||
+int RequiredExp
|
||||
+string? Description
|
||||
+string? IconUrl
|
||||
+string? Benefits
|
||||
+bool IsActive
|
||||
+DateTime CreatedAt
|
||||
+DateTime UpdatedAt
|
||||
}
|
||||
|
||||
Member --> MembershipLevel
|
||||
Member ..> LevelDefinition : uses rules
|
||||
|
||||
style Member fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style MembershipLevel fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style LevelDefinition fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
```
|
||||
|
||||
### Member Aggregate (Rich Domain Model)
|
||||
|
||||
```csharp
|
||||
public class Member : Entity, IAggregateRoot
|
||||
{
|
||||
// Private fields for encapsulation
|
||||
private int _currentLevel;
|
||||
private int _currentExp;
|
||||
private int _totalExpEarned;
|
||||
|
||||
public int CurrentLevel => _currentLevel;
|
||||
public int CurrentExp => _currentExp;
|
||||
public int TotalExpEarned => _totalExpEarned;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Add experience and automatically level up if thresholds are met.
|
||||
/// VI: Thêm exp và tự động lên level nếu đạt ngưỡng.
|
||||
/// </summary>
|
||||
public void AddExperience(int points, IReadOnlyList<LevelDefinition> levelRules)
|
||||
{
|
||||
if (points <= 0)
|
||||
throw new DomainException("EXP points must be positive");
|
||||
|
||||
int oldLevel = _currentLevel;
|
||||
_currentExp += points;
|
||||
_totalExpEarned += points;
|
||||
_updatedAt = DateTime.UtcNow;
|
||||
|
||||
// Business logic: Calculate new level
|
||||
var newLevel = CalculateLevel(_currentExp, levelRules);
|
||||
|
||||
if (newLevel > oldLevel)
|
||||
{
|
||||
_currentLevel = newLevel;
|
||||
// Raise domain event for side effects
|
||||
AddDomainEvent(new MemberLevelUpDomainEvent(this, oldLevel, newLevel));
|
||||
}
|
||||
|
||||
AddDomainEvent(new MemberExperienceAddedDomainEvent(this, points));
|
||||
}
|
||||
|
||||
private static int CalculateLevel(int exp, IReadOnlyList<LevelDefinition> rules)
|
||||
{
|
||||
return rules
|
||||
.Where(r => r.IsActive && r.RequiredExp <= exp)
|
||||
.OrderByDescending(r => r.LevelNumber)
|
||||
.FirstOrDefault()?.LevelNumber ?? 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Member entity does not store profile fields (phone, avatar, address, DOB). These fields are managed by IAM Service's UserProfile.
|
||||
> **Rich Domain Model**: Business logic for EXP and Level is encapsulated inside the Member entity, not in the Service Layer. This avoids the "Anemic Domain Model" anti-pattern.
|
||||
|
||||
### LevelDefinition Aggregate (Customizable Levels)
|
||||
|
||||
```csharp
|
||||
public class LevelDefinition : Entity, IAggregateRoot
|
||||
{
|
||||
public int LevelNumber { get; private set; } // 1, 2, 3...
|
||||
public string Name { get; private set; } // "Bronze", "Silver", "Gold"
|
||||
public int RequiredExp { get; private set; } // 0, 100, 300, 600...
|
||||
public string? Description { get; private set; }
|
||||
public string? IconUrl { get; private set; }
|
||||
public string? Benefits { get; private set; } // JSON benefits
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
public void UpdateRequiredExp(int newRequiredExp)
|
||||
{
|
||||
if (newRequiredExp < 0)
|
||||
throw new DomainException("Required EXP cannot be negative");
|
||||
RequiredExp = newRequiredExp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Events
|
||||
|
||||
@@ -111,53 +194,102 @@ classDiagram
|
||||
|-------|---------|---------|
|
||||
| `MemberCreatedDomainEvent` | New member created | Welcome email, analytics |
|
||||
| `MemberUpdatedDomainEvent` | Profile updated | Sync to other services |
|
||||
| `MembershipLevelChangedDomainEvent` | Level changed | Billing, benefits update |
|
||||
| `MemberExperienceAddedDomainEvent` | EXP received | Logging, analytics |
|
||||
| `MemberLevelUpDomainEvent` | Level up | **Rewards, notifications, unlock features** |
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Handler
|
||||
participant Member
|
||||
participant EventDispatcher
|
||||
participant WalletService
|
||||
|
||||
Client->>Handler: AddExperienceCommand
|
||||
Handler->>Member: AddExperience(100, rules)
|
||||
Member->>Member: _currentExp += 100
|
||||
Member->>Member: CalculateLevel()
|
||||
|
||||
alt Level Up
|
||||
Member->>Member: AddDomainEvent(LevelUpEvent)
|
||||
end
|
||||
|
||||
Handler->>EventDispatcher: SaveEntitiesAsync()
|
||||
EventDispatcher->>WalletService: Publish LevelUpEvent
|
||||
WalletService-->>WalletService: Grant Rewards
|
||||
```
|
||||
|
||||
## CQRS Pattern
|
||||
|
||||
### Write Side (Commands)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Commands["✏️ Write Side"]
|
||||
CMD1["CreateMemberCommand"]
|
||||
CMD2["UpdateMemberProfileCommand"]
|
||||
CMD3["ChangeMembershipLevelCommand"]
|
||||
subgraph Commands["✏️ Commands"]
|
||||
CMD1["AddExperienceCommand"]
|
||||
CMD2["CreateMemberCommand"]
|
||||
CMD3["UpdateMemberProfileCommand"]
|
||||
CMD4["CreateLevelDefinitionCommand"]
|
||||
end
|
||||
|
||||
subgraph Handlers["⚙️ Command Handlers"]
|
||||
H1["CreateMemberHandler"]
|
||||
H2["UpdateProfileHandler"]
|
||||
H3["ChangeLevelHandler"]
|
||||
H1["AddExperienceHandler"]
|
||||
H2["CreateMemberHandler"]
|
||||
H3["UpdateProfileHandler"]
|
||||
H4["CreateLevelHandler"]
|
||||
end
|
||||
|
||||
subgraph Queries["📖 Read Side"]
|
||||
Q1["GetMemberByIdQuery"]
|
||||
Q2["GetMembersQuery"]
|
||||
end
|
||||
|
||||
subgraph QueryHandlers["🔍 Query Handlers"]
|
||||
QH1["GetMemberByIdHandler"]
|
||||
QH2["GetMembersHandler"]
|
||||
subgraph Domain["🏛️ Domain Model"]
|
||||
DM["Rich Domain Model<br/>EF Core"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
CMD1 --> H1 --> DB
|
||||
CMD2 --> H2 --> DB
|
||||
CMD3 --> H3 --> DB
|
||||
|
||||
Q1 --> QH1 --> DB
|
||||
Q2 --> QH2 --> DB
|
||||
CMD1 --> H1 --> DM --> DB
|
||||
CMD2 --> H2 --> DM
|
||||
CMD3 --> H3 --> DM
|
||||
CMD4 --> H4 --> DM
|
||||
|
||||
style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD4 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
```
|
||||
|
||||
### Read Side (Queries)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Queries["📖 Queries"]
|
||||
Q1["GetMemberByIdQuery"]
|
||||
Q2["GetMemberProgressQuery"]
|
||||
Q3["GetLevelDefinitionsQuery"]
|
||||
end
|
||||
|
||||
subgraph Handlers["🔍 Query Handlers"]
|
||||
QH1["GetMemberByIdHandler"]
|
||||
QH2["GetMemberProgressHandler"]
|
||||
QH3["GetLevelDefinitionsHandler"]
|
||||
end
|
||||
|
||||
subgraph ReadModel["📊 Read Model"]
|
||||
RM["Dapper / Raw SQL"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
Q1 --> QH1 --> RM --> DB
|
||||
Q2 --> QH2 --> RM
|
||||
Q3 --> QH3 --> RM
|
||||
|
||||
style Q1 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q2 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q3 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
```
|
||||
|
||||
## Request Flow
|
||||
|
||||
### Create Member Flow
|
||||
### Add Experience Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -165,23 +297,58 @@ sequenceDiagram
|
||||
participant Controller
|
||||
participant MediatR
|
||||
participant Handler
|
||||
participant Repository
|
||||
participant MemberRepo
|
||||
participant LevelRepo
|
||||
participant DB
|
||||
|
||||
Client->>Controller: POST /api/v1/members
|
||||
Controller->>MediatR: Send(CreateMemberCommand)
|
||||
Client->>Controller: POST /api/v1/members/{id}/experience
|
||||
Note over Client,Controller: { "points": 100, "source": "purchase" }
|
||||
|
||||
Controller->>MediatR: Send(AddExperienceCommand)
|
||||
MediatR->>Handler: Handle(command)
|
||||
Handler->>Repository: ExistsByUserIdAsync(userId)
|
||||
Repository->>DB: SELECT EXISTS
|
||||
DB-->>Repository: false
|
||||
Handler->>Repository: Add(member)
|
||||
Handler->>Repository: SaveEntitiesAsync()
|
||||
Repository->>DB: INSERT + Domain Events
|
||||
DB-->>Repository: OK
|
||||
Repository-->>Handler: member
|
||||
Handler-->>MediatR: CreateMemberResult
|
||||
MediatR-->>Controller: result
|
||||
Controller-->>Client: 201 Created
|
||||
|
||||
Handler->>MemberRepo: GetAsync(memberId)
|
||||
MemberRepo->>DB: SELECT member
|
||||
DB-->>MemberRepo: member data
|
||||
|
||||
Handler->>LevelRepo: GetAllActiveAsync()
|
||||
LevelRepo->>DB: SELECT level_definitions
|
||||
DB-->>LevelRepo: level rules
|
||||
|
||||
Handler->>Handler: member.AddExperience(100, rules)
|
||||
Note over Handler: Business logic in Domain
|
||||
|
||||
Handler->>MemberRepo: UnitOfWork.SaveEntitiesAsync()
|
||||
MemberRepo->>DB: UPDATE + Dispatch Domain Events
|
||||
|
||||
DB-->>Handler: OK
|
||||
Handler-->>Controller: AddExperienceResult
|
||||
Controller-->>Client: 200 OK
|
||||
Note over Client,Controller: { "currentExp": 150, "currentLevel": 2, "leveledUp": true }
|
||||
```
|
||||
|
||||
### Get Member Progress Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Controller
|
||||
participant Handler
|
||||
participant Dapper
|
||||
participant DB
|
||||
|
||||
Client->>Controller: GET /api/v1/members/{id}/progress
|
||||
Controller->>Handler: GetMemberProgressQuery
|
||||
Handler->>Dapper: Raw SQL Query
|
||||
|
||||
Note over Dapper,DB: SELECT m.current_exp, m.current_level,<br/>l.name, l.required_exp, next_level.required_exp<br/>FROM members m<br/>JOIN level_definitions l ON ...
|
||||
|
||||
Dapper->>DB: Execute
|
||||
DB-->>Dapper: Result
|
||||
Dapper-->>Handler: MemberProgressDto
|
||||
Handler-->>Controller: Result
|
||||
Controller-->>Client: 200 OK
|
||||
Note over Client,Controller: { "currentLevel": 2, "levelName": "Silver",<br/>"currentExp": 150, "expToNextLevel": 150,<br/>"progressPercent": 50 }
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
@@ -192,35 +359,107 @@ erDiagram
|
||||
uuid id PK "Same as IAM UserId"
|
||||
varchar country_code "ISO 3166-1 alpha-2"
|
||||
varchar gender "male/female/other"
|
||||
int membership_level_id FK
|
||||
int current_level "Current member level"
|
||||
int current_exp "Current experience points"
|
||||
int total_exp_earned "Total EXP ever earned"
|
||||
jsonb preferences "User preferences"
|
||||
boolean is_deleted "Soft delete flag"
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
MEMBERSHIP_LEVELS {
|
||||
int id PK
|
||||
varchar name
|
||||
LEVEL_DEFINITIONS {
|
||||
uuid id PK
|
||||
int level_number UK "Level 1, 2, 3..."
|
||||
varchar name "Bronze, Silver, Gold..."
|
||||
int required_exp "EXP needed for this level"
|
||||
text description
|
||||
varchar icon_url
|
||||
jsonb benefits "Level benefits/rewards"
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
MEMBERS }o--|| MEMBERSHIP_LEVELS : has
|
||||
MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references"
|
||||
```
|
||||
|
||||
### SQL Schema
|
||||
|
||||
```sql
|
||||
-- Level Definitions table (Admin configurable)
|
||||
CREATE TABLE level_definitions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
level_number INT NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
required_exp INT NOT NULL DEFAULT 0,
|
||||
description TEXT,
|
||||
icon_url VARCHAR(500),
|
||||
benefits JSONB,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Members table
|
||||
CREATE TABLE members (
|
||||
id UUID PRIMARY KEY,
|
||||
country_code VARCHAR(2) NOT NULL DEFAULT 'VN',
|
||||
gender VARCHAR(10),
|
||||
current_level INT NOT NULL DEFAULT 1,
|
||||
current_exp INT NOT NULL DEFAULT 0,
|
||||
total_exp_earned INT NOT NULL DEFAULT 0,
|
||||
preferences JSONB,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX ix_members_current_level ON members(current_level);
|
||||
CREATE INDEX ix_members_current_exp ON members(current_exp);
|
||||
CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number);
|
||||
CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active);
|
||||
|
||||
-- Default Level Definitions
|
||||
INSERT INTO level_definitions (level_number, name, required_exp, description, is_active)
|
||||
VALUES
|
||||
(1, 'Bronze', 0, 'Starting level', true),
|
||||
(2, 'Silver', 100, 'Reach 100 EXP', true),
|
||||
(3, 'Gold', 300, 'Reach 300 EXP', true),
|
||||
(4, 'Platinum', 600, 'Reach 600 EXP', true),
|
||||
(5, 'Diamond', 1000, 'Reach 1000 EXP', true);
|
||||
```
|
||||
|
||||
## API Design
|
||||
|
||||
### RESTful Endpoints
|
||||
|
||||
#### Member Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/members # List (paginated, searchable)
|
||||
GET /api/v1/members/{id} # Get by ID
|
||||
GET /api/v1/members/me # Get current user
|
||||
GET /api/v1/members/{id}/progress # Get level progress
|
||||
POST /api/v1/members # Create
|
||||
POST /api/v1/members/{id}/experience # Add EXP
|
||||
PUT /api/v1/members/{id} # Update profile
|
||||
PUT /api/v1/members/{id}/level # Change level
|
||||
```
|
||||
|
||||
### Response Format
|
||||
#### Level Endpoints (Admin)
|
||||
|
||||
```
|
||||
GET /api/v1/levels # List level definitions
|
||||
GET /api/v1/levels/{id} # Get level definition
|
||||
POST /api/v1/levels # Create level definition (Admin)
|
||||
PUT /api/v1/levels/{id} # Update level definition (Admin)
|
||||
DELETE /api/v1/levels/{id} # Deactivate level (Admin)
|
||||
```
|
||||
|
||||
### Response Formats
|
||||
|
||||
#### Member Response
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -228,76 +467,113 @@ PUT /api/v1/members/{id}/level # Change level
|
||||
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"countryCode": "VN",
|
||||
"gender": "male",
|
||||
"preferences": "{\"theme\": \"dark\", \"language\": \"vi\"}",
|
||||
"membershipLevel": {
|
||||
"id": 1,
|
||||
"name": "Free"
|
||||
},
|
||||
"currentLevel": 2,
|
||||
"currentExp": 150,
|
||||
"totalExpEarned": 150,
|
||||
"preferences": "{\"theme\": \"dark\"}",
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Member Progress Response
|
||||
|
||||
```json
|
||||
{
|
||||
"memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"currentLevel": 2,
|
||||
"levelName": "Silver",
|
||||
"levelIconUrl": "/icons/silver.png",
|
||||
"currentExp": 150,
|
||||
"expToNextLevel": 150,
|
||||
"nextLevelRequiredExp": 300,
|
||||
"progressPercent": 50.0
|
||||
}
|
||||
```
|
||||
|
||||
#### Add Experience Response
|
||||
|
||||
```json
|
||||
{
|
||||
"currentExp": 250,
|
||||
"currentLevel": 2,
|
||||
"totalExpEarned": 250,
|
||||
"leveledUp": false,
|
||||
"newLevel": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Level Definition Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a1b2c3d4-...",
|
||||
"levelNumber": 3,
|
||||
"name": "Gold",
|
||||
"requiredExp": 300,
|
||||
"description": "Reach 300 EXP to unlock Gold status",
|
||||
"iconUrl": "/icons/gold.png",
|
||||
"benefits": {
|
||||
"discountPercent": 10,
|
||||
"prioritySupport": true,
|
||||
"freeShipping": false
|
||||
},
|
||||
"isActive": true
|
||||
}
|
||||
```
|
||||
|
||||
## Inter-Service Communication
|
||||
|
||||
### IAM Service Client
|
||||
|
||||
Membership Service communicates with IAM Service through HTTP Client:
|
||||
Membership Service communicates with IAM Service through HTTP Client to validate users.
|
||||
|
||||
### Integration Events (Planned)
|
||||
|
||||
When a member levels up, publish Integration Event for other services to process:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Membership
|
||||
participant IamClient["IIamServiceClient"]
|
||||
participant Cache["In-Memory Cache"]
|
||||
participant IAM["IAM Service"]
|
||||
flowchart LR
|
||||
MEMBER["👥 Membership Service"]
|
||||
BUS["📬 Event Bus<br/>(RabbitMQ)"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
NOTIF["🔔 Notification Service"]
|
||||
|
||||
Membership->>IamClient: ValidateUserAsync(token)
|
||||
IamClient->>Cache: TryGetFromCache(cacheKey)
|
||||
MEMBER -->|MemberLevelUpIntegrationEvent| BUS
|
||||
BUS -->|Subscribe| WALLET
|
||||
BUS -->|Subscribe| NOTIF
|
||||
|
||||
alt Cache Hit
|
||||
Cache-->>IamClient: cached user info
|
||||
else Cache Miss
|
||||
IamClient->>IAM: GET /api/v1/users/me
|
||||
IAM-->>IamClient: user info
|
||||
IamClient->>Cache: AddToCache(user, 5min)
|
||||
end
|
||||
|
||||
IamClient-->>Membership: IamUserInfo
|
||||
WALLET -->|Grant Rewards| WALLET
|
||||
NOTIF -->|Send Notification| NOTIF
|
||||
|
||||
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style BUS fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
|
||||
style NOTIF fill:#eab308,stroke:#a16207,color:#000
|
||||
```
|
||||
|
||||
### IAM Client Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| User Validation | Validate token and get user info |
|
||||
| Role/Permission Check | Check user roles and permissions |
|
||||
| Health Check | Check IAM Service availability |
|
||||
| Caching | In-memory cache with 5-minute TTL |
|
||||
|
||||
## Security
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Traefik
|
||||
participant Membership
|
||||
participant IAM
|
||||
|
||||
Client->>IAM: Login
|
||||
IAM-->>Client: JWT Token
|
||||
Client->>Traefik: Request + Bearer Token
|
||||
Traefik->>Membership: Forward request
|
||||
Membership->>Membership: Validate JWT
|
||||
Membership-->>Client: Response
|
||||
```
|
||||
|
||||
### Authorization
|
||||
|
||||
- All endpoints require JWT Bearer authentication
|
||||
- Token validated against IAM Service authority
|
||||
- UserId extracted from JWT claims (`sub` or `id`)
|
||||
- **Admin endpoints** (`/api/v1/levels` POST/PUT/DELETE) require `admin` role
|
||||
|
||||
### Experience Source Validation
|
||||
|
||||
```csharp
|
||||
// Validate experience source to prevent abuse
|
||||
public enum ExperienceSource
|
||||
{
|
||||
Purchase, // From orders
|
||||
Referral, // Friend referral
|
||||
Activity, // App activity
|
||||
Promotion, // Promotions
|
||||
Admin // Admin manual grant
|
||||
}
|
||||
```
|
||||
|
||||
## Infrastructure
|
||||
|
||||
@@ -315,7 +591,7 @@ membership-service-net:
|
||||
ports:
|
||||
- "5002:8080"
|
||||
labels:
|
||||
- "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`)"
|
||||
- "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`) || PathPrefix(`/api/v1/levels`)"
|
||||
```
|
||||
|
||||
### Kubernetes Deployment
|
||||
@@ -349,26 +625,6 @@ spec:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 8080
|
||||
env:
|
||||
- name: IamService__BaseUrl
|
||||
value: "http://iam-service:8080"
|
||||
```
|
||||
|
||||
## Scalability
|
||||
|
||||
### Horizontal Scaling
|
||||
|
||||
- Stateless service design
|
||||
- Database connection pooling
|
||||
- Read replicas for queries
|
||||
|
||||
### Caching Strategy (Future)
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Client │────▶│ Redis │────▶│ PostgreSQL │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
Cache Layer Persistence
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
@@ -383,15 +639,16 @@ spec:
|
||||
|
||||
### Metrics (Prometheus)
|
||||
|
||||
- `membership_experience_added_total` - Total EXP added
|
||||
- `membership_level_ups_total` - Number of level ups
|
||||
- `membership_requests_total` - Request count
|
||||
- `membership_request_duration_seconds` - Latency
|
||||
- `membership_errors_total` - Error count
|
||||
|
||||
### Logging
|
||||
|
||||
Structured Serilog logging with:
|
||||
- Request/Response logging
|
||||
- Domain event logging
|
||||
- Domain event logging (Level Up, EXP Added)
|
||||
- Error tracking with stack traces
|
||||
|
||||
## Error Handling
|
||||
@@ -401,9 +658,9 @@ Structured Serilog logging with:
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
"title": "Not Found",
|
||||
"status": 404,
|
||||
"detail": "Member with ID 'xxx' was not found.",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "EXP points must be positive.",
|
||||
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
|
||||
}
|
||||
```
|
||||
@@ -412,17 +669,19 @@ Structured Serilog logging with:
|
||||
|
||||
| HTTP Status | Scenario |
|
||||
|-------------|----------|
|
||||
| 400 | Validation error |
|
||||
| 400 | Validation error (negative EXP, etc.) |
|
||||
| 401 | Missing/invalid token |
|
||||
| 404 | Member not found |
|
||||
| 409 | Member already exists |
|
||||
| 403 | No permission (Admin endpoints) |
|
||||
| 404 | Member/level not found |
|
||||
| 409 | Level number already exists |
|
||||
| 500 | Internal server error |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Redis caching for member lookups
|
||||
- [ ] Event sourcing for audit trail
|
||||
- [ ] GraphQL endpoint
|
||||
- [ ] Batch member import/export
|
||||
- [ ] Member search with Elasticsearch
|
||||
- [ ] Webhook notifications on level changes
|
||||
- [ ] Redis caching for level definitions
|
||||
- [ ] Event sourcing for EXP history audit trail
|
||||
- [ ] Leaderboard with EXP ranking
|
||||
- [ ] Achievement/Badge system
|
||||
- [ ] EXP decay (reduce EXP if inactive)
|
||||
- [ ] Seasonal levels (reset by season)
|
||||
- [ ] Integration Events with RabbitMQ
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Tổng Quan Hệ Thống
|
||||
|
||||
Membership Service quản lý dữ liệu membership của user. Profile information được lưu tại IAM Service.
|
||||
Membership Service quản lý dữ liệu membership của user bao gồm **Level**, **Experience (EXP)**, và **Preferences**. Profile information được lưu tại IAM Service.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
@@ -17,8 +17,9 @@ graph TB
|
||||
|
||||
subgraph Services["Microservices"]
|
||||
IAM["🔐 IAM Service<br/>(User & Profile)"]
|
||||
MEMBER["👥 Membership Service<br/>(Membership Data)"]
|
||||
MEMBER["👥 Membership Service<br/>(Level, EXP, Preferences)"]
|
||||
STORAGE["📦 Storage Service"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
end
|
||||
|
||||
subgraph Data["Data Layer"]
|
||||
@@ -34,17 +35,20 @@ graph TB
|
||||
IAM --> |"JWT Token"| MEMBER
|
||||
MEMBER --> |"HTTP Client"| IAM
|
||||
MEMBER --> PG
|
||||
MEMBER -.-> |"Level Up Event"| WALLET
|
||||
|
||||
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style STORAGE fill:#eab308,stroke:#a16207,color:#000
|
||||
style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
|
||||
style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Phân chia trách nhiệm:**
|
||||
> - **IAM Service**: Quản lý User identity và UserProfile (avatar, phone, address, DOB)
|
||||
> - **Membership Service**: Quản lý membership data (membership level, preferences, country code, gender)
|
||||
> - **Membership Service**: Quản lý membership data (level, experience, preferences, country code, gender)
|
||||
> - **Wallet Service**: Nhận event khi Level Up để xử lý rewards
|
||||
|
||||
## Các Lớp Clean Architecture
|
||||
|
||||
@@ -55,6 +59,7 @@ graph TB
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Lớp Domain (Core) │
|
||||
│ Entities, Aggregates, Value Objects, Domain Events │
|
||||
│ Member Aggregate, LevelDefinition Aggregate │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Lớp Infrastructure │
|
||||
│ DbContext, Repositories, Entity Configurations │
|
||||
@@ -64,7 +69,7 @@ graph TB
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Cấu Trúc Aggregate
|
||||
### Tổng Quan Aggregates
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
@@ -73,37 +78,115 @@ classDiagram
|
||||
+Guid UserId
|
||||
+string CountryCode
|
||||
+string? Gender
|
||||
+int MembershipLevelId
|
||||
+MembershipLevel MembershipLevel
|
||||
+int CurrentLevel
|
||||
+int CurrentExp
|
||||
+int TotalExpEarned
|
||||
+string? Preferences
|
||||
+bool IsDeleted
|
||||
+DateTime CreatedAt
|
||||
+DateTime UpdatedAt
|
||||
+AddExperience(points, levelRules)
|
||||
+UpdateGender()
|
||||
+UpdateCountryCode()
|
||||
+UpdatePreferences()
|
||||
+ChangeMembershipLevel()
|
||||
+Delete()
|
||||
+Restore()
|
||||
}
|
||||
|
||||
class MembershipLevel {
|
||||
<<enumeration>>
|
||||
+int Id
|
||||
class LevelDefinition {
|
||||
+Guid Id
|
||||
+int LevelNumber
|
||||
+string Name
|
||||
+Free = 1
|
||||
+Basic = 2
|
||||
+Premium = 3
|
||||
+int RequiredExp
|
||||
+string? Description
|
||||
+string? IconUrl
|
||||
+string? Benefits
|
||||
+bool IsActive
|
||||
+DateTime CreatedAt
|
||||
+DateTime UpdatedAt
|
||||
}
|
||||
|
||||
Member --> MembershipLevel
|
||||
Member ..> LevelDefinition : uses rules
|
||||
|
||||
style Member fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style MembershipLevel fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style LevelDefinition fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
```
|
||||
|
||||
### Member Aggregate (Rich Domain Model)
|
||||
|
||||
```csharp
|
||||
public class Member : Entity, IAggregateRoot
|
||||
{
|
||||
// Private fields để encapsulate state
|
||||
private int _currentLevel;
|
||||
private int _currentExp;
|
||||
private int _totalExpEarned;
|
||||
|
||||
public int CurrentLevel => _currentLevel;
|
||||
public int CurrentExp => _currentExp;
|
||||
public int TotalExpEarned => _totalExpEarned;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Add experience and automatically level up if thresholds are met.
|
||||
/// VI: Thêm exp và tự động lên level nếu đạt ngưỡng.
|
||||
/// </summary>
|
||||
public void AddExperience(int points, IReadOnlyList<LevelDefinition> levelRules)
|
||||
{
|
||||
if (points <= 0)
|
||||
throw new DomainException("EXP points must be positive");
|
||||
|
||||
int oldLevel = _currentLevel;
|
||||
_currentExp += points;
|
||||
_totalExpEarned += points;
|
||||
_updatedAt = DateTime.UtcNow;
|
||||
|
||||
// Business logic: Calculate new level
|
||||
var newLevel = CalculateLevel(_currentExp, levelRules);
|
||||
|
||||
if (newLevel > oldLevel)
|
||||
{
|
||||
_currentLevel = newLevel;
|
||||
// Raise domain event for side effects
|
||||
AddDomainEvent(new MemberLevelUpDomainEvent(this, oldLevel, newLevel));
|
||||
}
|
||||
|
||||
AddDomainEvent(new MemberExperienceAddedDomainEvent(this, points));
|
||||
}
|
||||
|
||||
private static int CalculateLevel(int exp, IReadOnlyList<LevelDefinition> rules)
|
||||
{
|
||||
return rules
|
||||
.Where(r => r.IsActive && r.RequiredExp <= exp)
|
||||
.OrderByDescending(r => r.LevelNumber)
|
||||
.FirstOrDefault()?.LevelNumber ?? 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Member entity không lưu trữ profile fields (phone, avatar, address, DOB). Những fields này được quản lý bởi IAM Service's UserProfile.
|
||||
> **Rich Domain Model**: Logic nghiệp vụ về EXP và Level được đóng gói trong Member entity, không phải ở Service Layer. Điều này tránh mô hình "Anemic Domain Model".
|
||||
|
||||
### LevelDefinition Aggregate (Customizable Levels)
|
||||
|
||||
```csharp
|
||||
public class LevelDefinition : Entity, IAggregateRoot
|
||||
{
|
||||
public int LevelNumber { get; private set; } // 1, 2, 3...
|
||||
public string Name { get; private set; } // "Bronze", "Silver", "Gold"
|
||||
public int RequiredExp { get; private set; } // 0, 100, 300, 600...
|
||||
public string? Description { get; private set; }
|
||||
public string? IconUrl { get; private set; }
|
||||
public string? Benefits { get; private set; } // JSON benefits
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
public void UpdateRequiredExp(int newRequiredExp)
|
||||
{
|
||||
if (newRequiredExp < 0)
|
||||
throw new DomainException("Required EXP cannot be negative");
|
||||
RequiredExp = newRequiredExp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Events
|
||||
|
||||
@@ -111,53 +194,102 @@ classDiagram
|
||||
|-------|---------|----------|
|
||||
| `MemberCreatedDomainEvent` | Tạo thành viên mới | Email chào mừng, analytics |
|
||||
| `MemberUpdatedDomainEvent` | Cập nhật hồ sơ | Đồng bộ với services khác |
|
||||
| `MembershipLevelChangedDomainEvent` | Thay đổi cấp độ | Thanh toán, cập nhật quyền lợi |
|
||||
| `MemberExperienceAddedDomainEvent` | Nhận EXP | Logging, analytics |
|
||||
| `MemberLevelUpDomainEvent` | Lên level | **Rewards, notifications, unlock features** |
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Handler
|
||||
participant Member
|
||||
participant EventDispatcher
|
||||
participant WalletService
|
||||
|
||||
Client->>Handler: AddExperienceCommand
|
||||
Handler->>Member: AddExperience(100, rules)
|
||||
Member->>Member: _currentExp += 100
|
||||
Member->>Member: CalculateLevel()
|
||||
|
||||
alt Level Up
|
||||
Member->>Member: AddDomainEvent(LevelUpEvent)
|
||||
end
|
||||
|
||||
Handler->>EventDispatcher: SaveEntitiesAsync()
|
||||
EventDispatcher->>WalletService: Publish LevelUpEvent
|
||||
WalletService-->>WalletService: Grant Rewards
|
||||
```
|
||||
|
||||
## CQRS Pattern
|
||||
|
||||
### Write Side (Commands)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Commands["✏️ Write Side"]
|
||||
CMD1["CreateMemberCommand"]
|
||||
CMD2["UpdateMemberProfileCommand"]
|
||||
CMD3["ChangeMembershipLevelCommand"]
|
||||
subgraph Commands["✏️ Commands"]
|
||||
CMD1["AddExperienceCommand"]
|
||||
CMD2["CreateMemberCommand"]
|
||||
CMD3["UpdateMemberProfileCommand"]
|
||||
CMD4["CreateLevelDefinitionCommand"]
|
||||
end
|
||||
|
||||
subgraph Handlers["⚙️ Command Handlers"]
|
||||
H1["CreateMemberHandler"]
|
||||
H2["UpdateProfileHandler"]
|
||||
H3["ChangeLevelHandler"]
|
||||
H1["AddExperienceHandler"]
|
||||
H2["CreateMemberHandler"]
|
||||
H3["UpdateProfileHandler"]
|
||||
H4["CreateLevelHandler"]
|
||||
end
|
||||
|
||||
subgraph Queries["📖 Read Side"]
|
||||
Q1["GetMemberByIdQuery"]
|
||||
Q2["GetMembersQuery"]
|
||||
end
|
||||
|
||||
subgraph QueryHandlers["🔍 Query Handlers"]
|
||||
QH1["GetMemberByIdHandler"]
|
||||
QH2["GetMembersHandler"]
|
||||
subgraph Domain["🏛️ Domain Model"]
|
||||
DM["Rich Domain Model<br/>EF Core"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
CMD1 --> H1 --> DB
|
||||
CMD2 --> H2 --> DB
|
||||
CMD3 --> H3 --> DB
|
||||
|
||||
Q1 --> QH1 --> DB
|
||||
Q2 --> QH2 --> DB
|
||||
CMD1 --> H1 --> DM --> DB
|
||||
CMD2 --> H2 --> DM
|
||||
CMD3 --> H3 --> DM
|
||||
CMD4 --> H4 --> DM
|
||||
|
||||
style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD4 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
```
|
||||
|
||||
### Read Side (Queries)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Queries["📖 Queries"]
|
||||
Q1["GetMemberByIdQuery"]
|
||||
Q2["GetMemberProgressQuery"]
|
||||
Q3["GetLevelDefinitionsQuery"]
|
||||
end
|
||||
|
||||
subgraph Handlers["🔍 Query Handlers"]
|
||||
QH1["GetMemberByIdHandler"]
|
||||
QH2["GetMemberProgressHandler"]
|
||||
QH3["GetLevelDefinitionsHandler"]
|
||||
end
|
||||
|
||||
subgraph ReadModel["📊 Read Model"]
|
||||
RM["Dapper / Raw SQL"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
Q1 --> QH1 --> RM --> DB
|
||||
Q2 --> QH2 --> RM
|
||||
Q3 --> QH3 --> RM
|
||||
|
||||
style Q1 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q2 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q3 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
```
|
||||
|
||||
## Luồng Request
|
||||
|
||||
### Luồng Tạo Thành Viên
|
||||
### Luồng Add Experience
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -165,23 +297,58 @@ sequenceDiagram
|
||||
participant Controller
|
||||
participant MediatR
|
||||
participant Handler
|
||||
participant Repository
|
||||
participant MemberRepo
|
||||
participant LevelRepo
|
||||
participant DB
|
||||
|
||||
Client->>Controller: POST /api/v1/members
|
||||
Controller->>MediatR: Send(CreateMemberCommand)
|
||||
Client->>Controller: POST /api/v1/members/{id}/experience
|
||||
Note over Client,Controller: { "points": 100, "source": "purchase" }
|
||||
|
||||
Controller->>MediatR: Send(AddExperienceCommand)
|
||||
MediatR->>Handler: Handle(command)
|
||||
Handler->>Repository: ExistsByUserIdAsync(userId)
|
||||
Repository->>DB: SELECT EXISTS
|
||||
DB-->>Repository: false
|
||||
Handler->>Repository: Add(member)
|
||||
Handler->>Repository: SaveEntitiesAsync()
|
||||
Repository->>DB: INSERT + Domain Events
|
||||
DB-->>Repository: OK
|
||||
Repository-->>Handler: member
|
||||
Handler-->>MediatR: CreateMemberResult
|
||||
MediatR-->>Controller: result
|
||||
Controller-->>Client: 201 Created
|
||||
|
||||
Handler->>MemberRepo: GetAsync(memberId)
|
||||
MemberRepo->>DB: SELECT member
|
||||
DB-->>MemberRepo: member data
|
||||
|
||||
Handler->>LevelRepo: GetAllActiveAsync()
|
||||
LevelRepo->>DB: SELECT level_definitions
|
||||
DB-->>LevelRepo: level rules
|
||||
|
||||
Handler->>Handler: member.AddExperience(100, rules)
|
||||
Note over Handler: Business logic in Domain
|
||||
|
||||
Handler->>MemberRepo: UnitOfWork.SaveEntitiesAsync()
|
||||
MemberRepo->>DB: UPDATE + Dispatch Domain Events
|
||||
|
||||
DB-->>Handler: OK
|
||||
Handler-->>Controller: AddExperienceResult
|
||||
Controller-->>Client: 200 OK
|
||||
Note over Client,Controller: { "currentExp": 150, "currentLevel": 2, "leveledUp": true }
|
||||
```
|
||||
|
||||
### Luồng Get Member Progress
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Controller
|
||||
participant Handler
|
||||
participant Dapper
|
||||
participant DB
|
||||
|
||||
Client->>Controller: GET /api/v1/members/{id}/progress
|
||||
Controller->>Handler: GetMemberProgressQuery
|
||||
Handler->>Dapper: Raw SQL Query
|
||||
|
||||
Note over Dapper,DB: SELECT m.current_exp, m.current_level,<br/>l.name, l.required_exp, next_level.required_exp<br/>FROM members m<br/>JOIN level_definitions l ON ...
|
||||
|
||||
Dapper->>DB: Execute
|
||||
DB-->>Dapper: Result
|
||||
Dapper-->>Handler: MemberProgressDto
|
||||
Handler-->>Controller: Result
|
||||
Controller-->>Client: 200 OK
|
||||
Note over Client,Controller: { "currentLevel": 2, "levelName": "Silver",<br/>"currentExp": 150, "expToNextLevel": 150,<br/>"progressPercent": 50 }
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
@@ -192,35 +359,107 @@ erDiagram
|
||||
uuid id PK "Same as IAM UserId"
|
||||
varchar country_code "ISO 3166-1 alpha-2"
|
||||
varchar gender "male/female/other"
|
||||
int membership_level_id FK
|
||||
int current_level "Current member level"
|
||||
int current_exp "Current experience points"
|
||||
int total_exp_earned "Total EXP ever earned"
|
||||
jsonb preferences "User preferences"
|
||||
boolean is_deleted "Soft delete flag"
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
MEMBERSHIP_LEVELS {
|
||||
int id PK
|
||||
varchar name
|
||||
LEVEL_DEFINITIONS {
|
||||
uuid id PK
|
||||
int level_number UK "Level 1, 2, 3..."
|
||||
varchar name "Bronze, Silver, Gold..."
|
||||
int required_exp "EXP needed for this level"
|
||||
text description
|
||||
varchar icon_url
|
||||
jsonb benefits "Level benefits/rewards"
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
MEMBERS }o--|| MEMBERSHIP_LEVELS : has
|
||||
MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references"
|
||||
```
|
||||
|
||||
### SQL Schema
|
||||
|
||||
```sql
|
||||
-- Bảng Level Definitions (Admin configurable)
|
||||
CREATE TABLE level_definitions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
level_number INT NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
required_exp INT NOT NULL DEFAULT 0,
|
||||
description TEXT,
|
||||
icon_url VARCHAR(500),
|
||||
benefits JSONB,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Bảng Members
|
||||
CREATE TABLE members (
|
||||
id UUID PRIMARY KEY,
|
||||
country_code VARCHAR(2) NOT NULL DEFAULT 'VN',
|
||||
gender VARCHAR(10),
|
||||
current_level INT NOT NULL DEFAULT 1,
|
||||
current_exp INT NOT NULL DEFAULT 0,
|
||||
total_exp_earned INT NOT NULL DEFAULT 0,
|
||||
preferences JSONB,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX ix_members_current_level ON members(current_level);
|
||||
CREATE INDEX ix_members_current_exp ON members(current_exp);
|
||||
CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number);
|
||||
CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active);
|
||||
|
||||
-- Default Level Definitions
|
||||
INSERT INTO level_definitions (level_number, name, required_exp, description, is_active)
|
||||
VALUES
|
||||
(1, 'Bronze', 0, 'Starting level', true),
|
||||
(2, 'Silver', 100, 'Reach 100 EXP', true),
|
||||
(3, 'Gold', 300, 'Reach 300 EXP', true),
|
||||
(4, 'Platinum', 600, 'Reach 600 EXP', true),
|
||||
(5, 'Diamond', 1000, 'Reach 1000 EXP', true);
|
||||
```
|
||||
|
||||
## Thiết Kế API
|
||||
|
||||
### RESTful Endpoints
|
||||
|
||||
#### Member Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/members # Danh sách (phân trang, search)
|
||||
GET /api/v1/members/{id} # Lấy theo ID
|
||||
GET /api/v1/members/me # Lấy user hiện tại
|
||||
GET /api/v1/members/{id}/progress # Lấy tiến trình level
|
||||
POST /api/v1/members # Tạo mới
|
||||
POST /api/v1/members/{id}/experience # Thêm EXP
|
||||
PUT /api/v1/members/{id} # Cập nhật hồ sơ
|
||||
PUT /api/v1/members/{id}/level # Thay đổi cấp độ
|
||||
```
|
||||
|
||||
### Định Dạng Response
|
||||
#### Level Endpoints (Admin)
|
||||
|
||||
```
|
||||
GET /api/v1/levels # Danh sách level definitions
|
||||
GET /api/v1/levels/{id} # Lấy level definition
|
||||
POST /api/v1/levels # Tạo level definition (Admin)
|
||||
PUT /api/v1/levels/{id} # Cập nhật level definition (Admin)
|
||||
DELETE /api/v1/levels/{id} # Deactivate level (Admin)
|
||||
```
|
||||
|
||||
### Response Formats
|
||||
|
||||
#### Member Response
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -228,76 +467,113 @@ PUT /api/v1/members/{id}/level # Thay đổi cấp độ
|
||||
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"countryCode": "VN",
|
||||
"gender": "male",
|
||||
"preferences": "{\"theme\": \"dark\", \"language\": \"vi\"}",
|
||||
"membershipLevel": {
|
||||
"id": 1,
|
||||
"name": "Free"
|
||||
},
|
||||
"currentLevel": 2,
|
||||
"currentExp": 150,
|
||||
"totalExpEarned": 150,
|
||||
"preferences": "{\"theme\": \"dark\"}",
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Member Progress Response
|
||||
|
||||
```json
|
||||
{
|
||||
"memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"currentLevel": 2,
|
||||
"levelName": "Silver",
|
||||
"levelIconUrl": "/icons/silver.png",
|
||||
"currentExp": 150,
|
||||
"expToNextLevel": 150,
|
||||
"nextLevelRequiredExp": 300,
|
||||
"progressPercent": 50.0
|
||||
}
|
||||
```
|
||||
|
||||
#### Add Experience Response
|
||||
|
||||
```json
|
||||
{
|
||||
"currentExp": 250,
|
||||
"currentLevel": 2,
|
||||
"totalExpEarned": 250,
|
||||
"leveledUp": false,
|
||||
"newLevel": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Level Definition Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a1b2c3d4-...",
|
||||
"levelNumber": 3,
|
||||
"name": "Gold",
|
||||
"requiredExp": 300,
|
||||
"description": "Reach 300 EXP to unlock Gold status",
|
||||
"iconUrl": "/icons/gold.png",
|
||||
"benefits": {
|
||||
"discountPercent": 10,
|
||||
"prioritySupport": true,
|
||||
"freeShipping": false
|
||||
},
|
||||
"isActive": true
|
||||
}
|
||||
```
|
||||
|
||||
## Inter-Service Communication
|
||||
|
||||
### IAM Service Client
|
||||
|
||||
Membership Service giao tiếp với IAM Service thông qua HTTP Client:
|
||||
Membership Service giao tiếp với IAM Service thông qua HTTP Client để validate users.
|
||||
|
||||
### Integration Events (Planned)
|
||||
|
||||
Khi member lên level, publish Integration Event để các services khác xử lý:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Membership
|
||||
participant IamClient["IIamServiceClient"]
|
||||
participant Cache["In-Memory Cache"]
|
||||
participant IAM["IAM Service"]
|
||||
flowchart LR
|
||||
MEMBER["👥 Membership Service"]
|
||||
BUS["📬 Event Bus<br/>(RabbitMQ)"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
NOTIF["🔔 Notification Service"]
|
||||
|
||||
Membership->>IamClient: ValidateUserAsync(token)
|
||||
IamClient->>Cache: TryGetFromCache(cacheKey)
|
||||
MEMBER -->|MemberLevelUpIntegrationEvent| BUS
|
||||
BUS -->|Subscribe| WALLET
|
||||
BUS -->|Subscribe| NOTIF
|
||||
|
||||
alt Cache Hit
|
||||
Cache-->>IamClient: cached user info
|
||||
else Cache Miss
|
||||
IamClient->>IAM: GET /api/v1/users/me
|
||||
IAM-->>IamClient: user info
|
||||
IamClient->>Cache: AddToCache(user, 5min)
|
||||
end
|
||||
|
||||
IamClient-->>Membership: IamUserInfo
|
||||
WALLET -->|Grant Rewards| WALLET
|
||||
NOTIF -->|Send Notification| NOTIF
|
||||
|
||||
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style BUS fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
|
||||
style NOTIF fill:#eab308,stroke:#a16207,color:#000
|
||||
```
|
||||
|
||||
### IAM Client Features
|
||||
|
||||
| Feature | Mô tả |
|
||||
|---------|-------|
|
||||
| User Validation | Xác thực token và lấy user info |
|
||||
| Role/Permission Check | Kiểm tra roles và permissions của user |
|
||||
| Health Check | Kiểm tra IAM Service availability |
|
||||
| Caching | In-memory cache với TTL 5 phút |
|
||||
|
||||
## Bảo Mật
|
||||
|
||||
### Luồng Xác Thực
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Traefik
|
||||
participant Membership
|
||||
participant IAM
|
||||
|
||||
Client->>IAM: Login
|
||||
IAM-->>Client: JWT Token
|
||||
Client->>Traefik: Request + Bearer Token
|
||||
Traefik->>Membership: Forward request
|
||||
Membership->>Membership: Validate JWT
|
||||
Membership-->>Client: Response
|
||||
```
|
||||
|
||||
### Authorization
|
||||
|
||||
- Tất cả endpoints yêu cầu xác thực JWT Bearer
|
||||
- Token được xác thực với IAM Service authority
|
||||
- UserId được trích xuất từ JWT claims (`sub` hoặc `id`)
|
||||
- **Admin endpoints** (`/api/v1/levels` POST/PUT/DELETE) yêu cầu role `admin`
|
||||
|
||||
### Experience Source Validation
|
||||
|
||||
```csharp
|
||||
// Validate experience source để tránh abuse
|
||||
public enum ExperienceSource
|
||||
{
|
||||
Purchase, // Từ đơn hàng
|
||||
Referral, // Giới thiệu bạn bè
|
||||
Activity, // Hoạt động trên app
|
||||
Promotion, // Khuyến mãi
|
||||
Admin // Admin manual grant
|
||||
}
|
||||
```
|
||||
|
||||
## Hạ Tầng
|
||||
|
||||
@@ -315,7 +591,7 @@ membership-service-net:
|
||||
ports:
|
||||
- "5002:8080"
|
||||
labels:
|
||||
- "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`)"
|
||||
- "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`) || PathPrefix(`/api/v1/levels`)"
|
||||
```
|
||||
|
||||
### Kubernetes Deployment
|
||||
@@ -349,26 +625,6 @@ spec:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 8080
|
||||
env:
|
||||
- name: IamService__BaseUrl
|
||||
value: "http://iam-service:8080"
|
||||
```
|
||||
|
||||
## Khả Năng Mở Rộng
|
||||
|
||||
### Mở Rộng Theo Chiều Ngang
|
||||
|
||||
- Thiết kế service stateless
|
||||
- Connection pooling cho database
|
||||
- Read replicas cho queries
|
||||
|
||||
### Chiến Lược Caching (Tương Lai)
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Client │────▶│ Redis │────▶│ PostgreSQL │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
Cache Layer Persistence
|
||||
```
|
||||
|
||||
## Giám Sát
|
||||
@@ -383,27 +639,28 @@ spec:
|
||||
|
||||
### Metrics (Prometheus)
|
||||
|
||||
- `membership_experience_added_total` - Tổng EXP đã cộng
|
||||
- `membership_level_ups_total` - Số lần lên level
|
||||
- `membership_requests_total` - Số lượng request
|
||||
- `membership_request_duration_seconds` - Độ trễ
|
||||
- `membership_errors_total` - Số lỗi
|
||||
|
||||
### Logging
|
||||
|
||||
Structured Serilog logging với:
|
||||
- Request/Response logging
|
||||
- Domain event logging
|
||||
- Domain event logging (Level Up, EXP Added)
|
||||
- Error tracking với stack traces
|
||||
|
||||
## Xử Lý Lỗi
|
||||
|
||||
### Định Dạng Error Response
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
"title": "Not Found",
|
||||
"status": 404,
|
||||
"detail": "Không tìm thấy thành viên với ID 'xxx'.",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "EXP points must be positive.",
|
||||
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
|
||||
}
|
||||
```
|
||||
@@ -412,17 +669,19 @@ Structured Serilog logging với:
|
||||
|
||||
| HTTP Status | Tình huống |
|
||||
|-------------|------------|
|
||||
| 400 | Lỗi validation |
|
||||
| 400 | Validation error (EXP âm, etc.) |
|
||||
| 401 | Thiếu/sai token |
|
||||
| 404 | Không tìm thấy thành viên |
|
||||
| 409 | Thành viên đã tồn tại |
|
||||
| 403 | Không có quyền (Admin endpoints) |
|
||||
| 404 | Không tìm thấy member/level |
|
||||
| 409 | Level number đã tồn tại |
|
||||
| 500 | Lỗi server nội bộ |
|
||||
|
||||
## Cải Tiến Tương Lai
|
||||
|
||||
- [ ] Redis caching cho member lookups
|
||||
- [ ] Event sourcing cho audit trail
|
||||
- [ ] GraphQL endpoint
|
||||
- [ ] Batch member import/export
|
||||
- [ ] Member search với Elasticsearch
|
||||
- [ ] Webhook notifications khi level thay đổi
|
||||
- [ ] Redis caching cho level definitions
|
||||
- [ ] Event sourcing cho EXP history audit trail
|
||||
- [ ] Leaderboard với ranking theo EXP
|
||||
- [ ] Achievement/Badge system
|
||||
- [ ] EXP decay (giảm EXP nếu không hoạt động)
|
||||
- [ ] Seasonal levels (reset theo mùa)
|
||||
- [ ] Integration Events với RabbitMQ
|
||||
|
||||
Reference in New Issue
Block a user