// services/api/src/albums/support.ts —— 相册域服务共用小工具(Task 15 起自 // albums.service.ts 抽出,Task 17 起 members/categories 服务同用)。 import { HttpStatus } from '@nestjs/common'; import { ErrorCodes } from '@photo/contracts'; import { ApiException } from '../api-exception'; /** 统一 404:不可见(含不存在)同码防 id 枚举(契约 albums/categories 404 语义)。 */ export function albumNotFound(): ApiException { return new ApiException( ErrorCodes.ALBUM_NOT_FOUND, '相册不存在或不可见', HttpStatus.NOT_FOUND, ); } /** 统一 403:可见但无管理权限(协作者视角)。 */ export function albumForbidden(message = '仅创建者或管理员可操作'): ApiException { return new ApiException(ErrorCodes.AUTH_FORBIDDEN, message, HttpStatus.FORBIDDEN); } /** 统一 400(契约 VALIDATION_FAILED)。 */ export function validationFailed(message: string): ApiException { return new ApiException( ErrorCodes.VALIDATION_FAILED, message, HttpStatus.BAD_REQUEST, ); } /** * P2002 是否命中指定唯一索引。uniqueName 用 Prisma 唯一选择器名(单列 * 'publicToken' / 复合 'albumId_userId')。meta.target 两种形态都兼容: * 字段数组(部分引擎/版本)或 DB 约束名字符串(MySQL 实测形如 * 'albums_publicToken_key' / 'album_members_albumId_userId_key'—— * 表名前缀 + 选择器名 + _key 后缀,两种后缀都匹配)。 */ export function isUniqueViolationOn(err: unknown, uniqueName: string): boolean { if (typeof err !== 'object' || err === null) { return false; } const { code, meta } = err as { code?: unknown; meta?: { target?: unknown } }; if (code !== 'P2002') { return false; } const target = meta?.target; if (Array.isArray(target)) { return target.map(String).includes(uniqueName); } if (typeof target === 'string') { return ( target === uniqueName || target.endsWith(`${uniqueName}_key`) || target.endsWith(`_${uniqueName}_key`) ); } return false; }