feat: 异步解析大文件WPS报备资料

This commit is contained in:
hectorzhao
2026-09-04 22:51:07 +08:00
parent aaf96db2d0
commit 5925cf493b
22 changed files with 720 additions and 80 deletions
+18
View File
@@ -0,0 +1,18 @@
import { assertUploadSize } from './files.service';
describe('FilesService report-material upload limits', () => {
const workbook = {
originalname: '行业报备.xlsx',
mimetype: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
};
it('allows report-material imports up to 100MB without raising the generic file limit', () => {
expect(() =>
assertUploadSize('report_material_import', { ...workbook, size: 40 * 1024 * 1024 }),
).not.toThrow();
expect(() => assertUploadSize('other', { ...workbook, size: 40 * 1024 * 1024 })).toThrow('10MB');
expect(() =>
assertUploadSize('report_material_import', { ...workbook, size: 101 * 1024 * 1024 }),
).toThrow('100MB');
});
});
+31 -10
View File
@@ -40,10 +40,12 @@ export class FilesService {
) {}
list(tenantId?: string) {
return this.prisma.fileObject.findMany({
where: tenantId ? { tenantId } : undefined,
orderBy: { createdAt: 'desc' },
}).then((items) => items.map(serializeFileObject));
return this.prisma.fileObject
.findMany({
where: tenantId ? { tenantId } : undefined,
orderBy: { createdAt: 'desc' },
})
.then((items) => items.map(serializeFileObject));
}
create(data: CreateFileObjectDto) {
@@ -71,7 +73,7 @@ export class FilesService {
}
async upload(data: UploadFileDto, file: { originalname: string; mimetype: string; size: number; buffer: Buffer }) {
assertUploadSize(file);
assertUploadSize(data.purpose, file);
const fileName = normalizeMultipartFileName(file.originalname);
const safeName = fileName.replace(/[^\w.\-\u4e00-\u9fa5]/g, '_');
const objectKey = `${data.prefix ?? data.purpose}/${Date.now()}-${randomUUID()}-${safeName}`;
@@ -87,7 +89,11 @@ export class FilesService {
});
}
async uploadForClient(userId: string | undefined, data: UploadFileDto, file: { originalname: string; mimetype: string; size: number; buffer: Buffer }) {
async uploadForClient(
userId: string | undefined,
data: UploadFileDto,
file: { originalname: string; mimetype: string; size: number; buffer: Buffer },
) {
const tenantId = await this.resolveClientTenantId(userId);
const prefixRule = CLIENT_UPLOAD_RULES[data.purpose];
if (!prefixRule || !data.prefix || !prefixRule.test(data.prefix)) {
@@ -135,18 +141,33 @@ export class FilesService {
const IMAGE_UPLOAD_MAX_BYTES = 2 * 1024 * 1024;
const FILE_UPLOAD_MAX_BYTES = 10 * 1024 * 1024;
const REPORT_MATERIAL_IMPORT_MAX_BYTES = 100 * 1024 * 1024;
const IMAGE_FILE_EXTENSION = /\.(?:avif|bmp|gif|heic|heif|jpe?g|png|svg|webp)$/i;
function assertUploadSize(file: { originalname: string; mimetype: string; size: number }) {
export function assertUploadSize(purpose: string, file: { originalname: string; mimetype: string; size: number }) {
const image = file.mimetype.toLowerCase().startsWith('image/') || IMAGE_FILE_EXTENSION.test(file.originalname);
const limit = image ? IMAGE_UPLOAD_MAX_BYTES : FILE_UPLOAD_MAX_BYTES;
const reportMaterialImport = purpose === 'report_material_import';
const limit = reportMaterialImport
? REPORT_MATERIAL_IMPORT_MAX_BYTES
: image
? IMAGE_UPLOAD_MAX_BYTES
: FILE_UPLOAD_MAX_BYTES;
if (file.size > limit) {
throw new BadRequestException(image ? '图片大小不能超过 2MB' : '文件大小不能超过 10MB');
throw new BadRequestException(
reportMaterialImport
? '报备资料文件大小不能超过 100MB'
: image
? '图片大小不能超过 2MB'
: '文件大小不能超过 10MB',
);
}
}
function normalizeMultipartFileName(value: string) {
if (![...value].some((character) => character.charCodeAt(0) > 0x7f) || [...value].some((character) => character.charCodeAt(0) > 0xff)) {
if (
![...value].some((character) => character.charCodeAt(0) > 0x7f) ||
[...value].some((character) => character.charCodeAt(0) > 0xff)
) {
return value;
}