From 5aa48eb29c9577d4145dfa3869d9f5fb5d04ebac Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 18 Jan 2026 23:51:39 +0700 Subject: [PATCH] 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. --- docs/en/test-mermaid.md | 12 - services/chat-service-net/README.md | 56 +++ .../chat-service-net/docs/en/ARCHITECTURE.md | 395 +++++++---------- services/chat-service-net/docs/en/README.md | 148 +++---- .../chat-service-net/docs/vi/ARCHITECTURE.md | 413 ++++++++---------- services/chat-service-net/docs/vi/README.md | 154 +++---- services/membership-service-net/README.md | 34 ++ .../docs/en/ARCHITECTURE.md | 1 - .../membership-service-net/docs/en/README.md | 4 +- .../docs/vi/ARCHITECTURE.md | 1 - .../membership-service-net/docs/vi/README.md | 4 +- .../docs/en/ARCHITECTURE.md | 255 ++++++++--- services/wallet-service-net/docs/en/README.md | 100 ++++- .../docs/vi/ARCHITECTURE.md | 255 ++++++++--- services/wallet-service-net/docs/vi/README.md | 108 +++-- 15 files changed, 1096 insertions(+), 844 deletions(-) delete mode 100644 docs/en/test-mermaid.md create mode 100644 services/chat-service-net/README.md create mode 100644 services/membership-service-net/README.md diff --git a/docs/en/test-mermaid.md b/docs/en/test-mermaid.md deleted file mode 100644 index e5c82887..00000000 --- a/docs/en/test-mermaid.md +++ /dev/null @@ -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. diff --git a/services/chat-service-net/README.md b/services/chat-service-net/README.md new file mode 100644 index 00000000..c6995c67 --- /dev/null +++ b/services/chat-service-net/README.md @@ -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 diff --git a/services/chat-service-net/docs/en/ARCHITECTURE.md b/services/chat-service-net/docs/en/ARCHITECTURE.md index 62e47eb0..044db62c 100644 --- a/services/chat-service-net/docs/en/ARCHITECTURE.md +++ b/services/chat-service-net/docs/en/ARCHITECTURE.md @@ -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
(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 +public class ChatHub : Hub { - 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 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 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
Phone] - C2[Connection 2
Laptop] - C3[Connection 3
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(); -``` - -### 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/) diff --git a/services/chat-service-net/docs/en/README.md b/services/chat-service-net/docs/en/README.md index 4e7a0cf7..8c1b3944 100644 --- a/services/chat-service-net/docs/en/README.md +++ b/services/chat-service-net/docs/en/README.md @@ -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 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 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 diff --git a/services/chat-service-net/docs/vi/ARCHITECTURE.md b/services/chat-service-net/docs/vi/ARCHITECTURE.md index 85a15c8d..f748018c 100644 --- a/services/chat-service-net/docs/vi/ARCHITECTURE.md +++ b/services/chat-service-net/docs/vi/ARCHITECTURE.md @@ -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
(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 +public class ChatHub : Hub { - 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 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 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
Phone] - C2[Connection 2
Laptop] - C3[Connection 3
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(); -``` - -### 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/) diff --git a/services/chat-service-net/docs/vi/README.md b/services/chat-service-net/docs/vi/README.md index 2bd66cc3..422011c3 100644 --- a/services/chat-service-net/docs/vi/README.md +++ b/services/chat-service-net/docs/vi/README.md @@ -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 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 diff --git a/services/membership-service-net/README.md b/services/membership-service-net/README.md new file mode 100644 index 00000000..4e88e2ba --- /dev/null +++ b/services/membership-service-net/README.md @@ -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/). diff --git a/services/membership-service-net/docs/en/ARCHITECTURE.md b/services/membership-service-net/docs/en/ARCHITECTURE.md index 7109ac97..04dca07b 100644 --- a/services/membership-service-net/docs/en/ARCHITECTURE.md +++ b/services/membership-service-net/docs/en/ARCHITECTURE.md @@ -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) diff --git a/services/membership-service-net/docs/en/README.md b/services/membership-service-net/docs/en/README.md index b2c9fa7c..8577c15d 100644 --- a/services/membership-service-net/docs/en/README.md +++ b/services/membership-service-net/docs/en/README.md @@ -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 diff --git a/services/membership-service-net/docs/vi/ARCHITECTURE.md b/services/membership-service-net/docs/vi/ARCHITECTURE.md index d27d7e6c..bcdb7506 100644 --- a/services/membership-service-net/docs/vi/ARCHITECTURE.md +++ b/services/membership-service-net/docs/vi/ARCHITECTURE.md @@ -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) diff --git a/services/membership-service-net/docs/vi/README.md b/services/membership-service-net/docs/vi/README.md index 6fd7bfed..317ac9d4 100644 --- a/services/membership-service-net/docs/vi/README.md +++ b/services/membership-service-net/docs/vi/README.md @@ -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 diff --git a/services/wallet-service-net/docs/en/ARCHITECTURE.md b/services/wallet-service-net/docs/en/ARCHITECTURE.md index 60532c62..ebd2b040 100644 --- a/services/wallet-service-net/docs/en/ARCHITECTURE.md +++ b/services/wallet-service-net/docs/en/ARCHITECTURE.md @@ -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 diff --git a/services/wallet-service-net/docs/en/README.md b/services/wallet-service-net/docs/en/README.md index 331915b0..3a8d5a97 100644 --- a/services/wallet-service-net/docs/en/README.md +++ b/services/wallet-service-net/docs/en/README.md @@ -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 diff --git a/services/wallet-service-net/docs/vi/ARCHITECTURE.md b/services/wallet-service-net/docs/vi/ARCHITECTURE.md index 254f17a5..00f85146 100644 --- a/services/wallet-service-net/docs/vi/ARCHITECTURE.md +++ b/services/wallet-service-net/docs/vi/ARCHITECTURE.md @@ -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 diff --git a/services/wallet-service-net/docs/vi/README.md b/services/wallet-service-net/docs/vi/README.md index 040f2cd2..e406138e 100644 --- a/services/wallet-service-net/docs/vi/README.md +++ b/services/wallet-service-net/docs/vi/README.md @@ -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ữ tiền (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