feat(weibo): 区分充值资金来源并完善协作单记录

This commit is contained in:
yml
2026-11-13 21:07:50 +08:00
parent f0abca3d58
commit 76b344eb83
11 changed files with 740 additions and 10 deletions
+127 -1
View File
@@ -26,6 +26,7 @@ from ..models import (
WeiboCollabShop,
WeiboExternalOrder,
WeiboExternalOrderMetric,
WeiboRechargeRecord,
)
from .audit_service import record_audit
from .collab_common import apply_order_visibility, count_archived
@@ -58,7 +59,7 @@ ORDER_STATUSES = (
ORDER_STATUS_LABELS = {
STATUS_INCOMPLETE: "待补资料",
STATUS_TO_RECHARGE: "待充值",
STATUS_RECHARGED: "已充值",
STATUS_RECHARGED: "余额已核账",
STATUS_SCRIPTED: "已上脚本",
STATUS_DONE: "兑换成功",
STATUS_WEIDOU_ERROR: "微豆异常",
@@ -729,6 +730,9 @@ def set_status(
now = _now()
if target == STATUS_TO_RECHARGE:
order.recharged_at = None
order.funding_source = ""
order.funding_source_detail = ""
order.funding_recharge_record_id = None
order.scripted_at = None
order.exchange_started_at = None
order.done_at = None
@@ -933,6 +937,60 @@ def _metric(
return metric
FUNDING_SOURCE_WEB_RECHARGE = "web_recharge"
FUNDING_SOURCE_ACCOUNT_BALANCE = "account_balance"
FUNDING_SOURCE_LEGACY_WEB_RECHARGE = "legacy_web_recharge"
FUNDING_SOURCE_UNKNOWN = "unknown"
FUNDING_SOURCE_LABELS = {
FUNDING_SOURCE_WEB_RECHARGE: "系统 Web 充值",
FUNDING_SOURCE_ACCOUNT_BALANCE: "账号原有/外部充值",
FUNDING_SOURCE_LEGACY_WEB_RECHARGE: "历史 Web 充值下单",
FUNDING_SOURCE_UNKNOWN: "来源未知",
}
RECHARGE_STATUS_LABELS = {
"created": "已创建",
"pending": "等待支付",
"paid": "支付成功",
"failed": "支付失败",
"closed": "订单关闭",
"unknown": "状态未知",
}
def _classify_balance_funding(
db: Session,
account: WeiboAccount,
group_orders: Sequence[WeiboExternalOrder],
) -> tuple[str, str, int | None]:
"""按账号维度和订单创建时间识别本次核账余额来源。"""
earliest = min(
(order.created_at for order in group_orders if order.created_at),
default=None,
)
query = db.query(WeiboRechargeRecord).filter(
WeiboRechargeRecord.account_id == account.id,
WeiboRechargeRecord.status == "paid",
WeiboRechargeRecord.paid_at.isnot(None),
)
if earliest is not None:
query = query.filter(WeiboRechargeRecord.paid_at >= earliest)
paid = query.order_by(WeiboRechargeRecord.paid_at.desc()).first()
if paid is not None:
return (
FUNDING_SOURCE_WEB_RECHARGE,
(
f"核账前存在已支付的 Web 充值:{paid.amount_yuan} 元,"
f"赠送 {paid.single_reward} 微豆,订单号 {paid.order_id}"
),
int(paid.id),
)
return (
FUNDING_SOURCE_ACCOUNT_BALANCE,
"核账通过,但核账前未发现已支付的系统 Web 充值记录",
None,
)
def check_balance_groups(
db: Session,
current: User,
@@ -1080,6 +1138,14 @@ def check_balance_groups(
target = STATUS_RECHARGED
now = _now()
if balance is None:
funding_source = FUNDING_SOURCE_UNKNOWN
funding_detail = "未拿到微豆余额,无法识别资金来源"
funding_record_id = None
else:
funding_source, funding_detail, funding_record_id = (
_classify_balance_funding(db, account, group_orders)
)
for order in group_orders:
order.weidou_balance = balance
order.weidou_required_total = required_total
@@ -1097,7 +1163,13 @@ def check_balance_groups(
"surplus": surplus,
"threshold": threshold,
"order_ids": [item.id for item in group_orders],
"funding_source": funding_source,
"funding_detail": funding_detail,
"funding_recharge_record_id": funding_record_id,
}
order.funding_source = funding_source
order.funding_source_detail = funding_detail[:512]
order.funding_recharge_record_id = funding_record_id
if target is not None:
set_status(
db,
@@ -1126,6 +1198,9 @@ def check_balance_groups(
"reason": reason,
"role": game_role_name,
"order_ids": [item.id for item in group_orders],
"funding_source": funding_source,
"funding_detail": funding_detail,
"funding_recharge_record_id": funding_record_id,
},
)
_audit(
@@ -1141,6 +1216,9 @@ def check_balance_groups(
"surplus": surplus,
"threshold": threshold,
"reason": reason,
"funding_source": funding_source,
"funding_detail": funding_detail,
"funding_recharge_record_id": funding_record_id,
},
)
groups.append(
@@ -1540,6 +1618,7 @@ def serialize_order(
accounts: Mapping[int, WeiboAccount] | None = None,
cluster: Mapping[str, Any] | None = None,
last_metric: WeiboExternalOrderMetric | None = None,
recharge_records: Sequence[Mapping[str, Any]] | None = None,
) -> dict[str, Any]:
names = users or {}
account_map = accounts or {}
@@ -1568,6 +1647,12 @@ def serialize_order(
"remark": order.remark or "",
"status": order.status,
"status_label": ORDER_STATUS_LABELS.get(order.status, order.status),
"funding_source": order.funding_source or "",
"funding_source_label": FUNDING_SOURCE_LABELS.get(
order.funding_source or "", ""
),
"funding_source_detail": order.funding_source_detail or "",
"funding_recharge_record_id": order.funding_recharge_record_id,
"next_status": NEXT_STATUS.get(order.status),
"missing_fields": _missing_for_recharge(order),
"weidou_balance": order.weidou_balance,
@@ -1609,6 +1694,7 @@ def serialize_order(
"account_cluster": dict(cluster) if cluster else None,
"last_metric_at": last_metric.created_at if last_metric else None,
"last_metric_message": last_metric.message if last_metric else "",
"recharge_records": list(recharge_records or []),
}
@@ -1732,6 +1818,44 @@ def list_orders(
}
def _recharge_records_out(db: Session, account_id: int | None) -> list[dict[str, Any]]:
if not account_id:
return []
rows = (
db.query(WeiboRechargeRecord)
.filter(WeiboRechargeRecord.account_id == account_id)
.order_by(WeiboRechargeRecord.created_at.desc(), WeiboRechargeRecord.id.desc())
.limit(50)
.all()
)
users = _users_by_id(
db,
{int(row.created_by) for row in rows if row.created_by},
)
return [
{
"id": int(row.id),
"order_id": row.order_id,
"pay_id": row.pay_id or "",
"amount_yuan": int(row.amount_yuan or 0),
"single_reward": int(row.single_reward or 0),
"status": row.status,
"status_label": RECHARGE_STATUS_LABELS.get(row.status, row.status),
"sub_status": row.sub_status or "",
"is_paid": row.status == "paid",
"paid_at": row.paid_at,
"status_checked_at": row.status_checked_at,
"created_by": row.created_by,
"created_by_name": users.get(int(row.created_by), "")
if row.created_by
else "",
"created_at": row.created_at,
"updated_at": row.updated_at,
}
for row in rows
]
def serialize_one(db: Session, order: WeiboExternalOrder) -> dict[str, Any]:
users = _users_by_id(
db,
@@ -1751,12 +1875,14 @@ def serialize_one(db: Session, order: WeiboExternalOrder) -> dict[str, Any]:
)
cluster = _account_clusters(db, [order]).get(order.id)
metric = _latest_metrics(db, [order.id]).get(order.id)
recharge_records = _recharge_records_out(db, order.account_id)
return serialize_order(
order,
users=users,
accounts=accounts,
cluster=cluster,
last_metric=metric,
recharge_records=recharge_records,
)
+71 -1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from sqlalchemy import or_
from sqlalchemy.orm import Session
from core.weibo import (
@@ -18,7 +19,7 @@ from core.weibo import (
normalize_weibo_cookie,
)
from ..models import WeiboAccount, WeiboWorkbenchAccount
from ..models import WeiboAccount, WeiboRechargeRecord, WeiboWorkbenchAccount
from .weibo_image_service import embed_weibo_goods_images
@@ -468,6 +469,75 @@ def create_weibo_recharge_order(
raise
def persist_weibo_recharge_order(
db: Session,
account: WeiboAccount,
order: WeiboRechargeOrder,
*,
created_by: int | None,
) -> WeiboRechargeRecord:
"""持久化 Web 充值下单结果。"""
row = (
db.query(WeiboRechargeRecord)
.filter(WeiboRechargeRecord.order_id == order.order_id)
.first()
)
if row is None:
row = WeiboRechargeRecord(
account_id=account.id,
order_id=order.order_id,
amount_yuan=order.amount_yuan,
single_reward=order.single_reward,
created_by=created_by,
)
db.add(row)
row.account_id = account.id
row.pay_id = order.pay_id
row.amount_yuan = order.amount_yuan
row.single_reward = order.single_reward
row.status = "created"
db.commit()
db.refresh(row)
return row
def persist_weibo_payment_status(
db: Session,
account: WeiboAccount,
status: WeiboPaymentStatus,
) -> WeiboRechargeRecord | None:
"""把充值支付查询结果写回对应记录。"""
row = (
db.query(WeiboRechargeRecord)
.filter(
WeiboRechargeRecord.account_id == account.id,
or_(
WeiboRechargeRecord.pay_id == status.pay_id,
WeiboRechargeRecord.order_id == status.out_pay_id,
),
)
.order_by(WeiboRechargeRecord.id.desc())
.first()
)
if row is None:
return None
normalized = {
"PAY_STATUS_SUCCESS": "paid",
"PAY_STATUS_PENDING": "pending",
"PAY_STATUS_FAILED": "failed",
"PAY_STATUS_CLOSED": "closed",
}.get(status.status, "unknown")
now = _now()
row.status = normalized
row.sub_status = status.sub_status
row.status_checked_at = now
if normalized == "paid" and row.paid_at is None:
row.paid_at = now
db.commit()
db.refresh(row)
return row
def query_weibo_payment_status(
db: Session,
account: WeiboAccount,