droplinked-backend

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Droplinked Backend Development Guide

Droplinked后端开发指南

Architecture Overview

架构概述

Strict layered architecture with clear separation of concerns:
Controller -> Service Facade -> UseCase -> Repository
LayerRoleRules
ControllerHTTP onlyTransform params, call service. No logic. Use
@CurrentUser()
,
IsObjectIdPipe
Service FacadeThin orchestrationGet UseCase via
ModuleRef
, call
execute()
. No business logic
UseCaseAll business logicExtend
UseCase<T,R>
, implement
validate()
+
doExecute()
RepositoryData access onlyExtend
Repository<T>
, use Prisma. Semantic methods like
findCartForCheckout()
具有清晰关注点分离的严格分层架构:
Controller -> Service Facade -> UseCase -> Repository
层级职责规则
Controller仅处理HTTP转换参数,调用服务。无业务逻辑。使用
@CurrentUser()
IsObjectIdPipe
Service Facade轻量编排通过
ModuleRef
获取UseCase,调用
execute()
。无业务逻辑
UseCase所有业务逻辑继承
UseCase<T,R>
,实现
validate()
+
doExecute()
Repository仅数据访问继承
Repository<T>
,使用Prisma。提供语义化方法如
findCartForCheckout()

Module Structure

模块结构

All file names in
kebab-case
:
src/modules/<module-name>/
├── controllers/          # HTTP Controllers
│   └── <module>.controller.ts
├── services/             # Service Facades
│   └── <module>.service.ts
├── use-cases/            # Business Logic (one file per operation)
│   ├── create-order.use-case.ts
│   └── get-order.use-case.ts
├── repositories/         # Data Access Layer
│   └── <entity>.repository.ts
├── dtos/                 # Validation with class-validator
│   ├── create-order.dto.ts
│   └── order-response.dto.ts
├── events/               # EventEmitter definitions
│   └── order-created.event.ts
├── listeners/            # @OnEvent handlers
│   └── order.listener.ts
├── <module>.module.ts
└── docs.md               # UseCase documentation
所有文件名采用
kebab-case
命名:
src/modules/<module-name>/
├── controllers/          # HTTP控制器
│   └── <module>.controller.ts
├── services/             # Service Facade
│   └── <module>.service.ts
├── use-cases/            # 业务逻辑(每个操作对应一个文件)
│   ├── create-order.use-case.ts
│   └── get-order.use-case.ts
├── repositories/         # 数据访问层
│   └── <entity>.repository.ts
├── dtos/                 # 使用class-validator做验证
│   ├── create-order.dto.ts
│   └── order-response.dto.ts
├── events/               # EventEmitter定义
│   └── order-created.event.ts
├── listeners/            # @OnEvent处理器
│   └── order.listener.ts
├── <module>.module.ts
└── docs.md               # UseCase文档

UseCase Implementation Pattern

UseCase实现模式

Extend
UseCase<TRequest, TResponse>
from
src/common/services/use-case.base.ts
:
typescript
@Injectable()
export class CreateOrderUseCase extends UseCase<CreateOrderRequest, CreateOrderResponse> {
  validate(request: CreateOrderRequest): void {
    if (!isValidObjectId(request.cartId))
      throw new BadRequestException('Invalid cart ID');
  }

  async doExecute(request: CreateOrderRequest): Promise<CreateOrderResponse> {
    // 1. Fetch and validate context
    const context = await this.validateAndFetchContext(request);

    // 2. Perform core business logic
    const result = this.performCoreLogic(context);

    // 3. Persist changes
    await this.persistChanges(result);

    // 4. Return final result
    return this.fetchFinalResult(result.id);
  }

  // Use interfaces for data between private methods
  private async validateAndFetchContext(req: Request): Promise<ExecutionContext> { ... }
  private performCoreLogic(ctx: ExecutionContext): CalculationResult { ... }
}
Key rules:
  • doExecute
    reads like a table of contents, not the implementation
  • Use numbered comments for step-by-step flow
  • Define
    interface
    s for data passed between private methods
  • Never call
    this.prisma
    directly in
    doExecute
    ; use semantic private methods
src/common/services/use-case.base.ts
继承
UseCase<TRequest, TResponse>
typescript
@Injectable()
export class CreateOrderUseCase extends UseCase<CreateOrderRequest, CreateOrderResponse> {
  validate(request: CreateOrderRequest): void {
    if (!isValidObjectId(request.cartId))
      throw new BadRequestException('Invalid cart ID');
  }

  async doExecute(request: CreateOrderRequest): Promise<CreateOrderResponse> {
    // 1. 获取并验证上下文
    const context = await this.validateAndFetchContext(request);

    // 2. 执行核心业务逻辑
    const result = this.performCoreLogic(context);

    // 3. 持久化变更
    await this.persistChanges(result);

    // 4. 返回最终结果
    return this.fetchFinalResult(result.id);
  }

  // 私有方法间的数据传递使用接口
  private async validateAndFetchContext(req: Request): Promise<ExecutionContext> { ... }
  private performCoreLogic(ctx: ExecutionContext): CalculationResult { ... }
}
核心规则:
  • doExecute
    的写法应类似目录,而非具体实现
  • 使用编号注释展示分步流程
  • 为私有方法间传递的数据定义
    interface
  • 切勿在
    doExecute
    中直接调用
    this.prisma
    ;使用语义化私有方法

Cross-Module Data Access

跨模块数据访问

CRITICAL: Never query another module's tables directly.
typescript
// WRONG: Direct Prisma access to another module's table
const user = await this.prisma.user.findUnique({ where: { id } });

// CORRECT: Use the owning module's service
const user = await this.userService.findUserById(id);
Each module owns its tables exclusively. Cross-module data flows through Service Facades.
重要提示:切勿直接查询其他模块的表。
typescript
// 错误写法:直接访问其他模块的表
const user = await this.prisma.user.findUnique({ where: { id } });

// 正确写法:使用所属模块的服务
const user = await this.userService.findUserById(id);
每个模块独占其表。跨模块数据通过Service Facade流转。

Validation Strategy

验证策略

Layer 1: Syntactic (DTOs)

层级1:语法验证(DTO)

All fields require
@Is...
decorators from
class-validator
:
typescript
export class CreateOrderDto {
  @IsString()
  @IsNotEmpty()
  cartId: string;

  @IsOptional()
  @IsString()
  note?: string;
}
所有字段需使用
class-validator
@Is...
装饰器:
typescript
export class CreateOrderDto {
  @IsString()
  @IsNotEmpty()
  cartId: string;

  @IsOptional()
  @IsString()
  note?: string;
}

Layer 2: Controller Params

层级2:控制器参数

Use
IsObjectIdPipe
for MongoDB ObjectIds:
typescript
@Get(':id')
findOne(@Param('id', IsObjectIdPipe) id: string) { ... }
对MongoDB ObjectId使用
IsObjectIdPipe
typescript
@Get(':id')
findOne(@Param('id', IsObjectIdPipe) id: string) { ... }

Layer 3: Semantic (UseCase)

层级3:语义验证(UseCase)

Business rules checked in
validate()
or early in
doExecute()
:
typescript
private async validateBusinessRules(cart: CartV2, product: Product) {
  if (cart.shopId !== product.shopId) {
    throw new BadRequestException('Product does not belong to this shop');
  }
  if (product.inventory < 1) {
    throw new BadRequestException('Product out of stock');
  }
}
validate()
doExecute()
早期检查业务规则:
typescript
private async validateBusinessRules(cart: CartV2, product: Product) {
  if (cart.shopId !== product.shopId) {
    throw new BadRequestException('Product does not belong to this shop');
  }
  if (product.inventory < 1) {
    throw new BadRequestException('Product out of stock');
  }
}

Event-Driven Side Effects

事件驱动的副作用

Use
EventEmitter2
for non-blocking operations (emails, analytics, webhooks):
typescript
// Publisher (in UseCase)
this.eventEmitter.emit('order.created', new OrderCreatedEvent(order));

// Subscriber (in listeners/)
@OnEvent('order.created')
async handleOrderCreated(payload: OrderCreatedEvent) {
  await this.emailService.sendReceipt(payload.order.email);
}
When to use events:
  • Sending notifications (email, SMS)
  • Updating analytics/logs
  • Syncing with external systems (webhooks)
When NOT to use events:
  • Core logic requiring strict consistency (use direct calls)
使用
EventEmitter2
处理非阻塞操作(邮件、分析、Webhook):
typescript
// 发布者(在UseCase中)
this.eventEmitter.emit('order.created', new OrderCreatedEvent(order));

// 订阅者(在listeners/中)
@OnEvent('order.created')
async handleOrderCreated(payload: OrderCreatedEvent) {
  await this.emailService.sendReceipt(payload.order.email);
}
何时使用事件:
  • 发送通知(邮件、短信)
  • 更新分析/日志
  • 与外部系统同步(Webhook)
何时不使用事件:
  • 需要强一致性的核心逻辑(使用直接调用)

Database Conventions (Prisma + MongoDB)

数据库规范(Prisma + MongoDB)

  • Schema at
    prisma/schema.prisma
  • Use
    select
    to fetch only needed fields
  • Use
    Promise.all
    for independent parallel queries
  • Use
    findUnique
    over
    findFirst
    for indexed lookups
  • Semantic repository methods:
    findOrderWithRelations()
    , not
    findOne()
typescript
// Good: Parallel independent queries
const [user, cart] = await Promise.all([
  this.fetchUser(userId),
  this.fetchCart(cartId),
]);

// Good: Select only needed fields
this.prisma.product.findUnique({
  where: { id },
  select: { id: true, price: true },
});
  • 架构文件位于
    prisma/schema.prisma
  • 使用
    select
    仅获取所需字段
  • 对独立并行查询使用
    Promise.all
  • 索引查询使用
    findUnique
    而非
    findFirst
  • 语义化仓库方法:
    findOrderWithRelations()
    ,而非
    findOne()
typescript
// 推荐:并行独立查询
const [user, cart] = await Promise.all([
  this.fetchUser(userId),
  this.fetchCart(cartId),
]);

// 推荐:仅选择所需字段
this.prisma.product.findUnique({
  where: { id },
  select: { id: true, price: true },
});

Quick Commands

快速命令

bash
npm run db:generate     # Prisma generate + Swagger docs
npm run start:dev       # Development with watch
npm run test:e2e        # End-to-end tests
npm run docs:generate   # Generate unified Swagger
bash
npm run db:generate     # Prisma生成 + Swagger文档
npm run start:dev       # 开发模式(带热重载)
npm run test:e2e        # 端到端测试
npm run docs:generate   # 生成统一Swagger文档

Reference Files

参考文件

  • Architecture patterns: See references/architecture.md for detailed UseCase patterns, Saga pattern, and repository guidelines
  • Entity models: See references/entities.md for core data models (Shop, Product, Order, Cart, Customer, Merchant)
  • Developer checklist: See references/checklists.md for pre-commit and PR review checklists
  • 架构模式:查看references/architecture.md获取详细的UseCase模式、Saga模式及仓库指南
  • 实体模型:查看references/entities.md获取核心数据模型(Shop、Product、Order、Cart、Customer、Merchant)
  • 开发者检查清单:查看references/checklists.md获取提交前和PR审查检查清单