Add Ozon country CAC report
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
{
|
||||
"title": "Ozon · 过去14天国家投放表现",
|
||||
"generated_at": "2026-08-24T15:42:16+08:00",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"period": {
|
||||
"start": "2026-08-10",
|
||||
"end": "2026-08-23",
|
||||
"days": 14
|
||||
},
|
||||
"countries": [
|
||||
"KZ",
|
||||
"UZ",
|
||||
"KG",
|
||||
"AM"
|
||||
],
|
||||
"summary": [
|
||||
{
|
||||
"country": "KZ",
|
||||
"installs": 12980.0,
|
||||
"spend": 9086.0,
|
||||
"avg_cac": 807.3846,
|
||||
"cac_days": 13
|
||||
},
|
||||
{
|
||||
"country": "UZ",
|
||||
"installs": 5630.0,
|
||||
"spend": 2815.0,
|
||||
"avg_cac": 5723.6923,
|
||||
"cac_days": 13
|
||||
},
|
||||
{
|
||||
"country": "KG",
|
||||
"installs": 1724.0,
|
||||
"spend": 862.0,
|
||||
"avg_cac": 5302.0,
|
||||
"cac_days": 10
|
||||
},
|
||||
{
|
||||
"country": "AM",
|
||||
"installs": 971.0,
|
||||
"spend": 485.5,
|
||||
"avg_cac": 3127.553,
|
||||
"cac_days": 10
|
||||
}
|
||||
],
|
||||
"records": [
|
||||
{
|
||||
"date": "2026-08-10",
|
||||
"country": "KZ",
|
||||
"installs": 1075.0,
|
||||
"spend": 752.5,
|
||||
"cac": 636.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "KZ",
|
||||
"installs": 1027.0,
|
||||
"spend": 718.9,
|
||||
"cac": 745.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "KZ",
|
||||
"installs": 1034.0,
|
||||
"spend": 723.8,
|
||||
"cac": 954.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "KZ",
|
||||
"installs": 1097.0,
|
||||
"spend": 767.9,
|
||||
"cac": 837.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "KZ",
|
||||
"installs": 1072.0,
|
||||
"spend": 750.4,
|
||||
"cac": 724.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "KZ",
|
||||
"installs": 718.0,
|
||||
"spend": 502.6,
|
||||
"cac": 837.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "KZ",
|
||||
"installs": 675.0,
|
||||
"spend": 472.5,
|
||||
"cac": 1062.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "KZ",
|
||||
"installs": 715.0,
|
||||
"spend": 500.5,
|
||||
"cac": 702.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "KZ",
|
||||
"installs": 682.0,
|
||||
"spend": 477.4,
|
||||
"cac": 677.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "KZ",
|
||||
"installs": 612.0,
|
||||
"spend": 428.4,
|
||||
"cac": 597.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "KZ",
|
||||
"installs": 602.0,
|
||||
"spend": 421.4,
|
||||
"cac": 770.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "KZ",
|
||||
"installs": 1117.0,
|
||||
"spend": 781.9,
|
||||
"cac": 909.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "KZ",
|
||||
"installs": 1298.0,
|
||||
"spend": 908.6,
|
||||
"cac": 1046.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "KZ",
|
||||
"installs": 1256.0,
|
||||
"spend": 879.2,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-10",
|
||||
"country": "UZ",
|
||||
"installs": 550.0,
|
||||
"spend": 275.0,
|
||||
"cac": 4493.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "UZ",
|
||||
"installs": 614.0,
|
||||
"spend": 307.0,
|
||||
"cac": 5103.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "UZ",
|
||||
"installs": 614.0,
|
||||
"spend": 307.0,
|
||||
"cac": 3349.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "UZ",
|
||||
"installs": 608.0,
|
||||
"spend": 304.0,
|
||||
"cac": 7011.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "UZ",
|
||||
"installs": 587.0,
|
||||
"spend": 293.5,
|
||||
"cac": 2545.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "UZ",
|
||||
"installs": 224.0,
|
||||
"spend": 112.0,
|
||||
"cac": 3768.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "UZ",
|
||||
"installs": 176.0,
|
||||
"spend": 88.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "UZ",
|
||||
"installs": 211.0,
|
||||
"spend": 105.5,
|
||||
"cac": 8147.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "UZ",
|
||||
"installs": 170.0,
|
||||
"spend": 85.0,
|
||||
"cac": 20293.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "UZ",
|
||||
"installs": 141.0,
|
||||
"spend": 70.5,
|
||||
"cac": 8912.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "UZ",
|
||||
"installs": 124.0,
|
||||
"spend": 62.0,
|
||||
"cac": 3829.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "UZ",
|
||||
"installs": 509.0,
|
||||
"spend": 254.5,
|
||||
"cac": 3233.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "UZ",
|
||||
"installs": 530.0,
|
||||
"spend": 265.0,
|
||||
"cac": 3725.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "UZ",
|
||||
"installs": 572.0,
|
||||
"spend": 286.0,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-10",
|
||||
"country": "KG",
|
||||
"installs": 197.0,
|
||||
"spend": 98.5,
|
||||
"cac": 2186.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "KG",
|
||||
"installs": 196.0,
|
||||
"spend": 98.0,
|
||||
"cac": 2537.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "KG",
|
||||
"installs": 186.0,
|
||||
"spend": 93.0,
|
||||
"cac": 3633.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "KG",
|
||||
"installs": 179.0,
|
||||
"spend": 89.5,
|
||||
"cac": 20857.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "KG",
|
||||
"installs": 167.0,
|
||||
"spend": 83.5,
|
||||
"cac": 3520.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "KG",
|
||||
"installs": 31.0,
|
||||
"spend": 15.5,
|
||||
"cac": 2545.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "KG",
|
||||
"installs": 26.0,
|
||||
"spend": 13.0,
|
||||
"cac": 15506.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "KG",
|
||||
"installs": 30.0,
|
||||
"spend": 15.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "KG",
|
||||
"installs": 31.0,
|
||||
"spend": 15.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "KG",
|
||||
"installs": 22.0,
|
||||
"spend": 11.0,
|
||||
"cac": 2236.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "KG",
|
||||
"installs": 28.0,
|
||||
"spend": 14.0,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "KG",
|
||||
"installs": 192.0,
|
||||
"spend": 96.0,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "KG",
|
||||
"installs": 210.0,
|
||||
"spend": 105.0,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "KG",
|
||||
"installs": 229.0,
|
||||
"spend": 114.5,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-10",
|
||||
"country": "AM",
|
||||
"installs": 124.0,
|
||||
"spend": 62.0,
|
||||
"cac": 7132.05
|
||||
},
|
||||
{
|
||||
"date": "2026-08-11",
|
||||
"country": "AM",
|
||||
"installs": 136.0,
|
||||
"spend": 68.0,
|
||||
"cac": 7806.27
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"country": "AM",
|
||||
"installs": 126.0,
|
||||
"spend": 63.0,
|
||||
"cac": 3517.38
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"country": "AM",
|
||||
"installs": 124.0,
|
||||
"spend": 62.0,
|
||||
"cac": 7146.1
|
||||
},
|
||||
{
|
||||
"date": "2026-08-14",
|
||||
"country": "AM",
|
||||
"installs": 112.0,
|
||||
"spend": 56.0,
|
||||
"cac": 1686.59
|
||||
},
|
||||
{
|
||||
"date": "2026-08-15",
|
||||
"country": "AM",
|
||||
"installs": 19.0,
|
||||
"spend": 9.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"country": "AM",
|
||||
"installs": 25.0,
|
||||
"spend": 12.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"country": "AM",
|
||||
"installs": 30.0,
|
||||
"spend": 15.0,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-18",
|
||||
"country": "AM",
|
||||
"installs": 28.0,
|
||||
"spend": 14.0,
|
||||
"cac": 3987.14
|
||||
},
|
||||
{
|
||||
"date": "2026-08-19",
|
||||
"country": "AM",
|
||||
"installs": 19.0,
|
||||
"spend": 9.5,
|
||||
"cac": 0.0
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
"country": "AM",
|
||||
"installs": 15.0,
|
||||
"spend": 7.5,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-21",
|
||||
"country": "AM",
|
||||
"installs": 75.0,
|
||||
"spend": 37.5,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-22",
|
||||
"country": "AM",
|
||||
"installs": 67.0,
|
||||
"spend": 33.5,
|
||||
"cac": null
|
||||
},
|
||||
{
|
||||
"date": "2026-08-23",
|
||||
"country": "AM",
|
||||
"installs": 71.0,
|
||||
"spend": 35.5,
|
||||
"cac": null
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"installs": "Google Sheet 各国家区块的 Install",
|
||||
"spend": "Google Sheet 各国家区块的 Daily Spend(USD)",
|
||||
"cac": "Google Sheet 各国家区块的 CAC from client report;汇总值为有数据日期的日均 CAC"
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Ozon 国家投放表现</title>
|
||||
<style>
|
||||
:root{--bg:#f4f7fb;--card:#fff;--text:#14213d;--muted:#6b7280;--line:#e6eaf0;--blue:#5577ee;--shadow:0 8px 30px rgba(24,39,75,.06)}
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,"Microsoft YaHei",system-ui,sans-serif}.shell{max-width:1380px;margin:auto;padding:30px}.hero{background:linear-gradient(125deg,#111827,#26355e);color:white;border-radius:22px;padding:28px 30px;display:flex;justify-content:space-between;gap:20px;align-items:end;box-shadow:0 16px 40px rgba(17,24,39,.15)}h1{margin:0 0 8px;font-size:29px}.hero p{margin:0;color:#cbd5e1;font-size:13px}.updated{text-align:right;font-size:12px;color:#cbd5e1;line-height:1.6}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}.card,.panel{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}.card{padding:18px}.label{font-size:12px;color:var(--muted);margin-bottom:8px}.value{font-size:26px;font-weight:800}.hint{font-size:11px;color:#94a3b8;margin-top:6px}.panel{padding:20px;margin-top:15px}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px}.panel-title{font-size:17px;font-weight:800}.controls{display:flex;gap:9px;flex-wrap:wrap}button,select{border:1px solid var(--line);background:white;color:var(--text);padding:9px 12px;border-radius:9px;font:inherit;cursor:pointer}button.active{background:var(--blue);border-color:var(--blue);color:white}.chart-wrap{width:100%;overflow:auto}.chart{width:100%;min-width:760px;height:auto;display:block}.legend{display:flex;justify-content:center;gap:18px;flex-wrap:wrap;margin-top:8px;font-size:12px}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px}.tables{display:grid;grid-template-columns:.8fr 1.2fr;gap:15px}.table-wrap{overflow:auto;max-height:560px;border:1px solid var(--line);border-radius:11px}table{border-collapse:separate;border-spacing:0;width:100%;font-size:12px}th,td{padding:10px 12px;border-bottom:1px solid #edf0f5;text-align:right;white-space:nowrap}th{position:sticky;top:0;background:#f8fafc;color:#64748b;z-index:1}th:first-child,td:first-child{text-align:left}.country{font-weight:800}.error{padding:50px;text-align:center;color:#b42318}.footer{text-align:center;color:#94a3b8;font-size:11px;margin:20px}.skeleton{animation:pulse 1.2s infinite;background:#e9edf5;color:transparent;border-radius:5px}@keyframes pulse{50%{opacity:.55}}@media(max-width:950px){.grid{grid-template-columns:repeat(2,1fr)}.tables{grid-template-columns:1fr}}@media(max-width:620px){.shell{padding:14px}.hero{display:block}.updated{text-align:left;margin-top:14px}.grid{grid-template-columns:1fr 1fr}.value{font-size:21px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="hero"><div><h1>Ozon · 国家投放表现</h1><p id="period">正在加载过去 14 天数据…</p></div><div class="updated" id="updated">数据加载中</div></header>
|
||||
<div class="grid" id="kpis"><div class="card"><div class="label">总安装</div><div class="value skeleton">00000</div></div></div>
|
||||
<section class="panel"><div class="panel-head"><div class="panel-title">14 天趋势</div><div class="controls" id="metricButtons"><button data-metric="installs" class="active">安装数</button><button data-metric="spend">消耗</button><button data-metric="cac">CAC</button></div></div><div class="chart-wrap"><svg class="chart" id="chart" viewBox="0 0 1100 390" role="img" aria-label="国家趋势图"></svg></div><div class="legend" id="legend"></div></section>
|
||||
<div class="tables"><section class="panel"><div class="panel-head"><div class="panel-title">国家汇总</div></div><div class="table-wrap"><table><thead><tr><th>国家</th><th>安装数</th><th>消耗</th><th>日均 CAC</th></tr></thead><tbody id="summaryRows"></tbody></table></div></section><section class="panel"><div class="panel-head"><div class="panel-title">每日明细</div><select id="countryFilter"><option value="">全部国家</option></select></div><div class="table-wrap"><table><thead><tr><th>日期</th><th>国家</th><th>安装数</th><th>消耗</th><th>CAC</th></tr></thead><tbody id="detailRows"></tbody></table></div></section></div>
|
||||
<div class="footer">数据来源:Google Sheets · Ozon_IronSpace · 每 24 小时自动更新</div>
|
||||
</div>
|
||||
<script>
|
||||
const colors={KZ:'#5577ee',UZ:'#32b67a',KG:'#f59e0b',AM:'#e4568b'};let report=null;let metric='installs';
|
||||
const $=id=>document.getElementById(id);const number=(v,d=0)=>Number(v||0).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});const money=v=>v==null?'—':'$'+number(v,2);const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
function render(){const totalI=report.summary.reduce((a,x)=>a+x.installs,0),totalS=report.summary.reduce((a,x)=>a+x.spend,0),cac=report.summary.map(x=>x.avg_cac).filter(x=>x!=null);$('period').textContent=`${report.period.start} 至 ${report.period.end} · ${report.period.days} 个完整自然日`;$('updated').innerHTML=`最近更新:${report.generated_at.replace('T',' ')}<br>时区:${report.timezone}`;$('kpis').innerHTML=[['总安装',number(totalI),'四国合计'],['总消耗',money(totalS),'USD'],['平均 CAC',cac.length?number(cac.reduce((a,b)=>a+b,0)/cac.length,2):'—','来源表原始单位 · 各国家日均值'],['覆盖国家',report.countries.length,'KZ · UZ · KG · AM']].map(x=>`<div class="card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div><div class="hint">${x[2]}</div></div>`).join('');$('summaryRows').innerHTML=report.summary.map(x=>`<tr><td class="country" style="color:${colors[x.country]}">${x.country}</td><td>${number(x.installs)}</td><td>${money(x.spend)}</td><td>${number(x.avg_cac,2)}</td></tr>`).join('');$('countryFilter').innerHTML='<option value="">全部国家</option>'+report.countries.map(x=>`<option>${x}</option>`).join('');renderDetails();drawChart()}
|
||||
function renderDetails(){const country=$('countryFilter').value,rows=report.records.filter(x=>!country||x.country===country).sort((a,b)=>b.date.localeCompare(a.date)||a.country.localeCompare(b.country));$('detailRows').innerHTML=rows.map(x=>`<tr><td>${x.date}</td><td class="country" style="color:${colors[x.country]}">${x.country}</td><td>${number(x.installs)}</td><td>${money(x.spend)}</td><td>${x.cac==null?'—':number(x.cac,2)}</td></tr>`).join('')}
|
||||
function drawChart(){const svg=$('chart'),W=1100,H=390,m={l:70,r:25,t:30,b:58},iw=W-m.l-m.r,ih=H-m.t-m.b,dates=[...new Set(report.records.map(x=>x.date))].sort(),values=report.records.map(x=>x[metric]).filter(x=>x!=null),max=Math.max(...values,1)*1.12,x=i=>m.l+(dates.length===1?iw/2:i*iw/(dates.length-1)),y=v=>m.t+ih-(v/max)*ih,fmt=metric==='installs'?v=>number(v):(metric==='spend'?v=>money(v):v=>number(v,2));let h='';for(let i=0;i<=5;i++){const yy=m.t+ih*i/5,val=max*(1-i/5);h+=`<line x1="${m.l}" y1="${yy}" x2="${W-m.r}" y2="${yy}" stroke="#e8edf5"/><text x="${m.l-12}" y="${yy+4}" text-anchor="end" fill="#8190a5" font-size="11">${esc(fmt(val))}</text>`}dates.forEach((d,i)=>{if(i%2===0||dates.length<9)h+=`<text x="${x(i)}" y="${H-27}" text-anchor="middle" fill="#8190a5" font-size="11">${d.slice(5)}</text>`});for(const country of report.countries){const rows=dates.map(d=>report.records.find(r=>r.country===country&&r.date===d)),points=rows.map((r,i)=>r&&r[metric]!=null?`${x(i)},${y(r[metric])}`:null);let segment=[];const flush=()=>{if(segment.length>1)h+=`<polyline points="${segment.join(' ')}" fill="none" stroke="${colors[country]}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>`;segment=[]};points.forEach(p=>p?segment.push(p):flush());flush();rows.forEach((r,i)=>{if(r&&r[metric]!=null)h+=`<circle cx="${x(i)}" cy="${y(r[metric])}" r="4" fill="${colors[country]}" stroke="white" stroke-width="2"><title>${country} · ${r.date} · ${fmt(r[metric])}</title></circle>`})}svg.innerHTML=h;$('legend').innerHTML=report.countries.map(x=>`<span><i class="dot" style="background:${colors[x]}"></i>${x}</span>`).join('')}
|
||||
document.querySelectorAll('#metricButtons button').forEach(btn=>btn.onclick=()=>{metric=btn.dataset.metric;document.querySelectorAll('#metricButtons button').forEach(x=>x.classList.toggle('active',x===btn));drawChart()});$('countryFilter').onchange=renderDetails;
|
||||
fetch('data.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw new Error(`HTTP ${r.status}`);return r.json()}).then(data=>{report=data;render()}).catch(error=>{document.querySelector('.shell').innerHTML=`<div class="panel error"><h2>数据加载失败</h2><p>${esc(error.message)}</p><p>请稍后刷新页面。</p></div>`});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
24
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
TMP_FILE="$SCRIPT_DIR/data.json.tmp"
|
||||
OUTPUT_FILE="$SCRIPT_DIR/data.json"
|
||||
|
||||
python3 - "$TMP_FILE" <<'PY'
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
CSV_URL = (
|
||||
"https://docs.google.com/spreadsheets/d/"
|
||||
"1KaNOwaZ3DqjTXRK8XhF1OGF9qUmOh0NLXOgxVrh14h0/"
|
||||
"export?format=csv&gid=1445753035"
|
||||
)
|
||||
TIMEZONE = ZoneInfo("Asia/Shanghai")
|
||||
DAYS = 14
|
||||
BLOCKS = [
|
||||
{"country": "KZ", "date": 2, "geo": 3, "installs": 5, "spend": 8, "cac": 10},
|
||||
{"country": "UZ", "date": 14, "geo": 15, "installs": 17, "spend": 18, "cac": 20},
|
||||
{"country": "KG", "date": 25, "geo": 26, "installs": 28, "spend": 29, "cac": 31},
|
||||
{"country": "AM", "date": 35, "geo": 36, "installs": 38, "spend": 39, "cac": 41},
|
||||
]
|
||||
|
||||
|
||||
def parse_date(value):
|
||||
text = str(value or "").strip()
|
||||
for fmt in ("%Y/%m/%d", "%Y-%m-%d", "%Y%m%d", "%m/%d/%Y"):
|
||||
try:
|
||||
return datetime.strptime(text, fmt).date()
|
||||
except ValueError:
|
||||
pass
|
||||
match = re.fullmatch(r"(\d{4})[/.-](\d{1,2})[/.-](\d{1,2})", text)
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
return datetime(*map(int, match.groups())).date()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_number(value):
|
||||
text = str(value or "").strip().replace("\xa0", "").replace(",", "")
|
||||
if not text or text in {"-", "—"}:
|
||||
return None
|
||||
negative = text.startswith("(") and text.endswith(")")
|
||||
cleaned = re.sub(r"[^0-9.\-]", "", text)
|
||||
try:
|
||||
number = float(cleaned)
|
||||
return -number if negative else number
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def cell(row, index):
|
||||
return row[index] if index < len(row) else ""
|
||||
|
||||
|
||||
request = urllib.request.Request(CSV_URL, headers={"User-Agent": "AgentStage-Ozon-Report/1.0"})
|
||||
with urllib.request.urlopen(request, timeout=90) as response:
|
||||
text = response.read().decode("utf-8-sig")
|
||||
rows = list(csv.reader(text.splitlines()))
|
||||
|
||||
now = datetime.now(TIMEZONE)
|
||||
end_date = now.date() - timedelta(days=1)
|
||||
start_date = end_date - timedelta(days=DAYS - 1)
|
||||
aggregated = defaultdict(lambda: {"installs": 0.0, "spend": 0.0, "cac_values": []})
|
||||
|
||||
for row in rows:
|
||||
for block in BLOCKS:
|
||||
day = parse_date(cell(row, block["date"]))
|
||||
if day is None or not (start_date <= day <= end_date):
|
||||
continue
|
||||
country = str(cell(row, block["geo"])).strip().upper() or block["country"]
|
||||
if country != block["country"]:
|
||||
continue
|
||||
key = (country, day.isoformat())
|
||||
installs = parse_number(cell(row, block["installs"]))
|
||||
spend = parse_number(cell(row, block["spend"]))
|
||||
cac = parse_number(cell(row, block["cac"]))
|
||||
aggregated[key]["installs"] += installs or 0
|
||||
aggregated[key]["spend"] += spend or 0
|
||||
if cac is not None:
|
||||
aggregated[key]["cac_values"].append(cac)
|
||||
|
||||
records = []
|
||||
for country in [item["country"] for item in BLOCKS]:
|
||||
for offset in range(DAYS):
|
||||
day = (start_date + timedelta(days=offset)).isoformat()
|
||||
values = aggregated.get((country, day), {"installs": 0, "spend": 0, "cac_values": []})
|
||||
cac_values = values["cac_values"]
|
||||
records.append({
|
||||
"date": day,
|
||||
"country": country,
|
||||
"installs": round(values["installs"], 2),
|
||||
"spend": round(values["spend"], 4),
|
||||
"cac": round(sum(cac_values) / len(cac_values), 4) if cac_values else None,
|
||||
})
|
||||
|
||||
summary = []
|
||||
for country in [item["country"] for item in BLOCKS]:
|
||||
country_rows = [row for row in records if row["country"] == country]
|
||||
cac_values = [row["cac"] for row in country_rows if row["cac"] is not None]
|
||||
summary.append({
|
||||
"country": country,
|
||||
"installs": round(sum(row["installs"] for row in country_rows), 2),
|
||||
"spend": round(sum(row["spend"] for row in country_rows), 4),
|
||||
"avg_cac": round(sum(cac_values) / len(cac_values), 4) if cac_values else None,
|
||||
"cac_days": len(cac_values),
|
||||
})
|
||||
|
||||
payload = {
|
||||
"title": "Ozon · 过去14天国家投放表现",
|
||||
"generated_at": now.isoformat(timespec="seconds"),
|
||||
"timezone": "Asia/Shanghai",
|
||||
"period": {"start": start_date.isoformat(), "end": end_date.isoformat(), "days": DAYS},
|
||||
"countries": [item["country"] for item in BLOCKS],
|
||||
"summary": summary,
|
||||
"records": records,
|
||||
"definitions": {
|
||||
"installs": "Google Sheet 各国家区块的 Install",
|
||||
"spend": "Google Sheet 各国家区块的 Daily Spend(USD)",
|
||||
"cac": "Google Sheet 各国家区块的 CAC from client report;汇总值为有数据日期的日均 CAC",
|
||||
},
|
||||
}
|
||||
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||
PY
|
||||
|
||||
mv "$TMP_FILE" "$OUTPUT_FILE"
|
||||
echo "Updated $OUTPUT_FILE"
|
||||
Reference in New Issue
Block a user