From dd6ca43cd63edbacea57235ebd410f2da7f4cd55 Mon Sep 17 00:00:00 2001 From: guke Date: Tue, 7 Jul 2026 17:46:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E6=AF=94=E4=BB=B7/=E9=A2=86?= =?UTF-8?q?=E5=88=B8=E5=A5=96=E5=8A=B1=E9=87=91=E5=B8=81=E6=8C=89=20feed?= =?UTF-8?q?=5Fscene=20=E6=B1=87=E6=80=BB(=E4=BF=AE=E5=A4=8D=E6=81=92=200?= =?UTF-8?q?=20+=20=E6=BF=80=E5=8A=B1=E8=A7=86=E9=A2=91=E5=8F=8C=E8=AE=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 比价/领券奖励金币此前查 coin_transaction.biz_type in (comparison/coupon...), 但这些 biz_type 全站从未写入——比价/领券信息流广告金币实际记为 feed_ad_reward、 场景区分在 ad_feed_reward_record.feed_scene——故两卡恒 0;领券桶还误含 reward_video/ad_reward,把激励视频金币双计进领券。 改为:comparison/coupon 奖励金币 = biz_type 桶(历史空、留作兜底)+ 按 ad_feed_reward_record.feed_scene 的 granted 实发金币(reward_date 北京自然日窗口); reward_video/ad_reward 拆成独立 REWARD_VIDEO_BIZ_TYPES,不再混入领券, REGULAR_TASK_EXCLUDED_BIZ_TYPES 保持不变。 测试:tests/test_admin_read.py 加 3 个用例(比价/领券按 feed_scene 汇总、 too_short 不计、领券排除激励视频);全量 pytest 除 5 个既有失败外全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/admin/repositories/stats.py | 26 ++++++++-- tests/test_admin_read.py | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/app/admin/repositories/stats.py b/app/admin/repositories/stats.py index 42e7998..260c159 100644 --- a/app/admin/repositories/stats.py +++ b/app/admin/repositories/stats.py @@ -30,11 +30,17 @@ from app.models.user import User from app.models.wallet import CoinTransaction, WithdrawOrder _BEIJING = timezone(timedelta(hours=8)) -COUPON_REWARD_BIZ_TYPES = ("reward_video", "ad_reward", "coupon", "coupon_reward") +REWARD_VIDEO_BIZ_TYPES = ("reward_video", "ad_reward") +# 领券/比价奖励金币的真实来源是信息流广告发奖(ad_feed_reward_record,按 feed_scene 分场景); +# coin_transaction 里只有扁平的 feed_ad_reward、biz_type 不分 coupon/comparison,故这俩桶历史从未 +# 被写入,仅留作未来兜底,实际金额在下方按 feed_scene 汇总 ad_feed_reward_record 得出。reward_video/ +# ad_reward 是激励视频,单独成桶、不再混进领券奖励(历史误并会把激励视频金币双计进领券)。 +COUPON_REWARD_BIZ_TYPES = ("coupon", "coupon_reward") COMPARISON_REWARD_BIZ_TYPES = ("comparison", "compare_reward", "comparison_reward") EXCLUDED_REWARD_BIZ_TYPES = ("invite_inviter", "invite_invitee", "admin_grant") UNCLASSIFIED_FEED_BIZ_TYPES = ("feed_ad_reward",) REGULAR_TASK_EXCLUDED_BIZ_TYPES = ( + *REWARD_VIDEO_BIZ_TYPES, *COUPON_REWARD_BIZ_TYPES, *COMPARISON_REWARD_BIZ_TYPES, *EXCLUDED_REWARD_BIZ_TYPES, @@ -362,7 +368,7 @@ def dashboard_overview( period_reward_video_coin_total = _sum( CoinTransaction.amount, *period_coin_conds, - CoinTransaction.biz_type.in_(("reward_video", "ad_reward")), + CoinTransaction.biz_type.in_(REWARD_VIDEO_BIZ_TYPES), ) period_feed_ad_coin_total = _sum( CoinTransaction.amount, @@ -384,15 +390,29 @@ def dashboard_overview( *period_coin_conds, CoinTransaction.biz_type.like("task_%"), ) + # 领券/比价奖励金币 = biz_type 桶(历史空,兜底)+ 该场景信息流广告实发金币 + # (ad_feed_reward_record.feed_scene,granted;reward_date 是北京日期串,与 period 同自然日窗口)。 period_coupon_reward_coin_total = _sum( CoinTransaction.amount, *period_coin_conds, CoinTransaction.biz_type.in_(COUPON_REWARD_BIZ_TYPES), + ) + _sum( + AdFeedRewardRecord.coin, + AdFeedRewardRecord.status == "granted", + AdFeedRewardRecord.feed_scene == "coupon", + AdFeedRewardRecord.reward_date >= period_from.isoformat(), + AdFeedRewardRecord.reward_date <= period_to.isoformat(), ) period_comparison_reward_coin_total = _sum( CoinTransaction.amount, *period_coin_conds, CoinTransaction.biz_type.in_(COMPARISON_REWARD_BIZ_TYPES), + ) + _sum( + AdFeedRewardRecord.coin, + AdFeedRewardRecord.status == "granted", + AdFeedRewardRecord.feed_scene == "comparison", + AdFeedRewardRecord.reward_date >= period_from.isoformat(), + AdFeedRewardRecord.reward_date <= period_to.isoformat(), ) period_regular_task_coin_total = _sum( CoinTransaction.amount, @@ -543,7 +563,7 @@ def dashboard_overview( "reward_video_coin_total": _sum( CoinTransaction.amount, CoinTransaction.amount > 0, - CoinTransaction.biz_type.in_(("reward_video", "ad_reward")), + CoinTransaction.biz_type.in_(REWARD_VIDEO_BIZ_TYPES), ), "reward_video_watch_count": _count( AdRewardRecord, diff --git a/tests/test_admin_read.py b/tests/test_admin_read.py index 1babd3c..5f5313f 100644 --- a/tests/test_admin_read.py +++ b/tests/test_admin_read.py @@ -320,3 +320,92 @@ def test_read_apis_require_auth(admin_client: TestClient) -> None: "/admin/api/feedbacks", ]: assert admin_client.get(path).status_code == 401, path + + +def test_period_comparison_reward_coin_from_feed_scene( + admin_client: TestClient, admin_token: str +) -> None: + """比价奖励金币口径:按 ad_feed_reward_record.feed_scene='comparison' 的实发金币 + (status=granted)汇总,而非查从不写入的 biz_type 桶(修复大盘该卡恒 0)。 + too_short(未发奖)不计。""" + from app.models.ad_feed_reward import AdFeedRewardRecord + + d = "2021-06-15" # 独立历史日,隔离其它用例数据 + db = SessionLocal() + try: + uid = user_repo.upsert_user_for_login(db, phone="13800008801", register_channel="sms").id + db.add(AdFeedRewardRecord( + client_event_id="cmp-fs-granted", user_id=uid, reward_date=d, + ecpm_raw="0", coin=123, feed_scene="comparison", status="granted", + )) + db.add(AdFeedRewardRecord( + client_event_id="cmp-fs-tooshort", user_id=uid, reward_date=d, + ecpm_raw="0", coin=99, feed_scene="comparison", status="too_short", + )) + db.commit() + finally: + db.close() + + r = admin_client.get( + "/admin/api/stats/overview", params={"date_from": d, "date_to": d}, + headers=_auth(admin_token), + ) + assert r.status_code == 200, r.text + assert r.json()["period"]["coins"]["comparison_reward_coin_total"] == 123 + + +def test_period_coupon_reward_coin_from_feed_scene( + admin_client: TestClient, admin_token: str +) -> None: + """领券奖励金币口径:按 ad_feed_reward_record.feed_scene='coupon' 的实发金币汇总。""" + from app.models.ad_feed_reward import AdFeedRewardRecord + + d = "2021-06-16" + db = SessionLocal() + try: + uid = user_repo.upsert_user_for_login(db, phone="13800008802", register_channel="sms").id + db.add(AdFeedRewardRecord( + client_event_id="cpn-fs-granted", user_id=uid, reward_date=d, + ecpm_raw="0", coin=456, feed_scene="coupon", status="granted", + )) + db.commit() + finally: + db.close() + + r = admin_client.get( + "/admin/api/stats/overview", params={"date_from": d, "date_to": d}, + headers=_auth(admin_token), + ) + assert r.status_code == 200, r.text + assert r.json()["period"]["coins"]["coupon_reward_coin_total"] == 456 + + +def test_period_coupon_reward_excludes_reward_video( + admin_client: TestClient, admin_token: str +) -> None: + """领券奖励金币不再把激励视频金币算进来(reward_video/ad_reward 从领券桶拆出); + 激励视频仍单独计入 reward_video_coin_total。""" + from datetime import datetime + + from app.models.wallet import CoinTransaction + + d = "2021-06-17" + db = SessionLocal() + try: + uid = user_repo.upsert_user_for_login(db, phone="13800008803", register_channel="sms").id + db.add(CoinTransaction( + user_id=uid, amount=50, balance_after=50, biz_type="reward_video", + ref_id="rv-split-1", created_at=datetime(2021, 6, 17, 12, 0, 0), + )) + db.commit() + finally: + db.close() + + r = admin_client.get( + "/admin/api/stats/overview", params={"date_from": d, "date_to": d}, + headers=_auth(admin_token), + ) + assert r.status_code == 200, r.text + coins = r.json()["period"]["coins"] + assert coins["coupon_reward_coin_total"] == 0 # 激励视频不计入领券奖励 + assert coins["reward_video_coin_total"] == 50 # 仍计入激励视频卡