Files
DSP-routing-daily/update.sh
T

207 lines
11 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# DSP-routing-daily 数据更新脚本
# 每小时被服务器执行一次;仅在 UTC+8 11 点档真正跑,查询"前一天"(UTC+8)数据。
# 产出 data.json:总表每天累加一行;Kibana 监测明细表按 adping_id 聚合,保留近14天。
set -euo pipefail
cd "$(dirname "$0")"
# ============ 配置 ============
API_TOKEN='eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJkZTljM2IyMGZiYmE0ZTZiOTczODk3NzgwNDJmMGJmYSIsImlhdCI6MTc4ODc1MzI1OCwic3ViIjoidG9rZW4gYnkgdXNoYXJlaXQiLCJleHAiOjE3ODg4Mzk2NTh9.11wvyNVUw5XTZZJm0m5KNpeHSi528jQuCI1DfKqpaXQ'
API_BASE='https://ms-api.sandssea.com/api/v1'
KIBANA_ES='http://kibana-sg3.ushareit.org/elasticsearch'
DATA_FILE='data.json'
KEEP_DAYS=14
# ============ 11 点档判断(UTC+8)============
# 服务器每小时触发;只有当 UTC+8 当前小时 == 11 时才真正执行。
HOUR_CST=$(TZ='Asia/Shanghai' date +%H)
FORCE="${FORCE_RUN:-0}"
if [ "$HOUR_CST" != "11" ] && [ "$FORCE" != "1" ]; then
echo "[skip] 当前 UTC+8 $HOUR_CST 点,非 11 点档,跳过。(设 FORCE_RUN=1 可强制运行)"
exit 0
fi
# ============ 日期计算(UTC+8 前一天)============
# 前一天 UTC+8 的 00:00~24:00,换算成 ES 用的 UTC 时间窗口。
RUN_DATE=$(TZ='Asia/Shanghai' date '+%Y/%-m/%-d') # 总表"日期"= 运行当天,格式 2026/9/3
PREV_DATE_CST=$(TZ='Asia/Shanghai' date -v-1d '+%Y/%-m/%-d' 2>/dev/null || TZ='Asia/Shanghai' date -d 'yesterday' '+%Y/%-m/%-d')
# 明细表 dt = 前一天(UTC+8)
# ES 时间窗口(UTC): 前一天 00:00 CST = 前一天 -08:00 UTC;到 当天 00:00 CST
FROM_UTC=$(TZ='Asia/Shanghai' date -v-1d '+%Y-%m-%dT00:00:00' 2>/dev/null || TZ='Asia/Shanghai' date -d 'yesterday 00:00' '+%Y-%m-%dT00:00:00')
TO_UTC=$(TZ='Asia/Shanghai' date '+%Y-%m-%dT00:00:00')
# 转成带 +08:00 偏移的 ISO,让 ES 自己按时区解析
FROM_ISO="${FROM_UTC}+08:00"
TO_ISO="${TO_UTC}+08:00"
echo "[run] RUN_DATE=$RUN_DATE 明细dt=$PREV_DATE_CST 窗口(CST)=$FROM_ISO ~ $TO_ISO"
# ============ 源1:原始订单 (camp_info/pageWithOffer, status=1 Active) ============
# 原始订单 = Active 全部条数(不去重); pkg = 按 pkg_name+target_geo 去重后的条数
echo "[src1] 拉取 camp_info/pageWithOffer 全量..."
SRC1_JSON=$(python3 - "$API_BASE" "$API_TOKEN" <<'PYEOF'
import sys,urllib.request,json,ssl,math
from concurrent.futures import ThreadPoolExecutor
base,token=sys.argv[1],sys.argv[2]
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
HDR={'authorization':token,'referer':'https://sands.hellay.net/','origin':'https://sands.hellay.net','accept':'application/json'}
def get(url):
req=urllib.request.Request(url,headers=HDR)
with urllib.request.urlopen(req,timeout=120,context=ctx) as r:
return json.load(r)
SIZE=5000
# status=1 即 Active(仅生效中的订单);第一页拿 count
first=get(f"{base}/camp_info/pageWithOffer?pageIndex=1&pageSize={SIZE}&status=1&exclude_midas=true")
cnt=first.get('data',{}).get('count',0)
pages=max(1, math.ceil(cnt/SIZE))
def fetch(page):
if page==1: return first.get('data',{}).get('list') or []
d=get(f"{base}/camp_info/pageWithOffer?pageIndex={page}&pageSize={SIZE}&status=1&exclude_midas=true")
return d.get('data',{}).get('list') or []
total=0; seen=set()
with ThreadPoolExecutor(max_workers=8) as ex:
for lst in ex.map(fetch, range(1,pages+1)):
for o in lst:
total+=1
pkg=(o.get('pkg_name') or '').strip()
geo=(o.get('target_geo') or '').strip().upper()
seen.add(pkg+'_'+geo)
print(json.dumps({'raw_orders':total,'pkg_geo_distinct':len(seen)}))
PYEOF
)
RAW_ORDERS=$(echo "$SRC1_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['raw_orders'])")
PKG_DISTINCT=$(echo "$SRC1_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['pkg_geo_distinct'])")
echo "[src1] 原始订单=$RAW_ORDERS pkg(去重)=$PKG_DISTINCT"
# ============ 源2:中转订单 (campaignOffer/list offerSource=5) ============
echo "[src2] 拉取 campaignOffer offerSource=5 count..."
RELAY_ORDERS=$(python3 - "$API_BASE" "$API_TOKEN" <<'PYEOF'
import sys,urllib.request,json,ssl
base,token=sys.argv[1],sys.argv[2]
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
req=urllib.request.Request(f"{base}/campaignOffer/list?offerSource=5&pageSize=1&pageIndex=1",headers={'authorization':token,'referer':'https://sands.hellay.net/','origin':'https://sands.hellay.net'})
with urllib.request.urlopen(req,timeout=90,context=ctx) as r:
d=json.load(r)
print(d.get('data',{}).get('count',0))
PYEOF
)
echo "[src2] 中转订单=$RELAY_ORDERS"
# ============ 源3:Kibana 前一天 is_routing:1 ============
echo "[src3] 查询 Kibana..."
KIBANA_JSON=$(python3 - "$KIBANA_ES" "$FROM_ISO" "$TO_ISO" "$PREV_DATE_CST" <<'PYEOF'
import sys,urllib.request,json
es,frm,to,dt=sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4]
CLICK=es+'/ads_san-adping_cold_click_*/_search'
PB=es+'/ads_san-adping_cold_postback_*/_search'
ACCOUNT_MAP={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'}
def es_post(url,body):
req=urllib.request.Request(url,data=json.dumps(body).encode(),headers={'kbn-xsrf':'true','Content-Type':'application/json'})
with urllib.request.urlopen(req,timeout=120) as r:
return json.load(r)
flt=[{'term':{'is_routing':1}},{'range':{'@timestamp':{'gte':frm,'lt':to}}}]
# 总表: 有点击订单 = 不同 routing_campaign_id 个数 (click表)
r=es_post(CLICK,{'size':0,'query':{'bool':{'filter':flt}},'aggs':{'rc':{'cardinality':{'field':'routing_campaign_id.keyword','precision_threshold':40000}}}})
has_click_orders=r['aggregations']['rc']['value']
# 明细: 按 adping_id 聚合 (click表) -> clicks, revenue(sum payin), cost(sum deliver_price), 及维度
# advertiser_payin/deliver_price 是字符串,用 script 转 double 求和
agg={
'by_adp':{'terms':{'field':'adping_id','size':100000},
'aggs':{
'rev':{'sum':{'script':{'source':"try{return Double.parseDouble(doc['advertiser_payin.keyword'].value)}catch(Exception e){return 0.0}"}}},
'cost':{'sum':{'script':{'source':"try{return Double.parseDouble(doc['deliver_price.keyword'].value)}catch(Exception e){return 0.0}"}}},
'rc':{'terms':{'field':'routing_campaign_id.keyword','size':1}},
'cid':{'terms':{'field':'campaign_id.keyword','size':1}},
'sc':{'terms':{'field':'second_channel.keyword','size':1}},
'acc':{'terms':{'field':'advertiser_account_id','size':1}},
'pkg':{'terms':{'field':'pkg_name.keyword','size':1}},
}}
}
r=es_post(CLICK,{'size':0,'query':{'bool':{'filter':flt}},'aggs':agg})
buckets=r['aggregations']['by_adp']['buckets']
# conversion: postback表 按 adping_id 计数
r2=es_post(PB,{'size':0,'query':{'bool':{'filter':flt}},'aggs':{'by_adp':{'terms':{'field':'adping_id','size':100000}}}})
conv_map={b['key']:b['doc_count'] for b in r2['aggregations']['by_adp']['buckets']}
def first(agg_obj):
b=agg_obj.get('buckets',[])
return b[0]['key'] if b else ''
detail=[]
routing_ids_with_conv=set() # 有转化的 routing_campaign_id
for b in buckets:
adp=b['key']
clicks=b['doc_count']
rev=round(b['rev']['value'],6)
cost=round(b['cost']['value'],6)
conv=conv_map.get(adp,0)
rc=str(first(b['rc']))
cid=str(first(b['cid']))
sc=str(first(b['sc']))
accid=first(b['acc'])
acc=ACCOUNT_MAP.get(accid,'')
if not acc:
acc = f'未知({accid})' if accid not in ('',None) else '未知'
pkg=str(first(b['pkg']))
cvr=round(conv/clicks,6) if clicks else 0
gp=round(1-cost/rev,6) if rev else 0
ecpc=round(rev/clicks,6) if clicks else 0
detail.append({'dt':dt,'routing_campaign_id':rc,'campaign_id':cid,'adping_id':adp,
'second_channel':sc,'account':acc,'pkg':pkg,'clicks':clicks,'conversion':conv,
'cvr':cvr,'revenue':rev,'cost':cost,'gp':gp,'ecpc':ecpc})
if conv>0 and rc:
routing_ids_with_conv.add(rc)
# 无转化订单 = conversion=0 的 routing_campaign_id 个数
# = 总(有点击的routing_campaign_id) - 有转化的routing_campaign_id
no_conv_orders = int(has_click_orders) - len(routing_ids_with_conv)
if no_conv_orders<0: no_conv_orders=0
print(json.dumps({'has_click_orders':int(has_click_orders),'no_conv_orders':no_conv_orders,'detail':detail}))
PYEOF
)
HAS_CLICK=$(echo "$KIBANA_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['has_click_orders'])")
NO_CONV=$(echo "$KIBANA_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['no_conv_orders'])")
echo "[src3] 有点击订单=$HAS_CLICK 无转化订单=$NO_CONV 明细行=$(echo "$KIBANA_JSON" | python3 -c "import sys,json;print(len(json.load(sys.stdin)['detail']))")"
# ============ 合并写入 data.json(累加、明细保留14天)============
python3 - "$DATA_FILE" "$RUN_DATE" "$PREV_DATE_CST" "$RAW_ORDERS" "$PKG_DISTINCT" "$RELAY_ORDERS" "$HAS_CLICK" "$NO_CONV" "$KEEP_DAYS" "$KIBANA_JSON" <<'PYEOF'
import sys,json,os,datetime
f,run_date,prev_dt,raw,pkg,relay,hasclick,noconv,keep,kib=sys.argv[1:11]
raw,pkg,relay,hasclick,noconv,keep=int(raw),int(pkg),int(relay),int(hasclick),int(noconv),int(keep)
kib=json.loads(kib)
no_click = relay - hasclick # 无点击订单 = 中转订单 - 有点击订单
old={'summary':[],'detail':[],'updated_at':''}
if os.path.exists(f):
try:
old=json.load(open(f,encoding='utf-8'))
except Exception: pass
old.setdefault('summary',[]); old.setdefault('detail',[])
# 总表:同一 run_date 覆盖,否则新增
row={'date':run_date,'raw_orders':raw,'pkg':pkg,'relay_orders':relay,
'has_click_orders':hasclick,'no_conv_orders':noconv,'no_click_orders':no_click}
old['summary']=[r for r in old['summary'] if r.get('date')!=run_date]+[row]
old['summary'].sort(key=lambda r:r['date'])
# 明细:先删掉同 dt 的旧数据,再加入本次;然后按 dt 保留最近 keep 天
old['detail']=[d for d in old['detail'] if d.get('dt')!=prev_dt]+kib['detail']
# 保留最近 keep 个不同 dt
dts=sorted({d['dt'] for d in old['detail']}, key=lambda s: tuple(int(x) for x in s.split('/')))
keep_dts=set(dts[-keep:])
old['detail']=[d for d in old['detail'] if d['dt'] in keep_dts]
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(f,'w',encoding='utf-8'),ensure_ascii=False,separators=(',',':'))
print(f"[done] summary行数={len(old['summary'])} detail行数={len(old['detail'])} 保留dt={sorted(keep_dts)}")
PYEOF
echo "[ok] data.json 已更新"