diff --git a/app/admin/repositories/coupon_data.py b/app/admin/repositories/coupon_data.py index eb022fa..6fb6772 100644 --- a/app/admin/repositories/coupon_data.py +++ b/app/admin/repositories/coupon_data.py @@ -177,6 +177,7 @@ def _session_to_row( else: point_success_count = None point_total_count = None + point_event_count = point_stats["events"] if point_stats is not None else 0 return { "id": r.id, "trace_id": r.trace_id, @@ -195,6 +196,7 @@ def _session_to_row( "claimed_count": r.claimed_count, "point_success_count": point_success_count, "point_total_count": point_total_count, + "point_event_count": point_event_count, "trace_url": r.trace_url, "ad_revenue_yuan": ad_revenue_yuan, } @@ -205,21 +207,24 @@ def _point_scores_by_trace(db: Session, trace_ids: list[str]) -> dict[str, dict[ if not trace_ids: return {} succeeded = func.sum(case((CouponClaimEvent.status.in_(_SLOT_OK), 1), else_=0)) + tried = func.sum(case((CouponClaimEvent.status.in_(_SLOT_TRIED), 1), else_=0)) rows = db.execute( select( CouponClaimEvent.trace_id, succeeded.label("succeeded"), - func.count().label("tried"), - ) - .where( - CouponClaimEvent.trace_id.in_(trace_ids), - CouponClaimEvent.status.in_(_SLOT_TRIED), + tried.label("tried"), + func.count().label("events"), ) + .where(CouponClaimEvent.trace_id.in_(trace_ids)) .group_by(CouponClaimEvent.trace_id) ).all() return { - trace_id: {"succeeded": int(success_count or 0), "tried": int(tried or 0)} - for trace_id, success_count, tried in rows + trace_id: { + "succeeded": int(success_count or 0), + "tried": int(tried_count or 0), + "events": int(event_count or 0), + } + for trace_id, success_count, tried_count, event_count in rows if trace_id is not None } diff --git a/app/admin/schemas/coupon_data.py b/app/admin/schemas/coupon_data.py index fc063e0..3cad3c9 100644 --- a/app/admin/schemas/coupon_data.py +++ b/app/admin/schemas/coupon_data.py @@ -86,6 +86,10 @@ class CouponDataRow(BaseModel): None, description="本次尝试单券数(success+already_claimed+failed,不含 skipped);中途退出且无逐券结果为0,其它无事件为空", ) + point_event_count: int = Field( + 0, + description="本次全部逐券事件数(含 skipped);用于区分无有效计分事件与完全无事件", + ) trace_url: str | None = Field(None, description="pricebot 公网 trace 链接(仅 completed 有);admin 渲染可点链接,无则显示可复制 trace_id") ad_revenue_yuan: float = Field( 0.0, description="本次领券看的信息流广告预估收益(元);按 trace_id 聚合 ad_ecpm_record" diff --git a/tests/test_coupon_point_score.py b/tests/test_coupon_point_score.py index d0fba6a..b7c322a 100644 --- a/tests/test_coupon_point_score.py +++ b/tests/test_coupon_point_score.py @@ -39,6 +39,7 @@ def test_point_scores_by_trace() -> None: stats = _point_scores_by_trace(db, [trace])[trace] assert stats["succeeded"] == 2 assert stats["tried"] == 3 + assert stats["events"] == 4 details = coupon_point_details(db, trace_id=trace) assert [item["status"] for item in details] == [ "success", "already_claimed", "failed", "skipped" @@ -49,8 +50,8 @@ def test_point_scores_by_trace() -> None: db.close() -def test_skipped_detail_does_not_create_a_score() -> None: - """仅有 skipped 时按需明细仍可查到,但列表没有虚假的 0/0 分数。""" +def test_skipped_detail_is_distinguished_from_no_events() -> None: + """仅有 skipped 时分数仍为0/0,但保留事件数供前端开放明细。""" db = SessionLocal() trace = "point-score-skipped" try: @@ -64,7 +65,7 @@ def test_skipped_detail_does_not_create_a_score() -> None: db.flush() scores = _point_scores_by_trace(db, [trace, "missing-trace"]) - assert trace not in scores + assert scores[trace] == {"succeeded": 0, "tried": 0, "events": 1} assert "missing-trace" not in scores assert coupon_point_details(db, trace_id=trace)[0]["status"] == "skipped" finally: @@ -152,6 +153,16 @@ def test_coupon_data_report_marks_abandoned_without_point_results() -> None: started_at=datetime(2020, 1, 6, 2, tzinfo=UTC), started_date=report_date, ), + CouponSession( + trace_id="point-score-abandoned-skipped-only", + device_id="score-abandoned-skipped-device", + user_id=user_id, + status="abandoned", + app_env="prod", + platforms=["meituan-waimai"], + started_at=datetime(2020, 1, 6, 3, tzinfo=UTC), + started_date=report_date, + ), ]) db.add_all([ CouponClaimEvent( @@ -163,6 +174,13 @@ def test_coupon_data_report_marks_abandoned_without_point_results() -> None: ) for status in ("success", "failed") ]) + db.add(CouponClaimEvent( + trace_id="point-score-abandoned-skipped-only", + device_id="score-abandoned-skipped-device", + coupon_id="mt-abandoned-skipped", + claim_date=report_date, + status="skipped", + )) db.flush() report = coupon_data_report( @@ -175,10 +193,17 @@ def test_coupon_data_report_marks_abandoned_without_point_results() -> None: abandoned = rows["point-score-abandoned-without-result"] assert abandoned["point_success_count"] == 0 assert abandoned["point_total_count"] == 0 + assert abandoned["point_event_count"] == 0 abandoned_with_result = rows["point-score-abandoned-with-result"] assert abandoned_with_result["point_success_count"] == 1 assert abandoned_with_result["point_total_count"] == 2 + assert abandoned_with_result["point_event_count"] == 2 + + abandoned_skipped = rows["point-score-abandoned-skipped-only"] + assert abandoned_skipped["point_success_count"] == 0 + assert abandoned_skipped["point_total_count"] == 0 + assert abandoned_skipped["point_event_count"] == 1 completed = rows["point-score-completed-without-result"] assert completed["point_success_count"] is None @@ -190,6 +215,7 @@ def test_coupon_data_report_marks_abandoned_without_point_results() -> None: } assert user_rows["point-score-abandoned-without-result"]["point_total_count"] == 0 assert user_rows["point-score-abandoned-with-result"]["point_total_count"] == 2 + assert user_rows["point-score-abandoned-skipped-only"]["point_event_count"] == 1 assert user_rows["point-score-completed-without-result"]["point_total_count"] is None finally: db.rollback()