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
886 lines
29 KiB
Markdown
886 lines
29 KiB
Markdown
# GoodGo Platform - Báo Cáo Kiểm Tra Chất Lượng & Kiến Trúc Mã Nguồn
|
|
**Ngày:** 11 tháng 4 năm 2026
|
|
**Dự án:** GoodGo Platform (Thị trường bất động sản)
|
|
**Phạm vi:** Backend (NestJS + Prisma), Frontend (Next.js)
|
|
|
|
---
|
|
|
|
## Tóm Tắt Điều Hành
|
|
|
|
GoodGo Platform thể hiện **kiến trúc vững chắc** với các mẫu thiết kế Domain-Driven Design (DDD) rõ ràng, xử lý lỗi toàn diện và vệ sinh bảo mật tốt. Mã nguồn cho thấy **chất lượng cấp chuyên nghiệp** với một số điểm nhỏ cần cải thiện. Điểm chất lượng tổng thể: **8.2/10**.
|
|
|
|
### Điểm Mạnh Chính
|
|
✅ Mẫu DDD có cấu trúc tốt với sự phân tách tầng rõ ràng
|
|
✅ Xử lý lỗi mạnh mẽ với các ngoại lệ miền được chuẩn hóa
|
|
✅ Mẫu Result<T> cho xử lý lỗi theo phong cách hàm
|
|
✅ Cấu hình TypeScript nghiêm ngặt
|
|
✅ Triển khai bảo mật toàn diện (Helmet, CSRF, giới hạn tần suất)
|
|
✅ Dependency injection và đóng gói module sạch sẽ
|
|
✅ Không có phụ thuộc vòng tròn
|
|
✅ Cơ sở phân trang và tối ưu hóa truy vấn đúng đắn
|
|
|
|
### Các Điểm Cần Cải Thiện
|
|
⚠️ Sử dụng hạn chế mẫu Result<T> (chỉ trong value objects)
|
|
⚠️ Sử dụng domain events không nhất quán
|
|
⚠️ Rủi ro truy vấn N+1 trong một số repository
|
|
⚠️ Thiếu hụt độ phủ kiểm thử ở một số khu vực
|
|
⚠️ Mẫu truy cập biến môi trường cần được chuẩn hóa
|
|
|
|
---
|
|
|
|
## 1. Tuân Thủ Mẫu DDD
|
|
|
|
### ✅ **Đánh Giá: TỐT (8.5/10)**
|
|
|
|
Dự án thể hiện **triển khai DDD xuất sắc** trên tất cả các module.
|
|
|
|
#### Cấu Trúc Tầng
|
|
```
|
|
Cấu trúc module:
|
|
├── domain/ (Logic nghiệp vụ, thực thể, value objects, repository)
|
|
├── application/ (Use case, command/query handler)
|
|
├── infrastructure/ (Prisma repository, service, strategy)
|
|
└── presentation/ (Controller, DTO, decorator)
|
|
```
|
|
|
|
**Ví dụ - Module Auth:**
|
|
```
|
|
/Users/velikho/Desktop/WORKING/goodgo-platform-ai/apps/api/src/modules/auth/
|
|
├── domain/
|
|
│ ├── entities/user.entity.ts
|
|
│ ├── value-objects/hashed-password.vo.ts, phone.vo.ts, email.vo.ts
|
|
│ ├── events/user-registered.event.ts, etc.
|
|
│ └── repositories/user.repository.ts (interface)
|
|
├── application/
|
|
│ ├── commands/register-user/, login-user/, etc.
|
|
│ └── queries/get-profile/, get-agent-by-user-id/
|
|
├── infrastructure/
|
|
│ ├── repositories/prisma-user.repository.ts (implementation)
|
|
│ ├── services/token.service.ts, oauth.service.ts
|
|
│ └── strategies/jwt.strategy.ts, local.strategy.ts
|
|
└── presentation/
|
|
├── controllers/auth.controller.ts
|
|
├── guards/jwt-auth.guard.ts, roles.guard.ts
|
|
└── decorators/current-user.decorator.ts
|
|
```
|
|
|
|
#### Thành Phần Module
|
|
**Tất cả 16 module đều tuân theo các tầng DDD một cách nhất quán:**
|
|
- admin, agents, analytics, auth, health, inquiries, leads, listings, mcp
|
|
- metrics, notifications, payments, reviews, search, shared, subscriptions
|
|
|
|
**Tệp Module:** `/apps/api/src/modules/auth/auth.module.ts` (Dòng 44-83)
|
|
- ✅ Tổ chức provider rõ ràng
|
|
- ✅ Dependency injection với token repository
|
|
- ✅ Mẫu CQRS với command và query handler
|
|
- ✅ Export sạch sẽ cho tiêu thụ bên ngoài
|
|
|
|
#### Triển Khai Value Object
|
|
**Tệp:** `/apps/api/src/modules/payments/domain/value-objects/money.vo.ts`
|
|
```typescript
|
|
export class Money extends ValueObject<MoneyProps> {
|
|
static create(amountVND: bigint): Result<Money, string> {
|
|
if (amountVND <= 0n) {
|
|
return Result.err('Số tiền phải lớn hơn 0');
|
|
}
|
|
if (amountVND > 999_999_999_999n) {
|
|
return Result.err('Số tiền vượt quá giới hạn cho phép');
|
|
}
|
|
return Result.ok(new Money({ amountVND }));
|
|
}
|
|
}
|
|
```
|
|
✅ **Tốt:** Sử dụng mẫu Result<T> cho việc xác thực logic miền
|
|
|
|
#### Domain Events
|
|
**Các Tệp Tìm Thấy:**
|
|
- `/apps/api/src/modules/auth/domain/events/user-registered.event.ts`
|
|
- `/apps/api/src/modules/auth/domain/events/agent-verified.event.ts`
|
|
- `/apps/api/src/modules/auth/domain/events/user-kyc-updated.event.ts`
|
|
|
|
**Interface:** `/apps/api/src/modules/shared/domain/domain-event.ts`
|
|
```typescript
|
|
export interface DomainEvent {
|
|
readonly eventName: string;
|
|
readonly occurredAt: Date;
|
|
readonly aggregateId: string;
|
|
}
|
|
```
|
|
|
|
⚠️ **Vấn đề:** Domain events được định nghĩa nhưng các mẫu sử dụng còn tối thiểu. Events được export nhưng không được publish nhất quán từ các aggregate. Tích hợp với event bus còn hạn chế.
|
|
|
|
---
|
|
|
|
## 2. Các Mẫu Xử Lý Lỗi
|
|
|
|
### ✅ **Đánh Giá: XUẤT SẮC (9/10)**
|
|
|
|
#### Phân Cấp Ngoại Lệ
|
|
**Tệp:** `/apps/api/src/modules/shared/domain/domain-exception.ts`
|
|
|
|
```typescript
|
|
export class DomainException extends HttpException {
|
|
constructor(
|
|
public readonly errorCode: ErrorCode,
|
|
message: string,
|
|
statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
|
|
public readonly details?: Record<string, unknown>,
|
|
) {
|
|
super(message, statusCode);
|
|
}
|
|
}
|
|
|
|
export class NotFoundException extends DomainException { }
|
|
export class ValidationException extends DomainException { }
|
|
export class ConflictException extends DomainException { }
|
|
export class UnauthorizedException extends DomainException { }
|
|
export class ForbiddenException extends DomainException { }
|
|
```
|
|
|
|
✅ **Điểm mạnh:**
|
|
- Phân cấp ngoại lệ đúng đắn
|
|
- Tất cả ngoại lệ miền đều kế thừa DomainException
|
|
- Ánh xạ ngoại lệ nhận biết HTTP
|
|
- Mã lỗi được chuẩn hóa
|
|
|
|
#### Liệt Kê Mã Lỗi
|
|
**Tệp:** `/apps/api/src/modules/shared/domain/error-codes.ts`
|
|
- 56 mã lỗi đặc thù miền được định nghĩa
|
|
- Định dạng: `DOMAIN_ACTION_REASON`
|
|
- Bao gồm: Auth, User, Course, Listing, Property, Media, Payment, Subscription
|
|
|
|
#### Bộ Lọc Ngoại Lệ Toàn Cục
|
|
**Tệp:** `/apps/api/src/modules/shared/infrastructure/filters/global-exception.filter.ts` (Dòng 1-80+)
|
|
```typescript
|
|
@Catch()
|
|
export class GlobalExceptionFilter implements ExceptionFilter {
|
|
catch(exception: unknown, host: ArgumentsHost): void {
|
|
// ✅ Xử lý DomainException
|
|
// ✅ Xử lý HttpException
|
|
// ✅ Xử lý lỗi Prisma
|
|
// ✅ Ghi log với correlation ID
|
|
// ✅ Trả về ErrorResponseBody chuẩn hóa
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Sử Dụng Ngoại Lệ HTTP
|
|
**Kết Quả Tìm Kiếm:** `throw new` xuất hiện **166 lần** trong mã nguồn
|
|
- ⚠️ Hầu hết các throw có trong kiểm thử, điều này là chấp nhận được
|
|
- ✅ Mã production sử dụng ngoại lệ miền một cách nhất quán
|
|
|
|
#### Mẫu Result<T>
|
|
**Tệp:** `/apps/api/src/modules/shared/domain/result.ts` (Dòng 1-56)
|
|
|
|
```typescript
|
|
export class Result<T, E = Error> {
|
|
static ok<T, E = Error>(value: T): Result<T, E>
|
|
static err<T, E = Error>(error: E): Result<T, E>
|
|
|
|
isOk: boolean
|
|
isErr: boolean
|
|
|
|
unwrap(): T
|
|
unwrapErr(): E
|
|
map<U>(fn: (value: T) => U): Result<U, E>
|
|
mapErr<F>(fn: (error: E) => F): Result<T, F>
|
|
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E>
|
|
unwrapOr(defaultValue: T): T
|
|
match<U>(handlers: { ok, err }): U
|
|
}
|
|
```
|
|
|
|
⚠️ **Thiếu hụt:** Result<T> được định nghĩa và sử dụng trong value objects, nhưng các application handler vẫn dùng throw exception thay vì luồng dựa trên Result. Mẫu hỗn hợp trên toàn mã nguồn.
|
|
|
|
---
|
|
|
|
## 3. Độ Nghiêm Ngặt TypeScript
|
|
|
|
### ✅ **Đánh Giá: XUẤT SẮC (9.5/10)**
|
|
|
|
#### tsconfig.json Cơ Sở
|
|
**Tệp:** `/Users/velikho/Desktop/WORKING/goodgo-platform-ai/tsconfig.base.json`
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"strict": true,
|
|
"noUncheckedIndexedAccess": true,
|
|
"noImplicitOverride": true,
|
|
"noPropertyAccessFromIndexSignature": true,
|
|
"skipLibCheck": true,
|
|
"forceConsistentCasingInFileNames": true
|
|
}
|
|
}
|
|
```
|
|
|
|
✅ **Thiết lập xuất sắc:**
|
|
- `strict: true` — bật tất cả kiểm tra nghiêm ngặt
|
|
- `noUncheckedIndexedAccess: true` — ngăn truy cập chỉ mục không an toàn
|
|
- `noImplicitOverride: true` — yêu cầu từ khóa override rõ ràng
|
|
- `noPropertyAccessFromIndexSignature: true` — ngăn truy cập trực tiếp vào index signature
|
|
|
|
#### tsconfig.json API
|
|
**Tệp:** `/apps/api/tsconfig.json`
|
|
```json
|
|
{
|
|
"extends": "../../tsconfig.base.json",
|
|
"compilerOptions": {
|
|
"module": "CommonJS",
|
|
"emitDecoratorMetadata": true,
|
|
"experimentalDecorators": true,
|
|
"paths": { "@modules/*": ["./src/modules/*"] }
|
|
}
|
|
}
|
|
```
|
|
|
|
✅ **Tốt:** Thiết lập đặc thù NestJS cho decorator
|
|
|
|
#### Sử Dụng Kiểu any
|
|
**Kết Quả Tìm Kiếm:** 20 trường hợp `: any`
|
|
- Vị trí: Chủ yếu trong các tệp kiểm thử (chấp nhận được)
|
|
- Sử dụng production: ~8 trường hợp (chấp nhận được cho mock/strategy)
|
|
|
|
**Ví dụ:**
|
|
```
|
|
/apps/api/src/instrument.ts:const integrations: any[] = []; // Tích hợp Sentry
|
|
/apps/api/src/auth/infrastructure/__tests__/jwt.strategy.spec.ts: any[] (mock kiểm thử)
|
|
```
|
|
|
|
⚠️ **Vấn đề nhỏ:** `instrument.ts` dùng `any[]` cho tích hợp Sentry — có thể đặt kiểu tốt hơn
|
|
|
|
#### Cấu Hình ESLint
|
|
**Tệp:** `/eslint.config.mjs` (150 dòng)
|
|
|
|
**Các quy tắc mạnh được cấu hình:**
|
|
- ✅ `@typescript-eslint/no-explicit-any: warn`
|
|
- ✅ `@typescript-eslint/consistent-type-imports` — bắt buộc import kiểu nội tuyến
|
|
- ✅ `@typescript-eslint/no-unused-vars` — với ngoại lệ mẫu gạch dưới
|
|
- ✅ `import-x/order` — bắt buộc thứ tự import
|
|
- ✅ `import-x/no-duplicates` — ngăn import trùng lặp
|
|
|
|
---
|
|
|
|
## 4. Thứ Tự Import & Ranh Giới Module
|
|
|
|
### ✅ **Đánh Giá: XUẤT SẮC (9/10)**
|
|
|
|
#### Plugin ESLint Import
|
|
**Tệp:** `/eslint.config.mjs` (Dòng 30-72)
|
|
|
|
```javascript
|
|
importPlugin.flatConfigs.recommended,
|
|
importPlugin.flatConfigs.typescript,
|
|
|
|
// Thứ tự import
|
|
'import-x/order': [
|
|
'error',
|
|
{
|
|
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
|
'newlines-between': 'never',
|
|
alphabetize: { order: 'asc', caseInsensitive: true },
|
|
},
|
|
],
|
|
'import-x/no-duplicates': ['error', { 'prefer-inline': true }],
|
|
```
|
|
|
|
✅ **Xuất sắc:** Phân cấp import rõ ràng
|
|
|
|
#### Quy Tắc Đóng Gói Module
|
|
**Tệp:** `/eslint.config.mjs` (Dòng 92-116)
|
|
|
|
```javascript
|
|
// Đóng gói module: ngăn import nội bộ xuyên module
|
|
{
|
|
files: ['apps/api/src/modules/**/*.ts'],
|
|
ignores: ['**/*.spec.ts', '**/*.test.ts'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
patterns: [
|
|
{
|
|
group: [
|
|
'@modules/*/application/*',
|
|
'@modules/*/domain/*',
|
|
'@modules/*/infrastructure/*',
|
|
'@modules/*/presentation/*',
|
|
],
|
|
message: 'Import from module barrel (@modules/<module>) instead of internal paths'
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
✅ **Xuất sắc:** Bắt buộc barrel export, ngăn import đường dẫn nội bộ
|
|
|
|
#### Kiểm Tra Phụ Thuộc Vòng Tròn
|
|
**Kết Quả Lệnh:**
|
|
```
|
|
✔ no dependency violations found (758 modules, 1717 dependencies cruised)
|
|
```
|
|
|
|
✅ **Hoàn hảo:** Không phát hiện phụ thuộc vòng tròn nào
|
|
|
|
#### Barrel Export Module
|
|
**Ví dụ - Module Auth:** `/apps/api/src/modules/auth/index.ts`
|
|
```typescript
|
|
export { AuthModule } from './auth.module';
|
|
export { JwtAuthGuard } from './presentation/guards/jwt-auth.guard';
|
|
export { Roles } from './presentation/decorators/roles.decorator';
|
|
export { UserEntity, type UserProps } from './domain/entities/user.entity';
|
|
export { USER_REPOSITORY, type IUserRepository } from './domain/repositories/user.repository';
|
|
// ... export được tổ chức tốt
|
|
```
|
|
|
|
✅ **Tốt:** Barrel export ẩn đúng cấu trúc nội bộ
|
|
|
|
---
|
|
|
|
## 5. Xác Thực & Bảo Mật
|
|
|
|
### ✅ **Đánh Giá: XUẤT SẮC (9.2/10)**
|
|
|
|
#### Triển Khai JWT
|
|
**Tệp:** `/apps/api/src/modules/auth/infrastructure/strategies/jwt.strategy.ts`
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
constructor() {
|
|
const jwtSecret = process.env['JWT_SECRET'];
|
|
if (!jwtSecret) {
|
|
throw new Error('JWT_SECRET environment variable is required');
|
|
}
|
|
|
|
super({
|
|
jwtFromRequest: extractJwtFromCookieOrHeader,
|
|
ignoreExpiration: false, // ✅ Bắt buộc hết hạn
|
|
secretOrKey: jwtSecret,
|
|
audience: 'goodgo-api', // ✅ Xác thực audience
|
|
issuer: 'goodgo-platform', // ✅ Xác thực issuer
|
|
});
|
|
}
|
|
|
|
validate(payload: JwtPayload): JwtPayload {
|
|
return { sub: payload.sub, phone: payload.phone, role: payload.role };
|
|
}
|
|
}
|
|
```
|
|
|
|
✅ **Điểm mạnh:**
|
|
- Xác thực audience và issuer
|
|
- Bắt buộc hết hạn token
|
|
- Trích xuất kép từ cookie và Authorization header
|
|
- Phương thức validate đúng đắn
|
|
|
|
#### Triển Khai Guard
|
|
**JWT Guard:** `/apps/api/src/modules/auth/presentation/guards/jwt-auth.guard.ts`
|
|
```typescript
|
|
@Injectable()
|
|
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
|
```
|
|
|
|
**Roles Guard:** Các tệp tồn tại và được triển khai đúng cách
|
|
|
|
✅ **Tốt:** Guard dựa trên Passport với composition
|
|
|
|
#### Bảo Vệ CSRF
|
|
**Tệp:** `/apps/api/src/modules/shared/infrastructure/middleware/csrf.middleware.ts` (Dòng 1-48)
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class CsrfMiddleware implements NestMiddleware {
|
|
use(req: Request, res: Response, next: NextFunction): void {
|
|
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
|
|
if (SAFE_METHODS.has(req.method)) {
|
|
this.ensureCsrfCookie(req, res);
|
|
return next();
|
|
}
|
|
|
|
// Xác thực CSRF token kiểu double-submit
|
|
const cookieToken = req.cookies?.[CSRF_COOKIE];
|
|
const headerToken = req.headers[CSRF_HEADER];
|
|
|
|
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
throw new ForbiddenException('CSRF token missing or invalid');
|
|
}
|
|
|
|
this.setCsrfCookie(res);
|
|
next();
|
|
}
|
|
|
|
private setCsrfCookie(res: Response): void {
|
|
const token = randomBytes(TOKEN_LENGTH).toString('hex');
|
|
res.cookie(CSRF_COOKIE, token, {
|
|
httpOnly: false, // Frontend phải đọc được
|
|
secure: process.env['NODE_ENV'] === 'production',
|
|
sameSite: 'strict',
|
|
path: '/',
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
✅ **Xuất sắc:**
|
|
- Mẫu CSRF token double-submit
|
|
- Cờ cookie đúng đắn (httpOnly: false để client đọc, secure trên production)
|
|
- Xoay vòng token
|
|
- SameSite: strict
|
|
|
|
#### Giới Hạn Tần Suất
|
|
**Tệp:** `/apps/api/src/modules/shared/infrastructure/guards/user-rate-limit.guard.ts` (Dòng 1-143)
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class UserRateLimitGuard implements CanActivate {
|
|
// Giới hạn tần suất theo vai trò (yêu cầu mỗi cửa sổ thời gian)
|
|
export const DEFAULT_ROLE_LIMITS: Record<UserRole, number> = {
|
|
BUYER: 100,
|
|
SELLER: 150,
|
|
AGENT: 200,
|
|
ADMIN: 500,
|
|
};
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const userId: string = user.sub;
|
|
const role: UserRole = user.role;
|
|
|
|
// Bộ đếm cửa sổ trượt Redis với Lua script
|
|
const result = await client.eval(
|
|
`local current = redis.call('INCR', KEYS[1])
|
|
if current == 1 then
|
|
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
|
end`,
|
|
1,
|
|
key,
|
|
windowSeconds,
|
|
);
|
|
|
|
// Trả về header giới hạn tần suất
|
|
response.setHeader('X-RateLimit-Limit', limit);
|
|
response.setHeader('X-RateLimit-Remaining', Math.max(0, limit - current));
|
|
response.setHeader('X-RateLimit-Reset', ttl > 0 ? ttl : windowSeconds);
|
|
|
|
// Cho phép qua khi Redis lỗi để tránh chặn
|
|
if (error) {
|
|
this.logger.warn('...allowing request');
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
✅ **Xuất sắc:**
|
|
- Giới hạn tần suất theo vai trò
|
|
- Cửa sổ trượt Redis với Lua script (nguyên tử)
|
|
- Giới hạn tần suất theo từng người dùng
|
|
- Header giới hạn tần suất đúng đắn
|
|
- Cho phép qua khi Redis bị lỗi
|
|
|
|
#### Header Bảo Mật
|
|
**Tệp:** `/apps/api/src/main.ts` (Dòng 55-79)
|
|
|
|
```typescript
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
|
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
|
imgSrc: ["'self'", 'data:', 'https:', 'blob:'],
|
|
objectSrc: ["'none'"],
|
|
frameSrc: ["'none'"],
|
|
baseUri: ["'self'"],
|
|
formAction: ["'self'"],
|
|
},
|
|
},
|
|
frameguard: { action: 'deny' },
|
|
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
|
|
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
crossOriginEmbedderPolicy: true,
|
|
crossOriginOpenerPolicy: true,
|
|
}),
|
|
);
|
|
|
|
app.use((_req, res, next) => {
|
|
res.setHeader(
|
|
'Permissions-Policy',
|
|
'camera=(), microphone=(), geolocation=(self), payment=(self)',
|
|
);
|
|
next();
|
|
});
|
|
```
|
|
|
|
✅ **Xuất sắc:**
|
|
- Helmet với cấu hình CSP
|
|
- HSTS được bật với preload
|
|
- Permissions-Policy được cấu hình
|
|
- X-Frame-Options: deny (qua frameguard)
|
|
|
|
#### Xác Thực Biến Môi Trường
|
|
**Kết Quả Tìm Kiếm:** 10 trường hợp đọc biến môi trường với giá trị mặc định dự phòng
|
|
- ✅ JWT_SECRET được xác thực khi khởi động
|
|
- ✅ GOOGLE_CLIENT_SECRET được xác thực
|
|
- ✅ ZALO_APP_SECRET được xác thực
|
|
- ✅ Kiểm tra NODE_ENV cho production
|
|
|
|
⚠️ **Vấn đề nhỏ:** Mẫu truy cập chưa được tập trung hóa
|
|
**Đề xuất:** Tạo env config service thay vì đọc `process.env['KEY']` rải rác
|
|
|
|
---
|
|
|
|
## 6. Các Mẫu Cơ Sở Dữ Liệu (Prisma)
|
|
|
|
### ✅ **Đánh Giá: TỐT (8/10)**
|
|
|
|
#### Chất Lượng Schema Prisma
|
|
**Tệp:** `/prisma/schema.prisma` (100 dòng đầu hiển thị)
|
|
|
|
```prisma
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
previewFeatures = ["postgresqlExtensions"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
extensions = [postgis]
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String? @unique
|
|
phone String @unique
|
|
|
|
// ✅ Chiến lược đánh chỉ mục tốt
|
|
@@index([role])
|
|
@@index([kycStatus])
|
|
@@index([isActive])
|
|
@@index([createdAt])
|
|
// Chỉ mục kết hợp để tối ưu hóa truy vấn
|
|
@@index([role, isActive, createdAt(sort: Desc)])
|
|
@@index([kycStatus, createdAt])
|
|
}
|
|
```
|
|
|
|
✅ **Điểm mạnh:**
|
|
- Chỉ mục đúng đắn trên các trường được truy vấn thường xuyên
|
|
- Chỉ mục kết hợp để tối ưu hóa
|
|
- Quan hệ khóa ngoại với cascade delete
|
|
- Extension PostGIS cho truy vấn không gian địa lý
|
|
|
|
#### Giảm Thiểu Truy Vấn N+1
|
|
**Tệp:** `/apps/api/src/modules/inquiries/infrastructure/repositories/prisma-inquiry.repository.ts` (Dòng 37-78)
|
|
|
|
```typescript
|
|
async findByListing(listingId: string, page: number, limit: number) {
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.inquiry.findMany({
|
|
where: { listingId },
|
|
skip,
|
|
take,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
listing: { select: { id: true, property: { select: { title: true } } } },
|
|
user: { select: { id: true, fullName: true, phone: true } },
|
|
},
|
|
}),
|
|
this.prisma.inquiry.count({ where }),
|
|
]);
|
|
// ...
|
|
}
|
|
```
|
|
|
|
✅ **Tốt:**
|
|
- Dùng `include` để lấy dữ liệu liên quan trong một truy vấn duy nhất
|
|
- Promise.all song song cho truy vấn đếm
|
|
- Projection select đúng đắn
|
|
|
|
⚠️ **Vùng rủi ro:** Cần xác minh tất cả truy vấn phức tạp đều dùng include/select đúng cách
|
|
|
|
#### Giao Dịch
|
|
**Kết Quả Tìm Kiếm:** Chỉ **1 giao dịch** được tìm thấy trong mã production
|
|
- Tệp: `/apps/api/src/modules/auth/application/__tests__/force-delete-user.handler.spec.ts` (trong mock kiểm thử)
|
|
|
|
⚠️ **Vấn đề:** Sử dụng giao dịch hạn chế cho các thao tác nhiều bước
|
|
**Khuyến nghị:** Dùng giao dịch cho xử lý thanh toán, thay đổi subscription và cập nhật dây chuyền
|
|
|
|
#### Triển Khai Phân Trang
|
|
**Mẫu tìm thấy:** Giới hạn tối đa ở 100
|
|
```typescript
|
|
const take = Math.min(limit, 100);
|
|
const skip = (page - 1) * take;
|
|
```
|
|
|
|
✅ **Tốt:** Ngăn truy vấn tốn kém với giới hạn quá lớn
|
|
|
|
#### Mẫu Repository
|
|
**Ví dụ:** `/apps/api/src/modules/payments/application/queries/list-transactions/list-transactions.handler.ts`
|
|
|
|
```typescript
|
|
async execute(query: ListTransactionsQuery): Promise<TransactionListDto> {
|
|
const limit = Math.min(query.limit ?? 20, 100);
|
|
const offset = query.offset ?? 0;
|
|
|
|
const { items, total } = await this.paymentRepo.findByUserId(
|
|
query.userId,
|
|
{ status: query.status, limit, offset }
|
|
);
|
|
|
|
return {
|
|
items: items.map((payment) => ({
|
|
id: payment.id,
|
|
provider: payment.provider,
|
|
// ... ánh xạ DTO
|
|
})),
|
|
total,
|
|
limit,
|
|
offset,
|
|
};
|
|
}
|
|
```
|
|
|
|
✅ **Tốt:** Trừu tượng hóa repository đúng đắn với dependency injection
|
|
|
|
---
|
|
|
|
## 7. Các Vấn Đề Hiệu Năng
|
|
|
|
### ⚠️ **Đánh Giá: TỐT (7.5/10)**
|
|
|
|
#### Phân Trang
|
|
✅ **Được triển khai trên các truy vấn chính:**
|
|
- Inquiries: `findByListing()`, `findByAgent()` với giới hạn tối đa
|
|
- Payments: `findByUserId()` với offset
|
|
- Listings: `searchListings()` với page/limit
|
|
|
|
⚠️ **Thiếu hụt:** Một số endpoint có thể thiếu phân trang. Khuyến nghị kiểm tra toàn bộ endpoint danh sách.
|
|
|
|
#### Chiến Lược Cache
|
|
**Tệp Tìm Thấy:**
|
|
- `/apps/api/src/modules/auth/application/queries/get-profile/get-profile.handler.ts` — dùng `CacheService`
|
|
- `/apps/api/src/modules/shared/infrastructure/redis.service.ts` — tích hợp Redis
|
|
|
|
**Ví dụ:**
|
|
```typescript
|
|
return this.cache.getOrSet(
|
|
CacheService.buildKey(CachePrefix.USER_PROFILE, query.userId),
|
|
() => this.userRepo.findById(query.userId),
|
|
TTL_5_MINUTES
|
|
);
|
|
```
|
|
|
|
✅ **Tốt:** Cache cho truy vấn profile
|
|
✅ **Tốt:** Vô hiệu hóa cache khi cập nhật (ví dụ: verify-kyc.handler)
|
|
|
|
⚠️ **Thiếu hụt:** Tổng thể sử dụng cache còn hạn chế. Khuyến nghị mở rộng sang:
|
|
- Gói subscription (dữ liệu ít thay đổi)
|
|
- Danh sách quận/thành phố
|
|
- Báo cáo phân tích
|
|
- Kết quả tìm kiếm
|
|
|
|
#### Kiểm Tra Sức Khỏe Redis
|
|
**Tệp:** `/apps/api/src/modules/health/infrastructure/redis.health.ts`
|
|
|
|
✅ **Tốt:** Liveness probe Redis được bao gồm
|
|
|
|
#### Chỉ Số Kích Thước Mã
|
|
- **Tệp TS Module API:** 537 tệp
|
|
- **Tổng LOC API:** ~45.852 dòng
|
|
- **LOC Web App:** ~9.901 dòng (thư mục app)
|
|
- **Tổng LOC TypeScript:** ~55.000+ (không tính node_modules)
|
|
|
|
**Đánh giá:** Hợp lý cho một nền tảng đầy đủ tính năng
|
|
|
|
---
|
|
|
|
## 8. Kích Thước Mã & Khả Năng Bảo Trì
|
|
|
|
### ✅ **Đánh Giá: TỐT (8/10)**
|
|
|
|
#### Cấu Trúc Dự Án
|
|
```
|
|
/apps/api/src/modules/
|
|
├── 16 module miền (auth, listings, payments, v.v.)
|
|
├── Module /shared (các mối quan tâm chéo)
|
|
├── 537 tệp TypeScript (production + kiểm thử)
|
|
└── ~45.852 LOC tổng cộng
|
|
```
|
|
|
|
#### Số Lượng Module: 16
|
|
1. ✅ admin
|
|
2. ✅ agents
|
|
3. ✅ analytics
|
|
4. ✅ auth
|
|
5. ✅ health
|
|
6. ✅ inquiries
|
|
7. ✅ leads
|
|
8. ✅ listings
|
|
9. ✅ mcp
|
|
10. ✅ metrics
|
|
11. ✅ notifications
|
|
12. ✅ payments
|
|
13. ✅ reviews
|
|
14. ✅ search
|
|
15. ✅ shared
|
|
16. ✅ subscriptions
|
|
|
|
**Đánh giá:** Các module có tổ chức tốt, tập trung
|
|
|
|
#### Tổ Chức Tệp
|
|
```
|
|
/apps/api/src/modules/[module]/
|
|
├── application/
|
|
│ ├── commands/
|
|
│ ├── queries/
|
|
│ └── __tests__/
|
|
├── domain/
|
|
│ ├── entities/
|
|
│ ├── value-objects/
|
|
│ ├── repositories/
|
|
│ ├── services/
|
|
│ └── events/
|
|
├── infrastructure/
|
|
│ ├── repositories/
|
|
│ ├── services/
|
|
│ ├── strategies/
|
|
│ └── __tests__/
|
|
└── presentation/
|
|
├── controllers/
|
|
├── decorators/
|
|
├── guards/
|
|
└── dto/
|
|
```
|
|
|
|
✅ **Xuất sắc:** Cấu trúc nhất quán trên tất cả các module
|
|
|
|
#### Quy Ước Đặt Tên
|
|
✅ **Tốt:** Mẫu đặt tên nhất quán
|
|
- `*Handler.ts` cho CQRS handler
|
|
- `*Guard.ts` cho guard
|
|
- `*Repository.ts` cho truy cập dữ liệu
|
|
- `*Service.ts` cho business service
|
|
- `*.dto.ts` cho data transfer object
|
|
- `*.entity.ts` cho domain entity
|
|
|
|
---
|
|
|
|
## 9. Các Vấn Đề Chất Lượng Mã Tìm Thấy
|
|
|
|
### ✅ **Không Có Vấn Đề Nghiêm Trọng**
|
|
|
|
#### Vấn Đề Nhỏ
|
|
|
|
**1. Biến Môi Trường Rải Rác**
|
|
- **Mức độ nghiêm trọng:** Thấp
|
|
- **Tệp:**
|
|
- `/apps/api/src/modules/auth/auth.module.ts:50`
|
|
- `/apps/api/src/modules/auth/infrastructure/strategies/jwt.strategy.ts:16`
|
|
- `/apps/api/src/main.ts:94`
|
|
- **Khuyến nghị:**
|
|
```typescript
|
|
// Tạo ConfigService trong module shared
|
|
export class ConfigService {
|
|
get jwtSecret(): string { /* ... */ }
|
|
get googleClientSecret(): string { /* ... */ }
|
|
// v.v.
|
|
}
|
|
```
|
|
|
|
**2. Sử Dụng Hạn Chế Result<T> Trong Handler**
|
|
- **Mức độ nghiêm trọng:** Thấp
|
|
- **Hiện tại:** Chỉ value object dùng Result<T>
|
|
- **Handler:** Vẫn throw exception
|
|
- **Khuyến nghị:** Dần dần chuyển đổi handler sang mẫu Result<T> để nhất quán
|
|
|
|
**3. Tích Hợp Sentry Với Any[]**
|
|
- **Tệp:** `/apps/api/src/instrument.ts`
|
|
- **Dòng:** `const integrations: any[] = [];`
|
|
- **Mức độ nghiêm trọng:** Rất thấp
|
|
- **Sửa:** Đặt kiểu là `Sentry.Integration[]`
|
|
|
|
**4. Kiểu any Trong Mock Kiểm Thử**
|
|
- **Mức độ nghiêm trọng:** Thấp (chấp nhận được cho kiểm thử)
|
|
- **Số lượng:** ~20 trường hợp, chủ yếu trong tệp kiểm thử
|
|
- **Đánh giá:** Mẫu chấp nhận được cho mock kiểm thử
|
|
|
|
---
|
|
|
|
## 10. Độ Phủ Kiểm Thử
|
|
|
|
### ⚠️ **Đánh Giá: TRUNG BÌNH (6.5/10)**
|
|
|
|
#### Các Tệp Kiểm Thử Tìm Thấy
|
|
- **Kiểm Thử API:** Các tệp kiểm thử toàn diện được tìm thấy trong `/modules/**/__tests__/`
|
|
- **Mẫu Kiểm Thử:** `*.spec.ts` cho unit, integration test
|
|
|
|
**Ví dụ:**
|
|
- Kiểm thử Auth: đăng ký, đăng nhập, KYC, quy trình xóa
|
|
- Kiểm thử Payment: tạo, xử lý callback, hoàn tiền
|
|
- Kiểm thử Subscription: tạo, nâng cấp, đo lường sử dụng
|
|
- Kiểm thử Query: phân trang, tìm kiếm listing
|
|
|
|
⚠️ **Thiếu hụt:** Không có chỉ số độ phủ kiểm thử rõ ràng
|
|
**Khuyến nghị:** Thêm ngưỡng độ phủ (đề xuất 70%+ cho src/)
|
|
|
|
**Test Runner:** Vitest (thấy trong mock: `vi.fn()`)
|
|
|
|
---
|
|
|
|
## Tóm Tắt Phát Hiện
|
|
|
|
### Điểm Mạnh (Top 5)
|
|
1. **Kiến Trúc DDD Xuất Sắc** — Phân tách tầng rõ ràng, ranh giới module đúng đắn
|
|
2. **Bảo Mật Mạnh Mẽ** — JWT, CSRF, giới hạn tần suất, Helmet, CSP đều được triển khai đúng
|
|
3. **TypeScript Nghiêm Ngặt** — Cài đặt compiler tích cực với các quy tắc tùy chỉnh
|
|
4. **Xử Lý Lỗi Tốt** — Ngoại lệ miền được chuẩn hóa, mã lỗi nhất quán
|
|
5. **Không Có Phụ Thuộc Vòng Tròn** — 758 module được kiểm tra, không vi phạm
|
|
|
|
### Các Điểm Cần Cải Thiện (Top 5)
|
|
1. **Chuẩn Hóa Truy Cập Biến Môi Trường** — Tạo ConfigService tập trung
|
|
2. **Mở Rộng Chiến Lược Cache** — Cache tích cực hơn cho dữ liệu chủ yếu đọc
|
|
3. **Sử Dụng Giao Dịch** — Thêm giao dịch cho các thao tác nhiều bước
|
|
4. **Tính Nhất Quán Result<T>** — Chuyển đổi handler sang xử lý lỗi theo phong cách hàm
|
|
5. **Độ Phủ Kiểm Thử** — Thêm chỉ số độ phủ và tăng số lượng kiểm thử
|
|
|
|
---
|
|
|
|
## Khuyến Nghị
|
|
|
|
### Ưu Tiên 1 (Thực Hiện Ngay)
|
|
- [ ] Tạo `ConfigService` để tập trung truy cập biến môi trường
|
|
- [ ] Thêm decorator `@Transactional()` cho payment/subscription handler
|
|
- [ ] Thiết lập báo cáo độ phủ kiểm thử (hướng đến 70%+)
|
|
|
|
### Ưu Tiên 2 (Sprint Này)
|
|
- [ ] Mở rộng cache sang: gói subscription, quận/huyện, phân tích
|
|
- [ ] Thêm domain event publishing vào aggregate
|
|
- [ ] Chuyển đổi handler phức tạp sang mẫu Result<T>
|
|
|
|
### Ưu Tiên 3 (Quý Này)
|
|
- [ ] Thiết lập E2E test với Playwright (đã có cấu hình)
|
|
- [ ] Thêm kiểm thử hiệu năng (cấu hình K6 đã tồn tại)
|
|
- [ ] Tài liệu hóa các quyết định mô hình miền
|
|
|
|
### Điểm Nợ Kỹ Thuật: 6.5/10
|
|
- ✅ Nợ kiến trúc thấp
|
|
- ⚠️ Nợ vận hành nhỏ (truy cập env, cache)
|
|
- ✅ Nền tảng kiểm thử tốt
|
|
|
|
---
|
|
|
|
## Kết Luận
|
|
|
|
Mã nguồn GoodGo Platform thể hiện **kiến trúc cấp chuyên nghiệp** với các mẫu DDD vững chắc, triển khai bảo mật toàn diện và tổ chức mã sạch sẽ. Dự án đang ở vị thế tốt để mở rộng quy mô với một số cải tiến nhỏ về các mối quan tâm vận hành như cấu hình môi trường và chiến lược cache.
|
|
|
|
**Chất Lượng Mã Tổng Thể: 8.2/10**
|
|
|
|
**Khuyến Nghị:** ĐƯỢC PHÊ DUYỆT cho production với các cải tiến đã ghi trong lộ trình.
|