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:
@@ -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.
|
||||
56
services/chat-service-net/README.md
Normal file
56
services/chat-service-net/README.md
Normal 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
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
34
services/membership-service-net/README.md
Normal file
34
services/membership-service-net/README.md
Normal 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/).
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user