// scripts/fix-asset-urls.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const FROM = 'douyin-archive/'; const TO = ''; function escapeForPgRegex(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } const FROM_RE = `^${escapeForPgRegex(FROM)}`; // 只替换“以旧前缀开头”的字符串 const dryRun = false; // true: 只统计,不修改 async function main() { if (dryRun) { const rows = await prisma.$queryRawUnsafe(` WITH c AS ( SELECT 'Author.avatar_url' AS col, COUNT(*) AS n FROM "Author" WHERE avatar_url LIKE '${FROM}%' UNION ALL SELECT 'CommentUser.avatar_url' , COUNT(*) FROM "CommentUser" WHERE avatar_url LIKE '${FROM}%' UNION ALL SELECT 'CommentImage.url' , COUNT(*) FROM "CommentImage" WHERE url LIKE '${FROM}%' UNION ALL SELECT 'Video.cover_url' , COUNT(*) FROM "Video" WHERE cover_url LIKE '${FROM}%' UNION ALL SELECT 'Video.video_url' , COUNT(*) FROM "Video" WHERE video_url LIKE '${FROM}%' UNION ALL SELECT 'ImageFile.url' , COUNT(*) FROM "ImageFile" WHERE url LIKE '${FROM}%' UNION ALL SELECT 'ImageFile.animated' , COUNT(*) FROM "ImageFile" WHERE animated LIKE '${FROM}%' UNION ALL SELECT 'ImagePost.music_url' , COUNT(*) FROM "ImagePost" WHERE music_url LIKE '${FROM}%' UNION ALL SELECT 'Video.raw_json' , COUNT(*) FROM "Video" WHERE raw_json::text LIKE '%${FROM}%' UNION ALL SELECT 'ImagePost.raw_json' , COUNT(*) FROM "ImagePost" WHERE raw_json::text LIKE '%${FROM}%' ) SELECT * FROM c ORDER BY col; `); console.table(rows); return; } await prisma.$transaction(async (tx) => { // CommentUser.avatar_url await tx.$executeRawUnsafe(` UPDATE "CommentUser" SET avatar_url = regexp_replace(avatar_url, '${FROM_RE}', '${TO}') WHERE avatar_url LIKE '${FROM}%' `); // CommentImage.url await tx.$executeRawUnsafe(` UPDATE "CommentImage" SET url = regexp_replace(url, '${FROM_RE}', '${TO}') WHERE url LIKE '${FROM}%' `); // Author.avatar_url await tx.$executeRawUnsafe(` UPDATE "Author" SET avatar_url = regexp_replace(avatar_url, '${FROM_RE}', '${TO}') WHERE avatar_url LIKE '${FROM}%' `); // Video.cover_url await tx.$executeRawUnsafe(` UPDATE "Video" SET cover_url = regexp_replace(cover_url, '${FROM_RE}', '${TO}') WHERE cover_url LIKE '${FROM}%' `); // Video.video_url await tx.$executeRawUnsafe(` UPDATE "Video" SET video_url = regexp_replace(video_url, '${FROM_RE}', '${TO}') WHERE video_url LIKE '${FROM}%' `); // ImageFile.url await tx.$executeRawUnsafe(` UPDATE "ImageFile" SET url = regexp_replace(url, '${FROM_RE}', '${TO}') WHERE url LIKE '${FROM}%' `); // ImageFile.animated await tx.$executeRawUnsafe(` UPDATE "ImageFile" SET animated = regexp_replace(animated, '${FROM_RE}', '${TO}') WHERE animated LIKE '${FROM}%' `); // ImagePost.music_url await tx.$executeRawUnsafe(` UPDATE "ImagePost" SET music_url = regexp_replace(music_url, '${FROM_RE}', '${TO}') WHERE music_url LIKE '${FROM}%' `); // (可选)raw_json:简单文本整体替换;若想更精细可用递归 JSONB 方法 await tx.$executeRawUnsafe(` UPDATE "Video" SET raw_json = to_jsonb(replace(raw_json::text, '${FROM}', '${TO}')) WHERE raw_json::text LIKE '%${FROM}%' `); await tx.$executeRawUnsafe(` UPDATE "ImagePost" SET raw_json = to_jsonb(replace(raw_json::text, '${FROM}', '${TO}')) WHERE raw_json::text LIKE '%${FROM}%' `); }); // 快速抽样 const sample = await prisma.$queryRawUnsafe(` ( SELECT 'CommentImage.url' AS col, url AS v FROM "CommentImage" WHERE url LIKE '${TO}%' LIMIT 2 ) UNION ALL ( SELECT 'CommentUser.avatar_url', avatar_url FROM "CommentUser" WHERE avatar_url LIKE '${TO}%' LIMIT 2 ) UNION ALL ( SELECT 'Video.video_url', video_url FROM "Video" WHERE video_url LIKE '${TO}%' LIMIT 2 ) `); console.log('Sample after update:', sample); } main().catch((e) => { console.error(e); process.exit(1); }).finally(async () => { await prisma.$disconnect(); });