Communication — Khái niệm cốt lõi
Các artifact (vật thể) và "grain"
"Grain" = mức độ chi tiết của một bản ghi (một bản ghi đại diện cho cái gì).
| Artifact | Kho | Grain | Vai trò |
|---|---|---|---|
| NotificationJob | Azure Table | 1 sự kiện thông báo | Đầu vào: "có việc cần gửi". |
| NotificationScheduler | SQL | 1 việc chờ xử lý | Hàng đợi báo Scheduler đến lấy việc. |
| MessagingJob + Blob | SQL + Blob | 1 payload lớn | Điều phối campaign/report (nội dung lớn để ở Blob). |
| UserMessage | Azure Table | 1 người nhận | Tin in-app; hộp thư trong app. |
| OutgoingMessage | Azure Table | 1 người nhận × 1 kênh | Tin gửi ra qua provider; mang trạng thái gửi. |
| SmsUsage | Azure SQL | 1 SMS provider chấp nhận | Ledger tính phí SMS theo BusinessUnit (billing). |
Pipeline gửi: ba bước
| Bước | Làm gì |
|---|---|
| Scheduler | Quét việc Ready; đánh giá Notification Settings (loại này có bật, gửi cho ai). |
| Generator | Render template + resolve người nhận; sinh UserMessage (in-app) và/hoặc OutgoingMessage (kênh ngoài). |
| Distributor | Gửi thật qua SendGrid/Twilio; "claim trước khi gửi". |
| Webhook & Tracker | Cập nhật trạng thái (delivered/bounce/opened) về OutgoingMessage. |
Bốn nguyên tắc xương sống
Mọi quyết định trong Communication suy ra từ 4 nguyên tắc (chia sẻ với nền automation):
Khóa định danh thay vì khóa ngẫu nhiên — mỗi sự việc có đúng một khóa tất định; khóa hạ nguồn (OutgoingMessage…) dẫn xuất từ đó. Event lặp/retry sinh cùng khóa → không trùng.
Ensure*thay vìCreate*— thao tác tạo artifact là "đảm bảo tồn tại": đã có và tương thích → thành công luôn (idempotent); lệch nội dung (InputHashkhác) → xung đột, dừng gửi, báo support. Không bao giờUpsert Replace— tránh gửi lại vô tình.Claim trước khi gửi — Distributor phải chuyển
OutgoingMessagesangSendingbằng cập nhật có điều kiện trước khi gọi provider. Tin trùng thấy dòng đãSending/Sentthì thoát — không email đúp.Reconciliation có biên thay vì outbox — không xây outbox phức tạp; mỗi provider tự khai một đường quét-sửa có giới hạn (watermark, cửa sổ ngày, batch) tần suất thấp để vá sự cố hiếm.
⚠️ Ngoại lệ no-replace:
EntityHistory(audit) được phép upsert InsertOrReplace theo khóa tất định —RowKeykhông được tham chiếu ngoài, ghi đè khi retry vô hại.NotificationJobgiữ query-before-append.
Template
Nội dung tin là template có placeholder {{...}} (vd {{INCIDENT_DETAILS_LINK}}); Generator điền dữ liệu thật lúc render. Template có key + version (nhóm "immutable routing fields" — không đổi giữa chừng một lần gửi).
Trường immutable vs mutable
Khi Ensure* so khớp một artifact đã tồn tại, nó phân biệt:
- Immutable (định danh tin): source coordinates, event type, recipient identity, delivery method, template key/version, run key, automation coordinates. Lệch → xung đột, dừng gửi.
- Mutable (có thể đổi an toàn): retry counters, scheduler coordinates, provider status, webhook timestamps, diagnostics có giới hạn.
Campaign delivery: idempotency theo recipient × channel
- Mỗi campaign
OutgoingMessagecó khóa tất định từ (Campaign, recipient ổn định, đích đã chuẩn hóa, delivery method) — retry resolve về đúng một artifact; khóa mới không đè dòng khóa ngẫu nhiên cũ (vẫn đọc được). - Email và SMS của một người nhận là hai artifact độc lập, tính đầy đủ đánh giá theo từng channel — kênh có sẵn không cho kênh thiếu bị bỏ qua; kênh thành công không bị tạo lại. Retry chỉ tạo phần còn thiếu.
- Ensure không ghi đè provider-progress (claim, provider id, timestamps, trạng thái terminal); retry xung đột immutable → fail closed (dừng gửi tự động) + diagnostic có giới hạn.
- Dòng đã persist nhưng chưa vào queue → generation retry sửa lại (publish/restore đúng một work item) trong partition campaign; dòng đã tới provider không publish thêm.
In-app là kênh campaign độc lập
- Campaign hỗ trợ kênh In-app tùy chọn, tạo
UserMessageđộc lập vớiOutgoingMessage(Email/SMS); payload legacy thiếu field → In-app tắt; tắt In-app không tắt kênh provider. UserMessagechỉ tạo khi target resolve đúng một owner hợp lệ (Account/Employee/EnterpriseUser); không có portal identity hoặc target mơ hồ →SkippedNotEligible(skipped, không phải system failure), không ghi nhiều inbox.- Campaign không tạo/sửa Account/Employee/login để "biến" target thành hợp lệ; không dựng legacy
Customer_{UserId}cho delivery mới.
Ví dụ thực tế: Campaign
Holiday Notice: generation lỗi sau khi tạo Email, SMS chưa tạo. → Retry. → Chỉ SMS được tạo thêm; Email giữ provider-progress, phụ huynh không nhận email lần hai.
Campaign detail drawer: diagnostics theo từng phần
- Drawer campaign đã gửi hiển thị diagnostic độc lập theo từng phần: recipients, email content, attachments — một phần lỗi không che trạng thái phần khác.
- Mỗi diagnostic gồm status, reason (human-readable), error code (tùy chọn), failure timestamp (khi biết); metadata thiếu → bỏ/null, không bịa giá trị.
- Portal ưu tiên diagnostic có cấu trúc; chỉ fallback về generic error (
Failed to load recipient list) khi không có metadata — cho phép backend/frontend deploy lệch nhịp.
Ví dụ thực tế: Campaign
Holiday Notice: recipients lỗiPermission denied(403, có timestamp), attachments lỗi nhưng không code, email content tải được. → Admin mở drawer. → Recipients hiện reason/code/time; attachments hiện failed + reason; email content render bình thường.
Campaign: chọn recipient thủ công qua nhiều trang
- Trong một kết quả tìm kiếm, lựa chọn recipient thủ công giữ nguyên khi phân trang; checked state phản ánh global selection (hợp các trang đã chọn).
- Thao tác chỉ đổi selection trang hiện tại: Select All / Deselect All ở header chỉ áp dụng cho trang hiện tại; bỏ chọn một dòng không ảnh hưởng trang khác.
- Một quy tắc row identity duy nhất cho render/so khớp/lưu selection; mỗi selected key đúng một recipient record (không undefined); thiếu attendee id → fallback contact/account/employee id.
- Khi Add:
SelectedRowKeys+SelectedRecipientschứa hợp đa trang; count = kích thước hợp đó; mở lại draft giữ nguyên selection off-page. - Search mới reset selection + về trang 1; đổi phân trang trong kết quả hiện tại không reset.
Ví dụ thực tế: Tìm phụ huynh ra 3 trang, chọn vài người ở 1–2 rồi Select All ở 3. → Add. → Chỉ trang 3 thêm qua Select All; selection 1–2 giữ nguyên; count = hợp cả 3 trang.
Hiểu rồi? Xem cách chúng ráp thành hệ thống ở Kiến trúc.