From b89ee4fd18153e082b2bdbe79977e6ac871bde9f Mon Sep 17 00:00:00 2001 From: yml Date: Fri, 13 Nov 2026 17:20:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(weibo):=20=E9=9A=94=E7=A6=BB=E6=8D=A2?= =?UTF-8?q?=E7=BB=91=E8=A7=92=E8=89=B2=E7=9A=84=E5=BE=AE=E8=B1=86=E6=A0=B8?= =?UTF-8?q?=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_weibo_collab.py | 121 +++++++++ web/backend/services/weibo_collab_service.py | 244 +++++++++++-------- web/frontend/src/api/weiboCollab.ts | 1 + 3 files changed, 267 insertions(+), 99 deletions(-) diff --git a/tests/test_weibo_collab.py b/tests/test_weibo_collab.py index 6968c52..9b166c3 100644 --- a/tests/test_weibo_collab.py +++ b/tests/test_weibo_collab.py @@ -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) diff --git a/web/backend/services/weibo_collab_service.py b/web/backend/services/weibo_collab_service.py index d95f2fe..0d0953e 100644 --- a/web/backend/services/weibo_collab_service.py +++ b/web/backend/services/weibo_collab_service.py @@ -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, diff --git a/web/frontend/src/api/weiboCollab.ts b/web/frontend/src/api/weiboCollab.ts index 4d7034b..bfe71fe 100644 --- a/web/frontend/src/api/weiboCollab.ts +++ b/web/frontend/src/api/weiboCollab.ts @@ -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;