Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ REDIS_PORT="6379"
REDIS_DB="0"
# REDIS_PASSWORD="xxx"o # 若 Redis 设置了密码,请取消注释并填写密码

COOKIE_MAX_AGE="604800" # 7 * 24 * 60 * 60
COOKIE_REMEMBER_ME_MAX_AGE="2592000" # 30 * 24 * 60 * 60
# COOKIE_DOMAIN=".xxx.com" # 若需跨子域名使用 Cookie,请取消注释并设置为你的主域名

R2_ACCESS_KEY_ID="xxx"
Expand Down
5 changes: 4 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "nest start --watch",
"build": "tsdown",
"build": "tsc --noEmit && tsdown",
"start": "node dist/main.cjs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down Expand Up @@ -45,7 +45,9 @@
"fastify": "catalog:backend",
"ioredis": "catalog:backend",
"joi": "catalog:backend",
"js-yaml": "catalog:backend",
"json-schema-faker": "catalog:backend",
"openapi-types": "catalog:backend",
"pg": "catalog:backend",
"reflect-metadata": "catalog:backend",
"resend": "catalog:backend",
Expand All @@ -62,6 +64,7 @@
"@swc/core": "catalog:backend",
"@types/bcrypt": "catalog:backend",
"@types/express": "catalog:backend",
"@types/js-yaml": "catalog:backend",
"@types/pg": "catalog:backend",
"prisma": "catalog:backend",
"source-map-support": "catalog:backend",
Expand Down
11 changes: 7 additions & 4 deletions apps/backend/src/api/api.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HttpModule } from '@nestjs/axios'
import { Module } from '@nestjs/common'
import { AuthModule } from '@/auth/auth.module'
import { PrismaModule } from '@/infra/prisma/prisma.module'
Expand All @@ -9,14 +10,16 @@ import { ApiController } from './api.controller'
import { ApiService } from './api.service'
import { GroupController } from './group.controller'
import { GroupService } from './group.service'
import { ImportController } from './import.controller'
import { ImportService } from './import.service'
import { ApiUtilsService } from './utils.service'
import { VersionController } from './version.controller'
import { VersionService } from './version.service'

@Module({
imports: [PrismaModule, AuthModule, PermissionModule, ProjectModule, UtilModule, SystemConfigModule],
controllers: [ApiController, GroupController, VersionController],
providers: [ApiService, ApiUtilsService, GroupService, VersionService],
exports: [ApiService, ApiUtilsService, GroupService, VersionService],
imports: [PrismaModule, AuthModule, PermissionModule, ProjectModule, UtilModule, SystemConfigModule, HttpModule],
controllers: [ApiController, GroupController, VersionController, ImportController],
providers: [ApiService, ApiUtilsService, GroupService, VersionService, ImportService],
exports: [ApiService, ApiUtilsService, GroupService, VersionService, ImportService],
})
export class ApiModule {}
39 changes: 39 additions & 0 deletions apps/backend/src/api/dto/import/import-openapi.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator'

/** 冲突处理策略 */
export enum ConflictStrategy {
/** 跳过冲突的 API */
SKIP = 'skip',
/** 覆盖现有 API */
OVERWRITE = 'overwrite',
/** 重命名新 API */
RENAME = 'rename',
}

/** 解析 OpenAPI 文档请求 DTO(URL 或内容方式) */
export class ParseOpenapiReqDto {
@IsOptional()
@IsString({ message: 'OpenAPI 文档内容必须是字符串' })
content?: string

@IsOptional()
@IsString({ message: 'OpenAPI 文档 URL 必须是字符串' })
url?: string
}

/** 执行导入请求 DTO */
export class ExecuteImportReqDto {
@IsString({ message: 'OpenAPI 文档内容必须是字符串' })
content: string

@IsEnum(ConflictStrategy, { message: '冲突处理策略必须是有效的枚举值' })
conflictStrategy: ConflictStrategy

@IsOptional()
@IsString({ message: '目标分组ID必须是字符串' })
targetGroupId?: string

@IsOptional()
@IsBoolean({ message: '是否自动创建分组必须是布尔值' })
createMissingGroups?: boolean
}
97 changes: 97 additions & 0 deletions apps/backend/src/api/dto/import/import-preview.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { APIMethod } from 'prisma/generated/client'

/** 解析后的 API 信息预览 */
export interface ParsedApiPreview {
/** 请求路径 */
path: string
/** 请求方法 */
method: APIMethod
/** API 名称 */
name: string
/** 所属分组名称(基于 OpenAPI tags) */
groupName: string
/** 描述 */
description?: string
/** 是否与现有 API 冲突 */
hasConflict: boolean
/** 冲突的现有 API ID */
conflictApiId?: string
}

/** 解析后的分组信息预览 */
export interface ParsedGroupPreview {
/** 分组名称 */
name: string
/** 分组描述 */
description?: string
/** 该分组下的 API 数量 */
apiCount: number
}

/** 统计信息 */
export interface ImportStats {
/** 总 API 数量 */
totalApis: number
/** 新 API 数量 */
newApis: number
/** 冲突 API 数量 */
conflictApis: number
}

/** OpenAPI 文档基本信息 */
export interface OpenApiInfo {
/** 文档标题 */
title: string
/** 文档版本 */
version: string
/** 文档描述 */
description?: string
}

/** 解析预览响应 DTO */
export class ImportPreviewResDto {
/** OpenAPI 文档基本信息 */
info: OpenApiInfo
/** 分组列表 */
groups: ParsedGroupPreview[]
/** API 列表 */
apis: ParsedApiPreview[]
/** 统计信息 */
stats: ImportStats
/** 原始 OpenAPI 文档内容(用于后续导入) */
content: string
}

/** 导入结果中的单个 API 信息 */
export interface ImportedApiResult {
/** API 名称 */
name: string
/** 请求路径 */
path: string
/** 请求方法 */
method: APIMethod
/** 导入状态 */
status: 'created' | 'updated' | 'skipped' | 'failed'
/** 失败原因 */
error?: string
/** 创建的 API ID */
apiId?: string
}

/** 导入执行结果响应 DTO */
export class ImportResultResDto {
/** 是否成功 */
success: boolean
/** 导入的 API 结果列表 */
results: ImportedApiResult[]
/** 成功创建数量 */
createdCount: number
/** 成功更新数量 */
updatedCount: number
/** 跳过数量 */
skippedCount: number
/** 失败数量 */
failedCount: number
/** 创建的分组名称列表 */
createdGroups: string[]
}
2 changes: 2 additions & 0 deletions apps/backend/src/api/dto/import/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './import-openapi.dto'
export * from './import-preview.dto'
1 change: 1 addition & 0 deletions apps/backend/src/api/dto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './clone-api.dto'
export * from './create-api.dto'
export * from './get-apis.dto'
export * from './group'
export * from './import'
export * from './operation-log.dto'
export * from './sort-items.dto'
export * from './update-api.dto'
Expand Down
68 changes: 68 additions & 0 deletions apps/backend/src/api/import.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { FastifyRequest } from 'fastify'
import { Body, Controller, Param, Post, Req, UseGuards } from '@nestjs/common'
import { ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'
import { ProjectPermissions, RequireProjectMember } from '@/common/decorators/permissions.decorator'
import { ReqUser } from '@/common/decorators/req-user.decorator'
import { ResMsg } from '@/common/decorators/res-msg.decorator'
import { AuthGuard } from '@/common/guards/auth.guard'
import { PermissionsGuard } from '@/common/guards/permissions.guard'
import { ExecuteImportReqDto, ParseOpenapiReqDto } from './dto'
import { ImportService } from './import.service'

@ApiTags('OpenAPI 导入')
@Controller('api/:projectId/import')
@UseGuards(AuthGuard, PermissionsGuard)
export class ImportController {
constructor(private readonly importService: ImportService) {}

@Post('openapi/parse')
@RequireProjectMember()
@ProjectPermissions(['api:create'], 'projectId')
@ResMsg('OpenAPI 文档解析成功')
@ApiOperation({ summary: '解析 OpenAPI 文档' })
@ApiParam({ name: 'projectId', description: '项目ID' })
@ApiConsumes('multipart/form-data', 'application/json')
@ApiBody({
description: '上传 OpenAPI 文件或提供内容/URL',
schema: {
type: 'object',
properties: {
file: { type: 'string', format: 'binary', description: 'OpenAPI 文件 (JSON/YAML)' },
content: { type: 'string', description: 'OpenAPI 文档内容' },
url: { type: 'string', description: 'OpenAPI 文档 URL' },
},
},
})
async parseOpenapi(
@Param('projectId') projectId: string,
@Req() request: FastifyRequest,
@Body() dto: ParseOpenapiReqDto,
) {
let fileContent: string | undefined

// 如果是上传文件,直接读取内容
if (request.isMultipart()) {
const file = await request.file()
if (file) {
const buffer = await file.toBuffer()
fileContent = buffer.toString('utf-8')
}
}

return this.importService.parseOpenapi(dto, projectId, fileContent)
}

@Post('openapi/execute')
@RequireProjectMember()
@ProjectPermissions(['api:create'], 'projectId')
@ResMsg('OpenAPI 文档导入成功')
@ApiOperation({ summary: '执行 OpenAPI 导入' })
@ApiParam({ name: 'projectId', description: '项目ID' })
async executeImport(
@Param('projectId') projectId: string,
@Body() dto: ExecuteImportReqDto,
@ReqUser('id') userId: string,
) {
return this.importService.executeImport(dto, projectId, userId)
}
}
Loading
Loading