309 lines
16 KiB
Bash
Executable File
309 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# DSP-daily 日报数据更新脚本
|
||
# 每小时被平台执行;UTC+8 过11点且当天未更新则补跑,查询 T-1(前一天)。
|
||
# 产出 data.json:
|
||
# overall/cn/international : 每天一行,持续叠加
|
||
# pkg_midas / pkg_ms : {dt:{pkg:{account:rev}}} 保留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.eyJqdGkiOiI5OGNlN2YzZDY3NzE0NDFhOWQxM2I2YjJhOTY5M2UxZSIsImlhdCI6MTc4OTM1NTAzMywic3ViIjoidG9rZW4gYnkgdXNoYXJlaXQiLCJleHAiOjE3ODk0NDE0MzN9.6IupaUeP2gThNu0v7P5J8uA2aL44m8vvGDjFIhnmhKI'
|
||
|
||
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']
|
||
|
||
# ---- 各源独立容错:一个失败不影响另一个 ----
|
||
src_status={'superset':'ok','report':'ok'}
|
||
sup=[]; rep=[]
|
||
try:
|
||
print('[fetch] Superset...'); sup=fetch_superset()
|
||
except Exception as e:
|
||
src_status['superset']='fail: '+str(e)[:120]
|
||
print('[ERROR] Superset 拉取失败:', str(e)[:200])
|
||
try:
|
||
print('[fetch] report/v2/list T-1...'); rep=fetch_report(PREV_YMD)
|
||
except Exception as e:
|
||
src_status['report']='fail: '+str(e)[:120]
|
||
print('[ERROR] report/v2/list 拉取失败(可能MS token过期):', str(e)[:200])
|
||
|
||
# 两个源都失败 -> 放弃本次更新,不写 data.json
|
||
if src_status['superset'].startswith('fail') and src_status['report'].startswith('fail'):
|
||
print('[ABORT] 两个数据源都失败,本次不更新 data.json。')
|
||
print('!!! 需检查凭证: Superset cookie / MS token 可能均已过期 !!!')
|
||
import sys as _s; _s.exit(1)
|
||
# 单源失败:告警但继续(用另一个源的数据更新)
|
||
if src_status['report'].startswith('fail'):
|
||
print('!!!!! 告警: MS(report/v2/list) 数据源失效,本次 MS/带_API account 数据将缺失。请尽快更新 MS_TOKEN !!!!!')
|
||
if src_status['superset'].startswith('fail'):
|
||
print('!!!!! 告警: Superset 数据源失效,本次 Midas/无_API account 数据将缺失。请尽快更新 SUPERSET_COOKIE !!!!!')
|
||
|
||
# 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_acc=defaultdict(lambda:defaultdict(float)) # pkg -> {account -> rev} (Midas, account=attr_platform)
|
||
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 ''
|
||
if rev:
|
||
sup_pkg_acc[pkg][r.get('attr_platform') or acc]+=rev
|
||
|
||
# ---- 汇总:带_API account revenue (来自report, 按account_id映射, 排除18) ----
|
||
rep_acc_rev=defaultdict(float) # _API名(小写) -> rev
|
||
rep_pkg_acc=defaultdict(lambda:defaultdict(float)) # pkg -> {account -> 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 ''
|
||
disp = name if name else ('acct_%s'%aid)
|
||
if rev:
|
||
rep_pkg_acc[pkg][disp]+=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 -> {account: rev}) ============
|
||
pkg_midas={pkg:{a:round(v,4) for a,v in accs.items() if v} for pkg,accs in sup_pkg_acc.items() if accs}
|
||
pkg_ms={pkg:{a:round(v,4) for a,v in accs.items() if v} for pkg,accs in rep_pkg_acc.items() if accs}
|
||
|
||
# ============ 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')
|
||
old['source_status']=src_status
|
||
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 已更新"
|