Files
moboos-ad-report/update.sh
T

330 lines
15 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.
#!/bin/bash
# Moboost 报告 - 每日自动更新脚本
# 服务器每天执行一次(schedule.conf=24)。
# 逻辑:拉取"前一天"的 Mintegral 数据,增量合并进 mtg_data.json(去重,不覆盖历史)。
# 工作目录 = 仓库根目录(同目录下有 mtg_data.json / campaigns.json)。
set -e
cd "$(dirname "$0")"
python3 - <<'PYEOF'
import hashlib, time, json, os, urllib.request, urllib.parse, urllib.error
from datetime import date, timedelta
# ---- Mintegral 认证(access-key=平台"Success key",token secret=平台"API key")----
API_KEY = "22793ef96b076fbdefa7c1266d4ebccb"
SKEY = "c47267b67f9e4231325efe4283044721"
BASE = "https://ss-api.mintegral.com/api/v2/reports/data"
def headers():
ts = str(int(time.time()))
token = hashlib.md5((SKEY + hashlib.md5(ts.encode()).hexdigest()).encode()).hexdigest()
return {"access-key": API_KEY, "token": token, "timestamp": ts}
def req(params):
url = BASE + "?" + urllib.parse.urlencode(params)
for attempt in range(6):
try:
r = urllib.request.Request(url, headers=headers())
with urllib.request.urlopen(r, timeout=90) as resp:
return resp.status, resp.headers.get("Content-Type",""), resp.read()
except urllib.error.HTTPError as e:
return e.code, e.headers.get("Content-Type",""), e.read()
except Exception as e:
time.sleep(5*(attempt+1))
raise RuntimeError("网络请求失败")
# ---- 目标 campid(Mintegral 渠道),优先从 campaigns.json 读,读不到则用内置列表 ----
FALLBACK_IDS = ["91530535","91530534","91530536","91530547","91530548",
"91530549","91530550","91530537","91530538","91551606",
"91534913","91533906"]
try:
camps = json.load(open("campaigns.json"))
mtg = {c["campid"]: c for c in camps if c.get("channel")=="Mintegral"}
except Exception:
mtg = {}
if not mtg:
mtg = {cid: {"campid":cid,"product":"","pkg":""} for cid in FALLBACK_IDS}
mtg_ids = list(mtg.keys())
# ---- 目标日期:前一天(UTC+8)----
target = (date.today() - timedelta(days=1)).isoformat()
print(f"[update] 拉取 {target} 的 Mintegral 数据 ...")
base = {"timezone":"+8","start_time":target,"end_time":target,
"dimension_option":"Offer,Location","time_granularity":"daily"}
# type=1 生成
for attempt in range(20):
p = dict(base); p["type"]="1"
s,ct,b = req(p)
try: code = json.loads(b.decode("utf-8","replace")).get("code")
except: code = None
if code == 200: break
time.sleep(6)
else:
print("[update] 数据生成超时,退出"); raise SystemExit(0)
# type=2 下载
p = dict(base); p["type"]="2"
s,ct,b = req(p)
if "octet-stream" not in ct:
print("[update] 下载失败:", b.decode("utf-8","replace")[:200]); raise SystemExit(0)
lines = b.decode("utf-8","replace").splitlines()
header = lines[0].split("\t"); col = {n:i for i,n in enumerate(header)}
new_rows = []
for line in lines[1:]:
f = line.split("\t")
if len(f) < len(header): continue
offer = f[col["Offer Name"]]
matched = next((cid for cid in mtg_ids if cid in offer), None)
if not matched: continue
rec = mtg[matched]
d = f[col["Date"]]
new_rows.append({
"date": f"{d[:4]}-{d[4:6]}-{d[6:]}",
"channel":"Mintegral","campid":matched,"offer_name":offer,
"product":rec.get("product",""),"pkg":rec.get("pkg",""),
"location":f[col["Location"]],"bid_type":"",
"impression":int(f[col["Impression"]]),"clicks":int(f[col["Click"]]),
"conversions":int(f[col["Conversion"]]),
"ctr":float(f[col["Ctr"]]),"cvr":float(f[col["Cvr"]]),"spend":float(f[col["Spend"]]),
})
print(f"[update] 新增匹配 {len(new_rows)} 条")
# ---- 增量合并(按 date+campid+location 去重;新数据覆盖同键,历史保留)----
existing = []
if os.path.exists("mtg_data.json"):
try: existing = json.load(open("mtg_data.json"))
except: existing = []
merged = {(r["date"],r["campid"],r["location"],r.get("channel","")): r for r in existing}
for r in new_rows:
merged[(r["date"],r["campid"],r["location"],r.get("channel",""))] = r
out = sorted(merged.values(), key=lambda x:(x["date"], x.get("channel",""), -x["impression"]))
json.dump(out, open("mtg_data.json","w"), ensure_ascii=False, indent=2)
print(f"[update] Mintegral 合并后共 {len(out)} 条")
PYEOF
# ============ Bigo 每日更新 ============
python3 - <<'PYEOF'
import json, os, base64, urllib.request, urllib.parse, urllib.error
from datetime import date, timedelta
CLIENT_ID = "2603256442212040610048"
SECRET = "14u0a4anx5a5"
TOKEN_URL = "https://api.adsbigo.com/oauth/token"
REPORT_URL = "https://api.adsbigo.com/openapi/report/list"
STATE = "bigo_token.json"
# 初始 refresh_token(首次运行用;之后从 STATE 文件读取轮转后的)
INIT_REFRESH = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlA0SkRvOUx1emlkeUVoZlF6SG1qbHRKR0R5bC81UXNKVzBZMDhkTmkrTHM9In0.eyJhdWQiOlsib3BlbmFwaSJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInJlYWQiXSwiYXRpIjoiMzY2YjMxYTYtYzg2OS00ZDMzLTkyM2EtZjZlZDdjN2QxNzIzIiwiZXhwIjoxNzkxNDQ3NjgxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl0sImp0aSI6ImM4MmZiZmNmLTY2YzQtNDE5MS05ZjFkLWViYjVhYTVkNTk1MSIsImNsaWVudF9pZCI6IjI2MDMyNTY0NDIyMTIwNDA2MTAwNDgifQ.VB-ntRjhOvUKIF_BToHOXXT5SCsLKnIyYDGaQwiFICpLHQWJ8Nktyau8FMvbPjIe6DRcYshGCxNRXhP-W1H6R1Nd8Phv-5YW8IUT4ihhHdwarTjpXBU4B9HB9FIatomVr-OubN0RBHWRBy2LArnFIUDvnbD3d6eWr8K0XN8_eGm_5FIT81JhNNVi4jdaeqSMA0cUApQoPyLLZf_CjqyNMlLG3ph4vUm7xD0qM3-T9K7dZCNba2z4FuGZHZ3h_arsFx0nLk0WAIGymuitvY8RNZUbx9EqZdifF__ybtsD-Za2V7KTr37sMXmMnlTaPahBGl2kMfRizgVvwTm0WNw52Q"
def get_refresh():
if os.path.exists(STATE):
try: return json.load(open(STATE)).get("refresh_token") or INIT_REFRESH
except: return INIT_REFRESH
return INIT_REFRESH
# 1) 用 refresh_token 换新 access_token(并轮转保存新的 refresh_token)
auth = base64.b64encode(f"{CLIENT_ID}:{SECRET}".encode()).decode()
data = urllib.parse.urlencode({"grant_type":"refresh_token","refresh_token":get_refresh()}).encode()
req = urllib.request.Request(TOKEN_URL, data=data, method="POST",
headers={"Content-Type":"application/x-www-form-urlencoded","Authorization":"Basic "+auth})
try:
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read().decode())
except urllib.error.HTTPError as e:
print("[update] Bigo token 刷新失败:", e.read().decode()[:200]); raise SystemExit(0)
access = tok.get("access_token")
if not access:
print("[update] Bigo 未拿到 access_token,跳过"); raise SystemExit(0)
# 保存轮转后的 token
json.dump(tok, open(STATE,"w"))
print("[update] Bigo token 已刷新")
# 2) 拉前一天数据
target = (date.today() - timedelta(days=1)).isoformat()
camps = json.load(open("campaigns.json"))
bigo = {c["pkg"]: c for c in camps if c.get("channel")=="Bigo"}
our_pkgs = set(bigo.keys())
body = {"timezone":8,"startDate":target,"endDate":target,
"indicators":["impression","click","ctr","real_result_conversion",
"real_result_conversion_rate","totalCost","campaignName","cpm","cpc"],
"breakDowns":["campaignId","country","tgtPkgName"],
"aggregateType":2,"pageNo":1,"pageSize":100000}
req = urllib.request.Request(REPORT_URL, data=json.dumps(body).encode(),
headers={"Content-Type":"application/json","Authorization":"Bearer "+access}, method="POST")
try:
with urllib.request.urlopen(req, timeout=120) as r:
d = json.loads(r.read().decode())
except urllib.error.HTTPError as e:
print("[update] Bigo 报表请求失败:", e.read().decode()[:200]); raise SystemExit(0)
if d.get("retcode") != 0:
print("[update] Bigo API:", d.get("retmsg")); raise SystemExit(0)
new_rows = []
for r in d["result"].get("list") or []:
pkg = r.get("tgtPkgName")
if pkg not in our_pkgs: continue
rec = bigo[pkg]
new_rows.append({
"date": r["aggregateTime"], "channel":"Bigo", "campid": rec["campid"],
"offer_name": r.get("campaignName",""), "product": rec["product"], "pkg": pkg,
"location": (r.get("country") or "").upper(), "bid_type":"",
"impression": int(r.get("impression") or 0), "clicks": int(r.get("click") or 0),
"conversions": int(r.get("real_result_conversion") or 0),
"ctr": float(r.get("ctr") or 0), "cvr": float(r.get("real_result_conversion_rate") or 0),
"spend": round(float(r.get("totalCost") or 0)/100.0, 2),
})
print(f"[update] Bigo 新增匹配 {len(new_rows)} 条 ({target})")
existing = json.load(open("mtg_data.json")) if os.path.exists("mtg_data.json") else []
merged = {(r["date"],r["campid"],r["location"],r.get("channel","")): r for r in existing}
for r in new_rows:
merged[(r["date"],r["campid"],r["location"],r["channel"])] = r
out = sorted(merged.values(), key=lambda x:(x["date"], x.get("channel",""), -x["impression"]))
json.dump(out, open("mtg_data.json","w"), ensure_ascii=False, indent=2)
print(f"[update] Bigo 合并后共 {len(out)} 条,已写入 mtg_data.json")
PYEOF
# ============ MediaGo 每日更新 ============
python3 - <<'PYEOF'
import json, os, time, urllib.request, urllib.parse, urllib.error
from datetime import date, timedelta
AUTH_URL = "https://api.mediago.com/manage/api/v1/access_token"
COST_URL = "https://api.mediago.com/report/api/v1/cost/daily"
TOKENS = [
"YWNjb3VundF8zmu5wjlv1vcRwbpdqaCugRvQKslUg9kQH4XnrNXefCbtGDcC",
"YWNjb3VundF8XrY8TwaXlX6pJDLnmfqmGgZT654Hh6rBRH9Cj9GzPXpO48BW",
"YWNjb3VundF89iFLKQQHlvwxnhVbWvchTAz009Zcjw7jTkOPzCDC3oLK0RGO",
"YWNjb3VundF815Lb3KpFWB8pnajnDZwL8Aw0YHpquAqUPWl78NKSaSigRBsY",
"YWNjb3VundF8SKbXp3ceGoHmS9SyMBleK2bIglGThBhoHly8yPoYOP8jZtww",
"YWNjb3VundF80DXnMfSwD4LWsV1hVVSA2j5FZQsWMSTQbJ6lTZIUhhrQZ9c6",
"YWNjb3VundF8gK0LuXHCZkNyC4R14Adayu4j1xnNJMzxvu1hokiwUPNOPJxS",
"YWNjb3VundF82rld59cRZIn53Uf0NhkWdcGIdNzOSRCZrkyj81xemBTQrOmi",
"YWNjb3VundF86E54f6seADTvrVjgeY3mrFNYsTn4jKVH5G5UyixyKkJi7T1S",
"YWNjb3VundF8MCmSFzV7LO4VRrxpJME8YrZCb6MyLp5KhRz4Q3crCjsDxPvI",
"YWNjb3VundF8tKh2Lye5ehjWsrSQUBTq6L4caohAKvG71qJVSOkMKDCKcKGu",
"YWNjb3VundF8xZYwIg4AFeVlTmrjBovgjxCSsHxYenyTTNbnNo1a0gIgs75V",
"YWNjb3VundF8QcNhks2O2ObIVmVXoIr1fuPPGTQ34YsaE0idvLVqTM5bSjbr",
"YWNjb3VundF8JtqahIBkypktae5H98q3PYjFXYPzqukmwY2wIOYqLSrYg0JQ",
"YWNjb3VundF8x4GsJSnSBns6y5tVGZRlDdC0w40k1ApVlj8v9cme64KNHmKq",
"YWNjb3VundF8GbE7ycPaH0fSozZ80v38jC00qdJB5YJRJJeqdy4DomjKG2ns",
"YWNjb3VundF83KP9txx57PNlElIa61g7vi7QQ1BTw4nlyPn6Tw8P4bXfX5IT",
"YWNjb3VundF8CDELpJV0axNw1AH4Pb7N1yfoe9HzyL0u9XQEnW3tgXVj1rbB",
"YWNjb3VundF8AzmnXydKyjDthsIx7BaYn6xof2lRh4pdJRTJT8qmupaLi7pX",
"YWNjb3VundF8PwLbajYpTYLe2dVhm1EX8seXxsPJlbpkxT8LhSONHhffW48Z",
"YWNjb3VundF8OGELq8i2wxXDw2kniuY9wp9nRz6xvoDU5jfFo2vsiUeoO2MV",
"YWNjb3VundF8u5aQbeM0LrTQcxc3t4DcbvZp1kBoDueP0q0rMjDPvLefG3WR",
]
camps = json.load(open("campaigns.json"))
mg = {c["campid"]: c for c in camps if c.get("channel") == "MediaGo"}
mg_ids = list(mg.keys())
if not mg:
print("[update] MediaGo 无目标 campid,跳过"); raise SystemExit(0)
PKGS = ",".join(sorted({c["pkg"] for c in mg.values()}))
target = (date.today() - timedelta(days=1)).isoformat()
def get_token(api_token):
req = urllib.request.Request(AUTH_URL, data=json.dumps({"api_token": api_token}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=40) as r:
return json.loads(r.read().decode())["data"]["AccessToken"]
def cost_daily(tok):
p = [("dimension","campaign"),("tz","UTC+8"),("app_ids",PKGS),
("start_date",target),("end_date",target),("page_idx",0),("page_size",100)]
req = urllib.request.Request(COST_URL + "?" + urllib.parse.urlencode(p),
headers={"Authorization":"Bearer "+tok}, method="GET")
with urllib.request.urlopen(req, timeout=60) as r:
d = json.loads(r.read().decode())
return d.get("data",{}).get("list") or [] if d.get("code")==0 else []
new_rows = []
for api_token in TOKENS:
try:
tok = get_token(api_token)
lst = cost_daily(tok)
except Exception:
continue
for r in lst:
cn = r.get("campaign_name","")
cid = next((c for c in mg_ids if cn.startswith(c) or c in cn), None)
if not cid: continue
imp=int(r.get("imp") or 0); clk=int(r.get("click") or 0); cv=int(r.get("cv") or 0); sp=float(r.get("spend") or 0)
if imp==0 and clk==0 and sp==0: continue
rec = mg[cid]
new_rows.append({
"date": r.get("date"), "channel":"MediaGo", "campid":cid,
"offer_name":cn, "product":rec["product"], "pkg":rec["pkg"],
"location":(r.get("geo") or "").upper(), "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(sp,2),
})
time.sleep(0.3)
print(f"[update] MediaGo 新增匹配 {len(new_rows)} 条 ({target})")
existing = json.load(open("mtg_data.json")) if os.path.exists("mtg_data.json") else []
merged = {(r["date"],r["campid"],r["location"],r.get("channel","")): r for r in existing}
for r in new_rows:
merged[(r["date"],r["campid"],r["location"],r["channel"])] = r
out = sorted(merged.values(), key=lambda x:(x["date"], x.get("channel",""), -x["impression"]))
json.dump(out, open("mtg_data.json","w"), ensure_ascii=False, indent=2)
print(f"[update] MediaGo 合并后共 {len(out)} 条,已写入 mtg_data.json")
PYEOF
# ============ 重新生成渠道投放矩阵状态(matrix_overrides.json)============
python3 - <<'PYEOF'
import json, os
from collections import defaultdict
if not os.path.exists("mtg_data.json"):
raise SystemExit(0)
d = json.load(open("mtg_data.json"))
orders = json.load(open("orders.json")) if os.path.exists("orders.json") else []
# 产品状态:按 pkg -> status("a / b" 合并形式拆开)
pkg_status = {}
for o in orders:
for p in str(o.get("pkg","")).replace(" ","").split("/"):
if p: pkg_status[p] = o.get("status","Pause")
# 聚合每个 (channel, pkg) 的每日展示
g = defaultdict(lambda: defaultdict(int))
for r in d:
g[(r["channel"], r["pkg"])][r["date"]] += r.get("impression", 0)
alldates = sorted(set(r["date"] for r in d))
if not alldates:
raise SystemExit(0)
lastday = alldates[-1]
last3 = alldates[-3:]
overrides = {}
for (ch, pkg), days in g.items():
prod_status = pkg_status.get(pkg, "Pause")
# 第一优先级:产品订单状态为 Pause -> 直接 Pause
if prod_status != "Active":
overrides[f"{ch}|{pkg}"] = "Pause"
continue
# 产品在投 -> 看数据判定该渠道
total = sum(days.values())
lastval = days.get(lastday, 0)
recent3 = sum(days.get(dt, 0) for dt in last3)
if total == 0:
overrides[f"{ch}|{pkg}"] = "none" # 零曝光=从未投放(前端留空)
elif lastval >= 10 and recent3 >= 30:
overrides[f"{ch}|{pkg}"] = "Active" # 末日两位数以上且近期持续
else:
overrides[f"{ch}|{pkg}"] = "Pause" # 掉到个位数/断更=已停
json.dump(overrides, open("matrix_overrides.json","w"), ensure_ascii=False, indent=2)
print(f"[update] 矩阵状态已刷新,共 {len(overrides)} 个渠道×产品组合")
PYEOF
echo "[update] $(date '+%Y-%m-%d %H:%M:%S') 完成"