/** * # 脚本 导入产品属性术语顺序(对齐原站 /shop/ 筛选列表顺序) * 原站 WPC 筛选按 wp_term_taxonomy 中的术语顺序展示(取证:pa_product-type 顺序 * SoM → IoT Gateway → Wi-Fi Repeater → Router → Beacon,对应 term_id 756/764/845/846/862; * pa_cpu 顺序 PX30 → RK3328 → RK3588 → PX30K …)。本项目导入时按字母序写入,需要修复。 * 做法:解析 dump 的 wp_terms(term_id→name)与 wp_term_taxonomy(插入顺序), * 对每个 pa_<属性 slug>,按顺序把 product_attribute_values.sort 设为序号。 * 运行:bun scripts/import-attribute-term-order.ts */ import { SQL } from "bun"; const DUMP = "/home/bigcode/data/dev/sites/dusuniot/legacy/dusuniot-db-20261029.sql"; const db = new SQL("postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot"); /** # 函数 提取某张表的 INSERT 段文本(支持多段 INSERT) */ function tableInserts(lines: string[], table: string): string[] { const out: string[] = []; let buf = ""; let on = false; for (const l of lines) { if (l.startsWith(`INSERT INTO \`${table}\` VALUES`)) { on = true; buf = l; } else if (on) { buf += l; } if (on && l.trimEnd().endsWith(";")) { out.push(buf); buf = ""; on = false; } } return out; } /** # 函数 解析 SQL 值元组 */ function parseTuples(s: string): string[][] { const out: string[][] = []; let p = s.indexOf("("); while (p >= 0 && p < s.length) { const values: string[] = []; let cur = ""; let inQ = false; let esc = false; let depth = 0; p++; for (; p < s.length; p++) { const ch = s[p]!; if (esc) { cur += ch; esc = false; continue; } if (inQ) { if (ch === "\\") { esc = true; continue; } if (ch === "'") { inQ = false; continue; } cur += ch; continue; } if (ch === "'") { inQ = true; continue; } if (ch === "(") { depth++; cur += ch; continue; } if (ch === ")") { if (depth === 0) { values.push(cur); cur = ""; p++; break; } depth--; cur += ch; continue; } if (ch === "," && depth === 0) { values.push(cur); cur = ""; continue; } cur += ch; } out.push(values); p = s.indexOf("(", p); } return out; } const lines = (await Bun.file(DUMP).text()).split("\n"); // term_id → name const nameById = new Map(); for (const seg of tableInserts(lines, "wp_terms")) { for (const v of parseTuples(seg)) { if (v[0]) nameById.set(Number(v[0]), v[1] ?? ""); } } console.log("terms:", nameById.size); // taxonomy → [term names](按 dump 插入顺序) const byTax = new Map(); for (const seg of tableInserts(lines, "wp_term_taxonomy")) { for (const v of parseTuples(seg)) { const taxonomy = v[2] ?? ""; const termId = Number(v[1]); if (!taxonomy.startsWith("pa_")) continue; const name = nameById.get(termId); if (!name) continue; const list = byTax.get(taxonomy) ?? []; list.push(name); byTax.set(taxonomy, list); } } let updated = 0; for (const [taxonomy, names] of byTax) { const attrSlug = taxonomy.slice(3); for (let i = 0; i < names.length; i++) { const res = await db` UPDATE product_attribute_values v SET sort = ${i} FROM product_attributes a WHERE v.attribute_id = a.id AND a.slug = ${attrSlug} AND lower(trim(v.value)) = lower(trim(${names[i]!}))`; updated += res.count; } console.log(taxonomy, "→", names.length, "terms"); } console.log("updated rows:", updated); await db.end();