130 lines
6.1 KiB
Python
130 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
||
# 在 ads-center 上每天运行:取前一天3渠道数据 -> 合并 mtg_data.json -> 生成矩阵 -> git push
|
||
# 缺表渠道(Adsterra/TrafficStars/Eagllwin)由 excel_fetch.py 从运营 Excel 补数(可选,找不到就跳过)
|
||
# 由 launchd 定时触发。Python: /Users/ads-center/py314env/bin/python
|
||
import sqlite3, json, subprocess, sys, os
|
||
from collections import defaultdict
|
||
from datetime import date, timedelta
|
||
|
||
DB = "/Users/ads-center/Documents/database/report_api.db"
|
||
REPO = "/Users/ads-center/voypace-report-sync/voypace-ad-report"
|
||
|
||
def log(m): print(f"[sync] {m}", flush=True)
|
||
|
||
# 目标日期:前一天(可用参数覆盖,便于补数)
|
||
TARGET = sys.argv[1] if len(sys.argv) > 1 else (date.today() - timedelta(days=1)).isoformat()
|
||
|
||
os.chdir(REPO)
|
||
# 先 pull 最新,避免冲突
|
||
subprocess.run(["git", "pull", "--rebase", "-q"], cwd=REPO)
|
||
|
||
camps = json.load(open("campaigns.json"))
|
||
CAMPS = {r["campid"]: {"product": r["product"], "pkg": r["pkg"]} for r in camps}
|
||
IDS = list(CAMPS.keys())
|
||
|
||
conn = sqlite3.connect(DB); conn.row_factory = sqlite3.Row
|
||
agg = defaultdict(lambda: {"impression":0,"clicks":0,"conversions":0,"spend":0.0})
|
||
meta = {}
|
||
|
||
def cid_of(t):
|
||
t = str(t or "")
|
||
for c in IDS:
|
||
if c in t: return c
|
||
return None
|
||
|
||
# Mintegral(广告单元名可能落在 Offer_Name 或 Campaign_Id,两字段都匹配)
|
||
like = " OR ".join([f"Offer_Name LIKE '%{c}%' OR Campaign_Id LIKE '%{c}%'" for c in IDS])
|
||
for r in conn.execute(f"SELECT Date d,Offer_Name nm,Campaign_Id cmp,Location loc,Impression imp,Click clk,Conversion cv,Spend sp FROM mintegral WHERE Date=? AND ({like})", (TARGET,)):
|
||
c = cid_of(r["nm"]) or cid_of(r["cmp"])
|
||
if not c: continue
|
||
k = (r["d"],"Mintegral",c,(r["loc"] or "").upper()); a=agg[k]
|
||
a["impression"]+=int(r["imp"] or 0); a["clicks"]+=int(r["clk"] or 0)
|
||
a["conversions"]+=int(r["cv"] or 0); a["spend"]+=float(r["sp"] or 0)
|
||
meta[k]=(r["nm"] or r["cmp"],CAMPS[c])
|
||
|
||
# Bigo
|
||
like = " OR ".join([f"campaignName LIKE '%{c}%'" for c in IDS])
|
||
for r in conn.execute(f"SELECT aggregateTime d,campaignName nm,country loc,impression imp,click clk,real_result_conversion cv,totalCost cost FROM bigo WHERE aggregateTime=? AND ({like})", (TARGET,)):
|
||
c = cid_of(r["nm"])
|
||
if not c: continue
|
||
k = (r["d"],"Bigo",c,(r["loc"] or "").upper()); a=agg[k]
|
||
a["impression"]+=int(r["imp"] or 0); a["clicks"]+=int(r["clk"] or 0)
|
||
a["conversions"]+=int(r["cv"] or 0); a["spend"]+=float(r["cost"] or 0)/100.0
|
||
meta[k]=(r["nm"],CAMPS[c])
|
||
|
||
# MediaGo (spend = cpc*click)
|
||
like = " OR ".join([f"campaign_name LIKE '%{c}%'" for c in IDS])
|
||
for r in conn.execute(f"SELECT date d,campaign_name nm,adset_country loc,vimp imp,click clk,cv cvn,cpc FROM mediago WHERE date=? AND ({like})", (TARGET,)):
|
||
c = cid_of(r["nm"])
|
||
if not c: continue
|
||
k = (r["d"],"MediaGo",c,(r["loc"] or "").upper()); a=agg[k]
|
||
clk=int(r["clk"] or 0)
|
||
a["impression"]+=int(r["imp"] or 0); a["clicks"]+=clk
|
||
a["conversions"]+=int(r["cvn"] or 0); a["spend"]+=float(r["cpc"] or 0)*clk
|
||
meta[k]=(r["nm"],CAMPS[c])
|
||
conn.close()
|
||
|
||
new_rows=[]
|
||
for k,a in agg.items():
|
||
dt,ch,c,loc=k; nm,info=meta[k]
|
||
imp,clk,cv=a["impression"],a["clicks"],a["conversions"]
|
||
new_rows.append({"date":dt,"channel":ch,"campid":c,"offer_name":nm,"product":info["product"],"pkg":info["pkg"],
|
||
"location":loc,"bid_type":"","impression":imp,"clicks":clk,"conversions":cv,
|
||
"ctr":round(clk/imp,5) if imp else 0.0,"cvr":round(cv/clk,5) if clk else 0.0,"spend":round(a["spend"],2)})
|
||
log(f"{TARGET} DB三渠道新增匹配 {len(new_rows)} 条")
|
||
|
||
# 可选:从运营 Excel 补齐缺表渠道(Adsterra/TrafficStars/Eagllwin 等)
|
||
excel_rows = []
|
||
try:
|
||
ef = os.path.join(REPO, "excel_fetch.py")
|
||
if os.path.exists(ef):
|
||
p = subprocess.run([sys.executable, ef], cwd=REPO, capture_output=True, text=True)
|
||
if p.returncode == 0 and p.stdout.strip():
|
||
excel_rows = json.loads(p.stdout)
|
||
# 只保留目标日期,保持“每天增量”语义(补数全量跑时可去掉此过滤)
|
||
excel_rows = [r for r in excel_rows if r.get("date") == TARGET]
|
||
else:
|
||
log(f"Excel 补数无输出/失败:{p.stderr[:120]}")
|
||
except Exception as e:
|
||
log(f"Excel 补数跳过:{e}")
|
||
if excel_rows:
|
||
log(f"Excel 补数新增 {len(excel_rows)} 条")
|
||
new_rows += excel_rows
|
||
|
||
# 合并(按 date+channel+campid+location 去重)
|
||
existing = json.load(open("mtg_data.json")) if os.path.exists("mtg_data.json") else []
|
||
merged = {(r["date"],r.get("channel",""),r["campid"],r["location"]): r for r in existing}
|
||
for r in new_rows:
|
||
merged[(r["date"],r["channel"],r["campid"],r["location"])] = r
|
||
data = sorted(merged.values(), key=lambda x:(x["date"],x.get("channel",""),-x["impression"]))
|
||
json.dump(data, open("mtg_data.json","w"), ensure_ascii=False, indent=2)
|
||
log(f"合并后共 {len(data)} 条")
|
||
|
||
# 重新生成矩阵状态(近3天单日最高展示>=100 且产品订单为Active -> Active)
|
||
orders = json.load(open("orders.json"))
|
||
pkg_status={}
|
||
for o in orders:
|
||
for p in str(o["pkg"]).replace(" ","").split("/"):
|
||
if p: pkg_status[p]=o.get("status","Pause")
|
||
g=defaultdict(lambda: defaultdict(int))
|
||
for r in data: g[(r["channel"],r["pkg"])][r["date"]]+=r["impression"]
|
||
alldates=sorted(set(r["date"] for r in data)); last3=alldates[-3:]
|
||
ov={}
|
||
for (ch,pkg),days in g.items():
|
||
ps=pkg_status.get(pkg,"Pause")
|
||
if ps!="Active": ov[f"{ch}|{pkg}"]="Pause"; continue
|
||
total=sum(days.values()); rmax=max([days.get(dt,0) for dt in last3]) if last3 else 0
|
||
ov[f"{ch}|{pkg}"]= "none" if total==0 else ("Active" if rmax>=100 else "Pause")
|
||
json.dump(ov, open("matrix_overrides.json","w"), ensure_ascii=False, indent=2)
|
||
log("矩阵状态已刷新")
|
||
|
||
# git push
|
||
subprocess.run(["git","add","mtg_data.json","matrix_overrides.json"], cwd=REPO)
|
||
r=subprocess.run(["git","commit","-m",f"data: daily sync {TARGET}"], cwd=REPO, capture_output=True, text=True)
|
||
if "nothing to commit" in (r.stdout+r.stderr):
|
||
log("无变化,跳过 push")
|
||
else:
|
||
p=subprocess.run(["git","push","-q","origin","main"], cwd=REPO, capture_output=True, text=True)
|
||
log("已 push" if p.returncode==0 else f"push失败: {p.stderr[:200]}")
|
||
log("完成")
|