Platform — Luồng xử lý trong code
Trang này dành cho kỹ sư: ánh xạ kiến trúc tới file/class cụ thể trong Tux (.NET backend + workers). Đường dẫn là tương đối từ gốc repo.
Các thành phần & file
| Thành phần | File (khoảng) | Vai trò |
|---|---|---|
NotificationJobFactory | Tux.Core/Services/NotificationJobFactory.cs | Boundary tạo job duy nhất: serialize EventData → string đúng 1 lần, set StatusId, init Recipients. |
NotificationEventKey | Tux.Core/Services/NotificationEventKey.cs | Sinh EventKey tất định cho job tạo bởi worker (idempotency). |
IEntityEventNotificationMapping | Workers/Tux.Workers.EventHandler/Notifications/ | 1 domain = 1 mapping; trả về job hoặc no-match. |
EntityEventNotificationPostProcessor | Workers/Tux.Workers.EventHandler/Notifications/ | Duyệt mọi notification mapping; lo dedup + ghi job + scheduler. |
HintedNotificationMapping | nt | Đọc hint NotificationEventTypeId, tạo job id-only. |
SettingsUpdateNotificationMapping | nt | Mapping mẫu cho settings-update (event-derived). |
NotificationClient | Services/Tux.Service/Clients/NotificationClient.cs | Persist job + tạo scheduler (đường direct). |
EntityEventHistoryPostProcessor | Workers/Tux.Workers.EventHandler/... | Duyệt history registry; sở hữu RowKey tất định + persistence. |
IEntityEventHistoryMapping / ...MultiMapping | nt | Rule theo domain: khớp + dựng projection (single / một-nhiều). |
HistoryEventKey | Tux.Core/Services/... | RowKey tất định cho dòng history (mirror NotificationEventKey). |
| Pipeline gửi | Tux.Workers.Notification.{Scheduler,Generator,Distributor} | Scheduler → Generator → Distributor. |
Tên đường dẫn trên là điểm neo để
rg/grep tìm nhanh. Khi cấu trúc đổi, tra lại bằng tên class.
Luồng A — Event-derived (qua event)
Service phía phát:
// Chỉ publish event như cũ, kèm hint tại đúng nhánh cần notify
await SendAttendeeEventAsync(actionTypeId, attendee, timestamp,
notificationEventTypeId: (int)NotificationEventType.AttendeeNewProfile);
// (đã bỏ direct CreateNotificationJobAsync)Mapping phía worker (rút gọn):
public sealed class SettingsUpdateNotificationMapping : IEntityEventNotificationMapping
{
public bool TryMap(EntityEventPostProcessingContext ctx, out NotificationJobModel job)
{
// match theo event identity + (nếu cần) explicit hint
job = NotificationJobFactory.Create(
eventTypeId: NotificationEventType.SettingsUpdate,
businessUnitId: buId,
eventKey: NotificationEventKey.ForEntityEvent(...), // deterministic ⇒ idempotent
eventData: new NotificationSettingUpdateModel { /* ... */ });
return true;
}
}Hai kiểu hint (hiện tại)
EntityEventMessage.NotificationEventTypeId— field typed, dùng cho flow id-only (EventDatachỉ là entity id). Sạch, đượcHintedNotificationMappingđọc.notificationSettingUpdateTypeIdnhúng trong JSONPayload— doSettingsUpdateNotificationMapping.TryReadHinttự parse (cần đề phòng trùng numeric id).
Có kế hoạch (chưa làm) gộp (2) về field typed để chỉ còn một khái niệm hint.
Luồng B — Direct (action-derived)
var job = NotificationJobFactory.Create(
eventTypeId: NotificationEventType.AttendeeProfileUpdate,
eventKey: Guid.NewGuid().ToString(), // at-least-once cho flow không có identity ổn định
eventData: new { AttendeeId = id, SummaryChange = htmlDiff }); // context tính tại call-time
await _notificationClient.CreateNotificationJobAsync(_context, job);NotificationClient.CreateNotificationJobAsync set StatusId, resolve BusinessUnitId từ context, ghi NotificationJob + tạo NotificationScheduler.
Hợp đồng id-only quan trọng
Với flow đi qua hint, EventData luôn là entity id (số nguyên serialize thành string). Handler tương ứng trong Generator đọc lại bằng:
JsonConvert.DeserializeObject<int>((string)job.EventData)(xem HandleAccountNewRegistration_EmployeeAsync, HandleStaffProfileUpdateAsync, HandleIncidentSubmittedAsync).
⚠️ Đừng đổi shape id-only sang object
{ ... }— sẽ vỡ Generator.
Guardrail trong test
| Test | Bảo vệ điều gì |
|---|---|
NotificationDirectCreationGuardrailTests | Ghim mọi file còn gọi CreateNotificationJobAsync trực tiếp (kèm số lượng) vào allowlist có chú thích. Thêm direct call mới → test đỏ, buộc cân nhắc "đáng lẽ event-derived?". |
| Test guardrail dual-path | Tập loại event-derived (hinted) và tập loại direct phải rời nhau; trùng → fail (chống gửi 2 lần). |
Test round-trip EventData | Chốt EventData lưu xuống luôn là chuỗi parse được (chống tái phát bug (string)EventData). |
Chính sách lỗi & idempotency (đã chốt)
- Event-derived (worker): lỗi tạo job →
throwđể queue retry (an toàn vì deterministic key ⇒ idempotent). - Action-derived (direct, trong business transaction): best-effort — lỗi notification chỉ log, không làm hỏng giao dịch nghiệp vụ.
Các flow đã migrate sang event-derived
| Flow | Cơ chế | Event publish | Call-site |
|---|---|---|---|
AccountNewRegistration | hint (id-only) | Create_Account | AccountService.CreateAccountAsync (registration branch) |
StaffProfileUpdate | hint (id-only) | Update_Employee (nhánh isEdit) | EmployeeService.UpsertEmployeeAsync |
IncidentSubmitted | hint (id-only) | Submit_Incident (1750→FormInstance) | IncidentManagementService.SubmitAsync |
Finance_Account_Codes_Settings | SettingsUpdate mapping | Update_BusinessUnit_AccountCode_Setting (706) | AccountCodeService.UpdateAccountCodeAsync |
Notification_Settings | SettingsUpdate mapping | Update_BusinessUnit_Notification_Setting (707) | NotificationSettingService (3 call-sites) |
Các flow còn direct đều là action-derived (AccountProfileUpdate, AttendeeProfileUpdate, booking/quote, invoice/statement, direct-payment, subsidy, employee-absence…) — cố ý giữ direct.
Tiếp theo: các tình huống thực tế ở Case thường gặp.