Files
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

74 lines
3.9 KiB
Python

"""穿山甲 GroMore 天级收益报表(后台结算口径,定时拉取入库)。
每行 = GroMore 数据 API 返回的一条「日期 × 应用 × 代码位」聚合收益(`integrations/pangle_report`
+ `scripts/sync_pangle_revenue` 落库)。**权威/预估收益的来源**,与 `ad_ecpm_record`(客户端自报
eCPM 折算的预估)互为对照:
- `revenue_yuan` ← 接口 `revenue`(预估收益,元;排序价×展示/1000,所有 ADN 都有);
- `api_revenue_yuan` ← 接口 `api_revenue`(收益Api,元;各 ADN 经 Reporting 回传、更接近结算;
未配置该 ADN 的 Reporting 或查当天时为空)。
⚠️ 穿山甲不提供分用户/设备维度,故本表最细只到 日期×应用×代码位,**无法挂到逐条广告事件**;
广告收益报表里只用于汇总/趋势级的「穿山甲后台收益」,不改逐条行的客户端预估。
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import (
DateTime,
Float,
Integer,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class AdPangleDailyRevenue(Base):
__tablename__ = "ad_pangle_daily_revenue"
__table_args__ = (
# 一行 = (日期, 应用, 代码位, 广告源);adn 用 "" 表示「未分广告源、该代码位汇总」,
# 避免 NULL 在唯一约束里被视为各不相同导致 upsert 重复(SQLite/PG 行为一致)。
UniqueConstraint(
"report_date", "app_env", "our_code_id", "adn",
name="uq_ad_pangle_daily",
),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# 北京时间日期串 'YYYY-MM-DD'(拉取时 time_zone=8),与 ad_ecpm_record.report_date 同口径,可直接 join。
report_date: Mapped[str] = mapped_column(String(10), index=True, nullable=False)
# 我们的应用环境:prod(傻瓜比价正式)/ test(测试);由 site_id 经 settings 映射而来。
app_env: Mapped[str] = mapped_column(String(16), nullable=False)
# 原始 GroMore AppId(site_id),留痕便于排查映射。
site_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
# 广告位 ID(= 接口 ad_unit_id = 我们客户端配的 104xxx = ad_ecpm_record.our_code_id),join key。
# ⚠️ 不是接口的 code_id —— 那是底层各 ADN 的代码位(如 983674557),对不上我们的口径。
our_code_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
# 广告源(接口 network 数字→名,如 pangle/gdt);"" = 未分广告源的代码位汇总行(当前默认口径)。
adn: Mapped[str] = mapped_column(String(16), nullable=False, default="")
# 预估收益(元)← 接口 revenue。
revenue_yuan: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
# 收益Api(元)← 接口 api_revenue;未配 Reporting / 当天 等情况接口不返回 → NULL。
api_revenue_yuan: Mapped[float | None] = mapped_column(Float, nullable=True)
# 预估 eCPM 原值(接口 ecpm,单位元/千次,**与客户端 getEcpm 的「分」不同**),参考用原样存。
ecpm: Mapped[str | None] = mapped_column(String(32), nullable=True)
# 展示次数 ← 接口 imp_cnt。
impressions: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# 货币类型(接口 currency,正常为 cny)。
currency: Mapped[str] = mapped_column(String(8), nullable=False, default="cny")
# 最近一次同步写入时间(同一行可被多次回补覆盖;T+1 数据穿山甲会订正)。
synced_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
)
def __repr__(self) -> str: # pragma: no cover
return (
f"<AdPangleDailyRevenue {self.report_date} {self.app_env} "
f"code={self.our_code_id} revenue={self.revenue_yuan}>"
)