Sự kiện
| 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 | Redis pub/sub (sự kiện), WebSocket (phòng và 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
BANA sử dụng Redis pub/sub cho giao tiếp sự kiện liên dịch vụ. Tất cả kênh sự kiện được định nghĩa trong @nx/core để cả nhà xuất bản và nhà tiêu thụ tham chiếu cùng hằng số. Ngoài ra, hai lớp tiện ích cung cấp đặt tên chuẩn hóa cho phòng và chủ đề WebSocket được dịch vụ Signal 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ị | Nhà xuất bản | Nhà tiêu thụ |
|---|---|---|---|
PaymentEventChannels.PAYMENT_SUCCESS | payment.order.success | Dịch vụ Sale | Dịch vụ Finance, Dịch vụ Inventory |
UserOnboardingEventChannels.SELLER_REGISTERED | user.seller.registered | Dịch vụ Identity | Dịch vụ Commerce |
UserOnboardingEventChannels.COMMERCE_INITIALIZED | commerce.initialized | Dịch vụ Commerce | Dịch vụ Finance, Dịch vụ Inventory |
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) |
- Xuất bản bởi: Dịch vụ Sale (qua
SalePaymentEventHandlerService) - Tiêu thụ bởi: Dịch vụ Finance (ghi nhận giao dịch thu nhập), Dịch vụ Inventory (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 |
- Xuất bản bởi: Dịch vụ Identity (sau khi người dùng đăng ký)
- Tiêu thụ bởi: Dịch vụ Commerce (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 |
- Xuất bản bởi: Dịch vụ Commerce (sau khi tạo các thực thể)
- Tiêu thụ bởi: Dịch vụ Finance (tạo ví Cash mặc định), Dịch vụ Inventory (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 để xây dựng định danh phòng WebSocket chuẩn hóa được dịch vụ Signal 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 phòng |
ROOM_OBSERVATION_PREFIX | 'observation' | Tiền tố phạm vi mặc định cho phòng quan sát |
Định dạng Phòng
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 phòng 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 phòng |
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 phòng đã đị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 để xây dựng định danh chủ đề WebSocket chuẩn hóa. Chủ đề 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 chủ đề |
Định dạng Chủ đề
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 chủ đề đã đị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 Liên Dịch vụ
Sự kiện so với Hàng đợi
Sự kiện (Redis pub/sub) và hàng đợi (BullMQ) phục vụ các mục đích khác nhau trong BANA:
| Khía cạnh | Sự kiện (pub/sub) | Hàng đợi (BullMQ) |
|---|---|---|
| Phân phối | Phát quảng bá fire-and-forget | Phân phối đảm bảo với thử lại |
| Nhà tiêu thụ | Nhiều subscriber | Một consumer 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à hàng đợi thường hoạt động cùng nhau. Một trình xử lý sự kiện có thể đưa công việc vào hàng đợi để xử lý đáng tin cậy. Ví dụ, sự kiện PAYMENT_SUCCESS được nhận bởi Dịch vụ Sale, sau đó đưa công việc vào hàng đợi qua SaleQueueDefinitions cho cả 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`;