870 lines
43 KiB
Markdown
870 lines
43 KiB
Markdown
## 1. Các Thành phần Hệ thống Hiện tại (System Topology)
|
|
Dưới đây là các component đang có sẵn và vai trò của chúng trong luồng xử lý sự kiện:
|
|
- **Nguyên tắc cốt lõi:** Các giao dịch tài chính/điểm thưởng cần sự chính xác, an toàn tuyệt đối.
|
|
- **Kafka:** Xương sống giao tiếp bất đồng bộ giữa các service.
|
|
- **PostgreSQL:** Database chính yếu (lưu trữ an toàn, ACID).
|
|
- **Redis:** Hiện tại CHỈ làm cache server, chỉ dùng khi thực sự cần thiết (Không dùng làm primary data store cho counter/state quan trọng).
|
|
- **Doris:** Data Warehouse (Dùng cho BI, Dashboard, Reporting, Segment).
|
|
- **transaction-service:** Core Engine của hệ thống (Xử lý giao dịch tài chính, **Rule Engine**, cộng/trừ điểm, tặng voucher, xử lý các bộ đếm Rule counter, giới hạn thưởng). Lắng nghe (consume) từ Kafka `events-topic`.
|
|
- **event-processor-service:** Đóng vai trò **Event Gateway/Router**. Nhận event từ các nguồn (Client/Service) -> Validate/Định tuyến -> Đẩy vào Kafka `events-topic`.
|
|
- **notification-service:** Chịu trách nhiệm push noti (SMS/Email/Firebase). Lắng nghe từ Kafka `noti-topic`.
|
|
- **marketing-service:** Lập lịch (Scheduler), quét tập khách hàng theo chiến dịch.
|
|
- **attribute-service:** Quản lý Dynamic Attribute (mô hình EAV) để tạo thuộc tính động cho Customer, Card, Product_Account, Items
|
|
- **member-app & merchant-app:** Ứng dụng Frontend cho khách hàng và đối tác.
|
|
|
|
---
|
|
|
|
## 2. Bảng Tổng hợp Use Cases
|
|
|
|
| Use Case ID | Event | User case | Outcome |
|
|
| :--- | :--- | :--- | :--- |
|
|
| **UC-01** | Login | First login | Với lần đăng nhập đầu tiên vào portal (ngay sau khi kích hoạt):<br>1. Khách hàng nhận 1 in-app message (Display CP) chào mừng ngay khi đăng nhập thành công (realtime)<br>2. Khách hàng nhận được tin nhắn OTT ( marketing CP) thông báo các chương trình ưu đãi cho hội viên mới<br>3. Khách hàng nhận dc 100 điểm thưởng (Đi kèm push noti trên app ) |
|
|
| **UC-02** | Login | Daily login counter | Khách hàng đăng nhập liên tiếp 7 ngày liên tục nhận 100 điểm<br>Khách hàng đăng nhập chuỗi 30 ngày liên tiếp nhận 500 điểm<br>Giới hạn số lần thưởng 1k điểm/ 1 năm |
|
|
| **UC-03** | View item | View item | Khách hàng nhấn vào xem detail quà trên cổng portal từ home page hay từ phần refer item khi đang xem item khác ...<br>Hệ thống cập nhật số lần xem của từng item và item xem gần đây nhất để làm thuộc tính cho display campaign phần recommned for you |
|
|
| **UC-04** | Transfer balance | Transfer balance | Khách hàng thực hiện chuyển điểm sang cho tài khoản loy khác. Hệ thống thực hiện giao dịch điều chỉnh tăng/giảm điểm tương ứng |
|
|
| **UC-05** | Login | In-inacive account | Gửi noti cho khách hàng |
|
|
| **UC-06** | Welcome offer | Welcome offer | Khách hàng mở thẻ mới => hệ thống thưởng 100 điểm |
|
|
| **UC-07** | Update profile | Update profile | Khách hàng toàn tất cập nhật đầy đủ thông tin liên lạc trên portal trước ngày 30/09 sẽ được thưởng 100 điểm |
|
|
| **UC-08** | Survery | Complete survey | Với các khác hàng chưa thực hiện khảo sát hệ thống hiển thị khảo sát khi khách hàng đăng nhập vào app. Khách hàng hoàn thành một khảo sát trên cổng portal được thưởng 1 voucher và khảo sát không được hiển thị cho lần đăng nhập sau |
|
|
| **UC-09** | Redeem | Redeem item | Khách hàng thực hiện đổi quà trên portal, hệ thống hoàn tất giao dịch và hiển thị noti thông báo |
|
|
| **UC-10** | Merchant app | Scan voucher | Merchant staff quét voucher trên merchant app, hệ thống hiển thị thông báo trên cổng portal thông báo về việc voucher đã được sử dụng thành công |
|
|
| **UC-11** | Retention | Retention | Hệ thống gửi thông báo cho khách hàng có mật khẩu sắp hết hạn ( Cần trace được tệp khách hàng để gửi thông báo) |
|
|
| **UC-12** | Most redeem item | Most redeem item | Khi khách hàng redeem item hệ thống cần cập nhật số lượng item |
|
|
| **UC-13** | Retention | Retention | Khách hàng không có txn trong vòng 3 tháng sẽ được nhận ưu đãi riêng (Hệ thống cần track được thời điểm giao dịch gần nhất khi khách hàng phát sinh các giao dịch - Không tính các txn phát sinh chủ động bởi OLS) |
|
|
|
|
---
|
|
|
|
## 4. Tóm lược Vai trò của Event Processor
|
|
|
|
Dưới góc nhìn kiến trúc mới nhất, `event-processor-service` là một **Smart Event Gateway**:
|
|
|
|
1. **Ingest & Validate:** Hứng mọi loại event từ Client (member-app, hệ thống ngoài). Đảm bảo schema của event đúng định dạng chuẩn.
|
|
2. **Classify (Phân loại):** Xác định event thuộc loại nào:
|
|
- **Direct Event** → Cần xử lý logic/tài chính ngay.
|
|
- **Tracking Event** → Cần tích lũy trước khi trigger.
|
|
- **Notification Event** → Cần gửi thông báo trực tiếp.
|
|
3. **Routing (Định tuyến):**
|
|
- Direct Events (Redeem, Transfer, Profile_Completed...) → `events-topic` → `transaction-service`.
|
|
- Tracking Events (Login, ViewItem, RedeemItem count...) → `tracking-topic` → `attribute-service` (tích lũy, khi đạt threshold → trigger `events-topic`).
|
|
- Notification Events → `noti-topic` → `notification-service`.
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
member-app ─────────────┐ │ event-processor-service │
|
|
merchant-app ───────────┤ │ │
|
|
marketing-service ──────┤ │ ┌─────────┐ ┌────────────────┐ │
|
|
profile-service ────────┼──▶│ │ Validate │──▶│ Classify & │ │
|
|
... │ │ └─────────┘ │ Route │ │
|
|
│ │ └───┬────┬────┬──┘ │
|
|
│ └────────────────────┼────┼────┼──────┘
|
|
│ │ │ │
|
|
│ ┌────────┘ │ └────────┐
|
|
│ ▼ ▼ ▼
|
|
│ events-topic tracking-topic noti-topic
|
|
│ │ │ │
|
|
│ ▼ ▼ ▼
|
|
│ transaction- attribute- notification-
|
|
│ service service service
|
|
│ (Rule Engine) (Tracking (SMS/Email/
|
|
│ (TP Counter) Counter/EAV) Firebase)
|
|
│ (±Điểm/Vch) (Trigger)
|
|
│ │ │
|
|
│ │ ┌────────┘
|
|
│ │ │ (threshold reached)
|
|
│ │◀───┘
|
|
│ │
|
|
│ ▼
|
|
│ noti-topic
|
|
│ │
|
|
│ ▼
|
|
│ notification-service
|
|
└─────────────────────────────────────────
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Sơ đồ Kiến trúc Tổng thể & Giải pháp Chi tiết từng Use Case
|
|
|
|
### 6.1. Architecture Diagram — Tổng thể hệ thống
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "Client Layer"
|
|
MA["member-app<br/>(Mobile/Web)"]
|
|
MRA["merchant-app<br/>(Mobile)"]
|
|
end
|
|
|
|
subgraph "Service Layer"
|
|
EP["event-processor-service<br/>(Smart Event Gateway)"]
|
|
TP["transaction-service<br/>(Rule Engine + TP Counter)"]
|
|
NS["notification-service<br/>(SMS/Email/Firebase)"]
|
|
MKT["marketing-service<br/>(Scheduler + Cronjob)"]
|
|
ATTR["attribute-service<br/>(Tracking Counter / EAV)"]
|
|
PROF["profile-service"]
|
|
end
|
|
|
|
subgraph "Message Broker (Kafka)"
|
|
ET["events-topic"]
|
|
TT["tracking-topic"]
|
|
NT["noti-topic"]
|
|
end
|
|
|
|
subgraph "Data Layer"
|
|
PG_TP["PostgreSQL<br/>(Transaction DB)"]
|
|
PG_ATTR["PostgreSQL<br/>(Attribute DB / EAV)"]
|
|
PG_MKT["PostgreSQL<br/>(Marketing DB)"]
|
|
REDIS["Redis (Cache Only)"]
|
|
end
|
|
|
|
%% Client → Event Processor
|
|
MA -->|"Login / ViewItem / Redeem<br/>Transfer / Survey"| EP
|
|
MRA -.->|"Scan Voucher<br/>(Sync API)"| TP
|
|
|
|
%% Event Processor → Kafka
|
|
EP -->|"Direct Events"| ET
|
|
EP -->|"Tracking Events"| TT
|
|
EP -->|"Notification Events"| NT
|
|
|
|
%% Kafka → Consumers
|
|
ET --> TP
|
|
TT --> ATTR
|
|
NT --> NS
|
|
|
|
%% Attribute → Trigger
|
|
ATTR -->|"Threshold Reached"| ET
|
|
|
|
%% Transaction → Notification
|
|
TP -->|"Reward Granted"| NT
|
|
|
|
%% Marketing → Events
|
|
MKT -->|"Scheduled Events<br/>(Dormant)"| EP
|
|
MKT -->|"Direct Noti<br/>(Password)"| NT
|
|
|
|
%% Profile → Events
|
|
PROF -->|"Profile_Completed"| EP
|
|
|
|
%% Data connections
|
|
TP --> PG_TP
|
|
ATTR --> PG_ATTR
|
|
MKT --> PG_MKT
|
|
ATTR -.-> REDIS
|
|
|
|
%% Notification → Client
|
|
NS -->|"Firebase Push"| MA
|
|
```
|
|
|
|
### 6.2. Luồng Event Classification — Chi tiết phân loại
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
INPUT["Incoming Event"] --> EP["event-processor-service"]
|
|
|
|
EP --> V{"Validate<br/>Schema"}
|
|
V -->|"INVALID"| REJECT["Reject + Log"]
|
|
V -->|"VALID"| C{"Classify<br/>Event Type"}
|
|
|
|
C -->|"Direct"| ET["events-topic"]
|
|
C -->|"Counter"| TT["tracking-topic"]
|
|
C -->|"Notification"| NT["noti-topic"]
|
|
|
|
ET --> TP["transaction-service"]
|
|
TT --> ATTR["attribute-service"]
|
|
NT --> NS["notification-service"]
|
|
```
|
|
|
|
**Bảng phân loại Event:**
|
|
|
|
| Event Type | Classification | Target Topic | Consumer |
|
|
|:---|:---|:---|:---|
|
|
| Login | Counter | `tracking-topic` | `attribute-service` |
|
|
| Transfer_Balance | Direct | `events-topic` | `transaction-service` |
|
|
| Welcome_Offer | Direct | `events-topic` | `transaction-service` |
|
|
| Profile_Completed | Direct | `events-topic` | `transaction-service` |
|
|
| Survey_Completed | Dual (Direct + Counter) | `events-topic` & `tracking-topic` | `transaction-service` & `attribute-service` |
|
|
| Redeem_Item | Direct | `events-topic` | `transaction-service` |
|
|
| View_Item | Counter | `tracking-topic` | `attribute-service` |
|
|
| Item_Redeemed_Count | Counter | `tracking-topic` | `attribute-service` |
|
|
| Password_Expiring | Notification | `noti-topic` | `notification-service` |
|
|
| Dormant_User | Direct | `events-topic` | `transaction-service` |
|
|
|
|
---
|
|
|
|
### 6.3. Giải pháp Chi tiết từng Use Case
|
|
|
|
---
|
|
|
|
#### UC-01 & UC-02: First Login & Daily Login
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant EP as event-processor
|
|
participant TT as Kafka (tracking-topic)
|
|
participant ATTR as attribute-service
|
|
participant ET as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
|
|
MA->>EP: Login Event
|
|
EP->>EP: Classify: Tracking Event
|
|
EP->>TT: Publish Login
|
|
|
|
TT->>ATTR: Consume Login Event
|
|
|
|
par UC-01: First Login Bonus (SET_TRUE_ONCE)
|
|
ATTR->>ATTR: Check first_login_flag == false?
|
|
alt Is First Login
|
|
ATTR->>ATTR: Update first_login_flag = true
|
|
ATTR->>ET: Publish First_Login_Reward
|
|
end
|
|
and UC-02: Daily Login Streak
|
|
ATTR->>ATTR: Update last_login_date & current_streak
|
|
alt streak == 7
|
|
ATTR->>ET: Publish Login_Streak_7_Days
|
|
else streak == 30
|
|
ATTR->>ET: Publish Login_Streak_30_Days
|
|
end
|
|
end
|
|
|
|
ET->>TP: Consume Reward Events (First/Streak)
|
|
TP->>TP: Cộng điểm
|
|
TP->>KN: Publish Reward_Noti
|
|
KN->>NS: Consume notifications
|
|
NS->>MA: Firebase Push (Realtime)
|
|
```
|
|
|
|
**Phân tích thiết kế cho UC-01 (First Time Login Bonus):**
|
|
Bài toán "First Action" rất dễ gây lỗi trigger nhiều lần (abuse) nếu thiết kế không cẩn thận. Dưới đây là 2 cách tiếp cận kiến trúc (Patterns) chuẩn xác ở tầng Attribute Service:
|
|
|
|
**Cách 1: State Transition Trigger (Đang áp dụng UI - Hướng 2)**
|
|
- Sử dụng operation `SET_TRUE_ONCE` cho attribute `first_login_flag` (BOOLEAN).
|
|
- **Logic lõi:** Attribute Service lấy giá trị cũ từ DB. Nếu `false/null`, nó update thành `true` và bắn sự kiện `First_Login_Reward` (chỉ trigger khi có sự thay đổi trạng thái). Nếu DB đã là `true`, nó sẽ bỏ qua (No-op).
|
|
- **Ưu điểm:** Thiết kế JSON cấu hình cực kỳ gọn nhẹ. Ta có thể bỏ hẳn mảng `thresholds` và gán trực tiếp thuộc tính `triggerEventOnSuccess: 'First_Login_Reward'` ngay trên instruction.
|
|
|
|
**Cách 2: Đếm số lượng (Counter Pattern - Mẫu A)**
|
|
- Thay vì dùng cờ Boolean, sử dụng operation `INCREMENT` cho attribute `login_count` (INTEGER).
|
|
- **Logic lõi:** Mỗi sự kiện Login làm tăng `login_count += 1`.
|
|
- Cấu hình Threshold: `If login_count == 1 THEN triggerEvent: First_Login_Reward`.
|
|
- **Ưu điểm:** Tường minh nhất, dễ debug. Cực kỳ linh hoạt nếu tương lai Marketer muốn đổi logic thành "Thưởng cho lần login thứ 3" (chỉ cần sửa threshold thành `== 3`).
|
|
|
|
**Tổng kết luồng giải pháp (Đang implement theo Cách 1):**
|
|
- **Source:** Client App chỉ bắn duy nhất sự kiện `Login` (Thin Event).
|
|
- **Logic:** `attribute-service` xử lý rule State Transition để sinh ra trigger.
|
|
- **Output:** `transaction-service` hứng event `First_Login_Reward` từ `events-topic` để cộng điểm an toàn tuyệt đối.
|
|
|
|
---
|
|
|
|
#### UC-02: Daily Login Counter — Chuỗi đăng nhập liên tục
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant EP as event-processor
|
|
participant TT as Kafka (tracking-topic)
|
|
participant ATTR as attribute-service
|
|
participant ET as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
|
|
MA->>EP: Login Event
|
|
EP->>EP: Classify: Tracking Event
|
|
EP->>TT: Publish Login
|
|
|
|
TT->>ATTR: Consume Login Event
|
|
ATTR->>ATTR: Tính toán dựa trên last_login_date
|
|
|
|
alt Ngày hôm sau (last_login == yesterday)
|
|
ATTR->>ATTR: Update last_login = NOW, streak += 1
|
|
else Cách 1 ngày trở lên
|
|
ATTR->>ATTR: Update last_login = NOW, streak = 1 (Reset)
|
|
else Cùng ngày
|
|
Note over ATTR: Bỏ qua (Idempotent)
|
|
end
|
|
|
|
alt streak == 7
|
|
ATTR->>ET: Publish Login_Streak_7_Days
|
|
else streak == 30
|
|
ATTR->>ET: Publish Login_Streak_30_Days
|
|
end
|
|
|
|
ET->>TP: Consume Reward Events
|
|
TP->>TP: Check giới hạn 1k điểm/năm
|
|
TP->>TP: Cộng điểm & Push Noti
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Counter:** `attribute-service` quản lý `last_login_date` và `current_streak` cho User thông qua EAV/Dynamic Attribute.
|
|
- **Filter rác:** Các sự kiện Login trong cùng 1 ngày sẽ bị ignore, không làm ảnh hưởng tới TP.
|
|
- **Trigger:** Khi đủ điều kiện (7 hoặc 30 ngày), `attribute-service` mới bắn event sang cho TP. TP chỉ nhận đúng event đã "chín" để cộng điểm.
|
|
|
|
---
|
|
|
|
#### UC-03: View Item — Tracking + Counter + Recommend
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant EP as event-processor
|
|
participant KT as Kafka (tracking-topic)
|
|
participant ATTR as attribute-service
|
|
participant KE as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant REDIS as Redis (Cache)
|
|
|
|
MA->>EP: ViewItem Event<br/>{itemId, customerId}
|
|
EP->>EP: Classify: Tracking Event
|
|
EP->>KT: Publish to tracking-topic
|
|
|
|
KT->>ATTR: Consume ViewItem
|
|
ATTR->>ATTR: 1. Update item_view_count<br/>(Dynamic Attr on Item entity)
|
|
ATTR->>ATTR: 2. Update recently_viewed<br/>(Dynamic Attr on Customer)
|
|
ATTR->>ATTR: 3. Increment weekly_view_count<br/>(Counter on Customer)
|
|
ATTR-->>REDIS: Cache recently_viewed<br/>(async, for fast query)
|
|
|
|
ATTR->>ATTR: Check threshold:<br/>weekly_view_count >= 10?
|
|
|
|
alt Threshold Reached
|
|
ATTR->>KE: Publish View_10_Items_Reward
|
|
ATTR->>ATTR: Reset weekly_view_count = 0
|
|
KE->>TP: Consume
|
|
TP->>TP: Cộng 50 điểm
|
|
end
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Classification:** `event-processor-service` phân loại `ViewItem` là Tracking Event → đẩy `tracking-topic`.
|
|
- **attribute-service** xử lý 3 việc:
|
|
1. Tăng `item_view_count` (Dynamic Attribute trên entity **Item**) — phục vụ ranking/trending.
|
|
2. Cập nhật `recently_viewed` (Dynamic Attribute trên entity **Customer**) — danh sách item gần đây. Tính năng này sử dụng phép toán `APPEND_UNIQUE` lấy từ `event.itemId`, kết hợp cơ chế FIFO với giới hạn `maxLength` (VD: 50 items) để chống phình to dữ liệu.
|
|
3. Tăng `weekly_view_count` (Counter trên **Customer**) — đếm để trigger thưởng.
|
|
- **Threshold:** Cấu hình `{ threshold: 10, trigger_event: "View_10_Items_Reward" }` nằm trong metadata của attribute.
|
|
- **Redis:** Cache `recently_viewed` để `member-app` query nhanh cho trang chủ (Recommend for You).
|
|
|
|
---
|
|
|
|
#### UC-04: Transfer Balance — Chuyển điểm
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant EP as event-processor
|
|
participant K as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
|
|
MA->>EP: Transfer_Balance Event<br/>{from: custA, to: custB, amount: 500}
|
|
EP->>K: Publish Transfer_Balance
|
|
K->>TP: Consume
|
|
|
|
TP->>TP: BEGIN TRANSACTION
|
|
TP->>TP: Debit custA: -500 điểm
|
|
TP->>TP: Credit custB: +500 điểm
|
|
TP->>TP: COMMIT
|
|
|
|
TP->>KN: Noti cho custA (Đã chuyển 500đ)
|
|
TP->>KN: Noti cho custB (Nhận 500đ)
|
|
KN->>NS: Consume
|
|
NS->>MA: Firebase Push (cả 2 user)
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Giao dịch kép ACID:** `transaction-service` xử lý cả debit + credit trong 1 transaction PostgreSQL.
|
|
- **Validation:** TP kiểm tra số dư (`custA.balance >= amount`) trước khi thực hiện.
|
|
- **Output:** Gửi noti cho cả 2 bên (người gửi + người nhận).
|
|
|
|
---
|
|
|
|
#### UC-05: Inactive Account Login — Gửi noti chào mừng quay lại
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant EP as event-processor<br/>(Smart Router)
|
|
participant TT as Kafka (tracking-topic)
|
|
participant ATTR as attribute-service<br/>(Dumb Executor)
|
|
participant ET as Kafka (events-topic)
|
|
participant TP as transaction-service<br/>(Rule Engine)
|
|
|
|
MA->>EP: Login Event
|
|
|
|
Note over EP: Tra cứu Registry. Phát hiện instruction:<br/>CHECK_AND_UPDATE_TIMER cho last_login_date
|
|
|
|
EP->>TT: Publish Fat Payload<br/>[kèm instruction: CHECK_AND_UPDATE_TIMER]
|
|
|
|
TT->>ATTR: Consume Payload
|
|
|
|
Note over ATTR: Thực thi CHECK_AND_UPDATE_TIMER
|
|
ATTR->>ATTR: Lấy old_date từ DB. Tính GAP = NOW() - old_date
|
|
|
|
alt GAP >= 90 ngày (ngưỡng từ payload)
|
|
ATTR->>ET: Publish: Account_Reactivated
|
|
end
|
|
|
|
ATTR->>ATTR: Update last_login_date = NOW()
|
|
|
|
ET->>TP: Consume Account_Reactivated
|
|
TP->>TP: Update customer status -> ACTIVE
|
|
TP-->>MA: (Gửi Noti Welcome Back qua noti-topic)
|
|
```
|
|
|
|
**Giải pháp (Pattern: Accumulate & Trigger):**
|
|
- **Smart Router (Event Processor):** Sự kiện `Login` gánh thêm cấu hình `CHECK_AND_UPDATE_TIMER`. Event Processor đóng gói cấu hình kiểm tra khoảng trống (gap) và gửi sang `tracking-topic`. (Lưu ý: Cấu hình bắt buộc phải có `Unit` như Days/Hours/Minutes để định lượng chính xác).
|
|
- **Dumb Executor (Attribute Service):** Đóng vai trò tính toán. Lấy `last_login_date` cũ trong DB trừ đi hiện tại. Nếu GAP vượt ngưỡng cấu hình (VD: >= 90 Days), Attribute Service sinh ra event mới tên là `Account_Reactivated` đẩy sang `events-topic`. Sau đó update lại `last_login_date` = NOW().
|
|
- **Bảo vệ TP:** `transaction-service` (TP) không bị "dội bom" bởi hàng triệu sự kiện Login. Nó chỉ bị đánh thức bằng sự kiện `Account_Reactivated` hiếm hoi khi có user ngủ đông tỉnh giấc.
|
|
|
|
---
|
|
|
|
#### UC-06: Welcome Offer — Thưởng mở thẻ mới
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant SYS as Hệ thống<br/>(Card Issuance)
|
|
participant EP as event-processor
|
|
participant K as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
|
|
SYS->>EP: Welcome_Offer Event<br/>{customerId, cardId, cardType}
|
|
EP->>K: Publish Welcome_Offer
|
|
K->>TP: Consume
|
|
|
|
TP->>TP: Check Rule: "New Card = +100đ"
|
|
TP->>TP: Cộng 100 điểm
|
|
TP->>KN: Reward Notification
|
|
KN->>NS: Consume
|
|
NS->>SYS: Firebase Push<br/>"Chúc mừng! Bạn nhận 100 điểm"
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Source:** Hệ thống phát hành thẻ (Card Issuance) bắn event khi tạo thẻ mới.
|
|
- **Logic:** TP check rule đơn giản — thẻ mới = +100 điểm.
|
|
- **Idempotency:** TP phải đảm bảo mỗi thẻ chỉ thưởng 1 lần (check `card_id` đã nhận welcome offer chưa).
|
|
|
|
---
|
|
|
|
#### UC-07: Update Profile — Thưởng hoàn tất hồ sơ
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant PROF as profile-service
|
|
participant EP as event-processor
|
|
participant K as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
|
|
MA->>PROF: Update profile fields
|
|
PROF->>PROF: Check completeness<br/>(tất cả field bắt buộc đã điền?)
|
|
|
|
alt Profile Complete
|
|
PROF->>EP: Profile_Completed Event<br/>{customerId, completedAt}
|
|
EP->>K: Publish Profile_Completed
|
|
K->>TP: Consume
|
|
|
|
TP->>TP: Check Rule:<br/>"completedAt <= 30/09<br/>AND chưa nhận thưởng?"
|
|
alt Đủ điều kiện
|
|
TP->>TP: Cộng 100 điểm
|
|
TP->>KN: Reward Notification
|
|
end
|
|
end
|
|
|
|
KN->>NS: Consume
|
|
NS->>MA: Firebase Push
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Fat Event:** `profile-service` tự check completeness rồi emit `Profile_Completed` (kèm timestamp).
|
|
- **Time-bound Rule:** `transaction-service` check `completedAt <= 2024-09-30` — rule này được cấu hình trong Rule Engine.
|
|
- **Idempotency:** TP đánh dấu customer đã nhận thưởng Profile Complete, tránh thưởng lại.
|
|
|
|
---
|
|
|
|
#### UC-08: Survey — Thưởng hoàn thành khảo sát
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant ATTR as attribute-service
|
|
participant EP as event-processor
|
|
participant K as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant KT as Kafka (tracking-topic)
|
|
|
|
MA->>MA: Đăng nhập → Check attribute<br/>"survey_completed == false?"
|
|
alt Chưa làm khảo sát
|
|
MA->>MA: Hiển thị Survey popup
|
|
MA->>EP: Survey_Completed Event<br/>{customerId, surveyId}
|
|
EP->>K: Publish to events-topic
|
|
EP->>KT: Publish to tracking-topic
|
|
|
|
par Xử lý Voucher
|
|
K->>TP: Consume
|
|
TP->>TP: Cấp voucher cho customer
|
|
and Xử lý Attribute
|
|
KT->>ATTR: Consume
|
|
ATTR->>ATTR: Update survey_completed = true
|
|
end
|
|
end
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Dynamic Attribute:** `survey_completed` (BOOLEAN) trên entity Customer, quản lý bởi `attribute-service`.
|
|
- **Luồng 1 bước:** `member-app` chỉ bắn event `Survey_Completed` về `event-processor`. Event Processor sẽ tự động route song song:
|
|
1. `events-topic`: `transaction-service` hứng để tặng voucher.
|
|
2. `tracking-topic`: `attribute-service` hứng để update cờ `survey_completed = true` (đảm bảo survey không hiển thị lại).
|
|
- **Lưu ý:** Nếu có nhiều survey khác nhau, mỗi survey tạo 1 attribute riêng (VD: `survey_{surveyId}_completed`).
|
|
|
|
---
|
|
|
|
#### UC-09: Redeem Item — Đổi quà
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant EP as event-processor
|
|
participant K as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
|
|
MA->>EP: Redeem_Item Event<br/>{customerId, itemId, pointCost}
|
|
EP->>K: Publish Redeem_Item
|
|
K->>TP: Consume
|
|
|
|
TP->>TP: BEGIN TRANSACTION
|
|
TP->>TP: Check balance >= pointCost
|
|
TP->>TP: Debit: -pointCost điểm
|
|
TP->>TP: Reserve item (giảm inventory)
|
|
TP->>TP: COMMIT
|
|
|
|
TP->>KN: Redeem_Success Noti
|
|
KN->>NS: Consume
|
|
NS->>MA: Firebase Push<br/>"Đổi quà thành công!"
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **ACID Transaction:** TP xử lý trừ điểm + chốt kho trong 1 transaction.
|
|
- **Validation:** Kiểm tra đủ điểm, item còn hàng, customer đủ điều kiện.
|
|
- **Output:** Gửi noti thành công qua `noti-topic`.
|
|
|
|
---
|
|
|
|
#### UC-10: Merchant Scan Voucher — Quét mã voucher
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MRA as merchant-app
|
|
participant TP as transaction-service
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
participant MA as member-app
|
|
|
|
MRA->>TP: API: POST /vouchers/{code}/redeem<br/>(Sync call)
|
|
TP->>TP: BEGIN TRANSACTION
|
|
TP->>TP: Validate voucher code
|
|
TP->>TP: Mark voucher as USED
|
|
TP->>TP: COMMIT
|
|
TP-->>MRA: 200 OK (Voucher redeemed)
|
|
|
|
TP->>KN: Voucher_Used Noti<br/>{customerId, voucherName}
|
|
KN->>NS: Consume
|
|
NS->>MA: Firebase Push<br/>"Voucher [X] đã được sử dụng<br/>tại cửa hàng [Y]"
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Luồng Sync (không qua Event Processor):** `merchant-app` gọi thẳng API `transaction-service` vì cần response ngay.
|
|
- **ACID:** TP đánh dấu voucher đã sử dụng trong 1 transaction.
|
|
- **Push Realtime:** Sau khi xử lý xong, TP bắn event vào `noti-topic` → `notification-service` push Firebase tới `member-app` của khách hàng.
|
|
|
|
---
|
|
|
|
#### UC-11: Retention — Mật khẩu sắp hết hạn
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MKT as marketing-service
|
|
participant EP as event-processor
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
participant MA as member-app
|
|
|
|
Note over MKT: Cronjob chạy hàng ngày<br/>(VD: 08:00 AM)
|
|
MKT->>MKT: Query DB: Tìm customers có<br/>password_expiry_date <= NOW() + 7 days
|
|
|
|
loop Mỗi customer thoả mãn
|
|
MKT->>KN: Password_Expiring Noti<br/>{customerId, expiryDate}
|
|
end
|
|
|
|
KN->>NS: Consume batch
|
|
NS->>MA: Firebase Push / Email<br/>"Mật khẩu sắp hết hạn,<br/>vui lòng đổi mật khẩu"
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Scheduled Job:** `marketing-service` chạy Cronjob hàng ngày, quét DB tìm khách hàng có mật khẩu sắp hết hạn (trong 7 ngày tới).
|
|
- **Luồng ngắn:** Không cần qua Event Processor hay TP — đây chỉ là notification thuần túy.
|
|
- **Đẩy thẳng `noti-topic`:** `marketing-service` đẩy trực tiếp vào `noti-topic` để `notification-service` gửi.
|
|
|
|
---
|
|
|
|
#### UC-12: Most Redeem Item — Cập nhật ranking item
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as member-app
|
|
participant EP as event-processor
|
|
participant KT as Kafka (tracking-topic)
|
|
participant ATTR as attribute-service
|
|
participant PORTAL as member-app (Portal)
|
|
|
|
MA->>EP: Redeem_Item Event
|
|
Note over EP: Event này cũng được gửi<br/>song song vào events-topic<br/>cho TP xử lý giao dịch (UC-09)
|
|
|
|
EP->>KT: Publish to tracking-topic<br/>(bản sao cho tracking)
|
|
|
|
KT->>ATTR: Consume
|
|
ATTR->>ATTR: Increment total_redeem_count<br/>(Dynamic Attr on Item entity)
|
|
|
|
Note over ATTR: Counter thuần túy,<br/>KHÔNG trigger events-topic.<br/>Không cần threshold.
|
|
|
|
PORTAL->>ATTR: API: GET /items?sort=redeem_count&order=desc
|
|
ATTR-->>PORTAL: Top items by redeem count
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Dual Publishing:** `event-processor-service` đẩy event `Redeem_Item` vào CẢ HAI topic:
|
|
- `events-topic` → TP xử lý giao dịch trừ điểm (UC-09).
|
|
- `tracking-topic` → `attribute-service` cập nhật counter.
|
|
- **Counter thuần túy:** `total_redeem_count` trên entity Item — chỉ tăng, không trigger, không reset.
|
|
- **Query:** Portal gọi API `attribute-service` để lấy danh sách items sắp xếp theo `redeem_count`.
|
|
|
|
---
|
|
|
|
#### UC-13: Retention — Dormant 3 tháng
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MKT as marketing-service
|
|
participant EP as event-processor
|
|
participant K as Kafka (events-topic)
|
|
participant TP as transaction-service
|
|
participant KN as Kafka (noti-topic)
|
|
participant NS as notification-service
|
|
participant MA as member-app
|
|
|
|
Note over MKT: Cronjob chạy hàng ngày
|
|
MKT->>MKT: Query DB: Tìm customers có<br/>last_user_txn_date <= NOW() - 90 days<br/>(Loại trừ txn auto bởi OLS)
|
|
|
|
loop Mỗi customer thoả mãn
|
|
MKT->>EP: Dormant_User Event<br/>{customerId, lastTxnDate, inactiveDays}
|
|
EP->>K: Publish to events-topic
|
|
end
|
|
|
|
K->>TP: Consume
|
|
TP->>TP: Check Rule: "Dormant 90 days"
|
|
TP->>TP: Cấp ưu đãi riêng<br/>(VD: bonus 200 điểm / voucher)
|
|
TP->>KN: Noti: "Ưu đãi đặc biệt dành cho bạn"
|
|
KN->>NS: Consume
|
|
NS->>MA: Firebase Push / SMS
|
|
```
|
|
|
|
**Giải pháp:**
|
|
- **Scheduled Job:** `marketing-service` chạy Cronjob, query bảng transaction tìm khách hàng có `last_user_txn_date` >= 90 ngày trước. **Filter** loại trừ các transaction auto (phát sinh bởi OLS).
|
|
- **Qua Event Processor:** Khác UC-11, UC-13 cần cấp ưu đãi (điểm/voucher) nên phải đi qua `event-processor` → `events-topic` → `transaction-service`.
|
|
- **TP xử lý:** Áp dụng rule ưu đãi Dormant → cộng điểm/tặng voucher → gửi noti.
|
|
|
|
---
|
|
|
|
### 6.4. Tổng hợp: Bảng Mapping UC → Luồng xử lý
|
|
|
|
| UC | Trigger Source | Qua EP? | Target Topic | Processor | Output |
|
|
|:---|:---|:---|:---|:---|:---|
|
|
| **UC-01** | UI Client (`Login` -> trigger `First_Login_Reward`) | ✅ | `tracking-topic` | `attribute-service` -> `transaction-service` | +100đ, In-app msg, OTT |
|
|
| **UC-02** | `member-app` (Login) | ✅ | `tracking-topic` | `attribute-service` | Trigger -> TP +100đ/+500đ |
|
|
| **UC-03** | `member-app` (ViewItem) | ✅ | `tracking-topic` | `attribute-service` | Counter + Threshold → TP |
|
|
| **UC-04** | `member-app` (Transfer) | ✅ | `events-topic` | `transaction-service` | Debit/Credit ACID |
|
|
| **UC-05** | `member-app` (Login) | ✅ | `tracking-topic` | `attribute-service` | Trigger `Account_Reactivated` -> TP |
|
|
| **UC-06** | Card Issuance System | ✅ | `events-topic` | `transaction-service` | +100đ |
|
|
| **UC-07** | `profile-service` | ✅ | `events-topic` | `transaction-service` | +100đ (trước 30/09) |
|
|
| **UC-08** | `member-app` (Survey) | ✅ | `events-topic` | `transaction-service` | Tặng voucher |
|
|
| **UC-09** | `member-app` (Redeem) | ✅ | `events-topic` | `transaction-service` | Trừ điểm + Reserve item |
|
|
| **UC-10** | `merchant-app` (Scan) | ❌ Sync API | — | `transaction-service` | Mark voucher USED + Noti |
|
|
| **UC-11** | `marketing-service` (Cron) | ❌ | `noti-topic` | `notification-service` | Email/Push noti |
|
|
| **UC-12** | `member-app` (Redeem) | ✅ | `tracking-topic` | `attribute-service` | Counter (thuần tracking) |
|
|
| **UC-13** | `marketing-service` (Cron) | ✅ | `events-topic` | `transaction-service` | Ưu đãi đặc biệt + Noti |
|
|
|
|
---
|
|
|
|
## 7. Thiết kế chi tiết Pattern "Accumulate & Trigger"
|
|
|
|
Dựa trên nguyên lý **Smart Router (Event Processor)** và **Dumb Executor (Attribute Service)**, toàn bộ cấu hình (Rules, Threshold, Trigger) được định nghĩa và quản lý tập trung tại Event Processor.
|
|
|
|
Attribute Service không cần hiểu logic nghiệp vụ của `ViewItem` hay `Login`, nó chỉ hoạt động như một cỗ máy tính toán (Storage + Executor) thực thi các lệnh (instructions) được gửi đến.
|
|
|
|
### 7.1. Cấu hình Event Registry (Tại Event Processor)
|
|
|
|
Mỗi sự kiện loại `COUNTER` sẽ được cấu hình kèm theo khối `counter_instructions`. Khi nhận được Thin Event từ Client, EP sẽ tra cứu Registry này để đóng gói thành Fat Payload.
|
|
|
|
**Ví dụ cấu hình cho UC-02 & UC-05 (Daily Login Counter & Inactivity Check):**
|
|
```json
|
|
{
|
|
"event_name": "Login",
|
|
"event_type": "COUNTER",
|
|
"target_topic": "tracking-topic",
|
|
"counter_instructions": [
|
|
// 1. Instruction cho UC-02 (Chuỗi đăng nhập)
|
|
{
|
|
"attribute_code": "login_streak",
|
|
"entity_type": "CUSTOMER",
|
|
"operation": "STREAK_INCREMENT", // Lệnh đặc biệt đếm chuỗi thời gian
|
|
"time_window": "DAILY",
|
|
"thresholds": [
|
|
{ "value": 7, "trigger_event": "Login_Streak_7_Days", "reset_policy": "NONE" },
|
|
{ "value": 30, "trigger_event": "Login_Streak_30_Days", "reset_policy": "RESET_ON_TRIGGER" }
|
|
]
|
|
},
|
|
// 2. Instruction cho UC-05 (Kiểm tra khách hàng quay lại)
|
|
{
|
|
"attribute_code": "last_login_date",
|
|
"entity_type": "CUSTOMER",
|
|
"operation": "CHECK_AND_UPDATE_TIMER", // Phép toán mới: Tính khoảng trống Inactive
|
|
"thresholds": [
|
|
{ "value": 90, "unit": "DAYS", "trigger_event": "Account_Reactivated", "reset_policy": "RESET_ON_TRIGGER" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Ví dụ cấu hình cho UC-03 (View Item):**
|
|
```json
|
|
{
|
|
"event_name": "ViewItem",
|
|
"event_type": "COUNTER",
|
|
"target_topic": "tracking-topic",
|
|
"instructions": [
|
|
{
|
|
"attribute_code": "weekly_view_count",
|
|
"entity_type": "CUSTOMER",
|
|
"operation": "INCREMENT",
|
|
"period_type": "WEEKLY", // Lazy Reset theo tuần, sử dụng giờ hệ thống (UTC)
|
|
"step": 1,
|
|
"thresholds": [
|
|
{ "value": 10, "trigger_event": "View_10_Items_Reward", "reset_policy": "RESET_ON_TRIGGER" }
|
|
]
|
|
},
|
|
{
|
|
"attribute_code": "item_view_count",
|
|
"entity_type": "ITEM",
|
|
"operation": "INCREMENT",
|
|
// Không truyền period_type -> Mặc định đếm vĩnh viễn (LIFETIME)
|
|
"step": 1,
|
|
"thresholds": []
|
|
},
|
|
{
|
|
"attribute_code": "recently_viewed",
|
|
"entity_type": "CUSTOMER",
|
|
"operation": "APPEND_UNIQUE",
|
|
"value_to_append": "{event.itemId}",
|
|
"max_items": 50 // Giới hạn mảng tối đa 50 item gần nhất
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 7.2. Fat Payload trên Kafka (tracking-topic)
|
|
|
|
Khi Event Processor xử lý xong, nó đẩy một Fat Payload mang theo toàn bộ chỉ thị vào Kafka để Attribute Service xử lý.
|
|
|
|
```json
|
|
{
|
|
"event_id": "evt_998877",
|
|
"source_event": "Login",
|
|
"customer_id": "CUST-001",
|
|
"timestamp": "2026-07-10T16:00:00Z",
|
|
"instructions": [
|
|
{
|
|
"target_entity_id": "CUST-001",
|
|
"attribute_code": "login_streak",
|
|
"operation": "STREAK_INCREMENT",
|
|
"time_window": "DAILY",
|
|
"thresholds": [
|
|
{ "value": 7, "trigger_event": "Login_Streak_7_Days", "reset_policy": "NONE" },
|
|
{ "value": 30, "trigger_event": "Login_Streak_30_Days", "reset_policy": "RESET_ON_TRIGGER" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 7.3. Thực thi tại Attribute Service (Dumb Executor)
|
|
|
|
Để `attribute-service` bao quát được toàn bộ Use Cases mà không chứa business logic, hệ thống định nghĩa sẵn một **Bộ Toán Tử (Operation Set)**. `attribute-service` sẽ consume Fat Payload và xử lý hoàn toàn máy móc dựa trên các toán tử này:
|
|
|
|
| Operation | Kiểu dữ liệu | Diễn giải (Cách tính toán) | Áp dụng cho Use Case |
|
|
|:---|:---|:---|:---|
|
|
| `INCREMENT` / `DECREMENT` | NUMBER | Hợp nhất việc cộng/trừ. Hỗ trợ `period_type` (DAILY, WEEKLY, MONTHLY...) để tự động Lazy Reset dựa vào cột `updated_at` trong EAV, hoặc đếm vĩnh viễn (LIFETIME) nếu không truyền. | Đếm lượt view (UC-03), đếm giao dịch, đếm chu kỳ. |
|
|
| `COMPUTE_GAP` | DATETIME / NUMBER | Tính khoảng cách giữa 2 mốc thời gian (hoặc 2 con số). Trả ra GAP thuần túy để so sánh threshold, không ghi đè dữ liệu gốc. | Tính thời gian hoàn tất đơn hàng, thời gian duyệt. |
|
|
| `CHECK_AND_UPDATE_TIMER` | DATETIME | Lấy thời gian hiện tại (`NOW()`) trừ đi thời gian lưu trong DB. Sau khi kiểm tra threshold xong sẽ tự động ghi đè mốc thời gian đó bằng `NOW()`. | Bắt user ngủ đông (UC-05), timeout, chu kỳ nhắc nhở. |
|
|
| `APPEND_UNIQUE`| STRING (Mảng) | Thêm phần tử vào một mảng hoặc chuỗi nếu chưa tồn tại. Hỗ trợ `maxItems` để duy trì kích thước mảng. | Lưu vết Recently Viewed (UC-03), danh sách thiết bị. |
|
|
| `SET_TRUE_ONCE` | BOOLEAN | Chuyển trạng thái cờ sang `true`. Tính Idempotent cao (chỉ kích hoạt threshold khi chuyển từ `false` -> `true`). | Lần đầu đăng nhập (UC-01), hoàn tất khảo sát (UC-08). |
|
|
| `STREAK_INCREMENT`| NUMBER | So sánh `last_updated_date` với `NOW()`. Nếu qua ngày mới -> `+1`. Nếu cùng ngày -> `bỏ qua`. Nếu cách >= 2 ngày -> `reset = 1`. | Chuỗi đăng nhập liên tiếp (UC-02), chuỗi điểm danh. |
|
|
|
|
**Thuật toán Hợp nhất (Unified Algorithm) cho INCREMENT & Luồng thực thi:**
|
|
1. **Load State & Khóa (Optimistic Locking):** Lấy `old_state` (gồm `value_number` và cột `updated_at`) của entity từ DB lên memory, kèm `version` để chống Race Condition.
|
|
2. Duyệt qua mảng `instructions`, **thực thi Operation** trên memory. Riêng với `INCREMENT`, thuật toán Lazy Reset diễn ra như sau:
|
|
- *Kiểm tra Chu kỳ:* Nếu cấu hình có `period_type` (khác LIFETIME), hệ thống dùng giờ chuẩn (UTC) so sánh `updated_at` với `NOW()`.
|
|
- *Tính toán `new_value`:* Nếu cùng chu kỳ ➔ `value_number + step`. Nếu khác chu kỳ (hoặc `old_state = null`) ➔ Vứt số cũ, gán `new_value = step`.
|
|
3. **Kiểm tra Threshold:** So sánh `new_value` với `threshold` (VD: GAP >= 90 ngày, Streak = 7, View = 10).
|
|
- Nếu chạm ngưỡng, sinh ra event payload gửi sang Transaction Service.
|
|
- Xử lý `reset_policy` (áp dụng cho `INCREMENT`, `STREAK_INCREMENT` hoặc `APPEND_UNIQUE`):
|
|
- `NONE`: Giữ nguyên giá trị (việc reset định kỳ đã được lo bởi cơ chế Lazy Reset của `period_type`, hoàn toàn **không cần Cronjob**).
|
|
- `RESET_ON_TRIGGER`: Tự động reset về 0 (hoặc làm rỗng mảng) ngay lập tức sau khi nhận thưởng. *Không dùng cho `SET_TRUE_ONCE` hay `CHECK_AND_UPDATE_TIMER`.*
|
|
4. **Lưu DB:** Cập nhật đồng loạt các field xuống DB (EAV) trong cùng 1 transaction. Database tự động cập nhật cột `updated_at` thành `NOW()`. Tăng `version` để chốt commit.
|
|
|
|
**Phân tích 3 Use Cases điển hình qua Unified INCREMENT:**
|
|
- **Bài toán 1: Đếm tổng số View (LIFETIME)**
|
|
- *Payload:* Không truyền `period_type`.
|
|
- *Thực thi:* Hệ thống bỏ qua bước check thời gian (hoặc ngầm hiểu luôn cùng chu kỳ). Biến số liên tục cộng dồn vĩnh viễn mỗi khi có event.
|
|
- **Bài toán 2: Đếm View theo Tuần (WEEKLY)**
|
|
- *Payload:* `period_type = "WEEKLY"`.
|
|
- *Thực thi:* Nếu event xảy ra vào Thứ Hai (tuần mới), so sánh `updated_at` (của tuần trước) với `NOW()` sẽ ra khác tuần. Hệ thống tự động vứt bỏ số đếm cũ, reset giá trị về đúng bằng `step` (Lazy Reset). Không cần bất kỳ Cronjob nào chạy ngầm lúc nửa đêm.
|
|
- **Bài toán 3: Lần đầu tiên tham gia (Null State)**
|
|
- *Thực thi:* Record chưa tồn tại trong bảng EAV (`old_state = null`). Hệ thống tự động coi là khác chu kỳ, khởi tạo `new_value = step` và `updated_at = NOW()`.
|
|
|
|
**Góc độ Trải nghiệm người dùng (UX Implications):**
|
|
Vì bản chất là Lazy Reset (chỉ reset khi có tương tác), data dưới DB của user sẽ không tự động nhảy về 0 lúc `00:00` nếu user đó không làm gì. Do đó, API trả về dữ liệu hiển thị cho UI cần lưu ý:
|
|
- API read (ví dụ: `GET /customers/me/attributes`) phải chạy chung hàm `isSamePeriod(updated_at, NOW(), period_type)`.
|
|
- Nếu API phát hiện `updated_at` thuộc về tuần trước, nó sẽ **chủ động trả về giá trị 0** cho UI, mặc dù trong DB vẫn đang nằm nguyên con số của tuần trước.
|
|
- **Kết quả:** UI hiển thị số 0 chính xác ngay khoảnh khắc chuyển giao tuần, user experience cực kỳ mượt mà, DB không chịu bất kỳ tải trọng dư thừa nào.
|
|
|
|
> [!WARNING]
|
|
> **Edge Cases cần lưu ý cho Backend (Attribute Service) khi implement `CHECK_AND_UPDATE_TIMER`:**
|
|
> 1. **Null State (Lần đăng nhập đầu tiên):** Nếu DB trả về giá trị `null`, phép tính `NOW() - null` có thể văng exception hoặc tạo ra số vô cực, gây phát quà sai. **Quy ước:** Nếu Data gốc là `null`, mặc định bỏ qua bước xét Threshold (không phát sự kiện) và chỉ thực hiện gán `NOW()`.
|
|
> 2. **Concurrency (Race Condition):** Nếu user đăng nhập cùng lúc trên 2 thiết bị (2 sự kiện đến cùng lúc), 2 thread có thể đọc chung 1 `last_login_date` và cùng bắn 2 sự kiện thưởng. **Quy ước:** Bắt buộc áp dụng **Optimistic Locking** (dựa trên version entity) tại bước Load State và Lưu DB để chặn thread chạy sau.
|
|
|
|
### 7.4. Payload Trigger Event (Gửi vào events-topic cho Transaction Service)
|
|
|
|
Khi đạt Threshold, Attribute Service đóng gói một Event hợp lệ (Direct Event) và đẩy thẳng vào `events-topic` để TP (Rule Engine) cộng điểm.
|
|
|
|
```json
|
|
{
|
|
"event_id": "attr-gen-8832a74c", // ID mới được gen bởi Attribute Service
|
|
"event_name": "Login_Streak_7_Days", // Lấy từ 'trigger_event' trong cấu hình
|
|
"customer_id": "CUST-001",
|
|
"timestamp": "2026-07-10T16:00:01Z",
|
|
"metadata": {
|
|
"source_system": "attribute-service",
|
|
"trigger_attribute": "login_streak",
|
|
"threshold_reached": 7,
|
|
"original_trigger_event_id": "evt_998877"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Transaction Service (TP)**:
|
|
Lắng nghe `events-topic`, bắt được event `Login_Streak_7_Days`, lục tìm Rule Engine và thấy quy định: *"Tặng 100 điểm"*. Tiến hành trừ kho và cộng điểm, kết thúc flow.
|
|
|
|
--- |