| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- /**
- * DB 死链清理 (audioUrl 指向 OSS 但文件已删)
- *
- * 策略: 跟 OSS 上实际存在的文件做差集, 找出 DB 里指向死链的 audioUrl 字段
- *
- * 用法:
- * node clean-db-deadlinks.cjs --dry-run # 默认, 列出死链不更新
- * node clean-db-deadlinks.cjs --apply # 真清 (UPDATE audioUrl='', audioDuration=0)
- * node clean-db-deadlinks.cjs --apply --out deadlinks.json # 留档
- */
- const fs = require('fs');
- const path = require('path');
- const envTxt = fs.readFileSync(path.join(__dirname, '..', '..', 'server', '.env.production'), 'utf8');
- const env = {};
- for (const line of envTxt.split(/\r?\n/)) {
- const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
- if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '');
- }
- const serverNM = path.join(__dirname, '..', '..', 'server', 'node_modules');
- const OSS = require(path.join(serverNM, 'ali-oss'));
- const mysql = require(path.join(serverNM, 'mysql2', 'promise'));
- const args = process.argv.slice(2);
- const opts = { apply: args.includes('--apply'), dryRun: !args.includes('--apply'), out: null };
- for (let i = 0; i < args.length; i++) if (args[i] === '--out') opts.out = args[++i];
- // 解析 URL 拿 key, 不管 host
- function urlToKey(url) {
- if (!url) return null;
- const s = String(url).trim();
- if (!s || s.startsWith('/')) return null;
- try { return decodeURIComponent(new URL(s).pathname.replace(/^\/+/, '')); } catch { return null; }
- }
- (async () => {
- console.log(`\n=== DB 死链清理 (${opts.apply ? '🗑 真实更新' : '👀 dry-run'}) ===`);
- // 1. 列 OSS audio/ 所有 key
- const client = new OSS({ accessKeyId: env.OSS_ACCESS_KEY_ID, accessKeySecret: env.OSS_ACCESS_KEY_SECRET, bucket: 'rrbrr-book-audio', endpoint: 'oss-cn-shanghai.aliyuncs.com', secure: true });
- const liveKeys = new Set();
- let marker = null;
- do {
- const r = await client.list({ prefix: 'audio/', 'max-keys': 1000, marker });
- for (const o of (r.objects || [])) liveKeys.add(o.name);
- marker = r.nextMarker || null;
- } while (marker);
- console.log(`OSS 活文件数: ${liveKeys.size}`);
- // 2. 查 DB 所有 OSS audioUrl
- const conn = await mysql.createConnection({ uri: env.DATABASE_URL });
- const [bcRows] = await conn.query(`SELECT id, audioUrl, audioDuration FROM BookChapter WHERE audioUrl LIKE '%aliyuncs.com%'`);
- const [arRows] = await conn.query(`SELECT audioId, audioUrl, audioDuration FROM AudioRecord WHERE status='completed' AND audioUrl IS NOT NULL AND audioUrl<>'' AND audioUrl LIKE '%aliyuncs.com%'`);
- await conn.end();
- console.log(`DB BookChapter 有 OSS URL: ${bcRows.length}`);
- console.log(`DB AudioRecord 有 OSS URL: ${arRows.length}`);
- // 3. 找死链
- const deadBC = [];
- for (const r of bcRows) {
- const k = urlToKey(r.audioUrl);
- if (k && !liveKeys.has(k)) deadBC.push(r);
- }
- const deadAR = [];
- for (const r of arRows) {
- const k = urlToKey(r.audioUrl);
- if (k && !liveKeys.has(k)) deadAR.push(r);
- }
- console.log(`\n BookChapter 死链: ${deadBC.length}`);
- console.log(` AudioRecord 死链: ${deadAR.length}`);
- console.log(` 总计: ${deadBC.length + deadAR.length}`);
- if (deadBC.length) {
- console.log(`\n BookChapter 死链前 5:`);
- for (const r of deadBC.slice(0, 5)) console.log(` id=${r.id} ${r.audioUrl}`);
- }
- if (deadAR.length) {
- console.log(`\n AudioRecord 死链前 5:`);
- for (const r of deadAR.slice(0, 5)) console.log(` audioId=${r.audioId} ${r.audioUrl}`);
- }
- if (opts.out) {
- fs.writeFileSync(opts.out, JSON.stringify({ scannedAt: new Date().toISOString(), deadBookChapter: deadBC, deadAudioRecord: deadAR }, null, 2));
- console.log(`\n 📄 死链清单已写到 ${opts.out}`);
- }
- if (opts.dryRun) {
- console.log(`\n👀 DRY-RUN 模式, 没动任何数据`);
- console.log(` 确认后用 --apply 真清:`);
- console.log(` UPDATE BookChapter SET audioUrl='', audioDuration=0 WHERE id IN (${deadBC.map(r=>r.id).join(',')})`);
- console.log(` UPDATE AudioRecord SET audioUrl='', audioDuration=0 WHERE audioId IN (${deadAR.map(r=>`"${r.audioId}"`).join(',')})`);
- return;
- }
- // 4. 真清
- const conn2 = await mysql.createConnection({ uri: env.DATABASE_URL });
- let updatedBC = 0, updatedAR = 0;
- if (deadBC.length) {
- const ids = deadBC.map(r => r.id);
- // 一次最多 1000 个
- for (let i = 0; i < ids.length; i += 1000) {
- const batch = ids.slice(i, i + 1000);
- const [r] = await conn2.query(`UPDATE BookChapter SET audioUrl='', audioDuration=0 WHERE id IN (?)`, [batch]);
- updatedBC += r.affectedRows;
- }
- }
- if (deadAR.length) {
- const ids = deadAR.map(r => r.audioId);
- for (let i = 0; i < ids.length; i += 1000) {
- const batch = ids.slice(i, i + 1000);
- const [r] = await conn2.query(`UPDATE AudioRecord SET audioUrl='', audioDuration=0 WHERE audioId IN (?)`, [batch]);
- updatedAR += r.affectedRows;
- }
- }
- await conn2.end();
- console.log(`\n✅ BookChapter 更新: ${updatedBC} 行`);
- console.log(`✅ AudioRecord 更新: ${updatedAR} 行`);
- })().catch(e => { console.error('异常:', e); process.exit(1); });
|