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

21 KiB
Raw Blame History

Admin Service 开发指南

目录


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 推荐插件

{
  "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 工作区设置

{
  "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 克隆项目

git clone https://github.com/your-org/rwa-durian.git
cd rwa-durian/backend/services/admin-service

2.2 安装依赖

# 使用 npm
npm install

# 或使用 yarn
yarn install

# 或使用 pnpm
pnpm install

2.3 环境配置

创建 .env.development 文件:

# 应用配置
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

# 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

# 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 验证环境

# 检查数据库连接
npm run prisma:studio

# 运行测试
npm run test:unit

# 启动开发服务器
npm run start:dev

访问 http://localhost:3005/api/v1/health 应返回:

{"status": "ok"}

3. 开发流程

3.1 Git 工作流

分支策略

main (生产)
  ↑
develop (开发)
  ↑
feature/xxx (功能分支)
hotfix/xxx (紧急修复)

创建功能分支

# 从 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:

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

<body>

<footer>

Type 类型:

  • feat: 新功能
  • fix: Bug 修复
  • docs: 文档更新
  • style: 代码格式 (不影响功能)
  • refactor: 重构
  • test: 测试相关
  • chore: 构建/工具配置

示例:

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. 设计方案

领域层修改:

// 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 修改:

model AppVersion {
  // ... 现有字段
  deletedBy  String?
  deletedAt  DateTime?
}

3. 实现功能

步骤 1: 更新 Prisma Schema

# prisma/schema.prisma
# 添加 deletedBy 和 deletedAt 字段

# 创建迁移
npm run prisma:migrate:dev --name add_soft_delete

步骤 2: 更新领域实体

// 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: 创建命令和处理器

// 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: 添加控制器端点

// 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

// src/api/dtos/delete-version.dto.ts
export class DeleteVersionDto {
  @IsNotEmpty()
  @IsString()
  deletedBy: string;
}

4. 编写测试

单元测试:

// 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);
  });
});

集成测试:

// 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 测试:

// 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. 运行测试

# 单元测试
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. 本地验证

# 启动开发服务器
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. 代码审查

# 格式化代码
npm run format

# 检查代码规范
npm run lint

# 修复 lint 问题
npm run lint:fix

8. 提交代码

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 规范

类型定义

// ✅ 推荐: 显式类型注解
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;
}

命名规范

// ✅ 类名: 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;
}

导入顺序

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

// ✅ 推荐: 不可变、验证逻辑封装
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)

// ✅ 推荐: 封装业务逻辑、私有属性、工厂方法
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 接口

// ✅ 推荐: 领域层定义接口,基础设施层实现
// 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 规范

依赖注入

// ✅ 推荐: 构造函数注入
@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,
  ) {}
}

模块组织

// ✅ 推荐: 清晰的 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 定义

// ✅ 推荐: 清晰的模型定义和注释
/// 应用版本表
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")
}

查询优化

// ✅ 推荐: 使用索引字段查询
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:

{
  "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 日志调试

// 使用 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 数据库调试

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

### 创建版本
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 添加新的值对象

# 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 端点

# 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

# 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 性能优化

# 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 添加缓存层

// 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'

解决方案:

npm run prisma:generate

问题 2: 数据库连接失败

错误:

Error: P1001: Can't reach database server

解决方案:

# 检查数据库是否运行
docker ps | grep postgres

# 检查 DATABASE_URL 配置
echo $DATABASE_URL

# 测试连接
psql -U postgres -h localhost -p 5432 -d admin_service_dev

问题 3: 迁移冲突

错误:

Error: Migration failed

解决方案:

# 重置数据库 (开发环境)
npm run prisma:migrate:reset

# 或手动解决冲突
npx prisma migrate resolve --applied <migration_name>

7.2 性能分析

# 使用 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