Compare commits

..

2 Commits

Author SHA1 Message Date
linkeyu 2eb36b44c8 fix(admin): 按任务白名单聚合常规任务金币 (#157)
## 背景

大盘“常规任务金币”原先采用“全部正向金币减排除清单”的反向口径。线上新增 `feed_ad_reward_coupon` / `feed_ad_reward_comparison` 后未同步加入排除清单,导致领券和比价奖励误计入常规任务金币。

## 修改

- 改为明确白名单:`signin`、历史 `signin_boost`、全部 `task_` 任务、`price_report_reward`、`feedback_reward`
- 未知新 `biz_type` 默认不进入常规任务桶
- 增加覆盖领券、比价、广告、邀请、管理员及未知类型的回归测试
- 顺带修复改动文件已有的 Ruff `UP017`

## 验证

- 线上只读 PostgreSQL:新口径全量为 108,022,领券/比价误计差额为 450,675
- Ruff:通过
- `pytest tests/test_admin_read.py tests/test_cps_admin.py -q`:23 passed

---------

Co-authored-by: unknown <798648091@qq.com>
Reviewed-on: #157
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-22 12:09:06 +08:00
linkeyu 510df176b3 feat(admin): 返回逐场领券点位分数与明细 (#153)
## 变更内容
- 按 trace_id 批量统计每场领券成功数/尝试数
- success、already_claimed 计成功,failed 计尝试,skipped 排除
- 返回每个点位的名称、ID、状态和失败原因
- 无有效逐券埋点时返回空值,不伪造 0/0
- 用户领券记录抽屉同步返回点位分数及明细

## 性能
- 当前页全部 trace_id 一次批量查询,不产生逐行请求

## 验证
- 16 项后端测试通过
- 覆盖成功、已领、失败、跳过及失败原因

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: unknown <798648091@qq.com>
Reviewed-on: #153
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-22 11:46:00 +08:00
2 changed files with 75 additions and 14 deletions
+16 -14
View File
@@ -6,10 +6,10 @@ user.last_login_at / comparison_record.status / withdraw_order.status)要加索
from __future__ import annotations from __future__ import annotations
from collections import Counter from collections import Counter
from datetime import date, datetime, time, timedelta, timezone from datetime import UTC, date, datetime, time, timedelta, timezone
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from sqlalchemy import case, func, select from sqlalchemy import case, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.admin.repositories.coupon_data import _percentile from app.admin.repositories.coupon_data import _percentile
@@ -37,14 +37,13 @@ REWARD_VIDEO_BIZ_TYPES = ("reward_video", "ad_reward")
# ad_reward 是激励视频,单独成桶、不再混进领券奖励(历史误并会把激励视频金币双计进领券)。 # ad_reward 是激励视频,单独成桶、不再混进领券奖励(历史误并会把激励视频金币双计进领券)。
COUPON_REWARD_BIZ_TYPES = ("coupon", "coupon_reward") COUPON_REWARD_BIZ_TYPES = ("coupon", "coupon_reward")
COMPARISON_REWARD_BIZ_TYPES = ("comparison", "compare_reward", "comparison_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",) # biz_type 时会在排除清单更新前自动混入该桶。task_ 前缀在查询处单独覆盖现有及未来任务。
REGULAR_TASK_EXCLUDED_BIZ_TYPES = ( REGULAR_TASK_EXACT_BIZ_TYPES = (
*REWARD_VIDEO_BIZ_TYPES, "signin",
*COUPON_REWARD_BIZ_TYPES, "signin_boost",
*COMPARISON_REWARD_BIZ_TYPES, "price_report_reward",
*EXCLUDED_REWARD_BIZ_TYPES, "feedback_reward",
*UNCLASSIFIED_FEED_BIZ_TYPES,
) )
MEITUAN_CPS_INVALID_STATUSES = ("4", "5") MEITUAN_CPS_INVALID_STATUSES = ("4", "5")
MEITUAN_CPS_SETTLED_STATUS = "6" MEITUAN_CPS_SETTLED_STATUS = "6"
@@ -61,7 +60,7 @@ def _beijing_today_start_utc() -> datetime:
"""北京时间今天 0 点对应的 UTC 时刻(DAU / 今日新增按北京时区切天)。""" """北京时间今天 0 点对应的 UTC 时刻(DAU / 今日新增按北京时区切天)。"""
now_bj = datetime.now(_BEIJING) now_bj = datetime.now(_BEIJING)
start_bj = now_bj.replace(hour=0, minute=0, second=0, microsecond=0) start_bj = now_bj.replace(hour=0, minute=0, second=0, microsecond=0)
return start_bj.astimezone(timezone.utc) return start_bj.astimezone(UTC)
def today_dau(db: Session) -> int: def today_dau(db: Session) -> int:
@@ -94,8 +93,8 @@ def _period_bounds(date_from: date, date_to: date) -> tuple[datetime, datetime,
""" """
start_bj = datetime.combine(date_from, time.min, tzinfo=_BEIJING) start_bj = datetime.combine(date_from, time.min, tzinfo=_BEIJING)
end_bj = datetime.combine(date_to + timedelta(days=1), time.min, tzinfo=_BEIJING) end_bj = datetime.combine(date_to + timedelta(days=1), time.min, tzinfo=_BEIJING)
start_utc = start_bj.astimezone(timezone.utc) start_utc = start_bj.astimezone(UTC)
end_utc = end_bj.astimezone(timezone.utc) end_utc = end_bj.astimezone(UTC)
return ( return (
start_utc, start_utc,
end_utc, end_utc,
@@ -505,7 +504,10 @@ def dashboard_overview(
period_regular_task_coin_total = _sum( period_regular_task_coin_total = _sum(
CoinTransaction.amount, CoinTransaction.amount,
*period_coin_conds, *period_coin_conds,
CoinTransaction.biz_type.notin_(REGULAR_TASK_EXCLUDED_BIZ_TYPES), or_(
CoinTransaction.biz_type.in_(REGULAR_TASK_EXACT_BIZ_TYPES),
CoinTransaction.biz_type.like(r"task\_%", escape="\\"),
),
) )
period_cps_orders = list( period_cps_orders = list(
db.execute( db.execute(
+59
View File
@@ -455,3 +455,62 @@ def test_period_coupon_reward_excludes_reward_video(
coins = r.json()["period"]["coins"] coins = r.json()["period"]["coins"]
assert coins["coupon_reward_coin_total"] == 0 # 激励视频不计入领券奖励 assert coins["coupon_reward_coin_total"] == 0 # 激励视频不计入领券奖励
assert coins["reward_video_coin_total"] == 50 # 仍计入激励视频卡 assert coins["reward_video_coin_total"] == 50 # 仍计入激励视频卡
def test_period_regular_task_coin_uses_explicit_allowlist(
admin_client: TestClient, admin_token: str
) -> None:
"""常规任务金币只加明确任务来源,领券/比价及未知新类型不得自动混入。"""
from datetime import datetime
from app.models.wallet import CoinTransaction
d = "2021-06-18"
included = {
"signin": 100,
"signin_boost": 200,
"task_enable_notification": 300,
"task_other": 400,
"price_report_reward": 500,
"feedback_reward": 600,
}
excluded = {
"feed_ad_reward_coupon": 700,
"feed_ad_reward_comparison": 800,
"feed_ad_reward": 900,
"reward_video": 1000,
"admin_grant": 1100,
"invite_inviter": 1200,
"future_unknown_reward": 1300,
}
db = SessionLocal()
try:
uid = user_repo.upsert_user_for_login(
db, phone="13800008804", register_channel="sms"
).id
balance = 0
rows = []
for index, (biz_type, amount) in enumerate((included | excluded).items(), start=1):
balance += amount
rows.append(CoinTransaction(
user_id=uid,
amount=amount,
balance_after=balance,
biz_type=biz_type,
ref_id=f"regular-task-{index}",
created_at=datetime(2021, 6, 18, 12, 0, index),
))
db.add_all(rows)
db.commit()
finally:
db.close()
response = admin_client.get(
"/admin/api/stats/overview",
params={"date_from": d, "date_to": d},
headers=_auth(admin_token),
)
assert response.status_code == 200, response.text
coins = response.json()["period"]["coins"]
assert coins["regular_task_coin_total"] == sum(included.values())
assert coins["task_coin_total"] == 700