feat(models): 新增历史模型回填(reanalyze)并修复存储去重的悬挂文件问题
背景:升级到带 models.stats 的版本后,库里已有模型没有分析与缩略图,
审核界面会全部显示"未分析、无预览图"。E2E 验证时还发现一个真问题:
sha256 去重命中旧资产但文件不存在(换过 STORAGE_DIR、或只恢复数据库)时,
分析会静默产出"全 0 结果"。
- POST /api/models/:id/reanalyze:重新分析(面数/骨骼树/humanoid/可驱动性)
并重建缩略图,只改 stats 与 thumbnailAssetId;文件在存储中不存在时返回 409,
避免用空结果覆盖已有分析数据
- scripts/reanalyze-models.mjs:批量回填 CLI(--dry-run / --model-id / --force)
- StorageService.persistBuffer:去重命中但文件不存在时按原路径**自愈补写**
- 契约:rest-api 新增 /api/models/{id}/reanalyze(路由 29 → 30 条)
- 文档:README / tasks/005 / 发布说明补回填章节与升级步骤
实测:真实 21MB VRM(290 骨骼)回填出面数 53552 / 顶点 44662 / humanoid 7/22,
并重建 512×512 缩略图;文件缺失场景返回 409 且不覆盖既有数据。
测试 168 项通过;契约与 30 条路由一致。
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { createReadStream, existsSync } from 'node:fs';
|
||||
import { copyFile, mkdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
||||
import { extname, join, resolve, sep } from 'node:path';
|
||||
import { dirname, extname, join, resolve, sep } from 'node:path';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { AssetKind } from '../common/enums';
|
||||
import { AppConfig } from '../config/configuration';
|
||||
@@ -161,15 +161,26 @@ export class StorageService {
|
||||
mime: string,
|
||||
): Promise<StoredAsset> {
|
||||
const sha256 = createHash('sha256').update(content).digest('hex');
|
||||
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8');
|
||||
|
||||
const existing = await this.assets.findOne({ where: { sha256 } });
|
||||
if (existing) return { asset: existing, url: this.url(existing) as string };
|
||||
if (existing) {
|
||||
// 去重命中,但要确认文件真的还在:换过 STORAGE_DIR、或只恢复了数据库没恢复
|
||||
// 文件时,旧记录会指向一个不存在的路径。此时**按原路径补写一份**,
|
||||
// 否则后续的模型分析 / 缩略图 / 下载都会静默失败(曾实测到"全 0 分析结果")。
|
||||
const absolute = this.absolutePath(existing);
|
||||
if (!existsSync(absolute)) {
|
||||
await mkdir(dirname(absolute), { recursive: true });
|
||||
await writeFile(absolute, buf);
|
||||
}
|
||||
return { asset: existing, url: this.url(existing) as string };
|
||||
}
|
||||
|
||||
const ext = extname(originalName).toLowerCase();
|
||||
const now = new Date();
|
||||
const relDir = join(kind, String(now.getUTCFullYear()), String(now.getUTCMonth() + 1).padStart(2, '0'), sha256.slice(0, 2));
|
||||
const relPath = join(relDir, `${sha256}${ext}`);
|
||||
await mkdir(join(this.root, relDir), { recursive: true });
|
||||
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8');
|
||||
await writeFile(join(this.root, relPath), buf);
|
||||
|
||||
const entity = this.assets.create({
|
||||
|
||||
Reference in New Issue
Block a user