bdf80e577b
CI / verify (pull_request) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
261 lines
8.4 KiB
TypeScript
261 lines
8.4 KiB
TypeScript
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
import type { ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
|
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
|
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
|
import { logOssUpload, type OssActorRef } from '../../integrations/oss/oss-log.util';
|
|
import type { ResourceListQueryDto } from './dto/common-query.dto';
|
|
import type { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
|
|
|
|
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
|
|
export type OssUploadActor = OssActorRef & {
|
|
clientApp?: string;
|
|
};
|
|
|
|
@Injectable()
|
|
export class ResourceService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
|
) {}
|
|
|
|
getUploadToken(dto: UploadTokenDto, actor?: OssUploadActor) {
|
|
try {
|
|
const result = this.oss.getUploadToken(dto);
|
|
void logOssUpload(this.prisma, {
|
|
scene: 'UPLOAD_TOKEN',
|
|
actorRef: actor,
|
|
requestBody: {
|
|
bizType: dto.bizType,
|
|
mediaType: dto.mediaType,
|
|
fileName: dto.fileName,
|
|
clientApp: actor?.clientApp,
|
|
},
|
|
responseBody: {
|
|
bucket: result.bucket,
|
|
ossKey: result.ossKey,
|
|
url: result.url,
|
|
mock: result.mock ?? false,
|
|
},
|
|
externalNo: result.ossKey,
|
|
status: 'SUCCESS',
|
|
});
|
|
return result;
|
|
} catch (err) {
|
|
void logOssUpload(this.prisma, {
|
|
scene: 'UPLOAD_TOKEN',
|
|
actorRef: actor,
|
|
requestBody: {
|
|
bizType: dto.bizType,
|
|
mediaType: dto.mediaType,
|
|
fileName: dto.fileName,
|
|
clientApp: actor?.clientApp,
|
|
},
|
|
status: 'FAILED',
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
});
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async uploadFile(
|
|
file: Express.Multer.File | undefined,
|
|
dto: UploadFileDto,
|
|
actor?: OssUploadActor,
|
|
) {
|
|
const baseRequest = {
|
|
bizType: dto.bizType,
|
|
mediaType: dto.mediaType,
|
|
clientApp: actor?.clientApp,
|
|
};
|
|
|
|
if (!file) {
|
|
await logOssUpload(this.prisma, {
|
|
scene: 'UPLOAD_PUT_OBJECT',
|
|
actorRef: actor,
|
|
requestBody: baseRequest,
|
|
status: 'FAILED',
|
|
errorMessage: '请选择要上传的文件',
|
|
});
|
|
throw new BadRequestException('请选择要上传的文件');
|
|
}
|
|
|
|
const maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
|
|
const requestBody = {
|
|
...baseRequest,
|
|
fileName: file.originalname || 'upload.bin',
|
|
fileSize: file.size,
|
|
mimeType: file.mimetype,
|
|
};
|
|
|
|
if (file.size > maxUploadBytes) {
|
|
const message = `文件不能超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB`;
|
|
await logOssUpload(this.prisma, {
|
|
scene: 'UPLOAD_PUT_OBJECT',
|
|
actorRef: actor,
|
|
requestBody,
|
|
status: 'FAILED',
|
|
errorMessage: message,
|
|
});
|
|
throw new BadRequestException(message);
|
|
}
|
|
if (dto.bizType === 'AVATAR' && !file.mimetype?.startsWith('image/')) {
|
|
throw new BadRequestException('头像仅支持图片文件');
|
|
}
|
|
|
|
try {
|
|
const result = await this.oss.putObject({
|
|
bizType: dto.bizType,
|
|
mediaType: dto.mediaType,
|
|
fileName: file.originalname || 'upload.bin',
|
|
buffer: file.buffer,
|
|
mimeType: file.mimetype,
|
|
});
|
|
await logOssUpload(this.prisma, {
|
|
scene: 'UPLOAD_PUT_OBJECT',
|
|
actorRef: actor,
|
|
requestBody,
|
|
responseBody: {
|
|
bucket: result.bucket,
|
|
region: result.region,
|
|
ossKey: result.ossKey,
|
|
url: result.url,
|
|
mock: result.mock ?? false,
|
|
},
|
|
externalNo: result.ossKey,
|
|
status: 'SUCCESS',
|
|
});
|
|
if (actor?.refType === 'USER' && dto.bizType === 'AVATAR' && dto.mediaType === 'IMAGE') {
|
|
const resource = await this.prisma.commonResource.create({
|
|
data: {
|
|
ownerType: 'USER',
|
|
ownerId: actor.refId,
|
|
bizType: 'AVATAR',
|
|
mediaType: 'IMAGE',
|
|
ossBucket: result.bucket,
|
|
ossKey: result.ossKey,
|
|
url: result.url,
|
|
fileName: file.originalname || 'avatar',
|
|
fileSize: BigInt(file.size),
|
|
mimeType: file.mimetype,
|
|
status: 'ACTIVE',
|
|
},
|
|
});
|
|
return serializeBigInt({ ...result, resourceId: resource.id });
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
await logOssUpload(this.prisma, {
|
|
scene: 'UPLOAD_PUT_OBJECT',
|
|
actorRef: actor,
|
|
requestBody,
|
|
status: 'FAILED',
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
});
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async getOwnedActiveAvatar(resourceId: bigint, userId: bigint) {
|
|
const resource = await this.prisma.commonResource.findFirst({
|
|
where: {
|
|
id: resourceId,
|
|
ownerType: 'USER',
|
|
ownerId: userId,
|
|
bizType: 'AVATAR',
|
|
mediaType: 'IMAGE',
|
|
status: 'ACTIVE',
|
|
},
|
|
});
|
|
if (!resource) throw new BadRequestException('头像资源无效或不属于当前用户');
|
|
return resource;
|
|
}
|
|
|
|
async getOwnedActiveAvatarByUrl(url: string, userId: bigint) {
|
|
const resource = await this.prisma.commonResource.findFirst({
|
|
where: {
|
|
url,
|
|
ownerType: 'USER',
|
|
ownerId: userId,
|
|
bizType: 'AVATAR',
|
|
mediaType: 'IMAGE',
|
|
status: 'ACTIVE',
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
if (!resource) throw new BadRequestException('头像资源无效或不属于当前用户');
|
|
return resource;
|
|
}
|
|
|
|
async register(dto: RegisterResourceDto) {
|
|
const resource = await this.prisma.commonResource.create({
|
|
data: {
|
|
ownerType: dto.ownerType as ResourceOwnerType,
|
|
ownerId: BigInt(dto.ownerId),
|
|
bizType: dto.bizType as ResourceBizType,
|
|
mediaType: dto.mediaType as ResourceMediaType,
|
|
ossBucket: dto.ossBucket ?? process.env.OSS_BUCKET ?? 'mock-dukang',
|
|
ossKey: dto.ossKey,
|
|
url: dto.url || this.oss.buildPublicUrl(dto.ossKey),
|
|
fileName: dto.fileName,
|
|
sortOrder: dto.sortOrder ?? 0,
|
|
},
|
|
});
|
|
return serializeBigInt(resource);
|
|
}
|
|
|
|
async list(query: ResourceListQueryDto) {
|
|
const page = query.page ?? 1;
|
|
const pageSize = query.pageSize ?? 20;
|
|
const where: Prisma.CommonResourceWhereInput = {
|
|
status: (query.status ?? 'ACTIVE') as Prisma.EnumResourceStatusFilter['equals'],
|
|
};
|
|
if (query.ownerType) where.ownerType = query.ownerType as Prisma.EnumResourceOwnerTypeFilter['equals'];
|
|
if (query.ownerId) where.ownerId = BigInt(query.ownerId);
|
|
if (query.bizType) where.bizType = query.bizType as Prisma.EnumResourceBizTypeFilter['equals'];
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.commonResource.findMany({
|
|
where,
|
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
}),
|
|
this.prisma.commonResource.count({ where }),
|
|
]);
|
|
return serializeBigInt({ items, total, page, pageSize });
|
|
}
|
|
|
|
async detail(id: bigint) {
|
|
const resource = await this.prisma.commonResource.findUnique({ where: { id } });
|
|
if (!resource) throw new NotFoundException('资源不存在');
|
|
return serializeBigInt(resource);
|
|
}
|
|
|
|
async update(id: bigint, dto: UpdateResourceDto) {
|
|
await this.detail(id);
|
|
const resource = await this.prisma.commonResource.update({
|
|
where: { id },
|
|
data: {
|
|
...(dto.url !== undefined ? { url: dto.url, ossKey: dto.url } : {}),
|
|
...(dto.mediaType !== undefined ? { mediaType: dto.mediaType as 'IMAGE' | 'VIDEO' | 'FILE' } : {}),
|
|
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
|
|
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DELETED' } : {}),
|
|
},
|
|
});
|
|
return serializeBigInt(resource);
|
|
}
|
|
|
|
async remove(id: bigint) {
|
|
await this.detail(id);
|
|
await this.prisma.commonResource.update({
|
|
where: { id },
|
|
data: { status: 'DELETED' },
|
|
});
|
|
return { ok: true };
|
|
}
|
|
}
|