Sự kiện
Legacy - không phải seam liên service thật
Các hằng kênh sự kiện trên trang này (PaymentEventChannels, UserOnboardingEventChannels) và IEventBus / RedisPubSubAdapter mà chúng được xây cho là code chết trong @nx/core - không bao giờ được khởi tạo. Seam liên service thật là Kafka + Debezium CDC - xem Kiến trúc Sự kiện. payment.order.success được thay bằng topic Kafka nx.bana.evt.payment.succeeded; onboarding commerce.initialized được thay bằng CDC nx.bana.cdc.public.Merchant. Trang này tài liệu hóa các hằng vẫn tồn tại trong source.
| Trường | Chi tiết |
|---|---|
| Tệp nguồn | packages/core/src/common/events/payment-events.ts (51 dòng), user-onboarding-events.ts (61 dòng), websocket-events.ts (95 dòng) |
| Import | import { <ClassName> } from '@nx/core'; |
| Phương thức truyền tải | Room/topic WebSocket (Signal); các hằng kênh sự kiện là legacy (xem ghi chú) |
| Tổng số lớp kênh | 2 lớp kênh sự kiện + 2 lớp tiện ích WebSocket |
Tổng quan
Các hằng @nx/core này được thiết kế cho một event bus Redis-pub/sub không còn là seam thật (xem ghi chú trên); chúng vẫn được định nghĩa để các tham chiếu legacy phân giải được. Seam liên service thật là Kafka + Debezium CDC. Ngoài ra, hai lớp tiện ích cung cấp cách đặt tên chuẩn hóa cho room và topic WebSocket mà Signal service sử dụng.
Tất cả tên kênh sự kiện tuân theo quy ước đặt tên nhất quán:
<domain>.<entity>.<action>Ví dụ: payment.order.success, user.seller.registered, commerce.initialized
1. Tóm tắt Kênh Sự kiện
| Kênh | Giá trị | Bên publish | Bên consume |
|---|---|---|---|
PaymentEventChannels.PAYMENT_SUCCESS | payment.order.success | Sale service | Finance service, Inventory service |
UserOnboardingEventChannels.SELLER_REGISTERED | user.seller.registered | Identity service | Commerce service |
UserOnboardingEventChannels.COMMERCE_INITIALIZED | commerce.initialized | Commerce service | Finance service, Inventory service |
2. PaymentEventChannels
Nguồn: packages/core/src/common/events/payment-events.ts
Kênh
| Tên | Giá trị | Mô tả |
|---|---|---|
PAYMENT_SUCCESS | 'payment.order.success' | Thanh toán cho đơn hàng hoàn tất thành công (trạng thái PARTIAL hoặc COMPLETED) |
- Publish bởi: Sale service (qua
SalePaymentEventHandlerService) - Consume bởi: Finance service (ghi nhận giao dịch thu nhập), Inventory service (trừ tồn kho)
Payload: TPaymentSuccessEvent
export type TPaymentSuccessEvent = {
saleOrderId: string;
saleOrderNumber: string;
saleOrderStatus: string;
merchantId: string;
saleChannelId: string;
payment: {
total: number;
paid: number;
currency: string;
isFullyPaid: boolean;
paidAt: string; // ISO timestamp
financeWalletId?: string; // Optional: target wallet for finance recording
};
items: Array<{
id: string;
itemType: string;
itemId: string;
quantity: number;
mode: string;
}>;
createdBy: string;
modifiedBy: string;
};| Trường | Kiểu | Mô tả |
|---|---|---|
saleOrderId | string | Snowflake ID của đơn hàng |
saleOrderNumber | string | Số đơn hàng dễ đọc |
saleOrderStatus | string | Trạng thái đơn hàng hiện tại sau thanh toán |
merchantId | string | ID Merchant (cần cho thao tác finance và inventory) |
saleChannelId | string | Kênh bán hàng nơi đơn hàng được đặt |
payment.total | number | Tổng số tiền đơn hàng |
payment.paid | number | Số tiền đã thanh toán trong lần thanh toán này |
payment.currency | string | Mã loại tiền thanh toán |
payment.isFullyPaid | boolean | Đơn hàng đã được thanh toán đầy đủ hay chưa |
payment.paidAt | string | Dấu thời gian ISO của hoàn tất thanh toán |
payment.financeWalletId | string? | Ví tài chính đích cho ghi nhận thu nhập |
items | Array | Các mục đơn hàng với thông tin số lượng và loại |
createdBy | string | Người dùng đã tạo đơn hàng |
modifiedBy | string | Người dùng đã sửa đổi đơn hàng lần cuối |
Luồng Thanh toán
Cách sử dụng
import { PaymentEventChannels } from '@nx/core';
import type { TPaymentSuccessEvent } from '@nx/core';
// Publisher (Sale Service)
await eventBus.publish(PaymentEventChannels.PAYMENT_SUCCESS, {
saleOrderId: order.id,
saleOrderNumber: order.number,
saleOrderStatus: order.status,
merchantId: order.merchantId,
saleChannelId: order.saleChannelId,
payment: {
total: order.total,
paid: attemptAmount,
currency: 'VND',
isFullyPaid: true,
paidAt: new Date().toISOString(),
},
items: order.items.map(item => ({
id: item.id,
itemType: item.itemType,
itemId: item.itemId,
quantity: item.quantity,
mode: item.mode,
})),
createdBy: order.createdBy,
modifiedBy: order.modifiedBy,
} satisfies TPaymentSuccessEvent);
// Consumer (Finance Service)
eventBus.subscribe(PaymentEventChannels.PAYMENT_SUCCESS, async (data: TPaymentSuccessEvent) => {
await financeWorkerService.handlePaymentSuccess(data);
});3. UserOnboardingEventChannels
Nguồn: packages/core/src/common/events/user-onboarding-events.ts
Kênh
| Tên | Giá trị | Mô tả |
|---|---|---|
SELLER_REGISTERED | 'user.seller.registered' | Người bán hoàn tất đăng ký |
COMMERCE_INITIALIZED | 'commerce.initialized' | Các thực thể Commerce (Organizer, Merchant, SaleChannel) được tạo |
Payload: TSellerRegisteredEvent
export type TSellerRegisteredEvent = {
userId: string;
organizer: {
name: { en: string; vi: string };
slug: string;
};
merchant: {
name: { en: string; vi: string };
slug: string;
currency: string;
type: string;
};
saleChannel: {
name: { en: string; vi: string };
slug: string;
};
};| Trường | Kiểu | Mô tả |
|---|---|---|
userId | string | ID của người dùng mới đăng ký |
organizer.name | { en, vi } | Tên hiển thị organizer (song ngữ) |
organizer.slug | string | Định danh organizer thân thiện URL |
merchant.name | { en, vi } | Tên hiển thị merchant (song ngữ) |
merchant.slug | string | Định danh merchant thân thiện URL |
merchant.currency | string | Mã loại tiền mặc định (ví dụ: VND) |
merchant.type | string | Loại merchant (xem hằng số MerchantTypes) |
saleChannel.name | { en, vi } | Tên hiển thị kênh bán hàng (song ngữ) |
saleChannel.slug | string | Định danh kênh bán hàng thân thiện URL |
- Publish bởi: Identity service (sau khi người dùng đăng ký)
- Consume bởi: Commerce service (tạo các thực thể Organizer, Merchant, SaleChannel)
Payload: TCommerceInitializedEvent
export type TCommerceInitializedEvent = {
userId: string;
organizerId: string;
merchantId: string;
saleChannelId: string;
};| Trường | Kiểu | Mô tả |
|---|---|---|
userId | string | ID của người dùng đã kích hoạt quá trình onboarding |
organizerId | string | ID của Organizer mới tạo |
merchantId | string | ID của Merchant mới tạo |
saleChannelId | string | ID của SaleChannel mới tạo |
- Publish bởi: Commerce service (sau khi tạo các thực thể)
- Consume bởi: Finance service (tạo ví Cash mặc định), Inventory service (khởi tạo theo dõi tồn kho)
Luồng Onboarding
Cách sử dụng
import { UserOnboardingEventChannels } from '@nx/core';
import type { TSellerRegisteredEvent, TCommerceInitializedEvent } from '@nx/core';
// Publisher (Identity Service)
await eventBus.publish(UserOnboardingEventChannels.SELLER_REGISTERED, {
userId: newUser.id,
organizer: { name: { en: 'My Store', vi: 'Cua Hang' }, slug: 'my-store' },
merchant: { name: { en: 'My Store', vi: 'Cua Hang' }, slug: 'my-store', currency: 'VND', type: '000_DEFAULT' },
saleChannel: { name: { en: 'Default', vi: 'Mac dinh' }, slug: 'default' },
} satisfies TSellerRegisteredEvent);
// Consumer (Commerce Service)
eventBus.subscribe(
UserOnboardingEventChannels.SELLER_REGISTERED,
async (data: TSellerRegisteredEvent) => {
await commerceOnboardingService.initializeForSeller(data);
},
);
// Publisher (Commerce Service)
await eventBus.publish(UserOnboardingEventChannels.COMMERCE_INITIALIZED, {
userId,
organizerId: organizer.id,
merchantId: merchant.id,
saleChannelId: saleChannel.id,
} satisfies TCommerceInitializedEvent);
// Consumer (Finance Service)
eventBus.subscribe(
UserOnboardingEventChannels.COMMERCE_INITIALIZED,
async (data: TCommerceInitializedEvent) => {
await financeWorkerService.handleCommerceInitialized(data);
},
);4. WebSocketRooms
Nguồn: packages/core/src/common/events/websocket-events.ts
Lớp tiện ích để dựng định danh room WebSocket chuẩn hóa mà Signal service sử dụng.
Hằng số
| Tên | Giá trị | Mô tả |
|---|---|---|
ROOM_WR_PREFIX | 'wr' | Tiền tố cho tất cả định danh room |
ROOM_OBSERVATION_PREFIX | 'observation' | Tiền tố phạm vi mặc định cho room quan sát |
Định dạng Room
wr:{prefix}/{path1}/{path2}/...Tiền tố mặc định là observation. Có thể cung cấp tiền tố tùy chỉnh cho các room không phải quan sát.
Phương thức: build()
static build(opts: { prefix?: string; paths: Array<string | number> }): string| Tham số | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
prefix | string? | 'observation' | Tiền tố phạm vi room |
paths | Array<string | number> | -- | Các phân đoạn đường dẫn theo thứ tự được nối sau tiền tố |
Trả về: Chuỗi room đã định dạng: wr:{prefix}/{paths joined by /}
Ném lỗi: HttpError (400) nếu không có phân đoạn đường dẫn hợp lệ nào được cung cấp.
Các giá trị falsy trong mảng paths được tự động lọc bỏ.
Ví dụ
import { WebSocketRooms } from '@nx/core';
// Default prefix (observation)
WebSocketRooms.build({ paths: ['merchants', 'abc-123'] });
// => 'wr:observation/merchants/abc-123'
WebSocketRooms.build({ paths: ['merchants', 'abc-123', 'sale-orders'] });
// => 'wr:observation/merchants/abc-123/sale-orders'
// Custom prefix
WebSocketRooms.build({ prefix: 'control', paths: ['sessions', 'sess-456'] });
// => 'wr:control/sessions/sess-456'
// Falsy values are filtered
WebSocketRooms.build({ paths: ['merchants', '', 'orders'] });
// => 'wr:observation/merchants/orders'5. WebSocketTopics
Nguồn: packages/core/src/common/events/websocket-events.ts
Lớp tiện ích để dựng định danh topic WebSocket chuẩn hóa. Topic phân loại tin nhắn WebSocket để client có thể đăng ký theo dõi luồng sự kiện cụ thể.
Hằng số
| Tên | Giá trị | Mô tả |
|---|---|---|
TOPIC_WS_PREFIX | 'ws' | Tiền tố cho tất cả định danh topic |
Định dạng Topic
ws:{path1}.{path2}.{path3}Phương thức: build()
static build(opts: { paths: Array<string | number> }): string| Tham số | Kiểu | Mô tả |
|---|---|---|
paths | Array<string | number> | Các phân đoạn đường dẫn theo thứ tự được nối bằng . sau tiền tố ws: |
Trả về: Chuỗi topic đã định dạng: ws:{paths joined by .}
Ném lỗi: HttpError (400) nếu không có phân đoạn đường dẫn hợp lệ nào được cung cấp.
Các giá trị falsy trong mảng paths được tự động lọc bỏ.
Ví dụ
import { WebSocketTopics } from '@nx/core';
WebSocketTopics.build({ paths: ['observation', 'sale', 'sale-order'] });
// => 'ws:observation.sale.sale-order'
WebSocketTopics.build({ paths: ['observation', 'payment', 'transaction'] });
// => 'ws:observation.payment.transaction'
WebSocketTopics.build({ paths: ['notification', 'merchant', 'abc-123'] });
// => 'ws:notification.merchant.abc-123'6. Kiến trúc Tổng hợp
Luồng Sự kiện Xuyên Service
Sự kiện so với Queue
Sự kiện (Redis pub/sub) và queue (BullMQ) phục vụ các mục đích khác nhau trong BANA:
| Khía cạnh | Sự kiện (pub/sub) | Queue (BullMQ) |
|---|---|---|
| Phân phối | Phát quảng bá fire-and-forget | Phân phối đảm bảo, có retry |
| Bên consume | Nhiều subscriber | Một consumer cho mỗi phân vùng |
| Bền vững | Không bền vững | Được lưu trong Redis |
| Trường hợp sử dụng | Thông báo, cập nhật thời gian thực | Xử lý công việc quan trọng |
| Định nghĩa trong | common/events/ | common/queues/ |
TIP
Sự kiện và queue thường hoạt động cùng nhau. Một trình xử lý sự kiện có thể đẩy công việc vào queue để xử lý đáng tin cậy. Ví dụ, sự kiện PAYMENT_SUCCESS được Sale service nhận, sau đó đẩy công việc vào queue qua SaleQueueDefinitions cho cả phần xử lý Finance và Inventory.
Thực hành Tốt nhất
1. Luôn Sử dụng Hằng số Kênh
// Good -- type-safe and refactorable
eventBus.subscribe(PaymentEventChannels.PAYMENT_SUCCESS, handler);
// Bad -- magic string
eventBus.subscribe('payment.order.success', handler);2. Khai báo Kiểu cho Payload Sự kiện
// Good -- typed payload
eventBus.subscribe(
PaymentEventChannels.PAYMENT_SUCCESS,
async (data: TPaymentSuccessEvent) => {
// TypeScript knows the shape of data
const { merchantId, payment } = data;
},
);
// Bad -- untyped
eventBus.subscribe(PaymentEventChannels.PAYMENT_SUCCESS, async (data: any) => { });3. Sử dụng Các Trợ giúp WebSocket cho Đặt tên Nhất quán
// Good -- standardized room/topic naming
const room = WebSocketRooms.build({ paths: ['merchants', merchantId, 'sale-orders'] });
const topic = WebSocketTopics.build({ paths: ['observation', 'sale', 'sale-order'] });
// Bad -- manual string construction
const room = `wr:observation/merchants/${merchantId}/sale-orders`;
const topic = `ws:observation.sale.sale-order`;