feat(edit): 服务端代码拆分、优化

This commit is contained in:
R524809
2026-09-15 18:56:13 +08:00
parent da7342b466
commit 7f68aad9f3
15 changed files with 1635 additions and 586 deletions
+218 -32
View File
@@ -1,6 +1,27 @@
import { Body, Controller, Get, Headers, Inject, Param, Patch, Post, Query, Req, Res } from '@nestjs/common';
import {
Body,
Controller,
Get,
Headers,
Inject,
Param,
Patch,
Post,
Query,
Req,
Res,
} from '@nestjs/common';
import { z } from 'zod';
import { bulkDeleteCollectionItemsRequestSchema, clearCollectionItemsRequestSchema, createCollectionItemRequestSchema, listCollectionItemsQuerySchema, materializeCollectionAssetsRequestSchema, retryCollectionAssetsRequestSchema, updateCollectionItemRequestSchema, type Workspace } from '@atlas/contracts';
import {
bulkDeleteCollectionItemsRequestSchema,
clearCollectionItemsRequestSchema,
createCollectionItemRequestSchema,
listCollectionItemsQuerySchema,
materializeCollectionAssetsRequestSchema,
retryCollectionAssetsRequestSchema,
updateCollectionItemRequestSchema,
type Workspace,
} from '@atlas/contracts';
import { ApiError } from '../common/api-error.js';
import type { AuthenticatedRequest } from '../auth/auth.guard.js';
import { CurrentWorkspace } from '../workspace/current-workspace.decorator.js';
@@ -9,34 +30,199 @@ import { CollectionService } from './collection.service.js';
type Response = { status: (value: number) => Response };
@Controller('collection-items')
export class CollectionController {
public constructor(@Inject(CollectionService) private readonly collection: CollectionService) {}
@Get() async list(@Query() query: unknown, @Headers('x-atlas-platform-id') platformId: string | undefined, @CurrentWorkspace() workspace: Workspace) {
if (!platformId) throw new ApiError(400, 'PLATFORM_CONTEXT_REQUIRED', '请求缺少 X-Atlas-Platform-Id');
return this.collection.list(workspace.id, requireUuid('X-Atlas-Platform-Id', platformId), parse(listCollectionItemsQuerySchema, query));
}
@Get(':id') async detail(@Param('id') id: string, @CurrentWorkspace() workspace: Workspace) {
return { data: await this.collection.detail(workspace.id, requireUuid('collection_item_id', id)) };
}
@Post() async create(@Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response) {
const result = await this.collection.create(workspace.id, request.auth.userId, requireKey(key), parse(createCollectionItemRequestSchema, body)); response.status(result.status); return result.body;
}
@Patch(':id') async update(@Param('id') id: string, @Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Headers('if-match') match: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) {
return (await this.collection.update(workspace.id, request.auth.userId, requireUuid('collection_item_id', id), requireKey(key), revision(match), parse(updateCollectionItemRequestSchema, body))).body;
}
@Post('bulk-delete') async bulkDelete(@Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response) {
const result = await this.collection.bulkDelete(workspace.id, request.auth.userId, requireKey(key), parse(bulkDeleteCollectionItemsRequestSchema, body)); response.status(result.status); return result.body;
}
@Post('clear') async clear(@Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response) {
const result = await this.collection.clear(workspace.id, request.auth.userId, requireKey(key), parse(clearCollectionItemsRequestSchema, body)); response.status(result.status); return result.body;
}
@Post(':id/asset-retries') async retry(@Param('id') id: string, @Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response) {
const result = await this.collection.retryAssets(workspace.id, request.auth.userId, requireUuid('collection_item_id', id), requireKey(key), parse(retryCollectionAssetsRequestSchema, body)); response.status(result.status); return result.body;
}
@Post(':id/asset-materializations') async materialize(@Param('id') id: string, @Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response) {
const result = await this.collection.materializeAssets(workspace.id, request.auth.userId, requireUuid('collection_item_id', id), requireKey(key), parse(materializeCollectionAssetsRequestSchema, body)); response.status(result.status); return result.body;
}
/** 注入采集箱接口共用的应用服务。 */
public constructor(
@Inject(CollectionService)
private readonly collection: CollectionService,
) {}
/** 按页头平台上下文查询当前工作空间的采集箱。 */
@Get() async list(
@Query() query: unknown,
@Headers('x-atlas-platform-id') platformId: string | undefined,
@CurrentWorkspace() workspace: Workspace,
) {
if (!platformId)
throw new ApiError(
400,
'PLATFORM_CONTEXT_REQUIRED',
'请求缺少 X-Atlas-Platform-Id',
);
return this.collection.list(
workspace.id,
requireUuid('X-Atlas-Platform-Id', platformId),
parse(listCollectionItemsQuerySchema, query),
);
}
/** 返回采集记录、解析证据、SKU 与当前快照的素材任务。 */
@Get(':id') async detail(
@Param('id') id: string,
@CurrentWorkspace() workspace: Workspace,
) {
return {
data: await this.collection.detail(
workspace.id,
requireUuid('collection_item_id', id),
),
};
}
/** 幂等地创建手工采集草稿及其默认商品和 SKU。 */
@Post() async create(
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: Response,
) {
const result = await this.collection.create(
workspace.id,
request.auth.userId,
requireKey(key),
parse(createCollectionItemRequestSchema, body),
);
response.status(result.status);
return result.body;
}
/** 通过 If-Match 乐观锁更新当前允许编辑的采集记录字段。 */
@Patch(':id') async update(
@Param('id') id: string,
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Headers('if-match') match: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
) {
return (
await this.collection.update(
workspace.id,
request.auth.userId,
requireUuid('collection_item_id', id),
requireKey(key),
revision(match),
parse(updateCollectionItemRequestSchema, body),
)
).body;
}
/** 幂等地软删除用户指定的多条采集记录。 */
@Post('bulk-delete') async bulkDelete(
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: Response,
) {
const result = await this.collection.bulkDelete(
workspace.id,
request.auth.userId,
requireKey(key),
parse(bulkDeleteCollectionItemsRequestSchema, body),
);
response.status(result.status);
return result.body;
}
/** 在数量确认未过期时,幂等地清空当前工作空间的采集箱。 */
@Post('clear') async clear(
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: Response,
) {
const result = await this.collection.clear(
workspace.id,
request.auth.userId,
requireKey(key),
parse(clearCollectionItemsRequestSchema, body),
);
response.status(result.status);
return result.body;
}
/** 将当前采集快照中失败或取消的素材任务重新投递给 Worker。 */
@Post(':id/asset-retries') async retry(
@Param('id') id: string,
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: Response,
) {
const result = await this.collection.retryAssets(
workspace.id,
request.auth.userId,
requireUuid('collection_item_id', id),
requireKey(key),
parse(retryCollectionAssetsRequestSchema, body),
);
response.status(result.status);
return result.body;
}
/** 将当前采集快照中用户选定的来源素材请求转存为 Atlas 素材。 */
@Post(':id/asset-materializations') async materialize(
@Param('id') id: string,
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: Response,
) {
const result = await this.collection.materializeAssets(
workspace.id,
request.auth.userId,
requireUuid('collection_item_id', id),
requireKey(key),
parse(materializeCollectionAssetsRequestSchema, body),
);
response.status(result.status);
return result.body;
}
}
/** 使用契约 Schema 校验请求体或查询参数,并转换为统一错误响应。 */
function parse<
T extends {
safeParse: (input: unknown) => {
success: boolean;
data?: unknown;
error?: { issues: Array<{ path: PropertyKey[]; message: string }> };
};
},
>(schema: T, input: unknown) {
const result = schema.safeParse(input);
if (result.success) return result.data as never;
throw new ApiError(
422,
'VALIDATION_FAILED',
'请求参数不符合要求',
result.error?.issues.map((x) => ({
field: x.path.join('.'),
code: 'INVALID_FIELD',
message: x.message,
})),
);
}
/** 校验路径、请求头中的 UUID 标识。 */
function requireUuid(field: string, value: string) {
if (z.uuid().safeParse(value).success) return value;
throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求', [
{ field, code: 'INVALID_FIELD', message: `${field} 必须是 UUID` },
]);
}
/** 要求关键写操作提供可追踪的幂等键。 */
function requireKey(value: string | undefined) {
if (!value)
throw new ApiError(
400,
'IDEMPOTENCY_KEY_REQUIRED',
'请求缺少 Idempotency-Key',
);
return requireUuid('Idempotency-Key', value);
}
/** 解析单资源更新使用的 If-Match 版本号。 */
function revision(value: string | undefined) {
const valueMatch = value?.match(/^"([1-9]\d*)"$/);
if (!valueMatch)
throw new ApiError(
400,
'IF_MATCH_REQUIRED',
'请求缺少或格式错误的 If-Match',
);
return Number(valueMatch[1]);
}
function parse<T extends { safeParse: (input: unknown) => { success: boolean; data?: unknown; error?: { issues: Array<{ path: PropertyKey[]; message: string }> } } }>(schema: T, input: unknown) { const result = schema.safeParse(input); if (result.success) return result.data as never; throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求', result.error?.issues.map((x) => ({ field: x.path.join('.'), code: 'INVALID_FIELD', message: x.message }))); }
function requireUuid(field: string, value: string) { if (z.uuid().safeParse(value).success) return value; throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求', [{ field, code: 'INVALID_FIELD', message: `${field} 必须是 UUID` }]); }
function requireKey(value: string | undefined) { if (!value) throw new ApiError(400, 'IDEMPOTENCY_KEY_REQUIRED', '请求缺少 Idempotency-Key'); return requireUuid('Idempotency-Key', value); }
function revision(value: string | undefined) { const valueMatch = value?.match(/^"([1-9]\d*)"$/); if (!valueMatch) throw new ApiError(400, 'IF_MATCH_REQUIRED', '请求缺少或格式错误的 If-Match'); return Number(valueMatch[1]); }