修改3D模型预览报错
This commit is contained in:
parent
8210bf0ad3
commit
b57099df15
34
backend/package-lock.json
generated
34
backend/package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.3.3",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@prisma/client": "^6.19.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"bcrypt": "^6.0.0",
|
||||
@ -1914,6 +1915,39 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nestjs/serve-static": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/serve-static/-/serve-static-4.0.2.tgz",
|
||||
"integrity": "sha512-cT0vdWN5ar7jDI2NKbhf4LcwJzU4vS5sVpMkVrHuyLcltbrz6JdGi1TfIMMatP2pNiq5Ie/uUdPSFDVaZX/URQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-to-regexp": "0.2.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/static": "^6.5.0 || ^7.0.0",
|
||||
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^9.0.0 || ^10.0.0",
|
||||
"express": "^4.18.1",
|
||||
"fastify": "^4.7.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@fastify/static": {
|
||||
"optional": true
|
||||
},
|
||||
"express": {
|
||||
"optional": true
|
||||
},
|
||||
"fastify": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/serve-static/node_modules/path-to-regexp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz",
|
||||
"integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.20.tgz",
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.3.3",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@prisma/client": "^6.19.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"bcrypt": "^6.0.0",
|
||||
@ -75,6 +76,7 @@
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.36",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
Res,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import axios from 'axios';
|
||||
@ -26,6 +27,8 @@ import { Public } from '../auth/decorators/public.decorator';
|
||||
@Controller('ai-3d')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class AI3DController {
|
||||
private readonly logger = new Logger(AI3DController.name);
|
||||
|
||||
constructor(private readonly ai3dService: AI3DService) {}
|
||||
|
||||
/**
|
||||
@ -93,14 +96,18 @@ export class AI3DController {
|
||||
throw new HttpException('URL参数不能为空', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
this.logger.log(`[proxy-model] 开始处理请求,URL长度: ${url.length}`);
|
||||
|
||||
try {
|
||||
// URL解码(处理URL编码)
|
||||
let decodedUrl: string;
|
||||
try {
|
||||
decodedUrl = decodeURIComponent(url);
|
||||
this.logger.log(`[proxy-model] URL解码成功`);
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始URL
|
||||
decodedUrl = url;
|
||||
this.logger.warn(`[proxy-model] URL解码失败,使用原始URL`);
|
||||
}
|
||||
|
||||
// 验证URL是否为腾讯云COS链接(安全验证)
|
||||
@ -121,6 +128,8 @@ export class AI3DController {
|
||||
throw new HttpException('URL格式无效', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
this.logger.log(`[proxy-model] 开始下载文件...`);
|
||||
|
||||
// 从源URL获取文件
|
||||
const response = await axios.get(decodedUrl, {
|
||||
responseType: 'arraybuffer', // 使用arraybuffer以便处理ZIP
|
||||
@ -133,43 +142,92 @@ export class AI3DController {
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`[proxy-model] 文件下载成功,大小: ${response.data.byteLength} bytes`,
|
||||
);
|
||||
|
||||
let fileData: Buffer = Buffer.from(response.data);
|
||||
let contentType =
|
||||
response.headers['content-type'] || 'application/octet-stream';
|
||||
let contentLength = fileData.length;
|
||||
|
||||
// 如果是ZIP文件,解压并提取GLB文件
|
||||
this.logger.log(`[proxy-model] Content-Type: ${contentType}`);
|
||||
|
||||
// 如果是ZIP文件,解压并提取3D模型文件
|
||||
if (
|
||||
decodedUrl.toLowerCase().includes('.zip') ||
|
||||
contentType.includes('zip') ||
|
||||
contentType.includes('application/zip')
|
||||
) {
|
||||
this.logger.log(`[proxy-model] 检测到ZIP文件,开始解压...`);
|
||||
try {
|
||||
const zip = new AdmZip(fileData);
|
||||
const zipEntries = zip.getEntries();
|
||||
|
||||
// 查找ZIP中的GLB文件
|
||||
const glbEntry = zipEntries.find(
|
||||
(entry) =>
|
||||
entry.entryName.toLowerCase().endsWith('.glb') ||
|
||||
entry.entryName.toLowerCase().endsWith('.gltf'),
|
||||
this.logger.log(
|
||||
`[proxy-model] ZIP解压成功,包含 ${zipEntries.length} 个文件`,
|
||||
);
|
||||
|
||||
if (glbEntry) {
|
||||
fileData = glbEntry.getData();
|
||||
contentType = glbEntry.entryName.toLowerCase().endsWith('.glb')
|
||||
? 'model/gltf-binary'
|
||||
: 'model/gltf+json';
|
||||
// 列出所有文件便于调试
|
||||
const allFiles = zipEntries.map((e) => e.entryName);
|
||||
this.logger.log(`[proxy-model] ZIP文件列表: ${allFiles.join(', ')}`);
|
||||
|
||||
// 按优先级查找3D模型文件: GLB > GLTF > OBJ
|
||||
let modelEntry = zipEntries.find((entry) =>
|
||||
entry.entryName.toLowerCase().endsWith('.glb'),
|
||||
);
|
||||
|
||||
if (!modelEntry) {
|
||||
modelEntry = zipEntries.find((entry) =>
|
||||
entry.entryName.toLowerCase().endsWith('.gltf'),
|
||||
);
|
||||
}
|
||||
|
||||
if (!modelEntry) {
|
||||
modelEntry = zipEntries.find((entry) =>
|
||||
entry.entryName.toLowerCase().endsWith('.obj'),
|
||||
);
|
||||
}
|
||||
|
||||
if (modelEntry) {
|
||||
this.logger.log(
|
||||
`[proxy-model] 找到模型文件: ${modelEntry.entryName}`,
|
||||
);
|
||||
fileData = modelEntry.getData();
|
||||
const entryName = modelEntry.entryName.toLowerCase();
|
||||
let modelType = 'glb'; // 默认类型
|
||||
if (entryName.endsWith('.glb')) {
|
||||
contentType = 'model/gltf-binary';
|
||||
modelType = 'glb';
|
||||
} else if (entryName.endsWith('.gltf')) {
|
||||
contentType = 'model/gltf+json';
|
||||
modelType = 'gltf';
|
||||
} else if (entryName.endsWith('.obj')) {
|
||||
contentType = 'text/plain'; // OBJ 是文本格式
|
||||
modelType = 'obj';
|
||||
}
|
||||
contentLength = fileData.length;
|
||||
this.logger.log(
|
||||
`[proxy-model] 模型类型: ${modelType}, 大小: ${contentLength} bytes`,
|
||||
);
|
||||
// 添加自定义头部,告知前端实际的模型类型
|
||||
res.setHeader('X-Model-Type', modelType);
|
||||
} else {
|
||||
// 列出ZIP中的所有文件,便于调试
|
||||
const fileList = zipEntries.map((e) => e.entryName).join(', ');
|
||||
this.logger.error(
|
||||
`[proxy-model] ZIP中未找到3D模型文件。ZIP内容: ${fileList}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
`ZIP文件中未找到GLB或GLTF文件。ZIP内容: ${fileList}`,
|
||||
`ZIP文件中未找到3D模型文件(GLB/GLTF/OBJ)。ZIP内容: ${fileList}`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
} catch (zipError: any) {
|
||||
this.logger.error(
|
||||
`[proxy-model] ZIP处理失败: ${zipError.message}`,
|
||||
zipError.stack,
|
||||
);
|
||||
if (zipError instanceof HttpException) {
|
||||
throw zipError;
|
||||
}
|
||||
@ -189,8 +247,14 @@ export class AI3DController {
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时
|
||||
|
||||
// 发送文件数据
|
||||
this.logger.log(`[proxy-model] 发送响应,大小: ${contentLength} bytes`);
|
||||
res.send(fileData);
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`[proxy-model] 请求处理失败: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';
|
||||
import { join } from 'path';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
@ -35,6 +37,14 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
`.env.${process.env.NODE_ENV || 'development'}`, // 优先加载
|
||||
],
|
||||
}),
|
||||
// 静态文件服务 - 提供 uploads 目录的访问
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(process.cwd(), 'uploads'),
|
||||
serveRoot: '/api/uploads',
|
||||
serveStaticOptions: {
|
||||
index: false,
|
||||
},
|
||||
}),
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021"],
|
||||
"sourceMap": true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user