fix(douyu): 修复未换绑成功时角色信息被待确认新角色污染 & 增加绑定接口明细日志
This commit is contained in:
@@ -247,7 +247,27 @@ class DouyuActivityClient:
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询绑定信息失败")
|
||||
return self._parse_bind_info(payload, act_alias=act_alias, api_version="v2" if v2 else "v1")
|
||||
parsed = self._parse_bind_info(payload, act_alias=act_alias, api_version="v2" if v2 else "v1")
|
||||
self._log_bind_info(parsed)
|
||||
return parsed
|
||||
|
||||
def _log_bind_info(self, parsed: dict[str, Any]) -> None:
|
||||
"""输出绑定信息接口返回的角色明细(用于排查两个接口/多个 alias 的角色差异)。"""
|
||||
self.logger(
|
||||
f"查询绑定信息明细 api={parsed.get('api_version') or '-'} "
|
||||
f"act={parsed.get('act_alias') or '-'} "
|
||||
f"role={parsed.get('role_name') or '-'} "
|
||||
f"area={parsed.get('area_name') or '-'} plat={parsed.get('plat_name') or '-'} "
|
||||
f"nick={parsed.get('nickname') or '-'} "
|
||||
f"bound_account={1 if parsed.get('is_bound_account') else 0} "
|
||||
f"bound_role={1 if parsed.get('is_bound_role') else 0} "
|
||||
f"bound_act={1 if parsed.get('is_bound_act') else 0} "
|
||||
f"need_act={1 if parsed.get('need_bind_act') else 0} "
|
||||
f"need_role={1 if parsed.get('need_bind_role') else 0} "
|
||||
f"wait={parsed.get('change_role_wait_time') if parsed.get('change_role_wait_time') is not None else '-'} "
|
||||
f"can_change_role={parsed.get('can_change_role')} "
|
||||
f"can_change_time={parsed.get('can_change_time') if parsed.get('can_change_time') is not None else '-'}"
|
||||
)
|
||||
|
||||
def _parse_bind_info(self, payload: dict[str, Any], *, act_alias: str, api_version: str) -> dict[str, Any]:
|
||||
"""统一解析不同活动的腾讯游戏绑定信息。"""
|
||||
@@ -321,7 +341,9 @@ class DouyuActivityClient:
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询电竞手册绑定信息失败")
|
||||
return self._parse_bind_info(payload, act_alias=act_alias, api_version="esports")
|
||||
parsed = self._parse_bind_info(payload, act_alias=act_alias, api_version="esports")
|
||||
self._log_bind_info(parsed)
|
||||
return parsed
|
||||
|
||||
def get_esports_bind_qr(self, act_alias: str) -> dict[str, Any]:
|
||||
"""生成电竞手册的腾讯角色绑定二维码链接。"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
&& (
|
||||
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' },
|
||||
|
||||
Reference in New Issue
Block a user