fix(weibo): 隔离换绑角色的微豆核账
This commit is contained in:
@@ -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)
|
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):
|
def test_export_cks_returns_plaintext_and_audits_without_ck(db):
|
||||||
user = make_user(db)
|
user = make_user(db)
|
||||||
account = make_account(db)
|
account = make_account(db)
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ BALANCE_GROUP_STATUSES = (
|
|||||||
STATUS_WEIDOU_ERROR,
|
STATUS_WEIDOU_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUTO_DONE_SOURCE_STATUSES = (
|
||||||
|
STATUS_TO_RECHARGE,
|
||||||
|
STATUS_RECHARGED,
|
||||||
|
STATUS_SCRIPTED,
|
||||||
|
STATUS_WEIDOU_ERROR,
|
||||||
|
STATUS_FAILED,
|
||||||
|
)
|
||||||
|
|
||||||
VERDICT_SUCCESS = "success"
|
VERDICT_SUCCESS = "success"
|
||||||
VERDICT_FAILED = "failed"
|
VERDICT_FAILED = "failed"
|
||||||
VERDICT_UNKNOWN = "unknown"
|
VERDICT_UNKNOWN = "unknown"
|
||||||
@@ -935,7 +943,8 @@ def check_balance_groups(
|
|||||||
if len(rows) > CHECK_BATCH_MAX:
|
if len(rows) > CHECK_BATCH_MAX:
|
||||||
raise CollabError(f"一次最多核账 {CHECK_BATCH_MAX} 条", code="batch_too_large")
|
raise CollabError(f"一次最多核账 {CHECK_BATCH_MAX} 条", code="batch_too_large")
|
||||||
threshold = max(0, min(int(threshold), 1_000_000))
|
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]] = []
|
failures: list[dict[str, Any]] = []
|
||||||
skipped: list[dict[str, Any]] = []
|
skipped: list[dict[str, Any]] = []
|
||||||
for order in rows:
|
for order in rows:
|
||||||
@@ -958,28 +967,49 @@ def check_balance_groups(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
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 = {}
|
grouped = {}
|
||||||
for account_id in selected_account_ids:
|
if selected_account_ids:
|
||||||
grouped[account_id] = (
|
for row in (
|
||||||
db.query(WeiboExternalOrder)
|
db.query(WeiboExternalOrder)
|
||||||
.filter(
|
.filter(
|
||||||
WeiboExternalOrder.account_id == account_id,
|
WeiboExternalOrder.account_id.in_(selected_account_ids),
|
||||||
WeiboExternalOrder.status.in_(BALANCE_GROUP_STATUSES),
|
WeiboExternalOrder.status.in_(BALANCE_GROUP_STATUSES),
|
||||||
WeiboExternalOrder.archived_at.is_(None),
|
WeiboExternalOrder.archived_at.is_(None),
|
||||||
)
|
)
|
||||||
.order_by(WeiboExternalOrder.id.asc())
|
.order_by(WeiboExternalOrder.id.asc())
|
||||||
.all()
|
.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]] = []
|
groups: list[dict[str, Any]] = []
|
||||||
updated = 0
|
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()
|
account = db.query(WeiboAccount).filter(WeiboAccount.id == account_id).first()
|
||||||
if account is None:
|
if account is None:
|
||||||
for order in group_orders:
|
for order in account_orders:
|
||||||
failures.append(
|
failures.append(
|
||||||
{
|
{
|
||||||
"id": order.id,
|
"id": order.id,
|
||||||
@@ -989,7 +1019,7 @@ def check_balance_groups(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if not account.is_enabled:
|
if not account.is_enabled:
|
||||||
for order in group_orders:
|
for order in account_orders:
|
||||||
failures.append(
|
failures.append(
|
||||||
{
|
{
|
||||||
"id": order.id,
|
"id": order.id,
|
||||||
@@ -1001,114 +1031,130 @@ def check_balance_groups(
|
|||||||
try:
|
try:
|
||||||
data = WeiboClient(account.cookie or "").list_mall(activity)
|
data = WeiboClient(account.cookie or "").list_mall(activity)
|
||||||
except (WeiboError, ValueError) as exc:
|
except (WeiboError, ValueError) as exc:
|
||||||
for order in group_orders:
|
for order in account_orders:
|
||||||
failures.append(
|
failures.append(
|
||||||
{"id": order.id, "order_no": order.order_no, "message": str(exc)}
|
{"id": order.id, "order_no": order.order_no, "message": str(exc)}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
balance = _to_int(data.get("weidou_balance"))
|
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:
|
if balance is not None:
|
||||||
account.weidou_balance = balance
|
account.weidou_balance = balance
|
||||||
now = _now()
|
for account_key in account_keys:
|
||||||
for order in group_orders:
|
game_role_name = account_key[1]
|
||||||
order.weidou_balance = balance
|
group_orders = grouped.get(account_key, [])
|
||||||
order.weidou_required_total = required_total
|
if not group_orders:
|
||||||
order.weidou_surplus = surplus
|
continue
|
||||||
order.weidou_threshold = threshold
|
required_total = sum(
|
||||||
order.weidou_checked_at = now
|
int(order.required_weidou or 0) for order in group_orders
|
||||||
order.weidou_check_reason = reason
|
)
|
||||||
order.weidou_check_message = message[:512]
|
surplus = balance - required_total if balance is not None else None
|
||||||
order.weidou_check_detail = {
|
if surplus is None:
|
||||||
"account_id": account.id,
|
reason = "error"
|
||||||
"uid": account.uid,
|
message = "未拿到微豆余额"
|
||||||
"balance": balance,
|
target = None
|
||||||
"required_total": required_total,
|
elif surplus < 0:
|
||||||
"surplus": surplus,
|
reason = "shortage"
|
||||||
"threshold": threshold,
|
message = (
|
||||||
"order_ids": [item.id for item in group_orders],
|
f"微豆不足:角色「{game_role_name or '未绑定'}」,余额 {balance},"
|
||||||
}
|
f"订单合计 {required_total},还差 {-surplus}"
|
||||||
if target is not None:
|
)
|
||||||
set_status(
|
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,
|
db,
|
||||||
current,
|
current,
|
||||||
order,
|
order,
|
||||||
target,
|
balance=balance,
|
||||||
reason=message,
|
required_total=required_total,
|
||||||
allow_check=True,
|
surplus=surplus,
|
||||||
commit=False,
|
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
|
_audit(
|
||||||
updated += 1
|
db,
|
||||||
_metric(
|
current,
|
||||||
db,
|
order,
|
||||||
current,
|
"weibo:collab:balance_check",
|
||||||
order,
|
{
|
||||||
balance=balance,
|
"account_id": account.id,
|
||||||
required_total=required_total,
|
"role": game_role_name,
|
||||||
surplus=surplus,
|
"balance": balance,
|
||||||
records_total=0,
|
"required_total": required_total,
|
||||||
records_matched=0,
|
"surplus": surplus,
|
||||||
verdict=VERDICT_UNKNOWN if target is None else "",
|
"threshold": threshold,
|
||||||
message=message,
|
"reason": reason,
|
||||||
source="balance_check",
|
},
|
||||||
raw={"reason": reason, "order_ids": [item.id for item in group_orders]},
|
)
|
||||||
)
|
groups.append(
|
||||||
_audit(
|
|
||||||
db,
|
|
||||||
current,
|
|
||||||
order,
|
|
||||||
"weibo:collab:balance_check",
|
|
||||||
{
|
{
|
||||||
"account_id": account.id,
|
"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,
|
"balance": balance,
|
||||||
"required_total": required_total,
|
"required_total": required_total,
|
||||||
"surplus": surplus,
|
"surplus": surplus,
|
||||||
"threshold": threshold,
|
"threshold": threshold,
|
||||||
"reason": reason,
|
"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()
|
db.commit()
|
||||||
return {
|
return {
|
||||||
"total": len(rows),
|
"total": len(rows),
|
||||||
@@ -1231,7 +1277,7 @@ def check_exchange_records(
|
|||||||
order.exchange_record_snapshot = _json_safe(record)
|
order.exchange_record_snapshot = _json_safe(record)
|
||||||
order.exchange_verdict = VERDICT_SUCCESS
|
order.exchange_verdict = VERDICT_SUCCESS
|
||||||
order.exchange_message = "兑换记录唯一命中"
|
order.exchange_message = "兑换记录唯一命中"
|
||||||
if order.status in (STATUS_RECHARGED, STATUS_SCRIPTED):
|
if order.status in AUTO_DONE_SOURCE_STATUSES:
|
||||||
set_status(
|
set_status(
|
||||||
db,
|
db,
|
||||||
current,
|
current,
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export interface WeiboCollabBalanceCheckResult {
|
|||||||
groups: Array<{
|
groups: Array<{
|
||||||
account_id: number;
|
account_id: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
|
game_role_name: string;
|
||||||
status: string;
|
status: string;
|
||||||
status_label: string;
|
status_label: string;
|
||||||
balance: number | null;
|
balance: number | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user