25 KiB
Module Inquiries - Khám Phá Toàn Diện
GoodGo Platform Backend API
Vị Trí Module: apps/api/src/modules/inquiries/
Tổng Số Dòng Code: 1.212 dòng
Ngày Khám Phá: 11 tháng 4 năm 2026
📋 MỤC LỤC
- Cấu Trúc Thư Mục
- Danh Sách File Đầy Đủ
- Kiến Trúc Module
- Các Class & Handler Chính
- Phân Tích Tầng DDD
- Tóm Tắt File Test
📁 CẤU TRÚC THƯ MỤC
apps/api/src/modules/inquiries/
├── application/
│ ├── __tests__/
│ │ ├── create-inquiry.handler.spec.ts
│ │ ├── get-inquiries-by-agent.handler.spec.ts
│ │ ├── get-inquiries-by-listing.handler.spec.ts
│ │ └── mark-inquiry-read.handler.spec.ts
│ ├── commands/
│ │ ├── create-inquiry/
│ │ │ ├── create-inquiry.command.ts
│ │ │ └── create-inquiry.handler.ts
│ │ └── mark-inquiry-read/
│ │ ├── mark-inquiry-read.command.ts
│ │ └── mark-inquiry-read.handler.ts
│ └── queries/
│ ├── get-inquiries-by-agent/
│ │ ├── get-inquiries-by-agent.handler.ts
│ │ └── get-inquiries-by-agent.query.ts
│ └── get-inquiries-by-listing/
│ ├── get-inquiries-by-listing.handler.ts
│ └── get-inquiries-by-listing.query.ts
├── domain/
│ ├── __tests__/
│ │ └── inquiry-domain.spec.ts
│ ├── entities/
│ │ └── inquiry.entity.ts
│ ├── events/
│ │ ├── inquiry-created.event.ts
│ │ └── inquiry-read.event.ts
│ └── repositories/
│ ├── inquiry.repository.ts
│ └── inquiry-read.dto.ts
├── infrastructure/
│ └── repositories/
│ └── prisma-inquiry.repository.ts
├── presentation/
│ ├── __tests__/
│ │ └── inquiries.controller.spec.ts
│ ├── controllers/
│ │ └── inquiries.controller.ts
│ └── dto/
│ ├── create-inquiry.dto.ts
│ └── list-inquiries.dto.ts
├── index.ts
└── inquiries.module.ts
📄 DANH SÁCH FILE ĐẦY ĐỦ
TẦNG ỨNG DỤNG (APPLICATION LAYER) (8 file)
Lệnh (Commands) (4 file)
| Đường Dẫn File | Loại | Mô Tả |
|---|---|---|
application/commands/create-inquiry/create-inquiry.command.ts |
Command | DTO để tạo yêu cầu - chứa userId, listingId, message, phone |
application/commands/create-inquiry/create-inquiry.handler.ts |
Command Handler | Thực thi CreateInquiryCommand; xác thực listing tồn tại, tạo entity, phát sự kiện |
application/commands/mark-inquiry-read/mark-inquiry-read.command.ts |
Command | DTO để đánh dấu yêu cầu đã đọc - chứa inquiryId, agentUserId |
application/commands/mark-inquiry-read/mark-inquiry-read.handler.ts |
Command Handler | Thực thi MarkInquiryReadCommand; xác thực quyền, đánh dấu yêu cầu đã đọc, phát sự kiện |
Truy Vấn (Queries) (4 file)
| Đường Dẫn File | Loại | Mô Tả |
|---|---|---|
application/queries/get-inquiries-by-agent/get-inquiries-by-agent.query.ts |
Query | DTO để liệt kê yêu cầu của đại lý - chứa agentUserId, page, limit |
application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts |
Query Handler | Phân giải đại lý theo userId, ủy quyền cho repository findByAgent |
application/queries/get-inquiries-by-listing/get-inquiries-by-listing.query.ts |
Query | DTO để liệt kê yêu cầu theo listing - chứa listingId, page, limit |
application/queries/get-inquiries-by-listing/get-inquiries-by-listing.handler.ts |
Query Handler | Ủy quyền trực tiếp cho repository findByListing |
Test Tầng Ứng Dụng (4 file)
| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|---|---|---|---|
application/__tests__/create-inquiry.handler.spec.ts |
Jest | 4 test | Happy path, listing không tồn tại, phát sự kiện domain |
application/__tests__/mark-inquiry-read.handler.spec.ts |
Jest | 5 test | Happy path, yêu cầu không tồn tại, listing không tồn tại, truy cập bị cấm, đại lý không tìm thấy |
application/__tests__/get-inquiries-by-listing.handler.spec.ts |
Jest | 2 test | Kết quả phân trang, kết quả rỗng |
application/__tests__/get-inquiries-by-agent.handler.spec.ts |
Jest | 2 test | Kết quả phân trang, đại lý không tìm thấy |
TẦNG DOMAIN (6 file)
Thực Thể (Entities) (1 file)
| Đường Dẫn File | Loại | Mô Tả |
|---|---|---|
domain/entities/inquiry.entity.ts |
Aggregate Root | Thực thể domain cốt lõi với id, listingId, userId, message, phone, isRead; các phương thức: createNew(), markAsRead() |
Sự Kiện (Events) (2 file)
| Đường Dẫn File | Loại | Mô Tả |
|---|---|---|
domain/events/inquiry-created.event.ts |
Domain Event | Được phát khi yêu cầu được tạo - chứa aggregateId, listingId, userId |
domain/events/inquiry-read.event.ts |
Domain Event | Được phát khi yêu cầu được đánh dấu đã đọc - chứa aggregateId, listingId, userId |
Repository (2 file)
| Đường Dẫn File | Loại | Mô Tả |
|---|---|---|
domain/repositories/inquiry.repository.ts |
Interface + Symbol | Interface IInquiryRepository với 6 phương thức; symbol INQUIRY_REPOSITORY cho DI; interface PaginatedResult |
domain/repositories/inquiry-read.dto.ts |
Interface | Read DTO cho các truy vấn - bao gồm tiêu đề listing, thông tin người dùng, dấu thời gian |
Test Domain (1 file)
| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|---|---|---|---|
domain/__tests__/inquiry-domain.spec.ts |
Jest | 5 test | Tạo entity, phone null, sự kiện domain, markAsRead |
TẦNG CƠ SỞ HẠ TẦNG (INFRASTRUCTURE LAYER) (1 file)
Triển Khai Repository
| Đường Dẫn File | Loại | Mô Tả |
|---|---|---|
infrastructure/repositories/prisma-inquiry.repository.ts |
Service | Triển khai IInquiryRepository sử dụng Prisma; 6 phương thức: findById, save, markAsRead, findByListing, findByAgent, countUnreadByAgent |
TẦNG TRÌNH BÀY (PRESENTATION LAYER) (5 file)
Controller (1 file)
| Đường Dẫn File | Loại | Routes | Xác Thực |
|---|---|---|---|
presentation/controllers/inquiries.controller.ts |
NestJS Controller | POST /, GET /listing/:id, GET /agent/me, PATCH /:id/read | JWT + RBAC |
Các Endpoint:
POST /inquiries- Tạo yêu cầu (BUYER)GET /inquiries/listing/:listingId- Lấy yêu cầu theo listingGET /inquiries/agent/me- Lấy yêu cầu cho đại lý đang đăng nhập (chỉ AGENT)PATCH /inquiries/:id/read- Đánh dấu yêu cầu đã đọc (chỉ AGENT)
Đối Tượng Truyền Dữ Liệu - Data Transfer Objects (2 file)
| Đường Dẫn File | Loại | Thuộc Tính | Kiểm Tra Hợp Lệ |
|---|---|---|---|
presentation/dto/create-inquiry.dto.ts |
DTO | listingId, message, phone? | Chuỗi bắt buộc, message tối đa 2000 ký tự |
presentation/dto/list-inquiries.dto.ts |
DTO | page?, limit? | Số nguyên, min 1, max 100, mặc định 1/20 |
Test Tầng Trình Bày (1 file)
| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|---|---|---|---|
presentation/__tests__/inquiries.controller.spec.ts |
Jest | 6 test | Tất cả 4 endpoint, xử lý phone null, giá trị phân trang mặc định |
FILE MODULE (2 file)
| Đường Dẫn File | Loại | Mô Tả |
|---|---|---|
inquiries.module.ts |
NestJS Module | Xuất: InquiriesController; Provider: PrismaInquiryRepository, 2 command handler, 2 query handler |
index.ts |
Barrel Export | Xuất: InquiriesModule, symbol INQUIRY_REPOSITORY, interface IInquiryRepository, InquiryEntity |
🏗️ KIẾN TRÚC MODULE
Mẫu Thiết Kế: CQRS + Event Sourcing + DDD
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ Controllers + DTOs (inquiries.controller.ts) │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────┼────────────────┐
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Commands│ │ Queries │ │ Validation│
└────┬────┘ └────┬────┘ └─────────┘
│ │
└──────┬───────┘
↓
┌──────────────────────┐
│ APPLICATION LAYER │
│ Handlers + Services │
└──────────┬───────────┘
│
┌──────┴──────┐
↓ ↓
┌──────────┐ ┌──────────┐
│ Commands │ │ Queries │
│ Handlers │ │ Handlers │
└────┬─────┘ └────┬─────┘
│ │
└──────┬──────┘
↓
┌──────────────────────┐
│ DOMAIN LAYER │
│ Entities + Events │
│ Repository Interface │
└──────────┬───────────┘
│
↓
┌──────────────────────┐
│ INFRASTRUCTURE LAYER │
│ Prisma Repository │
│ Database Queries │
└──────────────────────┘
Luồng Dữ Liệu
Tạo Yêu Cầu:
POST /inquiries (CreateInquiryDto)
→ InquiriesController.createInquiry()
→ CommandBus.execute(CreateInquiryCommand)
→ CreateInquiryHandler.execute()
- Xác thực listing tồn tại (Prisma)
- Tạo InquiryEntity
- Lưu vào repository (Prisma)
- Phát InquiryCreatedEvent qua EventBus
→ Trả về { id, listingId, createdAt }
Đánh Dấu Đã Đọc:
PATCH /inquiries/:id/read (chỉ agent)
→ InquiriesController.markAsRead()
→ CommandBus.execute(MarkInquiryReadCommand)
→ MarkInquiryReadHandler.execute()
- Tìm inquiry entity
- Xác minh đại lý là chủ sở hữu listing
- Gọi entity.markAsRead()
- Cập nhật trong repository
- Phát InquiryReadEvent qua EventBus
→ Trả về { success: true }
Lấy Danh Sách Yêu Cầu:
GET /inquiries/listing/:id or /agent/me (có phân trang)
→ InquiriesController.getByListing() or getMyInquiries()
→ QueryBus.execute(GetInquiriesByListingQuery or GetInquiriesByAgentQuery)
→ Handler ủy quyền cho repository
→ Repository.findByListing() or findByAgent()
- Truy vấn Prisma với join
- Map sang InquiryReadDto
- Trả về PaginatedResult
→ Trả về dữ liệu phân trang
🔑 CÁC CLASS & HANDLER CHÍNH
Domain Entity: InquiryEntity
export class InquiryEntity extends AggregateRoot<string> {
// Thuộc tính
private _listingId: string;
private _userId: string;
private _message: string;
private _phone: string | null;
private _isRead: boolean;
// Factory Method
static createNew(id, listingId, userId, message, phone): InquiryEntity
→ Tạo yêu cầu mới với isRead=false
→ Phát InquiryCreatedEvent
// Business Logic
markAsRead(): void
→ Đặt isRead thành true
→ Phát InquiryReadEvent
}
Command Handlers
CreateInquiryHandler
class CreateInquiryHandler implements ICommandHandler<CreateInquiryCommand> {
async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> {
// 1. Xác thực listing tồn tại
const listing = await prisma.listing.findUnique(...)
if (!listing) throw NotFoundException
// 2. Tạo entity bằng factory
const inquiry = InquiryEntity.createNew(...)
// 3. Lưu vào cơ sở dữ liệu
await inquiryRepo.save(inquiry)
// 4. Phát các sự kiện domain
const events = inquiry.clearDomainEvents()
events.forEach(e => eventBus.publish(e))
return { id, listingId, createdAt }
}
}
MarkInquiryReadHandler
class MarkInquiryReadHandler implements ICommandHandler<MarkInquiryReadCommand> {
async execute(command: MarkInquiryReadCommand): Promise<void> {
// 1. Tải aggregate root
const inquiry = await inquiryRepo.findById(...)
if (!inquiry) throw NotFoundException
// 2. Xác minh phân quyền
const listing = await prisma.listing.findUnique(...)
const agent = await prisma.agent.findUnique(...)
if (agent.id !== listing.agentId) throw ForbiddenException
// 3. Cập nhật trạng thái domain
inquiry.markAsRead()
// 4. Lưu trạng thái
await inquiryRepo.markAsRead(...)
// 5. Phát sự kiện
const events = inquiry.clearDomainEvents()
events.forEach(e => eventBus.publish(e))
}
}
Query Handlers
GetInquiriesByListingHandler
class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListingQuery> {
async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> {
return this.inquiryRepo.findByListing(
query.listingId,
query.page,
query.limit
)
}
}
GetInquiriesByAgentHandler
class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByAgentQuery> {
async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> {
// 1. Phân giải agent ID từ user ID
const agent = await prisma.agent.findUnique({ where: { userId } })
if (!agent) throw NotFoundException
// 2. Ủy quyền cho repository
return this.inquiryRepo.findByAgent(agent.id, page, limit)
}
}
Triển Khai Repository
PrismaInquiryRepository
Các Phương Thức:
findById(id)- Tra cứu một yêu cầu đơn lẻsave(entity)- Tạo yêu cầumarkAsRead(id)- Cập nhật cờ isReadfindByListing(listingId, page, limit)- Tìm kiếm phân trang với joinfindByAgent(agentId, page, limit)- Tìm kiếm phân trang qua listing agentcountUnreadByAgent(agentId)- Tổng hợp số lượng chưa đọc
Quan Hệ Prisma Được Sử Dụng:
inquiry.listing→ property (để lấy tiêu đề)inquiry.user→ fullName, phonelisting.agentId→ lọc theo agent- Phân trang: skip/take với orderBy giảm dần
🎯 CẤU TRÚC TẦNG DDD
TẦNG DOMAIN (domain/)
Mục Đích: Logic nghiệp vụ thuần túy, độc lập với framework
Bao Gồm:
-
Entities -
inquiry.entity.ts(Aggregate Root)- Class TypeScript thuần túy
- Đóng gói quy tắc nghiệp vụ (cờ isRead, kiểm tra hợp lệ trường dữ liệu)
- Factory method để tạo đối tượng
- Các phương thức chuyển đổi trạng thái
- Thu thập sự kiện domain
-
Events -
inquiry-created.event.ts,inquiry-read.event.ts- Ghi lại các sự kiện nghiệp vụ quan trọng
- Dùng cho event sourcing và audit trail
- Các class dữ liệu đơn giản
-
Repositories -
inquiry.repository.ts,inquiry-read.dto.ts- Interface định nghĩa hợp đồng (đảo ngược phụ thuộc)
- Symbol dùng cho DI token
- Read DTO tách biệt với write entity
- Interface kết quả phân trang
Tính Cô Lập:
- Không có phụ thuộc NestJS
- Không có phụ thuộc cơ sở dữ liệu
- Không có phụ thuộc dịch vụ bên ngoài
TẦNG ỨNG DỤNG (application/)
Mục Đích: Các use case & điều phối
Bao Gồm:
-
Commands - Các thao tác thay đổi trạng thái
CreateInquiryCommand- Input DTOMarkInquiryReadCommand- Input DTO- Các handler điều phối các thao tác domain
-
Queries - Các thao tác đọc không thay đổi trạng thái
GetInquiriesByListingQueryGetInquiriesByAgentQuery- Các handler ủy quyền cho repository
Trách Nhiệm:
- Kiểm tra điều kiện tiên quyết (listing tồn tại, đại lý được phân quyền)
- Điều phối các thao tác entity domain
- Phát sự kiện domain
- Xử lý các mối quan tâm xuyên suốt (logging, v.v.)
Tích Hợp NestJS:
- Decorator
@CommandHandler() - Decorator
@QueryHandler() - Dependency injection qua constructor
TẦNG CƠ SỞ HẠ TẦNG (infrastructure/)
Mục Đích: Chi tiết cơ sở dữ liệu & lưu trữ
Bao Gồm:
- Repositories -
prisma-inquiry.repository.ts- Triển khai interface repository domain
- Sử dụng Prisma client cho các truy vấn
- Map bản ghi cơ sở dữ liệu ↔ domain entity
- Xử lý logic phân trang
Trách Nhiệm:
- Xây dựng truy vấn
- Map kết quả
- Tính toán phân trang
- Quan hệ join
- Tối ưu hóa đặc thù cơ sở dữ liệu
Tính Cô Lập:
- Có thể hoán đổi triển khai (có thể dùng TypeORM, MongoDB, v.v.)
- Code domain không bị ảnh hưởng bởi thay đổi cơ sở dữ liệu
TẦNG TRÌNH BÀY (presentation/)
Mục Đích: Giao diện HTTP & I/O
Bao Gồm:
-
Controllers -
inquiries.controller.ts- Decorator
@Controllercủa NestJS - Các handler route HTTP
- Điều phối đến CQRS bus
- Trả về HTTP response
- Decorator
-
DTOs -
create-inquiry.dto.ts,list-inquiries.dto.ts- Kiểm tra hợp lệ đầu vào (class-validator)
- Tài liệu Swagger (@ApiProperty)
- Tách biệt với domain entity
Trách Nhiệm:
- Xử lý route
- Kiểm tra hợp lệ yêu cầu
- Xác thực/Phân quyền
- Định dạng response
- Tài liệu API
Tích Hợp NestJS:
- Decorator:
@Post,@Get,@Patch - Guard:
JwtAuthGuard,RolesGuard - Middleware:
@CurrentUser,@Roles
📊 TÓM TẮT FILE TEST
Thống Kê Test
| Tầng | File | Số Test | Loại |
|---|---|---|---|
| Domain | domain/__tests__/inquiry-domain.spec.ts |
5 | Jest |
| Application | application/__tests__/create-inquiry.handler.spec.ts |
4 | Jest |
| Application | application/__tests__/mark-inquiry-read.handler.spec.ts |
5 | Jest |
| Application | application/__tests__/get-inquiries-by-listing.handler.spec.ts |
2 | Jest |
| Application | application/__tests__/get-inquiries-by-agent.handler.spec.ts |
2 | Jest |
| Presentation | presentation/__tests__/inquiries.controller.spec.ts |
6 | Jest |
| TỔNG CỘNG | 6 file | 24 test | Jest + Vitest |
Phạm Vi Test Theo File
Test Domain (inquiry-domain.spec.ts) - 5 test
✓ InquiryEntity.createNew() với phone
✓ InquiryEntity.createNew() với phone null
✓ createNew() phát InquiryCreatedEvent
✓ markAsRead() đặt cờ thành true
✓ markAsRead() phát InquiryReadEvent
Test CreateInquiryHandler - 4 test
✓ Tạo yêu cầu thành công
✓ Ném NotFoundException khi listing không tồn tại
✓ Phát sự kiện domain sau khi lưu
Test MarkInquiryReadHandler - 5 test
✓ Đánh dấu yêu cầu đã đọc thành công
✓ Ném NotFoundException khi không tìm thấy yêu cầu
✓ Ném NotFoundException khi không tìm thấy listing
✓ Ném ForbiddenException khi người dùng không phải đại lý
✓ Ném ForbiddenException khi không tìm thấy đại lý
Test GetInquiriesByListingHandler - 2 test
✓ Trả về kết quả phân trang
✓ Trả về dữ liệu rỗng khi không có yêu cầu
Test GetInquiriesByAgentHandler - 2 test
✓ Trả về kết quả phân trang
✓ Ném NotFoundException cho người dùng không phải đại lý
Test InquiriesController - 6 test
✓ POST tạo yêu cầu với command dispatch
✓ POST truyền phone null khi không được cung cấp
✓ GET /listing điều phối truy vấn với giá trị mặc định
✓ GET /listing truyền phân trang tùy chỉnh
✓ GET /agent/me điều phối truy vấn agent
✓ PATCH đánh dấu yêu cầu và trả về thành công
🔍 THỐNG KÊ TÓM TẮT
| Chỉ Số | Số Lượng |
|---|---|
| Tổng Số File | 25 |
| File Nguồn (.ts, không tính test) | 19 |
| File Test | 6 |
| Tổng Số Dòng Code | 1.212 |
| Commands | 2 |
| Queries | 2 |
| Command Handler | 2 |
| Query Handler | 2 |
| Domain Event | 2 |
| HTTP Endpoint | 4 |
| DTO | 2 |
| Interface | 3 (IInquiryRepository, PaginatedResult, InquiryReadDto) |
| Test Suite | 6 |
| Test Case | 24 |
🛠️ CÁC PHỤ THUỘC CHÍNH
Gói Bên Ngoài:
@nestjs/common- Framework@nestjs/cqrs- CQRS bus@paralleldrive/cuid2- Tạo ID@prisma/client- ORMclass-validator- Kiểm tra hợp lệ DTO@nestjs/swagger- Tài liệu API
Module Nội Bộ:
@modules/shared- AggregateRoot, DomainEvent, các ngoại lệ@modules/auth- JwtPayload, JwtAuthGuard, RolesGuard
🔐 Bảo Mật & Phân Quyền
Xác Thực:
- Tất cả endpoint yêu cầu
@UseGuards(JwtAuthGuard) - JWT token được trích xuất qua decorator
@CurrentUser()
Phân Quyền:
GET /agent/me→ bắt buộc@Roles('AGENT')PATCH /:id/read→ bắt buộc@Roles('AGENT')- MarkInquiryReadHandler xác minh:
- Yêu cầu tồn tại
- Listing tồn tại
- Người dùng đã đăng ký là đại lý
- Đại lý sở hữu listing
📝 HỢP ĐỒNG API
POST /inquiries
Request: { listingId, message, phone? }
Response: { id, listingId, createdAt }
Status: 201 Created | 400 Bad Request | 401 Unauthorized | 404 Not Found
GET /inquiries/listing/:listingId
Request: Query: page?, limit?
Response: PaginatedResult<InquiryReadDto>
Status: 200 OK | 401 Unauthorized
GET /inquiries/agent/me
Request: Query: page?, limit?
Response: PaginatedResult<InquiryReadDto>
Status: 200 OK | 401 Unauthorized | 403 Forbidden
PATCH /inquiries/:id/read
Request: (không có body)
Response: { success: boolean }
Status: 200 OK | 401 Unauthorized | 403 Forbidden | 404 Not Found
🎓 NHẬN XÉT VỀ KIẾN TRÚC
Điểm Mạnh
- Kiến Trúc Sạch - Phân tách mối quan tâm rõ ràng giữa các tầng
- Mẫu CQRS - Đường dẫn đọc/ghi riêng biệt cho phép mở rộng quy mô
- Domain-Driven Design - Logic nghiệp vụ trong entity, không phải mô hình thiếu máu
- Hướng Sự Kiện - Sự kiện domain cho phép audit trail và event sourcing
- Khả Năng Test - Mỗi tầng có thể test độc lập với mock
- An Toàn Kiểu - TypeScript đầy đủ với interface và kiểu chặt chẽ
- Framework DI - NestJS cung cấp dependency injection sẵn có
Quyết Định Thiết Kế
-
InquiryEntity là Aggregate Root
- Đóng gói quy tắc nghiệp vụ của yêu cầu
- Kiểm soát chuyển đổi trạng thái qua các phương thức (createNew, markAsRead)
- Thu thập sự kiện domain
-
Mẫu Repository
- Interface trong domain, triển khai trong infrastructure
- Cho phép hoán đổi nguồn dữ liệu mà không ảnh hưởng đến logic nghiệp vụ
-
Read/Write DTO Riêng Biệt
CreateInquiryDto(đầu vào) so vớiInquiryReadDto(đầu ra)- Cho phép phát triển API linh hoạt
-
CQRS Handler
- Command xử lý các thao tác thay đổi trạng thái có phân quyền
- Query xử lý các thao tác đọc có lọc
- Cả hai độc lập, có thể tối ưu hóa riêng
-
Interface Phân Trang
- Phân trang nhất quán trên tất cả các endpoint danh sách
- Mô hình Page + limit với totalPages được tính toán
Kết Thúc Báo Cáo Khám Phá