616 lines
14 KiB
Markdown
616 lines
14 KiB
Markdown
# Authorization Service 开发指南
|
||
|
||
## 目录
|
||
|
||
1. [环境准备](#环境准备)
|
||
2. [项目初始化](#项目初始化)
|
||
3. [开发规范](#开发规范)
|
||
4. [代码结构约定](#代码结构约定)
|
||
5. [DDD 实践指南](#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
|
||
|
||
```bash
|
||
# 使用 nvm 安装 Node.js
|
||
nvm install 20
|
||
nvm use 20
|
||
```
|
||
|
||
#### 2. 启动基础设施
|
||
|
||
使用 Docker Compose 启动开发环境:
|
||
|
||
```bash
|
||
# 启动 PostgreSQL、Redis、Kafka
|
||
docker compose up -d
|
||
```
|
||
|
||
#### 3. 配置环境变量
|
||
|
||
复制环境变量模板:
|
||
|
||
```bash
|
||
cp .env.example .env.development
|
||
```
|
||
|
||
编辑 `.env.development`:
|
||
|
||
```env
|
||
# 数据库
|
||
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. 安装依赖
|
||
|
||
```bash
|
||
npm install
|
||
```
|
||
|
||
### 2. 生成 Prisma 客户端
|
||
|
||
```bash
|
||
npx prisma generate
|
||
```
|
||
|
||
### 3. 运行数据库迁移
|
||
|
||
```bash
|
||
npx prisma migrate dev
|
||
```
|
||
|
||
### 4. 启动开发服务器
|
||
|
||
```bash
|
||
npm run start:dev
|
||
```
|
||
|
||
服务将在 `http://localhost:3002` 启动。
|
||
|
||
### 5. 访问 API 文档
|
||
|
||
```
|
||
http://localhost:3002/api/docs
|
||
```
|
||
|
||
---
|
||
|
||
## 开发规范
|
||
|
||
### 代码风格
|
||
|
||
项目使用 ESLint + Prettier 进行代码规范检查:
|
||
|
||
```bash
|
||
# 检查代码风格
|
||
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 | 构建/工具变更 |
|
||
|
||
#### 示例
|
||
|
||
```bash
|
||
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)
|
||
|
||
#### 聚合根结构
|
||
|
||
```typescript
|
||
// 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,
|
||
// ...
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 值对象结构
|
||
|
||
```typescript
|
||
// 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
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 领域事件结构
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
#### 命令服务结构
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
#### 仓储实现结构
|
||
|
||
```typescript
|
||
// 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. 领域事件设计
|
||
|
||
```typescript
|
||
// 事件命名:{聚合根}.{动作}
|
||
authorization.applied // 授权申请
|
||
authorization.approved // 授权通过
|
||
authorization.rejected // 授权拒绝
|
||
authorization.activated // 授权激活
|
||
authorization.revoked // 授权撤销
|
||
|
||
assessment.passed // 考核通过
|
||
assessment.failed // 考核失败
|
||
assessment.bypassed // 考核豁免
|
||
```
|
||
|
||
### 4. 仓储模式最佳实践
|
||
|
||
```typescript
|
||
// 仓储接口只定义业务需要的方法
|
||
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
|
||
|
||
---
|
||
|
||
## 调试技巧
|
||
|
||
### 启用调试日志
|
||
|
||
```typescript
|
||
// main.ts
|
||
const app = await NestFactory.create(AppModule, {
|
||
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||
})
|
||
```
|
||
|
||
### Prisma 查询日志
|
||
|
||
```typescript
|
||
// prisma.service.ts
|
||
const prisma = new PrismaClient({
|
||
log: ['query', 'info', 'warn', 'error'],
|
||
})
|
||
```
|
||
|
||
### VSCode 调试配置
|
||
|
||
```json
|
||
// .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
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 常用调试命令
|
||
|
||
```bash
|
||
# 查看 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
|
||
```
|
||
|
||
---
|
||
|
||
## 参考资源
|
||
|
||
- [NestJS 官方文档](https://docs.nestjs.com/)
|
||
- [Prisma 官方文档](https://www.prisma.io/docs/)
|
||
- [领域驱动设计参考](https://www.domainlanguage.com/ddd/reference/)
|
||
- [TypeScript 风格指南](https://google.github.io/styleguide/tsguide.html)
|