Files
shaguabijia-app-server/scripts/sync_pangle_revenue.py
T
zhuzihao b622f76a02 feat(ad-revenue): 接入穿山甲 GroMore 数据 API 拉取后台收益(预估+收益API) (#92)
- 新增穿山甲 GroMore「聚合数据报告 API」对接,按天 T+1 拉取后台 revenue(预估)与
  api_revenue(收益API,更接近结算),在广告收益报表大盘 + 按天趋势级展示,与客户端自报
  eCPM 折算的预估并列对照;逐条广告事件行不动(仍是客户端预估)。
- 链路:integrations/pangle_report.py(MD5 签名,与官方文档两个测试向量逐字节一致)→
  scripts/sync_pangle_revenue.py(拉昨天/--days 回补)→ 新表 ad_pangle_daily_revenue
  (repositories/ad_pangle_revenue.py:upsert + 按日聚合)→ admin/repositories/ad_revenue.py
  汇总,新增 total_pangle_revenue_yuan / total_pangle_api_revenue_yuan / daily[].pangle_*。
- 穿山甲无用户/类型/场景维度:仅全量视图(未按 user/ad_type/feed_scene 过滤)给值,否则置
  None;join key 用 ad_unit_id(=客户端配的 104xxx),非 code_id。
- 新增配置 PANGLE_REPORT_USER_ID/ROLE_ID/SECURITY_KEY(≠发奖 m-key)+ site_id→应用映射;
  含单测(签名向量+分页解析);已真连穿山甲验证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: zzhyyyyy <2685922758@qq.com>
Reviewed-on: #92
Co-authored-by: zhuzihao <zhuzihao@wonderable.ai>
Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
2026-06-29 09:54:13 +08:00

152 lines
6.3 KiB
Python
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.
"""每日拉取穿山甲 GroMore 天级收益报表入库(供 admin 广告收益报表的「穿山甲后台收益」)。
GroMore 数据 API 为 T+1:次日穿山甲约 10:00 出数。建议线上每天 ~10:30 由 systemd timer 跑一次
(默认拉【昨天】);穿山甲对历史数据可能订正,故支持回补近 N 天(幂等 upsert,重跑无害)。
用法:
python -m scripts.sync_pangle_revenue # 拉昨天(北京时间)
python -m scripts.sync_pangle_revenue --days 3 # 回补昨天起往前 3 天
python -m scripts.sync_pangle_revenue --date 2026-06-27 # 指定单天
python -m scripts.sync_pangle_revenue --start 2026-06-01 --end 2026-06-27 # 指定区间
约束(来自官方文档):天级跨度 ≤ 1 个月、不早于 12 个月;「今天」与「今天以前」必须分开查,
本脚本默认只拉昨天及更早,不混查今天。未配置 PANGLE_REPORT_* 时直接 no-op 退出。
"""
from __future__ import annotations
import argparse
import sys
from datetime import date, timedelta
# Windows 控制台按 UTF-8 输出中文/¥
try:
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except Exception: # noqa: BLE001
pass
from app.core.config import settings
from app.core.rewards import cn_today
from app.db.session import SessionLocal
from app.integrations import pangle_report
from app.integrations.pangle_report import PangleReportError
from app.repositories import ad_pangle_revenue as repo
# 拉取维度固定:date×site_id×ad_unit_id。⚠️ 用 ad_unit_id(GroMore 广告位ID = 我们客户端配的 104xxx
# = ad_ecpm_record.our_code_id)而**非** code_id —— 实测 code_id 返回的是底层各 ADN 的代码位
# (如 983674557 / youlianghui_xinxiliu),对不上我们的口径。指标取预估 + Api 两个收益。
_DIMENSIONS = "date,site_id,ad_unit_id"
_METRICS = "revenue,api_revenue,ecpm,imp_cnt"
def _to_float(v: object) -> float | None:
"""接口数值字段都是字符串;空/非法 → None(api_revenue 缺失时即为 None,前端显示「-」)。"""
if v is None or str(v).strip() == "":
return None
try:
return float(str(v).strip())
except (TypeError, ValueError):
return None
def _row_date(row: dict) -> str | None:
"""报表行的日期:优先 date 维度,回退 start_date;取前 10 位 'YYYY-MM-DD'"""
raw = row.get("date") or row.get("start_date") or ""
raw = str(raw).strip()
return raw[:10] if len(raw) >= 10 else None
def _normalize(rows: list[dict]) -> tuple[list[dict], int, set[str]]:
"""接口原始行 → 入库行;返回 (入库行, 跳过数, 未映射 site_id 集合)。
site_id 经 settings 映射成 app_env(prod/test);未映射的仍入库(app_env=site:<id>)但提示,
避免静默丢收益。缺 code_id / 日期的行跳过。
"""
site_map = settings.pangle_report_site_id_to_env
out: list[dict] = []
skipped = 0
unmapped: set[str] = set()
for row in rows:
rdate = _row_date(row)
# 用 ad_unit_id(GroMore 广告位ID = 我们的 104xxx),不是 code_id(底层 ADN 代码位)
code_id = str(row.get("ad_unit_id") or "").strip()
if not rdate or not code_id:
skipped += 1
continue
site_id = str(row.get("site_id") or "").strip()
app_env = site_map.get(site_id)
if app_env is None:
app_env = f"site:{site_id}" if site_id else "unknown"
if site_id:
unmapped.add(site_id)
out.append({
"report_date": rdate,
"app_env": app_env,
"site_id": site_id or None,
"our_code_id": code_id,
"adn": "",
"revenue_yuan": _to_float(row.get("revenue")) or 0.0,
"api_revenue_yuan": _to_float(row.get("api_revenue")),
"ecpm": (str(row.get("ecpm")).strip() or None) if row.get("ecpm") is not None else None,
"impressions": int(_to_float(row.get("imp_cnt")) or 0),
"currency": "cny",
})
return out, skipped, unmapped
def sync_range(start_date: str, end_date: str) -> None:
if not settings.pangle_report_configured:
print("PANGLE_REPORT_* 未配置,跳过(no-op)。需在后台「接入中心 → GroMore-API」领取 "
"user_id/role_id/Security Key 并填入 .env。")
return
print(f"拉取 GroMore 天级收益 {start_date} ~ {end_date} (dims={_DIMENSIONS}) ...")
try:
raw = pangle_report.fetch_daily_report(
start_date=start_date, end_date=end_date,
dimensions=_DIMENSIONS, metrics=_METRICS, time_zone=8,
)
except PangleReportError as e:
print(f"❌ 拉取失败:{e}")
sys.exit(1)
rows, skipped, unmapped = _normalize(raw)
if unmapped:
print(f"⚠️ 有未映射 site_id(收益仍入库,app_env=site:<id>):{sorted(unmapped)};"
f"如需归入 prod/test,补 PANGLE_REPORT_SITE_ID_* 配置。")
with SessionLocal() as db:
stats = repo.upsert_daily_rows(db, rows)
total_rev = round(sum(r["revenue_yuan"] for r in rows), 4)
print(f"✅ 完成:接口 {len(raw)} 行 → 入库 {len(rows)} 行(跳过 {skipped}),"
f"新增 {stats['inserted']} / 更新 {stats['updated']};预估收益合计 ¥{total_rev}")
def main() -> None:
ap = argparse.ArgumentParser(description="拉取穿山甲 GroMore 天级收益报表入库")
ap.add_argument("--date", help="指定单天 YYYY-MM-DD")
ap.add_argument("--start", help="区间起始 YYYY-MM-DD(与 --end 同用)")
ap.add_argument("--end", help="区间结束 YYYY-MM-DD(与 --start 同用)")
ap.add_argument("--days", type=int, help="回补:昨天起往前 N 天(含昨天)")
args = ap.parse_args()
yesterday = cn_today() - timedelta(days=1)
if args.date:
start = end = args.date
elif args.start and args.end:
start, end = args.start, args.end
elif args.days:
start = (yesterday - timedelta(days=args.days - 1)).isoformat()
end = yesterday.isoformat()
else:
start = end = yesterday.isoformat()
# 防呆:跨度 > 31 天直接拒(接口上限 1 个月,超了会返回 114)
if (date.fromisoformat(end) - date.fromisoformat(start)).days > 31:
print("❌ 区间跨度超过 31 天(接口上限 1 个月),请分批拉。")
sys.exit(1)
sync_range(start, end)
if __name__ == "__main__":
main()