Files
cn-ui/Use_Cases_Analysis.md

63 KiB

Phân tích Use Cases - Event Processor Service

Dựa trên các trao đổi và hệ sinh thái hiện tại của nền tảng, tài liệu này đã được cập nhật lại để làm rõ luồng xử lý và kiến trúc cho 13 Use Cases.

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 - counter). 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: Chịu trách nhiệm 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):
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)
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
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
Khách hàng đăng nhập chuỗi 30 ngày liên tiếp nhận 500 điểm
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 ...
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)

3. Đánh giá lại Kiến trúc (Re-evaluation)

Với hệ sinh thái hiện tại (đặc biệt: transaction-service là Rule Engine + Counter, và Redis chỉ làm cache), vai trò của event-processor-service đã thay đổi so với góc nhìn ban đầu. Nó KHÔNG phải là nơi tính toán Rule (Rule Engine), mà là Cửa ngõ tiếp nhận và định tuyến sự kiện (Event Ingestor & Router).

Dưới đây là đánh giá lại phương án xử lý cho từng nhóm vấn đề:

Vấn đề 1: Xử lý Login & Counter (UC-01 & UC-02)

  • Ràng buộc: Event Login có tần suất rất cao (nhiều lần/ngày/user). Nếu đẩy tất cả vào transaction-service sẽ gây nghẽn Core Engine. Tuy nhiên, UC-01 lại yêu cầu realtime cho lần đầu.
  • Phương án điều chỉnh (Tách biệt logic qua Attribute Service):
    • UC-01 & UC-02 (First Login & Daily Login): Đều sử dụng chung event Login (Loại Counter Event), được đẩy vào tracking-topic.
    • attribute-service sẽ hứng Login và thực hiện đồng thời 2 rule:
      • First Login Bonus (UC-01): Sử dụng instruction SET_TRUE_ONCE trên attribute first_login_flag. Nếu gán True thành công (tức là đăng nhập lần đầu), hệ thống sẽ trigger event First_Login_Reward gửi vào events-topic để TP cộng 100 điểm ngay lập tức.
      • Daily Login Counter (UC-02): Tính toán last_login_datecurrent_streak. Tránh được việc lưu rác vào TP (Idempotent nếu login nhiều lần 1 ngày). Khi streak chạm 7 hoặc 30 ngày, bắn trigger event (Login_Streak_7_Days) vào events-topic.

Vấn đề 2: Xử lý Tracking & Counter Events — Pattern "Accumulate & Trigger" (UC-03 & UC-12)

  • Ràng buộc: transaction-service xử lý giao dịch tài chính. Tuyệt đối không để các event theo dõi hành vi (tracking) làm nghẽn luồng tài chính cốt lõi.

  • Ý kiến của anh: Cần có giải pháp để xác định event thuộc loại counter (có thể cân nhắc Dynamic Attribute), phân loại vào topic riêng. Chỉ khi đạt mức cấu hình (threshold) thì mới trigger gửi vào events-topic để transaction-service xử lý.

  • Phương án: Pattern "Accumulate & Trigger" dùng Dynamic Attribute

    Ý tưởng cốt lõi: event-processor-service phân loại event thành 2 nhóm:

    • Direct Events (Login, Redeem, Transfer...): Đẩy thẳng events-topictransaction-service.
    • Counter Events (ViewItem, RedeemItem count...): Đẩy vào tracking-topic → tích lũy counter qua attribute-service → chỉ khi chạm ngưỡng mới trigger events-topic.

    Luồng xử lý chi tiết:

    member-app ──(ViewItem)──▶ event-processor-service
                                      │
                                      │ Phân loại: Counter Event
                                      ▼
                               tracking-topic (Kafka)
                                      │
                                      ▼
                               attribute-service
                                      │
                           ┌──────────┴──────────┐
                           │ 1. Tăng counter      │
                           │    (Dynamic Attr)     │
                           │ 2. Check threshold    │
                           │    (cấu hình)         │
                           └──────────┬──────────┘
                                      │
                            Counter < threshold?
                             ┌───────┴───────┐
                             │ YES           │ NO (đạt ngưỡng)
                             ▼               ▼
                           (dừng)     events-topic (Kafka)
                                             │
                                             ▼
                                     transaction-service
                                     (cộng điểm/voucher)
    

    Ví dụ cụ thể cho UC-03 (View Item):

    • Cấu hình Rule: "Xem 10 sản phẩm khác nhau trong tuần → thưởng 50 điểm".
    • attribute-service tạo Dynamic Attribute weekly_view_count (type: INTEGER) cho entity Customer.
    • Mỗi event ViewItemattribute-service tăng counter weekly_view_count += 1.
    • Khi weekly_view_count >= 10 → attribute-service bắn event Counter_Threshold_Reached vào events-topic.
    • transaction-service hứng event → cộng 50 điểm → reset counter.

    Ví dụ cụ thể cho UC-12 (Most Redeem Item):

    • Cấu hình Rule: Counter thuần túy, không cần trigger (chỉ cập nhật số lượng).
    • attribute-service tạo Dynamic Attribute total_redeem_count (type: INTEGER) cho entity Product (Item).
    • Mỗi event ItemRedeemedattribute-service tăng counter → Portal query attribute này để hiển thị "Most Popular".

    Thống nhất giải pháp cho 2 câu hỏi mở (Theo hướng giảm tải TP):

    Q1: Ai quản lý cấu hình threshold?

    • Phương án chốt: Đặt cấu hình threshold ngay tại cấu hình events / metadata của Dynamic Attribute (trong attribute-service), tách biệt hoàn toàn khỏi transaction-service.
    • Lý do & Luồng chạy: Để giảm tải tối đa cho transaction-service (TP). Nhóm Event/Attribute sẽ hoạt động độc lập (self-contained).
      • Attribute weekly_view_count sẽ lưu kèm cấu hình: { threshold: 10, trigger_event: "View_10_Items_Reward" }.
      • Cứ mỗi event ViewItem, attribute-service tự tăng counter và tự đối chiếu với threshold nội tại.
      • CHỈ KHI chạm ngưỡng 10, attribute-service mới sinh ra 1 event View_10_Items_Reward đẩy vào events-topic. Lúc này TP mới nhảy vào cuộc để cộng điểm.

    Q2: Reset counter khi nào? Ai chịu trách nhiệm?

    • Phương án chốt: Hoạt động theo mô hình tự trị.
      • Tự reset khi Trigger: Ngay khoảnh khắc attribute-service đẩy event View_10_Items_Reward đi, nó tự động reset counter của user đó về 0 (hoặc trừ đi 10). Không cần đợi TP xử lý xong để giảm giao tiếp vòng vèo (vì đây là event tracking, chấp nhận eventual consistency).
      • Reset theo chu kỳ (Time-based): Đối với các counter có thời hạn (ví dụ: đếm trong tuần), hệ thống cần cơ chế Cronjob (từ marketing-service hoặc nội tại attribute-service) quét và reset toàn bộ counter về 0 vào lúc 00:00 thứ Hai hàng tuần.

Vấn đề 3: Dynamic Attribute cho Khảo sát (UC-08)

  • Phương án điều chỉnh (Event-Driven hoàn toàn):
    • Trạng thái "đã làm khảo sát" được lưu động bằng EAV trong attribute-service.
    • Không cần gọi API đồng bộ. Khi user hoàn thành, member-app chỉ cần bắn event Survey_Completed (loại COUNTER) về event-processor-service.
    • Event Processor sẽ tự động route song song:
      1. Đẩy vào events-topic: transaction-service hứng để tặng voucher.
      2. Đẩy vào tracking-topic: attribute-service hứng và thực thi lệnh SET_TRUE_ONCE để update attribute survey_completed = true.

Vấn đề 4: Time-based Triggers & Cross-System (UC-11, UC-13, UC-10)

  • UC-11 & UC-13 (Retention): marketing-service chạy Cronjob quét database (PostgreSQL). Tìm ra danh sách User thoả mãn -> Bắn event vào events-topic. transaction-service hứng và cấp ưu đãi, sau đó đẩy thông báo sang noti-topic.
  • UC-10 (Merchant Scan Voucher): merchant-app quét mã -> gọi API của transaction-service. transaction-service xử lý trừ voucher trong DB (đảm bảo ACID). Thành công -> bắn event Voucher_Redeemed vào noti-topic -> notification-service hứng và gọi Firebase push realtime hiển thị thông báo lên member-app.

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.
    • Counter 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-topictransaction-service.
    • Counter Events (Login, ViewItem, RedeemItem count...) → tracking-topicattribute-service (tích lũy, khi đạt threshold → trigger events-topic).
    • Notification Events → noti-topicnotification-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
                          └─────────────────────────────────────────

5. Các Câu hỏi Cần Bàn bạc với Team

Các phương án ở trên dựa trên giả định ban đầu trong quá trình phân tích. Dưới đây là tổng hợp tất cả các quyết định kiến trúc cần team xác nhận hoặc thảo luận thêm.

5.1. Kiến trúc Tổng quan

# Câu hỏi Giả định hiện tại Cần xác nhận
A1 Fat Event vs Thin Event: Event gửi vào hệ thống nên chứa sẵn context (Fat) hay chỉ chứa ID (Thin) rồi service tự tra cứu? Hybrid: Profile API bắn Fat Event (Profile_Completed), còn Login là Thin Event (Event Processor/TP tự check). Team có đồng ý mô hình hybrid này không? Có event nào khác cần là Fat Event?
A2 Vai trò của event-processor-service: Nó chỉ là Gateway/Router hay cần tham gia xử lý logic? Chỉ làm Smart Gateway: Ingest → Classify → Route. Không chứa business logic. Có trường hợp nào Event Processor cần tự xử lý logic trước khi route không?
A3 Phân tách Kafka Topics: Hiện tại có events-topicnoti-topic. Có cần thêm tracking-topic cho counter events không? Cần thêm tracking-topic để tách luồng tracking/counter khỏi luồng giao dịch tài chính. Team đồng ý tách thêm topic? Hay dùng cách khác (VD: header-based routing trên cùng 1 topic)?

5.2. Xử lý Login & Trạng thái Khách hàng (UC-01, UC-02, UC-05)

# Câu hỏi Giả định hiện tại Cần xác nhận
B1 First Login Event (UC-01): Dùng Login chung với chuỗi đăng nhập. Dùng SET_TRUE_ONCE để bắn trigger event First_Login_Reward. Cần đảm bảo UI Client xử lý popup thưởng 100 điểm realtime khi nhận Noti trả về.
B2 Phát hiện "Inactive" (UC-05) như thế nào? Bao nhiêu ngày không đăng nhập? Sử dụng phép toán CHECK_AND_UPDATE_TIMER tại Event Processor. Khi Login, EP đóng gói lệnh check last_login_date gửi sang Attribute Service tính toán khoảng thời gian (Gap). Chốt lại ngưỡng bao nhiêu ngày thì gửi Noti Welcome Back (VD: 90 ngày)? Có cần cấu hình ngưỡng này khác nhau cho từng hạng thẻ không?
B3 Reset Counter Login (UC-02): Khi user lỡ 1 ngày, streak bị reset về 0. attribute-service tự so sánh last_login_date để reset streak. Logic "ngày" tính theo múi giờ nào (VD: UTC+7)? Hệ thống có ghi nhận time zone của user không?
B4 Giới hạn 1k điểm/năm (UC-02): Tính theo năm dương lịch hay 365 ngày kể từ lần thưởng đầu tiên? Năm dương lịch (01/01 - 31/12). Xác nhận lại logic tính năm.

5.3. Counter Events & Tracking (UC-03, UC-12)

# Câu hỏi Giả định hiện tại Cần xác nhận
C1 Pattern "Accumulate & Trigger": Counter events (ViewItem) được tích lũy ở attribute-service bằng Dynamic Attribute. Khi đạt threshold thì mới trigger sang events-topic. attribute-service đảm nhận việc đếm và check threshold. attribute-service hiện có khả năng consume Kafka topic không? Hay cần build thêm consumer module?
C2 Cấu hình threshold nằm ở đâu? Đề xuất: Gắn vào metadata của Dynamic Attribute (VD: attribute weekly_view_count có field threshold: 10). Attribute-service self-contained. Hay nên để transaction-service (Rule Engine) quản lý threshold vì đó là business rule?
C3 Reset counter khi nào? Kết hợp: (1) Reset sau khi thưởng (TP bắn Counter_Reset_Command), (2) Reset theo chu kỳ bởi marketing-service Cronjob (dọn dẹp). Chu kỳ reset cụ thể là gì (weekly/monthly)? Có counter nào không cần reset (UC-12: tổng lượt redeem)?
C4 UC-03: Dữ liệu "Recently Viewed" (Recommend for You): Lưu ở đâu để query nhanh cho trang chủ? Dynamic Attribute trong PostgreSQL, có thể dùng Redis làm cache. Có yêu cầu về thời gian phản hồi (latency) cho phần Recommend? Có cần real-time hay batch update là đủ?

5.4. Luồng Giao dịch & Orchestration (UC-04, UC-06, UC-09, UC-10)

# Câu hỏi Giả định hiện tại Cần xác nhận
D1 UC-09 (Redeem): Event Processor chỉ route, transaction-service hoàn tất giao dịch (trừ điểm, chốt kho), notification-service gửi noti. Event Processor KHÔNG tham gia xử lý giao dịch. Luồng Redeem hiện tại đã hoạt động theo mô hình này chưa, hay đang đi thẳng API?
D2 UC-10 (Merchant Scan Voucher): merchant-app gọi thẳng API transaction-service, không đi qua Event Processor. Luồng đồng bộ (sync) vì cần response ngay cho Merchant. Merchant app gọi API nào hiện tại? Có cần bổ sung API mới không?
D3 UC-04 (Transfer Balance): Chuyển điểm giữa 2 tài khoản — giao dịch kép (debit + credit). transaction-service xử lý cả 2 vế trong 1 transaction ACID. Đã có API transfer chưa? Có giới hạn số điểm chuyển không?

5.5. Scheduled Jobs & Retention (UC-07, UC-08, UC-11, UC-13)

# Câu hỏi Giả định hiện tại Cần xác nhận
E1 UC-07 (Update Profile): Logic "đầy đủ thông tin" do ai check? Profile API tự check rồi emit Profile_Completed_Event. Profile API hiện có logic check completeness chưa? Danh sách field bắt buộc gồm những gì?
E2 UC-07 (Update Profile): "Trước ngày 30/09" — đây là ngày cố định hay cấu hình được? Cố định theo campaign. Nếu campaign thay đổi mốc thời gian thì cập nhật ở đâu?
E3 UC-08 (Survey): Trạng thái "đã làm khảo sát" lưu bằng Dynamic Attribute trong attribute-service. Tạo attribute survey_completed (BOOLEAN) cho entity Customer. Nếu có nhiều survey khác nhau thì sao? Cần 1 attribute per survey hay 1 attribute chung?
E4 UC-11 (Password Expiring): marketing-service quét tập khách hàng có mật khẩu sắp hết hạn. marketing-service chạy Cronjob. Thông tin "ngày hết hạn mật khẩu" nằm ở DB nào? marketing-service có quyền truy cập DB đó không? Hay cần gọi API từ IAM?
E5 UC-13 (Dormant 3 tháng): Xác định txn cuối cùng, không tính txn auto (phát sinh bởi OLS). marketing-service query bảng transaction, filter loại trừ txn auto. Txn auto có flag riêng để phân biệt không? Query có nặng không nếu bảng transaction lớn?

5.6. Notification & Push Realtime (UC-10)

# Câu hỏi Giả định hiện tại Cần xác nhận
F1 Push realtime lên Portal: Khi Merchant quét voucher, Portal khách hàng nhận thông báo ngay lập tức. Dùng Firebase Cloud Messaging (đã có sẵn). Firebase hiện push tới member-app (mobile) hay cả web portal? Cần kiểm tra scope tích hợp.

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

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 -->|"Counter 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

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

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: Counter 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

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: Counter 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_datecurrent_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

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: Counter 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à Counter 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

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

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

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ơ

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

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à

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

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-topicnotification-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

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

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-topicattribute-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

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-processorevents-topictransaction-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)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):

{
  "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):

{
  "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ý.

{
  "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 = stepupdated_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.

{
  "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.


8. Phân tích Edge Cases & Giải pháp Kỹ thuật (Mitigations)

Để đảm bảo hệ thống an toàn ở quy mô lớn, chúng ta cần xử lý triệt để các Edge Cases tiềm ẩn (đặc biệt là các lỗi liên quan đến đồng thời - Concurrency và Idempotency). Dưới đây là 2 Edge Cases nguy hiểm nhất và giải pháp thiết kế tương ứng.

8.1. Vấn đề "Threshold Jumping" (Cộng dồn vượt ngưỡng)

Ngữ cảnh: Thuộc tính weekly_view_count đang có giá trị là 9. Khách hàng sử dụng tool gửi liên tục 5 request ViewItem vào cùng 1 tích tắc. Các Kafka consumer xử lý 5 event này gần như đồng thời.

Rủi ro: Nếu không kiểm soát đồng thời (Concurrency Control), giá trị có thể được tăng lên 14. Nếu logic trigger chỉ đơn thuần là if (new_value >= threshold) (14 >= 10), hệ thống sẽ lọt vào nhánh kích hoạt (Trigger) tới 5 lần, dẫn đến việc tặng điểm 5 lần cho 1 mốc.

Giải pháp (Boundary Check & DB Row Lock):

  1. Pessimistic Locking (Khóa bi quan): Tại attribute-service, mọi hành động cập nhật customer_attribute_values đều phải sử dụng khóa dòng (Row-level lock) trong PostgreSQL (SELECT ... FOR UPDATE). Điều này buộc 5 request đồng thời phải xếp hàng chạy tuần tự.
  2. Boundary Condition (Điều kiện ranh giới): Logic trigger phải được cập nhật thành: if (old_value < threshold AND new_value >= threshold) THEN trigger()
  3. Luồng thực thi an toàn:
    • Req 1: old=9, new=10 -> TRÚNG ĐIỀU KIỆN (9 < 10 và 10 >= 10) -> Bắn Trigger -> Reset value về 0.
    • Req 2: old=0, new=1 -> TRƯỢT.
    • Các Req 3, 4, 5 tương tự sẽ làm value tăng lên 2, 3, 4 mà không bao giờ kích hoạt lại Threshold.

Rủi ro DB Contention (Đặc biệt lưu ý): Dù Kafka đã đứng làm buffer (không làm sập upstream), nhưng nếu có hàng ngàn event ViewItem cho cùng một sản phẩm HOT đẩy vào, các Kafka Consumer Threads sẽ cùng lúc thực thi SELECT ... FOR UPDATE lên đúng 1 dòng (row) của entity ITEM trong DB. Điều này gây ra Database Contention (Tắc nghẽn DB) giữa các thread xử lý, giới hạn thông lượng (throughput) và có thể gây lock timeout hoặc thread starvation.

Cách xử lý: Cần phân loại Counter:

  1. Entity-Level Counter (Customer): VD weekly_view_count, login_streak. Mức độ đồng thời trên 1 khách hàng là cực kỳ thấp -> Dùng Row-level Lock (Pessimistic Locking) là hoàn toàn phù hợp và an toàn tuyệt đối.
  2. Global/Hot Item Counter: VD item_view_count, total_redeem_count. Mức độ đồng thời trên 1 row cực kỳ cao. -> Không nên lock trực tiếp vào DB. Hãy chuyển các counter thuần tracking này lên Redis (Redis INCR) để đếm trong RAM, sau đó cấu hình 1 Scheduler (VD: chạy mỗi 10 giây) để Flush (Batch Update) giá trị từ Redis xuống PostgreSQL.

8.2. "Bóng ma" HTTP Timeout tại Merchant App (UC-10)

Ngữ cảnh: Merchant quét Voucher, App gọi Sync API POST lên transaction-service. TP xử lý trừ Voucher thành công (đã commit DB), nhưng quá trình trả Response (HTTP 200 OK) về cho App bị gián đoạn do rớt mạng (Network Timeout).

Rủi ro: Merchant App nhận lỗi Timeout, Merchant tưởng chưa thành công nên bấm quét lại. TP nhận request thứ 2, thấy Voucher đã ở trạng thái USED nên báo lỗi "Voucher đã được sử dụng". Merchant không chịu giao hàng cho khách.

Giải pháp (Idempotency Key): Thiết kế Idempotent API cho mọi endpoint đồng bộ (Sync API).

  1. Client Generation: Merchant App tự sinh ra một UUID (VD: Idempotency-Key: req-556677) và đính kèm vào Header của Request đầu tiên.
  2. TP Caching / DB Table: transaction-service lưu một bảng idempotent_requests(key, status, response_body).
    • Lần gọi 1: TP tra bảng thấy key chưa tồn tại -> Xử lý giao dịch -> Commit -> Lưu status=SUCCESS, response_body={"msg": "Thành công"} vào bảng. Do rớt mạng, App không nhận được.
    • Lần gọi 2 (Retry): App gửi nguyên lại Idempotency-Key: req-556677.
    • TP tra bảng, thấy key đã tồn tại và status=SUCCESS. Nó bỏ qua giao dịch và chỉ trả về nguyên vẹn cái response_body cũ (HTTP 200 OK).
  3. Kết quả: Merchant App nhận được phản hồi thành công như chưa từng có lỗi mạng xảy ra. Không hề có lỗi "Voucher đã sử dụng".