import { Body, Controller, Get, Headers, Inject, Param, Post, Put, Query, Req, Res, } from '@nestjs/common'; import { z } from 'zod'; import { saveProductEditorDraftRequestSchema, 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'; import { ProductEditorService } from './product-editor.service.js'; type Response = { status: (value: number) => Response }; @Controller() export class ProductEditorController { /** 注入所有商品编辑接口共用的工作流服务。 */ public constructor( @Inject(ProductEditorService) private readonly editor: ProductEditorService, ) {} /** 为采集记录创建或复用当前平台的商品编辑上下文。 */ @Post('collection-items/:collectionItemId/editor-contexts') async enter( @Param('collectionItemId') collectionItemId: string, @Headers('x-atlas-platform-id') platformId: string | undefined, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response, ) { const result = await this.editor.enter( workspace.id, request.auth.userId, requiredUuid('collection_item_id', collectionItemId), requiredUuid('X-Atlas-Platform-Id', platformId), requiredUuid('Idempotency-Key', key), ); response.status(result.status); return result.body; } /** 返回当前平台下完整的商品编辑读模型。 */ @Get('products/:productId/editor') async read( @Param('productId') productId: string, @Headers('x-atlas-platform-id') platformId: string | undefined, @CurrentWorkspace() workspace: Workspace, ) { return { data: await this.editor.read( workspace.id, requiredUuid('product_id', productId), requiredUuid('X-Atlas-Platform-Id', platformId), ), }; } /** 返回目标店铺已缓存的类目树,供编辑器选择。 */ @Get('product-platform-profiles/:profileId/category-options') async categoryOptions( @Param('profileId') profileId: string, @Query('q') query: string | undefined, @Query('store_id') storeId: string | undefined, @Headers('x-atlas-platform-id') platformId: string | undefined, @CurrentWorkspace() workspace: Workspace, ) { return { data: await this.editor.categoryOptions( workspace.id, requiredUuid('profile_id', profileId), requiredUuid('X-Atlas-Platform-Id', platformId), query, optionalUuid('store_id', storeId), ), }; } /** 返回所选类目的缓存动态属性定义。 */ @Get('product-platform-profiles/:profileId/attribute-schema') async attributeSchema( @Param('profileId') profileId: string, @Query('category_id') categoryId: string | undefined, @Query('store_id') storeId: string | undefined, @Headers('x-atlas-platform-id') platformId: string | undefined, @CurrentWorkspace() workspace: Workspace, ) { return { data: await this.editor.attributeSchema( workspace.id, requiredUuid('profile_id', profileId), requiredUuid('X-Atlas-Platform-Id', platformId), optionalUuid('category_id', categoryId), optionalUuid('store_id', storeId), ), }; } /** 返回首次上架前的服务端发布就绪校验。 */ @Get('product-platform-profiles/:profileId/validation') async validation( @Param('profileId') profileId: string, @Headers('x-atlas-platform-id') platformId: string | undefined, @CurrentWorkspace() workspace: Workspace, ) { return { data: await this.editor.validation( workspace.id, requiredUuid('profile_id', profileId), requiredUuid('X-Atlas-Platform-Id', platformId), ), }; } /** 幂等地创建所选类目属性定义的刷新任务。 */ @Post('product-platform-profiles/:profileId/attribute-schema-syncs') async attributeSchemaSync( @Param('profileId') profileId: string, @Query('category_id') categoryId: string | undefined, @Headers('x-atlas-platform-id') platformId: string | undefined, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response, ) { const result = await this.editor.requestAttributeSchemaSync( workspace.id, request.auth.userId, requiredUuid('profile_id', profileId), requiredUuid('X-Atlas-Platform-Id', platformId), requiredUuid('Idempotency-Key', key), requiredUuid('category_id', categoryId), ); response.status(result.status); return result.body; } /** 从本地缓存中查询属性字典值。 */ @Get( 'product-platform-profiles/:profileId/attribute-dictionaries/:dictionaryId', ) async attributeDictionary( @Param('profileId') profileId: string, @Param('dictionaryId') dictionaryId: string, @Query('q') query: string | undefined, @Query('cursor') cursor: string | undefined, @Query('category_id') categoryId: string | undefined, @Query('store_id') storeId: string | undefined, @Headers('x-atlas-platform-id') platformId: string | undefined, @CurrentWorkspace() workspace: Workspace, ) { if (!dictionaryId.trim() || dictionaryId.length > 200) throw new ApiError(400, 'VALIDATION_FAILED', '字典标识不合法'); return { data: await this.editor.attributeDictionary( workspace.id, requiredUuid('profile_id', profileId), requiredUuid('X-Atlas-Platform-Id', platformId), dictionaryId, query, cursor, optionalUuid('category_id', categoryId), optionalUuid('store_id', storeId), ), }; } /** 本地缓存不足时幂等地创建远程字典查询任务。 */ @Post( 'product-platform-profiles/:profileId/attribute-dictionaries/:dictionaryId/syncs', ) async attributeDictionarySync( @Param('profileId') profileId: string, @Param('dictionaryId') dictionaryId: string, @Query('category_id') categoryId: string | undefined, @Query('q') query: string | undefined, @Headers('x-atlas-platform-id') platformId: string | undefined, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response, ) { if (!dictionaryId.trim() || dictionaryId.length > 200) throw new ApiError(400, 'VALIDATION_FAILED', '字典标识不合法'); const result = await this.editor.requestAttributeDictionarySync( workspace.id, request.auth.userId, requiredUuid('profile_id', profileId), requiredUuid('X-Atlas-Platform-Id', platformId), requiredUuid('Idempotency-Key', key), requiredUuid('category_id', categoryId), dictionaryId, query ?? '', ); response.status(result.status); return result.body; } /** 校验并原子保存商品及其上架草稿。 */ @Put('products/:productId/editor-draft') async save( @Param('productId') productId: string, @Body() body: unknown, @Headers('x-atlas-platform-id') platformId: string | undefined, @Headers('idempotency-key') key: string | undefined, @Headers('if-match') ifMatch: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, ) { const parsed = saveProductEditorDraftRequestSchema.safeParse(body); if (!parsed.success) { throw new ApiError( 422, 'VALIDATION_FAILED', '请求参数不符合要求', parsed.error.issues.map((issue) => ({ field: issue.path.join('.'), code: 'INVALID_FIELD', message: issue.message, })), ); } return ( await this.editor.save( workspace.id, request.auth.userId, requiredUuid('product_id', productId), requiredUuid('X-Atlas-Platform-Id', platformId), requiredUuid('Idempotency-Key', key), parseEditorRevision(ifMatch), parsed.data, ) ).body; } } /** 校验路由参数或请求头中的 UUID,并返回原始值。 */ function requiredUuid(field: string, value: string | undefined) { if (value && z.uuid().safeParse(value).success) return value; throw new ApiError(400, 'VALIDATION_FAILED', '请求参数不符合要求', [ { field, code: 'INVALID_FIELD', message: `${field} 必须是 UUID` }, ]); } /** 将缺省查询参数保留为 undefined;有值时校验其 UUID 格式。 */ function optionalUuid(field: string, value: string | undefined) { if (value === undefined || value === '') return undefined; return requiredUuid(field, value); } /** 解析 If-Match 中成对传递的商品和发布配置版本号。 */ function parseEditorRevision(value: string | undefined) { const match = value?.match(/^"([1-9]\d*):([1-9]\d*)"$/); if (!match) throw new ApiError( 400, 'IF_MATCH_REQUIRED', '请求缺少或格式错误的 If-Match', ); return { product: Number(match[1]), profile: Number(match[2]) }; }