feat(提现): 新增 100 元档并向客户端下发每档每日次数上限 (#218)

Co-authored-by: exinglang <exinglang@qq.com>
Reviewed-on: #218
Co-authored-by: zuochenyong <zuochenyong@wonderable.ai>
Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
This commit was merged in pull request #218.
This commit is contained in:
2026-08-05 13:47:19 +08:00
committed by guke
parent 03129e059f
commit b39d918cda
6 changed files with 32 additions and 19 deletions
+3 -5
View File
@@ -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(
+2 -1
View File
@@ -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),
)
+6 -4
View File
@@ -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
+8
View File
@@ -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<daily_limit"
"(= 今天已提过至少一次)才展示「今日还可提N次」角标。"
),
)
class WithdrawInfoOut(BaseModel):
+1 -1
View File
@@ -140,7 +140,7 @@ def test_compare_start_rejects_101st_beijing_day_attempt(client) -> 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(
+12 -8
View File
@@ -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<daily_limit
# (= 今天已提过)时才画「今日还可提N次」角标,光看 remaining_today 分不出单次档没提的情况。
assert [t["daily_limit"] for t in tiers] == [1, 1, 3, 1, 1, 1]
assert _tiers(client, token, source="invite_cash") == []
@@ -101,9 +104,9 @@ def test_newbie_tiers_independent_and_once_forever(client, monkeypatch) -> 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