DSP-daily 日报: 总日报+PKG波动+客户分类

This commit is contained in:
2026-09-11 16:41:53 +08:00
commit 0cd8d81f47
4 changed files with 545 additions and 0 deletions
+1
View File
File diff suppressed because one or more lines are too long
+261
View File
@@ -0,0 +1,261 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DSP Daily 日报</title>
<style>
:root{--bg:#f5f6f8;--card:#fff;--line:#e3e6ea;--head:#1f2937;--sub:#6b7280;--accent:#2563eb;}
*{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:1600px;margin:0 auto;padding:22px 18px 60px;}
h1{font-size:20px;margin:0 0 4px;}
.meta{color:var(--sub);font-size:12px;margin-bottom:16px;}
.tabs{display:flex;gap:6px;margin-bottom:18px;flex-wrap:wrap;}
.tab{padding:8px 16px;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer;font-size:13px;}
.tab.active{background:var(--accent);color:#fff;border-color:var(--accent);}
.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;}
.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;margin-left:6px;}
.scroll{overflow:auto;max-height:640px;border:1px solid var(--line);border-radius:8px;}
table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums;}
th,td{border:1px solid var(--line);padding:6px 9px;text-align:right;white-space:nowrap;}
th{background:var(--head);color:#fff;font-weight:600;text-align:center;position:sticky;top:0;z-index:2;}
td.txt,th.txt{text-align:left;}
tbody tr:nth-child(even){background:#fafbfc;}
tbody tr:hover{background:#eff4ff;}
.pos{color:#059669;} .neg{color:#dc2626;}
.hide{display:none;}
.reason-inp{width:260px;border:1px solid var(--line);border-radius:5px;padding:4px 6px;font-size:12px;}
.sticky-col{position:sticky;left:0;background:#fff;z-index:1;text-align:left;}
tbody tr:nth-child(even) .sticky-col{background:#fafbfc;}
.dod-hi-pos{background:#dcfce7;color:#059669;font-weight:600;}
.dod-hi-neg{background:#fee2e2;color:#dc2626;font-weight:600;}
.sub-note{color:var(--sub);font-size:12px;margin:4px 2px 10px;}
.total-row td{font-weight:700;background:#eef2f7;}
</style>
</head>
<body>
<div class="wrap">
<h1>DSP Daily 日报</h1>
<div class="meta" id="meta">加载中…</div>
<div class="tabs">
<div class="tab active" data-tab="overview">总日报</div>
<div class="tab" data-tab="pkg">PKG 收入波动</div>
<div class="tab" data-tab="cust">客户分类</div>
</div>
<!-- ===== 总日报 ===== -->
<div id="tab-overview">
<div class="card">
<h2>1.1 Overall <span class="tag">持续叠加</span></h2>
<div class="scroll"><table id="tOverall"><thead><tr>
<th>dt</th><th>DSP total revenue</th><th>DSP CN revenue</th><th>DSP international</th><th>波动(与前一天total差)</th>
</tr></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>1.2 CN <span class="tag">持续叠加 · 各account收入</span></h2>
<div class="scroll"><table id="tCN"><thead></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>1.3 International <span class="tag">持续叠加 · 各account收入</span></h2>
<div class="scroll"><table id="tIntl"><thead></thead><tbody></tbody></table></div>
</div>
</div>
<!-- ===== PKG 收入波动 ===== -->
<div id="tab-pkg" class="hide">
<div class="card">
<h2>2.1 PKG 收入波动 - Midas <span class="tag">保留14天</span></h2>
<div class="sub-note">DoD=最新日与前一日收入差;|DoD|&gt;100 标注,+绿 −红。波动原因可编辑(暂存本浏览器)。</div>
<div class="scroll"><table id="tPkgMidas"><thead></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>2.2 PKG 收入波动 - MS <span class="tag">保留14天</span></h2>
<div class="sub-note">DoD=最新日与前一日收入差;|DoD|&gt;100 标注,+绿 −红。波动原因可编辑(暂存本浏览器)。</div>
<div class="scroll"><table id="tPkgMS"><thead></thead><tbody></tbody></table></div>
</div>
</div>
<!-- ===== 客户分类 ===== -->
<div id="tab-cust" class="hide">
<div class="card">
<h2>3.1 客户分类日报</h2>
<div class="sub-note">DoD=T-1−T-2,DoD%=T-1/T-2−1。波动原因可编辑(暂存本浏览器)。</div>
<div class="scroll"><table id="tCustDaily"><thead><tr>
<th class="txt">客户分类</th><th>iDSP个数</th><th>base daily revenue</th>
<th>T-4收入</th><th>T-3收入</th><th>T-2收入</th><th>T-1收入</th><th>DoD</th><th>DoD%</th><th class="txt">波动原因</th>
</tr></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>3.2 客户明细 <span class="tag">保留14天</span></h2>
<div class="scroll"><table id="tCustDetail"><thead><tr>
<th class="txt">客户分类</th><th class="txt">客户</th><th>base收入</th><th>T-1收入</th><th>DoD(与T-2差)</th>
</tr></thead><tbody></tbody></table></div>
</div>
</div>
</div>
<script>
var D=null;
function n2(x){ if(x==null||x===''||isNaN(x))return''; return Number(x).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); }
function pct(x){ if(x==null||x===''||isNaN(x))return''; return (Number(x)*100).toFixed(2)+'%'; }
function dkey(s){ var p=String(s).split('/'); return parseInt(p[0])*10000+parseInt(p[1])*100+parseInt(p[2]); }
function sortedDates(obj){ return Object.keys(obj).sort(function(a,b){return dkey(a)-dkey(b);}); }
// 波动原因本地持久化
function noteKey(tbl,id){ return 'dspdaily_note::'+tbl+'::'+id; }
function getNote(tbl,id){ try{return localStorage.getItem(noteKey(tbl,id))||'';}catch(e){return'';} }
function setNote(tbl,id,v){ try{localStorage.setItem(noteKey(tbl,id),v);}catch(e){} }
function reasonCell(tbl,id){
var v=getNote(tbl,id).replace(/"/g,'&quot;');
return '<td class="txt"><input class="reason-inp" data-tbl="'+tbl+'" data-id="'+id+'" value="'+v+'" placeholder="点击填写"></td>';
}
// ===== Tab 切换 =====
document.querySelectorAll('.tab').forEach(function(t){
t.onclick=function(){
document.querySelectorAll('.tab').forEach(function(x){x.classList.remove('active');});
t.classList.add('active');
['overview','pkg','cust'].forEach(function(name){
document.getElementById('tab-'+name).classList.toggle('hide', name!==t.getAttribute('data-tab'));
});
};
});
// ===== 1.1 Overall =====
function renderOverall(){
var rows=D.overall.slice().sort(function(a,b){return dkey(a.dt)-dkey(b.dt);});
var tb=document.querySelector('#tOverall tbody');
var html='';
for(var i=rows.length-1;i>=0;i--){
var r=rows[i]; var prev=rows[i-1];
var wave = prev? (r['DSP total revenue']-prev['DSP total revenue']) : null;
var wc = wave==null?'':(wave>=0?'pos':'neg');
html+='<tr><td>'+r.dt+'</td><td>'+n2(r['DSP total revenue'])+'</td><td>'+n2(r['DSP CN revenue'])+'</td><td>'+n2(r['DSP international'])+'</td>'
+'<td class="'+wc+'">'+(wave==null?'—':n2(wave))+'</td></tr>';
}
tb.innerHTML=html;
}
// ===== 1.2/1.3 CN & International (dt为行, account为列; 最新在上) =====
function renderAccTable(tblId, rowsData, totalKey){
var rows=rowsData.slice().sort(function(a,b){return dkey(b.dt)-dkey(a.dt);});
if(!rows.length) return;
// 列 = dt + totalKey + 所有account(除dt/totalKey)
var accs=Object.keys(rows[0]).filter(function(k){return k!=='dt'&&k!==totalKey;});
var thead=document.querySelector('#'+tblId+' thead');
var th='<tr><th>dt</th><th>'+totalKey+'</th>';
accs.forEach(function(a){ th+='<th>'+a+'</th>'; });
th+='</tr>'; thead.innerHTML=th;
var body='';
rows.forEach(function(r){
body+='<tr><td>'+r.dt+'</td><td class="pos">'+n2(r[totalKey])+'</td>';
accs.forEach(function(a){ body+='<td>'+n2(r[a])+'</td>'; });
body+='</tr>';
});
document.querySelector('#'+tblId+' tbody').innerHTML=body;
}
// ===== 2.1/2.2 PKG波动 (pkg为行, dt为列, 末列DoD+波动原因) =====
function renderPkg(tblId, byDt, noteTbl){
var dts=sortedDates(byDt); // 升序
var showDts=dts.slice(-14);
var latest=showDts[showDts.length-1], prev=showDts[showDts.length-2];
// 收集所有pkg
var pkgset={};
showDts.forEach(function(dt){ Object.keys(byDt[dt]).forEach(function(p){pkgset[p]=1;}); });
var pkgs=Object.keys(pkgset);
// 按最新日收入降序
pkgs.sort(function(a,b){ return (byDt[latest][b]||0)-(byDt[latest][a]||0); });
var thead=document.querySelector('#'+tblId+' thead');
var th='<tr><th class="txt sticky-col">PKG</th>';
showDts.forEach(function(dt){ th+='<th>'+dt+'</th>'; });
th+='<th>DoD</th><th class="txt">波动原因</th></tr>';
thead.innerHTML=th;
var body='';
pkgs.forEach(function(p){
var row='<tr><td class="txt sticky-col">'+p+'</td>';
showDts.forEach(function(dt){ var v=byDt[dt][p]; row+='<td>'+(v==null?'':n2(v))+'</td>'; });
var lv=byDt[latest]&&byDt[latest][p]||0, pv=(prev&&byDt[prev]&&byDt[prev][p])||0;
var dod=lv-pv;
var cls='';
if(Math.abs(dod)>100){ cls = dod>0?'dod-hi-pos':'dod-hi-neg'; }
else { cls = dod>=0?'pos':'neg'; }
row+='<td class="'+cls+'">'+n2(dod)+'</td>';
row+=reasonCell(noteTbl, p);
row+='</tr>';
body+=row;
});
document.querySelector('#'+tblId+' tbody').innerHTML=body;
}
// ===== 3.1 客户分类日报 =====
function renderCustDaily(){
var dts=sortedDates(D.cust_daily); // 升序
var t1=dts[dts.length-1], t2=dts[dts.length-2], t3=dts[dts.length-3], t4=dts[dts.length-4];
var cats=Object.keys(D.cust_idsp);
// base = 该分类各account base之和
var baseByCat={};
(D.cust_base||[]).forEach(function(x){ baseByCat[x.cat]=(baseByCat[x.cat]||0)+(+x.base||0); });
var body='';
cats.forEach(function(cat){
function val(dt){ return dt&&D.cust_daily[dt]? (D.cust_daily[dt][cat]||0):null; }
var v1=val(t1),v2=val(t2),v3=val(t3),v4=val(t4);
var dod = (v1!=null&&v2!=null)? v1-v2 : null;
var dodp= (v1!=null&&v2)? (v1/v2-1) : null;
var dc = dod==null?'':(dod>=0?'pos':'neg');
body+='<tr><td class="txt">'+cat+'</td><td>'+(D.cust_idsp[cat]||'')+'</td><td>'+n2(baseByCat[cat])+'</td>'
+'<td>'+(v4==null?'':n2(v4))+'</td><td>'+(v3==null?'':n2(v3))+'</td><td>'+(v2==null?'':n2(v2))+'</td>'
+'<td class="pos">'+(v1==null?'':n2(v1))+'</td>'
+'<td class="'+dc+'">'+(dod==null?'':n2(dod))+'</td>'
+'<td class="'+dc+'">'+(dodp==null?'':pct(dodp))+'</td>'
+reasonCell('cust_daily', cat)+'</tr>';
});
document.querySelector('#tCustDaily tbody').innerHTML=body;
}
// ===== 3.2 客户明细 =====
function renderCustDetail(){
var dts=sortedDates(D.cust_detail);
var t1=dts[dts.length-1], t2=dts[dts.length-2];
var t1list=D.cust_detail[t1]||[];
var t2map={};
(D.cust_detail[t2]||[]).forEach(function(x){ t2map[x.cat+'||'+x.acc]=x.t1; });
// 按分类、再按T-1收入降序
var rows=t1list.slice().sort(function(a,b){ return a.cat<b.cat?-1:a.cat>b.cat?1:(b.t1-a.t1); });
var body='';
rows.forEach(function(r){
var prev=t2map[r.cat+'||'+r.acc];
var dod = prev!=null? r.t1-prev : null;
var dc=dod==null?'':(dod>=0?'pos':'neg');
body+='<tr><td class="txt">'+r.cat+'</td><td class="txt">'+r.acc+'</td><td>'+n2(r.base)+'</td>'
+'<td>'+n2(r.t1)+'</td><td class="'+dc+'">'+(dod==null?'':n2(dod))+'</td></tr>';
});
document.querySelector('#tCustDetail tbody').innerHTML=body;
}
// 波动原因输入监听(事件委托)
document.addEventListener('input',function(e){
var el=e.target;
if(el.classList&&el.classList.contains('reason-inp')){
setNote(el.getAttribute('data-tbl'), el.getAttribute('data-id'), el.value);
}
});
fetch('data.json?_='+Date.now()).then(function(r){return r.json();}).then(function(d){
D=d;
document.getElementById('meta').textContent='数据更新时间:'+(d.updated_at||'—')+' | Overall '+(d.overall||[]).length+' 天';
renderOverall();
renderAccTable('tCN', d.cn, 'DSP CN revenue');
renderAccTable('tIntl', d.international, 'DSP international');
renderPkg('tPkgMidas', d.pkg_midas, 'midas');
renderPkg('tPkgMS', d.pkg_ms, 'ms');
renderCustDaily();
renderCustDetail();
}).catch(function(e){
document.getElementById('meta').textContent='数据加载失败:'+e;
});
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
1
Executable
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env bash
# DSP-daily 日报数据更新脚本
# 每小时被平台执行;UTC+8 过11点且当天未更新则补跑,查询 T-1(前一天)。
# 产出 data.json:
# overall/cn/international : 每天一行,持续叠加
# pkg_midas / pkg_ms : 每个pkg按天的revenue,保留14天(前端算DoD)
# cust_daily / cust_detail : 客户分类,保留14天
# ============================================================
set -euo pipefail
cd "$(dirname "$0")"
# ============ 凭证(会过期,过期后替换这两行即可)============
# Superset cookie(用于 Midas / 无_API account)
SUPERSET_COOKIE='SR-PROD=dea34a1f6d50f4426c0a9fa3d19d75a5; oidc=d86576ac9fae4782b20b16044ca089a0; session=.eJztkMFqAzEMRH8l-LwHWbaz1t4K-YsSgmRJzdJlA-v0FPLvNfQ3ehoG9IbRvMLNN-5362H5fIXTc0hQ3r_sCFP4aM16P639dLF9NQ3X9_R_dJ3GaIf1e1ict27DrhqWoK65ZtCcIEWPqK0QzAiVhbmm1oZKpUh5BkiKLOoulJgRFItkAyzguYKVVCl5xsqALatxKQk51aIyUEaspYmcudQyq0uV7Oc82rZ--O35-LZ99IFIJplJm9GslWb0AlGcyVFoBCM1oDkNbns03mwwA5zCT7fj76WIAOH9C_vZpvc.aqOlrQ.xmkrmwsNe89-wy3pSP20QhNNuC8'
# MS report/v2/list token(用于 MS / 带_API account)
MS_TOKEN='eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1YTE1ZDZkNTU0ODc0ODc4YTdhNTNlNjMxNDkyNDMzYSIsImlhdCI6MTc4ODc1MDE1OSwic3ViIjoidG9rZW4gYnkgdXNoYXJlaXQiLCJleHAiOjE3ODg4MzY1NTl9.mXzeDaQ9A9QjbB3xurXU9dPnqH0Gi_oBwG9NuXPjSTM'
SUPERSET_URL='https://ads-bi.ushareit.org/superset/explore_json/'
SLICE_ID=821
REPORT_URL='https://ms-api.sandssea.com/api/v1/report/v2/list'
DATA_FILE='data.json'
KEEP_DAYS=14
# ============ 执行时机(UTC+8,过11点且当天未更新则补跑)============
HOUR_CST=$(TZ='Asia/Shanghai' date +%H)
RUN_DATE=$(TZ='Asia/Shanghai' date '+%Y/%-m/%-d')
FORCE="${FORCE_RUN:-0}"
if [ "$FORCE" != "1" ]; then
if [ "$((10#$HOUR_CST))" -lt 11 ]; then echo "[skip] UTC+8 $HOUR_CST 点,未到11点"; exit 0; fi
if [ -f "$DATA_FILE" ]; then
A=$(python3 -c "import json;d=json.load(open('$DATA_FILE',encoding='utf-8'));print('1' if any(r.get('date')=='$RUN_DATE' for r in d.get('overall',[])) else '0')" 2>/dev/null || echo 0)
if [ "$A" = "1" ]; then echo "[skip] 今天($RUN_DATE)已更新"; exit 0; fi
fi
fi
# T-1 日期(前一天)
PREV_YMD=$(TZ='Asia/Shanghai' date -v-1d '+%Y%m%d' 2>/dev/null || TZ='Asia/Shanghai' date -d 'yesterday' '+%Y%m%d') # 20260910
PREV_DATE=$(TZ='Asia/Shanghai' date -v-1d '+%Y/%-m/%-d' 2>/dev/null || TZ='Asia/Shanghai' date -d 'yesterday' '+%Y/%-m/%-d') # 2026/9/10
echo "[run] RUN_DATE=$RUN_DATE T-1=$PREV_YMD($PREV_DATE)"
# ============ 主处理:Python 完成拉取+聚合+合并 ============
export SUPERSET_COOKIE MS_TOKEN SUPERSET_URL SLICE_ID REPORT_URL DATA_FILE KEEP_DAYS RUN_DATE PREV_YMD PREV_DATE
python3 <<'PYEOF'
import os,json,urllib.parse,urllib.request,ssl,datetime
from collections import defaultdict
SUPERSET_COOKIE=os.environ['SUPERSET_COOKIE']; MS_TOKEN=os.environ['MS_TOKEN']
SUPERSET_URL=os.environ['SUPERSET_URL']; SLICE_ID=int(os.environ['SLICE_ID'])
REPORT_URL=os.environ['REPORT_URL']; DATA_FILE=os.environ['DATA_FILE']
KEEP=int(os.environ['KEEP_DAYS']); RUN_DATE=os.environ['RUN_DATE']
PREV_YMD=os.environ['PREV_YMD']; PREV_DATE=os.environ['PREV_DATE']
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
# ---- account 对照表 (advertiser_account_id -> _API名) ----
ACC={102:'nativex_API',99:'adsplay_API',98:'clickpuff_API',97:'affle_API',96:'mobhuge_API',95:'altosum_API',94:'seanear_API',93:'stridemobi_API',92:'volo_API',91:'strongmetrics_API',87:'aba_API',86:'hope_api',85:'stackyads_API',84:'isummer_API',83:'vertex_API',82:'viking_API',80:'acmemobi_API',79:'flymobi_API',78:'motilal_API',77:'flatmedia_API',76:'hypertap_API',75:'Influx_API',74:'sync_API',73:'c2c_API',72:'tecdo_API',71:'yomiads_API',70:'goodjoy_API',69:'apkpure_API',68:'flatads_API',67:'adnov_API',66:'dolemobi_API',65:'witgala_API',64:'mejoy_API',63:'adwisdom_ddj_API',62:'vfineads_ddj_API',60:'Iogerads_API',56:'quarklio_API',55:'quarklio_ddj_API',54:'bidneuron_API',52:'mstoko_API',51:'hopemobi_API',23:'Macan_API',18:'Midas_API',11:'Dreammobi_API',9:'Flowbox_API'}
# ---- CN / International 列头 account ----
CN_ACCS=['acshare','adsdeep','adwisdom','affinityad','alphadsp','altosum','apkpure','appfortis','appnextads','bidmatrix','bidneuron','bytemobi','cookeads','dolphin','dolphin2','flowbox','flymob','gleamx','gridads','imax','influxtec','iogerads','leapmob','lightspot','mbupps','mobgem','moblegend','mocaads','movablead','mstoko','nativexads','Ricefruit','shockit','smartconnectads','startym','tenads','verse','vertexs','vikingmedia','xoomads','yolo','youmi',
'bidneuron_API','quarklio_API','quarklio_ddj_API','Iogerads_API','adwisdom_ddj_API','mejoy_API','witgala_API','dolemobi_API','flatads_API','adnov_API','goodjoy_API','yomiads_API','flymobi_API','vertex_api','isummer_API','viking_api','hope_api','aba_api','volo_api','seanear_API','mobhuge_API','altosum_API','clickpuff_API']
INTL_ACCS=['adsflourish','affleads','avowads','cubicom','keendigi','launchpad','mobavenue','stackyads','vserv','xapad',
'sync_API','motilal_API','acmemobi_API','strongmetrics_API','c2c']
# ---- 客户分类 base 明细 (以文字明细为准) ----
CUST_BASE_RAW="""头部直客渠道|appnextads|7422.98
头部直客渠道|xapad|4200.73
头部直客渠道|vserv|3551.89
头部直客渠道|affinityad|2099.18
头部直客渠道|avowads|381.55
头部直客渠道|alphadsp|0
厂商预算|flymob|1002.15
厂商预算|flymobi_API|0
厂商预算|dolphin|773.56
厂商预算|dolphin2|0
厂商预算|iogerads|371.16
厂商预算|Iogerads_API|0
厂商预算|shockit|292.4
中小客户|adwisdom|738.61
中小客户|adwisdom_ddj_API|0
中小客户|appfortis|257.96
中小客户|mobavenue|228.43
中小客户|verse|199.27
中小客户|startym|116.72
中小客户|mbupps|112.17
中小客户|nativexads|102
中小客户|mstoko|97.96
中小客户|flatmedia_API|72.42
中小客户|leapmob|60.46
中小客户|affleads|59.83
中小客户|moblegend|51.33
中小客户|vertexs|51.09
中小客户|vertex_api|0
中小客户|stackyads|43.7
中小客户|apkpure|37.73
中小客户|acshare|33.72
中小客户|bytemobi|33.55
中小客户|adsdeep|32.19
中小客户|Ricefruit|31.7
中小客户|abamobi|31.29
中小客户|aba_api|0
中小客户|imax|29.14
中小客户|vfineads|27.77
中小客户|influxtec|26.75
中小客户|isummer|24.8
中小客户|isummer_API|0
中小客户|smartconnectads|23.99
中小客户|macan|21.64
中小客户|xoomads|21.62
中小客户|flowbox|20.62
中小客户|bidmatrix|18.91
中小客户|seanear_API|17.48
中小客户|gridads|16.51
中小客户|yomiads_API|15.55
中小客户|gleamx|14.37
中小客户|bidneuron|12.89
中小客户|bidneuron_API|0
中小客户|quarklio_API|12.83
中小客户|altosum|11.95
中小客户|altosum_API|0
中小客户|lightspot|11.75
中小客户|vikingmedia|10.87
中小客户|viking_api|0
中小客户|mobgem|5.86
中小客户|launchpad|4.2
中小客户|adnov_API|0.24
中小客户|hopemobi_API|2.55
中小客户|hope_API|0
中小客户|volo|10.84
中小客户|volo_api|0
待抛弃客户|mobhugeads|9.95
待抛弃客户|wayne|7.86
待抛弃客户|c2c|6.3
待抛弃客户|tenads|4.45
待抛弃客户|movablead|4.22
待抛弃客户|tecdo|3.17
待抛弃客户|cookeads|1.52
待抛弃客户|mocaads|0.57
待抛弃客户|cubicom|0.55
待抛弃客户|sync_API|0.43
待抛弃客户|youmi|0.65"""
CUST_IDSP={'头部直客渠道':6,'厂商预算':4,'中小客户':42,'待抛弃客户':12}
# 解析客户分类明细
cust_items=[] # (cat, acc, base)
for ln in CUST_BASE_RAW.strip().split('\n'):
cat,acc,base=ln.split('|')
cust_items.append((cat,acc,float(base) if base.strip() else 0.0))
def norm(s): return str(s).strip().lower()
# ============ 拉 Superset (Midas / 无_API account) ============
def fetch_superset():
# 先用 slice_id 拿完整 form_data
fd0=urllib.parse.quote(json.dumps({'slice_id':SLICE_ID}))
req=urllib.request.Request(SUPERSET_URL+'?form_data='+fd0,headers={'cookie':SUPERSET_COOKIE,'accept':'application/json'})
base=json.load(urllib.request.urlopen(req,timeout=120,context=ctx))
fd=base['form_data']; fd['row_limit']=100000
enc=urllib.parse.quote(json.dumps(fd))
req2=urllib.request.Request(SUPERSET_URL+'?form_data='+enc,headers={'cookie':SUPERSET_COOKIE,'accept':'application/json'})
d=json.load(urllib.request.urlopen(req2,timeout=180,context=ctx))
return d['data']['records']
# ============ 拉 report/v2/list (MS / 带_API account) ============
def fetch_report(ymd):
cols=[{"tCode":"adnouter_dws_report_san","tName":"dws_san_report_sum_hour","sqlType":"select","field":"action_dt","title":"Dt","sqlField":"action_dt","fieldType":1,"sorted":10,"checked":True},
{"tCode":"adnouter_dws_report_san","tName":"dws_san_report_sum_hour","sqlType":"select","field":"advertiser_account_id","title":"Adv Account","sqlField":"advertiser_account_id","fieldType":1,"sorted":26,"checked":True},
{"tCode":"adnouter_dws_report_san","tName":"dws_san_report_sum_hour","sqlType":"select","field":"package_name","title":"Pkg Name","sqlField":"pkg_name","fieldType":1,"sorted":60,"checked":True},
{"tCode":"adnouter_dws_report_san","tName":"dws_san_report_sum_hour","sqlType":"select","field":"revenues","title":"Revenue","sqlField":"sum(revenue)","fieldType":2,"sorted":230,"checked":True}]
body={"pageSize":100000,"pageIndex":1,"query":{"dt":[ymd,ymd],"pb_geo":[],"target_geo":[]},"columns":cols,"orderBy":{}}
req=urllib.request.Request(REPORT_URL,data=json.dumps(body).encode(),
headers={'authorization':MS_TOKEN,'content-type':'application/json;charset=UTF-8','origin':'https://ms.sandssea.com','referer':'https://ms.sandssea.com/'})
d=json.load(urllib.request.urlopen(req,timeout=180,context=ctx))
return d['data']['list']
print('[fetch] Superset...'); sup=fetch_superset()
print('[fetch] report/v2/list T-1...'); rep=fetch_report(PREV_YMD)
# Superset: 只取 T-1 那天(dt格式20260910)
sup_prev=[r for r in sup if str(r.get('dt'))==PREV_YMD]
print(f' superset T-1行={len(sup_prev)} (全部{len(sup)}) ; report行={len(rep)}')
# ---- 汇总:无_API account revenue (来自Superset attr_platform) ----
sup_acc_rev=defaultdict(float) # account(小写) -> rev
sup_pkg_rev=defaultdict(float) # pkg -> rev (Midas)
for r in sup_prev:
acc=norm(r.get('attr_platform'))
rev=float(r.get('revenue(还原前)') or 0)
sup_acc_rev[acc]+=rev
pkg=r.get('pkg name(EN)') or ''
sup_pkg_rev[pkg]+=rev
# ---- 汇总:带_API account revenue (来自report, 按account_id映射, 排除18) ----
rep_acc_rev=defaultdict(float) # _API名(小写) -> rev
rep_pkg_rev=defaultdict(float) # pkg -> rev (MS, 排除account18)
for r in rep:
aid=r.get('advertiser_account_id')
rev=float(r.get('revenues') or 0)
if aid==18: # Midas_API 不记录
continue
name=ACC.get(aid)
if name:
rep_acc_rev[norm(name)]+=rev
pkg=r.get('package_name') or ''
rep_pkg_rev[pkg]+=rev
# 统一取某 account 收入:无_API走superset,带_API走report
def acc_rev(acc):
a=norm(acc)
if acc.endswith('_API') or acc.endswith('_api'):
return round(rep_acc_rev.get(a,0.0),4)
else:
return round(sup_acc_rev.get(a,0.0),4)
# ============ 1.2 CN / 1.3 International ============
cn_row={'dt':PREV_DATE}; cn_total=0.0
for a in CN_ACCS:
v=acc_rev(a); cn_row[a]=v; cn_total+=v
cn_row['DSP CN revenue']=round(cn_total,2)
intl_row={'dt':PREV_DATE}; intl_total=0.0
for a in INTL_ACCS:
v=acc_rev(a); intl_row[a]=v; intl_total+=v
intl_row['DSP international']=round(intl_total,2)
# ============ 1.1 Overall ============
overall_row={'dt':PREV_DATE,'DSP total revenue':round(cn_total+intl_total,2),
'DSP CN revenue':round(cn_total,2),'DSP international':round(intl_total,2)}
# ============ 2.1 PKG-Midas / 2.2 PKG-MS (当天pkg->rev) ============
pkg_midas={pkg:round(v,4) for pkg,v in sup_pkg_rev.items() if v} # Midas: superset
pkg_ms={pkg:round(v,4) for pkg,v in rep_pkg_rev.items() if v} # MS: report(已排除18)
# ============ 3.x 客户分类 (当天) ============
# 每个 account 的 T-1 收入
cust_detail_today=[] # {cat,acc,base,t1}
cat_t1=defaultdict(float)
for cat,acc,base in cust_items:
t1=acc_rev(acc)
cust_detail_today.append({'cat':cat,'acc':acc,'base':base,'t1':round(t1,4)})
cat_t1[cat]+=t1
cust_cat_today={cat:round(v,4) for cat,v in cat_t1.items()} # 分类->T-1收入
# ============ 合并入 data.json ============
def dkey(s): return tuple(int(x) for x in s.split('/'))
old={'overall':[],'cn':[],'international':[],'pkg_midas':{},'pkg_ms':{},
'cust_daily':{},'cust_detail':{},'cust_base':[],'cust_idsp':CUST_IDSP,'updated_at':''}
if os.path.exists(DATA_FILE):
try: old=json.load(open(DATA_FILE,encoding='utf-8'))
except Exception: pass
for k,dv in [('overall',[]),('cn',[]),('international',[]),('pkg_midas',{}),('pkg_ms',{}),('cust_daily',{}),('cust_detail',{})]:
old.setdefault(k,dv)
old['cust_idsp']=CUST_IDSP
# 客户分类 base 明细(固定,每次覆盖为最新配置)
old['cust_base']=[{'cat':c,'acc':a,'base':b} for c,a,b in cust_items]
# 叠加型:overall/cn/international,按dt去重后append
def upsert_daily(lst,row):
lst=[x for x in lst if x.get('dt')!=row['dt']]+[row]
lst.sort(key=lambda x:dkey(x['dt']))
return lst
old['overall']=upsert_daily(old['overall'],overall_row)
old['cn']=upsert_daily(old['cn'],cn_row)
old['international']=upsert_daily(old['international'],intl_row)
# 14天型:pkg_midas/pkg_ms 是 {dt: {pkg:rev}};cust_daily {dt:{cat:t1}};cust_detail {dt:[{cat,acc,base,t1}]}
def keep_recent(dct):
dts=sorted(dct.keys(),key=dkey)
for d in dts[:-KEEP]: dct.pop(d,None)
return dct
old['pkg_midas'][PREV_DATE]=pkg_midas
old['pkg_ms'][PREV_DATE]=pkg_ms
old['cust_daily'][PREV_DATE]=cust_cat_today
old['cust_detail'][PREV_DATE]=cust_detail_today
for key in ['pkg_midas','pkg_ms','cust_daily','cust_detail']:
old[key]=keep_recent(old[key])
old['updated_at']=datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M:%S UTC+8')
json.dump(old,open(DATA_FILE,'w',encoding='utf-8'),ensure_ascii=False,separators=(',',':'))
print(f"[done] overall={len(old['overall'])}天 CN合计={cn_row['DSP CN revenue']} Intl合计={intl_row['DSP international']}")
print(f" pkg_midas={len(pkg_midas)}个pkg pkg_ms={len(pkg_ms)}个pkg cust分类={cust_cat_today}")
PYEOF
echo "[ok] data.json 已更新"