docs: Add service-level READMEs, update Wallet Service documentation with multi-currency and admin APIs, refine Chat Service architecture, and remove a test Mermaid file.

This commit is contained in:
Ho Ngoc Hai
2026-01-18 23:51:39 +07:00
parent 86c9e7303c
commit 5aa48eb29c
15 changed files with 1096 additions and 844 deletions

View File

@@ -1,12 +0,0 @@
# Mermaid Test
This is a test diagram.
```mermaid
graph TD;
A[Start] --> B{Is it working?};
B -- Yes --> C[Great!];
B -- No --> D[Debug more];
```
End of test.

View File

@@ -0,0 +1,56 @@
# Chat Service
> Real-time chat service with End-to-End Encryption (E2EE) for GoodGo platform.
## Documentation / Tài Liệu
> **EN**: [English Documentation](docs/en/README.md)
> **VI**: [Tài liệu Tiếng Việt](docs/vi/README.md)
## Quick Links
| English | Vietnamese |
|---------|------------|
| [Architecture](docs/en/ARCHITECTURE.md) | [Kiến trúc](docs/vi/ARCHITECTURE.md) |
| [Quick Start](docs/en/README.md#quick-start) | [Bắt Đầu Nhanh](docs/vi/README.md#bắt-đầu-nhanh) |
| [API Reference](docs/en/README.md#api-endpoints) | [API Reference](docs/vi/README.md#api-endpoints) |
## Tech Stack
- **.NET 10** - Core framework
- **ASP.NET Core SignalR** - Real-time communication
- **PostgreSQL 16+** - Message persistence
- **Redis 7+** - Backplane & caching
- **E2EE with X3DH** - End-to-end encryption
## Key Features
- 🔒 **End-to-End Encryption** - X3DH key exchange, AES-256-GCM
- 💬 **Real-time Chat** - SignalR with WebSocket/SSE/Long Polling
- 🤖 **AI Integration** - Smart chatbot with streaming responses
- 📱 **Multi-device** - User mapping across devices
- 🚀 **High Performance** - MessagePack protocol
## Development
```bash
# Restore dependencies
dotnet restore
# Build
dotnet build
# Run
dotnet run --project src/ChatService.API
```
## Docker
```bash
docker build -t chatservice:latest .
docker run -p 5000:8080 --env-file .env chatservice:latest
```
## License
Proprietary - GoodGo Platform

View File

@@ -1,6 +1,6 @@
# Chat Service Architecture Documentation
> Detailed architecture for Chat Service with SignalR, scalability patterns, and AI integration.
> Detailed architecture for Chat Service with E2EE, SignalR, scalability patterns, and AI integration.
## Architecture Overview
@@ -43,12 +43,138 @@ graph TB
CS1 & CS2 & CS3 --> RC
CS1 & CS2 & CS3 --> AI
style LB fill:#3498db,stroke:#2980b9,color:#fff
style RD fill:#e74c3c,stroke:#c0392b,color:#fff
style PG fill:#27ae60,stroke:#1e8449,color:#fff
style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
style LB fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
style RD fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
style PG fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
```
## End-to-End Encryption (E2EE)
### X3DH Key Exchange Protocol
The service implements the Extended Triple Diffie-Hellman (X3DH) protocol for establishing encrypted sessions:
```mermaid
sequenceDiagram
participant Alice as Alice (Sender)
participant Server as Chat Server
participant Bob as Bob (Receiver)
Note over Bob,Server: Bob registers keys
Bob->>Server: Register Key Bundle<br/>(Identity, SignedPreKey, OneTimePreKeys)
Server->>Server: Store public keys only
Note over Alice,Server: Alice initiates session
Alice->>Server: Request Bob's Key Bundle
Server->>Alice: {IdentityKey, SignedPreKey, OneTimePreKey}
Alice->>Alice: X3DH → Session Key
Alice->>Alice: AES-256-GCM encrypt message
Alice->>Server: Send encrypted message
Server->>Bob: Deliver encrypted message
Bob->>Bob: X3DH → Session Key
Bob->>Bob: AES-256-GCM decrypt message
style Server fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
```
### Key Components
| Component | Description | Storage |
|-----------|-------------|---------|
| **Identity Key** | Long-term Curve25519 key pair | Private: Client only |
| **Signed Pre-Key** | Rotated every 30 days | Public: Server |
| **One-Time Pre-Keys** | Consumed per session | Public: Server (deleted after use) |
| **Session Key** | Derived via X3DH | Never stored |
### Security Properties
-**Forward Secrecy**: Compromised keys don't reveal past messages
-**Zero-Knowledge Server**: Server cannot decrypt messages
-**Deniability**: No cryptographic proof of authorship
## Domain Model
### Aggregate Roots
```mermaid
erDiagram
Conversation ||--o{ Message : contains
Conversation ||--o{ ConversationParticipant : has
ChatUser ||--o{ Conversation : participates
ChatUser ||--|| UserKeyBundle : has
ChatUser ||--o{ OneTimePreKey : owns
Conversation {
uuid Id PK
string Name
string AvatarUrl
enum Type "Direct|Group"
uuid CreatorId
datetime CreatedAt
datetime LastActivityAt
}
Message {
uuid Id PK
uuid ConversationId FK
uuid SenderId FK
string EncryptedContent
string Nonce
string AuthTag
enum Type "Text|Image|File|System"
enum Status "Sent|Delivered|Read|Failed"
datetime CreatedAt
}
ConversationParticipant {
uuid Id PK
uuid ConversationId FK
uuid ChatUserId FK
enum Role "Owner|Admin|Member"
datetime JoinedAt
datetime LastReadAt
}
ChatUser {
uuid Id PK
string IdentityUserId
string DisplayName
string AvatarUrl
enum Status "Online|Away|Offline"
datetime LastSeenAt
}
UserKeyBundle {
string IdentityPublicKey
string SignedPreKey
string SignedPreKeySignature
datetime SignedPreKeyTimestamp
}
OneTimePreKey {
uuid Id PK
int KeyId
string PublicKey
bool IsUsed
datetime CreatedAt
}
```
### Domain Events
| Event | Trigger | Handler |
|-------|---------|---------|
| `ConversationCreatedDomainEvent` | New conversation created | Notify participants |
| `MessageSentDomainEvent` | Message sent | Update last_activity, broadcast |
| `MessageDeliveredDomainEvent` | Message delivered | Notify sender |
| `MessageReadDomainEvent` | Message read | Notify sender |
| `UserJoinedRoomDomainEvent` | User joins room | Notify members |
| `UserLeftRoomDomainEvent` | User leaves room | Notify members |
| `TypingDomainEvent` | User typing | Broadcast to room |
| `ChatUserCreatedDomainEvent` | New chat user | Initialize presence |
| `UserKeyBundleUpdatedDomainEvent` | Keys updated | Invalidate cached keys |
## SignalR Hub Architecture
### Connection Lifecycle
@@ -63,74 +189,47 @@ sequenceDiagram
Client->>Hub: Connect (JWT Token)
Hub->>Hub: OnConnectedAsync()
Hub->>DB: Load user rooms
Hub->>Groups: AddToGroupAsync(roomId)
Hub->>DB: Load user conversations
Hub->>Groups: AddToGroupAsync(conversationId)
Hub->>Redis: Publish(UserOnline)
Hub-->>Client: Connected
Note over Client,Hub: User is now online
Client->>Hub: SendMessage(roomId, content)
Hub->>DB: Save message
Client->>Hub: SendMessage(conversationId, encryptedContent)
Hub->>DB: Save encrypted message
Hub->>Redis: Publish(NewMessage)
Redis-->>Hub: Broadcast to all instances
Hub->>Groups: Clients.Group(roomId)
Hub->>Groups: Clients.Group(conversationId)
Hub-->>Client: ReceiveMessage
style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
style Redis fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
style DB fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
### Hub Implementation
### Hub Methods
```csharp
public class ChatHub : Hub<IChatClient>
public class ChatHub : Hub<IChatHubClient>
{
private readonly IChatRoomRepository _roomRepository;
private readonly IMessageRepository _messageRepository;
private readonly IAIService _aiService;
// Connection management
public override Task OnConnectedAsync();
public override Task OnDisconnectedAsync(Exception? exception);
public async Task JoinRoom(Guid roomId)
{
var userId = Context.UserIdentifier;
// Add to SignalR Group
await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
// Notify other members
await Clients.Group(roomId.ToString())
.UserJoined(userId, roomId);
// Persist state to DB
await _roomRepository.AddParticipantAsync(roomId, userId);
}
// Room management
public Task JoinRoom(Guid conversationId);
public Task LeaveRoom(Guid conversationId);
public async Task SendMessage(Guid roomId, string content)
{
var message = new Message(roomId, Context.UserIdentifier, content);
await _messageRepository.AddAsync(message);
// Broadcast message
await Clients.Group(roomId.ToString())
.ReceiveMessage(message.ToDto());
// Check AI trigger
if (content.StartsWith("@gpt "))
{
await StreamAIResponse(roomId, content[5..]);
}
}
// Messaging
public Task SendMessage(Guid conversationId, string encryptedContent,
string nonce, string? authTag);
public Task SendTypingIndicator(Guid conversationId, bool isTyping);
public Task MarkMessageRead(Guid conversationId, Guid messageId);
public async IAsyncEnumerable<string> StreamAIResponse(
Guid roomId,
string prompt,
[EnumeratorCancellation] CancellationToken ct = default)
{
// Get chat history for context
var history = await _messageRepository.GetHistoryAsync(roomId, 20);
await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
{
yield return chunk;
}
}
// AI Integration
public IAsyncEnumerable<string> StreamAIResponse(Guid conversationId,
string prompt);
}
```
@@ -156,7 +255,9 @@ graph LR
CH -->|Subscribe| H2
H2 -->|Deliver| C2
style CH fill:#e74c3c,stroke:#c0392b,color:#fff
style CH fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
style H1 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
style H2 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
```
### Scaling Options
@@ -167,86 +268,6 @@ graph LR
| **Azure SignalR** | Serverless, no sticky sessions | Vendor lock-in, cost |
| **Sticky Sessions** | Simplest | Imperfect for failover |
```csharp
// Redis Backplane Configuration
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration.ChannelPrefix =
RedisChannel.Literal("ChatService");
options.Configuration.AbortOnConnectFail = false;
});
// Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString = azureSignalRConnectionString;
options.ServerStickyMode = ServerStickyMode.Required;
});
```
## User & Group Management
### User Mapping
```mermaid
graph TB
subgraph "User Mapping"
U1[User ID: user-123]
C1[Connection 1<br/>Phone]
C2[Connection 2<br/>Laptop]
C3[Connection 3<br/>Tablet]
end
U1 --> C1 & C2 & C3
style U1 fill:#3498db,stroke:#2980b9,color:#fff
```
### IUserIdProvider
```csharp
public class ClaimsUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
{
// Get User ID from JWT Claims
return connection.User?
.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
}
// Registration
builder.Services.AddSingleton<IUserIdProvider, ClaimsUserIdProvider>();
```
### Group State Management
```csharp
// Problem: SignalR Groups don't persist on restart
// Solution: Store in database
public class GroupStateService
{
private readonly IDistributedCache _cache;
private readonly IChatRoomRepository _repository;
public async Task RestoreUserGroups(string userId, string connectionId)
{
var rooms = await _repository.GetUserRoomsAsync(userId);
foreach (var room in rooms)
{
await _hubContext.Groups.AddToGroupAsync(
connectionId,
room.Id.ToString()
);
}
}
}
```
## AI Integration Flow
```mermaid
@@ -270,74 +291,10 @@ sequenceDiagram
Hub->>DB: Save AI response
Hub-->>User: AIResponseComplete
```
## Domain Model
### Aggregate Roots
```mermaid
erDiagram
ChatRoom ||--o{ Message : contains
ChatRoom ||--o{ Participant : has
ChatRoom {
uuid id PK
string name
enum type
uuid owner_id
timestamp created_at
}
Message {
uuid id PK
uuid room_id FK
uuid sender_id
string content
enum type
timestamp sent_at
}
Participant {
uuid id PK
uuid room_id FK
uuid user_id
enum role
timestamp joined_at
timestamp last_read
}
```
### Domain Events
| Event | Trigger | Handler |
|-------|---------|---------|
| `MessageSentEvent` | New message | Update last_activity, broadcast |
| `UserJoinedRoomEvent` | User joins room | Notify members |
| `UserLeftRoomEvent` | User leaves room | Notify members |
| `RoomCreatedEvent` | Create new room | Add creator as admin |
## MessagePack Protocol
### JSON vs MessagePack Comparison
| Metric | JSON | MessagePack |
|--------|------|-------------|
| Size | 100% | ~50-70% |
| Parse time | 1x | 0.5-0.8x |
| Type safety | Weak | Strong |
### Configuration
```csharp
// Server
builder.Services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.SerializerOptions =
MessagePackSerializerOptions.Standard
.WithSecurity(MessagePackSecurity.UntrustedData)
.WithCompression(MessagePackCompression.Lz4BlockArray);
});
style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
style OpenAI fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
## Resiliency Patterns
@@ -413,32 +370,6 @@ spec:
targetPort: 8080
```
### Ingress with WebSocket Support
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: chatservice
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
nginx.ingress.kubernetes.io/affinity: "cookie"
spec:
rules:
- host: chat.goodgo.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: chatservice
port:
number: 80
```
## Health Checks
```csharp
@@ -452,5 +383,5 @@ builder.Services.AddHealthChecks()
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [SignalR Scale-out with Redis](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
- [Stateful Reconnect](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration)
- [X3DH Protocol Specification](https://signal.org/docs/specifications/x3dh/)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)

View File

@@ -1,14 +1,15 @@
# Chat Service
> Real-time chat service for GoodGo platform, built on ASP.NET Core SignalR.
> Real-time chat service with End-to-End Encryption (E2EE) for GoodGo platform, built on ASP.NET Core SignalR.
## Overview
Chat Service provides real-time communication capabilities in the microservices system with:
Chat Service provides secure real-time communication in the microservices system with:
- **End-to-End Encryption** - X3DH key exchange protocol with AES-256-GCM
- **Real-time Communication** - ASP.NET Core SignalR with WebSockets/SSE/Long Polling
- **Scalability** - Redis Backplane or Azure SignalR Service
- **User & Group Management** - Chat rooms, cross-device user mapping
- **User & Group Management** - Conversations, multi-device user mapping
- **AI Integration** - Smart chatbot with streaming response
- **High Performance** - MessagePack protocol
- **Resiliency** - Auto reconnect, Stateful Reconnect
@@ -54,7 +55,27 @@ dotnet run --project src/ChatService.API
## Feature Details
### A. Real-time Communication
### A. End-to-End Encryption (E2EE)
| Feature | Description |
|---------|-------------|
| **X3DH Protocol** | Extended Triple Diffie-Hellman for key exchange |
| **Identity Keys** | Long-term Curve25519 identity key pair |
| **Signed Pre-Keys** | Rotated periodically (every 30 days) |
| **One-Time Pre-Keys** | Forward secrecy for initial messages |
| **AES-256-GCM** | Symmetric encryption for messages |
```csharp
// Server stores ONLY encrypted content - cannot decrypt without client's private key
public class Message
{
public string EncryptedContent { get; } // Base64 encoded
public string Nonce { get; } // IV for AES-GCM
public string? AuthTag { get; } // Authentication tag
}
```
### B. Real-time Communication
| Feature | Description |
|---------|-------------|
@@ -62,19 +83,7 @@ dotnet run --project src/ChatService.API
| **Multi-Transport** | Auto-select WebSockets → SSE → Long Polling |
| **Streaming** | IAsyncEnumerable support for streaming AI responses |
```csharp
// Send message to group
await Clients.Group(roomId).SendAsync("ReceiveMessage", message);
// Streaming AI response
public async IAsyncEnumerable<string> StreamAIResponse(string prompt)
{
await foreach (var chunk in _aiService.StreamAsync(prompt))
yield return chunk;
}
```
### B. Scalability
### C. Scalability
| Solution | Use Case |
|----------|----------|
@@ -82,91 +91,58 @@ public async IAsyncEnumerable<string> StreamAIResponse(string prompt)
| **Azure SignalR Service** | Azure cloud, serverless scenarios |
| **Sticky Sessions** | Fallback when not using Azure SignalR |
```csharp
// Configure Redis Backplane
builder.Services.AddSignalR()
.AddStackExchangeRedis(connectionString, options => {
options.Configuration.ChannelPrefix = RedisChannel.Literal("ChatService");
});
```
### C. User & Group Management
| Feature | Description |
|---------|-------------|
| **Groups** | Group connections by chat room |
| **User Mapping** | Map ConnectionId → UserId via Claims |
| **Persistent State** | Save group state to database |
```csharp
// Custom User ID Provider
public class CustomUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
=> connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
```
### D. AI Integration
| Feature | Description |
|---------|-------------|
| **AI Assistant** | In-group chatbot (trigger: @gpt) |
| **AI Assistant** | Chatbot in group (trigger: @gpt) |
| **Streaming Response** | Push response chunks in real-time |
| **Context History** | Save chat history for AI context |
### E. MessagePack Protocol
```csharp
// Server configuration
builder.Services.AddSignalR()
.AddMessagePackProtocol();
// Client configuration (JavaScript)
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
.build();
```
### F. Resiliency
| Feature | Description |
|---------|-------------|
| **Auto Reconnect** | Client auto-reconnects |
| **Stateful Reconnect** | Buffer data during interruption (.NET 8+) |
```csharp
// Server: Enable Stateful Reconnect
builder.Services.AddSignalR(options => {
options.EnableDetailedErrors = true;
options.StatefulReconnectBufferSize = 32 * 1024; // 32KB buffer
});
// Client: Configure reconnect
connection.WithAutomaticReconnect([0, 2000, 10000, 30000]);
```
## API Endpoints
### HTTP REST APIs
### Conversations API
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/rooms` | Get chat room list |
| `POST` | `/api/v1/rooms` | Create new chat room |
| `GET` | `/api/v1/rooms/{id}/messages` | Get message history |
| `POST` | `/api/v1/rooms/{id}/participants` | Add participant |
| `POST` | `/api/conversations` | Create new conversation |
| `GET` | `/api/conversations` | Get user's conversations |
| `GET` | `/api/conversations/{id}` | Get specific conversation |
### Messages API
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/messages` | Send encrypted message |
| `GET` | `/api/messages/conversation/{id}` | Get messages in conversation |
| `POST` | `/api/messages/read` | Mark messages as read |
### Keys API (E2EE)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/keys/register` | Register E2EE key bundle |
| `POST` | `/api/keys/rotate` | Rotate signed pre-key |
| `POST` | `/api/keys/prekeys` | Upload one-time pre-keys |
| `GET` | `/api/keys/bundle/{userId}` | Get user's key bundle |
| `GET` | `/api/keys/my-bundle` | Get own key bundle status |
### SignalR Hub Methods
| Method | Direction | Description |
|--------|-----------|-------------|
| `SendMessage` | Client → Server | Send message |
| `JoinRoom` | Client → Server | Join room |
| `LeaveRoom` | Client → Server | Leave room |
| `ReceiveMessage` | Server → Client | Receive message |
| `UserJoined` | Server → Client | New user notification |
| `JoinRoom` | Client → Server | Join conversation room |
| `LeaveRoom` | Client → Server | Leave conversation room |
| `SendMessage` | Client → Server | Send encrypted message |
| `SendTypingIndicator` | Client → Server | Send typing status |
| `MarkMessageRead` | Client → Server | Mark message as read |
| `StreamAIResponse` | Client → Server | Request AI streaming response |
| `ReceiveMessage` | Server → Client | Receive new message |
| `UserJoined` | Server → Client | User joined notification |
| `UserLeft` | Server → Client | User left notification |
| `TypingIndicator` | Server → Client | Typing indicator |
| `MessageDelivered` | Server → Client | Message delivered status |
| `MessageRead` | Server → Client | Message read status |
### Health Endpoints
@@ -226,7 +202,7 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed Kubernetes configuration w
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [Redis Backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
- [MessagePack Protocol](https://docs.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol)
- [X3DH Protocol](https://signal.org/docs/specifications/x3dh/)
## License

View File

@@ -1,6 +1,6 @@
# Tài Liệu Kiến Trúc Chat Service
> Kiến trúc chi tiết cho Chat Service với SignalR, scalability patterns, và AI integration.
> Kiến trúc chi tiết cho Chat Service với E2EE, SignalR, scalability patterns, và AI integration.
## Tổng Quan Kiến Trúc
@@ -43,12 +43,138 @@ graph TB
CS1 & CS2 & CS3 --> RC
CS1 & CS2 & CS3 --> AI
style LB fill:#3498db,stroke:#2980b9,color:#fff
style RD fill:#e74c3c,stroke:#c0392b,color:#fff
style PG fill:#27ae60,stroke:#1e8449,color:#fff
style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
style LB fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
style RD fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
style PG fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
```
## Mã Hóa Đầu-cuối (E2EE)
### Giao Thức Trao Đổi Khóa X3DH
Service triển khai giao thức Extended Triple Diffie-Hellman (X3DH) để thiết lập phiên mã hóa:
```mermaid
sequenceDiagram
participant Alice as Alice (Người gửi)
participant Server as Chat Server
participant Bob as Bob (Người nhận)
Note over Bob,Server: Bob đăng ký keys
Bob->>Server: Đăng ký Key Bundle<br/>(Identity, SignedPreKey, OneTimePreKeys)
Server->>Server: Lưu chỉ public keys
Note over Alice,Server: Alice khởi tạo session
Alice->>Server: Yêu cầu Key Bundle của Bob
Server->>Alice: {IdentityKey, SignedPreKey, OneTimePreKey}
Alice->>Alice: X3DH → Session Key
Alice->>Alice: AES-256-GCM mã hóa tin nhắn
Alice->>Server: Gửi tin nhắn đã mã hóa
Server->>Bob: Chuyển tin nhắn đã mã hóa
Bob->>Bob: X3DH → Session Key
Bob->>Bob: AES-256-GCM giải mã tin nhắn
style Server fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
```
### Các Thành Phần Khóa
| Thành phần | Mô tả | Lưu trữ |
|------------|-------|---------|
| **Identity Key** | Cặp khóa Curve25519 dài hạn | Private: Chỉ client |
| **Signed Pre-Key** | Xoay vòng mỗi 30 ngày | Public: Server |
| **One-Time Pre-Keys** | Tiêu thụ mỗi session | Public: Server (xóa sau khi dùng) |
| **Session Key** | Derive qua X3DH | Không lưu trữ |
### Tính Năng Bảo Mật
-**Forward Secrecy**: Khóa bị lộ không tiết lộ tin nhắn cũ
-**Zero-Knowledge Server**: Server không thể giải mã tin nhắn
-**Deniability**: Không có bằng chứng mật mã về tác giả
## Domain Model
### Aggregate Roots
```mermaid
erDiagram
Conversation ||--o{ Message : contains
Conversation ||--o{ ConversationParticipant : has
ChatUser ||--o{ Conversation : participates
ChatUser ||--|| UserKeyBundle : has
ChatUser ||--o{ OneTimePreKey : owns
Conversation {
uuid Id PK
string Name
string AvatarUrl
enum Type "Direct|Group"
uuid CreatorId
datetime CreatedAt
datetime LastActivityAt
}
Message {
uuid Id PK
uuid ConversationId FK
uuid SenderId FK
string EncryptedContent
string Nonce
string AuthTag
enum Type "Text|Image|File|System"
enum Status "Sent|Delivered|Read|Failed"
datetime CreatedAt
}
ConversationParticipant {
uuid Id PK
uuid ConversationId FK
uuid ChatUserId FK
enum Role "Owner|Admin|Member"
datetime JoinedAt
datetime LastReadAt
}
ChatUser {
uuid Id PK
string IdentityUserId
string DisplayName
string AvatarUrl
enum Status "Online|Away|Offline"
datetime LastSeenAt
}
UserKeyBundle {
string IdentityPublicKey
string SignedPreKey
string SignedPreKeySignature
datetime SignedPreKeyTimestamp
}
OneTimePreKey {
uuid Id PK
int KeyId
string PublicKey
bool IsUsed
datetime CreatedAt
}
```
### Domain Events
| Event | Trigger | Handler |
|-------|---------|---------|
| `ConversationCreatedDomainEvent` | Hội thoại mới được tạo | Thông báo participants |
| `MessageSentDomainEvent` | Tin nhắn được gửi | Cập nhật last_activity, broadcast |
| `MessageDeliveredDomainEvent` | Tin nhắn đã gửi đến | Thông báo sender |
| `MessageReadDomainEvent` | Tin nhắn đã đọc | Thông báo sender |
| `UserJoinedRoomDomainEvent` | User tham gia room | Thông báo members |
| `UserLeftRoomDomainEvent` | User rời room | Thông báo members |
| `TypingDomainEvent` | User đang gõ | Broadcast to room |
| `ChatUserCreatedDomainEvent` | Chat user mới | Khởi tạo presence |
| `UserKeyBundleUpdatedDomainEvent` | Keys được cập nhật | Invalidate cached keys |
## SignalR Hub Architecture
### Connection Lifecycle
@@ -63,74 +189,47 @@ sequenceDiagram
Client->>Hub: Connect (JWT Token)
Hub->>Hub: OnConnectedAsync()
Hub->>DB: Load user rooms
Hub->>Groups: AddToGroupAsync(roomId)
Hub->>DB: Load danh sách hội thoại
Hub->>Groups: AddToGroupAsync(conversationId)
Hub->>Redis: Publish(UserOnline)
Hub-->>Client: Connected
Note over Client,Hub: User is now online
Note over Client,Hub: User hiện đang online
Client->>Hub: SendMessage(roomId, content)
Hub->>DB: Save message
Client->>Hub: SendMessage(conversationId, encryptedContent)
Hub->>DB: Lưu tin nhắn đã mã hóa
Hub->>Redis: Publish(NewMessage)
Redis-->>Hub: Broadcast to all instances
Hub->>Groups: Clients.Group(roomId)
Redis-->>Hub: Broadcast đến tất cả instances
Hub->>Groups: Clients.Group(conversationId)
Hub-->>Client: ReceiveMessage
style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
style Redis fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
style DB fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
### Hub Implementation
### Hub Methods
```csharp
public class ChatHub : Hub<IChatClient>
public class ChatHub : Hub<IChatHubClient>
{
private readonly IChatRoomRepository _roomRepository;
private readonly IMessageRepository _messageRepository;
private readonly IAIService _aiService;
// Quản lý kết nối
public override Task OnConnectedAsync();
public override Task OnDisconnectedAsync(Exception? exception);
public async Task JoinRoom(Guid roomId)
{
var userId = Context.UserIdentifier;
// Thêm vào SignalR Group
await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
// Thông báo cho members khác
await Clients.Group(roomId.ToString())
.UserJoined(userId, roomId);
// Lưu trạng thái vào DB
await _roomRepository.AddParticipantAsync(roomId, userId);
}
// Quản lý phòng
public Task JoinRoom(Guid conversationId);
public Task LeaveRoom(Guid conversationId);
public async Task SendMessage(Guid roomId, string content)
{
var message = new Message(roomId, Context.UserIdentifier, content);
await _messageRepository.AddAsync(message);
// Broadcast tin nhắn
await Clients.Group(roomId.ToString())
.ReceiveMessage(message.ToDto());
// Kiểm tra AI trigger
if (content.StartsWith("@gpt "))
{
await StreamAIResponse(roomId, content[5..]);
}
}
// Nhắn tin
public Task SendMessage(Guid conversationId, string encryptedContent,
string nonce, string? authTag);
public Task SendTypingIndicator(Guid conversationId, bool isTyping);
public Task MarkMessageRead(Guid conversationId, Guid messageId);
public async IAsyncEnumerable<string> StreamAIResponse(
Guid roomId,
string prompt,
[EnumeratorCancellation] CancellationToken ct = default)
{
// Lấy lịch sử chat cho context
var history = await _messageRepository.GetHistoryAsync(roomId, 20);
await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
{
yield return chunk;
}
}
// Tích hợp AI
public IAsyncEnumerable<string> StreamAIResponse(Guid conversationId,
string prompt);
}
```
@@ -156,7 +255,9 @@ graph LR
CH -->|Subscribe| H2
H2 -->|Deliver| C2
style CH fill:#e74c3c,stroke:#c0392b,color:#fff
style CH fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
style H1 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
style H2 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
```
### Cấu Hình Scaling
@@ -167,86 +268,6 @@ graph LR
| **Azure SignalR** | Serverless, không sticky sessions | Vendor lock-in, chi phí |
| **Sticky Sessions** | Đơn giản nhất | Không hoàn hảo cho failover |
```csharp
// Redis Backplane Configuration
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration.ChannelPrefix =
RedisChannel.Literal("ChatService");
options.Configuration.AbortOnConnectFail = false;
});
// Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString = azureSignalRConnectionString;
options.ServerStickyMode = ServerStickyMode.Required;
});
```
## User & Group Management
### User Mapping
```mermaid
graph TB
subgraph "User Mapping"
U1[User ID: user-123]
C1[Connection 1<br/>Phone]
C2[Connection 2<br/>Laptop]
C3[Connection 3<br/>Tablet]
end
U1 --> C1 & C2 & C3
style U1 fill:#3498db,stroke:#2980b9,color:#fff
```
### IUserIdProvider
```csharp
public class ClaimsUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
{
// Lấy User ID từ JWT Claims
return connection.User?
.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
}
// Registration
builder.Services.AddSingleton<IUserIdProvider, ClaimsUserIdProvider>();
```
### Group State Management
```csharp
// Vấn đề: SignalR Groups không persist khi restart
// Giải pháp: Lưu trữ trong database
public class GroupStateService
{
private readonly IDistributedCache _cache;
private readonly IChatRoomRepository _repository;
public async Task RestoreUserGroups(string userId, string connectionId)
{
var rooms = await _repository.GetUserRoomsAsync(userId);
foreach (var room in rooms)
{
await _hubContext.Groups.AddToGroupAsync(
connectionId,
room.Id.ToString()
);
}
}
}
```
## AI Integration Flow
```mermaid
@@ -258,7 +279,7 @@ sequenceDiagram
participant DB as Database
User->>Hub: "@gpt Giải thích DDD"
Hub->>DB: Load history (20 messages)
Hub->>DB: Load lịch sử (20 tin nhắn)
Hub->>AI: StreamAsync(prompt, history)
AI->>OpenAI: ChatCompletion (stream: true)
@@ -268,76 +289,12 @@ sequenceDiagram
Hub-->>User: ReceiveAIChunk
end
Hub->>DB: Save AI response
Hub->>DB: Lưu AI response
Hub-->>User: AIResponseComplete
```
## Domain Model
### Aggregate Roots
```mermaid
erDiagram
ChatRoom ||--o{ Message : contains
ChatRoom ||--o{ Participant : has
ChatRoom {
uuid id PK
string name
enum type
uuid owner_id
timestamp created_at
}
Message {
uuid id PK
uuid room_id FK
uuid sender_id
string content
enum type
timestamp sent_at
}
Participant {
uuid id PK
uuid room_id FK
uuid user_id
enum role
timestamp joined_at
timestamp last_read
}
```
### Domain Events
| Event | Trigger | Handler |
|-------|---------|---------|
| `MessageSentEvent` | Tin nhắn mới | Update last_activity, broadcast |
| `UserJoinedRoomEvent` | User join room | Notify members |
| `UserLeftRoomEvent` | User leave room | Notify members |
| `RoomCreatedEvent` | Tạo room mới | Add creator as admin |
## MessagePack Protocol
### So sánh JSON vs MessagePack
| Metric | JSON | MessagePack |
|--------|------|-------------|
| Size | 100% | ~50-70% |
| Parse time | 1x | 0.5-0.8x |
| Type safety | Weak | Strong |
### Cấu Hình
```csharp
// Server
builder.Services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.SerializerOptions =
MessagePackSerializerOptions.Standard
.WithSecurity(MessagePackSecurity.UntrustedData)
.WithCompression(MessagePackCompression.Lz4BlockArray);
});
style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
style OpenAI fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
## Resiliency Patterns
@@ -347,11 +304,11 @@ builder.Services.AddSignalR()
```mermaid
stateDiagram-v2
[*] --> Connected
Connected --> Disconnected: Connection lost
Disconnected --> Reconnecting: Auto retry
Reconnecting --> Connected: Success
Reconnecting --> Reconnecting: Retry with backoff
Reconnecting --> Disconnected: Max retries exceeded
Connected --> Disconnected: Mất kết nối
Disconnected --> Reconnecting: Tự động retry
Reconnecting --> Connected: Thành công
Reconnecting --> Reconnecting: Retry với backoff
Reconnecting --> Disconnected: Vượt quá max retries
Disconnected --> [*]: User logout
```
@@ -413,32 +370,6 @@ spec:
targetPort: 8080
```
### Ingress với WebSocket Support
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: chatservice
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
nginx.ingress.kubernetes.io/affinity: "cookie"
spec:
rules:
- host: chat.goodgo.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: chatservice
port:
number: 80
```
## Health Checks
```csharp
@@ -452,5 +383,5 @@ builder.Services.AddHealthChecks()
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [SignalR Scale-out with Redis](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
- [Stateful Reconnect](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration)
- [X3DH Protocol Specification](https://signal.org/docs/specifications/x3dh/)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)

View File

@@ -1,14 +1,15 @@
# Chat Service
> Dịch vụ Chat thời gian thực cho nền tảng GoodGo, xây dựng trên ASP.NET Core SignalR.
> Dịch vụ Chat thời gian thực với Mã hóa Đầu-cuối (E2EE) cho nền tảng GoodGo, xây dựng trên ASP.NET Core SignalR.
## Tổng Quan
Chat Service cung cấp khả năng giao tiếp thời gian thực trong hệ thống microservices với:
Chat Service cung cấp khả năng giao tiếp bảo mật thời gian thực trong hệ thống microservices với:
- **Mã hóa Đầu-cuối** - Giao thức trao đổi khóa X3DH với AES-256-GCM
- **Giao tiếp thời gian thực** - ASP.NET Core SignalR với WebSockets/SSE/Long Polling
- **Khả năng mở rộng** - Redis Backplane hoặc Azure SignalR Service
- **Quản lý người dùng & nhóm** - Phòng chat, ánh xạ user across devices
- **Quản lý người dùng & nhóm** - Hội thoại, ánh xạ user đa thiết bị
- **Tích hợp AI** - Chatbot thông minh với streaming response
- **Hiệu năng cao** - Giao thức MessagePack
- **Khả năng phục hồi** - Auto reconnect, Stateful Reconnect
@@ -54,59 +55,42 @@ dotnet run --project src/ChatService.API
## Tính Năng Chi Tiết
### A. Giao Tiếp Thời Gian Thực
### A. Mã Hóa Đầu-cuối (E2EE)
| Tính năng | Mô tả |
|-----------|-------|
| **SignalR Hub** | Hub trung tâm xử lý tất cả real-time connections |
| **Giao thức X3DH** | Extended Triple Diffie-Hellman cho trao đổi khóa |
| **Identity Keys** | Cặp khóa định danh Curve25519 dài hạn |
| **Signed Pre-Keys** | Xoay vòng định kỳ (mỗi 30 ngày) |
| **One-Time Pre-Keys** | Forward secrecy cho tin nhắn đầu tiên |
| **AES-256-GCM** | Mã hóa đối xứng cho tin nhắn |
```csharp
// Server CHỈ lưu nội dung đã mã hóa - không thể giải mã mà không có private key của client
public class Message
{
public string EncryptedContent { get; } // Mã hóa Base64
public string Nonce { get; } // IV cho AES-GCM
public string? AuthTag { get; } // Authentication tag
}
```
### B. Giao Tiếp Thời Gian Thực
| Tính năng | Mô tả |
|-----------|-------|
| **SignalR Hub** | Hub trung tâm xử lý tất cả kết nối realtime |
| **Multi-Transport** | Tự động chọn WebSockets → SSE → Long Polling |
| **Streaming** | Hỗ trợ IAsyncEnumerable cho streaming AI responses |
```csharp
// Gửi tin nhắn đến nhóm
await Clients.Group(roomId).SendAsync("ReceiveMessage", message);
// Streaming AI response
public async IAsyncEnumerable<string> StreamAIResponse(string prompt)
{
await foreach (var chunk in _aiService.StreamAsync(prompt))
yield return chunk;
}
```
### B. Khả Năng Mở Rộng
### C. Khả Năng Mở Rộng
| Giải pháp | Use Case |
|-----------|----------|
| **Redis Backplane** | On-premise, multi-instance deployment |
| **Redis Backplane** | On-premise, triển khai multi-instance |
| **Azure SignalR Service** | Azure cloud, serverless scenarios |
| **Sticky Sessions** | Fallback khi không dùng Azure SignalR |
```csharp
// Cấu hình Redis Backplane
builder.Services.AddSignalR()
.AddStackExchangeRedis(connectionString, options => {
options.Configuration.ChannelPrefix = RedisChannel.Literal("ChatService");
});
```
### C. Quản Lý Người Dùng & Nhóm
| Tính năng | Mô tả |
|-----------|-------|
| **Groups** | Gom kết nối theo phòng chat |
| **User Mapping** | Ánh xạ ConnectionId → UserId qua Claims |
| **Persistent State** | Lưu trạng thái nhóm vào database |
```csharp
// Custom User ID Provider
public class CustomUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
=> connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
```
### D. Tích Hợp AI
| Tính năng | Mô tả |
@@ -115,58 +99,50 @@ public class CustomUserIdProvider : IUserIdProvider
| **Streaming Response** | Đẩy từng phần câu trả lời realtime |
| **Context History** | Lưu lịch sử chat cho context AI |
### E. Giao Thức MessagePack
```csharp
// Server configuration
builder.Services.AddSignalR()
.AddMessagePackProtocol();
// Client configuration (JavaScript)
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
.build();
```
### F. Khả Năng Phục Hồi
| Tính năng | Mô tả |
|-----------|-------|
| **Auto Reconnect** | Client tự động kết nối lại |
| **Stateful Reconnect** | Buffer dữ liệu khi gián đoạn (.NET 8+) |
```csharp
// Server: Bật Stateful Reconnect
builder.Services.AddSignalR(options => {
options.EnableDetailedErrors = true;
options.StatefulReconnectBufferSize = 32 * 1024; // 32KB buffer
});
// Client: Cấu hình reconnect
connection.WithAutomaticReconnect([0, 2000, 10000, 30000]);
```
## API Endpoints
### HTTP REST APIs
### Conversations API
| Method | Endpoint | Mô tả |
|--------|----------|-------|
| `GET` | `/api/v1/rooms` | Lấy danh sách phòng chat |
| `POST` | `/api/v1/rooms` | Tạo phòng chat mới |
| `GET` | `/api/v1/rooms/{id}/messages` | Lấy lịch sử tin nhắn |
| `POST` | `/api/v1/rooms/{id}/participants` | Thêm thành viên |
| `POST` | `/api/conversations` | Tạo hội thoại mới |
| `GET` | `/api/conversations` | Lấy danh sách hội thoại của user |
| `GET` | `/api/conversations/{id}` | Lấy hội thoại cụ thể |
### Messages API
| Method | Endpoint | Mô tả |
|--------|----------|-------|
| `POST` | `/api/messages` | Gửi tin nhắn đã mã hóa |
| `GET` | `/api/messages/conversation/{id}` | Lấy tin nhắn trong hội thoại |
| `POST` | `/api/messages/read` | Đánh dấu tin nhắn đã đọc |
### Keys API (E2EE)
| Method | Endpoint | Mô tả |
|--------|----------|-------|
| `POST` | `/api/keys/register` | Đăng ký E2EE key bundle |
| `POST` | `/api/keys/rotate` | Xoay vòng signed pre-key |
| `POST` | `/api/keys/prekeys` | Upload one-time pre-keys |
| `GET` | `/api/keys/bundle/{userId}` | Lấy key bundle của user |
| `GET` | `/api/keys/my-bundle` | Lấy trạng thái key bundle của mình |
### SignalR Hub Methods
| Method | Direction | Mô tả |
|--------|-----------|-------|
| `SendMessage` | Client → Server | Gửi tin nhắn |
| `JoinRoom` | Client → Server | Tham gia phòng |
| `LeaveRoom` | Client → Server | Rời phòng |
| `ReceiveMessage` | Server → Client | Nhận tin nhắn |
| `UserJoined` | Server → Client | Thông báo user mới |
| Method | Hướng | Mô tả |
|--------|-------|-------|
| `JoinRoom` | Client → Server | Tham gia phòng hội thoại |
| `LeaveRoom` | Client → Server | Rời phòng hội thoại |
| `SendMessage` | Client → Server | Gửi tin nhắn đã mã hóa |
| `SendTypingIndicator` | Client → Server | Gửi trạng thái đang gõ |
| `MarkMessageRead` | Client → Server | Đánh dấu tin nhắn đã đọc |
| `StreamAIResponse` | Client → Server | Yêu cầu AI streaming response |
| `ReceiveMessage` | Server → Client | Nhận tin nhắn mới |
| `UserJoined` | Server → Client | Thông báo user tham gia |
| `UserLeft` | Server → Client | Thông báo user rời đi |
| `TypingIndicator` | Server → Client | Chỉ báo đang gõ |
| `MessageDelivered` | Server → Client | Trạng thái tin nhắn đã gửi |
| `MessageRead` | Server → Client | Trạng thái tin nhắn đã đọc |
### Health Endpoints
@@ -226,7 +202,7 @@ Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để biết chi tiết cấu hình Kub
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [Redis Backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
- [MessagePack Protocol](https://docs.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol)
- [Giao thức X3DH](https://signal.org/docs/specifications/x3dh/)
## Giấy Phép

View File

@@ -0,0 +1,34 @@
# Membership Service
> **EN**: [English Documentation](docs/en/README.md)
> **VI**: [Tài liệu Tiếng Việt](docs/vi/README.md)
## Quick Links
- 📖 [Architecture Documentation](docs/en/ARCHITECTURE.md) / [Tài liệu Kiến trúc](docs/vi/ARCHITECTURE.md)
- 🚀 [Quick Start](docs/en/README.md#quick-start)
- 🔧 [Configuration](docs/en/README.md#configuration)
## Tech Stack
- .NET 10
- PostgreSQL
- Redis (caching)
- MediatR (CQRS)
- FluentValidation
## Development
```bash
# Restore and build
dotnet restore
dotnet build
# Run the API
dotnet run --project src/MembershipService.API
# Run tests
dotnet test
```
See detailed documentation in [docs/en/](docs/en/) or [docs/vi/](docs/vi/).

View File

@@ -584,7 +584,6 @@ PUT /api/v1/members/{id} # Update profile
```
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)

View File

@@ -121,9 +121,11 @@ membership-service-net/
| `GET` | `/api/v1/members` | Get paginated members (with search) | Yes |
| `GET` | `/api/v1/members/{id}` | Get member by ID | Yes |
| `GET` | `/api/v1/members/me` | Get current user's profile | Yes |
| `GET` | `/api/v1/members/{id}/progress` | Get member's level progress | Yes |
| `GET` | `/api/v1/members/{id}/experience` | Get member's EXP history | Yes |
| `POST` | `/api/v1/members` | Create new member | Yes |
| `POST` | `/api/v1/members/{id}/experience` | Add experience points | Yes |
| `PUT` | `/api/v1/members/{id}` | Update member profile | Yes |
| `PUT` | `/api/v1/members/{id}/level` | Change membership level | Yes |
### Health Endpoints

View File

@@ -584,7 +584,6 @@ PUT /api/v1/members/{id} # Cập nhật hồ sơ
```
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)

View File

@@ -121,9 +121,11 @@ membership-service-net/
| `GET` | `/api/v1/members` | Lấy danh sách thành viên (phân trang, search) | Có |
| `GET` | `/api/v1/members/{id}` | Lấy thành viên theo ID | Có |
| `GET` | `/api/v1/members/me` | Lấy hồ sơ người dùng hiện tại | Có |
| `GET` | `/api/v1/members/{id}/progress` | Lấy tiến độ level của thành viên | Có |
| `GET` | `/api/v1/members/{id}/experience` | Lấy lịch sử EXP của thành viên | Có |
| `POST` | `/api/v1/members` | Tạo thành viên mới | Có |
| `POST` | `/api/v1/members/{id}/experience` | Thêm điểm kinh nghiệm | Có |
| `PUT` | `/api/v1/members/{id}` | Cập nhật hồ sơ thành viên | Có |
| `PUT` | `/api/v1/members/{id}/level` | Thay đổi cấp thành viên | Có |
### Health Endpoints

View File

@@ -4,18 +4,36 @@
The Wallet Service manages digital wallets and loyalty point accounts for the GoodGo Platform.
```
┌─────────────────────────────────────────────────────────────┐
│ Wallet Service │
├─────────────────────────────────────────────────────────────┤
API Layer (Controllers, CQRS) │
├─────────────────────────────────────────────────────────────┤
Domain Layer (Wallet, PointAccount Aggregates) │
├─────────────────────────────────────────────────────────────┤
Infrastructure Layer (EF Core, Repositories) │
├─────────────────────────────────────────────────────────────┤
│ PostgreSQL Database │
└─────────────────────────────────────────────────────────────┘
```mermaid
flowchart TB
subgraph API["🌐 API Layer"]
Controllers["Controllers"]
Commands["Commands"]
Queries["Queries"]
end
subgraph Domain["💎 Domain Layer"]
Wallet["Wallet Aggregate"]
PointAccount["PointAccount Aggregate"]
end
subgraph Infra["⚙️ Infrastructure Layer"]
Repos["Repositories"]
EF["EF Core"]
end
subgraph DB["💾 PostgreSQL"]
Tables["Tables"]
end
API --> Domain
Domain --> Infra
Infra --> DB
style API fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
style Domain fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
style Infra fill:#E67E22,color:#ECF0F1,stroke:#D35400,stroke-width:2px
style DB fill:#34495E,color:#ECF0F1,stroke:#2C3E50,stroke-width:2px
```
## Architecture Patterns
@@ -23,22 +41,33 @@ The Wallet Service manages digital wallets and loyalty point accounts for the Go
### Domain-Driven Design (DDD)
- **Aggregates**: Wallet, PointAccount
- **Entities**: WalletTransaction, PointTransaction
- **Value Objects**: Money, PointBalance
- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned
- **Entities**: WalletTransaction, PointTransaction, HoldItem, WalletItem
- **Value Objects**: Money, CurrencyType
- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned, EscrowHeld, EscrowExecuted
### CQRS Pattern
```
Commands (Write) Queries (Read)
│ │
CreateWallet GetWalletQuery
DepositCommand GetTransactionsQuery
WithdrawCommand GetPointAccountQuery
TransferCommand GetBalanceSummaryQuery
EarnPointsCommand
SpendPointsCommand
```mermaid
flowchart LR
subgraph Commands["Commands (Write)"]
C1["CreateWallet"]
C2["Deposit/Withdraw"]
C3["Exchange"]
C4["Hold/Execute/Release"]
C5["EarnPoints/SpendPoints"]
C6["Admin Commands"]
end
subgraph Queries["Queries (Read)"]
Q1["GetWallet"]
Q2["GetTransactions"]
Q3["GetPointAccount"]
Q4["GetStatistics"]
Q5["Admin Queries"]
end
style Commands fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
style Queries fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
```
## Domain Model
@@ -50,32 +79,68 @@ classDiagram
class Wallet {
+Guid Id
+Guid UserId
+Money Balance
+WalletStatus Status
+List~WalletTransaction~ Transactions
+Deposit(Money, description, ref)
+Withdraw(Money, description, ref)
+CurrencyType DefaultCurrency
+List~WalletItem~ Items
+List~HoldItem~ Holds
+Deposit(amount, currency, desc, ref)
+Withdraw(amount, currency, desc, ref)
+Exchange(fromAmount, fromCurrency, toCurrency)
+Hold(amount, currency, refType, refId, desc)
+ExecuteHold(holdId, amount, ref)
+ReleaseHold(holdId, amount)
+Freeze()
+Unfreeze()
+Close()
}
class WalletItem {
+Guid Id
+CurrencyType Currency
+decimal Balance
+decimal HeldBalance
+decimal AvailableBalance
}
class HoldItem {
+Guid Id
+decimal OriginalAmount
+decimal RemainingAmount
+decimal ExecutedAmount
+decimal ReleasedAmount
+HoldStatus Status
+string ReferenceType
+Guid ReferenceId
+Execute(amount, ref)
+Release(amount)
+Cancel()
}
class WalletTransaction {
+Guid Id
+TransactionType Type
+Money Amount
+Money BalanceAfter
+decimal Amount
+CurrencyType Currency
+decimal BalanceAfter
+string Description
+DateTime CreatedAt
}
class Money {
+decimal Amount
+string Currency
class CurrencyType {
+int Id
+string Name
+decimal BaseExchangeRate
+VND
+USD
+PPoint
+GetExchangeRateTo(currency)
+ConvertTo(amount, currency)
}
Wallet "1" --> "*" WalletItem
Wallet "1" --> "*" HoldItem
Wallet "1" --> "*" WalletTransaction
Wallet --> Money
WalletItem --> CurrencyType
HoldItem --> CurrencyType
```
### PointAccount Aggregate
@@ -85,9 +150,9 @@ classDiagram
class PointAccount {
+Guid Id
+Guid UserId
+int TotalPoints
+int AvailablePoints
+int PendingPoints
+long TotalPoints
+long AvailablePoints
+long PendingPoints
+EarnPoints(points, source, desc, expires)
+SpendPoints(points, source, desc)
+AdjustPoints(points, source, desc)
@@ -96,8 +161,9 @@ classDiagram
class PointTransaction {
+Guid Id
+PointTransactionType Type
+int Points
+int BalanceAfter
+long Points
+long BalanceAfter
+string Source
+DateTime? ExpiresAt
}
@@ -111,52 +177,113 @@ classDiagram
| Table | Description |
|-------|-------------|
| `Wallets` | User wallet accounts |
| `WalletItems` | Currency balances per wallet |
| `WalletTransactions` | Wallet transaction history |
| `HoldItems` | Escrow holds |
| `PointAccounts` | User point accounts |
| `PointTransactions` | Point transaction history |
### Key Indexes
- `IX_Wallets_UserId` - Fast lookup by user
- `IX_WalletItems_WalletId_CurrencyTypeId` - Balance by currency
- `IX_WalletTransactions_WalletId` - Transaction history
- `IX_HoldItems_WalletId_Status` - Active holds
- `IX_PointAccounts_UserId` - Fast lookup by user
## API Flow
### Deposit Flow
```
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
```mermaid
sequenceDiagram
participant C as Client
participant API as Controller
participant H as Handler
participant W as Wallet
participant DB as Database
C->>API: POST /wallets/{userId}/deposit
API->>H: DepositCommand
H->>W: wallet.Deposit(amount)
W->>W: Create Transaction
W->>W: Raise DomainEvent
H->>DB: SaveChanges
DB-->>API: Success
API-->>C: Updated Balance
```
### Transfer Flow
### Escrow Flow
```mermaid
sequenceDiagram
participant C as Client
participant API as Controller
participant W as Wallet
participant H as HoldItem
C->>API: POST /holds (Create)
API->>W: wallet.Hold(amount)
W->>H: Create HoldItem
W-->>C: HoldId
C->>API: POST /holds/{id}/execute
API->>W: wallet.ExecuteHold(id, amount)
W->>H: hold.Execute(amount)
H-->>C: Executed Amount
C->>API: POST /holds/{id}/release
API->>W: wallet.ReleaseHold(id, amount)
W->>H: hold.Release(amount)
H-->>C: Released Amount
```
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
### Currency Exchange Flow
```mermaid
sequenceDiagram
participant C as Client
participant API as Controller
participant W as Wallet
participant CT as CurrencyType
C->>API: POST /exchange
API->>W: wallet.Exchange(100 USD, VND)
W->>CT: GetExchangeRate(USD → VND)
CT-->>W: Rate = 25000
W->>W: Withdraw 100 USD
W->>W: Deposit 2,500,000 VND
W->>W: Raise WalletExchangedEvent
W-->>C: Exchange Result
```
## Domain Events
| Event | Trigger | Data |
|-------|---------|------|
| `WalletCreatedDomainEvent` | Wallet creation | WalletId, UserId |
| `WalletBalanceChangedDomainEvent` | Deposit/Withdraw | WalletId, Amount, Type |
| `WalletExchangedDomainEvent` | Currency exchange | FromCurrency, ToCurrency, Rate |
| `EscrowHeldDomainEvent` | Hold creation | HoldId, Amount, RefType |
| `EscrowExecutedDomainEvent` | Hold execution | HoldId, Amount |
| `EscrowReleasedDomainEvent` | Hold release | HoldId, Amount |
| `PointsEarnedDomainEvent` | Points earned | AccountId, Points |
| `PointsSpentDomainEvent` | Points spent | AccountId, Points |
## Inter-Service Communication
### IAM Service Integration
```
Wallet Service ──────► IAM Service
User Validation
JWT Verification
```mermaid
flowchart LR
WS["🔐 Wallet Service"] --> IAM["👤 IAM Service"]
IAM --> UV["User Validation"]
IAM --> JV["JWT Verification"]
style WS fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
style IAM fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
style UV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
style JV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
### Authentication Flow
@@ -198,7 +325,7 @@ wallet-service:
### Authorization
- User can only access own wallet
- Admin endpoints for system operations
- Admin endpoints require Admin/SuperAdmin role
### Data Protection
- All amounts stored with precision

View File

@@ -1,7 +1,6 @@
# Wallet Service .NET
> **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.
Wallet and Point Account management service for GoodGo Platform.
## Overview
@@ -11,7 +10,9 @@ The Wallet Service provides comprehensive wallet and loyalty points management w
- **Escrow Module** - Hold, commit, and release funds (for Promotion Service)
- **Point Account** - Earn, spend, and track loyalty points
- **Transaction History** - Full audit trail of all transactions
- **Multi-Currency Support** - Default VND with currency support
- **Multi-Currency Support** - VND, USD, PPoint with exchange capabilities
- **Currency Exchange** - Convert between currencies with configurable rates
- **Admin Backoffice** - Full admin APIs for wallet/points management
- **Domain-Driven Design** - Clean Architecture with CQRS pattern
## Tech Stack
@@ -64,33 +65,53 @@ dotnet run --project src/WalletService.API
| Method | Endpoint | Description |
|--------|----------|-------------|
| `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 |
| `GET` | `/api/v1/wallets/{userId}` | Get wallet by user ID |
| `POST` | `/api/v1/wallets/{userId}/deposit` | Deposit funds |
| `POST` | `/api/v1/wallets/{userId}/withdraw` | Withdraw funds |
| `GET` | `/api/v1/wallets/{userId}/transactions` | Get transaction history |
### Escrow APIs (New)
### Escrow/Hold APIs
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/wallets/{walletId}/holds` | Create a hold |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Execute/Commit hold (deduct funds) |
| `POST` | `/api/v1/wallets/{walletId}/holds` | Create escrow hold |
| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Get hold details |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Execute hold (deduct funds) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Release hold (return funds) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Cancel hold |
| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Get hold details |
### 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 |
| `POST` | `/api/v1/points` | Create point account |
| `GET` | `/api/v1/points/{userId}` | Get point account |
| `POST` | `/api/v1/points/{userId}/earn` | Earn points |
| `POST` | `/api/v1/points/{userId}/spend` | Spend points |
| `GET` | `/api/v1/points/{userId}/transactions` | Get point transactions |
### Admin Wallet APIs
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/wallets` | Get all wallets (paginated) |
| `GET` | `/api/v1/admin/wallets/{walletId}` | Get wallet details |
| `POST` | `/api/v1/admin/wallets/{walletId}/freeze` | Freeze wallet |
| `POST` | `/api/v1/admin/wallets/{walletId}/unfreeze` | Unfreeze wallet |
| `POST` | `/api/v1/admin/wallets/{walletId}/adjust` | Adjust balance |
| `GET` | `/api/v1/admin/wallets/statistics` | Get wallet statistics |
| `GET` | `/api/v1/admin/wallets/search` | Search wallets |
### Admin Points APIs
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/points` | Get all point accounts |
| `GET` | `/api/v1/admin/points/{accountId}` | Get point account details |
| `POST` | `/api/v1/admin/points/{accountId}/adjust` | Adjust points |
| `POST` | `/api/v1/admin/points/{accountId}/bonus` | Grant bonus points |
| `GET` | `/api/v1/admin/points/statistics` | Get points statistics |
| `GET` | `/api/v1/admin/points/search` | Search point accounts |
### Health Endpoints
@@ -100,6 +121,34 @@ dotnet run --project src/WalletService.API
| `/health/live` | Liveness probe (K8s) |
| `/health/ready` | Readiness probe (K8s) |
## Multi-Currency Support
Wallet supports multiple currency types with exchange capabilities:
| Currency | Code | Base Rate to VND |
|----------|------|------------------|
| Vietnamese Dong | `VND` | 1 |
| US Dollar | `USD` | 25,000 |
| Loyalty Points | `PPoint` | 1,000 |
### Currency Exchange
```csharp
// Exchange USD to VND
wallet.Exchange(
fromAmount: 100m,
fromCurrency: CurrencyType.USD,
toCurrency: CurrencyType.VND
); // Returns 2,500,000 VND
// Exchange PPoints to VND
wallet.Exchange(
fromAmount: 50m,
fromCurrency: CurrencyType.PPoint,
toCurrency: CurrencyType.VND
); // Returns 50,000 VND
```
## Project Structure
```
@@ -107,13 +156,14 @@ wallet-service-net/
├── src/
│ ├── WalletService.API/ # API Layer
│ │ ├── Controllers/ # REST endpoints
│ │ │ └── Admin/ # Admin endpoints
│ │ └── Application/ # Commands & Queries
│ │ ├── Commands/ # Write operations
│ │ └── Queries/ # Read operations
│ │
│ ├── WalletService.Domain/ # Domain Layer
│ │ ├── AggregatesModel/
│ │ │ ├── WalletAggregate/ # Wallet, Transaction, Money
│ │ │ ├── WalletAggregate/ # Wallet, HoldItem, CurrencyType
│ │ │ └── PointAccountAggregate/ # Points, PointTransaction
│ │ ├── Events/ # Domain events
│ │ └── Exceptions/ # Domain exceptions
@@ -140,24 +190,26 @@ wallet-service-net/
```csharp
// Create wallet
var wallet = new Wallet(userId, "VND");
var wallet = new Wallet(userId, CurrencyType.VND);
// Deposit funds
wallet.Deposit(new Money(1000000m, "VND"), "Salary", "REF001");
wallet.Deposit(1000000m, CurrencyType.VND, "Salary", "REF001");
// Withdraw funds
wallet.Withdraw(new Money(500000m, "VND"), "Shopping", "REF002");
wallet.Withdraw(500000m, CurrencyType.VND, "Shopping", "REF002");
// Freeze/Unfreeze
wallet.Freeze();
wallet.Unfreeze();
// Escrow
// Escrow operations
var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Hold for campaign");
wallet.ExecuteHold(hold.Id, 50000m, "ORDER123"); // Commit 50k
wallet.ReleaseHold(hold.Id, 50000m); // Return 50k
wallet.CancelHold(hold.Id); // Cancel remaining
// Currency exchange
wallet.Exchange(100m, CurrencyType.USD, CurrencyType.VND);
```
### Point Account Aggregate

View File

@@ -4,18 +4,36 @@
Wallet Service quản lý ví điện tử và tài khoản điểm thưởng cho GoodGo Platform.
```
┌─────────────────────────────────────────────────────────────┐
│ Wallet Service │
├─────────────────────────────────────────────────────────────┤
API Layer (Controllers, CQRS) │
├─────────────────────────────────────────────────────────────┤
Domain Layer (Wallet, PointAccount Aggregates) │
├─────────────────────────────────────────────────────────────┤
Infrastructure Layer (EF Core, Repositories) │
├─────────────────────────────────────────────────────────────┤
│ PostgreSQL Database │
└─────────────────────────────────────────────────────────────┘
```mermaid
flowchart TB
subgraph API["🌐 API Layer"]
Controllers["Controllers"]
Commands["Commands"]
Queries["Queries"]
end
subgraph Domain["💎 Domain Layer"]
Wallet["Wallet Aggregate"]
PointAccount["PointAccount Aggregate"]
end
subgraph Infra["⚙️ Infrastructure Layer"]
Repos["Repositories"]
EF["EF Core"]
end
subgraph DB["💾 PostgreSQL"]
Tables["Tables"]
end
API --> Domain
Domain --> Infra
Infra --> DB
style API fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
style Domain fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
style Infra fill:#E67E22,color:#ECF0F1,stroke:#D35400,stroke-width:2px
style DB fill:#34495E,color:#ECF0F1,stroke:#2C3E50,stroke-width:2px
```
## Các Pattern Kiến Trúc
@@ -23,22 +41,33 @@ Wallet Service quản lý ví điện tử và tài khoản điểm thưởng ch
### Domain-Driven Design (DDD)
- **Aggregates**: Wallet, PointAccount
- **Entities**: WalletTransaction, PointTransaction
- **Value Objects**: Money, PointBalance
- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned
- **Entities**: WalletTransaction, PointTransaction, HoldItem, WalletItem
- **Value Objects**: Money, CurrencyType
- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned, EscrowHeld, EscrowExecuted
### CQRS Pattern
```
Commands (Ghi) Queries (Đọc)
│ │
CreateWallet GetWalletQuery
DepositCommand GetTransactionsQuery
WithdrawCommand GetPointAccountQuery
TransferCommand GetBalanceSummaryQuery
EarnPointsCommand
SpendPointsCommand
```mermaid
flowchart LR
subgraph Commands["Commands (Ghi)"]
C1["CreateWallet"]
C2["Deposit/Withdraw"]
C3["Exchange"]
C4["Hold/Execute/Release"]
C5["EarnPoints/SpendPoints"]
C6["Admin Commands"]
end
subgraph Queries["Queries (Đọc)"]
Q1["GetWallet"]
Q2["GetTransactions"]
Q3["GetPointAccount"]
Q4["GetStatistics"]
Q5["Admin Queries"]
end
style Commands fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
style Queries fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
```
## Domain Model
@@ -50,32 +79,68 @@ classDiagram
class Wallet {
+Guid Id
+Guid UserId
+Money Balance
+WalletStatus Status
+List~WalletTransaction~ Transactions
+Deposit(Money, description, ref)
+Withdraw(Money, description, ref)
+CurrencyType DefaultCurrency
+List~WalletItem~ Items
+List~HoldItem~ Holds
+Deposit(amount, currency, desc, ref)
+Withdraw(amount, currency, desc, ref)
+Exchange(fromAmount, fromCurrency, toCurrency)
+Hold(amount, currency, refType, refId, desc)
+ExecuteHold(holdId, amount, ref)
+ReleaseHold(holdId, amount)
+Freeze()
+Unfreeze()
+Close()
}
class WalletItem {
+Guid Id
+CurrencyType Currency
+decimal Balance
+decimal HeldBalance
+decimal AvailableBalance
}
class HoldItem {
+Guid Id
+decimal OriginalAmount
+decimal RemainingAmount
+decimal ExecutedAmount
+decimal ReleasedAmount
+HoldStatus Status
+string ReferenceType
+Guid ReferenceId
+Execute(amount, ref)
+Release(amount)
+Cancel()
}
class WalletTransaction {
+Guid Id
+TransactionType Type
+Money Amount
+Money BalanceAfter
+decimal Amount
+CurrencyType Currency
+decimal BalanceAfter
+string Description
+DateTime CreatedAt
}
class Money {
+decimal Amount
+string Currency
class CurrencyType {
+int Id
+string Name
+decimal BaseExchangeRate
+VND
+USD
+PPoint
+GetExchangeRateTo(currency)
+ConvertTo(amount, currency)
}
Wallet "1" --> "*" WalletItem
Wallet "1" --> "*" HoldItem
Wallet "1" --> "*" WalletTransaction
Wallet --> Money
WalletItem --> CurrencyType
HoldItem --> CurrencyType
```
### PointAccount Aggregate
@@ -85,9 +150,9 @@ classDiagram
class PointAccount {
+Guid Id
+Guid UserId
+int TotalPoints
+int AvailablePoints
+int PendingPoints
+long TotalPoints
+long AvailablePoints
+long PendingPoints
+EarnPoints(points, source, desc, expires)
+SpendPoints(points, source, desc)
+AdjustPoints(points, source, desc)
@@ -96,8 +161,9 @@ classDiagram
class PointTransaction {
+Guid Id
+PointTransactionType Type
+int Points
+int BalanceAfter
+long Points
+long BalanceAfter
+string Source
+DateTime? ExpiresAt
}
@@ -111,52 +177,113 @@ classDiagram
| Bảng | Mô Tả |
|------|-------|
| `Wallets` | Tài khoản ví người dùng |
| `WalletItems` | Số dư tiền tệ mỗi ví |
| `WalletTransactions` | Lịch sử giao dịch ví |
| `HoldItems` | Các lệnh ký quỹ |
| `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_WalletItems_WalletId_CurrencyTypeId` - Số dư theo tiền tệ
- `IX_WalletTransactions_WalletId` - Lịch sử giao dịch
- `IX_HoldItems_WalletId_Status` - Các hold đang hoạt động
- `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
```mermaid
sequenceDiagram
participant C as Client
participant API as Controller
participant H as Handler
participant W as Wallet
participant DB as Database
C->>API: POST /wallets/{userId}/deposit
API->>H: DepositCommand
H->>W: wallet.Deposit(amount)
W->>W: Tạo Transaction
W->>W: Raise DomainEvent
H->>DB: SaveChanges
DB-->>API: Thành công
API-->>C: Số dư cập nhật
```
### Luồng Chuyển Khoản
### Luồng Escrow
```mermaid
sequenceDiagram
participant C as Client
participant API as Controller
participant W as Wallet
participant H as HoldItem
C->>API: POST /holds (Tạo)
API->>W: wallet.Hold(amount)
W->>H: Tạo HoldItem
W-->>C: HoldId
C->>API: POST /holds/{id}/execute
API->>W: wallet.ExecuteHold(id, amount)
W->>H: hold.Execute(amount)
H-->>C: Số tiền đã thực thi
C->>API: POST /holds/{id}/release
API->>W: wallet.ReleaseHold(id, amount)
W->>H: hold.Release(amount)
H-->>C: Số tiền đã giải phóng
```
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
### Luồng Quy Đổi Tiền Tệ
```mermaid
sequenceDiagram
participant C as Client
participant API as Controller
participant W as Wallet
participant CT as CurrencyType
C->>API: POST /exchange
API->>W: wallet.Exchange(100 USD, VND)
W->>CT: GetExchangeRate(USD → VND)
CT-->>W: Tỷ giá = 25000
W->>W: Rút 100 USD
W->>W: Nạp 2.500.000 VND
W->>W: Raise WalletExchangedEvent
W-->>C: Kết quả quy đổi
```
## Domain Events
| Event | Kích Hoạt | Dữ Liệu |
|-------|-----------|---------|
| `WalletCreatedDomainEvent` | Tạo ví | WalletId, UserId |
| `WalletBalanceChangedDomainEvent` | Nạp/Rút | WalletId, Amount, Type |
| `WalletExchangedDomainEvent` | Quy đổi tiền tệ | FromCurrency, ToCurrency, Rate |
| `EscrowHeldDomainEvent` | Tạo hold | HoldId, Amount, RefType |
| `EscrowExecutedDomainEvent` | Thực thi hold | HoldId, Amount |
| `EscrowReleasedDomainEvent` | Giải phóng hold | HoldId, Amount |
| `PointsEarnedDomainEvent` | Tích điểm | AccountId, Points |
| `PointsSpentDomainEvent` | Tiêu điểm | AccountId, Points |
## 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
```mermaid
flowchart LR
WS["🔐 Wallet Service"] --> IAM["👤 IAM Service"]
IAM --> UV["Xác thực User"]
IAM --> JV["Kiểm tra JWT"]
style WS fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
style IAM fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
style UV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
style JV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
### Luồng Xác Thực
@@ -198,7 +325,7 @@ wallet-service:
### Phân Quyền
- User chỉ truy cập được ví của mình
- Admin endpoints cho system operations
- Admin endpoints yêu cầu role Admin/SuperAdmin
### Bảo Vệ Dữ Liệu
- Lưu số tiền với độ chính xác cao

View File

@@ -1,7 +1,6 @@
# Wallet Service .NET
> **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.
Dịch vụ quản lý Ví và Tài khoản Điểm cho GoodGo Platform.
## Tổng Quan
@@ -11,7 +10,9 @@ Wallet Service cung cấp quản lý ví và điểm thưởng toàn diện:
- **Escrow Module** - Ký quỹ, cam kết và giải phóng tiền (cho Promotion Service)
- **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ệ
- **Hỗ Trợ Đa Tiền Tệ** - VND, USD, PPoint với khả năng quy đổi
- **Quy Đổi Tiền Tệ** - Chuyển đổi giữa các loại tiền với tỷ giá cấu hình
- **Admin Backoffice** - API quản trị đầy đủ cho ví/điểm
- **Domain-Driven Design** - Clean Architecture với CQRS pattern
## Tech Stack
@@ -64,33 +65,53 @@ dotnet run --project src/WalletService.API
| Method | Endpoint | Mô Tả |
|--------|----------|-------|
| `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 |
| `GET` | `/api/v1/wallets/{userId}` | Lấy ví theo user ID |
| `POST` | `/api/v1/wallets/{userId}/deposit` | Nạp tiền vào ví |
| `POST` | `/api/v1/wallets/{userId}/withdraw` | Rút tiền từ ví |
| `GET` | `/api/v1/wallets/{userId}/transactions` | Lấy lịch sử giao dịch |
### Escrow APIs (New)
### Escrow/Hold APIs
| Method | Endpoint | Mô Tả |
|--------|----------|-------|
| `POST` | `/api/v1/wallets/{walletId}/holds` | Tạo lệnh giữ tiền (Hold) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Thực thi lệnh giữ tin (trừ tiền thực) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Giải phóng tiền giữ (trả lại ví) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Hủy lệnh giữ tiền |
| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Lấy thông tin lệnh giữ tiền |
| `POST` | `/api/v1/wallets/{walletId}/holds` | Tạo lệnh ký quỹ |
| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Lấy thông tin ký quỹ |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Thực thi (trừ tiền) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Giải phóng (trả lại) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Hủy ký quỹ |
### 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 |
| `POST` | `/api/v1/points` | Tạo tài khoản điểm |
| `GET` | `/api/v1/points/{userId}` | Lấy tài khoản điểm |
| `POST` | `/api/v1/points/{userId}/earn` | Tích điểm |
| `POST` | `/api/v1/points/{userId}/spend` | Tiêu điểm |
| `GET` | `/api/v1/points/{userId}/transactions` | Lấy lịch sử điểm |
### Admin Wallet APIs
| Method | Endpoint | Mô Tả |
|--------|----------|-------|
| `GET` | `/api/v1/admin/wallets` | Lấy tất cả ví (phân trang) |
| `GET` | `/api/v1/admin/wallets/{walletId}` | Lấy chi tiết ví |
| `POST` | `/api/v1/admin/wallets/{walletId}/freeze` | Đóng băng ví |
| `POST` | `/api/v1/admin/wallets/{walletId}/unfreeze` | Mở băng ví |
| `POST` | `/api/v1/admin/wallets/{walletId}/adjust` | Điều chỉnh số dư |
| `GET` | `/api/v1/admin/wallets/statistics` | Thống kê ví |
| `GET` | `/api/v1/admin/wallets/search` | Tìm kiếm ví |
### Admin Points APIs
| Method | Endpoint | Mô Tả |
|--------|----------|-------|
| `GET` | `/api/v1/admin/points` | Lấy tất cả tài khoản điểm |
| `GET` | `/api/v1/admin/points/{accountId}` | Lấy chi tiết tài khoản |
| `POST` | `/api/v1/admin/points/{accountId}/adjust` | Điều chỉnh điểm |
| `POST` | `/api/v1/admin/points/{accountId}/bonus` | Tặng điểm thưởng |
| `GET` | `/api/v1/admin/points/statistics` | Thống kê điểm |
| `GET` | `/api/v1/admin/points/search` | Tìm kiếm tài khoản |
### Health Endpoints
@@ -100,6 +121,34 @@ dotnet run --project src/WalletService.API
| `/health/live` | Liveness probe (K8s) |
| `/health/ready` | Readiness probe (K8s) |
## Hỗ Trợ Đa Tiền Tệ
Ví hỗ trợ nhiều loại tiền tệ với khả năng quy đổi:
| Tiền Tệ | Mã | Tỷ Giá Cơ Sở (VND) |
|---------|----|--------------------|
| Đồng Việt Nam | `VND` | 1 |
| Đô La Mỹ | `USD` | 25.000 |
| Điểm Thưởng | `PPoint` | 1.000 |
### Quy Đổi Tiền Tệ
```csharp
// Quy đổi USD sang VND
wallet.Exchange(
fromAmount: 100m,
fromCurrency: CurrencyType.USD,
toCurrency: CurrencyType.VND
); // Trả về 2.500.000 VND
// Quy đổi PPoints sang VND
wallet.Exchange(
fromAmount: 50m,
fromCurrency: CurrencyType.PPoint,
toCurrency: CurrencyType.VND
); // Trả về 50.000 VND
```
## Cấu Trúc Dự Án
```
@@ -107,13 +156,14 @@ wallet-service-net/
├── src/
│ ├── WalletService.API/ # API Layer
│ │ ├── Controllers/ # REST endpoints
│ │ │ └── Admin/ # Admin endpoints
│ │ └── Application/ # Commands & Queries
│ │ ├── Commands/ # Thao tác ghi
│ │ └── Queries/ # Thao tác đọc
│ │
│ ├── WalletService.Domain/ # Domain Layer
│ │ ├── AggregatesModel/
│ │ │ ├── WalletAggregate/ # Wallet, Transaction, Money
│ │ │ ├── WalletAggregate/ # Wallet, HoldItem, CurrencyType
│ │ │ └── PointAccountAggregate/ # Points, PointTransaction
│ │ ├── Events/ # Domain events
│ │ └── Exceptions/ # Domain exceptions
@@ -140,24 +190,26 @@ wallet-service-net/
```csharp
// Tạo ví
var wallet = new Wallet(userId, "VND");
var wallet = new Wallet(userId, CurrencyType.VND);
// Nạp tiền
wallet.Deposit(new Money(1000000m, "VND"), "Lương", "REF001");
wallet.Deposit(1000000m, CurrencyType.VND, "Lương", "REF001");
// Rút tiền
wallet.Withdraw(new Money(500000m, "VND"), "Mua sắm", "REF002");
wallet.Withdraw(500000m, CurrencyType.VND, "Mua sắm", "REF002");
// Đóng băng/Mở đóng băng
// Đóng băng/Mở băng
wallet.Freeze();
wallet.Unfreeze();
// Escrow / Ký Quỹ
var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Hold for campaign");
// Thao tác Escrow
var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Ký quỹ chiến dịch");
wallet.ExecuteHold(hold.Id, 50000m, "ORDER123"); // Thực thi 50k
wallet.ReleaseHold(hold.Id, 50000m); // Trả lại 50k
wallet.CancelHold(hold.Id); // Hủy phần còn lại
// Quy đổi tiền tệ
wallet.Exchange(100m, CurrencyType.USD, CurrencyType.VND);
```
### Point Account Aggregate