Compare commits

..

9 Commits

Author SHA1 Message Date
guke a97faeb179 Merge branch 'main' into feat/admin-comparison-outcome-display 2026-08-04 18:45:31 +08:00
guke f7f5a36250 docs: 补 admin 记录页技术完成率口径说明
admin 比价页/概览/大盘把外部缺失(店/菜/打烊/不配送/不支持/未满起送)记为成功、
仅纯技术故障算失败,刻意宽于 C 端;故 admin 成功率≠C端口径,非 bug。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 16:50:44 +08:00
guke 8498144703 feat(admin): 比价记录「状态」筛选对齐 admin 口径 + 清理 #209 死常量
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 16:39:16 +08:00
guke b703bec7e4 feat(admin): 数据大盘比价成功率改按 admin 口径(对齐概览)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 16:33:26 +08:00
guke abd6329035 feat(admin): 概览成功率/耗时统计改按 admin 口径(外部缺失记为成功)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 16:21:24 +08:00
guke 5a66c302cb fix(admin): 修 queries import 排序 + 补比价详情 admin 字段断言
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 16:01:24 +08:00
guke 09b9381d03 feat(admin): 比价记录列表/详情下发 admin_status + outcome_hint
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 15:48:55 +08:00
guke 7419f35f4b fix(admin): 口径模块 SQL 侧 nullif 对齐空串,消除 Python/SQL 分歧
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 15:44:58 +08:00
guke 9de73152ec feat(admin): 比价记录展示口径共享模块(外部缺失判为成功)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 15:35:32 +08:00
6 changed files with 19 additions and 32 deletions
+5 -3
View File
@@ -37,8 +37,6 @@ logger = logging.getLogger("shagua.compare_record")
router = APIRouter(prefix="/api/v1/compare", tags=["compare-record"])
COMPARE_DAILY_LIMIT_MESSAGE = "今日比价额度用完啦,明天再来吧~"
@router.post(
"/start",
@@ -80,7 +78,11 @@ def reserve_compare_start(
except crud_compare.DailyCompareStartLimitExceeded:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=COMPARE_DAILY_LIMIT_MESSAGE,
detail=(
f"今日已比价超过{policy.limit}次,请明天再试"
if policy.limit is not None
else "今日比价次数已达上限,请明天再试"
),
) from None
except crud_compare.ComparisonTraceOwnershipError:
raise HTTPException(
+1 -2
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 / 100×1),档每天只能选一个。
# - 常规档:按北京日计次(0.5×3 / 10×1 / 20×1),档每天只能选一个。
# invite_cash(邀请页)本轮无档位概念,不在此表。改档位=改这里发版。
class WithdrawTier(NamedTuple):
amount_cents: int
@@ -74,7 +74,6 @@ 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),
)
+4 -6
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 / 100×1):按北京日计次,"发起就算占用"(当天创建的单不论最终状态
都计入,被拒/失败不退当天名额);档每天只能选一个,选定后其余档当天 other_tier_selected。
- 常规档(0.5×3 / 10×1 / 20×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, "daily_limit": t.daily_limit,
"remaining_today": 1,
})
continue
used = today_counts.get(t.amount_cents, 0)
@@ -798,9 +798,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": False, "available": available, "disabled_reason": reason,
# daily_limit 一起下发:客户端要靠它区分「本档能提 3 次」和「本档只能提 1 次」——
# 只看 remaining_today 分不出「0.5 已提两次剩 1」与「10 元一次没提剩 1」。
"remaining_today": remaining, "daily_limit": t.daily_limit,
"remaining_today": remaining,
})
return out
-8
View File
@@ -88,14 +88,6 @@ 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"] == "今日比价额度用完啦,明天再来吧~"
assert response.json()["detail"] == "今日比价超过100次,请明天再试"
with SessionLocal() as db:
assert db.scalar(
select(func.count(ComparisonRecord.id)).where(
+8 -12
View File
@@ -1,9 +1,9 @@
"""福利页(coin_cash)提现档位规则测试(7-9提现ui对齐)。
规则(2026-07-09 拍板):
- 档位 0.1/0.3(新人,历史一次性,免广告)+ 0.5(日3次)/10/20/100(日1次)。
- 档位 0.1/0.3(新人,历史一次性,免广告)+ 0.5(日3次)/10/20(日1次)。
- 计次口径"发起就算":当天创建的单不论最终状态(含被拒)都占名额;新人档任何状态都算用过。
- 常规档每天只能选一个;新人档不参与该互斥,两个新人档同天可各提一次。
- 常规档每天只能选一个;新人档不参与该互斥,两个新人档同天可各提一次。
- invite_cash 无档位概念:tiers 为空、下单不走档位闸(邀请页行为不变)。
wxpay 调用全部 monkeypatch;现金余额 DB 直灌(同 test_withdraw.py 套路)。
"""
@@ -74,18 +74,15 @@ 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 下发 6 档(新人角标齐);invite_cash tiers 为空。"""
"""新用户 coin_cash 下发 5 档(新人角标齐);invite_cash tiers 为空。"""
token = _login(client, "13800006001")
tiers = _tiers(client, token)
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 [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 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") == []
@@ -104,9 +101,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, 10000))
assert all(regular[a]["available"] for a in (50, 1000, 2000))
r = _withdraw(client, token, 30) # 同天 0.3 照提
assert r.status_code == 200, r.text
@@ -118,7 +115,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/100 置灰 other_tier_selected,下单 409;0.5 还能继续提(3 次内)。"""
"""当天提过 0.5 → 10/20 置灰 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)
@@ -132,7 +129,6 @@ 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