#!/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 echo "[update] $(date '+%Y-%m-%d %H:%M:%S') 完成"