Merge branch 'main' into codex/coupon-point-score
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -25,7 +25,7 @@ from app.models.coupon_state import (
|
||||
from app.models.cps_order import CpsOrder
|
||||
from app.models.feedback import Feedback
|
||||
from app.models.savings import SavingsRecord
|
||||
from app.models.signin import SigninBoostRecord, SigninRecord
|
||||
from app.models.signin import SigninRecord
|
||||
from app.models.user import User
|
||||
from app.models.wallet import CoinTransaction, WithdrawOrder
|
||||
|
||||
@@ -678,7 +678,14 @@ def dashboard_overview(
|
||||
CoinTransaction.amount > 0,
|
||||
CoinTransaction.biz_type == "signin_boost",
|
||||
),
|
||||
"signin_boost_watch_count": _count(SigninBoostRecord),
|
||||
# 签到膨胀 2026-07 已下线,signin_boost_record 表随之 drop。这两项保留为**历史口径**
|
||||
# (钱是真发过的,账要能查回)。次数改数金币流水:一次膨胀 = 一笔 signin_boost 流水,
|
||||
# 与原来数 signin_boost_record 行数等价。
|
||||
"signin_boost_watch_count": _count(
|
||||
CoinTransaction,
|
||||
CoinTransaction.biz_type == "signin_boost",
|
||||
CoinTransaction.amount > 0,
|
||||
),
|
||||
},
|
||||
"cash": {
|
||||
"withdraw_success_cents": _sum(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)。"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user