gongxiangp2/.kiro/specs/ui-fixes/S3素材统一迁移方案.md

12 KiB
Raw Blame History

S3素材统一迁移方案

背景

当前系统中素材分散在两个S3存储位置:

  • 老位置:s3.1080p.hk/jsxiangche - 老素材
  • 新位置:s3.yydd.ai/gongxiangpan - 新素材

目标:将所有素材统一迁移到新位置 s3.yydd.ai/gongxiangpan,方便后期维护。


项目存储结构

文件存储方式

  1. 源文件(压缩包) → 本地 ./uploads 目录(Docker挂载)- 不需要迁移(1,411个,699.5 GB)
  2. 音频/视频源文件 → MinIO S3(zipFileName字段)- 需要迁移(97个在老S3)
  3. 预览图/视频转码文件 → MinIO S3 - 需要迁移

数据库中的S3相关字段

字段 说明 存储内容
previewUrl 预览图URL 图片(webp/jpg)
zipFileName 源文件名/音频视频URL 本地文件名 或 S3音频/视频URL
zipFilePath 源文件路径 本地路径(uploads/xxx 或 /app/uploads/xxx)
displayImages 详情图数组 多张图片URL
videoSourceUrl 视频源文件 S3视频URL
videoPreview540Url 540P预览视频 S3视频URL
videoPreviewClipUrl 视频片段 S3视频URL
videoCoverUrl 视频封面 S3图片URL

重要说明:

  • zipFilePath 存储本地路径,有两种格式:uploads/xxx.zip 或 /app/uploads/xxx.zip
  • 音频/视频类素材的源文件存储在 zipFileName 字段(S3 URL),zipFilePath 为空

迁移数据量统计

需要迁移的S3文件(从 s3.1080p.hk 到 s3.yydd.ai)

字段 总数 老S3 (需迁移) 新S3 (已在目标) 空/本地
previewUrl 1,525 1,114 313 97
zipFileName (音频) 1,525 97 13 1,415
videoSourceUrl 1,525 4 13 1,401
videoPreview540Url 1,525 122 1 1,402
videoPreviewClipUrl 1,525 122 1 1,402
videoCoverUrl 1,525 78 45 1,402
displayImages 2,665张 2,358 307 -

需要迁移的文件总数

  • previewUrl: 1,114个
  • zipFileName (音频): 97个
  • videoSourceUrl: 4个
  • videoPreview540Url: 122个
  • videoPreviewClipUrl: 122个
  • videoCoverUrl: 78个
  • displayImages: 2,358个
  • 总计:约 3,895 个文件

文件大小估算

1. 音频/视频源文件(zipFileName字段,已知精确大小)

从数据库 zipFileSize 字段获取:

  • 音乐:93个,523.11 MB(平均 5.62 MB/个)
  • 主图视频源文件:4个,183.06 MB(平均 45.76 MB/个)
  • 音频/视频源文件总计:706.17 MB

2. 预览图(估算)

  • 数量:1,114个
  • 平均大小:约 50KB
  • 预览图总计:约 55.7 MB

3. 详情图(估算)

  • 数量:2,358个
  • 平均大小:约 100KB
  • 详情图总计:约 235.8 MB

4. 视频转码文件(估算)

视频时长统计:

  • 种草视频:56个,总时长 16.53分钟
  • 主图视频:39个,总时长 17.18分钟
  • 安装视频:27个,总时长 77.33分钟
  • 总时长:约 111分钟

估算大小(540P视频约 2MB/分钟,片段约 0.5MB/分钟):

  • videoPreview540Url:122个,约 222 MB
  • videoPreviewClipUrl:122个,约 55 MB
  • videoCoverUrl:78个,约 4 MB
  • 视频转码文件总计:约 281 MB

总数据量

类型 文件数 大小
音频/视频源文件 101 706 MB
预览图 1,114 56 MB
详情图 2,358 236 MB
视频转码文件 322 281 MB
总计 3,895 约 1.28 GB

迁移时间估算

网络速度参考

根据网络诊断测试结果:

  • s3.1080p.hk 公网下载速度:20-33 KB/s(frp内网穿透,很慢)
  • s3.yydd.ai 公网下载速度:类似(也是frp)
  • 内网速度:服务器在内网,速度快得多

迁移方式对比

方案1:通过服务器内网中转(推荐)

流程:s3.1080p.hk → 192.168.58.20服务器 → s3.yydd.ai

优点:

  • 服务器在内网,下载/上传速度快
  • 可以并发处理多个文件
  • 可以断点续传

预计时间:

  • 假设内网速度:10 MB/s
  • 数据量:1.28 GB
  • 下载时间:1.28 GB ÷ 10 MB/s = 约 2分钟
  • 上传时间:1.28 GB ÷ 10 MB/s = 约 2分钟
  • 总计:约 4分钟(理想情况)

实际时间:

  • 考虑并发处理、网络波动、错误重试
  • 预计:10-20分钟

方案2:直接S3间复制

流程:s3.1080p.hk → s3.yydd.ai(直接复制)

缺点:

  • 两个S3可能不支持服务端复制
  • 可能走公网,速度慢
  • 不推荐

迁移方案(推荐方案1)

迁移步骤

1. 准备阶段

  • 统计需要迁移的文件数量和大小
  • 了解项目存储结构
  • 备份数据库(防止迁移失败)
  • 创建迁移脚本
  • 在影子系统测试迁移脚本

2. 迁移脚本功能

需要迁移的字段:

  • previewUrl - 预览图
  • zipFileName - 音频文件(仅当包含S3 URL时)
  • displayImages - 详情图数组
  • videoSourceUrl - 视频源文件
  • videoPreview540Url - 540P预览视频
  • videoPreviewClipUrl - 视频片段
  • videoCoverUrl - 视频封面

脚本逻辑:

  • 从数据库查询所有包含 s3.1080p.hk 的素材
  • 遍历每个素材的每个S3字段:
    • 下载文件到服务器临时目录
    • 上传到 s3.yydd.ai
    • 更新数据库中的URL
  • 记录迁移日志(成功/失败)
  • 支持断点续传(失败后可继续)

3. 迁移执行

  • 在影子系统先测试(少量素材)
  • 验证迁移后的素材可正常访问
  • 在正式系统执行完整迁移
  • 迁移过程中系统可正常使用(只读)

4. 验证阶段

  • 检查数据库中所有URL已更新为 s3.yydd.ai
  • 随机抽查素材可正常访问
  • 检查详情图、视频预览可正常显示
  • 确认数据量一致

5. 清理阶段

  • 确认迁移成功后,可选择删除 s3.1080p.hk 上的旧文件
  • 更新 .env 配置,统一使用 s3.yydd.ai
  • 重新部署应用

迁移脚本设计

脚本功能

// scripts/migrate-s3-materials.ts

interface MigrationResult {
  success: number;
  failed: number;
  skipped: number;
  errors: Array<{ materialId: string; field: string; error: string }>;
}

// 需要迁移的S3字段
const S3_FIELDS = [
  'previewUrl',
  'zipFileName',      // 仅当包含S3 URL时
  'videoSourceUrl',
  'videoPreview540Url',
  'videoPreviewClipUrl',
  'videoCoverUrl',
];

async function migrateMaterials() {
  // 1. 查询所有需要迁移的素材(任意字段包含老S3 URL)
  const materials = await prisma.$queryRaw`
    SELECT * FROM materials
    WHERE "deletedAt" IS NULL
    AND (
      "previewUrl" LIKE '%s3.1080p.hk%'
      OR "zipFileName" LIKE '%s3.1080p.hk%'
      OR "videoSourceUrl" LIKE '%s3.1080p.hk%'
      OR "videoPreview540Url" LIKE '%s3.1080p.hk%'
      OR "videoPreviewClipUrl" LIKE '%s3.1080p.hk%'
      OR "videoCoverUrl" LIKE '%s3.1080p.hk%'
      OR EXISTS (
        SELECT 1 FROM unnest("displayImages") AS url
        WHERE url LIKE '%s3.1080p.hk%'
      )
    )
  `;

  // 2. 遍历迁移
  for (const material of materials) {
    try {
      const updates: Record<string, string> = {};
      
      // 2.1 迁移各个S3字段
      for (const field of S3_FIELDS) {
        const url = material[field];
        if (url && url.includes('s3.1080p.hk')) {
          const newUrl = await migrateFile(url);
          updates[field] = newUrl;
        }
      }
      
      // 2.2 迁移详情图数组
      let newDisplayImages = material.displayImages;
      if (material.displayImages && material.displayImages.length > 0) {
        newDisplayImages = [];
        for (const imageUrl of material.displayImages) {
          if (imageUrl.includes('s3.1080p.hk')) {
            const newImageUrl = await migrateFile(imageUrl);
            newDisplayImages.push(newImageUrl);
          } else {
            newDisplayImages.push(imageUrl);
          }
        }
        updates.displayImages = newDisplayImages;
      }
      
      // 2.3 更新数据库
      if (Object.keys(updates).length > 0) {
        await prisma.material.update({
          where: { id: material.id },
          data: updates
        });
      }
      
      console.log(`✅ Migrated: ${material.id}`);
    } catch (error) {
      console.error(`❌ Failed: ${material.id}`, error);
    }
  }
}

async function migrateFile(oldUrl: string): Promise<string> {
  // 1. 从旧S3下载文件
  const response = await fetch(oldUrl);
  const buffer = await response.arrayBuffer();
  
  // 2. 提取文件名和类型
  const fileName = extractFileName(oldUrl);
  const contentType = getContentType(fileName);
  
  // 3. 上传到新S3
  const newUrl = await uploadToNewS3(Buffer.from(buffer), fileName, contentType);
  
  return newUrl;
}

并发控制

// 使用 p-limit 控制并发数
import pLimit from 'p-limit';

const limit = pLimit(5); // 同时处理5个文件(避免内存溢出)

const promises = materials.map(material => 
  limit(() => migrateMaterial(material))
);

await Promise.all(promises);

风险评估

高风险

  • ❌ 数据丢失:迁移过程中文件丢失

    • 缓解措施:先备份数据库,迁移后验证
  • ❌ URL更新失败:数据库更新失败导致URL不一致

    • 缓解措施:使用事务,失败自动回滚

中风险

  • ⚠️ 迁移时间过长:影响用户使用

    • 缓解措施:选择低峰期迁移,支持断点续传
  • ⚠️ 网络不稳定:下载/上传失败

    • 缓解措施:自动重试机制,记录失败日志

低风险

  • ℹ️ 旧文件残留:迁移后旧文件未删除
    • 缓解措施:迁移成功后手动清理

回滚方案

如果迁移失败,需要回滚:

回滚步骤

  1. 停止迁移脚本
  2. 恢复数据库备份
  3. 检查已迁移的素材
  4. 删除新S3上的文件(如果需要)
  5. 重新执行迁移(修复问题后)

回滚SQL

-- 恢复备份(如果有)
-- 或者手动更新URL
UPDATE materials
SET 
  "previewUrl" = REPLACE("previewUrl", 's3.yydd.ai/gongxiangpan', 's3.1080p.hk/jsxiangche'),
  "displayImages" = ARRAY(
    SELECT REPLACE(url::text, 's3.yydd.ai/gongxiangpan', 's3.1080p.hk/jsxiangche')
    FROM unnest("displayImages") AS url
  )
WHERE "previewUrl" LIKE '%s3.yydd.ai%';

迁移后配置更新

1. 更新 .env 配置

# 旧配置(s3.1080p.hk)
S3_ENDPOINT=https://s3.1080p.hk
S3_BUCKET=jsxiangche
S3_PUBLIC_DOMAIN=https://s3.1080p.hk/jsxiangche

# 新配置(s3.yydd.ai)
S3_ENDPOINT=https://s3.yydd.ai
S3_BUCKET=gongxiangpan
S3_PUBLIC_DOMAIN=https://s3.yydd.ai/gongxiangpan
S3_FILE_PATH=由壹点AI/

2. 需要确认的新S3凭证

迁移前需要确认 s3.yydd.ai 的访问凭证:

  • S3_ACCESS_KEY_ID
  • S3_SECRET_ACCESS_KEY

3. 重新部署应用

# 部署到影子系统测试
.\deploy-local-build.ps1 -Target shadow

# 验证成功后部署到正式系统
.\deploy-local-build.ps1

执行计划

时间安排

  • 准备阶段:30分钟(创建脚本、配置凭证)
  • 影子系统测试:10分钟(迁移少量素材测试)
  • 正式系统迁移:10-20分钟(迁移全部素材)
  • 验证阶段:10分钟(检查数据完整性)
  • 总计:约 1小时

执行时间建议

  • 选择低峰期(晚上或周末)
  • 提前通知用户(如果需要)
  • 准备回滚方案

总结

迁移数据量

  • 文件数量:3,895个文件
  • 数据大小:约 1.28 GB
  • 预计时间:10-20分钟(实际迁移)

文件类型分布

类型 文件数 大小
音频/视频源文件 101 706 MB
预览图 1,114 56 MB
详情图 2,358 236 MB
视频转码文件 322 281 MB
总计 3,895 约 1.28 GB

推荐方案

  • 使用方案1(通过服务器内网中转)
  • 并发处理(5个文件同时)
  • 支持断点续传
  • 完整的日志记录

下一步

  1. 确认 s3.yydd.ai 的访问凭证
  2. 创建迁移脚本
  3. 在影子系统测试
  4. 执行正式迁移
  5. 验证数据完整性
  6. 更新配置并重新部署