Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
588 lines
24 KiB
Markdown
588 lines
24 KiB
Markdown
# GoodGo Platform - Báo Cáo Kiểm Toán Chất Lượng Code
|
|
**Mức Độ Chi Tiết**: Rất Kỹ Lưỡng
|
|
**Ngày Kiểm Toán**: Ngày 9 tháng 4 năm 2026
|
|
**Codebase**: /Users/velikho/Desktop/WORKING/goodgo-platform-ai/
|
|
|
|
---
|
|
|
|
## 1. XỬ LÝ LỖI
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Mẫu DomainException Được Triển Khai Đúng Cách**: Phân cấp ngoại lệ tập trung tại `/modules/shared/domain/domain-exception.ts` (Dòng 13-56)
|
|
- `DomainException`, `NotFoundException`, `ValidationException`, `ConflictException`, `UnauthorizedException`, `ForbiddenException`
|
|
- Tất cả kế thừa `HttpException` với mã trạng thái phù hợp
|
|
|
|
- **Bộ Lọc Ngoại Lệ Toàn Cục**: `/modules/shared/infrastructure/filters/global-exception.filter.ts` (Dòng 1-84)
|
|
- Bắt tất cả ngoại lệ tại ranh giới ứng dụng
|
|
- Chuyển đổi sang định dạng `ErrorResponseBody` chuẩn
|
|
- Ghi log đúng cách với correlation ID
|
|
|
|
- **Mẫu Result<T, E>**: `/modules/shared/domain/result.ts` (Dòng 1-56)
|
|
- Kiểu Result hàm với các phương thức `ok()`, `err()`, `map()`, `andThen()`, `match()`
|
|
- Tốt cho xử lý lỗi ở cấp độ domain
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[NGHIÊM TRỌNG] Các entity domain ném `Error` thông thường thay vì ngoại lệ domain:**
|
|
- `payments/domain/entities/payment.entity.ts` (Dòng 94, 107, 134)
|
|
- `throw new Error('Cannot complete payment in status ${this._status}')`
|
|
- `throw new Error('Cannot fail payment in status ${this._status}')`
|
|
- `throw new Error('Chỉ có thể hoàn tiền cho thanh toán đã hoàn tất')`
|
|
|
|
- `subscriptions/domain/entities/subscription.entity.ts` (Dòng 75, 90, 104, 112)
|
|
- `throw new Error('Không thể nâng cấp subscription...')`
|
|
- `throw new Error('Subscription đã bị hủy')`
|
|
- Nhiều trường hợp tương tự
|
|
|
|
**Giải pháp**: Các entity domain KHÔNG NÊN ném ngoại lệ; thay vào đó nên trả về Result<T, E>.
|
|
|
|
**[CAO] Các service infrastructure ném Error thông thường cho việc kiểm tra biến môi trường:**
|
|
- `payments/infrastructure/services/vnpay.service.ts` (Dòng 16)
|
|
- `payments/infrastructure/services/momo.service.ts` (Dòng 16)
|
|
- `payments/infrastructure/services/zalopay.service.ts` (Dòng 16)
|
|
- `auth/infrastructure/strategies/google-oauth.strategy.ts` (Dòng 22)
|
|
- `auth/auth.module.ts` (Dòng 39)
|
|
|
|
**Giải pháp**: Sử dụng kiểm tra cấu hình khi khởi động module, không phải khi khởi tạo service.
|
|
|
|
**[TRUNG BÌNH] Một số controller ném trực tiếp thay vì thông qua DomainException:**
|
|
- `auth/presentation/controllers/oauth.controller.ts` (Dòng 74, 101)
|
|
- `throw new UnauthorizedException(...)` - Được, nhưng mẫu không nhất quán
|
|
- Nên sử dụng mẫu Result trong các handler thay thế
|
|
|
|
---
|
|
|
|
## 2. THỨ TỰ IMPORT & ALIAS ĐƯỜNG DẪN
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Cấu Hình Path Alias Đúng**: `tsconfig.base.json` bật đường dẫn `@modules/*` (tsconfig.json Dòng 14)
|
|
- **Quy Tắc Import ESLint Được Cấu Hình Tốt**: `eslint.config.mjs` (Dòng 64-71)
|
|
- Nhóm: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index']
|
|
- `import-x/no-duplicates`: được áp dụng
|
|
- Sắp xếp theo bảng chữ cái được áp dụng
|
|
|
|
- **Các Dependency Được Export Đúng Cách**: Tất cả module có barrel export `index.ts`
|
|
- `auth/index.ts` export: AuthModule, guards, decorators, kiểu JwtPayload
|
|
- `payments/index.ts` export: PaymentsModule, token repository, interface gateway
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[CAO] Các import nội bộ liên module KHÔNG tuân thủ mẫu barrel:**
|
|
|
|
158 trường hợp import nội bộ trực tiếp được tìm thấy:
|
|
|
|
1. **`@modules/auth/infrastructure/services/token.service` được import trực tiếp:**
|
|
- `payments/presentation/controllers/payments.controller.ts` (Dòng 21)
|
|
- Nên import từ `@modules/auth` (barrel) nhưng TokenService/JwtPayload được export trong index.ts
|
|
|
|
2. **Các service infrastructure được import từ nhiều module:**
|
|
- `auth/application/commands/refresh-token/refresh-token.handler.ts` (Dòng ?)
|
|
- Import `TokenService` từ `'../../../infrastructure/services/token.service'`
|
|
- `auth/application/commands/login-user/login-user.handler.ts`
|
|
- Mẫu tương tự
|
|
|
|
3. **CacheService được import trực tiếp:**
|
|
- `auth/application/queries/get-profile/get-profile.handler.ts`
|
|
- `from '@modules/shared/infrastructure/cache.service'`
|
|
- Nên sử dụng barrel `@modules/shared`
|
|
|
|
**Giải pháp**:
|
|
1. Cập nhật `auth/index.ts` để export `TokenService` (không chỉ kiểu)
|
|
2. Cập nhật `shared/index.ts` để export `CacheService`
|
|
3. Thay thế tất cả import infrastructure trực tiếp bằng import barrel
|
|
|
|
---
|
|
|
|
## 3. ĐỘ CHẶT CHẼ CỦA TYPESCRIPT
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Chế Độ Strict Được Bật**: `tsconfig.base.json` Dòng 7: `"strict": true`
|
|
- **Các Cờ Nâng Cao Được Đặt**:
|
|
- `noUncheckedIndexedAccess: true` (Dòng 15)
|
|
- `noImplicitOverride: true` (Dòng 16)
|
|
- `noPropertyAccessFromIndexSignature: true` (Dòng 17)
|
|
- `forceConsistentCasingInFileNames: true` (Dòng 10)
|
|
- `declaration: true`, `declarationMap: true`, `sourceMap: true`
|
|
|
|
- **Áp Dụng ESLint**:
|
|
- `@typescript-eslint/no-explicit-any: warn` (Dòng 57)
|
|
- `@typescript-eslint/no-unused-vars` với mẫu `^_` (Dòng 53-55)
|
|
- Áp dụng import kiểu: inline type imports (Dòng 58-60)
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[TRUNG BÌNH] Không đặt `noImplicitAny` tường minh** (mặc định là true với strict):
|
|
- Một số file có thể có kiểm tra kiểu lỏng hơn trong file test
|
|
- ESLint cho phép `any` trong file test (eslint.config.mjs Dòng 108-109)
|
|
- **Đề xuất**: Thêm `@typescript-eslint/no-explicit-any: error` cho các file không phải test
|
|
|
|
---
|
|
|
|
## 4. TRÙNG LẶP CODE
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[TRUNG BÌNH] Mẫu Logger Lặp Lại (50+ trường hợp):**
|
|
```typescript
|
|
private readonly logger = new Logger(ClassName.name);
|
|
```
|
|
Tìm thấy trong:
|
|
- `payments/application/commands/handle-callback/handle-callback.handler.ts`
|
|
- `payments/application/commands/create-payment/create-payment.handler.ts`
|
|
- `payments/application/commands/refund-payment/refund-payment.handler.ts`
|
|
- `payments/infrastructure/services/zalopay.service.ts`
|
|
- `payments/infrastructure/services/momo.service.ts`
|
|
- Và 45+ trường hợp khác
|
|
|
|
**Giải pháp**: Tạo lớp handler cơ sở hoặc factory có thể inject cho việc khởi tạo logger:
|
|
```typescript
|
|
@Injectable()
|
|
export abstract class BaseCommandHandler {
|
|
protected readonly logger = this.getLoggerService();
|
|
constructor(protected readonly loggerService: LoggerService) {}
|
|
protected getLoggerService() { return this.loggerService; }
|
|
}
|
|
```
|
|
|
|
**[TRUNG BÌNH] Mẫu Inject Prisma Service (50+ trường hợp):**
|
|
```typescript
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
```
|
|
Tất cả repository đều theo mẫu này, nhưng không có lớp repository cơ sở để giảm trùng lặp.
|
|
|
|
**Giải pháp**: Tạo lớp repository cơ sở:
|
|
```typescript
|
|
@Injectable()
|
|
export abstract class BasePrismaRepository {
|
|
constructor(protected readonly prisma: PrismaService) {}
|
|
protected buildPaginationParams(page: number, limit: number) {
|
|
return { skip: (page - 1) * limit, take: limit };
|
|
}
|
|
}
|
|
```
|
|
|
|
**[THẤP] Định dạng thông báo lỗi bị trùng lặp:**
|
|
- Nhiều service thủ công định dạng bigint thành string
|
|
- Không có utility dùng chung cho định dạng tiền tệ/số
|
|
- `admin/infrastructure/repositories/prisma-admin-query.repository.ts` Dòng 56: `Math.ceil(total / limit)`
|
|
- `listings/infrastructure/repositories/prisma-listing.repository.ts`: Logic phân trang tương tự
|
|
|
|
---
|
|
|
|
## 5. DEPENDENCY INJECTION
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Mẫu Module Đúng Cách**: Tất cả module được cấu trúc đúng với decorator `@Module()`
|
|
- **Tích Hợp CQRS**: CqrsModule được import trong tất cả module xử lý command/query
|
|
- **Đăng Ký Provider Rõ Ràng**: Các handler được đăng ký trong mảng (CommandHandlers, QueryHandlers)
|
|
- **Export Rõ Ràng**: Export module định nghĩa API công khai
|
|
|
|
**Ví dụ - `payments.module.ts` (Dòng 1-44):**
|
|
```typescript
|
|
@Module({
|
|
imports: [CqrsModule],
|
|
controllers: [PaymentsController],
|
|
providers: [
|
|
{ provide: PAYMENT_REPOSITORY, useClass: PrismaPaymentRepository },
|
|
VnpayService, MomoService, ZalopayService,
|
|
{ provide: PAYMENT_GATEWAY_FACTORY, useClass: PaymentGatewayFactory },
|
|
...CommandHandlers, ...QueryHandlers,
|
|
],
|
|
exports: [PAYMENT_REPOSITORY, PAYMENT_GATEWAY_FACTORY],
|
|
})
|
|
```
|
|
|
|
**Ví dụ - `auth.module.ts` (Dòng 33-70):**
|
|
- Cấu hình JWT với kiểm tra biến môi trường ✅
|
|
- Đăng ký Passport & JWT strategy ✅
|
|
- Provider repository và service ✅
|
|
- Export: TokenService, OAuthService, USER_REPOSITORY ✅
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[THẤP] Import module không sử dụng barrel export:**
|
|
- Bên trong module, các component import trực tiếp từ đường dẫn nội bộ (chấp nhận được, nhưng không nhất quán với quy tắc liên module)
|
|
- Ví dụ: `payments.module.ts` Dòng 3: `import { CreatePaymentHandler } from './application/commands/...'`
|
|
- Có thể đơn giản hóa bằng `import { CreatePaymentHandler } from './application'` nếu sử dụng barrel export
|
|
|
|
**[THẤP] Thiếu export SharedModule:**
|
|
- `shared.module.ts` cần export tường minh LoggerService
|
|
- Hiện tại một số test import trực tiếp: `auth/__tests__/auth.integration.spec.ts` (Dòng 18)
|
|
- `import { PrismaService } from '@modules/shared/infrastructure/prisma.service'`
|
|
- Nên là `import { PrismaService } from '@modules/shared'`
|
|
|
|
---
|
|
|
|
## 6. XỬ LÝ SỰ KIỆN (Mẫu @OnEvent)
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Mẫu Sự Kiện Được Triển Khai Đúng Cách**:
|
|
- 10 event listener được tìm thấy (tất cả trong module notifications)
|
|
- Sử dụng `@OnEvent('event.name', { async: true })`
|
|
|
|
**Ví dụ - `notifications/application/listeners/payment-completed.listener.ts` (Dòng 17-43):**
|
|
```typescript
|
|
@OnEvent('payment.completed', { async: true })
|
|
async handle(event: PaymentCompletedEvent): Promise<void> {
|
|
// Xử lý async đúng cách
|
|
const user = await this.prisma.user.findUnique({...});
|
|
await this.commandBus.execute(new SendNotificationCommand(...));
|
|
}
|
|
```
|
|
|
|
### ✅ SỰ KIỆN DOMAIN ĐƯỢC TÌM THẤY
|
|
- `payments/domain/events/payment-created.event.ts`
|
|
- `payments/domain/events/payment-completed.event.ts`
|
|
- `payments/domain/events/payment-failed.event.ts`
|
|
- Các sự kiện triển khai interface `DomainEvent`
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[TRUNG BÌNH] Không tìm thấy việc phát sự kiện trong các entity domain:**
|
|
- Các sự kiện được định nghĩa nhưng không có bằng chứng về `publishEvent()` hoặc `getUncommittedEvents()`
|
|
- Các entity không phát sự kiện khi trạng thái thay đổi
|
|
- Ví dụ: `payments/domain/entities/payment.entity.ts` (188 dòng) - không phát sự kiện
|
|
|
|
**Giải pháp**: Triển khai mẫu event sourcing:
|
|
```typescript
|
|
export class PaymentEntity extends AggregateRoot {
|
|
private events: DomainEvent[] = [];
|
|
|
|
complete(): void {
|
|
if (this._status !== PaymentStatus.PENDING) throw new Error(...);
|
|
this._status = PaymentStatus.COMPLETED;
|
|
this.events.push(new PaymentCompletedEvent(...));
|
|
}
|
|
|
|
getUncommittedEvents(): DomainEvent[] { return this.events; }
|
|
}
|
|
```
|
|
|
|
**[TRUNG BÌNH] Chỉ có 10 event listener cho toàn bộ nền tảng:**
|
|
- 2 sự kiện module Listings (theo dõi sử dụng khi tạo listing)
|
|
- 2 sự kiện module Payments
|
|
- 8 listener module Notifications
|
|
|
|
**Cải tiến được kỳ vọng:**
|
|
- Sự kiện Auth: user.registered, user.verified, user.banned
|
|
- Sự kiện Listings: listing.created, listing.approved, listing.rejected, listing.expired
|
|
- Sự kiện Subscriptions: subscription.expired, subscription.upgraded
|
|
- Hiện tại chỉ có notifications phản hồi với sự kiện
|
|
|
|
---
|
|
|
|
## 7. KIỂM TRA DỮ LIỆU ĐẦU VÀO
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Mẫu DTO với class-validator**: Tất cả DTO trình bày sử dụng decorator
|
|
- **Validation Pipe Toàn Cục**: `main.ts` (Dòng 90-98)
|
|
- `whitelist: true`, `forbidNonWhitelisted: true`
|
|
- `transform: true`, `transformOptions: { enableImplicitConversion: true }`
|
|
|
|
**Ví dụ - `auth/presentation/dto/register.dto.ts` (Dòng 1-23):**
|
|
```typescript
|
|
@IsString()
|
|
@MinLength(8)
|
|
password!: string;
|
|
|
|
@IsOptional()
|
|
@IsEmail()
|
|
email?: string;
|
|
```
|
|
|
|
**Ví dụ - `listings/presentation/dto/create-listing.dto.ts` (Dòng 1-50+):**
|
|
- 163 dòng với kiểm tra toàn diện
|
|
- Kiểm tra enum, độ dài min/max, phạm vi số đúng cách
|
|
- `@Transform(({ value }) => BigInt(value))` cho xử lý bigint
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[THẤP] Thiếu kiểm tra trong một số DTO:**
|
|
- `payments/presentation/dto/refund-payment.dto.ts` - cơ bản nhưng kiểm tra tối thiểu
|
|
- `notifications/presentation/dto` - không tìm thấy (controller notifications có thể bỏ qua DTO)
|
|
|
|
**[TRUNG BÌNH] Xử lý BigInt không nhất quán:**
|
|
- `listings/presentation/dto/create-listing.dto.ts` sử dụng `@Transform(({ value }) => BigInt(value))`
|
|
- Nhưng không phải tất cả trường giá/số tiền trong các module khác đều dùng mẫu này
|
|
- `payments/presentation/dto/create-payment.dto.ts` - kiểm tra xem số tiền bigint có được kiểm tra không
|
|
|
|
**[THẤP] Các validator tùy chỉnh chưa được tách ra:**
|
|
- Kiểm tra số điện thoại Việt Nam trong `auth/infrastructure/strategies/local.strategy.ts`
|
|
- Không tìm thấy decorator `@IsVietnamPhone()` có thể tái sử dụng
|
|
- Nên tạo: `shared/decorators/vietnam-phone.decorator.ts`
|
|
|
|
---
|
|
|
|
## 8. GHI LOG
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **LoggerService Tùy Chỉnh**: `/modules/shared/infrastructure/logger.service.ts` (Dòng 1-52)
|
|
- Sử dụng logger Pino với transport dựa trên môi trường
|
|
- In đẹp trong môi trường không phải production, JSON có cấu trúc trong production
|
|
- Che giấu PII qua hàm `maskPii()`
|
|
- Hỗ trợ tham số context và trace
|
|
|
|
- **Mẫu Inject Logger**: Các service inject đúng cách `LoggerService`
|
|
- Constructor `PaymentCompletedListener` (Dòng 14)
|
|
- Tất cả handler có quyền truy cập vào logging tập trung
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[TRUNG BÌNH] `Logger` trực tiếp từ `@nestjs/common` vẫn được dùng ở 50+ nơi:**
|
|
```typescript
|
|
private readonly logger = new Logger(ClassName.name);
|
|
```
|
|
|
|
Nên sử dụng:
|
|
```typescript
|
|
constructor(private readonly logger: LoggerService) {}
|
|
```
|
|
|
|
**Tìm thấy trong:**
|
|
- `payments/infrastructure/services/zalopay.service.ts` (Dòng 11)
|
|
- `payments/infrastructure/services/momo.service.ts` (Dòng 11)
|
|
- `payments/infrastructure/services/vnpay.service.ts` (Dòng 11)
|
|
- Tất cả payment handler
|
|
- Tất cả OAuth strategy
|
|
|
|
**[TRUNG BÌNH] LoggerService chưa được đăng ký trong SharedModule:**
|
|
- Cần xác minh `shared.module.ts` export LoggerService
|
|
- Nếu không, điều này giải thích tại sao các handler sử dụng import Logger trực tiếp
|
|
|
|
**[THẤP] Mức độ log không nhất quán:**
|
|
- Một số dùng `.log()`, một số dùng `.warn()`, một số dùng `.error()`
|
|
- Không có log phục hồi LỖI (chỉ báo cáo lỗi)
|
|
- Cân nhắc thêm `.verbose()` để debug
|
|
|
|
---
|
|
|
|
## 9. PHIÊN BẢN API
|
|
|
|
### ⚠️ VẤN ĐỀ NGHIÊM TRỌNG
|
|
**[CAO] Không tìm thấy phiên bản API:**
|
|
- `main.ts` Dòng 40: `SwaggerModule.setup('api/docs', app, document)`
|
|
- Các controller sử dụng `@Controller('payments')`, `@Controller('auth')`, v.v.
|
|
- **Không tìm thấy tiền tố `/api/v1/`**
|
|
|
|
**Cấu trúc mong đợi:**
|
|
```typescript
|
|
@Controller('api/v1/payments') // Hiện tại: 'payments'
|
|
@Controller('api/v1/auth') // Hiện tại: 'auth'
|
|
```
|
|
|
|
**Hoặc thông qua tiền tố toàn cục:**
|
|
```typescript
|
|
app.setGlobalPrefix('api/v1'); // Trong bootstrap main.ts
|
|
```
|
|
|
|
**Giải pháp**: Thêm vào `main.ts` sau khi tạo app:
|
|
```typescript
|
|
app.setGlobalPrefix('api/v1');
|
|
```
|
|
|
|
Điều này đảm bảo:
|
|
- Tất cả route trở thành `/api/v1/*`
|
|
- Tài liệu Swagger tại `/api/v1/docs`
|
|
- Sẵn sàng cho tương lai với hỗ trợ v2
|
|
|
|
---
|
|
|
|
## 10. VI PHẠM KÍCH THƯỚC FILE (>200 dòng)
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[TRUNG BÌNH] Các file vượt quá quy ước 200 dòng:**
|
|
|
|
1. **admin/infrastructure/repositories/prisma-admin-query.repository.ts** (313 dòng)
|
|
- Nhiều phương thức query (getModerationQueue, getDashboardStats, getRevenueStats, v.v.)
|
|
- **Giải pháp**: Tách thành các repository query riêng biệt theo domain
|
|
|
|
2. **admin/presentation/controllers/admin.controller.ts** (289 dòng)
|
|
- Tất cả endpoint admin trong một controller duy nhất
|
|
- **Giải pháp**: Tách thành controller admin-listings, admin-users, admin-subscriptions
|
|
|
|
3. **listings/infrastructure/repositories/prisma-listing.repository.ts** (274 dòng)
|
|
- Quá nhiều phương thức (findById, findByIdWithProperty, search, save, v.v.)
|
|
- **Giải pháp**: Tách thao tác đọc/ghi
|
|
|
|
4. **analytics/infrastructure/__tests__/prisma-market-index.repository.spec.ts** (254 dòng)
|
|
- File test lớn, chấp nhận được
|
|
|
|
5. **listings/domain/__tests__/property.entity.spec.ts** (234 dòng)
|
|
- File test lớn, chấp nhận được
|
|
|
|
6. **listings/presentation/controllers/listings.controller.ts** (213 dòng)
|
|
- Nhiều endpoint (create, update, delete, search, v.v.)
|
|
- **Giải pháp**: Tách thành các lớp action riêng biệt hoặc thu gọn lại
|
|
|
|
7. **payments/infrastructure/services/zalopay.service.ts** (211 dòng)
|
|
- Service payment gateway xử lý nhiều thao tác
|
|
- Chấp nhận được nhưng nên cân nhắc tái cấu trúc
|
|
|
|
8. **payments/infrastructure/services/momo.service.ts** (209 dòng)
|
|
- Tương tự service ZaloPay
|
|
- Chấp nhận được nhưng cân nhắc tách ra
|
|
|
|
9. **auth/presentation/controllers/auth.controller.ts** (200 dòng)
|
|
- Ranh giới, chấp nhận được nhưng gần giới hạn
|
|
- Theo dõi sự tăng trưởng
|
|
|
|
**Tổng số file >200 dòng: 9 file (3 nghiêm trọng, 6 chấp nhận được)**
|
|
|
|
---
|
|
|
|
## 11. CẤU HÌNH ESLINT
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Định Dạng Flat Config Hiện Đại**: `eslint.config.mjs` (ESLint v9+)
|
|
- **Phạm Vi Quy Tắc Toàn Diện** (Dòng 8-122):
|
|
- Quy tắc TypeScript được khuyến nghị ✅
|
|
- Plugin Import (sắp xếp builtin, external, internal) ✅
|
|
- Tích hợp Prettier ✅
|
|
- Biến không dùng với mẫu `^_` ✅
|
|
- Áp dụng type import ✅
|
|
|
|
- **Các Override Cụ Thể**:
|
|
- Quy tắc module NestJS: `@typescript-eslint/no-extraneous-class: off` (Dòng 85)
|
|
- Override React/Next (Dòng 92-102)
|
|
- Nới lỏng cho file test (Dòng 105-112)
|
|
- Nới lỏng cho file script (Dòng 114-121)
|
|
|
|
### ⚠️ QUY TẮC CÒN THIẾU
|
|
|
|
**[TRUNG BÌNH] Thiếu các quy tắc linting quan trọng:**
|
|
1. Không có `no-restricted-imports` để ngăn import infrastructure trực tiếp
|
|
2. Không có `@typescript-eslint/explicit-function-return-types` được áp dụng
|
|
3. Không có `@typescript-eslint/explicit-module-boundary-types`
|
|
4. Không có plugin `sonarjs` cho độ phức tạp nhận thức
|
|
5. Không có `eslint-plugin-decorator-frame` cho các quy tắc đặc trưng NestJS
|
|
|
|
**Khuyến nghị**: Thêm vào eslint.config.mjs:
|
|
```javascript
|
|
{
|
|
files: ['apps/api/**/*.ts'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
patterns: [
|
|
'@modules/*/infrastructure/*',
|
|
'@modules/*/application/*',
|
|
'@modules/*/presentation/*'
|
|
]
|
|
}
|
|
],
|
|
'@typescript-eslint/explicit-function-return-types': ['warn', {
|
|
allowExpressions: true,
|
|
allowTypedFunctionExpressions: true
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 12. MẪU HIỆU SUẤT
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Phân Trang Được Triển Khai**: Các repository bao gồm logic phân trang
|
|
- `admin/infrastructure/repositories/prisma-admin-query.repository.ts` (Dòng 18-52)
|
|
- `listings/infrastructure/repositories/prisma-listing.repository.ts`
|
|
- **Tối Ưu Hóa Query**: Sử dụng `select` và `include` đúng cách ở nhiều nơi
|
|
- Ví dụ: `listings/infrastructure/repositories/prisma-listing.repository.ts` (Dòng 21-29)
|
|
- Giới hạn media ở 10 mục với `take: 10`
|
|
|
|
### ⚠️ VẤN ĐỀ PHÁT HIỆN
|
|
|
|
**[TRUNG BÌNH] Rủi Ro Query N+1 Tiềm Ẩn:**
|
|
|
|
1. **admin/infrastructure/repositories/prisma-admin-query.repository.ts:**
|
|
- Dòng 21-32: `findMany` với `include` trên property, seller ✅ (Tốt)
|
|
- Dòng 69-77: Nhiều lời gọi `.count()` tuần tự ✅ (Dùng Promise.all - Tốt)
|
|
|
|
2. **payments/application/commands/handle-callback/handle-callback.handler.ts:**
|
|
- Cần xác minh xem `payment.findUnique()` có bao gồm tất cả dữ liệu liên quan không
|
|
- Các listener có thể thực hiện thêm query sau khi thanh toán hoàn tất
|
|
|
|
3. **listings/infrastructure/repositories/prisma-listing.repository.ts:**
|
|
- Dòng 24: `media: { orderBy: { order: 'asc' }, take: 10 }` ✅ (Giới hạn)
|
|
- Nhưng các phương thức khác có thể không bao gồm tất cả quan hệ cần thiết
|
|
|
|
**[THẤP] Thiếu index cơ sở dữ liệu:**
|
|
- Schema Prisma nên định nghĩa index cho:
|
|
- `listing.status` (PENDING_REVIEW, ACTIVE, v.v.)
|
|
- `payment.status` và phạm vi thời gian
|
|
- `user.createdAt` cho phạm vi ngày
|
|
- Kiểm tra `schema.prisma` để biết định nghĩa index
|
|
|
|
**[THẤP] Không thấy cache kết quả query:**
|
|
- `CacheService` tồn tại nhưng việc sử dụng giới hạn ở:
|
|
- `auth/application/queries/get-profile` (Dòng ?)
|
|
- Nên cache:
|
|
- Hồ sơ người dùng (TTL 5 phút)
|
|
- Listings (TTL 1 phút)
|
|
- Trạng thái thanh toán (TTL 30 giây)
|
|
|
|
---
|
|
|
|
## CẤU HÌNH DEPENDENCY CRUISER
|
|
|
|
### ✅ ĐIỂM MẠNH
|
|
- **Quy tắc được cấu hình tốt**: `.dependency-cruiser.cjs` (Dòng 1-79)
|
|
- Phát hiện dependency vòng tròn ✅
|
|
- Import nội bộ liên module bị cấm ✅
|
|
- App import nội bộ module bị cấm ✅
|
|
- Phát hiện module mồ côi ✅
|
|
|
|
### GHI CHÚ
|
|
- Các quy tắc này nên bắt được các vi phạm import được tìm thấy trong mục 2
|
|
- Chạy `pnpx depcruise` để kiểm tra tuân thủ
|
|
|
|
---
|
|
|
|
## TỔNG KẾT CÁC PHÁT HIỆN
|
|
|
|
### Vấn Đề Nghiêm Trọng (Phải Sửa)
|
|
1. **Các entity domain ném Error thông thường** - Nên trả về Result hoặc ném DomainException
|
|
2. **Không có phiên bản API** - Thêm tiền tố `/api/v1/`
|
|
3. **Import nội bộ liên module** - Cập nhật barrel export
|
|
|
|
### Vấn Đề Ưu Tiên Cao
|
|
1. **Các service infrastructure ném Error khi kiểm tra biến môi trường** - Chuyển sang module factory
|
|
2. **Phát sự kiện chưa được triển khai** - Thêm vào aggregate root
|
|
3. **Mẫu Logger không nhất quán** - 50+ import Logger trực tiếp thay vì inject
|
|
|
|
### Vấn Đề Ưu Tiên Trung Bình
|
|
1. **Trùng lặp code** - Logger, Prisma service, logic phân trang
|
|
2. **Vi phạm file lớn** - 3 file vượt đáng kể >200 dòng
|
|
3. **Thiếu validator tùy chỉnh** - Không có decorator @IsVietnamPhone()
|
|
4. **Rủi ro query N+1** - Một số repository cần tối ưu hóa
|
|
|
|
### Vấn Đề Ưu Tiên Thấp
|
|
1. **Thiếu quy tắc ESLint** - Thiếu kiểu trả về hàm tường minh
|
|
2. **Export module chưa đầy đủ** - SharedModule không export tất cả service
|
|
3. **Không có chiến lược cache** - Cân nhắc triển khai cho các query thường xuyên
|
|
4. **File test dùng Logger trực tiếp** - Không nghiêm trọng nhưng không nhất quán
|
|
|
|
---
|
|
|
|
## KHUYẾN NGHỊ
|
|
|
|
### Thực Hiện Nhanh (1-2 ngày)
|
|
- [ ] Thêm tiền tố toàn cục `/api/v1/` vào main.ts
|
|
- [ ] Export các service còn thiếu trong barrel module
|
|
- [ ] Cập nhật 10 file để import từ barrel thay vì đường dẫn trực tiếp
|
|
|
|
### Trung Hạn (1 tuần)
|
|
- [ ] Tạo BaseRepository và BaseHandler cho tính nhất quán DI
|
|
- [ ] Thêm @IsVietnamPhone() và các validator tùy chỉnh khác
|
|
- [ ] Tách các file controller/repository lớn
|
|
- [ ] Thay thế import Logger trực tiếp bằng inject
|
|
|
|
### Dài Hạn (2+ tuần)
|
|
- [ ] Triển khai phát sự kiện trong các entity domain
|
|
- [ ] Thêm event handler cho nhiều sự kiện domain hơn
|
|
- [ ] Triển khai xử lý lỗi dựa trên result trong các handler
|
|
- [ ] Thêm chiến lược cache toàn diện
|
|
- [ ] Mở rộng quy tắc ESLint để áp dụng kiến trúc
|