// 单品级深度分析 v2 import fs from 'node:fs'; const J = JSON.parse(fs.readFileSync('.cdp/out/items-7d-30d.json', 'utf8')); const N = s => Number(s) || 0; const L = []; const say = s => L.push(s); const MODELS = [ ['小天才 D2A/Q2A', /D2A|Q2A|D2\b|Q1A|D2\b/i], ['小天才 D3 系', /D3S|D3\b|W2028/i], ['小天才 D5', /D5\b/i], ['小天才 Q3', /Q3\b/i], ['小天才 Z6Pro', /Z6Pro|Z6\b/i], ['小天才 Z7/Z7S', /Z7/i], ['小天才 Z8', /Z8/i], ['小天才 Z9/Z9少年', /Z9/i], ['小天才 Z10/Z11', /Z1[01]/i], ['小天才 X10/保护壳', /X10|保护[壳套]/i], ['小天才 Y6/其他', /Y6|Z2A|Z5|Z1\b/i], ['小米手环 9Pro/8Pro', /手环\s*9Pro|手环\s*8Pro|9Pro|8Pro/i], ['小米手环 10', /手环\s*10|手环10/i], ['小米手环 3/其他', /手环\s*3|手环\s*4|手环\s*5|手环\s*6|手环\s*7/i], ['Redmi Watch 3/5/6', /watch\s*[356]|watch[356]/i], ['华为 儿童 4Pro/5', /4Pro|华为.*5|活力版/i], ['华为 Band 8/9', /Band\s*[89]|ASK-B19|B29/i], ['Keep 手环', /Keep/i], ['防丢挂绳', /挂绳|防丢/i], ['充电线/充电器', /充电[线器]|数据线/i], ['卡盖/卡托/螺丝杆', /螺丝杆|卡盖|卡托|拆装/i], ['底托/支架', /底托|支架|底座/i], ['组合装', /组合|\+|套装/i], ['表带(未识别型号)', /表带|腕带/i], ]; for (const dt of ['recent7d', 'recent30d']) { const d = J.data[dt]; if (!d) continue; const items = d.items; const label = dt === 'recent7d' ? '近7天' : '近30天'; const sA = items.reduce((a, b) => a + N(b.amt), 0); const sO = items.reduce((a, b) => a + N(b.ord), 0); const sP = items.reduce((a, b) => a + N(b.pv), 0); const sI = items.reduce((a, b) => a + N(b.ipv), 0); say(`\n${'='.repeat(104)}`); say(`## ${label} 商品数=${items.length} 订单=${sO} 金额=¥${sA.toFixed(2)} 曝光=${sP} 浏览=${sI}`); say(` 对账缺口:总览订单 102/584、金额 1106.70/5870.30 → 缺口 ${dt === 'recent7d' ? '5单/¥89.00' : '待算'}`); say(`\n### ${label} · 零浏览商品(ipv=0,即"被浏览商品数"口径)`); const zeroI = items.filter(x => N(x.ipv) === 0); say(` 共 ${zeroI.length} 个 / ${items.length}(占 ${(zeroI.length / items.length * 100).toFixed(1)}%),合计曝光 ${zeroI.reduce((a, b) => a + N(b.pv), 0)}`); say(`\n### ${label} · 型号关键词聚合(按成交金额)`); say(' 型号/方向 | 商品数 | 金额 | 单 | 曝光 | 询单 | 有单商品数 | 单/曝光‰'); const seen = new Set(); const rows = []; for (const [name, re] of MODELS) { const g = items.filter(x => re.test(String(x.name))); if (!g.length) continue; rows.push({ name, n: g.length, amt: g.reduce((a, b) => a + N(b.amt), 0), ord: g.reduce((a, b) => a + N(b.ord), 0), pv: g.reduce((a, b) => a + N(b.pv), 0), chat: g.reduce((a, b) => a + N(b.chat), 0), win: g.filter(x => N(x.ord) > 0).length, }); } rows.sort((a, b) => b.amt - a.amt); for (const r of rows) say(` ${r.name.padEnd(18)} | ${String(r.n).padStart(3)} | ¥${r.amt.toFixed(2).padStart(8)} | ${String(r.ord).padStart(3)} | ${String(r.pv).padStart(6)} | ${String(r.chat).padStart(3)} | ${String(r.win).padStart(2)}/${String(r.n).padStart(3)} | ${r.pv ? (r.ord / r.pv * 1000).toFixed(2) : '-'}`); if (dt === 'recent30d') { say(`\n### ${label} · 支付金额 Top30`); say(' # | 商品ID | 金额 | 单 | 曝光 | 浏览人 | 询单 | 转化率 | 价 | 天 | 类目 | 名称'); [...items].sort((a, b) => N(b.amt) - N(a.amt)).slice(0, 30).forEach((x, i) => say( ` ${String(i + 1).padStart(2)} | ${x.id} | ¥${N(x.amt).toFixed(2).padStart(7)} | ${String(x.ord).padStart(2)} | ${String(x.pv).padStart(5)} | ${String(x.ipvUv).padStart(4)} | ${String(x.chat).padStart(2)} | ${String(x.cvr).padStart(7)} | ¥${x.price.padStart(5)} | ${String(x.days).padStart(3)} | ${String(x.c4).padEnd(7)} | ${String(x.name).slice(0, 26)}`)); say(`\n### ${label} · 高曝光零成交(曝光>=500 且 0 单)`); const w = items.filter(x => N(x.pv) >= 500 && N(x.ord) === 0).sort((a, b) => N(b.pv) - N(a.pv)); w.forEach(x => say(` ${x.id} 曝光${String(x.pv).padStart(5)} 浏览人${String(x.ipvUv).padStart(4)} 询单${String(x.chat).padStart(2)} ¥${x.price.padStart(6)} ${String(x.c4).padEnd(7)} | ${String(x.name).slice(0, 34)}`)); say(` 合计 ${w.length} 个,吃掉曝光 ${w.reduce((a, b) => a + N(b.pv), 0)}`); say(`\n### ${label} · 类目聚合`); const byC = {}; for (const x of items) { const k = x.c4 || '未知'; byC[k] = byC[k] || { n: 0, amt: 0, ord: 0, pv: 0, win: 0 }; byC[k].n++; byC[k].amt += N(x.amt); byC[k].ord += N(x.ord); byC[k].pv += N(x.pv); if (N(x.ord) > 0) byC[k].win++; } say(' 类目 | 商品数 | 金额 | 单 | 曝光 | 有单商品 | 单/曝光‰'); Object.entries(byC).sort((a, b) => b[1].amt - a[1].amt).forEach(([k, v]) => say( ` ${k.padEnd(12)} | ${String(v.n).padStart(3)} | ¥${v.amt.toFixed(2).padStart(8)} | ${String(v.ord).padStart(3)} | ${String(v.pv).padStart(6)} | ${String(v.win).padStart(2)}/${String(v.n).padStart(3)} | ${v.pv ? (v.ord / v.pv * 1000).toFixed(2) : '-'}`)); } } fs.writeFileSync('.cdp/out/items-analysis2.md', L.join('\n'), 'utf8');