Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 084c6e1894 | |||
| 8e01ae0bf9 | |||
| 7e17df4130 | |||
| c1cc6bb43f | |||
| 91f9981885 |
@@ -948,7 +948,7 @@ def get_withdraw_by_out_bill_no(db: Session, out_bill_no: str) -> WithdrawOrder
|
||||
|
||||
|
||||
def withdraw_summary(db: Session, *, source: str | None = None) -> dict:
|
||||
"""提现审核台顶部统计。金额单位:分。"""
|
||||
"""提现审核台顶部统计。金额单位:分;口径与列表的 source 筛选一致。"""
|
||||
summary_stmt = select(
|
||||
WithdrawOrder.status,
|
||||
func.count(WithdrawOrder.id),
|
||||
@@ -977,21 +977,29 @@ def withdraw_summary(db: Session, *, source: str | None = None) -> dict:
|
||||
stmt = stmt.where(WithdrawOrder.source == source)
|
||||
return db.execute(stmt).scalar_one()
|
||||
|
||||
today_amount_stmt = select(
|
||||
today_success_amount_stmt = select(
|
||||
func.coalesce(func.sum(WithdrawOrder.amount_cents), 0)
|
||||
).where(
|
||||
WithdrawOrder.status == "success",
|
||||
WithdrawOrder.updated_at >= today_start,
|
||||
)
|
||||
if source:
|
||||
today_amount_stmt = today_amount_stmt.where(WithdrawOrder.source == source)
|
||||
today_success_amount = db.execute(today_amount_stmt).scalar_one()
|
||||
today_success_amount_stmt = today_success_amount_stmt.where(
|
||||
WithdrawOrder.source == source
|
||||
)
|
||||
today_success_amount = db.execute(today_success_amount_stmt).scalar_one()
|
||||
|
||||
def _status_count(status: str) -> int:
|
||||
return by_status.get(status, {}).get("count", 0)
|
||||
|
||||
return {
|
||||
"reviewing_count": by_status.get("reviewing", {}).get("count", 0),
|
||||
"reviewing_count": _status_count("reviewing"),
|
||||
"reviewing_amount_cents": by_status.get("reviewing", {}).get("amount_cents", 0),
|
||||
"pending_count": by_status.get("pending", {}).get("count", 0),
|
||||
"failed_count": by_status.get("failed", {}).get("count", 0),
|
||||
"pending_count": _status_count("pending"),
|
||||
"success_count": _status_count("success"),
|
||||
"rejected_count": _status_count("rejected"),
|
||||
"failed_count": _status_count("failed"),
|
||||
"total_count": sum(item["count"] for item in by_status.values()),
|
||||
"today_success_count": _today_count("success"),
|
||||
"today_success_amount_cents": int(today_success_amount),
|
||||
"today_rejected_count": _today_count("rejected"),
|
||||
|
||||
@@ -69,7 +69,10 @@ class WithdrawSummaryOut(BaseModel):
|
||||
reviewing_count: int
|
||||
reviewing_amount_cents: int
|
||||
pending_count: int
|
||||
success_count: int
|
||||
rejected_count: int
|
||||
failed_count: int
|
||||
total_count: int
|
||||
today_success_count: int
|
||||
today_success_amount_cents: int
|
||||
today_rejected_count: int
|
||||
|
||||
@@ -85,6 +85,10 @@ class ComparisonResultIn(BaseModel):
|
||||
status: str | None = None
|
||||
# 门店打烊原因(price 为 None 时带): 与 status="store_closed" 等价的更早信号, 一并落库供前端兜底判打烊。
|
||||
store_closed: str | None = None
|
||||
# 该平台缺菜(漏菜)数量(pricebot 冗余进 comparison_results 行): 有价但少买了菜时 >0。
|
||||
# 记录页三平台网格据此逐格标"缺少 X 个菜品"(网格只拿 comparison_results, 拿不到 platform_results)。
|
||||
# 必须显式声明: 落库走 model_dump(), 不声明会被 pydantic 静默丢弃 → 记录页拿不到数量。
|
||||
skipped_dish_count: int | None = None
|
||||
|
||||
|
||||
class ComparisonRecordIn(BaseModel):
|
||||
|
||||
@@ -504,6 +504,95 @@ def test_invite_withdraw_detail_contains_invitee_first_actions(
|
||||
assert filtered.json()["invite_overview"]["invite_total"] == 0
|
||||
|
||||
|
||||
def test_withdraw_summary_counts_match_status_lists_by_source(
|
||||
admin_client: TestClient, admin_token: str
|
||||
) -> None:
|
||||
"""每个审核页签的汇总数必须等于相同 source、status 的列表总数。"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
uid = user_repo.upsert_user_for_login(
|
||||
db, phone="13800009003", register_channel="sms"
|
||||
).id
|
||||
for index, status in enumerate(
|
||||
("reviewing", "pending", "success", "rejected", "failed"), start=1
|
||||
):
|
||||
db.add(
|
||||
WithdrawOrder(
|
||||
user_id=uid,
|
||||
out_bill_no=f"summary{uid}invite{index}",
|
||||
amount_cents=index * 10,
|
||||
status=status,
|
||||
source="invite_cash",
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
WithdrawOrder(
|
||||
user_id=uid,
|
||||
out_bill_no=f"summary{uid}coinfailed",
|
||||
amount_cents=99,
|
||||
status="failed",
|
||||
source="coin_cash",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
status_fields = {
|
||||
"reviewing": "reviewing_count",
|
||||
"pending": "pending_count",
|
||||
"success": "success_count",
|
||||
"rejected": "rejected_count",
|
||||
"failed": "failed_count",
|
||||
}
|
||||
for source in ("coin_cash", "invite_cash"):
|
||||
summary_response = admin_client.get(
|
||||
"/admin/api/withdraws/summary",
|
||||
params={"source": source},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert summary_response.status_code == 200, summary_response.text
|
||||
summary = summary_response.json()
|
||||
|
||||
for status, field in status_fields.items():
|
||||
list_response = admin_client.get(
|
||||
"/admin/api/withdraws",
|
||||
params={"source": source, "status": status, "limit": 1},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert list_response.status_code == 200, list_response.text
|
||||
assert summary[field] == list_response.json()["total"]
|
||||
|
||||
reviewing_response = admin_client.get(
|
||||
"/admin/api/withdraws",
|
||||
params={"source": source, "status": "reviewing", "limit": 100},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert reviewing_response.status_code == 200, reviewing_response.text
|
||||
reviewing_body = reviewing_response.json()
|
||||
assert reviewing_body["total"] == len(reviewing_body["items"])
|
||||
assert summary["reviewing_amount_cents"] == sum(
|
||||
item["amount_cents"] for item in reviewing_body["items"]
|
||||
)
|
||||
|
||||
all_response = admin_client.get(
|
||||
"/admin/api/withdraws",
|
||||
params={"source": source, "limit": 1},
|
||||
headers=_auth(admin_token),
|
||||
)
|
||||
assert all_response.status_code == 200, all_response.text
|
||||
assert summary["total_count"] == all_response.json()["total"]
|
||||
assert summary["total_count"] == sum(
|
||||
summary[field] for field in status_fields.values()
|
||||
)
|
||||
|
||||
assert admin_client.get(
|
||||
"/admin/api/withdraws/summary",
|
||||
params={"source": "unknown"},
|
||||
headers=_auth(admin_token),
|
||||
).status_code == 422
|
||||
|
||||
|
||||
def test_ad_coin_audit_full_count_truncate_and_only_mismatch(
|
||||
admin_client: TestClient, admin_token: str
|
||||
) -> None:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""ComparisonResultIn 保留逐平台 skipped_dish_count(落库/回读用)。
|
||||
|
||||
记录页三平台网格要按逐平台缺菜数标"缺少 X 个菜品"; pricebot 现把 skipped_dish_count 冗余进
|
||||
comparison_results 行。上报边界必须显式声明该字段, 否则 model_dump() 会静默丢弃(POST 路径),
|
||||
记录页拿不到。纯 schema, 不碰 DB。
|
||||
"""
|
||||
from app.schemas.compare_record import ComparisonResultIn
|
||||
|
||||
|
||||
def test_comparison_result_in_preserves_skipped_dish_count():
|
||||
r = ComparisonResultIn(platform_id="jd_waimai", price=25.0, skipped_dish_count=2)
|
||||
assert r.model_dump().get("skipped_dish_count") == 2
|
||||
|
||||
|
||||
def test_comparison_result_in_skipped_defaults_none():
|
||||
r = ComparisonResultIn(platform_id="meituan", price=42.0, is_source=True)
|
||||
assert r.model_dump().get("skipped_dish_count") is None
|
||||
Reference in New Issue
Block a user