/** * # 脚本 同步文章分类成员(dump 快照 → 原站当前状态) * * 背景:/blog/ 归档本地 302 篇(26 页)vs 原站当前 7 页——dump 之后原站调整了 * Blog/Gateway 等分类的成员。本脚本按原站 REST API 的 categories 重建本地 * content_terms 分类关联(仅调整 REST 返回的文章;本地有而原站已删除的不动)。 * * 用法:bun scripts/import-live-terms.ts [--dry] */ import { SQL } from "bun"; const LIVE = "https://www.dusuniot.com"; const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot", { max: 1 }); const dry = process.argv.includes("--dry"); /** 本地分类 legacy_id → id(WP term id 与 legacy_id 一致) */ const catRows = await sql`select id, legacy_id, slug from categories where taxonomy = 'category' and legacy_id is not null`; const catByLegacy = new Map(catRows.map((r: any) => [Number(r.legacy_id), r.id as number])); /** 本地已发布文章 legacy_id → content id */ const posts = await sql`select id, legacy_id from contents where type = 'post' and status = 'published' and legacy_id is not null`; const contentByLegacy = new Map(posts.map((r: any) => [Number(r.legacy_id), r.id as number])); console.log("本地已发布文章:", posts.length); /** 逐页拉原站文章的分类(_fields 精简返回) */ const liveCats = new Map(); // wp post id → term ids let page = 1; while (true) { const res = await fetch(`${LIVE}/wp-json/wp/v2/posts?per_page=100&page=${page}&_fields=id,categories`, { headers: { "User-Agent": "Mozilla/5.0" } }); if (!res.ok) { if (res.status === 400) break; // 翻页越界 console.log("REST", res.status, "停止"); break; } const items = (await res.json()) as { id: number; categories: number[] }[]; for (const it of items) liveCats.set(it.id, it.categories || []); if (items.length < 100) break; page++; await new Promise((r) => setTimeout(r, 400)); // 限速(429 防护) } console.log("原站文章:", liveCats.size); let added = 0, removed = 0; for (const [wpId, cats] of liveCats) { const cid = contentByLegacy.get(wpId); if (!cid) continue; const localRows = await sql`select category_id from content_terms where content_id = ${cid}`; const localSet = new Set(localRows.map((r: any) => r.category_id as number)); const liveSet = new Set(cats.map((t) => catByLegacy.get(t)).filter((x): x is number => x != null)); for (const add of liveSet) { if (localSet.has(add)) continue; added++; if (!dry) await sql`insert into content_terms (content_id, category_id) values (${cid}, ${add}) on conflict do nothing`; } for (const rm of localSet) { if (liveSet.has(rm)) continue; removed++; if (!dry) await sql`delete from content_terms where content_id = ${cid} and category_id = ${rm}`; } } console.log(`分类成员同步: 新增 ${added} 移除 ${removed}${dry ? "(dry 未写库)" : ""}`); await sql.end();