rwadurian/backend/services/authorization-service/docs/DEVELOPMENT.md

14 KiB
Raw Blame History

Authorization Service 开发指南

目录

  1. 环境准备
  2. 项目初始化
  3. 开发规范
  4. 代码结构约定
  5. DDD 实践指南
  6. 常见开发任务
  7. 调试技巧

环境准备

系统要求

软件 版本 说明
Node.js 20.x LTS 推荐使用 nvm 管理
npm 10.x 随 Node.js 安装
PostgreSQL 15.x 关系型数据库
Redis 7.x 缓存服务
Kafka 3.7.x 消息队列
Docker 24.x 容器运行时

开发工具推荐

  • IDE: VSCode
  • VSCode 扩展:
    • ESLint
    • Prettier
    • Prisma
    • REST Client
    • Docker

本地环境配置

1. 安装 Node.js

# 使用 nvm 安装 Node.js
nvm install 20
nvm use 20

2. 启动基础设施

使用 Docker Compose 启动开发环境:

# 启动 PostgreSQL、Redis、Kafka
docker compose up -d

3. 配置环境变量

复制环境变量模板:

cp .env.example .env.development

编辑 .env.development

# 数据库
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/authorization_dev"

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379

# Kafka
KAFKA_BROKERS=localhost:9092

# JWT
JWT_SECRET=your-development-secret-key
JWT_EXPIRES_IN=1h

# 应用
NODE_ENV=development
PORT=3002

项目初始化

1. 安装依赖

npm install

2. 生成 Prisma 客户端

npx prisma generate

3. 运行数据库迁移

npx prisma migrate dev

4. 启动开发服务器

npm run start:dev

服务将在 http://localhost:3002 启动。

5. 访问 API 文档

http://localhost:3002/api/docs

开发规范

代码风格

项目使用 ESLint + Prettier 进行代码规范检查:

# 检查代码风格
npm run lint

# 自动修复
npm run lint:fix

# 格式化代码
npm run format

命名规范

文件命名

类型 格式 示例
聚合根 {name}.aggregate.ts authorization-role.aggregate.ts
实体 {name}.entity.ts ladder-target-rule.entity.ts
值对象 {name}.vo.ts month.vo.ts
服务 {name}.service.ts authorization-command.service.ts
控制器 {name}.controller.ts authorization.controller.ts
仓储接口 {name}.repository.ts authorization-role.repository.ts
仓储实现 {name}.repository.impl.ts authorization-role.repository.impl.ts
DTO {name}.dto.ts apply-authorization.dto.ts
测试 {name}.spec.ts authorization-role.aggregate.spec.ts

类命名

类型 格式 示例
聚合根 {Name} AuthorizationRole
值对象 {Name} Month, RegionCode
服务 {Name}Service AuthorizationCommandService
控制器 {Name}Controller AuthorizationController
仓储接口 I{Name}Repository IAuthorizationRoleRepository
DTO {Name}Dto ApplyAuthorizationDto

Git 提交规范

使用 Conventional Commits 规范:

<type>(<scope>): <subject>

<body>

<footer>

Type 类型

类型 说明
feat 新功能
fix Bug 修复
docs 文档变更
style 代码格式(不影响功能)
refactor 重构
test 测试相关
chore 构建/工具变更

示例

git commit -m "feat(authorization): add province company authorization"
git commit -m "fix(assessment): correct ranking calculation"
git commit -m "docs(api): update API documentation"

代码结构约定

领域层Domain Layer

聚合根结构

// src/domain/aggregates/authorization-role.aggregate.ts

import { AggregateRoot } from './aggregate-root.base'
import { AuthorizationId, UserId, RegionCode } from '@/domain/value-objects'
import { RoleType, AuthorizationStatus } from '@/domain/enums'
import { AuthorizationAppliedEvent } from '@/domain/events'

export interface AuthorizationRoleProps {
  // 定义聚合根属性
}

export class AuthorizationRole extends AggregateRoot {
  // 私有属性
  private _id: AuthorizationId
  private _userId: UserId
  private _roleType: RoleType
  private _status: AuthorizationStatus

  // Getters只读访问
  get id(): AuthorizationId { return this._id }
  get userId(): UserId { return this._userId }

  // 私有构造函数
  private constructor(props: AuthorizationRoleProps) {
    super()
    // 初始化属性
  }

  // 工厂方法 - 创建新实例
  static createAuthProvinceCompany(params: {
    userId: UserId
    provinceCode: string
    provinceName: string
  }): AuthorizationRole {
    const role = new AuthorizationRole({
      // 初始化
    })

    // 添加领域事件
    role.addDomainEvent(new AuthorizationAppliedEvent({
      // 事件数据
    }))

    return role
  }

  // 工厂方法 - 从持久化恢复
  static fromPersistence(props: AuthorizationRoleProps): AuthorizationRole {
    return new AuthorizationRole(props)
  }

  // 业务方法
  authorize(adminId: UserId): void {
    // 状态验证
    if (this._status !== AuthorizationStatus.PENDING) {
      throw new DomainError('只有待审核状态才能审核')
    }

    // 状态变更
    this._status = AuthorizationStatus.APPROVED
    this._authorizedBy = adminId
    this._authorizedAt = new Date()

    // 发布领域事件
    this.addDomainEvent(new AuthorizationApprovedEvent({
      // 事件数据
    }))
  }

  // 持久化转换
  toPersistence(): Record<string, any> {
    return {
      id: this._id.value,
      userId: this._userId.value,
      // ...
    }
  }
}

值对象结构

// src/domain/value-objects/month.vo.ts

export class Month {
  private constructor(private readonly _value: string) {}

  get value(): string {
    return this._value
  }

  // 工厂方法
  static create(value: string): Month {
    // 验证格式
    if (!/^\d{4}-\d{2}$/.test(value)) {
      throw new DomainError('月份格式无效,应为 YYYY-MM')
    }
    return new Month(value)
  }

  static current(): Month {
    const now = new Date()
    const year = now.getFullYear()
    const month = String(now.getMonth() + 1).padStart(2, '0')
    return new Month(`${year}-${month}`)
  }

  // 业务方法
  next(): Month {
    const [year, month] = this._value.split('-').map(Number)
    if (month === 12) {
      return new Month(`${year + 1}-01`)
    }
    return new Month(`${year}-${String(month + 1).padStart(2, '0')}`)
  }

  // 比较方法
  equals(other: Month): boolean {
    return this._value === other._value
  }

  isBefore(other: Month): boolean {
    return this._value < other._value
  }
}

领域事件结构

// src/domain/events/authorization-applied.event.ts

import { DomainEvent } from './domain-event.base'

export interface AuthorizationAppliedEventPayload {
  authorizationId: string
  userId: string
  roleType: string
  regionCode: string
}

export class AuthorizationAppliedEvent extends DomainEvent {
  static readonly EVENT_NAME = 'authorization.applied'

  constructor(public readonly payload: AuthorizationAppliedEventPayload) {
    super(AuthorizationAppliedEvent.EVENT_NAME)
  }
}

应用层Application Layer

命令服务结构

// src/application/services/authorization-command.service.ts

import { Injectable, Inject } from '@nestjs/common'
import { IAuthorizationRoleRepository } from '@/domain/repositories'
import { AUTHORIZATION_ROLE_REPOSITORY } from '@/infrastructure/persistence/repositories'
import { AuthorizationValidatorService } from '@/domain/services'
import { EventPublisherService } from '@/infrastructure/messaging/kafka'

@Injectable()
export class AuthorizationCommandService {
  constructor(
    @Inject(AUTHORIZATION_ROLE_REPOSITORY)
    private readonly authorizationRepository: IAuthorizationRoleRepository,
    private readonly validatorService: AuthorizationValidatorService,
    private readonly eventPublisher: EventPublisherService,
  ) {}

  async applyProvincialAuthorization(
    userId: string,
    provinceCode: string,
    provinceName: string,
  ): Promise<AuthorizationRole> {
    // 1. 创建值对象
    const userIdVo = UserId.create(userId)
    const regionCodeVo = RegionCode.create(provinceCode)

    // 2. 业务验证
    const validationResult = await this.validatorService.validateAuthorizationRequest(
      userIdVo,
      RoleType.AUTH_PROVINCE_COMPANY,
      regionCodeVo,
    )

    if (!validationResult.isValid) {
      throw new BusinessException(validationResult.errorMessage)
    }

    // 3. 创建聚合根
    const authorization = AuthorizationRole.createAuthProvinceCompany({
      userId: userIdVo,
      provinceCode,
      provinceName,
    })

    // 4. 持久化
    await this.authorizationRepository.save(authorization)

    // 5. 发布领域事件
    await this.eventPublisher.publishAll(authorization.domainEvents)

    return authorization
  }
}

基础设施层Infrastructure Layer

仓储实现结构

// src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts

import { Injectable } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { IAuthorizationRoleRepository } from '@/domain/repositories'
import { AuthorizationRole } from '@/domain/aggregates'
import { AuthorizationId, UserId, RegionCode } from '@/domain/value-objects'

export const AUTHORIZATION_ROLE_REPOSITORY = Symbol('AUTHORIZATION_ROLE_REPOSITORY')

@Injectable()
export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleRepository {
  constructor(private readonly prisma: PrismaService) {}

  async save(authorization: AuthorizationRole): Promise<void> {
    const data = authorization.toPersistence()

    await this.prisma.authorizationRole.upsert({
      where: { id: data.id },
      create: data,
      update: data,
    })
  }

  async findById(id: AuthorizationId): Promise<AuthorizationRole | null> {
    const record = await this.prisma.authorizationRole.findUnique({
      where: { id: id.value },
    })

    if (!record) return null

    return this.toDomain(record)
  }

  private toDomain(record: any): AuthorizationRole {
    return AuthorizationRole.fromPersistence({
      id: AuthorizationId.create(record.id),
      userId: UserId.create(record.userId),
      // ... 映射其他属性
    })
  }
}

DDD 实践指南

1. 聚合根设计原则

  • 单一职责:每个聚合根只负责一个业务概念
  • 事务边界:聚合根是事务的边界,一次事务只修改一个聚合根
  • 不变量保护:聚合根负责保护业务不变量
  • 最小化聚合:聚合应该尽可能小

2. 值对象使用场景

适合使用值对象的场景:

  • ID 类型AuthorizationId, UserId
  • 度量和数量Money, Percentage
  • 时间相关Month, DateRange
  • 描述性数据Address, Email

3. 领域事件设计

// 事件命名:{聚合根}.{动作}
authorization.applied     // 授权申请
authorization.approved    // 授权通过
authorization.rejected    // 授权拒绝
authorization.activated   // 授权激活
authorization.revoked     // 授权撤销

assessment.passed         // 考核通过
assessment.failed         // 考核失败
assessment.bypassed       // 考核豁免

4. 仓储模式最佳实践

// 仓储接口只定义业务需要的方法
interface IAuthorizationRoleRepository {
  save(authorization: AuthorizationRole): Promise<void>
  findById(id: AuthorizationId): Promise<AuthorizationRole | null>
  findByUserId(userId: UserId): Promise<AuthorizationRole[]>
  findActiveByRoleTypeAndRegion(
    roleType: RoleType,
    regionCode: RegionCode,
  ): Promise<AuthorizationRole[]>
}

// 避免:
// - 暴露底层数据库细节
// - 返回原始数据库记录
// - 提供过于通用的查询方法

常见开发任务

添加新的授权类型

  1. RoleType 枚举中添加新类型
  2. AuthorizationRole 聚合根中添加工厂方法
  3. LadderTargetRule 中添加对应的考核目标
  4. 添加相应的 DTO 和控制器端点
  5. 编写单元测试和集成测试

添加新的领域事件

  1. src/domain/events 中创建事件类
  2. 在聚合根的相应方法中发布事件
  3. 在 Kafka 配置中注册事件主题
  4. (可选)创建事件处理器

修改数据库模型

  1. 修改 prisma/schema.prisma
  2. 生成迁移:npx prisma migrate dev --name describe_change
  3. 更新仓储实现中的映射逻辑
  4. 更新相应的 DTO

调试技巧

启用调试日志

// main.ts
const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn', 'log', 'debug', 'verbose'],
})

Prisma 查询日志

// prisma.service.ts
const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error'],
})

VSCode 调试配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug NestJS",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start:debug"],
      "console": "integratedTerminal",
      "restart": true,
      "protocol": "inspector",
      "port": 9229
    }
  ]
}

常用调试命令

# 查看 Prisma 生成的 SQL
DEBUG=prisma:query npm run start:dev

# 查看 Redis 操作
redis-cli monitor

# 查看 Kafka 消息
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic authorization-events --from-beginning

参考资源