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