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