Skip to content

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ườngChi tiết
Tệp nguồnpackages/core/src/common/events/payment-events.ts (51 dòng), user-onboarding-events.ts (61 dòng), websocket-events.ts (95 dòng)
Importimport { <ClassName> } from '@nx/core';
Phương thức truyền tảiRoom/topic WebSocket (Signal); các hằng kênh sự kiện là legacy (xem ghi chú)
Tổng số lớp kênh2 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ênhGiá trịBên publishBên consume
PaymentEventChannels.PAYMENT_SUCCESSpayment.order.successSale serviceFinance service, Inventory service
UserOnboardingEventChannels.SELLER_REGISTEREDuser.seller.registeredIdentity serviceCommerce service
UserOnboardingEventChannels.COMMERCE_INITIALIZEDcommerce.initializedCommerce serviceFinance service, Inventory service

2. PaymentEventChannels

Nguồn: packages/core/src/common/events/payment-events.ts

Kênh

TênGiá 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

typescript
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ườngKiểuMô tả
saleOrderIdstringSnowflake ID của đơn hàng
saleOrderNumberstringSố đơn hàng dễ đọc
saleOrderStatusstringTrạng thái đơn hàng hiện tại sau thanh toán
merchantIdstringID Merchant (cần cho thao tác finance và inventory)
saleChannelIdstringKênh bán hàng nơi đơn hàng được đặt
payment.totalnumberTổng số tiền đơn hàng
payment.paidnumberSố tiền đã thanh toán trong lần thanh toán này
payment.currencystringMã loại tiền thanh toán
payment.isFullyPaidbooleanĐơn hàng đã được thanh toán đầy đủ hay chưa
payment.paidAtstringDấu thời gian ISO của hoàn tất thanh toán
payment.financeWalletIdstring?Ví tài chính đích cho ghi nhận thu nhập
itemsArrayCác mục đơn hàng với thông tin số lượng và loại
createdBystringNgười dùng đã tạo đơn hàng
modifiedBystringNgười dùng đã sửa đổi đơn hàng lần cuối

Luồng Thanh toán

Cách sử dụng

typescript
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ênGiá 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

typescript
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ườngKiểuMô tả
userIdstringID của người dùng mới đăng ký
organizer.name{ en, vi }Tên hiển thị organizer (song ngữ)
organizer.slugstringĐịnh danh organizer thân thiện URL
merchant.name{ en, vi }Tên hiển thị merchant (song ngữ)
merchant.slugstringĐịnh danh merchant thân thiện URL
merchant.currencystringMã loại tiền mặc định (ví dụ: VND)
merchant.typestringLoạ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.slugstringĐị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

typescript
export type TCommerceInitializedEvent = {
  userId: string;
  organizerId: string;
  merchantId: string;
  saleChannelId: string;
};
TrườngKiểuMô tả
userIdstringID của người dùng đã kích hoạt quá trình onboarding
organizerIdstringID của Organizer mới tạo
merchantIdstringID của Merchant mới tạo
saleChannelIdstringID 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

typescript
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ênGiá 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()

typescript
static build(opts: { prefix?: string; paths: Array<string | number> }): string
Tham sốKiểuMặc địnhMô tả
prefixstring?'observation'Tiền tố phạm vi room
pathsArray<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ụ

typescript
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ênGiá 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()

typescript
static build(opts: { paths: Array<string | number> }): string
Tham sốKiểuMô tả
pathsArray<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ụ

typescript
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ạnhSự kiện (pub/sub)Queue (BullMQ)
Phân phốiPhát quảng bá fire-and-forgetPhân phối đảm bảo, có retry
Bên consumeNhiều subscriberMột consumer cho mỗi phân vùng
Bền vữngKhông bền vữngĐược lưu trong Redis
Trường hợp sử dụngThông báo, cập nhật thời gian thựcXử lý công việc quan trọng
Định nghĩa trongcommon/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

typescript
// 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

typescript
// 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

typescript
// 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`;

Tài liệu Liên quan

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.