From b39d918cda99e237e5bcefd484a2f0a1904c66cc Mon Sep 17 00:00:00 2001 From: zuochenyong Date: Wed, 5 Aug 2026 13:47:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8F=90=E7=8E=B0):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20100=20=E5=85=83=E6=A1=A3=E5=B9=B6=E5=90=91=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E4=B8=8B=E5=8F=91=E6=AF=8F=E6=A1=A3=E6=AF=8F=E6=97=A5?= =?UTF-8?q?=E6=AC=A1=E6=95=B0=E4=B8=8A=E9=99=90=20(#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: exinglang Reviewed-on: https://gitea.shaguabijia.com/WonderableAI/shaguabijia-app-server/pulls/218 Co-authored-by: zuochenyong Co-committed-by: zuochenyong --- app/api/v1/compare_record.py | 8 +++----- app/core/rewards.py | 3 ++- app/repositories/wallet.py | 10 ++++++---- app/schemas/welfare.py | 8 ++++++++ tests/test_compare_daily_limit.py | 2 +- tests/test_withdraw_tiers.py | 20 ++++++++++++-------- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/app/api/v1/compare_record.py b/app/api/v1/compare_record.py index 66ae136..a085d53 100644 --- a/app/api/v1/compare_record.py +++ b/app/api/v1/compare_record.py @@ -37,6 +37,8 @@ logger = logging.getLogger("shagua.compare_record") router = APIRouter(prefix="/api/v1/compare", tags=["compare-record"]) +COMPARE_DAILY_LIMIT_MESSAGE = "今日比价额度用完啦,明天再来吧~" + @router.post( "/start", @@ -78,11 +80,7 @@ def reserve_compare_start( except crud_compare.DailyCompareStartLimitExceeded: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, - detail=( - f"今日已比价超过{policy.limit}次,请明天再试" - if policy.limit is not None - else "今日比价次数已达上限,请明天再试" - ), + detail=COMPARE_DAILY_LIMIT_MESSAGE, ) from None except crud_compare.ComparisonTraceOwnershipError: raise HTTPException( diff --git a/app/core/rewards.py b/app/core/rewards.py index f37fbad..842f0ae 100644 --- a/app/core/rewards.py +++ b/app/core/rewards.py @@ -58,7 +58,7 @@ WITHDRAW_MAX_CENTS: int = 5_000_000 # 5 万元 # 规则(2026-07-09 拍板): # - 新人档(is_newbie):账号历史一次性,"发起就算用过"(任意状态含被拒),用过即不再下发; # 0.1 与 0.3 各自独立同天可各提一次,且不参与常规档"每日选一个额度"互斥。 -# - 常规档:按北京日计次(0.5×3 / 10×1 / 20×1),三档每天只能选一个。 +# - 常规档:按北京日计次(0.5×3 / 10×1 / 20×1 / 100×1),四档每天只能选一个。 # invite_cash(邀请页)本轮无档位概念,不在此表。改档位=改这里发版。 class WithdrawTier(NamedTuple): amount_cents: int @@ -74,6 +74,7 @@ WITHDRAW_TIERS_COIN_CASH: tuple[WithdrawTier, ...] = ( WithdrawTier(50, "0.5", None, 3, False), WithdrawTier(1000, "10", None, 1, False), WithdrawTier(2000, "20", None, 1, False), + WithdrawTier(10000, "100", None, 1, False), ) diff --git a/app/repositories/wallet.py b/app/repositories/wallet.py index f2420cf..fbaebbb 100644 --- a/app/repositories/wallet.py +++ b/app/repositories/wallet.py @@ -735,8 +735,8 @@ def withdraw_tier_states(db: Session, user_id: int, source: str = "coin_cash") - - 新人档(0.1/0.3):账号历史一次性——进行中(reviewing/pending)或成功打款(success)即视为 已用,直接**从返回列表消失**;被拒/转账失败/解绑退回(均已退款、钱没到手)则恢复可提,不永久 占用资格。两档各自独立互不影响,不参与"每日选一个额度"互斥。 - - 常规档(0.5×3 / 10×1 / 20×1):按北京日计次,"发起就算占用"(当天创建的单不论最终状态 - 都计入,被拒/失败不退当天名额);三档每天只能选一个,选定后其余两档当天 other_tier_selected。 + - 常规档(0.5×3 / 10×1 / 20×1 / 100×1):按北京日计次,"发起就算占用"(当天创建的单不论最终状态 + 都计入,被拒/失败不退当天名额);四档每天只能选一个,选定后其余三档当天 other_tier_selected。 - invite_cash 本轮无档位概念 → 返回空列表(邀请页客户端仍用本地写死档位,行为不变)。 余额是否足够由客户端本地判断(余额随兑换实时变化,不在此快照)。 """ @@ -785,7 +785,7 @@ def withdraw_tier_states(db: Session, user_id: int, source: str = "coin_cash") - out.append({ "amount_cents": t.amount_cents, "label": t.label, "badge": t.badge, "is_newbie": True, "available": True, "disabled_reason": None, - "remaining_today": 1, + "remaining_today": 1, "daily_limit": t.daily_limit, }) continue used = today_counts.get(t.amount_cents, 0) @@ -798,7 +798,9 @@ def withdraw_tier_states(db: Session, user_id: int, source: str = "coin_cash") - out.append({ "amount_cents": t.amount_cents, "label": t.label, "badge": t.badge, "is_newbie": False, "available": available, "disabled_reason": reason, - "remaining_today": remaining, + # daily_limit 一起下发:客户端要靠它区分「本档能提 3 次」和「本档只能提 1 次」—— + # 只看 remaining_today 分不出「0.5 已提两次剩 1」与「10 元一次没提剩 1」。 + "remaining_today": remaining, "daily_limit": t.daily_limit, }) return out diff --git a/app/schemas/welfare.py b/app/schemas/welfare.py index a9aa811..27bfd19 100644 --- a/app/schemas/welfare.py +++ b/app/schemas/welfare.py @@ -88,6 +88,14 @@ class WithdrawTierOut(BaseModel): description="不可提原因:quota_exhausted(今日次数满) / other_tier_selected(今日已选其他额度)", ) remaining_today: int = Field(0, description="今日剩余可提次数") + daily_limit: int = Field( + 1, + description=( + "该档每日可提次数上限(rewards.WITHDRAW_TIERS_COIN_CASH.daily_limit)。" + "客户端据此判「本档可提多次」——只有 daily_limit>1 且 remaining_today None: ) assert response.status_code == 429 - assert response.json()["detail"] == "今日已比价超过100次,请明天再试" + assert response.json()["detail"] == "今日比价额度用完啦,明天再来吧~" with SessionLocal() as db: assert db.scalar( select(func.count(ComparisonRecord.id)).where( diff --git a/tests/test_withdraw_tiers.py b/tests/test_withdraw_tiers.py index 73e8de9..3ba8e52 100644 --- a/tests/test_withdraw_tiers.py +++ b/tests/test_withdraw_tiers.py @@ -1,9 +1,9 @@ """福利页(coin_cash)提现档位规则测试(7-9提现ui对齐)。 规则(2026-07-09 拍板): - - 档位 0.1/0.3(新人,历史一次性,免广告)+ 0.5(日3次)/10/20(日1次)。 + - 档位 0.1/0.3(新人,历史一次性,免广告)+ 0.5(日3次)/10/20/100(日1次)。 - 计次口径"发起就算":当天创建的单不论最终状态(含被拒)都占名额;新人档任何状态都算用过。 - - 常规三档每天只能选一个;新人档不参与该互斥,两个新人档同天可各提一次。 + - 常规四档每天只能选一个;新人档不参与该互斥,两个新人档同天可各提一次。 - invite_cash 无档位概念:tiers 为空、下单不走档位闸(邀请页行为不变)。 wxpay 调用全部 monkeypatch;现金余额 DB 直灌(同 test_withdraw.py 套路)。 """ @@ -74,15 +74,18 @@ def _tiers(client, token: str, source: str = "coin_cash") -> list[dict]: def test_withdraw_info_tiers_full_and_invite_empty(client, monkeypatch) -> None: - """新用户 coin_cash 下发 5 档(新人角标齐);invite_cash tiers 为空。""" + """新用户 coin_cash 下发 6 档(新人角标齐);invite_cash tiers 为空。""" token = _login(client, "13800006001") tiers = _tiers(client, token) - assert [t["amount_cents"] for t in tiers] == [10, 30, 50, 1000, 2000] - assert [t["label"] for t in tiers] == ["0.1", "0.3", "0.5", "10", "20"] + assert [t["amount_cents"] for t in tiers] == [10, 30, 50, 1000, 2000, 10000] + assert [t["label"] for t in tiers] == ["0.1", "0.3", "0.5", "10", "20", "100"] assert tiers[0]["badge"] == "新人福利" and tiers[0]["is_newbie"] is True assert tiers[1]["badge"] == "新人福利" and tiers[1]["is_newbie"] is True assert all(t["available"] for t in tiers) assert tiers[2]["remaining_today"] == 3 # 0.5 日 3 次 + # daily_limit 一起下发:客户端只有在 daily_limit>1 且 remaining_today None: amounts = [t["amount_cents"] for t in tiers] assert 10 not in amounts # 0.1 消失 assert 30 in amounts # 0.3 还在,同天仍可提 - # 新人档不参与"选一个额度":常规三档全部仍可提 + # 新人档不参与"选一个额度":常规四档全部仍可提 regular = {t["amount_cents"]: t for t in tiers if not t["is_newbie"]} - assert all(regular[a]["available"] for a in (50, 1000, 2000)) + assert all(regular[a]["available"] for a in (50, 1000, 2000, 10000)) r = _withdraw(client, token, 30) # 同天 0.3 照提 assert r.status_code == 200, r.text @@ -115,7 +118,7 @@ def test_newbie_tiers_independent_and_once_forever(client, monkeypatch) -> None: def test_regular_daily_select_one_tier(client, monkeypatch) -> None: - """当天提过 0.5 → 10/20 置灰 other_tier_selected,下单 409;0.5 还能继续提(3 次内)。""" + """当天提过 0.5 → 10/20/100 置灰 other_tier_selected,下单 409;0.5 还能继续提(3 次内)。""" _patch_userinfo(monkeypatch, "openid_tier_3") token = _login(client, "13800006003") _seed_balances(client, token, "13800006003", cash=10_000) @@ -129,6 +132,7 @@ def test_regular_daily_select_one_tier(client, monkeypatch) -> None: assert tiers[50]["available"] and tiers[50]["remaining_today"] == 2 assert not tiers[1000]["available"] and tiers[1000]["disabled_reason"] == "other_tier_selected" assert not tiers[2000]["available"] and tiers[2000]["disabled_reason"] == "other_tier_selected" + assert not tiers[10000]["available"] and tiers[10000]["disabled_reason"] == "other_tier_selected" r = _withdraw(client, token, 1000) # 选一额度互斥 → 409 assert r.status_code == 409, r.text