Merge branch 'main' into fix/moneypagefix

This commit is contained in:
2026-07-22 10:53:07 +08:00
9 changed files with 436 additions and 29 deletions
+40 -7
View File
@@ -33,7 +33,29 @@ from app.admin.repositories import stats as admin_stats
from app.core import rewards
from app.models.ad_ecpm import AdEcpmRecord
from app.models.user import User
from app.repositories import ad_pangle_revenue
from app.repositories import ad_pangle_revenue, app_config
# 已上线过的正式业务代码位要永久保留,避免运营切换当前配置后,历史报表把旧业务位误判成测试流量。
_KNOWN_PROD_BUSINESS_CODE_IDS = frozenset({"104098712", "104099389"})
# 测试应用中实际承载业务链路的代码位。广告测试 demo 的插屏/半屏/信息流测试位不在这里,
# 避免“业务口径”把开发诊断曝光混进客户端与穿山甲对账。
_TEST_BUSINESS_CODE_IDS = frozenset({"104127529", "104127626", "104137445"})
def _business_code_ids(db: Session, app_env: str | None) -> set[str]:
"""返回指定应用环境下可用于业务收益对账的 GroMore 聚合代码位。"""
prod_config = app_config.get_ad_config(db)
prod_ids = set(_KNOWN_PROD_BUSINESS_CODE_IDS) | {
str(prod_config.get(key) or "").strip()
for key in ("reward_code_id", "compare_draw_code_id", "coupon_draw_code_id")
}
prod_ids.discard("")
if app_env == "prod":
return prod_ids
if app_env == "test":
return set(_TEST_BUSINESS_CODE_IDS)
return prod_ids | set(_TEST_BUSINESS_CODE_IDS)
def _cn_hour(dt: datetime) -> int:
@@ -82,6 +104,7 @@ def ad_revenue_report(
ad_type: str | None = None,
feed_scene: str | None = None,
app_env: str | None = None,
revenue_scope: str = "all",
granularity: str = "day",
limit: int = 500,
offset: int = 0,
@@ -277,14 +300,18 @@ def ad_revenue_report(
if feed_scene is not None:
events = [e for e in events if e.get("feed_scene") == feed_scene]
# app_env 过滤(2026-06-29 新增能力,修隐患:测试应用上报的假 eCPM 如 ¥678 CPM 会污染正式收益合计/平均):
# 显式传 "prod"/"test" 只看该环境;不传=全部(维持现状)。**不擅自把默认改成排除 test**——本地 dev 库多为
# test 数据、默认排除会使本地报表空,且「正式报表是否含 test」属产品口径。建议前端报表页加 app_env 筛选器
# (默认选 prod),或产品确认后再把默认改成排除 test。注:穿山甲后台收益列(total_pangle_*)暂未联动此过滤
# (它是独立对照列,且 pangle 的 test 是真实小额、非客户端那种假值)。
# app_env 过滤:显式传 "prod"/"test" 只看该环境;不传=全部。该参数也会传给下方穿山甲聚合,
# 保证客户端预估与 GroMore 汇总使用同一应用环境口径。
if app_env is not None:
events = [e for e in events if e.get("app_env") == app_env]
# 业务口径仅保留正式配置/测试业务链路实际使用的代码位。穿山甲“全量”还包含广告测试
# demo、插屏等没有客户端收益上报的曝光,两边直接比较会天然产生假差额。
business_code_ids: set[str] | None = None
if revenue_scope == "business":
business_code_ids = _business_code_ids(db, app_env)
events = [e for e in events if e.get("our_code_id") in business_code_ids]
# 排序:time=按时间倒序(新→旧);ecpm=按 eCPM 数值倒序(eCPM 原值是字符串「分」,转数值排;
# 纯发奖行用其发奖采用的 eCPM,缺失/非法计 0 排末尾)。
if sort == "ecpm":
@@ -336,7 +363,13 @@ def ad_revenue_report(
total_pangle_revenue_yuan: float | None = None
total_pangle_api_revenue_yuan: float | None = None
if pangle_filterable:
pangle_aggs = ad_pangle_revenue.aggregate_by_date(db, date_from=date_from, date_to=date_to)
pangle_aggs = ad_pangle_revenue.aggregate_by_date(
db,
date_from=date_from,
date_to=date_to,
app_env=app_env,
our_code_ids=business_code_ids,
)
if pangle_aggs:
by_date = {a["date"]: a for a in pangle_aggs}
for d in daily:
+133 -19
View File
@@ -5,7 +5,7 @@
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import date, datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo
from sqlalchemy import Select, asc, case, desc, func, or_, select
@@ -209,6 +209,45 @@ def _attach_user_info(db: Session, records: list[ComparisonRecord | Feedback | P
r.nickname = nick
def _comparison_conditions(
*,
user_id: int | None = None,
phone: str | None = None,
status: str | None = None,
business_type: str | None = None,
store: str | None = None,
product: str | None = None,
date_from: date | None = None,
date_to: date | None = None,
) -> list:
"""比价列表与概览共用筛选条件;日期按北京自然日闭区间解释。"""
conditions = []
if user_id is not None:
conditions.append(ComparisonRecord.user_id == user_id)
if phone:
conditions.append(
ComparisonRecord.user_id.in_(
select(User.id).where(User.phone.like(f"{phone}%"))
)
)
if status:
conditions.append(ComparisonRecord.status == status)
if business_type:
conditions.append(ComparisonRecord.business_type == business_type)
if store:
conditions.append(ComparisonRecord.store_name.like(f"%{store}%"))
if product:
conditions.append(ComparisonRecord.product_names.like(f"%{product}%"))
beijing = ZoneInfo("Asia/Shanghai")
if date_from is not None:
start_utc = datetime.combine(date_from, time.min, tzinfo=beijing).astimezone(timezone.utc)
conditions.append(ComparisonRecord.created_at >= start_utc)
if date_to is not None:
end_utc = datetime.combine(date_to + timedelta(days=1), time.min, tzinfo=beijing).astimezone(timezone.utc)
conditions.append(ComparisonRecord.created_at < end_utc)
return conditions
def list_comparison_records(
db: Session,
*,
@@ -218,30 +257,19 @@ def list_comparison_records(
business_type: str | None = None,
store: str | None = None,
product: str | None = None,
date_from: date | None = None,
date_to: date | None = None,
limit: int = 20,
cursor: int | None = None,
) -> tuple[list[ComparisonRecord], int | None, int]:
"""admin 比价记录列表(debug)。按 user_id 精确 或 phone 前缀定位用户 + status/业务类型筛,
store(店名)/product(商品名)子串模糊匹配,offset 分页(创建时间倒序、id 兜底)。
join User 取 phone/nickname 瞬态挂记录上。"""
stmt = select(ComparisonRecord)
if user_id is not None:
stmt = stmt.where(ComparisonRecord.user_id == user_id)
if phone:
stmt = stmt.where(
ComparisonRecord.user_id.in_(
select(User.id).where(User.phone.like(f"{phone}%"))
)
)
if status:
stmt = stmt.where(ComparisonRecord.status == status)
if business_type:
stmt = stmt.where(ComparisonRecord.business_type == business_type)
if store:
stmt = stmt.where(ComparisonRecord.store_name.like(f"%{store}%"))
if product:
# 商品名搜 product_names 派生文本列(非 items JSON:SQLite 下 JSON 中文被转义无法直接 LIKE)。
stmt = stmt.where(ComparisonRecord.product_names.like(f"%{product}%"))
conditions = _comparison_conditions(
user_id=user_id, phone=phone, status=status, business_type=business_type,
store=store, product=product, date_from=date_from, date_to=date_to,
)
stmt = select(ComparisonRecord).where(*conditions)
items, next_cursor, total = offset_paginate(
db, stmt,
(desc(ComparisonRecord.created_at), desc(ComparisonRecord.id)),
@@ -256,6 +284,92 @@ def list_comparison_records(
return items, next_cursor, total
def _comparison_percentile(sorted_values: list[int], q: float) -> int | None:
"""线性插值分位数(非负毫秒值四舍五入;单条数据返回自身)。"""
if not sorted_values:
return None
if len(sorted_values) == 1:
return sorted_values[0]
index = (len(sorted_values) - 1) * q
lower = int(index)
upper = min(lower + 1, len(sorted_values) - 1)
value = sorted_values[lower] * (upper - index) + sorted_values[upper] * (index - lower)
return int(value + 0.5)
def comparison_records_summary(
db: Session,
*,
user_id: int | None = None,
phone: str | None = None,
status: str | None = None,
business_type: str | None = None,
store: str | None = None,
product: str | None = None,
date_from: date | None = None,
date_to: date | None = None,
) -> dict:
"""比价记录页概览聚合;主耗时均值及分位数只取成功记录。"""
conditions = _comparison_conditions(
user_id=user_id, phone=phone, status=status, business_type=business_type,
store=store, product=product, date_from=date_from, date_to=date_to,
)
row = db.execute(
select(
func.count(ComparisonRecord.id),
func.sum(case((ComparisonRecord.status.in_(("success", "failed")), 1), else_=0)),
func.sum(case((ComparisonRecord.status == "success", 1), else_=0)),
func.avg(ComparisonRecord.llm_cost_yuan),
func.sum(case((
(ComparisonRecord.status == "success")
& (ComparisonRecord.saved_amount_cents > 0), 1
), else_=0)),
func.sum(case((ComparisonRecord.status == "cancelled", 1), else_=0)),
).where(*conditions)
).one()
started = int(row[0] or 0)
completed = int(row[1] or 0)
success = int(row[2] or 0)
lower_price = int(row[4] or 0)
cancelled = int(row[5] or 0)
success_durations = sorted(db.execute(
select(ComparisonRecord.total_ms).where(
*conditions,
ComparisonRecord.status == "success",
ComparisonRecord.total_ms.is_not(None),
)
).scalars().all())
cancelled_durations = sorted(db.execute(
select(ComparisonRecord.total_ms).where(
*conditions,
ComparisonRecord.status == "cancelled",
ComparisonRecord.total_ms.is_not(None),
)
).scalars().all())
success_rate_denominator = started - cancelled
return {
"started": started,
"completed": completed,
"success": success,
"success_rate": success / success_rate_denominator if success_rate_denominator else None,
"avg_token_cost": float(row[3]) if row[3] is not None else None,
"lower_price_rate": lower_price / success if success else None,
"avg_duration_ms": (
int(sum(success_durations) / len(success_durations) + 0.5)
if success_durations else None
),
"p5_duration_ms": _comparison_percentile(success_durations, 0.05),
"p50_duration_ms": _comparison_percentile(success_durations, 0.5),
"p95_duration_ms": _comparison_percentile(success_durations, 0.95),
"p99_duration_ms": _comparison_percentile(success_durations, 0.99),
"cancelled": cancelled,
"cancelled_rate": cancelled / started if started else None,
"cancelled_p5_ms": _comparison_percentile(cancelled_durations, 0.05),
"cancelled_p50_ms": _comparison_percentile(cancelled_durations, 0.5),
"cancelled_p95_ms": _comparison_percentile(cancelled_durations, 0.95),
}
def get_comparison_record(db: Session, record_id: int) -> ComparisonRecord | None:
"""admin 取单条比价记录(任意用户,不限本人;附 phone/nickname 瞬态)。"""
rec = db.get(ComparisonRecord, record_id)
+9 -1
View File
@@ -5,7 +5,7 @@
from __future__ import annotations
from datetime import date as _date
from typing import Annotated
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, HTTPException, Query
@@ -63,6 +63,13 @@ def get_ad_revenue_report(
"建议正式收益报表选 prod,避免测试应用的假 eCPM 污染收益合计/平均"
),
] = None,
revenue_scope: Annotated[
Literal["business", "all"],
Query(
description="business=仅业务代码位(用于客户端与穿山甲同口径对账)/ "
"all=穿山甲应用全部代码位(包含广告测试等非业务曝光)"
),
] = "all",
granularity: Annotated[
str, Query(description="day=按天 / hour=按小时(北京时间);区间>1 天建议用 day")
] = "day",
@@ -83,6 +90,7 @@ def get_ad_revenue_report(
result = ad_revenue.ad_revenue_report(
db, date_from=d_from.isoformat(), date_to=d_to.isoformat(),
user_id=user_id, ad_type=ad_type, feed_scene=feed_scene, app_env=app_env,
revenue_scope=revenue_scope,
granularity=granularity, limit=limit, offset=offset, sort=sort,
)
return AdRevenueReportOut(
+32 -1
View File
@@ -5,6 +5,7 @@ trace_url 无条件下发——admin 是内部 debug 工具,不走 C 端 user.de
"""
from __future__ import annotations
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Query
@@ -12,7 +13,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from app.admin.deps import AdminDb, get_current_admin
from app.admin.repositories import queries
from app.admin.schemas.common import CursorPage
from app.admin.schemas.comparison import AdminComparisonDetail, AdminComparisonListItem
from app.admin.schemas.comparison import (
AdminComparisonDetail,
AdminComparisonListItem,
AdminComparisonSummary,
)
router = APIRouter(
prefix="/admin/api/comparison-records",
@@ -34,12 +39,15 @@ def list_comparison_records(
business_type: Annotated[str | None, Query()] = None,
store: Annotated[str | None, Query(description="店名子串模糊匹配")] = None,
product: Annotated[str | None, Query(description="商品名子串模糊匹配")] = None,
date_from: Annotated[date | None, Query(description="北京自然日起始日")] = None,
date_to: Annotated[date | None, Query(description="北京自然日结束日")] = None,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
cursor: Annotated[int | None, Query()] = None,
) -> CursorPage[AdminComparisonListItem]:
items, next_cursor, total = queries.list_comparison_records(
db, user_id=user_id, phone=phone, status=status,
business_type=business_type, store=store, product=product,
date_from=date_from, date_to=date_to,
limit=limit, cursor=cursor,
)
return CursorPage(
@@ -49,6 +57,29 @@ def list_comparison_records(
)
@router.get(
"/summary",
response_model=AdminComparisonSummary,
summary="比价记录概览聚合",
)
def comparison_records_summary(
db: AdminDb,
user_id: Annotated[int | None, Query()] = None,
phone: Annotated[str | None, Query(description="手机号前缀")] = None,
status: Annotated[str | None, Query(pattern="^(success|failed|cancelled)$")] = None,
business_type: Annotated[str | None, Query()] = None,
store: Annotated[str | None, Query(description="店名子串模糊匹配")] = None,
product: Annotated[str | None, Query(description="商品名子串模糊匹配")] = None,
date_from: Annotated[date | None, Query(description="北京自然日起始日")] = None,
date_to: Annotated[date | None, Query(description="北京自然日结束日")] = None,
) -> AdminComparisonSummary:
return AdminComparisonSummary(**queries.comparison_records_summary(
db, user_id=user_id, phone=phone, status=status,
business_type=business_type, store=store, product=product,
date_from=date_from, date_to=date_to,
))
@router.get(
"/{record_id}",
response_model=AdminComparisonDetail,
+21
View File
@@ -47,6 +47,27 @@ class AdminComparisonListItem(BaseModel):
created_at: datetime
class AdminComparisonSummary(BaseModel):
"""比价记录页概览;主耗时指标仅统计 status=success。"""
started: int
completed: int
success: int
success_rate: float | None = None
avg_token_cost: float | None = None
lower_price_rate: float | None = None
avg_duration_ms: int | None = None
p5_duration_ms: int | None = None
p50_duration_ms: int | None = None
p95_duration_ms: int | None = None
p99_duration_ms: int | None = None
cancelled: int
cancelled_rate: float | None = None
cancelled_p5_ms: int | None = None
cancelled_p50_ms: int | None = None
cancelled_p95_ms: int | None = None
class AdminComparisonDetail(AdminComparisonListItem):
"""详情:概要 + 全量明细(逐平台对比 / LLM 每次调用 / 原始 payload)。"""
+4
View File
@@ -6,6 +6,7 @@
"""
from __future__ import annotations
from collections.abc import Collection
from typing import Any, TypedDict
from sqlalchemy import func, select
@@ -73,6 +74,7 @@ def aggregate_by_date(
date_to: str,
app_env: str | None = None,
our_code_id: str | None = None,
our_code_ids: Collection[str] | None = None,
) -> list[PangleDateAgg]:
"""按日期汇总穿山甲收益(闭区间,北京时间),供报表趋势 + 合计。
@@ -97,6 +99,8 @@ def aggregate_by_date(
stmt = stmt.where(AdPangleDailyRevenue.app_env == app_env)
if our_code_id is not None:
stmt = stmt.where(AdPangleDailyRevenue.our_code_id == our_code_id)
if our_code_ids is not None:
stmt = stmt.where(AdPangleDailyRevenue.our_code_id.in_(our_code_ids))
out: list[PangleDateAgg] = []
for report_date, rev, api_rev, imp in db.execute(stmt).all():
+3 -1
View File
@@ -28,6 +28,8 @@
| `user_id` | int | 全部 | 只看某用户;不传=所有用户 |
| `ad_type` | string | 全部 | `reward_video` / `feed` / `draw`;不传=全部类型 |
| `feed_scene` | string | 全部 | `comparison`(比价)/ `coupon`(领券)/ `welfare`(福利);**全局筛选**,同时作用于明细 / 合计 / `daily`·`hourly` 趋势;不传=全部场景 |
| `app_env` | string | 全部 | `prod`=正式应用 / `test`=测试应用;同时过滤客户端预估与穿山甲汇总 |
| `revenue_scope` | string | `all` | `business`=仅业务代码位,用于同口径对账 / `all`=应用全部代码位,包含广告测试等非业务曝光 |
| `granularity` | string | `day` | `day`=按天 / `hour`=按小时(聚合键再加北京时间小时 0–23);**区间>1 天建议用 day** |
| `limit` | int(1~1000) | 500 | **每页条数**(分页大小);`total`/`total_*`/`daily`/`hourly` 按全量统计不受分页影响 |
| `offset` | int(≥0) | 0 | 分页偏移(已跳过条数)=(页码−1)×`limit` |
@@ -131,5 +133,5 @@
- **历史 Draw 不可拆**:迁移(Draw→普通信息流)前,Draw 发奖混在 `ad_feed_reward_record` 且无类型标记,金币侧统一记 `feed`;迁移后 Draw 不再产生新数据。展示侧 `ad_type` 由客户端上报区分,故 `draw` 桶基本为空。
- **来源字段从上线起齐全**:`app_env`/`our_code_id` 是本期新增列,历史记录为 NULL(报表来源列留空)。
- **逐条/明细的收益是预估**:`items[].revenue_yuan` 基于客户端上报的 eCPM 折算,非穿山甲后台结算值。
- **穿山甲后台收益(汇总/趋势级)**:`total_pangle_revenue_yuan`(预估 `revenue`)与 `total_pangle_api_revenue_yuan`(收益Api `api_revenue`,更接近结算)来自穿山甲 **GroMore 数据 API**(`integrations/pangle_report` + `scripts/sync_pangle_revenue` 按天 T+1 拉取入 [ad_pangle_daily_revenue](../database/ad_pangle_daily_revenue.md))。穿山甲**不提供分用户/设备/类型/场景维度**(官方明确),最细到 日期×应用×代码位,故只用于汇总与按天趋势的对照,**不挂到逐条事件行**;且仅在全量视图(未按 user/类型/场景过滤)展示。配置见 `.env``PANGLE_REPORT_*`
- **穿山甲后台收益(汇总/趋势级)**:`total_pangle_revenue_yuan`(预估 `revenue`)与 `total_pangle_api_revenue_yuan`(收益Api `api_revenue`,更接近结算)来自穿山甲 **GroMore 数据 API**(`integrations/pangle_report` + `scripts/sync_pangle_revenue` 按天 T+1 拉取入 [ad_pangle_daily_revenue](../database/ad_pangle_daily_revenue.md))。穿山甲**不提供分用户/设备/类型/场景维度**(官方明确),最细到 日期×应用×代码位,故只用于汇总与按天趋势的对照,**不挂到逐条事件行**;且仅在未按 user/类型/场景过滤展示。`app_env``revenue_scope` 会同时过滤客户端和穿山甲数据,其中 `business` 排除广告测试等非业务代码位。配置见 `.env``PANGLE_REPORT_*`
- **对账聚合级 + 逐条下钻**:行级 `matched` 给出该组(用户×类型×应用×代码位)应发是否==实发;**展开 `records` 即可看该组逐条明细**(eCPM/因子1/份数/LT/因子2/应发/实发/一致)定位到具体记录。独立逐条审计接口 [admin-ad-coin-audit](./admin-ad-coin-audit.md) 仍保留(同一复算口径,可全局按场景/只看不符筛选)。
+129
View File
@@ -0,0 +1,129 @@
"""广告收益报表的环境与业务代码位口径。"""
from __future__ import annotations
from datetime import UTC, datetime
from sqlalchemy import delete
from app.admin.repositories import ad_revenue
from app.db.session import SessionLocal
from app.models.ad_ecpm import AdEcpmRecord
from app.models.ad_pangle_revenue import AdPangleDailyRevenue
from app.models.user import User
REPORT_DATE = "2040-02-03"
def test_business_scope_filters_client_and_pangle_by_env_and_code(monkeypatch) -> None:
db = SessionLocal()
try:
user = User(
phone="18800009991",
username="29999999991",
register_channel="sms",
)
db.add(user)
db.flush()
db.add_all([
AdEcpmRecord(
user_id=user.id, ad_type="reward_video", ad_session_id="scope-prod-business",
app_env="prod", our_code_id="prod-reward", ecpm_raw="10000",
report_date=REPORT_DATE, created_at=datetime(2040, 2, 3, tzinfo=UTC),
),
AdEcpmRecord(
user_id=user.id, ad_type="draw", ad_session_id="scope-prod-demo",
app_env="prod", our_code_id="prod-demo", ecpm_raw="20000",
report_date=REPORT_DATE, created_at=datetime(2040, 2, 3, tzinfo=UTC),
),
AdEcpmRecord(
user_id=user.id, ad_type="draw", ad_session_id="scope-prod-known-business",
app_env="prod", our_code_id="104098712", ecpm_raw="40000",
report_date=REPORT_DATE, created_at=datetime(2040, 2, 3, tzinfo=UTC),
),
AdEcpmRecord(
user_id=user.id, ad_type="reward_video", ad_session_id="scope-test-business",
app_env="test", our_code_id="104127529", ecpm_raw="30000",
report_date=REPORT_DATE, created_at=datetime(2040, 2, 3, tzinfo=UTC),
),
AdPangleDailyRevenue(
report_date=REPORT_DATE, app_env="prod", our_code_id="prod-reward",
adn="", revenue_yuan=1.5, api_revenue_yuan=1.2, impressions=10,
),
AdPangleDailyRevenue(
report_date=REPORT_DATE, app_env="prod", our_code_id="prod-demo",
adn="", revenue_yuan=8.0, api_revenue_yuan=7.0, impressions=40,
),
AdPangleDailyRevenue(
report_date=REPORT_DATE, app_env="prod", our_code_id="104098712",
adn="", revenue_yuan=2.5, api_revenue_yuan=2.0, impressions=20,
),
AdPangleDailyRevenue(
report_date=REPORT_DATE, app_env="test", our_code_id="104127529",
adn="", revenue_yuan=9.0, api_revenue_yuan=8.0, impressions=50,
),
])
db.commit()
monkeypatch.setattr(
ad_revenue.app_config,
"get_ad_config",
lambda _db: {
"reward_code_id": "prod-reward",
"compare_draw_code_id": "prod-draw",
"coupon_draw_code_id": "prod-draw",
},
)
business = ad_revenue.ad_revenue_report(
db,
date_from=REPORT_DATE,
date_to=REPORT_DATE,
app_env="prod",
revenue_scope="business",
)
assert business["total_impressions"] == 2
assert business["total_revenue_yuan"] == 0.5
assert business["total_pangle_revenue_yuan"] == 4.0
assert business["total_pangle_api_revenue_yuan"] == 3.2
all_codes = ad_revenue.ad_revenue_report(
db,
date_from=REPORT_DATE,
date_to=REPORT_DATE,
app_env="prod",
revenue_scope="all",
)
assert all_codes["total_impressions"] == 3
assert all_codes["total_revenue_yuan"] == 0.7
assert all_codes["total_pangle_revenue_yuan"] == 12.0
assert all_codes["total_pangle_api_revenue_yuan"] == 10.2
test_business = ad_revenue.ad_revenue_report(
db,
date_from=REPORT_DATE,
date_to=REPORT_DATE,
app_env="test",
revenue_scope="business",
)
assert test_business["total_impressions"] == 1
assert test_business["total_revenue_yuan"] == 0.3
assert test_business["total_pangle_revenue_yuan"] == 9.0
assert test_business["total_pangle_api_revenue_yuan"] == 8.0
all_env_business = ad_revenue.ad_revenue_report(
db,
date_from=REPORT_DATE,
date_to=REPORT_DATE,
app_env=None,
revenue_scope="business",
)
assert all_env_business["total_impressions"] == 3
assert all_env_business["total_revenue_yuan"] == 0.8
assert all_env_business["total_pangle_revenue_yuan"] == 13.0
assert all_env_business["total_pangle_api_revenue_yuan"] == 11.2
finally:
db.rollback()
db.execute(delete(AdPangleDailyRevenue).where(AdPangleDailyRevenue.report_date == REPORT_DATE))
db.execute(delete(AdEcpmRecord).where(AdEcpmRecord.report_date == REPORT_DATE))
db.execute(delete(User).where(User.phone == "18800009991"))
db.commit()
db.close()
+65
View File
@@ -0,0 +1,65 @@
"""比价记录页后端分页与概览聚合。"""
from __future__ import annotations
from datetime import UTC, date, datetime
import pytest
from app.admin.repositories import queries
from app.db.session import SessionLocal
from app.models.comparison import ComparisonRecord
def test_summary_uses_only_success_durations_and_filters_beijing_date() -> None:
db = SessionLocal()
try:
rows = [
("summary-success-a", "success", 1000, 1.0, 100),
("summary-success-b", "success", 3000, 2.0, 0),
("summary-failed", "failed", 100_000, 3.0, 0),
("summary-cancelled", "cancelled", 5000, 4.0, 0),
]
for trace_id, status, total_ms, cost, saved in rows:
db.add(ComparisonRecord(
trace_id=trace_id,
status=status,
total_ms=total_ms,
llm_cost_yuan=cost,
saved_amount_cents=saved,
created_at=datetime(2038, 1, 15, 12, tzinfo=UTC),
))
db.add(ComparisonRecord(
trace_id="summary-outside-day",
status="success",
total_ms=999_999,
created_at=datetime(2038, 1, 16, 16, tzinfo=UTC),
))
db.flush()
summary = queries.comparison_records_summary(
db, date_from=date(2038, 1, 15), date_to=date(2038, 1, 15)
)
assert summary["started"] == 4
assert summary["completed"] == 3
assert summary["success"] == 2
assert summary["success_rate"] == pytest.approx(2 / 3)
assert summary["avg_token_cost"] == pytest.approx(2.5)
assert summary["lower_price_rate"] == 0.5
assert summary["avg_duration_ms"] == 2000
assert summary["p5_duration_ms"] == 1100
assert summary["p50_duration_ms"] == 2000
assert summary["p95_duration_ms"] == 2900
assert summary["p99_duration_ms"] == 2980
assert summary["cancelled"] == 1
assert summary["cancelled_rate"] == 0.25
assert summary["cancelled_p50_ms"] == 5000
items, _next_cursor, total = queries.list_comparison_records(
db, date_from=date(2038, 1, 15), date_to=date(2038, 1, 15), limit=20
)
assert total == 4
assert {item.trace_id for item in items} == {row[0] for row in rows}
finally:
db.rollback()
db.close()