gongxiangp2/.kiro/steering/任务-详情素材逻辑说明与迁移计划.md
chenpei 4dba93c4e5 feat: 完成任务-详情素材迁移到新逻辑
- 迁移57个老素材到新逻辑(本地zip + S3预览图)
- 优化文件命名:SPU_作者_店铺_类型.zip
- PSD文件单独存储,不打包进all.zip
- 修复下载接口,优先使用本地zip文件
- 修复预览图重复问题
- 添加迁移脚本和验证脚本
- 更新文档和迁移计划

迁移统计:
- 总素材数:77个
- 成功迁移:57个
- 新逻辑上传:18个
- 空记录:2个
2026-01-21 10:16:28 +08:00

15 KiB
Raw Blame History

任务-详情素材逻辑说明与迁移计划

文档目的: 记录"任务-详情"素材的老逻辑、新逻辑和迁移计划 创建时间: 2026-01-20 最后更新: 2026-01-20


一、迁移完成统计(2026-01-20)

最终结果

项目 数量
总素材数 77 个
✅ 已成功迁移 75 个
❌ 空记录(无需迁移) 2 个

迁移详情

第一轮迁移(修复 zipFilePath='' 的素材):

  • 第一批:9 个成功
  • 第二批:8 个成功
  • 第三批:8 个成功
  • 第四批:3 个成功
  • 小计:28 个素材

第二轮迁移(修复有 zipFilePath 但没有本地 zip 文件的素材):

  • 第一批:10 个成功
  • 第二批:9 个成功(1 个空素材失败)
  • 第三批:9 个成功(1 个文件名问题)
  • 第四批:1 个成功
  • 小计:29 个素材

第三轮迁移(新逻辑上传的素材):

  • 18 个素材使用新逻辑上传,无需迁移

总计本次迁移:57 个素材成功

空记录说明

2 个空记录(无任务号,无资产):

  • CYJ0062 (紫苏) - 无任务号
  • GBPM-SN0208-389 (瑶光) - 无任务号

这些是测试数据或上传失败的记录,不需要迁移。

验证结果

使用 scripts/verify-migrated-files.ts 验证:

  • ✅ 所有文件存在:75 个
  • ⚠️ 部分文件缺失:0 个
  • ❌ 所有文件缺失:0 个
  • ❌ 没有 zip 记录:2 个(空记录)

结论:迁移工作已全部完成!


二、老逻辑(已废弃)

2.1 数据流程

用户上传文件
    ↓
后台压缩生成预览图(像素偏高的预览图)
    ↓
预览图上传到 S3(存储类型:detail/main/sku)
    ↓
数据库保存 assetType = 'detail'/'main'/'sku'(指向 S3 URL)
    ↓
用户下载时:临时打包预览图并返回

2.2 关键特点

  1. 不保存原图:原图在临时目录,上传完成后会被删除
  2. 只有预览图:material_asset 表中只有 S3 预览图,没有本地 zip
  3. 下载时动态打包:每次下载时,从 S3 下载预览图并临时打包
  4. 预览图质量高:像素偏高,接近原图

2.3 数据库结构(老逻辑)

表 字段 说明
materials zipFilePath 空
materials zipFileName 空
materials zipFileSize 空
materials previewUrl S3 预览图 URL
material_asset assetType 'detail'/'main'/'sku'
material_asset fileUrl S3 预览图 URL
material_asset fileName 预览图文件名(如 detail-preview-0.webp)

2.4 示例数据

-- materials 表
{
  id: "7d01a3ab-8a99-4966-9f5e-53b5ef6cfad1",
  taskNo: "TASK202601201149497501-01-01",
  productModel: "CJ0252",
  previewUrl: "https://s3.yydd.ai/gongxiangpan/由壹点AI/product-package/detail-xxx.webp",
  zipFilePath: NULL
}

-- material_asset 表
{
  id: "xxx",
  materialId: "7d01a3ab-8a99-4966-9f5e-53b5ef6cfad1",
  assetType: "detail",
  fileUrl: "https://s3.yydd.ai/gongxiangpan/由壹点AI/product-package/detail-xxx.webp",
  fileName: "1200-欧.jpg"
}

三、新逻辑(当前使用)

3.1 数据流程

用户上传文件(分片上传)
    ↓
保存到本地临时目录
    ↓
    ├─→ 原图文件(临时目录)
    │        ↓
    │    打包成分类 zip(main.zip / sku.zip / detail.zip)
    │        ↓
    │    保存到本地永久目录
    │        ↓
    │    数据库记录 assetType = 'main'/'sku'/'detail'(本地路径)
    │
    └─→ 压缩生成预览图(800×1200,200KB,webp 格式)
             ↓
         上传到 S3
             ↓
         数据库记录 assetType = 'main-preview'/'sku-preview'/'detail-preview'(S3 URL)

3.2 关键特点

  1. 保存原图:原图打包成 zip 保存到本地永久目录
  2. 预览图独立:预览图单独上传 S3,用于网页快速展示
  3. 下载直接返回 zip:下载时直接返回本地 zip 文件,无需动态打包
  4. 预览图质量优化:800×1200,200KB,节省带宽

3.3 数据库结构(新逻辑)

表 字段 说明
materials previewUrl 第一张预览图 URL(S3)
materials zipFilePath 总 zip 文件路径(本地)
materials zipFileName 总 zip 文件名
materials zipFileSize 总 zip 文件大小
material_asset assetType 'main-preview'/'sku-preview'/'detail-preview'/'main'/'sku'/'detail'/'all'/'psd'
material_asset fileUrl 预览图是 S3 URL,zip 是本地路径
material_asset fileName 预览图文件名 或 zip 文件名

3.4 资产类型说明

assetType 含义 fileUrl 内容 用途
main-preview 主图预览图 S3 URL (webp) 网页展示
sku-preview SKU图预览图 S3 URL (webp) 网页展示
detail-preview 详情图预览图 S3 URL (webp) 网页展示
main 主图 zip 包 本地路径 下载
sku SKU图 zip 包 本地路径 下载
detail 详情图 zip 包 本地路径 下载
all 总 zip 包(含分类zip + PSD) 本地路径 下载
psd PSD 源文件 本地路径 下载

3.5 示例数据

-- materials 表
{
  id: "404a427e-42a8-4251-ac57-01846c07dcab",
  taskNo: "TASK202601161107108287-01-01",
  productModel: "DSG0095",
  previewUrl: "https://s3.yydd.ai/gongxiangpan/由壹点AI/product-package/main-preview-xxx.webp",
  zipFilePath: "uploads/product-package/404a427e-42a8-4251-ac57-01846c07dcab/source.zip",
  zipFileName: "DSG0095_任务-详情_404a427e.zip",
  zipFileSize: 12345678
}

-- material_asset 表
{
  id: "xxx",
  materialId: "404a427e-42a8-4251-ac57-01846c07dcab",
  assetType: "main-preview",
  fileUrl: "https://s3.yydd.ai/gongxiangpan/由壹点AI/product-package/main-preview-xxx.webp",
  fileName: "main-preview-0.webp",
  sortOrder: 0
}

{
  id: "yyy",
  materialId: "404a427e-42a8-4251-ac57-01846c07dcab",
  assetType: "main",
  fileUrl: "uploads/product-package/404a427e-42a8-4251-ac57-01846c07dcab/main.zip",
  fileName: "DSG0095_主图_404a427e.zip",
  fileSize: 5678901,
  sortOrder: 0
}

四、老素材迁移计划

4.1 迁移目标

将所有使用老逻辑的"任务-详情"素材(67 个,全部需要迁移)迁移到新逻辑。

重要说明

⚠️ 所有 67 个素材都是老逻辑,包括:

  1. 那 42 个"有 zipFilePath"的素材:

    • zipFileSize = 0,说明 zip 文件实际上没有正确创建
    • 可能是早期测试时部分功能不完整导致
    • 需要重新生成 zip 文件
  2. 那 25 个"无 zipFilePath"的素材:

    • 只有 S3 预览图,没有任何 zip 记录
    • 需要下载预览图并打包成 zip

结论:67 个素材全部需要迁移

4.2 迁移步骤

步骤 1: 下载预览图

从 S3 下载当前存储的所有预览图:

// 1. 查询需要迁移的素材
const materialsToMigrate = await prisma.material.findMany({
  where: {
    materialType: '任务-详情',
    deletedAt: null,
    zipFilePath: '',  // 无 zip 文件
    taskNo: { not: null },  // 有任务号
  },
  select: { id: true, productModel: true, taskNo: true },
});

// 2. 对每个素材,下载所有预览图
for (const material of materialsToMigrate) {
  const assets = await prisma.materialAsset.findMany({
    where: { materialId: material.id },
    orderBy: { sortOrder: 'asc' },
  });

  // 按 assetType 分组
  const mainAssets = assets.filter(a => a.assetType === 'main');
  const skuAssets = assets.filter(a => a.assetType === 'sku');
  const detailAssets = assets.filter(a => a.assetType === 'detail');

  // 下载预览图到临时目录
  for (const asset of mainAssets) {
    const buffer = await downloadFromS3(asset.fileUrl);
    const tempPath = path.join(tempDir, asset.fileName);
    await fs.writeFile(tempPath, buffer);
  }
  // ... 同样处理 skuAssets 和 detailAssets
}

步骤 2: 打包分类 zip

使用原图(预览图)打包成分类 zip:

// 使用 createZipFromFiles 函数打包
const zipInfos = {};
const zipFilesForAll = [];

// 打包主图
if (mainFiles.length > 0) {
  zipInfos.main = await createZipFromFiles(
    mainFiles,
    material.id,
    material.productModel,
    '任务-详情',
    'main'
  );
  zipFilesForAll.push({ path: zipInfos.main.filePath, name: zipInfos.main.fileName });
}

// 打包 SKU 图
if (skuFiles.length > 0) {
  zipInfos.sku = await createZipFromFiles(
    skuFiles,
    material.id,
    material.productModel,
    '任务-详情',
    'sku'
  );
  zipFilesForAll.push({ path: zipInfos.sku.filePath, name: zipInfos.sku.fileName });
}

// 打包详情图
if (detailFiles.length > 0) {
  zipInfos.detail = await createZipFromFiles(
    detailFiles,
    material.id,
    material.productModel,
    '任务-详情',
    'detail'
  );
  zipFilesForAll.push({ path: zipInfos.detail.filePath, name: zipInfos.detail.fileName });
}

步骤 3: 打包总 zip

// 打包总 zip(包含所有分类 zip + PSD)
const psdFilesForAll = [];
// 检查是否有 PSD 文件
const psdAssets = assets.filter(a => a.assetType === 'psd');
for (const psd of psdAssets) {
  psdFilesForAll.push({ path: psd.fileUrl, name: psd.fileName });
}

zipInfos.all = await createZipFromZips(
  zipFilesForAll,
  psdFilesForAll,
  material.id,
  material.productModel,
  '任务-详情'
);

步骤 4: 更新数据库

// 4.1 更新 assets 表(添加 main/sku/detail/all 类型的记录)
const assetRecords = [];

// 添加分类 zip 资产
if (zipInfos.main) {
  assetRecords.push({
    materialId: material.id,
    assetType: 'main',
    fileUrl: zipInfos.main.filePath,
    fileName: zipInfos.main.fileName,
    fileSize: BigInt(zipInfos.main.fileSize),
    sortOrder: 0,
  });
}

if (zipInfos.sku) {
  assetRecords.push({
    materialId: material.id,
    assetType: 'sku',
    fileUrl: zipInfos.sku.filePath,
    fileName: zipInfos.sku.fileName,
    fileSize: BigInt(zipInfos.sku.fileSize),
    sortOrder: 0,
  });
}

if (zipInfos.detail) {
  assetRecords.push({
    materialId: material.id,
    assetType: 'detail',
    fileUrl: zipInfos.detail.filePath,
    fileName: zipInfos.detail.fileName,
    fileSize: BigInt(zipInfos.detail.fileSize),
    sortOrder: 0,
  });
}

// 添加总 zip 资产
assetRecords.push({
  materialId: material.id,
  assetType: 'all',
  fileUrl: zipInfos.all.filePath,
  fileName: zipInfos.all.fileName,
  fileSize: BigInt(zipInfos.all.fileSize),
  sortOrder: 0,
});

await prisma.materialAsset.createMany({
  data: assetRecords,
});

// 4.2 更新 materials 表
await prisma.material.update({
  where: { id: material.id },
  data: {
    zipFilePath: zipInfos.all.filePath,
    zipFileName: zipInfos.all.fileName,
    zipFileSize: BigInt(zipInfos.all.fileSize),
  },
});

步骤 5: 清理临时文件

// 删除下载的临时文件
for (const file of mainFiles.concat(skuFiles, detailFiles)) {
  await fs.unlink(file.path);
}

4.3 迁移脚本

完整的迁移脚本:scripts/migrate-product-package-assets.ts

4.4 回滚计划

如果迁移失败,可以执行以下回滚操作:

-- 删除新增的资产记录
DELETE FROM material_asset
WHERE material_id IN (
  SELECT id FROM materials
  WHERE material_type = '任务-详情'
  AND zip_file_path = ''
)
AND asset_type IN ('main', 'sku', 'detail', 'all');

-- 恢复 materials 表
UPDATE materials
SET
  zip_file_path = '',
  zip_file_name = '',
  zip_file_size = 0
WHERE material_type = '任务-详情'
AND zip_file_path != '';

五、注意事项

5.1 任务平台接口兼容性

外部接口: /api/external/product-assets

  • 返回结构: 保持不变,兼容新老格式
  • 下载链接: 优先返回 all 类型的 zip,如果没有则回退到老逻辑

外部下载接口: /api/external/download

  • 处理逻辑: 优先查找 all 类型的资产,如果没有则动态打包预览图

5.2 前端展示兼容性

详情页组件: components/detail/ProductPackageDetail.tsx

  • 展示逻辑: 兼容 main-preview/main、sku-preview/sku、detail-preview/detail
  • 下载逻辑: 优先下载 zip 文件,如果没有则动态打包预览图

5.3 迁移前后对比

特性 迁移前(老逻辑) 迁移后(新逻辑)
预览图 S3 S3
原图存储 无 本地 zip
下载方式 动态打包预览图 直接返回 zip
下载速度 较慢(需要打包) 快速(直接返回)
存储空间 较少(只有预览图) 较多(预览图 + 原图)

六、执行计划

6.1 准备阶段

  1. ✅ 完成新逻辑的代码实现
  2. ✅ 完成前端兼容性修改
  3. ✅ 完成外部接口兼容性修改
  4. ✅ 编写迁移脚本
  5. ✅ 在生产环境测试新逻辑
  6. ✅ 在生产环境测试迁移脚本

6.2 执行阶段

  1. ✅ 备份生产数据库
  2. ✅ 执行迁移脚本(迁移 57 个老素材)
  3. ✅ 验证迁移结果
  4. ✅ 测试下载功能
  5. ✅ 测试任务平台接口

6.3 迁移完成

迁移时间:2026-01-20

迁移结果:

  • 总素材数:77 个
  • 成功迁移:57 个
  • 新逻辑上传:18 个
  • 空记录:2 个

验证通过:所有 75 个有效素材的本地 zip 文件均存在


七、附录

7.1 迁移完成总结

迁移时间:2026-01-20

迁移统计:

  • 总素材数:77 个
  • ✅ 成功迁移:57 个(从老逻辑迁移到新逻辑)
  • ✅ 新逻辑上传:18 个(无需迁移)
  • ❌ 空记录:2 个(无任务号,无资产,不需要迁移)

迁移过程:

  1. 第一轮:28 个素材(修复 zipFilePath='' 的素材)
  2. 第二轮:29 个素材(修复有 zipFilePath 但没有本地 zip 文件的素材)
  3. 验证:所有 75 个有效素材的本地 zip 文件均存在

空记录详情:

  • CYJ0062 (紫苏) - 无任务号,无资产
  • GBPM-SN0208-389 (瑶光) - 无任务号,无资产

这些是测试数据或上传失败的记录,可以忽略或删除。

7.2 相关文件

文件路径 说明
lib/task-material-processor.ts 任务-详情素材处理逻辑
lib/product-package.ts 产品全案相关函数
app/api/materials/upload/product-package/complete/route.ts 任务-详情上传完成接口
app/api/external/product-assets/route.ts 外部接口(任务平台)
components/detail/ProductPackageDetail.tsx 任务-详情详情页组件
scripts/migrate-product-package-old-materials.ts 迁移脚本
scripts/verify-migrated-files.ts 验证脚本
scripts/check-remaining-materials.ts 检查剩余素材脚本

文档版本: 2.0.0 最后更新: 2026-01-20 状态: ✅ 迁移完成