Files
ObsidianHome/小鱼/.cdp/items-analyze.mjs
T
2026-11-05 15:16:24 +08:00

54 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 单品级分析
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); console.log(s); };
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 sumAmt = items.reduce((a, b) => a + N(b.amt), 0);
const sumOrd = items.reduce((a, b) => a + N(b.ord), 0);
const sumPv = items.reduce((a, b) => a + N(b.pv), 0);
say('\n' + '='.repeat(100));
say(`### ${label} 商品数=${items.length} 合计订单=${sumOrd} 合计金额=¥${sumAmt.toFixed(2)} 合计曝光=${sumPv}`);
say(` (对账:总览支付笔数 7天=102 / 30天=584;金额 7天=1106.70 / 30天=5870.30)`);
const top = [...items].sort((a, b) => N(b.amt) - N(a.amt)).slice(0, 25);
say(`\n--- ${label} 支付金额 Top25 ---`);
say('排名 | 商品ID | 金额 | 单 | 曝光 | 浏览人数 | 询单 | 转化率 | 价 | 在架天 | 名称');
top.forEach((x, i) => say(
`${String(i + 1).padStart(2)} | ${x.id} | ¥${String(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(6)} | ¥${x.price} | ${x.days} | ${String(x.name).slice(0, 30)}`));
const waste = items.filter(x => N(x.pv) >= 400 && N(x.ord) === 0).sort((a, b) => N(b.pv) - N(a.pv));
say(`\n--- ${label} 高曝光零成交(曝光>=400 且 0 单):${waste.length} 个 ---`);
waste.slice(0, 20).forEach(x => say(
` ${x.id} 曝光${String(x.pv).padStart(5)} 浏览人${String(x.ipvUv).padStart(4)} 询单${String(x.chat).padStart(2)} ¥${x.price} ${x.c4} | ${String(x.name).slice(0, 34)}`));
say(` 这 ${waste.length} 个商品合计吃掉曝光 ${waste.reduce((a, b) => a + N(b.pv), 0)}`);
const zero = items.filter(x => N(x.pv) === 0);
say(`\n--- ${label} 零曝光商品:${zero.length} 个(占 ${(zero.length / items.length * 100).toFixed(1)}%)---`);
const news = items.filter(x => N(x.days) <= 21).sort((a, b) => N(b.ord) - N(a.ord));
say(`\n--- ${label} 新品(在架<=21天):${news.length} 个,有成交 ${news.filter(x => N(x.ord) > 0).length} 个 ---`);
news.slice(0, 22).forEach(x => say(
` ${x.id} ${String(x.days).padStart(2)}天 曝光${String(x.pv).padStart(5)} 浏览人${String(x.ipvUv).padStart(4)} 询单${String(x.chat).padStart(2)} ${String(x.ord).padStart(2)}单 ¥${String(N(x.amt).toFixed(2)).padStart(7)} cvr${String(x.cvr).padStart(7)} ¥${x.price} ${x.c4} | ${String(x.name).slice(0, 30)}`));
const byC = {};
for (const x of items) {
const k = x.c4 || '未知';
byC[k] = byC[k] || { n: 0, amt: 0, ord: 0, pv: 0, chat: 0 };
byC[k].n++; byC[k].amt += N(x.amt); byC[k].ord += N(x.ord); byC[k].pv += N(x.pv); byC[k].chat += N(x.chat);
}
say(`\n--- ${label} 类目聚合 Top18(按金额)---`);
say('类目 | 商品数 | 金额 | 单 | 曝光 | 询单 | 单/曝光%');
Object.entries(byC).sort((a, b) => b[1].amt - a[1].amt).slice(0, 18).forEach(([k, v]) => say(
`${k} | ${v.n} | ¥${v.amt.toFixed(2)} | ${v.ord} | ${v.pv} | ${v.chat} | ${v.pv ? (v.ord / v.pv * 100).toFixed(3) : '-'}`));
}
fs.writeFileSync('.cdp/out/items-analysis.md', L.join('\n'), 'utf8');
console.log('\nwritten .cdp/out/items-analysis.md');