Files
shaguabijia-app-server/app/admin/repositories/cps_orders.py
T
lowmaster-chen e1bd0e3ef7 feat: 接入数据大盘聚合与美团 CPS 对账
改了什么:新增新版大盘日期窗口聚合、广告收益拆分、比价耗时字段、美团 CPS 拉单入库与大盘展示,并同步反馈审核和邀请奖励下线口径。
验证:python -m pytest tests/test_admin_read.py tests/test_admin_write.py tests/test_compare_record.py tests/test_invite.py tests/test_feedback.py -q。
2026-06-28 09:56:13 +08:00

131 lines
4.1 KiB
Python

"""Admin CPS 订单对账:调用美团 query_order 并 upsert 到 cps_order。"""
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.integrations import meituan
from app.models.cps_order import CpsOrder
def _yuan_to_cents(v: object) -> int | None:
if v is None:
return None
s = str(v).strip()
if not s or s.lower() == "null":
return None
try:
return int((Decimal(s) * 100).to_integral_value())
except (InvalidOperation, ValueError):
return None
def _ts_to_dt(ts: object) -> datetime | None:
if ts is None:
return None
s = str(ts).strip()
if not s or s.lower() == "null":
return None
try:
return datetime.fromtimestamp(int(s), tz=timezone.utc)
except (OSError, TypeError, ValueError):
return None
def _map_order_fields(row: dict) -> dict:
product_name = row.get("productName")
if product_name and len(product_name) > 500:
product_name = product_name[:500]
return {
"sid": row.get("sid") or None,
"act_id": str(row["actId"]) if row.get("actId") is not None else None,
"biz_line": row.get("businessLine"),
"trade_type": row.get("tradeType"),
"pay_price_cents": _yuan_to_cents(row.get("payPrice")),
"commission_cents": _yuan_to_cents(row.get("profit") or row.get("cpaProfit")),
"commission_rate": (
str(row["commissionRate"]) if row.get("commissionRate") is not None else None
),
"refund_price_cents": _yuan_to_cents(row.get("refundPrice")),
"refund_profit_cents": _yuan_to_cents(row.get("refundProfit") or row.get("cpaRefundProfit")),
"mt_status": str(row["status"]) if row.get("status") is not None else None,
"invalid_reason": row.get("invalidReason") or None,
"product_name": product_name or None,
"pay_time": _ts_to_dt(row.get("payTime")),
"mt_update_time": _ts_to_dt(row.get("updateTime")),
"raw": row,
}
def reconcile_orders(
db: Session,
*,
start_time: int,
end_time: int,
query_time_type: int = 1,
sid: str | None = None,
platform: int | None = None,
business_line: list[int] | None = None,
trade_type: int | None = 1,
search_type: int = 2,
max_pages: int = 200,
) -> dict:
fetched = inserted = updated = pages = 0
page = 1
scroll_id: str | None = None
seen_scroll_ids: set[str] = set()
while pages < max_pages:
resp = meituan.query_order(
start_time=start_time,
end_time=end_time,
query_time_type=query_time_type,
sid=sid,
platform=platform,
business_line=business_line,
trade_type=trade_type,
search_type=search_type,
scroll_id=scroll_id,
page=page,
limit=100,
)
data = resp.get("data") or {}
rows = data.get("dataList") or []
if not rows:
break
pages += 1
for row in rows:
order_id = str(row.get("orderId") or "").strip()
if not order_id:
continue
fetched += 1
fields = _map_order_fields(row)
existing = db.execute(
select(CpsOrder).where(CpsOrder.order_id == order_id)
).scalar_one_or_none()
if existing is None:
db.add(CpsOrder(order_id=order_id, **fields))
inserted += 1
else:
for key, value in fields.items():
setattr(existing, key, value)
updated += 1
if search_type == 2:
next_scroll_id = data.get("scrollId")
if not next_scroll_id or next_scroll_id in seen_scroll_ids or len(rows) < 100:
break
seen_scroll_ids.add(next_scroll_id)
scroll_id = next_scroll_id
else:
if len(rows) < 100:
break
page += 1
db.commit()
return {"fetched": fetched, "inserted": inserted, "updated": updated, "pages": pages}