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

616 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)