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

1062 lines
22 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.

# Admin Service 开发指南
## 目录
- [1. 开发环境设置](#1-开发环境设置)
- [2. 项目初始化](#2-项目初始化)
- [3. 开发流程](#3-开发流程)
- [4. 代码规范](#4-代码规范)
- [5. 调试技巧](#5-调试技巧)
- [6. 常见开发任务](#6-常见开发任务)
---
## 1. 开发环境设置
### 1.1 系统要求
| 工具 | 版本要求 | 说明 |
|-----|---------|------|
| **Node.js** | >= 20.x | 推荐使用 LTS 版本 |
| **npm** | >= 10.x | 或使用 yarn/pnpm |
| **PostgreSQL** | >= 16.x | 本地开发或 Docker |
| **Docker** | >= 24.x | (可选) 容器化开发 |
| **Git** | >= 2.x | 版本控制 |
| **VSCode** | 最新版 | 推荐 IDE |
### 1.2 VSCode 推荐插件
```json
{
"recommendations": [
"dbaeumer.vscode-eslint", // ESLint
"esbenp.prettier-vscode", // Prettier
"prisma.prisma", // Prisma
"firsttris.vscode-jest-runner", // Jest Runner
"orta.vscode-jest", // Jest
"ms-vscode.vscode-typescript-next", // TypeScript
"usernamehw.errorlens", // Error Lens
"eamodio.gitlens" // GitLens
]
}
```
保存到 `.vscode/extensions.json`
### 1.3 VSCode 工作区设置
```json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"typescript.preferences.importModuleSpecifier": "relative",
"jest.autoRun": "off",
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}
}
```
保存到 `.vscode/settings.json`
---
## 2. 项目初始化
### 2.1 克隆项目
```bash
git clone https://github.com/your-org/rwa-durian.git
cd rwa-durian/backend/services/admin-service
```
### 2.2 安装依赖
```bash
# 使用 npm
npm install
# 或使用 yarn
yarn install
# 或使用 pnpm
pnpm install
```
### 2.3 环境配置
创建 `.env.development` 文件:
```env
# 应用配置
NODE_ENV=development
APP_PORT=3005
API_PREFIX=api/v1
# 数据库配置
DATABASE_URL=postgresql://postgres:password@localhost:5432/admin_service_dev?schema=public
# 日志配置
LOG_LEVEL=debug
# CORS 配置
CORS_ORIGIN=http://localhost:3000,http://localhost:3001
```
**注意**: 不要提交 `.env.*` 文件到 Git已添加到 `.gitignore`
### 2.4 数据库初始化
#### 方案 1: 本地 PostgreSQL
```bash
# 1. 创建数据库
psql -U postgres -c "CREATE DATABASE admin_service_dev;"
# 2. 运行迁移
npm run prisma:migrate:dev
# 3. 生成 Prisma Client
npm run prisma:generate
```
#### 方案 2: Docker PostgreSQL
```bash
# 1. 启动数据库容器
docker run -d \
--name admin-dev-db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=admin_service_dev \
-p 5432:5432 \
postgres:16-alpine
# 2. 运行迁移
npm run prisma:migrate:dev
# 3. 生成 Prisma Client
npm run prisma:generate
```
### 2.5 验证环境
```bash
# 检查数据库连接
npm run prisma:studio
# 运行测试
npm run test:unit
# 启动开发服务器
npm run start:dev
```
访问 `http://localhost:3005/api/v1/health` 应返回:
```json
{"status": "ok"}
```
---
## 3. 开发流程
### 3.1 Git 工作流
#### 分支策略
```
main (生产)
develop (开发)
feature/xxx (功能分支)
hotfix/xxx (紧急修复)
```
#### 创建功能分支
```bash
# 从 develop 创建功能分支
git checkout develop
git pull origin develop
git checkout -b feature/add-version-delete
# 开发...
# 提交
git add .
git commit -m "feat(version): add delete version functionality"
# 推送
git push origin feature/add-version-delete
# 创建 Pull Request
```
#### Commit Message 规范
遵循 [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <subject>
<body>
<footer>
```
**Type 类型**:
- `feat`: 新功能
- `fix`: Bug 修复
- `docs`: 文档更新
- `style`: 代码格式 (不影响功能)
- `refactor`: 重构
- `test`: 测试相关
- `chore`: 构建/工具配置
**示例**:
```bash
git commit -m "feat(version): add soft delete for versions"
git commit -m "fix(repository): handle duplicate version code error"
git commit -m "docs(api): update version check endpoint documentation"
git commit -m "test(handler): add unit tests for EnableVersionHandler"
```
### 3.2 开发迭代流程
#### 1. 需求分析
**示例**: 添加版本删除功能
- 功能需求: 支持软删除版本记录
- 业务规则:
- 只能删除未启用的版本
- 删除后不可恢复
- 记录删除人和删除时间
#### 2. 设计方案
**领域层修改**:
```typescript
// domain/entities/app-version.entity.ts
class AppVersion {
delete(deletedBy: string): void {
if (this._isEnabled) {
throw new DomainException('Cannot delete an enabled version');
}
this._deletedBy = deletedBy;
this._deletedAt = new Date();
}
get isDeleted(): boolean {
return this._deletedAt !== null;
}
}
```
**Prisma Schema 修改**:
```prisma
model AppVersion {
// ... 现有字段
deletedBy String?
deletedAt DateTime?
}
```
#### 3. 实现功能
**步骤 1**: 更新 Prisma Schema
```bash
# prisma/schema.prisma
# 添加 deletedBy 和 deletedAt 字段
# 创建迁移
npm run prisma:migrate:dev --name add_soft_delete
```
**步骤 2**: 更新领域实体
```typescript
// src/domain/entities/app-version.entity.ts
export class AppVersion {
private _deletedBy: string | null = null;
private _deletedAt: Date | null = null;
delete(deletedBy: string): void {
if (this._isEnabled) {
throw new DomainException('Cannot delete an enabled version');
}
this._deletedBy = deletedBy;
this._deletedAt = new Date();
}
get isDeleted(): boolean {
return this._deletedAt !== null;
}
}
```
**步骤 3**: 创建命令和处理器
```typescript
// src/application/commands/delete-version.command.ts
export class DeleteVersionCommand {
constructor(
public readonly versionId: string,
public readonly deletedBy: string,
) {}
}
// src/application/handlers/delete-version.handler.ts
@Injectable()
export class DeleteVersionHandler {
constructor(
@Inject(APP_VERSION_REPOSITORY)
private readonly repository: AppVersionRepository,
) {}
async execute(command: DeleteVersionCommand): Promise<void> {
const version = await this.repository.findById(command.versionId);
if (!version) {
throw new ApplicationException('Version not found');
}
version.delete(command.deletedBy);
await this.repository.save(version);
}
}
```
**步骤 4**: 添加控制器端点
```typescript
// src/api/controllers/version.controller.ts
@Delete(':id')
async deleteVersion(
@Param('id') id: string,
@Body() dto: DeleteVersionDto,
): Promise<void> {
const command = new DeleteVersionCommand(id, dto.deletedBy);
await this.deleteVersionHandler.execute(command);
}
```
**步骤 5**: 添加 DTO
```typescript
// src/api/dtos/delete-version.dto.ts
export class DeleteVersionDto {
@IsNotEmpty()
@IsString()
deletedBy: string;
}
```
#### 4. 编写测试
**单元测试**:
```typescript
// test/unit/domain/entities/app-version.entity.spec.ts
describe('delete', () => {
it('should delete version when disabled', () => {
const version = AppVersion.create({...});
version.disable('admin');
version.delete('admin');
expect(version.isDeleted).toBe(true);
});
it('should throw error when deleting enabled version', () => {
const version = AppVersion.create({...});
expect(() => version.delete('admin')).toThrow(DomainException);
});
});
```
**集成测试**:
```typescript
// test/integration/handlers/delete-version.handler.spec.ts
describe('DeleteVersionHandler', () => {
it('should delete version successfully', async () => {
const version = await createTestVersion({ isEnabled: false });
const command = new DeleteVersionCommand(version.id, 'admin');
await handler.execute(command);
const deleted = await repository.findById(version.id);
expect(deleted.isDeleted).toBe(true);
});
});
```
**E2E 测试**:
```typescript
// test/e2e/version.controller.spec.ts
describe('/version/:id (DELETE)', () => {
it('should delete version successfully', () => {
return request(app.getHttpServer())
.delete(`/api/v1/version/${versionId}`)
.send({ deletedBy: 'admin' })
.expect(204);
});
});
```
#### 5. 运行测试
```bash
# 单元测试
npm run test:unit
# 集成测试 (需要数据库)
DATABASE_URL="postgresql://postgres:password@localhost:5432/admin_service_test" \
npm run test:integration
# E2E 测试 (需要数据库)
DATABASE_URL="postgresql://postgres:password@localhost:5432/admin_service_test" \
npm run test:e2e
# 全部测试 + 覆盖率
npm run test:cov
```
#### 6. 本地验证
```bash
# 启动开发服务器
npm run start:dev
# 测试 API
curl -X DELETE http://localhost:3005/api/v1/version/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{"deletedBy": "admin"}'
```
#### 7. 代码审查
```bash
# 格式化代码
npm run format
# 检查代码规范
npm run lint
# 修复 lint 问题
npm run lint:fix
```
#### 8. 提交代码
```bash
git add .
git commit -m "feat(version): add soft delete functionality
- Add deletedBy and deletedAt fields to AppVersion entity
- Implement delete business logic with validation
- Add DELETE /api/v1/version/:id endpoint
- Add comprehensive unit, integration, and E2E tests
- Update Prisma schema with migration
Closes #123"
git push origin feature/add-version-delete
```
---
## 4. 代码规范
### 4.1 TypeScript 规范
#### 类型定义
```typescript
// ✅ 推荐: 显式类型注解
function calculateFileSize(bytes: bigint): string {
return `${bytes} bytes`;
}
// ❌ 避免: 使用 any
function process(data: any) { // 不推荐
return data.value;
}
// ✅ 推荐: 使用具体类型
interface ProcessData {
value: string;
}
function process(data: ProcessData) {
return data.value;
}
```
#### 命名规范
```typescript
// ✅ 类名: PascalCase
class AppVersion {}
class VersionCheckService {}
// ✅ 接口名: PascalCase, 以 I 开头 (可选)
interface AppVersionRepository {}
interface IAppVersionRepository {} // 也可以
// ✅ 方法/变量: camelCase
const versionCode = 100;
function findLatestVersion() {}
// ✅ 常量: SCREAMING_SNAKE_CASE
const MAX_FILE_SIZE = 1024 * 1024 * 100; // 100MB
const DEFAULT_PAGE_SIZE = 10;
// ✅ 私有属性: 下划线前缀
class AppVersion {
private _versionCode: VersionCode;
private _isEnabled: boolean;
}
```
#### 导入顺序
```typescript
// 1. Node.js 内置模块
import * as path from 'path';
import * as fs from 'fs';
// 2. 外部依赖
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/infrastructure/prisma/prisma.service';
// 3. 内部模块 (绝对路径)
import { AppVersion } from '@/domain/entities/app-version.entity';
import { VersionCode } from '@/domain/value-objects/version-code.vo';
// 4. 相对路径导入
import { CreateVersionDto } from './dtos/create-version.dto';
import { VersionController } from './version.controller';
```
### 4.2 DDD 代码规范
#### 值对象 (Value Object)
```typescript
// ✅ 推荐: 不可变、验证逻辑封装
export class VersionCode {
private readonly value: number;
constructor(value: number) {
if (!Number.isInteger(value) || value < 1) {
throw new DomainException('Version code must be a positive integer');
}
this.value = value;
}
static create(value: number): VersionCode {
return new VersionCode(value);
}
getValue(): number {
return this.value;
}
equals(other: VersionCode): boolean {
return this.value === other.value;
}
}
```
#### 实体 (Entity)
```typescript
// ✅ 推荐: 封装业务逻辑、私有属性、工厂方法
export class AppVersion {
private readonly _id: string;
private _isEnabled: boolean;
private constructor(props: AppVersionProps) {
this._id = props.id;
this._isEnabled = props.isEnabled;
}
// 工厂方法
static create(props: CreateAppVersionProps): AppVersion {
// 验证逻辑
return new AppVersion({...});
}
// 业务方法
enable(updatedBy: string): void {
this._isEnabled = true;
this._updatedBy = updatedBy;
this._updatedAt = new Date();
}
// 查询方法
isEnabledVersion(): boolean {
return this._isEnabled;
}
}
```
#### Repository 接口
```typescript
// ✅ 推荐: 领域层定义接口,基础设施层实现
// domain/repositories/app-version.repository.ts
export interface AppVersionRepository {
save(version: AppVersion): Promise<AppVersion>;
findById(id: string): Promise<AppVersion | null>;
findLatestEnabledVersion(platform: Platform): Promise<AppVersion | null>;
}
// infrastructure/persistence/repositories/app-version.repository.impl.ts
@Injectable()
export class AppVersionRepositoryImpl implements AppVersionRepository {
constructor(private readonly prisma: PrismaService) {}
async save(version: AppVersion): Promise<AppVersion> {
// 实现逻辑
}
}
```
### 4.3 NestJS 规范
#### 依赖注入
```typescript
// ✅ 推荐: 构造函数注入
@Injectable()
export class VersionController {
constructor(
private readonly createVersionHandler: CreateVersionHandler,
private readonly enableVersionHandler: EnableVersionHandler,
) {}
}
// ✅ 推荐: 使用 @Inject 注入接口
@Injectable()
export class CreateVersionHandler {
constructor(
@Inject(APP_VERSION_REPOSITORY)
private readonly repository: AppVersionRepository,
) {}
}
```
#### 模块组织
```typescript
// ✅ 推荐: 清晰的 Provider 定义
@Module({
imports: [PrismaModule],
controllers: [VersionController],
providers: [
// Handlers
CreateVersionHandler,
EnableVersionHandler,
DisableVersionHandler,
// Services
VersionCheckService,
// Repositories
{
provide: APP_VERSION_REPOSITORY,
useClass: AppVersionRepositoryImpl,
},
],
exports: [APP_VERSION_REPOSITORY],
})
export class VersionModule {}
```
### 4.4 Prisma 规范
#### Schema 定义
```prisma
// ✅ 推荐: 清晰的模型定义和注释
/// 应用版本表
model AppVersion {
/// 主键 (UUID)
id String @id @default(uuid())
/// 平台 (android/ios)
platform String
/// 版本号 (整数, 递增)
versionCode Int
/// 版本名称 (语义化版本)
versionName String
/// 创建时间
createdAt DateTime @default(now())
/// 更新时间
updatedAt DateTime @updatedAt
/// 平台 + 版本号唯一索引
@@unique([platform, versionCode], name: "platform_versionCode")
/// 表名
@@map("AppVersion")
}
```
#### 查询优化
```typescript
// ✅ 推荐: 使用索引字段查询
await prisma.appVersion.findFirst({
where: {
platform: 'android',
isEnabled: true,
},
orderBy: {
versionCode: 'desc',
},
});
// ✅ 推荐: 选择必要字段
await prisma.appVersion.findMany({
select: {
id: true,
versionCode: true,
versionName: true,
},
});
// ❌ 避免: N+1 查询问题
// 如果需要关联查询,使用 include
```
---
## 5. 调试技巧
### 5.1 VSCode 调试配置
创建 `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug NestJS",
"runtimeArgs": [
"-r",
"ts-node/register",
"-r",
"tsconfig-paths/register"
],
"args": ["${workspaceFolder}/src/main.ts"],
"envFile": "${workspaceFolder}/.env.development",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": ["<node_internals>/**"]
},
{
"type": "node",
"request": "launch",
"name": "Jest Current File",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"${fileBasenameNoExtension}",
"--runInBand",
"--no-cache",
"--watchAll=false"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
}
]
}
```
### 5.2 日志调试
```typescript
// 使用 NestJS Logger
import { Logger } from '@nestjs/common';
@Injectable()
export class CreateVersionHandler {
private readonly logger = new Logger(CreateVersionHandler.name);
async execute(command: CreateVersionCommand): Promise<AppVersion> {
this.logger.log(`Creating version: ${command.platform} v${command.versionName}`);
this.logger.debug(`Command details: ${JSON.stringify(command)}`);
try {
const version = await this.repository.save(appVersion);
this.logger.log(`Version created successfully: ${version.id}`);
return version;
} catch (error) {
this.logger.error(`Failed to create version: ${error.message}`, error.stack);
throw error;
}
}
}
```
### 5.3 数据库调试
```bash
# Prisma Studio - 可视化数据库工具
npm run prisma:studio
# 查看生成的 SQL
DATABASE_URL="..." npx prisma migrate dev --create-only
# 直接连接数据库
psql -U postgres -d admin_service_dev
# 查看迁移状态
npx prisma migrate status
```
### 5.4 HTTP 请求调试
#### 使用 REST Client (VSCode 插件)
创建 `test.http`:
```http
### 创建版本
POST http://localhost:3005/api/v1/version
Content-Type: application/json
{
"platform": "android",
"versionCode": 100,
"versionName": "1.0.0",
"buildNumber": "1",
"downloadUrl": "https://cdn.example.com/app-v1.0.0.apk",
"fileSize": 52428800,
"fileSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"changelog": "Initial release",
"isEnabled": true,
"isForceUpdate": false,
"createdBy": "admin"
}
### 检查更新
GET http://localhost:3005/api/v1/version/check?platform=android&versionCode=99
```
---
## 6. 常见开发任务
### 6.1 添加新的值对象
```bash
# 1. 创建值对象文件
touch src/domain/value-objects/download-url.vo.ts
# 2. 实现值对象
# 3. 添加单元测试
touch test/unit/domain/value-objects/download-url.vo.spec.ts
# 4. 运行测试
npm run test:unit -- download-url.vo.spec
```
### 6.2 添加新的 API 端点
```bash
# 1. 创建 DTO
# src/api/dtos/update-version.dto.ts
# 2. 创建命令
# src/application/commands/update-version.command.ts
# 3. 创建处理器
# src/application/handlers/update-version.handler.ts
# 4. 添加控制器方法
# src/api/controllers/version.controller.ts
# 5. 添加测试
# test/e2e/version.controller.spec.ts
# 6. 验证
npm run start:dev
curl -X PATCH http://localhost:3005/api/v1/version/{id} -d '{...}'
```
### 6.3 修改数据库 Schema
```bash
# 1. 修改 prisma/schema.prisma
# 2. 创建迁移
npm run prisma:migrate:dev --name add_new_field
# 3. 生成 Prisma Client
npm run prisma:generate
# 4. 更新领域实体
# src/domain/entities/app-version.entity.ts
# 5. 更新 Mapper
# src/infrastructure/persistence/mappers/app-version.mapper.ts
# 6. 运行测试验证
npm run test
```
### 6.4 性能优化
```bash
# 1. 分析慢查询
# 启用 Prisma 查询日志
# prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
log = ["query", "info", "warn", "error"]
}
# 2. 添加数据库索引
# prisma/schema.prisma
model AppVersion {
// ...
@@index([platform, isEnabled, versionCode(sort: Desc)])
}
# 3. 创建迁移
npm run prisma:migrate:dev --name add_performance_index
```
### 6.5 添加缓存层
```typescript
// 1. 安装依赖
npm install @nestjs/cache-manager cache-manager
// 2. 配置缓存模块
// src/app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
@Module({
imports: [
CacheModule.register({
ttl: 300, // 5 分钟
max: 100, // 最多缓存 100 项
}),
],
})
export class AppModule {}
// 3. 使用缓存
@Injectable()
export class VersionCheckService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
async checkForUpdate(platform: Platform, versionCode: number) {
const cacheKey = `version:${platform}:${versionCode}`;
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return cached;
}
const result = await this.performCheck(platform, versionCode);
await this.cacheManager.set(cacheKey, result);
return result;
}
}
```
---
## 7. 故障排查
### 7.1 常见问题
#### 问题 1: Prisma Client 未生成
**错误**:
```
Cannot find module '@prisma/client'
```
**解决方案**:
```bash
npm run prisma:generate
```
#### 问题 2: 数据库连接失败
**错误**:
```
Error: P1001: Can't reach database server
```
**解决方案**:
```bash
# 检查数据库是否运行
docker ps | grep postgres
# 检查 DATABASE_URL 配置
echo $DATABASE_URL
# 测试连接
psql -U postgres -h localhost -p 5432 -d admin_service_dev
```
#### 问题 3: 迁移冲突
**错误**:
```
Error: Migration failed
```
**解决方案**:
```bash
# 重置数据库 (开发环境)
npm run prisma:migrate:reset
# 或手动解决冲突
npx prisma migrate resolve --applied <migration_name>
```
### 7.2 性能分析
```bash
# 使用 Prisma Studio 查看数据
npm run prisma:studio
# 分析查询性能
DATABASE_URL="..." npx prisma db execute \
--file analyze_queries.sql
# 使用 Node.js profiler
node --inspect-brk dist/main.js
# 访问 chrome://inspect
```
---
**最后更新**: 2025-12-03
**版本**: 1.0.0
**维护者**: RWA Durian Team