14 KiB
14 KiB
Authorization Service 开发指南
目录
环境准备
系统要求
| 软件 | 版本 | 说明 |
|---|---|---|
| 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[]>
}
// 避免:
// - 暴露底层数据库细节
// - 返回原始数据库记录
// - 提供过于通用的查询方法
常见开发任务
添加新的授权类型
- 在
RoleType枚举中添加新类型 - 在
AuthorizationRole聚合根中添加工厂方法 - 在
LadderTargetRule中添加对应的考核目标 - 添加相应的 DTO 和控制器端点
- 编写单元测试和集成测试
添加新的领域事件
- 在
src/domain/events中创建事件类 - 在聚合根的相应方法中发布事件
- 在 Kafka 配置中注册事件主题
- (可选)创建事件处理器
修改数据库模型
- 修改
prisma/schema.prisma - 生成迁移:
npx prisma migrate dev --name describe_change - 更新仓储实现中的映射逻辑
- 更新相应的 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