From 91f99818854e7da48615d81a65e365c0dc68c430 Mon Sep 17 00:00:00 2001 From: unknown <798648091@qq.com> Date: Mon, 27 Jul 2026 15:44:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=8C=89?= =?UTF-8?q?=E6=8F=90=E7=8E=B0=E6=9D=A5=E6=BA=90=E6=B1=87=E6=80=BB=E5=90=84?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/repositories/queries.py | 59 ++++++++++++++--------- app/admin/routers/withdraw.py | 9 +++- app/admin/schemas/wallet.py | 3 ++ tests/test_admin_read.py | 77 +++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 25 deletions(-) diff --git a/app/admin/repositories/queries.py b/app/admin/repositories/queries.py index 9b28695..4383787 100644 --- a/app/admin/repositories/queries.py +++ b/app/admin/repositories/queries.py @@ -946,15 +946,16 @@ def get_withdraw_by_out_bill_no(db: Session, out_bill_no: str) -> WithdrawOrder ).scalar_one_or_none() -def withdraw_summary(db: Session) -> dict: - """提现审核台顶部统计。金额单位:分。""" - rows = db.execute( - select( - WithdrawOrder.status, - func.count(WithdrawOrder.id), - func.coalesce(func.sum(WithdrawOrder.amount_cents), 0), - ).group_by(WithdrawOrder.status) - ).all() +def withdraw_summary(db: Session, *, source: str | None = None) -> dict: + """提现审核台顶部统计。金额单位:分;口径与列表的 source 筛选一致。""" + summary_stmt = select( + WithdrawOrder.status, + func.count(WithdrawOrder.id), + func.coalesce(func.sum(WithdrawOrder.amount_cents), 0), + ) + if source: + summary_stmt = summary_stmt.where(WithdrawOrder.source == source) + rows = db.execute(summary_stmt.group_by(WithdrawOrder.status)).all() by_status = { status: {"count": int(count), "amount_cents": int(amount_cents)} for status, count, amount_cents in rows @@ -967,25 +968,37 @@ def withdraw_summary(db: Session) -> dict: ) def _today_count(status: str) -> int: - return db.execute( - select(func.count(WithdrawOrder.id)).where( - WithdrawOrder.status == status, - WithdrawOrder.updated_at >= today_start, - ) - ).scalar_one() - - today_success_amount = db.execute( - select(func.coalesce(func.sum(WithdrawOrder.amount_cents), 0)).where( - WithdrawOrder.status == "success", + stmt = select(func.count(WithdrawOrder.id)).where( + WithdrawOrder.status == status, WithdrawOrder.updated_at >= today_start, ) - ).scalar_one() + if source: + stmt = stmt.where(WithdrawOrder.source == source) + return db.execute(stmt).scalar_one() + + 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_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/routers/withdraw.py b/app/admin/routers/withdraw.py index fa5035f..b82c47b 100644 --- a/app/admin/routers/withdraw.py +++ b/app/admin/routers/withdraw.py @@ -95,8 +95,13 @@ def list_withdraws( @router.get("/summary", response_model=WithdrawSummaryOut, summary="提现审核台统计") -def withdraws_summary(db: AdminDb) -> WithdrawSummaryOut: - return WithdrawSummaryOut(**queries.withdraw_summary(db)) +def withdraws_summary( + db: AdminDb, + source: Annotated[ + str | None, Query(pattern="^(coin_cash|invite_cash)$") + ] = None, +) -> WithdrawSummaryOut: + return WithdrawSummaryOut(**queries.withdraw_summary(db, source=source)) @router.get( diff --git a/app/admin/schemas/wallet.py b/app/admin/schemas/wallet.py index 7f61d7b..68b66cd 100644 --- a/app/admin/schemas/wallet.py +++ b/app/admin/schemas/wallet.py @@ -67,7 +67,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 dfe720d..6ba2d09 100644 --- a/tests/test_admin_read.py +++ b/tests/test_admin_read.py @@ -341,6 +341,83 @@ def test_withdraw_list_exposes_source_and_filters( assert items and all(it["source"] == "invite_cash" for it in items) +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"] + + 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: -- 2.52.0 From c1cc6bb43f5762479225336799c1c9750225d5b6 Mon Sep 17 00:00:00 2001 From: unknown <798648091@qq.com> Date: Mon, 27 Jul 2026 15:52:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=B5=8B=E8=AF=95=EF=BC=9A=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=8F=90=E7=8E=B0=E5=AE=A1=E6=A0=B8=E9=A1=B6=E9=83=A8?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E6=8C=89=E6=9D=A5=E6=BA=90=E6=B1=87=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_admin_read.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_admin_read.py b/tests/test_admin_read.py index 6ba2d09..ff1892c 100644 --- a/tests/test_admin_read.py +++ b/tests/test_admin_read.py @@ -400,6 +400,18 @@ def test_withdraw_summary_counts_match_status_lists_by_source( 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}, -- 2.52.0