fix(weibo): 隔离换绑角色的微豆核账

This commit is contained in:
yml
2026-11-13 17:20:44 +08:00
parent d2550d4ab7
commit b89ee4fd18
3 changed files with 267 additions and 99 deletions
+121
View File
@@ -172,6 +172,127 @@ def test_exchange_record_unique_match_and_archive(monkeypatch, db):
assert any(item["action"] == "weibo:collab:archive" for item in timeline)
def test_exchange_success_promotes_weidou_error_to_done(monkeypatch, db):
user = make_user(db)
account = make_account(db)
order = collab.create_order(
db,
user,
order_no="WB-ERROR-DONE",
shop="店铺A",
account_id=account.id,
goods_name="商品A",
required_weidou=100,
require_verified=False,
)
collab.set_status(
db,
user,
order,
collab.STATUS_WEIDOU_ERROR,
allow_check=True,
)
class FakeClient:
def __init__(self, *_args, **_kwargs):
pass
def list_exchange_records(self, *_args, **_kwargs):
return {
"list": [
{
"goods_name": "商品A",
"game_account": "角色A",
"cost_weidou": 100,
"exchange_time": "2099-01-01 00:00:00",
"reward_status": 2,
}
]
}
monkeypatch.setattr(collab, "WeiboClient", FakeClient)
result = collab.check_exchange_records(db, user, [order])
db.refresh(order)
assert result["success"] == 1
assert order.exchange_verdict == collab.VERDICT_SUCCESS
assert order.status == collab.STATUS_DONE
assert order.done_at is not None
assert collab.summary(db)["by_status"] == {collab.STATUS_DONE: 1}
def test_balance_check_does_not_mix_roles_after_rebind(monkeypatch, db):
user = make_user(db)
account = make_account(db)
first = collab.create_order(
db,
user,
order_no="WB-ROLE-A",
shop="店铺A",
account_id=account.id,
goods_name="商品A",
required_weidou=100,
require_verified=False,
)
collab.set_status(
db,
user,
first,
collab.STATUS_WEIDOU_ERROR,
allow_check=True,
)
account.game_role_name = "角色B"
db.commit()
second = collab.create_order(
db,
user,
order_no="WB-ROLE-B",
shop="店铺A",
account_id=account.id,
goods_name="商品B",
required_weidou=200,
require_verified=False,
)
class FakeClient:
calls = 0
def __init__(self, *_args, **_kwargs):
pass
def list_mall(self, *_args, **_kwargs):
FakeClient.calls += 1
return {"weidou_balance": 200, "list": []}
monkeypatch.setattr(collab, "WeiboClient", FakeClient)
result = collab.check_balance_groups(db, user, [second], threshold=100)
db.refresh(first)
db.refresh(second)
assert result["groups"] == [
{
"account_id": account.id,
"uid": account.uid,
"game_role_name": "角色B",
"status": collab.STATUS_RECHARGED,
"status_label": collab.ORDER_STATUS_LABELS[collab.STATUS_RECHARGED],
"balance": 200,
"required_total": 200,
"surplus": 0,
"threshold": 100,
"reason": "ok",
"message": "核账通过:角色「角色B」,余额 200,订单合计 200,结余 0",
"order_ids": [second.id],
}
]
assert FakeClient.calls == 1
assert first.status == collab.STATUS_WEIDOU_ERROR
assert second.status == collab.STATUS_RECHARGED
def test_export_cks_returns_plaintext_and_audits_without_ck(db):
user = make_user(db)
account = make_account(db)
+145 -99
View File
@@ -82,6 +82,14 @@ BALANCE_GROUP_STATUSES = (
STATUS_WEIDOU_ERROR,
)
AUTO_DONE_SOURCE_STATUSES = (
STATUS_TO_RECHARGE,
STATUS_RECHARGED,
STATUS_SCRIPTED,
STATUS_WEIDOU_ERROR,
STATUS_FAILED,
)
VERDICT_SUCCESS = "success"
VERDICT_FAILED = "failed"
VERDICT_UNKNOWN = "unknown"
@@ -935,7 +943,8 @@ def check_balance_groups(
if len(rows) > CHECK_BATCH_MAX:
raise CollabError(f"一次最多核账 {CHECK_BATCH_MAX} 条", code="batch_too_large")
threshold = max(0, min(int(threshold), 1_000_000))
grouped: dict[int, list[WeiboExternalOrder]] = {}
selected_keys: list[tuple[int, str]] = []
grouped: dict[tuple[int, str], list[WeiboExternalOrder]] = {}
failures: list[dict[str, Any]] = []
skipped: list[dict[str, Any]] = []
for order in rows:
@@ -958,28 +967,49 @@ def check_balance_groups(
}
)
continue
grouped.setdefault(int(order.account_id), []).append(order)
key = (
int(order.account_id),
_text(order.game_role_name, max_length=128),
)
if key not in grouped:
selected_keys.append(key)
grouped.setdefault(key, []).append(order)
selected_account_ids = list(dict.fromkeys(key[0] for key in selected_keys))
keys_by_account: dict[int, list[tuple[int, str]]] = {}
for key in selected_keys:
keys_by_account.setdefault(key[0], []).append(key)
selected_account_ids = set(grouped)
grouped = {}
for account_id in selected_account_ids:
grouped[account_id] = (
if selected_account_ids:
for row in (
db.query(WeiboExternalOrder)
.filter(
WeiboExternalOrder.account_id == account_id,
WeiboExternalOrder.account_id.in_(selected_account_ids),
WeiboExternalOrder.status.in_(BALANCE_GROUP_STATUSES),
WeiboExternalOrder.archived_at.is_(None),
)
.order_by(WeiboExternalOrder.id.asc())
.all()
)
):
if not row.account_id:
continue
key = (
int(row.account_id),
_text(row.game_role_name, max_length=128),
)
grouped.setdefault(key, []).append(row)
groups: list[dict[str, Any]] = []
updated = 0
for account_id, group_orders in grouped.items():
for account_id in selected_account_ids:
account_keys = keys_by_account.get(account_id, [])
account_orders = [
order for key in account_keys for order in grouped.get(key, [])
]
account = db.query(WeiboAccount).filter(WeiboAccount.id == account_id).first()
if account is None:
for order in group_orders:
for order in account_orders:
failures.append(
{
"id": order.id,
@@ -989,7 +1019,7 @@ def check_balance_groups(
)
continue
if not account.is_enabled:
for order in group_orders:
for order in account_orders:
failures.append(
{
"id": order.id,
@@ -1001,114 +1031,130 @@ def check_balance_groups(
try:
data = WeiboClient(account.cookie or "").list_mall(activity)
except (WeiboError, ValueError) as exc:
for order in group_orders:
for order in account_orders:
failures.append(
{"id": order.id, "order_no": order.order_no, "message": str(exc)}
)
continue
balance = _to_int(data.get("weidou_balance"))
required_total = sum(int(order.required_weidou or 0) for order in group_orders)
surplus = balance - required_total if balance is not None else None
if surplus is None:
reason = "error"
message = "未拿到微豆余额"
target = None
elif surplus < 0:
reason = "shortage"
message = (
f"微豆不足:余额 {balance},订单合计 {required_total},还差 {-surplus}"
)
target = STATUS_WEIDOU_ERROR
elif surplus > threshold:
reason = "excess"
message = (
f"微豆超出阈值:余额 {balance},订单合计 {required_total},"
f"多出 {surplus}(阈值 {threshold})"
)
target = STATUS_WEIDOU_ERROR
else:
reason = "ok"
message = (
f"核账通过:余额 {balance},订单合计 {required_total},结余 {surplus}"
)
target = STATUS_RECHARGED
if balance is not None:
account.weidou_balance = balance
now = _now()
for order in group_orders:
order.weidou_balance = balance
order.weidou_required_total = required_total
order.weidou_surplus = surplus
order.weidou_threshold = threshold
order.weidou_checked_at = now
order.weidou_check_reason = reason
order.weidou_check_message = message[:512]
order.weidou_check_detail = {
"account_id": account.id,
"uid": account.uid,
"balance": balance,
"required_total": required_total,
"surplus": surplus,
"threshold": threshold,
"order_ids": [item.id for item in group_orders],
}
if target is not None:
set_status(
for account_key in account_keys:
game_role_name = account_key[1]
group_orders = grouped.get(account_key, [])
if not group_orders:
continue
required_total = sum(
int(order.required_weidou or 0) for order in group_orders
)
surplus = balance - required_total if balance is not None else None
if surplus is None:
reason = "error"
message = "未拿到微豆余额"
target = None
elif surplus < 0:
reason = "shortage"
message = (
f"微豆不足:角色「{game_role_name or '未绑定'}」,余额 {balance},"
f"订单合计 {required_total},还差 {-surplus}"
)
target = STATUS_WEIDOU_ERROR
elif surplus > threshold:
reason = "excess"
message = (
f"微豆超出阈值:角色「{game_role_name or '未绑定'}」,余额 {balance},"
f"订单合计 {required_total},多出 {surplus}(阈值 {threshold})"
)
target = STATUS_WEIDOU_ERROR
else:
reason = "ok"
message = (
f"核账通过:角色「{game_role_name or '未绑定'}」,余额 {balance},"
f"订单合计 {required_total},结余 {surplus}"
)
target = STATUS_RECHARGED
now = _now()
for order in group_orders:
order.weidou_balance = balance
order.weidou_required_total = required_total
order.weidou_surplus = surplus
order.weidou_threshold = threshold
order.weidou_checked_at = now
order.weidou_check_reason = reason
order.weidou_check_message = message[:512]
order.weidou_check_detail = {
"account_id": account.id,
"uid": account.uid,
"role": game_role_name,
"balance": balance,
"required_total": required_total,
"surplus": surplus,
"threshold": threshold,
"order_ids": [item.id for item in group_orders],
}
if target is not None:
set_status(
db,
current,
order,
target,
reason=message,
allow_check=True,
commit=False,
)
order.recharged_at = now
updated += 1
_metric(
db,
current,
order,
target,
reason=message,
allow_check=True,
commit=False,
balance=balance,
required_total=required_total,
surplus=surplus,
records_total=0,
records_matched=0,
verdict=VERDICT_UNKNOWN if target is None else "",
message=message,
source="balance_check",
raw={
"reason": reason,
"role": game_role_name,
"order_ids": [item.id for item in group_orders],
},
)
order.recharged_at = now
updated += 1
_metric(
db,
current,
order,
balance=balance,
required_total=required_total,
surplus=surplus,
records_total=0,
records_matched=0,
verdict=VERDICT_UNKNOWN if target is None else "",
message=message,
source="balance_check",
raw={"reason": reason, "order_ids": [item.id for item in group_orders]},
)
_audit(
db,
current,
order,
"weibo:collab:balance_check",
_audit(
db,
current,
order,
"weibo:collab:balance_check",
{
"account_id": account.id,
"role": game_role_name,
"balance": balance,
"required_total": required_total,
"surplus": surplus,
"threshold": threshold,
"reason": reason,
},
)
groups.append(
{
"account_id": account.id,
"uid": account.uid,
"game_role_name": game_role_name,
"status": target or "",
"status_label": ORDER_STATUS_LABELS.get(target or "", ""),
"balance": balance,
"required_total": required_total,
"surplus": surplus,
"threshold": threshold,
"reason": reason,
},
"message": message,
"order_ids": [item.id for item in group_orders],
}
)
groups.append(
{
"account_id": account.id,
"uid": account.uid,
"status": target or "",
"status_label": ORDER_STATUS_LABELS.get(target or "", ""),
"balance": balance,
"required_total": required_total,
"surplus": surplus,
"threshold": threshold,
"reason": reason,
"message": message,
"order_ids": [item.id for item in group_orders],
}
)
db.commit()
return {
"total": len(rows),
@@ -1231,7 +1277,7 @@ def check_exchange_records(
order.exchange_record_snapshot = _json_safe(record)
order.exchange_verdict = VERDICT_SUCCESS
order.exchange_message = "兑换记录唯一命中"
if order.status in (STATUS_RECHARGED, STATUS_SCRIPTED):
if order.status in AUTO_DONE_SOURCE_STATUSES:
set_status(
db,
current,
+1
View File
@@ -248,6 +248,7 @@ export interface WeiboCollabBalanceCheckResult {
groups: Array<{
account_id: number;
uid: string;
game_role_name: string;
status: string;
status_label: string;
balance: number | null;