import { Body, Controller, Delete, Get, Headers, HttpCode, Inject, Param, Patch, Post, Put, Query, Req, } from '@nestjs/common'; import { ApiBearerAuth, ApiCreatedResponse, ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { bindStoreWatermarkRequestSchema, createWatermarkTemplateRequestSchema, listWatermarkTemplatesQuerySchema, updateWatermarkTemplateRequestSchema, type Workspace, } from '@atlas/contracts'; import { z } from 'zod'; import { ApiError } from '../common/api-error.js'; import type { AuthenticatedRequest } from '../auth/auth.guard.js'; import { CurrentWorkspace } from '../workspace/current-workspace.decorator.js'; import { WatermarksService } from './watermarks.service.js'; @ApiTags('Watermarks') @ApiBearerAuth('access-token') @Controller() export class WatermarksController { public constructor(@Inject(WatermarksService) private readonly watermarks: WatermarksService) {} @Get('watermark-fonts') @ApiOperation({ summary: '读取当前渲染服务可用字体' }) public async fonts() { return { data: await this.watermarks.fonts() }; } @Get('watermark-templates') @ApiOperation({ summary: '分页读取当前工作空间活动水印模板' }) public async list(@Query() query: unknown, @CurrentWorkspace() workspace: Workspace) { return this.watermarks.list(workspace.id, parse(listWatermarkTemplatesQuerySchema, query)); } @Get('watermark-templates/default') @ApiOperation({ summary: '读取当前工作空间的默认水印模板' }) public async getDefault(@CurrentWorkspace() workspace: Workspace) { return { data: await this.watermarks.getDefault(workspace.id) }; } @Get('watermark-templates/:templateId') @ApiOperation({ summary: '读取水印模板当前版本' }) public async get(@Param('templateId') templateId: string, @CurrentWorkspace() workspace: Workspace) { return { data: await this.watermarks.get(workspace.id, uuid('template_id', templateId)) }; } @Post('watermark-templates') @ApiCreatedResponse({ description: '已创建水印模板及首个渲染版本' }) public async create(@Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) { return (await this.watermarks.create(workspace.id, request.auth.userId, idempotencyKey(key), parse(createWatermarkTemplateRequestSchema, body))).body; } @Patch('watermark-templates/:templateId') @ApiOkResponse({ description: '已更新水印模板' }) public async update(@Param('templateId') templateId: string, @Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Headers('if-match') ifMatch: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) { return (await this.watermarks.update(workspace.id, request.auth.userId, uuid('template_id', templateId), idempotencyKey(key), revision(ifMatch), parse(updateWatermarkTemplateRequestSchema, body))).body; } @Put('watermark-templates/:templateId/default') @ApiOkResponse({ description: '已设为当前工作空间的默认水印' }) public async setDefault(@Param('templateId') templateId: string, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) { return (await this.watermarks.setDefault(workspace.id, request.auth.userId, uuid('template_id', templateId), idempotencyKey(key))).body; } @Delete('watermark-templates/:templateId') @HttpCode(204) @ApiNoContentResponse({ description: '已软删除模板并解除当前店铺绑定' }) public async remove(@Param('templateId') templateId: string, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) { await this.watermarks.remove(workspace.id, request.auth.userId, uuid('template_id', templateId), idempotencyKey(key)); } @Put('stores/:storeId/watermark-binding') @ApiOkResponse({ description: '已更新店铺推荐水印' }) public async bindStore(@Param('storeId') storeId: string, @Body() body: unknown, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) { const input = parse(bindStoreWatermarkRequestSchema, body) as { template_id: string | null }; return (await this.watermarks.bindStore(workspace.id, request.auth.userId, uuid('store_id', storeId), idempotencyKey(key), input.template_id)).body; } } function parse { 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 ReturnType extends { data: infer Data } ? Data : never; throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求', result.error?.issues.map((issue) => ({ field: issue.path.join('.'), code: 'INVALID_FIELD', message: issue.message }))); } function uuid(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 idempotencyKey(value: string | undefined) { if (!value) throw new ApiError(400, 'IDEMPOTENCY_KEY_REQUIRED', '请求缺少 Idempotency-Key'); return uuid('Idempotency-Key', value); } function revision(value: string | undefined) { const match = value?.match(/^"([1-9]\d*)"$/); if (!match) throw new ApiError(400, 'IF_MATCH_REQUIRED', '请求缺少或格式错误的 If-Match'); return Number(match[1]); }