Merge commit '7fa69c2521eefbfc1b78cca0e8a45264291c967d' into dev_jacy
CI / verify (pull_request) Waiting to run
CI / verify (pull_request) Waiting to run
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import {
|
||||
activityPosterResizeTarget,
|
||||
activityPosterTemplateTooLarge,
|
||||
} from '@dukang/shared-types';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const JPEG_QUALITY = 85;
|
||||
|
||||
export type ActivityPosterUploadPrepared = {
|
||||
buffer: Buffer;
|
||||
mimeType: string;
|
||||
width: number;
|
||||
height: number;
|
||||
originalWidth: number;
|
||||
originalHeight: number;
|
||||
compressed: boolean;
|
||||
};
|
||||
|
||||
function resolveOutputMime(hasAlpha: boolean, inputMime?: string): string {
|
||||
if (hasAlpha) return 'image/png';
|
||||
if (inputMime === 'image/webp') return 'image/jpeg';
|
||||
if (inputMime?.startsWith('image/')) return inputMime;
|
||||
return 'image/jpeg';
|
||||
}
|
||||
|
||||
async function encodeImage(
|
||||
pipeline: sharp.Sharp,
|
||||
mimeType: string,
|
||||
): Promise<Buffer> {
|
||||
if (mimeType === 'image/png') {
|
||||
return pipeline.png().toBuffer();
|
||||
}
|
||||
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') {
|
||||
return pipeline.jpeg({ quality: JPEG_QUALITY }).toBuffer();
|
||||
}
|
||||
if (mimeType === 'image/webp') {
|
||||
return pipeline.webp({ quality: JPEG_QUALITY }).toBuffer();
|
||||
}
|
||||
return pipeline.jpeg({ quality: JPEG_QUALITY }).toBuffer();
|
||||
}
|
||||
|
||||
export async function prepareActivityPosterUpload(input: {
|
||||
buffer: Buffer;
|
||||
mimeType?: string;
|
||||
}): Promise<ActivityPosterUploadPrepared> {
|
||||
if (!input.buffer?.length) {
|
||||
throw new BadRequestException('活动图文件为空');
|
||||
}
|
||||
if (input.mimeType && !input.mimeType.startsWith('image/')) {
|
||||
throw new BadRequestException('活动图仅支持图片文件');
|
||||
}
|
||||
|
||||
const meta = await sharp(input.buffer, { failOn: 'none' }).metadata();
|
||||
if (!meta.width || !meta.height) {
|
||||
throw new BadRequestException('活动图无法读取尺寸,请换一张图片');
|
||||
}
|
||||
|
||||
const originalWidth = meta.width;
|
||||
const originalHeight = meta.height;
|
||||
const resizeTarget = activityPosterResizeTarget(originalWidth, originalHeight);
|
||||
|
||||
if (!resizeTarget) {
|
||||
return {
|
||||
buffer: input.buffer,
|
||||
mimeType: input.mimeType || 'image/jpeg',
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
compressed: false,
|
||||
};
|
||||
}
|
||||
|
||||
const hasAlpha = meta.hasAlpha === true;
|
||||
const outputMime = resolveOutputMime(hasAlpha, input.mimeType);
|
||||
const buffer = await encodeImage(
|
||||
sharp(input.buffer, { failOn: 'none' }).resize(resizeTarget.width, resizeTarget.height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
}),
|
||||
outputMime,
|
||||
);
|
||||
const metaOut = await sharp(buffer).metadata();
|
||||
const width = metaOut.width ?? resizeTarget.width;
|
||||
const height = metaOut.height ?? resizeTarget.height;
|
||||
|
||||
if (activityPosterTemplateTooLarge(width, height)) {
|
||||
throw new BadRequestException('活动图压缩后仍超出尺寸限制,请换一张较小的图片');
|
||||
}
|
||||
|
||||
return {
|
||||
buffer,
|
||||
mimeType: outputMime,
|
||||
width,
|
||||
height,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
compressed: true,
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@pri
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { prepareActivityPosterUpload } from '../../common/image/activity-poster-upload.util';
|
||||
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';
|
||||
@@ -105,19 +106,75 @@ export class ResourceService {
|
||||
if (dto.bizType === 'AVATAR' && !file.mimetype?.startsWith('image/')) {
|
||||
throw new BadRequestException('头像仅支持图片文件');
|
||||
}
|
||||
if (dto.bizType === 'ACTIVITY_POSTER' && dto.mediaType === 'IMAGE' && !file.mimetype?.startsWith('image/')) {
|
||||
throw new BadRequestException('活动图仅支持图片文件');
|
||||
}
|
||||
|
||||
let uploadBuffer = file.buffer;
|
||||
let uploadMimeType = file.mimetype;
|
||||
let imageMeta: {
|
||||
width: number;
|
||||
height: number;
|
||||
originalWidth: number;
|
||||
originalHeight: number;
|
||||
compressed: boolean;
|
||||
fileSize: number;
|
||||
} | undefined;
|
||||
|
||||
if (dto.bizType === 'ACTIVITY_POSTER' && dto.mediaType === 'IMAGE') {
|
||||
const prepared = await prepareActivityPosterUpload({
|
||||
buffer: file.buffer,
|
||||
mimeType: file.mimetype,
|
||||
});
|
||||
if (prepared.buffer.length > maxUploadBytes) {
|
||||
const message = `活动图压缩后仍超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB(当前 ${prepared.width}×${prepared.height})`;
|
||||
await logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_PUT_OBJECT',
|
||||
actorRef: actor,
|
||||
requestBody: {
|
||||
...requestBody,
|
||||
imageWidth: prepared.width,
|
||||
imageHeight: prepared.height,
|
||||
compressed: prepared.compressed,
|
||||
},
|
||||
status: 'FAILED',
|
||||
errorMessage: message,
|
||||
});
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
uploadBuffer = prepared.buffer;
|
||||
uploadMimeType = prepared.mimeType;
|
||||
imageMeta = {
|
||||
width: prepared.width,
|
||||
height: prepared.height,
|
||||
originalWidth: prepared.originalWidth,
|
||||
originalHeight: prepared.originalHeight,
|
||||
compressed: prepared.compressed,
|
||||
fileSize: prepared.buffer.length,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.oss.putObject({
|
||||
bizType: dto.bizType,
|
||||
mediaType: dto.mediaType,
|
||||
fileName: file.originalname || 'upload.bin',
|
||||
buffer: file.buffer,
|
||||
mimeType: file.mimetype,
|
||||
buffer: uploadBuffer,
|
||||
mimeType: uploadMimeType,
|
||||
});
|
||||
await logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_PUT_OBJECT',
|
||||
actorRef: actor,
|
||||
requestBody,
|
||||
requestBody: {
|
||||
...requestBody,
|
||||
...(imageMeta
|
||||
? {
|
||||
imageWidth: imageMeta.width,
|
||||
imageHeight: imageMeta.height,
|
||||
compressed: imageMeta.compressed,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
responseBody: {
|
||||
bucket: result.bucket,
|
||||
region: result.region,
|
||||
@@ -139,14 +196,14 @@ export class ResourceService {
|
||||
ossKey: result.ossKey,
|
||||
url: result.url,
|
||||
fileName: file.originalname || 'avatar',
|
||||
fileSize: BigInt(file.size),
|
||||
mimeType: file.mimetype,
|
||||
fileSize: BigInt(uploadBuffer.length),
|
||||
mimeType: uploadMimeType,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
return serializeBigInt({ ...result, resourceId: resource.id });
|
||||
return serializeBigInt({ ...result, resourceId: resource.id, ...(imageMeta ? { image: imageMeta } : {}) });
|
||||
}
|
||||
return result;
|
||||
return { ...result, ...(imageMeta ? { image: imageMeta } : {}) };
|
||||
} catch (err) {
|
||||
await logOssUpload(this.prisma, {
|
||||
scene: 'UPLOAD_PUT_OBJECT',
|
||||
|
||||
Reference in New Issue
Block a user