fix(douyu): 修复未换绑成功时角色信息被待确认新角色污染 & 增加绑定接口明细日志

This commit is contained in:
yml2213
2026-08-02 13:18:56 +08:00
parent 4fab6ed0aa
commit de2e53748d
3 changed files with 180 additions and 27 deletions
+89 -18
View File
@@ -272,6 +272,7 @@ class DouyuBatchRunner:
f" need_role={1 if snap['need_bind_role'] else 0}"
f" wait={snap['change_role_wait_time'] if snap['change_role_wait_time'] is not None else '-'}"
f" can_change_time={snap['can_change_time'] if snap['can_change_time'] is not None else '-'}"
f" can={snap['can_change_role']}"
f"{pending_text}"
)
@@ -716,6 +717,10 @@ class DouyuBatchRunner:
self._push_log("warning", f"查询绑定信息失败 act={alias}: {exc}")
continue
info = {**info, "act_alias": alias}
self._push_log(
"info",
f"绑定信息 act={alias} {self._format_bind_summary(info)}",
)
results.append(info)
return results
@@ -882,7 +887,9 @@ class DouyuBatchRunner:
],
})
role_name = snapshot["role_name"]
self._apply_bind_info_to_account(account, bind_info, "game_queried")
# 待确认角色只回传前端展示,不写入账号表,避免“未换绑成功但角色信息已变新”
account.bind_status = "game_queried"
account.updated_at = datetime.now(timezone.utc)
self._push_log("success", f"识别到待确认角色: {role_name} (act={query_alias})")
self._update_task_progress(
db,
@@ -1127,6 +1134,15 @@ class DouyuBatchRunner:
before_snapshot = self._bind_snapshot(before)
role_name = before_snapshot["role_name"]
query_alias = str(before.get("act_alias") or "")
# 确认前“已生效绑定”角色(bound_act=1),确认失败/回查失败时写库用,避免待确认角色污染 game_name
before_bound = next(
(
info
for info in before_candidates
if self._is_bound_act(info) and str(info.get("role_name") or "").strip()
),
None,
)
self._push_log(
"info",
f"确认前状态 confirm_act={confirm_alias or '-'} hit={query_alias or '-'} "
@@ -1174,6 +1190,12 @@ class DouyuBatchRunner:
# 确认接口优先用配置的确认 alias;没有则回退到命中查询的 alias
use_confirm_alias = confirm_alias or query_alias
confirm_result = client.confirm_bind(use_confirm_alias)
confirm_raw = confirm_result.get("raw") or {}
self._push_log(
"info",
f"确认绑定接口返回 act={use_confirm_alias} "
f"error={confirm_raw.get('error')} msg={confirm_raw.get('msg') or '-'}",
)
try:
after_candidates = self._fetch_bind_info_candidates(client, query_aliases)
after = self._pick_bind_info(
@@ -1184,9 +1206,15 @@ class DouyuBatchRunner:
) if after_candidates else None
if after is None:
raise DouyuActivityError("确认后回查绑定信息失败")
self._push_log(
"info",
f"确认后回查 hit={after.get('act_alias') or '-'} "
f"{self._format_bind_summary(after)}",
)
except DouyuActivityError as exc:
# 确认接口已成功时,回查失败仍按确认成功处理,但保留错误信息。
self._apply_bind_info_to_account(account, before, "bind_confirmed")
# 写库优先确认前已生效绑定,避免待确认角色被误写入。
self._apply_bind_info_to_account(account, before_bound or before, "bind_confirmed")
self._mark_task(
db,
task,
@@ -1212,7 +1240,8 @@ class DouyuBatchRunner:
final_snapshot = after_snapshot if after_snapshot["role_name"] else before_snapshot
final_role_name = final_snapshot["role_name"] or role_name
if not after_snapshot["is_bound_act"]:
self._apply_bind_info_to_account(account, final_info, "game_queried")
# 确认未生效:保留确认前已生效绑定,不把待确认新角色写入账号表
self._apply_bind_info_to_account(account, before_bound or before, "game_queried")
self._mark_task(
db,
task,
@@ -1929,32 +1958,60 @@ class DouyuBatchRunner:
if not candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
return
bind_info = self._pick_bind_info(
# 1) 优先“已生效绑定”角色(bound_act=1 且有角色名,通常是活动 alias)。
# 避免把扫码后未确认的新角色当成当前绑定结果。
bound_info = next(
(
info
for info in candidates
if self._is_bound_act(info) and str(info.get("role_name") or "").strip()
),
None,
)
# 2) 待确认角色(cjm 扫码后未确认;无已绑定时也用于首次绑定展示)
pending_info = self._pick_bind_info(
candidates,
prefer_pending=True,
prefer_aliases=query_aliases,
) or candidates[0]
snapshot = self._bind_snapshot(bind_info)
role_name = snapshot["role_name"]
source_alias = str(bind_info.get("act_alias") or "")
summary = f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}"
pending_role = str(pending_info.get("role_name") or "").strip()
bound_role = str(bound_info.get("role_name") or "") if bound_info else ""
has_pending = bool(pending_role) and (pending_role != bound_role or not bound_info)
# 查询结果角色 = 已生效绑定;首次绑定(无绑定)时回退待确认角色
bind_info = bound_info if bound_info else (pending_info if has_pending else None)
snapshot = self._bind_snapshot(bind_info) if bind_info else {}
role_name = snapshot.get("role_name") or ""
is_bound = snapshot.get("is_bound_act", False)
source_alias = str((bind_info or {}).get("act_alias") or "")
summary = (
f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}"
if bind_info
else "act=- role=-"
)
self._push_log(
"info",
"查询角色 "
f"aliases={','.join(query_aliases)} hit={source_alias or '-'} "
f"{self._format_bind_summary(bind_info)}",
f"aliases={','.join(query_aliases)} bound_hit={(bound_info or {}).get('act_alias') or '-'} "
f"pending_hit={pending_info.get('act_alias') or '-'} "
f"{self._format_bind_summary(bind_info) if bind_info else 'role=- bound_act=-'}",
)
self._apply_bind_info_to_account(account, bind_info, "game_queried" if role_name else "game_not_bound")
pending = bool(role_name) and not snapshot["is_bound_act"]
# 只有已生效绑定才写账号表;待确认角色不污染 game_name
if bind_info and is_bound:
self._apply_bind_info_to_account(account, bind_info, "game_queried")
else:
account.bind_status = "game_queried" if has_pending else "game_not_bound"
account.updated_at = datetime.now(timezone.utc)
result = {
"act_alias": source_alias,
"query_act_alias": source_alias,
"query_act_aliases": query_aliases,
**snapshot,
"bind_ready_for_confirm": pending,
"bind_confirmed": snapshot["is_bound_act"],
"bind_ready_for_confirm": has_pending,
"bind_confirmed": is_bound,
"bind_phase": (
"confirmed" if snapshot["is_bound_act"]
"confirmed" if is_bound
else ("role_ready" if role_name else "waiting_role")
),
"bind_summary": summary,
@@ -1967,10 +2024,18 @@ class DouyuBatchRunner:
for item in candidates
],
}
if not role_name:
# 已绑定角色之外另有待确认新角色时,单独字段展示,不覆盖 role_name
if has_pending and bound_info and pending_role != bound_role:
result["pending_role_name"] = pending_role
result["pending_area_name"] = str(pending_info.get("area_name") or "")
result["pending_plat_name"] = str(pending_info.get("plat_name") or "")
if not bind_info:
message = f"未获取到游戏名 | {summary}"
elif snapshot["is_bound_act"]:
message = f"当前已绑定: {role_name} | {summary}"
elif is_bound:
if has_pending and pending_role != bound_role:
message = f"当前已绑定: {bound_role},待确认: {pending_role} | {summary}"
else:
message = f"当前已绑定: {bound_role} | {summary}"
else:
message = f"待确认角色: {role_name} | {summary}"
self._mark_task(db, task, "success" if role_name else "failed", message, result)
@@ -2015,6 +2080,12 @@ class DouyuBatchRunner:
if not candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
return
for item in candidates:
self._push_log(
"info",
f"查询换绑时间候选 act={item.get('act_alias') or '-'} "
f"{self._format_bind_summary(item)}",
)
# 换绑时间看“当前已绑定”活动态,不要优先 cjm(cjm 常无 changeRoleWaitTime
bind_info = self._pick_change_wait_bind_info(candidates, config) or candidates[0]
snapshot = self._bind_snapshot(bind_info)
+67 -7
View File
@@ -26,12 +26,33 @@ import {
getExchangeImage,
} from '../utils/exchangeImage';
import { getErrorMessage } from '../utils/error';
import { formatTime } from '../utils/time';
import { message } from '../utils/antdMessage';
const { Text } = Typography;
type HandbookKind = 'elite' | 'esports';
const TASK_STATUS_COLORS: Record<string, string> = {
planned: 'default',
pending: 'default',
running: 'processing',
success: 'success',
failed: 'error',
error: 'error',
stopped: 'warning',
};
const TASK_STATUS_LABELS: Record<string, string> = {
planned: '已计划',
pending: '等待中',
running: '执行中',
success: '成功',
failed: '失败',
error: '异常',
stopped: '已停止',
};
const ELITE_QUICK_ACTIONS = [
{ key: 'get_bind_qr', icon: <QrcodeOutlined /> },
{ key: 'confirm_bind', icon: <CheckCircleOutlined /> },
@@ -845,22 +866,29 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const qrQueryResult = qrTask && qrQueryTask && qrQueryTask.id > qrTask.id ? qrQueryTask.result : null;
const qrQueryRunning = Boolean(qrQueryTask && qrTask && qrQueryTask.id > qrTask.id && qrQueryTask.status === 'running');
const qrQueryRoleName = resultText(qrQueryResult, 'role_name');
const qrQueryPendingRoleName = resultText(qrQueryResult, 'pending_role_name');
const qrQueryIsBoundAct = resultFlag(qrQueryResult, 'is_bound_act') || resultFlag(qrQueryResult, 'bind_confirmed');
const qrQueryPending = Boolean(
qrQueryResult
&& qrQueryRoleName
&& (
qrQueryResult.bind_ready_for_confirm === true
|| (!qrQueryIsBoundAct)
qrQueryPendingRoleName
|| (
qrQueryRoleName
&& (
qrQueryResult.bind_ready_for_confirm === true
|| (!qrQueryIsBoundAct)
)
)
),
);
const qrBindReady = bindReadyForConfirm(qrTask) || qrQueryPending;
const qrRoleSourceResult = qrQueryPending
? qrQueryResult
: (bindReadyForConfirm(qrTask) ? qrResult : null);
const qrRoleName = resultText(qrRoleSourceResult, 'role_name');
const qrAreaName = resultText(qrRoleSourceResult, 'area_name');
const qrPlatName = resultText(qrRoleSourceResult, 'plat_name');
// 待确认角色优先用 pending_* 字段(已绑定角色之外的扫码新角色)
const qrRoleName = resultText(qrRoleSourceResult, 'pending_role_name') || resultText(qrRoleSourceResult, 'role_name');
const qrAreaName = resultText(qrRoleSourceResult, 'pending_area_name') || resultText(qrRoleSourceResult, 'area_name');
const qrPlatName = resultText(qrRoleSourceResult, 'pending_plat_name') || resultText(qrRoleSourceResult, 'plat_name');
const qrCurrentRoleName = qrQueryRoleName
|| resultText(qrResult, 'current_role_name')
|| (
@@ -943,8 +971,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
? latestEsportsStateTaskByAccount.get(record.id)
: latestQueryGameTaskByAccount.get(record.id)
)?.result || null;
// role_name = 当前已生效绑定角色;pending_role_name = 扫码后待确认的新角色
const roleName = resultText(queryResult, 'role_name')
|| (isEsportsHandbook ? record.esports_game_name : record.game_name);
const pendingRoleName = resultText(queryResult, 'pending_role_name');
const channel = (
[resultText(queryResult, 'area_name'), resultText(queryResult, 'plat_name')]
.filter(Boolean)
@@ -954,6 +984,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return roleName ? (
<Space direction="vertical" size={0}>
<Text>{roleName}</Text>
{pendingRoleName ? (
<Text type="warning" style={{ fontSize: 12 }}>: {pendingRoleName}</Text>
) : null}
{channel ? <Text type="secondary" style={{ fontSize: 12 }}>{channel}</Text> : null}
</Space>
) : <Text type="secondary"></Text>;
@@ -1035,6 +1068,33 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return aw - bw;
},
},
{
title: '最近操作', width: 220,
render: (_, record) => {
const latest = latestTaskByAccount.get(record.id);
if (!latest) return <Text type="secondary"></Text>;
return (
<Space direction="vertical" size={1} style={{ width: '100%' }}>
<Space size={4} wrap>
<Tag color={TASK_STATUS_COLORS[latest.status] || 'default'}>
{TASK_STATUS_LABELS[latest.status] || latest.status}
</Tag>
<Text strong ellipsis style={{ maxWidth: 130 }}>
{taskTypes[latest.task_type] || latest.task_type}
</Text>
</Space>
{latest.message ? (
<Text type="secondary" style={{ fontSize: 12 }} ellipsis title={latest.message}>
{latest.message}
</Text>
) : null}
<Text type="secondary" style={{ fontSize: 12 }}>
{formatTime(latest.finished_at || latest.created_at)}
</Text>
</Space>
);
},
},
{
title: '二维码', width: 76, align: 'center',
render: (_, record) => {
@@ -1428,7 +1488,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
},
}}
tableLayout="fixed"
scroll={{ x: 960, y: Math.max(120, accountTableAreaHeight - 80) }}
scroll={{ x: 1180, y: Math.max(120, accountTableAreaHeight - 80) }}
onRow={(record) => ({
onContextMenu: (e) => handleRowContextMenu(record, e),
style: { cursor: 'context-menu' },