805 lines
30 KiB
Markdown
805 lines
30 KiB
Markdown
# Event Processor Service — High-Level Design v2
|
|
## Event-Driven Loyalty & Rewards Platform
|
|
|
|
---
|
|
|
|
### 1. Mục tiêu & Phạm vi
|
|
|
|
#### 1.1. Mục tiêu
|
|
|
|
- **Chuẩn hóa luồng xử lý event:** Các event phi tài chính đi qua `event-processor-service` để validate, enrich và route.
|
|
- **Bảo vệ Transaction Service (TP):** TP chỉ xử lý command/event có tác động tài chính, voucher, inventory hoặc reward rule.
|
|
- **Tách tracking khỏi core ledger:** Các counter, streak, trạng thái hiển thị và thuộc tính động được xử lý tại `attribute-service`.
|
|
- **Đảm bảo đúng dữ liệu tài chính:** Mọi nghiệp vụ cộng/trừ điểm, cấp voucher, đổi trạng thái voucher, reserve inventory phải nằm trong `transaction-service` và transaction DB.
|
|
- **Cho phép cấu hình chiến dịch phổ biến mà không cần sửa code:** Operation catalog chỉ giữ các primitive cần thiết cho phần lớn use case hiện tại, tránh tạo quá nhiều operation sớm.
|
|
|
|
#### 1.2. Nguyên tắc thiết kế
|
|
|
|
1. **Direct Flow khác Direct Event.**
|
|
- **Direct Flow:** Command cần phản hồi ngay cho user/merchant và cần ACID mạnh. Client gọi thẳng API của `transaction-service`.
|
|
- **Direct Event:** Event bất đồng bộ cần TP xử lý rule/reward, nhưng không cần response sync cho caller. Producer publish vào Kafka `events-topic`.
|
|
2. **Transaction Service** là nơi duy nhất thực hiện nghiệp vụ tài chính: cộng/trừ điểm, cấp voucher, reserve inventory, dùng voucher, giới hạn thưởng.
|
|
3. **Attribute Service** chỉ lưu state và chạy primitive operations. Service này không quyết định "thưởng bao nhiêu"; khi điều kiện tracking đạt threshold, nó phát sinh **một Direct Event mới** để `transaction-service` xử lý rule/reward.
|
|
4. **Event Processor** là smart gateway cho event async: validate schema, tra registry, enrich payload, route vào topic tương ứng.
|
|
5. **Marketing Service** chỉ lập lịch, quét tập khách hàng và phát sinh event/notification. Không cộng điểm hay cấp voucher trực tiếp.
|
|
6. **Mọi publish quan trọng sau khi ghi DB phải dùng Outbox Pattern** để tránh mất event hoặc duplicate do crash/retry.
|
|
|
|
---
|
|
|
|
### 2. Kiến trúc Tổng quan
|
|
|
|
```mermaid
|
|
graph TD
|
|
subgraph Client Layer
|
|
MA[Member App]
|
|
MRA[Merchant App]
|
|
EXT[External Systems]
|
|
end
|
|
|
|
subgraph Gateway Layer
|
|
EP[event-processor-service<br/>Validate + Registry + Route]
|
|
API[TP Sync API]
|
|
end
|
|
|
|
subgraph Kafka
|
|
ET[events-topic<br/>Direct Events / Reward Triggers]
|
|
TT[tracking-topic<br/>Tracking Events]
|
|
NT[noti-topic<br/>Notification Events]
|
|
DLQ[dead-letter-topics]
|
|
end
|
|
|
|
subgraph Core Services
|
|
TP[transaction-service<br/>Rule + Ledger + Inventory]
|
|
ATTR[attribute-service<br/>State + Counters]
|
|
NS[notification-service]
|
|
MKT[marketing-service]
|
|
end
|
|
|
|
subgraph Data Layer
|
|
DB_TP[(PostgreSQL<br/>Transaction DB)]
|
|
DB_ATTR[(PostgreSQL<br/>Attribute DB)]
|
|
REDIS[(Redis<br/>Cache)]
|
|
DORIS[(Doris<br/>BI / Reporting)]
|
|
end
|
|
|
|
MA -->|Tracking / Notification Events| EP
|
|
EXT -->|Async Direct Events| EP
|
|
MKT -->|Scheduled Events| EP
|
|
|
|
MA -->|Direct Flow: Redeem / Transfer| API
|
|
MRA -->|Direct Flow: Scan Voucher| API
|
|
API --> TP
|
|
|
|
EP -->|Direct Event| ET
|
|
EP -->|Tracking Event| TT
|
|
EP -->|Notification Event| NT
|
|
|
|
ET --> TP
|
|
TT --> ATTR
|
|
NT --> NS
|
|
|
|
ATTR -->|Threshold Trigger<br/>via Outbox| ET
|
|
TP -->|Reward / Txn Result<br/>via Outbox| NT
|
|
TP -->|Post-transaction Tracking Event<br/>via Outbox| TT
|
|
|
|
TP --- DB_TP
|
|
ATTR --- DB_ATTR
|
|
ATTR -. recently viewed .-> REDIS
|
|
DB_TP -. CDC/ETL .-> DORIS
|
|
DB_ATTR -. CDC/ETL .-> DORIS
|
|
|
|
ET -. failed .-> DLQ
|
|
TT -. failed .-> DLQ
|
|
NT -. failed .-> DLQ
|
|
```
|
|
|
|
#### 2.1. Event Categories
|
|
|
|
| Category | Khi nào dùng | Đường đi chính | Ví dụ |
|
|
|:---|:---|:---|:---|
|
|
| **Direct Flow** | User/merchant cần kết quả ngay, có ACID tài chính/inventory | Client/Merchant -> `transaction-service` API | `Transfer_Balance`, `Redeem_Item`, `Scan_Voucher` |
|
|
| **Direct Event** | Async event cần TP xử lý reward/rule, không cần sync response. Có thể đến từ producer bên ngoài hoặc được sinh ra bởi Attribute khi Tracking Event đạt threshold | Producer -> EP -> `events-topic` -> TP; hoặc Attribute outbox -> `events-topic` -> TP | `Welcome_Offer`, `Profile_Completed`, `Dormant_User`, `Login_Streak_7_Days` |
|
|
| **Tracking Event** | Cần đếm, tích lũy, set cờ, lưu trạng thái phi tài chính | Producer -> `event-processor-service` -> `tracking-topic` -> `attribute-service` | `Login`, `View_Item`, `Survey_Completed`, `Item_Redeemed_Count` |
|
|
| **Notification Event** | Chỉ gửi thông báo, không cần ledger/counter | Producer -> `event-processor-service` hoặc service outbox -> `noti-topic` -> `notification-service` | `Password_Expiring`, transaction success notification |
|
|
|
|
#### 2.2. Quan hệ Tracking Event và Direct Event phát sinh
|
|
|
|
Tracking Event không trực tiếp yêu cầu TP cộng điểm/cấp voucher. Nó chỉ cập nhật state tại Attribute. Khi state đạt threshold, Attribute tạo ra **Generated Direct Event** mới và publish vào `events-topic`.
|
|
|
|
Ví dụ:
|
|
|
|
```text
|
|
Tracking Event: Login
|
|
-> Attribute cập nhật login_streak = 7
|
|
-> Threshold Login_Streak_7_Days đạt
|
|
-> Attribute publish Direct Event: Login_Streak_7_Days
|
|
-> Transaction Service consume Direct Event và cộng điểm theo rule
|
|
```
|
|
|
|
Quy ước:
|
|
|
|
- Direct Event phát sinh từ threshold phải có `eventCategory = "DIRECT"`.
|
|
- `source` nên là `attribute-service`.
|
|
- Payload phải giữ `sourceEventId` hoặc `causedByEventId` để trace ngược về Tracking Event gốc.
|
|
- TP xử lý Generated Direct Event giống các Direct Event async khác: check idempotency, eligibility, campaign rule, rồi mới apply reward.
|
|
|
|
---
|
|
|
|
### 3. Luồng Xử lý Chuẩn
|
|
|
|
#### 3.1. Tracking Flow — Accumulate & Trigger
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant MA as Member App
|
|
participant EP as Event Processor
|
|
participant TT as tracking-topic
|
|
participant ATTR as Attribute Service
|
|
participant DB as Attribute DB
|
|
participant OUT as Attribute Outbox
|
|
participant ET as events-topic
|
|
participant TP as Transaction Service
|
|
|
|
MA->>EP: Thin Event: Login / View_Item
|
|
EP->>EP: Validate schema + load registry
|
|
EP->>TT: Fat Payload with instructions
|
|
TT->>ATTR: Consume
|
|
ATTR->>DB: BEGIN transaction
|
|
ATTR->>DB: Update attribute state
|
|
ATTR->>DB: Insert threshold_fire_log if threshold reached
|
|
ATTR->>OUT: Insert Generated Direct Event into outbox
|
|
ATTR->>DB: COMMIT
|
|
OUT->>ET: Publish Generated Direct Event
|
|
ET->>TP: Consume Direct Event and apply reward rule
|
|
```
|
|
|
|
**Điểm bắt buộc:**
|
|
|
|
- Update attribute, insert `threshold_fire_log`, và insert outbox record phải nằm trong cùng DB transaction.
|
|
- Generated Direct Event chỉ publish từ outbox sau khi commit thành công.
|
|
- Generated Direct Event là event mới, có `eventId` riêng, nhưng phải tham chiếu Tracking Event gốc qua `causedByEventId`.
|
|
- Nếu publish lỗi, outbox worker retry; không rollback state đã xử lý.
|
|
|
|
#### 3.2. Direct Flow — Sync Financial Command
|
|
|
|
Áp dụng cho nghiệp vụ user/merchant cần biết kết quả ngay: chuyển điểm, đổi quà, quét voucher.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant APP as Member/Merchant App
|
|
participant TP as Transaction Service
|
|
participant DB as Transaction DB
|
|
participant OUT as TP Outbox
|
|
participant NT as noti-topic
|
|
participant TT as tracking-topic
|
|
|
|
APP->>TP: Sync API command
|
|
TP->>DB: BEGIN transaction
|
|
TP->>DB: Validate balance / voucher / inventory
|
|
TP->>DB: Update ledger + inventory/voucher state
|
|
TP->>OUT: Insert notification/tracking events
|
|
TP->>DB: COMMIT
|
|
TP-->>APP: Return success/failure
|
|
OUT->>NT: Publish notification event
|
|
OUT->>TT: Publish post-transaction tracking event if needed
|
|
```
|
|
|
|
**Ví dụ:**
|
|
|
|
- `Redeem_Item`: App gọi TP sync API. Sau khi redeem thành công, TP publish `Item_Redeemed_Count` vào `tracking-topic` để Attribute tăng `total_redeem_count`.
|
|
- `Transfer_Balance`: App gọi TP sync API. Sau commit, TP publish notification cho người gửi và người nhận.
|
|
- `Scan_Voucher`: Merchant App gọi TP sync API. Sau commit, TP publish notification cho customer.
|
|
|
|
#### 3.3. Direct Event Flow — Async Reward/Rule
|
|
|
|
Áp dụng cho event không cần trả kết quả tức thì cho caller nhưng cần TP xử lý rule/reward.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant P as Producer
|
|
participant EP as Event Processor
|
|
participant ET as events-topic
|
|
participant TP as Transaction Service
|
|
participant DB as Transaction DB
|
|
participant OUT as TP Outbox
|
|
participant NT as noti-topic
|
|
|
|
P->>EP: Direct Event: Welcome_Offer / Profile_Completed / Dormant_User
|
|
EP->>EP: Validate + route
|
|
EP->>ET: Publish Direct Event
|
|
ET->>TP: Consume
|
|
TP->>DB: BEGIN transaction
|
|
TP->>DB: Check idempotency + apply reward rule
|
|
TP->>OUT: Insert notification event
|
|
TP->>DB: COMMIT
|
|
OUT->>NT: Publish notification
|
|
```
|
|
|
|
Direct Event async có 2 nguồn hợp lệ:
|
|
|
|
- **Producer-originated Direct Event:** service/hệ thống ngoài gửi event vào EP, ví dụ `Welcome_Offer`, `Profile_Completed`, `Dormant_User`.
|
|
- **Threshold-generated Direct Event:** Attribute sinh event khi Tracking Event đạt threshold, ví dụ `First_Login_Reward`, `Login_Streak_7_Days`, `View_10_Items_Weekly`, `Survey_Completed_Reward`, `Account_Reactivated`.
|
|
|
|
#### 3.4. Notification Flow
|
|
|
|
- Notification thuần túy đi thẳng `noti-topic`.
|
|
- Notification phát sinh từ transaction phải được TP ghi vào outbox cùng transaction DB.
|
|
- Notification Service cần idempotency theo `eventId` để tránh gửi trùng khi Kafka retry.
|
|
|
|
---
|
|
|
|
### 4. Event Envelope Chuẩn
|
|
|
|
Tất cả event đi qua Kafka nên có envelope thống nhất để trace, replay và idempotency.
|
|
|
|
```json
|
|
{
|
|
"eventId": "evt-20260716-abc123",
|
|
"eventName": "Login",
|
|
"eventCategory": "TRACKING",
|
|
"schemaVersion": 1,
|
|
"source": "member-app",
|
|
"occurredAt": "2026-07-16T10:30:00+07:00",
|
|
"publishedAt": "2026-07-16T10:30:02+07:00",
|
|
"correlationId": "corr-789",
|
|
"idempotencyKey": "member-login:CUST-001:2026-07-16",
|
|
"actor": {
|
|
"customerId": "CUST-001"
|
|
},
|
|
"entities": {
|
|
"itemId": "ITEM-789"
|
|
},
|
|
"payload": {}
|
|
}
|
|
```
|
|
|
|
| Field | Bắt buộc | Mục đích |
|
|
|:---|:---:|:---|
|
|
| `eventId` | Yes | Unique event id, dùng cho dedup, DLQ, audit |
|
|
| `eventName` | Yes | Tên event canonical, ví dụ `Login`, `View_Item` |
|
|
| `eventCategory` | Yes | `DIRECT`, `TRACKING`, `NOTIFICATION` |
|
|
| `schemaVersion` | Yes | Cho phép evolve schema |
|
|
| `source` | Yes | Producer/service phát event |
|
|
| `occurredAt` | Yes | Event time |
|
|
| `publishedAt` | Yes | Processing/publish time |
|
|
| `correlationId` | Yes | Trace một flow end-to-end |
|
|
| `idempotencyKey` | Yes | Business dedup key |
|
|
| `actor` | Theo event | Customer/member/merchant liên quan |
|
|
| `entities` | Theo event | Item, voucher, card, survey... |
|
|
| `payload` | Theo event | Data nghiệp vụ riêng |
|
|
|
|
---
|
|
|
|
### 5. Registry & Instruction Model
|
|
|
|
#### 5.1. Registry
|
|
|
|
Registry nằm tại `event-processor-service`, định nghĩa:
|
|
|
|
- Event name và category.
|
|
- Target topic.
|
|
- Required fields/schema.
|
|
- Instruction list cho tracking event.
|
|
- Transaction code cho direct event.
|
|
- Notification template cho notification event.
|
|
|
|
Ví dụ registry cho `Login`:
|
|
|
|
```json
|
|
{
|
|
"eventName": "Login",
|
|
"eventCategory": "TRACKING",
|
|
"targetTopic": "tracking-topic",
|
|
"requiredFields": ["actor.customerId", "occurredAt"],
|
|
"partitionKeyExpression": "Customer:${actor.customerId}",
|
|
"instructions": [
|
|
{
|
|
"name": "First Login Bonus Gate",
|
|
"op": "SET_TRUE_ONCE",
|
|
"entity": "Customer",
|
|
"entityId": "${actor.customerId}",
|
|
"key": "first_login_flag",
|
|
"triggerEventOnSuccess": "First_Login_Reward"
|
|
},
|
|
{
|
|
"name": "Daily Login Streak",
|
|
"op": "INCREMENT",
|
|
"entity": "Customer",
|
|
"entityId": "${actor.customerId}",
|
|
"key": "login_streak",
|
|
"val": 1,
|
|
"mode": "STREAK",
|
|
"period": "DAILY",
|
|
"thresholds": [
|
|
{
|
|
"thresholdId": "Login_Streak_7_Days",
|
|
"evaluateOn": "AFTER_UPDATE",
|
|
"operator": "==",
|
|
"left": "CURRENT_VALUE",
|
|
"target": 7,
|
|
"triggerEvent": "Login_Streak_7_Days",
|
|
"triggerPolicy": "ONCE_LIFETIME"
|
|
},
|
|
{
|
|
"thresholdId": "Login_Streak_30_Days",
|
|
"evaluateOn": "AFTER_UPDATE",
|
|
"operator": "==",
|
|
"left": "CURRENT_VALUE",
|
|
"target": 30,
|
|
"triggerEvent": "Login_Streak_30_Days",
|
|
"triggerPolicy": "ONCE_LIFETIME"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Inactive Account Monitor",
|
|
"op": "SET",
|
|
"entity": "Customer",
|
|
"entityId": "${actor.customerId}",
|
|
"key": "last_login_date",
|
|
"val": "${occurredAt}",
|
|
"thresholds": [
|
|
{
|
|
"thresholdId": "Account_Reactivated_90D",
|
|
"evaluateOn": "BEFORE_UPDATE",
|
|
"operator": "TIME_GAP_GT",
|
|
"left": "OLD_VALUE",
|
|
"right": "${occurredAt}",
|
|
"target": 90,
|
|
"unit": "DAY",
|
|
"triggerEvent": "Account_Reactivated",
|
|
"triggerPolicy": "ONCE_PER_PERIOD",
|
|
"period": "YEARLY"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### 5.2. Instruction Format
|
|
|
|
```json
|
|
{
|
|
"name": "Weekly View Count",
|
|
"op": "INCREMENT",
|
|
"entity": "Customer",
|
|
"entityId": "${actor.customerId}",
|
|
"key": "weekly_view_count",
|
|
"val": 1,
|
|
"mode": "WINDOW",
|
|
"period": "WEEKLY",
|
|
"thresholds": [
|
|
{
|
|
"thresholdId": "View_10_Items_Weekly",
|
|
"evaluateOn": "AFTER_UPDATE",
|
|
"operator": ">=",
|
|
"left": "CURRENT_VALUE",
|
|
"target": 10,
|
|
"triggerEvent": "View_10_Items_Weekly",
|
|
"triggerPolicy": "ONCE_PER_PERIOD",
|
|
"period": "WEEKLY"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
| Field | Bắt buộc | Ghi chú |
|
|
|:---|:---:|:---|
|
|
| `name` | Yes | Tên dễ đọc cho BA/operation |
|
|
| `op` | Yes | `SET`, `SET_TRUE_ONCE`, `INCREMENT`, `APPEND_UNIQUE` |
|
|
| `entity` | Yes | `Customer`, `Item`, `Voucher`, `Survey`... |
|
|
| `entityId` | Yes | ID object được update state |
|
|
| `key` | Yes | Attribute key |
|
|
| `val` | Theo op | Giá trị input |
|
|
| `mode` | Optional | Chỉ dùng cho `INCREMENT`: `LIFETIME`, `WINDOW`, `STREAK` |
|
|
| `period` | Optional | `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY` |
|
|
| `thresholds` | Optional | Mảng threshold. Mỗi threshold tự khai báo `evaluateOn` là `BEFORE_UPDATE` hoặc `AFTER_UPDATE` |
|
|
| `triggerEventOnSuccess` | Optional | Dùng cho state transition như `SET_TRUE_ONCE` |
|
|
|
|
---
|
|
|
|
### 6. Operation Catalog Phase 1
|
|
|
|
Thiết kế phase 1 chỉ cần 4 primitive operations. Đây là bộ đủ cho phần lớn nghiệp vụ loyalty thực tế: first action, streak, periodic counter, total counter, timer gap và recent list.
|
|
|
|
#### 6.1. `SET`
|
|
|
|
Ghi đè giá trị attribute.
|
|
|
|
| Thuộc tính | Giá trị |
|
|
|:---|:---|
|
|
| Kiểu value | String, Number, Boolean, DateTime |
|
|
| Idempotent | Không mặc định; phụ thuộc `idempotencyKey` event |
|
|
| Use cases | UC-05 `last_login_date`, các mốc thời gian/state đơn giản |
|
|
|
|
Với bài toán inactive login, `SET` dùng threshold có `evaluateOn = BEFORE_UPDATE`: check khoảng cách giữa `occurredAt` và old value trước, sau đó mới ghi value mới.
|
|
|
|
```text
|
|
old last_login_date = 2026-04-15
|
|
event occurredAt = 2026-07-16
|
|
TIME_GAP_GT 90 days = true
|
|
=> emit Account_Reactivated
|
|
=> SET last_login_date = 2026-07-16
|
|
```
|
|
|
|
#### 6.2. `SET_TRUE_ONCE`
|
|
|
|
Chuyển Boolean từ `false/null/not_exists` sang `true` đúng một lần.
|
|
|
|
| Thuộc tính | Giá trị |
|
|
|:---|:---|
|
|
| Kiểu value | Boolean |
|
|
| Idempotent | Có |
|
|
| Trigger | Chỉ trigger khi state transition thành công |
|
|
| Use cases | UC-01 First Login, UC-08 Survey Completed |
|
|
|
|
Best practice: dùng operation này làm **gate** cho reward chỉ được nhận một lần. Với UC-08, chỉ khi `survey_completed` chuyển thành true thì mới emit `Survey_Completed_Reward`.
|
|
|
|
#### 6.3. `INCREMENT`
|
|
|
|
Cộng số theo 3 mode.
|
|
|
|
| Mode | Ý nghĩa | Use cases |
|
|
|:---|:---|:---|
|
|
| `LIFETIME` | Counter tích lũy không reset | UC-12 `total_redeem_count`, UC-03 `item_view_count` |
|
|
| `WINDOW` | Counter reset theo kỳ cố định | UC-03 `weekly_view_count` |
|
|
| `STREAK` | Đếm chuỗi liên tiếp theo ngày/tuần/tháng | UC-02 `login_streak` |
|
|
|
|
Quy tắc:
|
|
|
|
- `period` chỉ bắt buộc với `WINDOW` và `STREAK`.
|
|
- `STREAK DAILY` bỏ qua event trùng ngày để login nhiều lần không tăng streak.
|
|
- `WINDOW WEEKLY` tính theo ISO week, thứ Hai là ngày đầu tuần.
|
|
- Nếu khác period/window, value reset về `val`, không reset về 0 rồi increment riêng.
|
|
|
|
#### 6.4. `APPEND_UNIQUE`
|
|
|
|
Thêm phần tử vào list, có chống trùng và giới hạn độ dài.
|
|
|
|
| Thuộc tính | Giá trị |
|
|
|:---|:---|
|
|
| Kiểu value | Array |
|
|
| Options | `maxSize`, `dedupeKey` |
|
|
| Use cases | UC-03 `recently_viewed` |
|
|
|
|
Ví dụ:
|
|
|
|
```json
|
|
{
|
|
"op": "APPEND_UNIQUE",
|
|
"entity": "Customer",
|
|
"entityId": "${actor.customerId}",
|
|
"key": "recently_viewed",
|
|
"val": {
|
|
"itemId": "${entities.itemId}",
|
|
"viewedAt": "${occurredAt}"
|
|
},
|
|
"options": {
|
|
"dedupeKey": "itemId",
|
|
"maxSize": 50
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 6.5. Không tạo operation riêng ở phase 1
|
|
|
|
Các operation sau **chưa cần tách riêng** vì 13 use case hiện tại xử lý được bằng 4 primitive trên:
|
|
|
|
- `STREAK_INCREMENT`: dùng `INCREMENT` với `mode = STREAK`.
|
|
- `CHECK_AND_UPDATE_TIMER`: dùng `SET` với threshold `evaluateOn = BEFORE_UPDATE`.
|
|
- `APPEND`: dùng `APPEND_UNIQUE`; nếu cần cho phép trùng sau này, thêm option `unique = false` thay vì tạo op mới.
|
|
- `COMPUTE_GAP`: dùng threshold operator `TIME_GAP_GT` trên old value và `occurredAt`.
|
|
|
|
---
|
|
|
|
### 7. Threshold & Trigger Policy
|
|
|
|
#### 7.1. Threshold Format
|
|
|
|
```json
|
|
{
|
|
"thresholdId": "View_10_Items_Weekly",
|
|
"evaluateOn": "AFTER_UPDATE",
|
|
"operator": ">=",
|
|
"left": "CURRENT_VALUE",
|
|
"target": 10,
|
|
"triggerEvent": "View_10_Items_Weekly",
|
|
"triggerPolicy": "ONCE_PER_PERIOD",
|
|
"period": "WEEKLY"
|
|
}
|
|
```
|
|
|
|
| Field | Bắt buộc | Ghi chú |
|
|
|:---|:---:|:---|
|
|
| `thresholdId` | Yes | Stable id, không đổi khi đổi display name |
|
|
| `evaluateOn` | Yes | `AFTER_UPDATE` cho counter/state thông thường; `BEFORE_UPDATE` cho check giá trị cũ trước khi ghi |
|
|
| `operator` | Yes | `==`, `>=`, `>`, `TIME_GAP_GT` |
|
|
| `left` | Theo operator | `CURRENT_VALUE` hoặc `OLD_VALUE`; UI có thể ẩn field này và tự set theo `evaluateOn` |
|
|
| `right` | Theo operator | Giá trị so sánh bên phải, ví dụ `${occurredAt}` cho `TIME_GAP_GT` |
|
|
| `target` | Yes | Giá trị so sánh |
|
|
| `triggerEvent` | Yes | Tên Generated Direct Event emit vào `events-topic` |
|
|
| `triggerPolicy` | Yes | `ONCE_LIFETIME` hoặc `ONCE_PER_PERIOD` |
|
|
| `period` | Khi cần | Bắt buộc với `ONCE_PER_PERIOD` |
|
|
|
|
`evaluateOn` giúp UI chỉ cần một Threshold Builder duy nhất:
|
|
|
|
- Mặc định là `AFTER_UPDATE`, dùng cho `INCREMENT`, `SET_TRUE_ONCE` và phần lớn threshold.
|
|
- `BEFORE_UPDATE` chỉ nên hiển thị khi operation là `SET` và attribute type là `DateTime`.
|
|
- Với `TIME_GAP_GT`, UI có thể hiển thị đơn giản: "Giá trị cũ của `last_login_date` cách `occurredAt` lớn hơn 90 ngày", không cần bắt user chọn `left/right`.
|
|
|
|
#### 7.2. Trigger Policy
|
|
|
|
| Policy | Ý nghĩa | Ví dụ |
|
|
|:---|:---|:---|
|
|
| `ONCE_LIFETIME` | Một entity chỉ trigger một lần trong toàn bộ vòng đời | First login, survey completed, welcome offer |
|
|
| `ONCE_PER_PERIOD` | Một entity chỉ trigger một lần trong mỗi kỳ | Weekly view reward, yearly inactive account reward |
|
|
|
|
`threshold_fire_log` cần có đủ scope để enforce đúng policy.
|
|
|
|
```text
|
|
Unique key for ONCE_LIFETIME:
|
|
entity_type + entity_id + attribute_key + threshold_id
|
|
|
|
Unique key for ONCE_PER_PERIOD:
|
|
entity_type + entity_id + attribute_key + threshold_id + period_type + period_key
|
|
```
|
|
|
|
Ví dụ `period_key`:
|
|
|
|
- `WEEKLY`: `2026-W29`
|
|
- `MONTHLY`: `2026-07`
|
|
- `YEARLY`: `2026`
|
|
|
|
#### 7.3. Threshold Fire Log
|
|
|
|
```text
|
|
threshold_fire_log
|
|
- id
|
|
- entity_type
|
|
- entity_id
|
|
- attribute_key
|
|
- threshold_id
|
|
- trigger_policy
|
|
- period_type
|
|
- period_key
|
|
- source_event_id
|
|
- trigger_event_id
|
|
- fired_at
|
|
- created_at
|
|
```
|
|
|
|
Quy trình:
|
|
|
|
1. Operation thực thi xong và value thật sự thay đổi.
|
|
2. Attribute Service evaluate threshold.
|
|
3. Nếu đạt, insert `threshold_fire_log` bằng unique key theo policy.
|
|
4. Nếu insert thành công, tạo Generated Direct Event và insert vào outbox.
|
|
5. Nếu unique conflict, bỏ qua vì threshold đã fire trong scope đó.
|
|
|
|
#### 7.4. Generated Direct Event Payload
|
|
|
|
Khi threshold đạt, Attribute không publish lại Tracking Event gốc. Nó tạo một Direct Event mới, có semantic rõ ràng cho TP.
|
|
|
|
```json
|
|
{
|
|
"eventId": "evt-direct-20260716-001",
|
|
"eventName": "Login_Streak_7_Days",
|
|
"eventCategory": "DIRECT",
|
|
"schemaVersion": 1,
|
|
"source": "attribute-service",
|
|
"occurredAt": "2026-07-16T10:30:00+07:00",
|
|
"publishedAt": "2026-07-16T10:30:05+07:00",
|
|
"correlationId": "corr-789",
|
|
"idempotencyKey": "reward:Login_Streak_7_Days:Customer:CUST-001",
|
|
"causedByEventId": "evt-login-20260716-abc123",
|
|
"actor": {
|
|
"customerId": "CUST-001"
|
|
},
|
|
"entities": {
|
|
"attributeKey": "login_streak"
|
|
},
|
|
"payload": {
|
|
"thresholdId": "Login_Streak_7_Days",
|
|
"attributeValue": 7,
|
|
"triggerPolicy": "ONCE_LIFETIME"
|
|
}
|
|
}
|
|
```
|
|
|
|
TP chỉ dựa vào `eventName`/transaction code/rule config để quyết định reward. Attribute không truyền số điểm/voucher cần cấp, trừ khi đó là metadata để audit.
|
|
|
|
---
|
|
|
|
### 8. Partitioning, Concurrency & Idempotency
|
|
|
|
#### 8.1. Partition Key
|
|
|
|
Không partition cố định theo `customerId`. Partition key phải theo state được update.
|
|
|
|
```text
|
|
partitionKey = entityType + ":" + entityId
|
|
```
|
|
|
|
Ví dụ:
|
|
|
|
| Event | State update | Partition key |
|
|
|:---|:---|:---|
|
|
| `Login` | Customer streak/cờ login | `Customer:CUST-001` |
|
|
| `View_Item` weekly count | Customer weekly view | `Customer:CUST-001` |
|
|
| `View_Item` item counter | Item view count | `Item:ITEM-789` |
|
|
| `Item_Redeemed_Count` | Item redeem count | `Item:ITEM-789` |
|
|
| `Survey_Completed` | Customer survey flag | `Customer:CUST-001` |
|
|
|
|
Nếu một incoming event cần update nhiều entity khác nhau, Event Processor tách thành nhiều tracking messages, mỗi message có partition key riêng. Ví dụ `View_Item` có thể tạo:
|
|
|
|
- Message 1: update `Customer:CUST-001` cho `weekly_view_count` và `recently_viewed`.
|
|
- Message 2: update `Item:ITEM-789` cho `item_view_count`.
|
|
|
|
#### 8.2. Idempotency
|
|
|
|
- Mỗi service lưu processed event theo `eventId` hoặc `idempotencyKey`.
|
|
- TP dùng business idempotency key cho transaction/reward để chống cộng điểm/cấp voucher trùng.
|
|
- Attribute dùng event idempotency để tránh tăng counter trùng khi producer retry.
|
|
- Notification Service dùng `eventId` hoặc notification dedupe key để tránh gửi thông báo trùng.
|
|
|
|
---
|
|
|
|
### 9. Use Case Mapping
|
|
|
|
| UC | Event/Command | Category | Flow | Operation | Trigger/Output |
|
|
|:---|:---|:---|:---|:---|:---|
|
|
| UC-01 First Login | `Login` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `SET_TRUE_ONCE first_login_flag` | `First_Login_Reward` Direct Event -> +100đ + noti |
|
|
| UC-02 Daily Login | `Login` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `INCREMENT login_streak mode=STREAK period=DAILY` | `Login_Streak_7_Days`, `Login_Streak_30_Days` Direct Events |
|
|
| UC-03 View Item | `View_Item` | Tracking Event, optional Generated Direct Event | EP -> `tracking-topic` -> ATTR | `INCREMENT weekly_view_count mode=WINDOW period=WEEKLY`, `INCREMENT item_view_count mode=LIFETIME`, `APPEND_UNIQUE recently_viewed` | Weekly threshold có thể emit `View_10_Items_Weekly` Direct Event; item/recent list phục vụ recommend |
|
|
| UC-04 Transfer Balance | `Transfer_Balance` command | Direct Flow | App -> TP sync API | N/A | Debit/Credit ACID + noti |
|
|
| UC-05 Inactive Account Login | `Login` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `SET last_login_date` with threshold `evaluateOn=BEFORE_UPDATE`, `TIME_GAP_GT 90D` | `Account_Reactivated` Direct Event -> welcome back offer/noti |
|
|
| UC-06 Welcome Offer | `Welcome_Offer` | Direct Event | External/Card System -> EP -> `events-topic` -> TP | N/A | +100đ, `ONCE_LIFETIME` by TP idempotency |
|
|
| UC-07 Update Profile | `Profile_Completed` | Direct Event | Profile Service -> EP -> `events-topic` -> TP | N/A | +100đ nếu hoàn tất trước deadline |
|
|
| UC-08 Survey | `Survey_Completed` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `SET_TRUE_ONCE survey_completed:{surveyId}` | `Survey_Completed_Reward` Direct Event; chỉ reward khi set flag thành công |
|
|
| UC-09 Redeem Item | `Redeem_Item` command | Direct Flow | App -> TP sync API | N/A | Trừ điểm + reserve inventory ACID; TP publish noti và tracking event sau commit |
|
|
| UC-10 Merchant Scan | `Scan_Voucher` command | Direct Flow | Merchant App -> TP sync API | N/A | Voucher USED ACID + noti |
|
|
| UC-11 Password Expiring | `Password_Expiring` | Notification Event | Marketing -> EP/`noti-topic` -> NS | N/A | Push/Email cảnh báo |
|
|
| UC-12 Most Redeem Item | `Item_Redeemed_Count` | Tracking Event | TP outbox -> `tracking-topic` -> ATTR | `INCREMENT total_redeem_count mode=LIFETIME` on Item | Cập nhật ranking/reporting |
|
|
| UC-13 Dormant User | `Dormant_User` | Direct Event | Marketing -> EP -> `events-topic` -> TP | N/A | Cấp ưu đãi riêng + noti |
|
|
|
|
---
|
|
|
|
### 10. Chi tiết Các Use Case Quan trọng
|
|
|
|
#### 10.1. UC-08 Survey Reward Gate
|
|
|
|
UC-08 không nên dual publish trực tiếp tới cả TP và Attribute, vì có nguy cơ cấp voucher trước khi biết survey flag đã được set thành công.
|
|
|
|
Luồng chuẩn:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant APP as Member App
|
|
participant EP as Event Processor
|
|
participant TT as tracking-topic
|
|
participant ATTR as Attribute Service
|
|
participant ET as events-topic
|
|
participant TP as Transaction Service
|
|
|
|
APP->>EP: Survey_Completed
|
|
EP->>TT: tracking message
|
|
TT->>ATTR: Consume
|
|
ATTR->>ATTR: SET_TRUE_ONCE survey_completed:{surveyId}
|
|
alt first completion
|
|
ATTR->>ET: Generated Direct Event: Survey_Completed_Reward
|
|
ET->>TP: Consume Direct Event and grant voucher
|
|
else already completed
|
|
ATTR->>ATTR: No-op, no reward
|
|
end
|
|
```
|
|
|
|
#### 10.2. UC-12 Redeem Counter
|
|
|
|
Redeem là Direct Flow, không đi qua Event Processor cho primary transaction.
|
|
|
|
Luồng chuẩn:
|
|
|
|
1. Member App gọi TP API `Redeem_Item`.
|
|
2. TP validate balance, inventory, voucher rule trong DB transaction.
|
|
3. TP commit thành công.
|
|
4. TP outbox publish:
|
|
- Notification event vào `noti-topic`.
|
|
- `Item_Redeemed_Count` vào `tracking-topic`.
|
|
5. Attribute tăng `Item:{itemId}:total_redeem_count`.
|
|
|
|
Nếu redeem fail, không publish tracking event.
|
|
|
|
---
|
|
|
|
### 11. Data Model Khuyến nghị
|
|
|
|
#### 11.1. Attribute Record
|
|
|
|
```text
|
|
attribute_record
|
|
- entity_type -- Customer, Item, Voucher...
|
|
- entity_id
|
|
- attribute_key
|
|
- value_json
|
|
- value_type
|
|
- last_event_id
|
|
- last_event_time
|
|
- created_at
|
|
- updated_at
|
|
|
|
unique(entity_type, entity_id, attribute_key)
|
|
```
|
|
|
|
#### 11.2. Processed Event
|
|
|
|
```text
|
|
processed_event
|
|
- service_name
|
|
- event_id
|
|
- idempotency_key
|
|
- processed_at
|
|
- status
|
|
|
|
unique(service_name, event_id)
|
|
unique(service_name, idempotency_key) where idempotency_key is not null
|
|
```
|
|
|
|
#### 11.3. Outbox
|
|
|
|
```text
|
|
outbox_event
|
|
- id
|
|
- aggregate_type
|
|
- aggregate_id
|
|
- event_name
|
|
- event_payload
|
|
- target_topic
|
|
- idempotency_key
|
|
- status
|
|
- retry_count
|
|
- next_retry_at
|
|
- created_at
|
|
- published_at
|
|
```
|
|
|
|
---
|
|
|
|
### 12. Reliability, DLQ & Replay
|
|
|
|
- Consumer retry có giới hạn, ví dụ 3 lần nhanh + backoff.
|
|
- Sau khi retry hết, event vào DLQ tương ứng: `events-dlq`, `tracking-dlq`, `noti-dlq`.
|
|
- DLQ payload phải giữ nguyên envelope, error code, stack trace tóm tắt, service name và failed timestamp.
|
|
- Replay DLQ phải đi qua cùng idempotency guard như event bình thường.
|
|
- Monitoring tối thiểu:
|
|
- consumer lag theo topic/consumer group;
|
|
- outbox pending count;
|
|
- DLQ rate;
|
|
- reward duplicate/conflict count;
|
|
- notification send failure rate.
|
|
|
|
---
|
|
|
|
### 13. Security & Governance
|
|
|
|
- Client không được gửi instruction/rule trong event. Instruction chỉ lấy từ registry server-side.
|
|
- Event Processor validate schema, source, authentication/authorization và schema version.
|
|
- TP không tin tuyệt đối Generated Direct Event; TP vẫn check transaction code, campaign rule, eligibility và idempotency trước khi cấp reward.
|
|
- Các field nhạy cảm trong event payload cần được mask/encrypt theo chuẩn bảo mật hiện hành.
|
|
- Registry thay đổi phải có audit log: ai sửa, sửa gì, effective time, rollback version.
|
|
|
|
---
|
|
|
|
### 14. Kết luận Thiết kế
|
|
|
|
Thiết kế v2 chốt các điểm chính:
|
|
|
|
- `Direct Flow` là sync API vào TP cho nghiệp vụ cần phản hồi ngay và ACID.
|
|
- `Direct Event` là async Kafka event vào `events-topic` cho reward/rule không cần sync response.
|
|
- Tracking/counter/state nằm ở Attribute, không đẩy counter tần suất cao vào TP.
|
|
- Khi Tracking Event đạt threshold, Attribute sinh một Generated Direct Event mới vào `events-topic`; TP consume event này như Direct Event async bình thường.
|
|
- UC-08 dùng Attribute làm gate để tránh cấp voucher trùng.
|
|
- UC-12 lấy event sau transaction thành công từ TP outbox, không làm ảnh hưởng luồng redeem sync.
|
|
- Operation catalog phase 1 chỉ gồm `SET`, `SET_TRUE_ONCE`, `INCREMENT`, `APPEND_UNIQUE`; các biến thể streak/window/timer là mode hoặc threshold, không phải operation riêng.
|
|
- Trigger policy chỉ cần `ONCE_LIFETIME` và `ONCE_PER_PERIOD` cho giai đoạn hiện tại.
|