66c1e3ad8f
线上后台「提现审核」页现金账本报红(缺扣款/缺退款流水),根因是 withdraw_ledger_check 用全部提现单去比对普通现金流水(cash_transaction), 而 source=invite_cash 的提现,其扣款/退款流水写在独立的 invite_cash_transaction 表,于是每笔邀请提现单都在普通现金流水里找不到、被误报为「缺流水」。 改动: - 抽出 _check_withdraw_ledger_side 复用单账核对逻辑;withdraw_ledger_check 改为按 order.source 分两本账各自对账: coin_cash ↔ cash_transaction(withdraw/withdraw_refund), invite_cash ↔ invite_cash_transaction(invite_withdraw/invite_withdraw_refund)。 - 邀请奖励金账户的余额差额也纳入校验(此前完全未对账)。 - 纯只读校验,不改任何资金/流水写入;不掩盖真实缺流水 (coin_cash 单若真缺流水仍照报)。 - WithdrawLedgerCheckOut 新增 7 个 invite_* 字段(默认 0,向后兼容)。 - 补 tests/test_withdraw_ledger_check.py(此前无 ledger-check 测试): 覆盖邀请提现不再误报、邀请账缺流水能被抓、普通现金账未回归。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #121 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
172 lines
4.6 KiB
Python
172 lines
4.6 KiB
Python
"""admin 钱包(金币/现金流水 + 提现单)schemas。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from app.admin.schemas.admin import AdminAuditLogOut
|
|
|
|
|
|
class CoinTxnOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
user_id: int
|
|
amount: int
|
|
balance_after: int
|
|
biz_type: str
|
|
ref_id: str | None = None
|
|
remark: str | None = None
|
|
created_at: datetime
|
|
|
|
|
|
class CashTxnOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
user_id: int
|
|
amount_cents: int
|
|
balance_after_cents: int
|
|
biz_type: str
|
|
ref_id: str | None = None
|
|
remark: str | None = None
|
|
created_at: datetime
|
|
|
|
|
|
class WithdrawOrderOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
user_id: int
|
|
out_bill_no: str
|
|
amount_cents: int
|
|
# 提现类型:coin_cash(福利页提现,金币兑换的现金)/ invite_cash(邀请提现,邀请奖励金)
|
|
source: str = "coin_cash"
|
|
user_name: str | None = None # 提现实名(审核核对 + 打款用)
|
|
status: str
|
|
wechat_state: str | None = None
|
|
transfer_bill_no: str | None = None
|
|
fail_reason: str | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class WithdrawListItemOut(WithdrawOrderOut):
|
|
"""提现单列表项:在提现单字段基础上,补本页用户的手机号/昵称 + 累计成功提现金额(分)。
|
|
|
|
仅列表接口用(需联表 User + 聚合);详情/批量仍用 WithdrawOrderOut(不带这些字段)。
|
|
"""
|
|
|
|
phone: str | None = None
|
|
nickname: str | None = None
|
|
cumulative_success_cents: int = 0 # 累计成功提现 = SUM(amount_cents) WHERE status='success'
|
|
|
|
|
|
class WithdrawSummaryOut(BaseModel):
|
|
reviewing_count: int
|
|
reviewing_amount_cents: int
|
|
pending_count: int
|
|
failed_count: int
|
|
today_success_count: int
|
|
today_success_amount_cents: int
|
|
today_rejected_count: int
|
|
|
|
|
|
class WithdrawUserSnapshot(BaseModel):
|
|
id: int
|
|
phone: str
|
|
nickname: str | None = None
|
|
status: str
|
|
wechat_nickname: str | None = None
|
|
wechat_avatar_url: str | None = None
|
|
created_at: datetime
|
|
last_login_at: datetime
|
|
cash_balance_cents: int
|
|
withdraw_total: int
|
|
withdraw_success_cents: int
|
|
|
|
|
|
class WithdrawDetailOut(BaseModel):
|
|
order: WithdrawOrderOut
|
|
user: WithdrawUserSnapshot | None = None
|
|
risk_flags: list[str]
|
|
risk_score: int
|
|
recent_withdraws: list[WithdrawOrderOut]
|
|
recent_cash_transactions: list[CashTxnOut]
|
|
audit_logs: list[AdminAuditLogOut]
|
|
|
|
|
|
class ReconcileResult(BaseModel):
|
|
checked: int
|
|
resolved: int
|
|
|
|
|
|
class WithdrawBulkRequest(BaseModel):
|
|
out_bill_nos: list[str] = Field(
|
|
..., min_length=1, max_length=50, description="提现商户单号列表"
|
|
)
|
|
|
|
|
|
class WithdrawBulkRejectRequest(WithdrawBulkRequest):
|
|
reason: str = Field(
|
|
..., min_length=1, max_length=200, description="批量拒绝理由(用户可见)"
|
|
)
|
|
|
|
|
|
class WithdrawBulkItemResult(BaseModel):
|
|
out_bill_no: str
|
|
ok: bool
|
|
status: str | None = None
|
|
error: str | None = None
|
|
|
|
|
|
class WithdrawBulkResult(BaseModel):
|
|
total: int
|
|
success: int
|
|
failed: int
|
|
items: list[WithdrawBulkItemResult]
|
|
|
|
|
|
class WithdrawLedgerCheckOut(BaseModel):
|
|
ok: bool
|
|
# 普通现金账(coin_cash:金币兑换的现金)
|
|
cash_balance_total_cents: int
|
|
cash_transaction_total_cents: int
|
|
balance_diff_cents: int
|
|
missing_withdraw_txn_count: int
|
|
missing_refund_txn_count: int
|
|
duplicate_refund_txn_count: int
|
|
refund_txn_on_non_terminal_count: int
|
|
# 邀请奖励金账(invite_cash:与普通现金物理隔离,各自对账)。默认 0 向后兼容。
|
|
invite_cash_balance_total_cents: int = 0
|
|
invite_cash_transaction_total_cents: int = 0
|
|
invite_balance_diff_cents: int = 0
|
|
invite_missing_withdraw_txn_count: int = 0
|
|
invite_missing_refund_txn_count: int = 0
|
|
invite_duplicate_refund_txn_count: int = 0
|
|
invite_refund_txn_on_non_terminal_count: int = 0
|
|
|
|
|
|
class WxpayHealthCheckOut(BaseModel):
|
|
ok: bool
|
|
wxpay_configured: bool
|
|
wxpay_auth_configured: bool
|
|
private_key_path: str
|
|
private_key_exists: bool
|
|
private_key_loadable: bool
|
|
public_key_path: str
|
|
public_key_exists: bool
|
|
public_key_loadable: bool
|
|
auth_notify_url_configured: bool
|
|
auto_reconcile_enabled: bool
|
|
auto_reconcile_interval_sec: int
|
|
auto_reconcile_older_than_minutes: int
|
|
issues: list[str]
|
|
|
|
|
|
class WithdrawRejectRequest(BaseModel):
|
|
reason: str = Field(
|
|
..., min_length=1, max_length=200, description="拒绝理由(写入 fail_reason,用户可见)"
|
|
)
|