Files
DSP-routing-daily/index.html
T

369 lines
18 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DSP Routing Daily 报告</title>
<style>
:root{--bg:#f5f6f8;--card:#fff;--line:#e3e6ea;--head:#1f2937;--sub:#6b7280;--accent:#2563eb;--accent-bg:#eff4ff;}
*{box-sizing:border-box;}
body{margin:0;background:var(--bg);color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:13px;}
.wrap{max-width:1500px;margin:0 auto;padding:24px 20px 60px;}
h1{font-size:20px;margin:0 0 4px;}
.meta{color:var(--sub);font-size:12px;margin-bottom:20px;}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 18px;margin-bottom:22px;box-shadow:0 1px 2px rgba(0,0,0,.03);}
.card h2{font-size:15px;margin:0 0 12px;display:flex;align-items:center;gap:8px;}
.card h2 .tag{font-size:11px;font-weight:normal;color:var(--sub);background:var(--bg);border:1px solid var(--line);padding:1px 8px;border-radius:20px;}
table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums;}
th,td{border:1px solid var(--line);padding:7px 10px;text-align:right;white-space:nowrap;}
th{background:var(--head);color:#fff;font-weight:600;text-align:center;position:sticky;top:0;cursor:default;}
td:first-child,th:first-child{text-align:center;}
.detail-scroll{max-height:620px;overflow:auto;border:1px solid var(--line);border-radius:8px;}
.detail-scroll table th{top:0;}
tbody tr:nth-child(even){background:#fafbfc;}
tbody tr:hover{background:var(--accent-bg);}
.txt{text-align:left;}
.filters{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end;margin-bottom:14px;}
.fld{display:flex;flex-direction:column;gap:4px;}
.fld label{font-size:11px;color:var(--sub);}
.fld input,.fld select{border:1px solid var(--line);border-radius:6px;padding:6px 8px;font-size:13px;min-width:150px;background:#fff;}
.fld input[type=date]{min-width:140px;}
.btn{border:1px solid var(--accent);background:var(--accent);color:#fff;border-radius:6px;padding:7px 16px;font-size:13px;cursor:pointer;}
.btn.ghost{background:#fff;color:var(--accent);}
.count-note{color:var(--sub);font-size:12px;margin:6px 2px 0;}
.pos{color:#059669;} .neg{color:#dc2626;}
.empty{padding:30px;text-align:center;color:var(--sub);}
.sum-metric{color:var(--accent);font-weight:600;}
.note-list{margin:0 0 14px;padding:10px 14px 10px 28px;background:var(--bg);border:1px solid var(--line);border-radius:8px;color:#4b5563;font-size:12px;line-height:1.9;}
.note-list b{color:var(--head);}
.total-row td{position:sticky;top:33px;background:#eef2f7;font-weight:700;color:var(--head);border-top:2px solid #cbd5e1;z-index:1;}
.total-row td.txt{text-align:left;}
th.sortable,th.fsortable{cursor:pointer;user-select:none;}
th.sortable:hover,th.fsortable:hover{background:#374151;}
th .arrow{display:inline-block;width:10px;margin-left:3px;font-size:10px;color:#93c5fd;}
th.sort-asc .arrow::after{content:"\25B2";}
th.sort-desc .arrow::after{content:"\25BC";}
</style>
</head>
<body>
<div class="wrap">
<h1>DSP Routing Daily 报告</h1>
<div class="meta" id="meta">加载中…</div>
<div class="card">
<h2>每日总表 <span class="tag">保留近3天</span></h2>
<ul class="note-list">
<li><b>日期</b>:查询当日,有点击订单等数据来自前一天</li>
<li><b>原始订单</b>:全部可融合订单【DSP Campaign(可融合)】</li>
<li><b>pkg</b>:原始订单按照 pkg+geo 去重后的订单数</li>
<li><b>中转订单</b>:已 link 订单【Order List】</li>
</ul>
<div style="overflow:auto;">
<table id="summaryTable">
<thead>
<tr>
<th>日期</th><th>原始订单</th><th>pkg</th><th>中转订单</th>
<th>有点击订单</th><th>无转化订单</th><th>无点击订单</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="card">
<h2>Kibana 监测明细表 <span class="tag">按 adping_id 聚合 · 近14天</span></h2>
<div class="filters">
<div class="fld"><label>dt 起</label><input type="date" id="fDtFrom"></div>
<div class="fld"><label>dt 止</label><input type="date" id="fDtTo"></div>
<div class="fld"><label>融合后campaign id (routing_campaign_id)</label><input type="text" id="fRc" placeholder="模糊匹配"></div>
<div class="fld"><label>融合前campaign id (campaign_id)</label><input type="text" id="fCid" placeholder="模糊匹配"></div>
<div class="fld"><label>pkg</label><input type="text" id="fPkg" placeholder="模糊匹配"></div>
<div class="fld"><label>second_channel</label><select id="fSc"><option value="">全部</option></select></div>
<div class="fld"><label>account</label><select id="fAcc"><option value="">全部</option></select></div>
<button class="btn" id="applyBtn">筛选</button>
<button class="btn ghost" id="resetBtn">重置</button>
</div>
<div class="count-note" id="detailNote"></div>
<div class="detail-scroll">
<table id="detailTable">
<thead>
<tr>
<th class="sortable" data-key="dt" data-type="dt">dt<span class="arrow"></span></th>
<th class="sortable" data-key="routing_campaign_id" data-type="str">融合后campaign id<span class="arrow"></span></th>
<th class="sortable" data-key="campaign_id" data-type="str">融合前campaign id<span class="arrow"></span></th>
<th class="sortable" data-key="adping_id" data-type="num">adping_id<span class="arrow"></span></th>
<th class="sortable" data-key="second_channel" data-type="str">second_channel<span class="arrow"></span></th>
<th class="sortable" data-key="account" data-type="str">account<span class="arrow"></span></th>
<th class="sortable" data-key="pkg" data-type="str">pkg<span class="arrow"></span></th>
<th class="sortable" data-key="clicks" data-type="num">clicks<span class="arrow"></span></th>
<th class="sortable" data-key="conversion" data-type="num">conversion<span class="arrow"></span></th>
<th class="sortable" data-key="cvr" data-type="num">cvr<span class="arrow"></span></th>
<th class="sortable" data-key="revenue" data-type="num">revenue<span class="arrow"></span></th>
<th class="sortable" data-key="cost" data-type="num">cost<span class="arrow"></span></th>
<th class="sortable" data-key="gp" data-type="num">GP<span class="arrow"></span></th>
<th class="sortable" data-key="ecpc" data-type="num">ecpc<span class="arrow"></span></th>
</tr>
<tr id="totalRow" class="total-row">
<td colspan="7" class="txt">Total</td>
<td id="tClicks"></td>
<td id="tConv"></td>
<td id="tCvr"></td>
<td id="tRev"></td>
<td id="tCost"></td>
<td id="tGp"></td>
<td id="tEcpc"></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="card">
<h2>骗量排行榜 <span class="tag">前一天 · 按 account · 占比降序</span></h2>
<ul class="note-list">
<li>按 <b>campaign_id</b> 重新聚合(同 campaign_id 的所有 adping_id 的 clicks、conversion 累加)</li>
<li><b>可疑 campaign</b>:clicks 总数 &gt; 500 且 conversion = 0 的 campaign</li>
<li><b>骗量占比</b> = 可疑 campaign 数 / campaign 总数,占比越高越靠前</li>
</ul>
<div style="overflow:auto;">
<table id="fraudTable">
<thead>
<tr>
<th>排名</th>
<th class="txt fsortable" data-key="account" data-type="str">account<span class="arrow"></span></th>
<th class="fsortable" data-key="total" data-type="num">campaign 总数<span class="arrow"></span></th>
<th class="fsortable" data-key="suspicious" data-type="num">可疑 campaign 数<span class="arrow"></span><br><span style="font-weight:normal;font-size:11px;">(&gt;500点击 &amp; 0转化)</span></th>
<th class="fsortable" data-key="ratio" data-type="num">骗量占比<span class="arrow"></span></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<script>
var ALL = []; // 全部明细
var SUMMARY = [];
function num(n){ return (n==null||n==='')?'':Number(n).toLocaleString('en-US'); }
function pct(n){ if(n==null||n===''||isNaN(n)) return ''; return (Number(n)*100).toFixed(2)+'%'; }
function money(n){ if(n==null||n===''||isNaN(n)) return ''; return Number(n).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); }
function dec(n,d){ if(n==null||n===''||isNaN(n)) return ''; return Number(n).toFixed(d==null?4:d); }
function dtKey(s){ // "2026/9/9" -> 20260909 数值,便于比较
var p=String(s).split('/'); return parseInt(p[0])*10000+parseInt(p[1])*100+parseInt(p[2]);
}
function dtToInput(s){ var p=String(s).split('/'); return p[0]+'-'+String(p[1]).padStart(2,'0')+'-'+String(p[2]).padStart(2,'0'); }
function inputToKey(v){ if(!v) return null; var p=v.split('-'); return parseInt(p[0])*10000+parseInt(p[1])*100+parseInt(p[2]); }
function renderSummary(){
var tb=document.querySelector('#summaryTable tbody');
if(!SUMMARY.length){ tb.innerHTML='<tr><td colspan="7" class="empty">暂无数据</td></tr>'; return; }
tb.innerHTML = SUMMARY.slice().sort(function(a,b){return dtKey(b.date)-dtKey(a.date);}).map(function(r){
return '<tr><td>'+r.date+'</td>'
+'<td class="sum-metric">'+num(r.raw_orders)+'</td>'
+'<td>'+num(r.pkg)+'</td>'
+'<td class="sum-metric">'+num(r.relay_orders)+'</td>'
+'<td>'+num(r.has_click_orders)+'</td>'
+'<td>'+num(r.no_conv_orders)+'</td>'
+'<td>'+num(r.no_click_orders)+'</td></tr>';
}).join('');
}
function fillSelects(){
var scSet={}, accSet={};
ALL.forEach(function(d){ if(d.second_channel) scSet[d.second_channel]=1; if(d.account) accSet[d.account]=1; });
var scSel=document.getElementById('fSc'), accSel=document.getElementById('fAcc');
Object.keys(scSet).sort().forEach(function(v){ scSel.insertAdjacentHTML('beforeend','<option>'+v+'</option>'); });
Object.keys(accSet).sort().forEach(function(v){ accSel.insertAdjacentHTML('beforeend','<option>'+v+'</option>'); });
}
function currentFilter(d){
var from=inputToKey(document.getElementById('fDtFrom').value);
var to=inputToKey(document.getElementById('fDtTo').value);
var rc=document.getElementById('fRc').value.trim().toLowerCase();
var cid=document.getElementById('fCid').value.trim().toLowerCase();
var pkg=document.getElementById('fPkg').value.trim().toLowerCase();
var sc=document.getElementById('fSc').value;
var acc=document.getElementById('fAcc').value;
var k=dtKey(d.dt);
if(from!=null && k<from) return false;
if(to!=null && k>to) return false;
if(rc && String(d.routing_campaign_id).toLowerCase().indexOf(rc)<0) return false;
if(cid && String(d.campaign_id).toLowerCase().indexOf(cid)<0) return false;
if(pkg && String(d.pkg).toLowerCase().indexOf(pkg)<0) return false;
if(sc && d.second_channel!==sc) return false;
if(acc && d.account!==acc) return false;
return true;
}
var SORT_KEY='clicks', SORT_TYPE='num', SORT_DIR='desc';
function sortRows(rows){
var k=SORT_KEY, type=SORT_TYPE, dir=(SORT_DIR==='asc'?1:-1);
return rows.sort(function(a,b){
var va,vb;
if(type==='dt'){ va=dtKey(a.dt); vb=dtKey(b.dt); }
else if(type==='num'){ va=Number(a[k])||0; vb=Number(b[k])||0; }
else { va=String(a[k]||''); vb=String(b[k]||''); return dir*va.localeCompare(vb); }
if(va<vb) return -1*dir; if(va>vb) return 1*dir;
// 次级排序:dt 降序、clicks 降序,保持稳定观感
return dtKey(b.dt)-dtKey(a.dt) || (b.clicks-a.clicks);
});
}
function updateSortArrows(){
document.querySelectorAll('#detailTable th.sortable').forEach(function(th){
th.classList.remove('sort-asc','sort-desc');
if(th.getAttribute('data-key')===SORT_KEY){
th.classList.add(SORT_DIR==='asc'?'sort-asc':'sort-desc');
}
});
}
function renderDetail(){
var rows = sortRows(ALL.filter(currentFilter));
var tb=document.querySelector('#detailTable tbody');
if(!rows.length){ tb.innerHTML='<tr><td colspan="14" class="empty">无匹配数据</td></tr>'; }
else {
tb.innerHTML = rows.map(function(d){
var gpCls = (d.gp>=0)?'pos':'neg';
return '<tr>'
+'<td>'+d.dt+'</td>'
+'<td>'+d.routing_campaign_id+'</td>'
+'<td>'+d.campaign_id+'</td>'
+'<td>'+d.adping_id+'</td>'
+'<td class="txt">'+d.second_channel+'</td>'
+'<td class="txt">'+d.account+'</td>'
+'<td class="txt">'+d.pkg+'</td>'
+'<td>'+num(d.clicks)+'</td>'
+'<td>'+num(d.conversion)+'</td>'
+'<td>'+pct(d.cvr)+'</td>'
+'<td>'+money(d.revenue)+'</td>'
+'<td>'+money(d.cost)+'</td>'
+'<td class="'+gpCls+'">'+pct(d.gp)+'</td>'
+'<td>'+dec(d.ecpc,4)+'</td>'
+'</tr>';
}).join('');
}
document.getElementById('detailNote').textContent='显示 '+rows.length+' / '+ALL.length+' 行';
renderTotal(rows);
updateSortArrows();
}
var FRAUD = [];
var FSORT_KEY='ratio', FSORT_TYPE='num', FSORT_DIR='desc';
function computeFraud(){
FRAUD=[];
if(!ALL.length) return;
var maxDt = ALL.reduce(function(m,d){ return dtKey(d.dt)>dtKey(m)?d.dt:m; }, ALL[0].dt);
var rows = ALL.filter(function(d){ return d.dt===maxDt; });
var byAcc = {};
rows.forEach(function(d){
var acc=d.account||'未知', cid=String(d.campaign_id||'');
if(!byAcc[acc]) byAcc[acc]={};
if(!byAcc[acc][cid]) byAcc[acc][cid]={clicks:0,conv:0};
byAcc[acc][cid].clicks += (+d.clicks||0);
byAcc[acc][cid].conv += (+d.conversion||0);
});
FRAUD = Object.keys(byAcc).map(function(acc){
var camps = byAcc[acc], ids = Object.keys(camps);
var total = ids.length;
var suspicious = ids.filter(function(cid){ return camps[cid].clicks>500 && camps[cid].conv===0; }).length;
return { account:acc, total:total, suspicious:suspicious, ratio: total? suspicious/total : 0 };
});
}
function renderFraud(){
var dir=(FSORT_DIR==='asc'?1:-1), k=FSORT_KEY, type=FSORT_TYPE;
var list = FRAUD.slice().sort(function(a,b){
if(type==='str'){ return dir*String(a[k]).localeCompare(String(b[k])); }
var va=Number(a[k])||0, vb=Number(b[k])||0;
return (va<vb?-1:va>vb?1:0)*dir || b.ratio-a.ratio || b.suspicious-a.suspicious;
});
var tb=document.querySelector('#fraudTable tbody');
if(!list.length){ tb.innerHTML='<tr><td colspan="5" class="empty">暂无数据</td></tr>'; }
else {
tb.innerHTML = list.map(function(r,i){
var hot = r.ratio>=0.5 ? 'neg' : '';
return '<tr>'
+'<td>'+(i+1)+'</td>'
+'<td class="txt">'+r.account+'</td>'
+'<td>'+num(r.total)+'</td>'
+'<td>'+num(r.suspicious)+'</td>'
+'<td class="'+hot+'">'+pct(r.ratio)+'</td>'
+'</tr>';
}).join('');
}
document.querySelectorAll('#fraudTable th.fsortable').forEach(function(th){
th.classList.remove('sort-asc','sort-desc');
if(th.getAttribute('data-key')===FSORT_KEY) th.classList.add(FSORT_DIR==='asc'?'sort-asc':'sort-desc');
});
}
function renderTotal(rows){
var c=0,cv=0,rev=0,cost=0;
rows.forEach(function(d){ c+=(+d.clicks||0); cv+=(+d.conversion||0); rev+=(+d.revenue||0); cost+=(+d.cost||0); });
var cvr = c? cv/c : 0;
var gp = rev? 1-cost/rev : 0;
var ecpc= c? rev/c : 0;
document.getElementById('tClicks').textContent=num(c);
document.getElementById('tConv').textContent=num(cv);
document.getElementById('tCvr').textContent=pct(cvr);
document.getElementById('tRev').textContent=money(rev);
document.getElementById('tCost').textContent=money(cost);
var tGp=document.getElementById('tGp');
tGp.textContent=pct(gp); tGp.className=(gp>=0)?'pos':'neg';
document.getElementById('tEcpc').textContent=dec(ecpc,4);
}
document.querySelectorAll('#detailTable th.sortable').forEach(function(th){
th.addEventListener('click',function(){
var k=th.getAttribute('data-key'), type=th.getAttribute('data-type');
if(SORT_KEY===k){ SORT_DIR=(SORT_DIR==='asc'?'desc':'asc'); }
else { SORT_KEY=k; SORT_TYPE=type; SORT_DIR='desc'; }
renderDetail();
});
});
document.querySelectorAll('#fraudTable th.fsortable').forEach(function(th){
th.addEventListener('click',function(){
var key=th.getAttribute('data-key'), type=th.getAttribute('data-type');
if(FSORT_KEY===key){ FSORT_DIR=(FSORT_DIR==='asc'?'desc':'asc'); }
else { FSORT_KEY=key; FSORT_TYPE=type; FSORT_DIR='desc'; }
renderFraud();
});
});
document.getElementById('applyBtn').onclick=renderDetail;
document.getElementById('resetBtn').onclick=function(){
['fDtFrom','fDtTo','fRc','fCid','fPkg'].forEach(function(id){document.getElementById(id).value='';});
document.getElementById('fSc').value=''; document.getElementById('fAcc').value='';
renderDetail();
};
fetch('data.json?_='+Date.now()).then(function(r){return r.json();}).then(function(d){
SUMMARY = d.summary||[];
ALL = d.detail||[];
document.getElementById('meta').textContent='数据更新时间:'+(d.updated_at||'—')+' | 总表 '+SUMMARY.length+' 天 | 明细 '+ALL.length+' 行';
renderSummary();
computeFraud();
renderFraud();
fillSelects();
// 默认 dt 范围 = 数据里的最早~最新
if(ALL.length){
var ks=ALL.map(function(x){return x.dt;}).sort(function(a,b){return dtKey(a)-dtKey(b);});
document.getElementById('fDtFrom').value=dtToInput(ks[0]);
document.getElementById('fDtTo').value=dtToInput(ks[ks.length-1]);
}
renderDetail();
}).catch(function(e){
document.getElementById('meta').textContent='数据加载失败:'+e;
});
</script>
</body>
</html>