diff --git a/app/admin/repositories/queries.py b/app/admin/repositories/queries.py index d9d6a2b..efa5f0b 100644 --- a/app/admin/repositories/queries.py +++ b/app/admin/repositories/queries.py @@ -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"), diff --git a/app/admin/schemas/wallet.py b/app/admin/schemas/wallet.py index db12793..a3fe899 100644 --- a/app/admin/schemas/wallet.py +++ b/app/admin/schemas/wallet.py @@ -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 diff --git a/tests/test_admin_read.py b/tests/test_admin_read.py index 4e2ec71..fbcf291 100644 --- a/tests/test_admin_read.py +++ b/tests/test_admin_read.py @@ -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: