Compare commits

..

5 Commits

Author SHA1 Message Date
guke 5401d2cc94 Merge branch 'main' into fix/moneypagefix 2026-07-22 10:53:07 +08:00
左辰勇 70aa8dcde2 fix(alembic): 合并 88f2380 引入的双 head,恢复 upgrade head 可用
88f2380 把 origin/main 合进本分支后,两条迁移线在 git 上汇合了、在 alembic 图上
却没有:本分支的 ad_reward_boost_round_id → drop_signin_boost_record 与 main 的
merge_active_phone 同从 comparison_llm_cost 分叉,此后再无迁移 revise
drop_signin_boost_record,它成了悬空的第二个 head。

后果:`alembic upgrade head`(单数)报 "Multiple head revisions are present"。
按 CLAUDE.md,run.sh 启动即自动迁移 —— app server 直接起不来,CI/线上同样报错。

用标准 merge revision 收敛(而非改 down_revision 指向),保留两侧作者文件不动。
纯 merge,无表结构/数据改动。

验证:alembic heads 恰好一个 merge_signin_boost_main;空库 alembic upgrade head
全链路跑通,current 停在 (head) (mergepoint);ruff 通过;pytest 435 passed
(6 个既有失败,与本次改动前一致)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 20:27:14 +08:00
左辰勇 88f2380f37 Merge remote-tracking branch 'origin/main' into fix/moneypagefix 2026-07-21 14:56:09 +08:00
左辰勇 3250a465c7 '赚钱中心的接口修改' 2026-07-20 21:15:42 +08:00
左辰勇 de1fd58749 feat(ad): 膨胀弹窗改用服务端权威金额 + 本轮累计口径,下线 signin_boost
要守住的不变量:弹窗数字 == 本轮实际到账之和 == 用户看到的余额涨幅。三者对不上,
用户就会认为少发了钱(走查现象:弹窗 240、余额只涨 40)。

- reward-result:按 ad_session_id 查本次实发金币,替代余额差 / coin_per_ad 估算。
  S2S 异步未到账返 200+pending 而非 404(404 只表示路由不存在,混在一起客户端没法
  区分「后端没部署」和「再等等」);同 session 多条时显式优先 granted——客户端先报
  closed_early、S2S 后到时,granted 反而是后写的。

- boost_round_id:客户端经 mediaExtra 透传「这条广告属于哪一轮膨胀」,穿山甲 S2S 原样
  带回后随发奖记录落库。**纯标签,不参与发奖判定**。reward-result 新增 round_coin,按
  (user_id, boost_round_id) 对 granted 记录求和。之所以由服务端求和而非客户端自己累加
  ——客户端进程被杀/重建后本地累计会丢,发奖记录不会。
  · 求和恒带 user_id:轮 id 是客户端生成的,不带就等于让任何人拿别人的轮 id 查别人发了多少。
  · 本条非 granted(capped 等)时仍返本轮累计、该条按 0 计,让限额 toast 有数可显。
  · test-grant 加可选 boost_round_id:它不经 S2S 拿不到 extra,不补则 debug 包验不了累计。
  · 客户端复用同一轮 id 只会把展示数字滚大,求和的是已发生的记录,不产生新入账,无资损。

- 下线 signin_boost(签到膨胀):它按固定 3000 金币发、与广告实际收益脱钩,产品确认从来
  不是设计内的口径——奖励只有「签到」和「看视频」两种。签到弹窗的「看广告膨胀」改与福利页
  看视频同走 reward_video(按 eCPM 公式)。摘除回调分支、POST /signin/boost、
  SigninBoostRecord、signin_boost_coin 配置,并 drop signin_boost_record 表。
  **coin_transaction.biz_type='signin_boost' 的历史流水保留不动**——钱是真发过的,账必须
  留得住;admin 大盘那两项改从金币流水统计(一次膨胀 = 一笔,与原口径等价),继续能查回历史。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:55:25 +08:00
2 changed files with 14 additions and 75 deletions
+14 -16
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 UTC, date, datetime, time, timedelta, timezone from datetime import 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, or_, select from sqlalchemy import case, func, 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,13 +37,14 @@ 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")
# biz_type 时会在排除清单更新前自动混入该桶。task_ 前缀在查询处单独覆盖现有及未来任务。 UNCLASSIFIED_FEED_BIZ_TYPES = ("feed_ad_reward",)
REGULAR_TASK_EXACT_BIZ_TYPES = ( REGULAR_TASK_EXCLUDED_BIZ_TYPES = (
"signin", *REWARD_VIDEO_BIZ_TYPES,
"signin_boost", *COUPON_REWARD_BIZ_TYPES,
"price_report_reward", *COMPARISON_REWARD_BIZ_TYPES,
"feedback_reward", *EXCLUDED_REWARD_BIZ_TYPES,
*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"
@@ -60,7 +61,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(UTC) return start_bj.astimezone(timezone.utc)
def today_dau(db: Session) -> int: def today_dau(db: Session) -> int:
@@ -93,8 +94,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(UTC) start_utc = start_bj.astimezone(timezone.utc)
end_utc = end_bj.astimezone(UTC) end_utc = end_bj.astimezone(timezone.utc)
return ( return (
start_utc, start_utc,
end_utc, end_utc,
@@ -504,10 +505,7 @@ 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,
or_( CoinTransaction.biz_type.notin_(REGULAR_TASK_EXCLUDED_BIZ_TYPES),
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,62 +455,3 @@ 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