67ac2dcbbb
## 改动说明 - 领券成功率分母改为:发起数 - 中途退出数 - failed 与 started 仍保留在分母 - 接口新增 abandoned 和 success_denominator 字段 - 中途退出已有单券结果时返回真实成功/尝试数 - 中途退出且没有逐券终态时明确返回 0/0 - 新增 point_event_count,区分「只有 skipped、无有效计分结果」和「完全无逐券事件」 - 用户领券记录接口同步聚合逐券结果 ## 验证 - 相关后端测试:21 passed - Ruff:通过 - 线上数据库只读核对:空白记录确实没有 coupon_claim_event 配套前端:WonderableAI/shaguabijia-admin-web#101 --------- Co-authored-by: guke <guke@wonderable.ai> Co-authored-by: linkeyu <798648091@qq.com> Reviewed-on: #213 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
270 lines
9.8 KiB
Python
270 lines
9.8 KiB
Python
"""admin 领券明细逐场点位分数与按需明细。"""
|
|
from datetime import UTC, date, datetime
|
|
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import delete
|
|
|
|
from app.admin.main import admin_app
|
|
from app.admin.repositories import admin_user as admin_repo
|
|
from app.admin.repositories.coupon_data import (
|
|
_point_scores_by_trace,
|
|
coupon_data_report,
|
|
coupon_point_details,
|
|
coupon_user_records,
|
|
)
|
|
from app.admin.security import create_admin_token
|
|
from app.db.session import SessionLocal
|
|
from app.models.coupon_state import CouponClaimEvent, CouponSession
|
|
|
|
|
|
def test_point_scores_by_trace() -> None:
|
|
"""已领算成功、失败算尝试、跳过不进分母。"""
|
|
db = SessionLocal()
|
|
trace = "point-score-trace"
|
|
try:
|
|
db.add_all([
|
|
CouponClaimEvent(
|
|
trace_id=trace,
|
|
device_id="score-device",
|
|
coupon_id=f"mt-score-{status}",
|
|
claim_date=date(2020, 1, 2),
|
|
status=status,
|
|
coupon_name=f"测试点位-{status}",
|
|
reason="测试失败" if status == "failed" else None,
|
|
)
|
|
for status in ("success", "already_claimed", "failed", "skipped")
|
|
])
|
|
db.flush()
|
|
|
|
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"
|
|
]
|
|
assert details[2]["reason"] == "测试失败"
|
|
finally:
|
|
db.rollback()
|
|
db.close()
|
|
|
|
|
|
def test_skipped_detail_is_distinguished_from_no_events() -> None:
|
|
"""仅有 skipped 时分数仍为0/0,但保留事件数供前端开放明细。"""
|
|
db = SessionLocal()
|
|
trace = "point-score-skipped"
|
|
try:
|
|
db.add(CouponClaimEvent(
|
|
trace_id=trace,
|
|
device_id="score-device-skipped",
|
|
coupon_id="mt-score-skipped-only",
|
|
claim_date=date(2020, 1, 2),
|
|
status="skipped",
|
|
))
|
|
db.flush()
|
|
|
|
scores = _point_scores_by_trace(db, [trace, "missing-trace"])
|
|
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:
|
|
db.rollback()
|
|
db.close()
|
|
|
|
|
|
def test_coupon_data_report_returns_scores_without_embedding_details() -> None:
|
|
"""主列表只返回聚合分数,逐券记录必须走按 trace 的明细查询。"""
|
|
db = SessionLocal()
|
|
trace = "point-score-report"
|
|
report_date = date(2020, 1, 4)
|
|
try:
|
|
db.add(CouponSession(
|
|
trace_id=trace,
|
|
device_id="score-report-device",
|
|
status="completed",
|
|
app_env="prod",
|
|
platforms=["meituan-waimai"],
|
|
started_at=datetime(2020, 1, 4, tzinfo=UTC),
|
|
started_date=report_date,
|
|
))
|
|
db.add_all([
|
|
CouponClaimEvent(
|
|
trace_id=trace,
|
|
device_id="score-report-device",
|
|
coupon_id=f"mt-report-{status}",
|
|
claim_date=report_date,
|
|
status=status,
|
|
)
|
|
for status in ("success", "failed")
|
|
])
|
|
db.flush()
|
|
|
|
report = coupon_data_report(
|
|
db,
|
|
date_from=report_date.isoformat(),
|
|
date_to=report_date.isoformat(),
|
|
app_env="prod",
|
|
)
|
|
row = next(item for item in report["items"] if item["trace_id"] == trace)
|
|
assert row["point_success_count"] == 1
|
|
assert row["point_total_count"] == 2
|
|
assert "point_details" not in row
|
|
assert len(coupon_point_details(db, trace_id=trace)) == 2
|
|
finally:
|
|
db.rollback()
|
|
db.close()
|
|
|
|
|
|
def test_coupon_data_report_marks_abandoned_without_point_results() -> None:
|
|
"""中途退出且没有逐券终态时返回0/0,其他状态缺埋点仍保持为空。"""
|
|
db = SessionLocal()
|
|
report_date = date(2020, 1, 6)
|
|
user_id = 910006
|
|
try:
|
|
db.add_all([
|
|
CouponSession(
|
|
trace_id="point-score-abandoned-without-result",
|
|
device_id="score-abandoned-device",
|
|
user_id=user_id,
|
|
status="abandoned",
|
|
app_env="prod",
|
|
platforms=["meituan-waimai"],
|
|
started_at=datetime(2020, 1, 6, tzinfo=UTC),
|
|
started_date=report_date,
|
|
),
|
|
CouponSession(
|
|
trace_id="point-score-completed-without-result",
|
|
device_id="score-completed-device",
|
|
user_id=user_id,
|
|
status="completed",
|
|
app_env="prod",
|
|
platforms=["meituan-waimai"],
|
|
started_at=datetime(2020, 1, 6, 1, tzinfo=UTC),
|
|
started_date=report_date,
|
|
),
|
|
CouponSession(
|
|
trace_id="point-score-abandoned-with-result",
|
|
device_id="score-abandoned-result-device",
|
|
user_id=user_id,
|
|
status="abandoned",
|
|
app_env="prod",
|
|
platforms=["meituan-waimai"],
|
|
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(
|
|
trace_id="point-score-abandoned-with-result",
|
|
device_id="score-abandoned-result-device",
|
|
coupon_id=f"mt-abandoned-{status}",
|
|
claim_date=report_date,
|
|
status=status,
|
|
)
|
|
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(
|
|
db,
|
|
date_from=report_date.isoformat(),
|
|
date_to=report_date.isoformat(),
|
|
app_env="prod",
|
|
)
|
|
rows = {item["trace_id"]: item for item in report["items"]}
|
|
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
|
|
assert completed["point_total_count"] is None
|
|
|
|
user_rows = {
|
|
item["trace_id"]: item
|
|
for item in coupon_user_records(db, user_id=user_id)["items"]
|
|
}
|
|
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()
|
|
db.close()
|
|
|
|
|
|
def test_coupon_point_details_endpoint() -> None:
|
|
"""前端点击使用的接口按约定返回 trace_id 和逐券 items。"""
|
|
db = SessionLocal()
|
|
trace = "point-details-endpoint"
|
|
try:
|
|
admin = admin_repo.get_by_username(db, "point_details_admin")
|
|
if admin is None:
|
|
admin = admin_repo.create_admin(
|
|
db,
|
|
username="point_details_admin",
|
|
password="pass1234",
|
|
role="super_admin",
|
|
)
|
|
token, _expires_at = create_admin_token(admin_id=admin.id, role=admin.role)
|
|
db.add(CouponClaimEvent(
|
|
trace_id=trace,
|
|
device_id="point-details-endpoint-device",
|
|
coupon_id="mt-point-details-endpoint",
|
|
coupon_name="接口测试券",
|
|
claim_date=date(2020, 1, 5),
|
|
status="failed",
|
|
reason="接口测试失败",
|
|
))
|
|
db.commit()
|
|
|
|
response = TestClient(admin_app).get(
|
|
"/admin/api/coupon-data/point-details",
|
|
params={"trace_id": trace},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"trace_id": trace,
|
|
"items": [{
|
|
"coupon_id": "mt-point-details-endpoint",
|
|
"coupon_name": "接口测试券",
|
|
"status": "failed",
|
|
"reason": "接口测试失败",
|
|
}],
|
|
}
|
|
finally:
|
|
db.rollback()
|
|
db.execute(delete(CouponClaimEvent).where(CouponClaimEvent.trace_id == trace))
|
|
db.commit()
|
|
db.close()
|