type: 收窄商城与斗鱼绑定执行器类型

This commit is contained in:
yml2213
2026-08-30 19:55:08 +08:00
parent 40e6f482b4
commit d400db987e
2 changed files with 328 additions and 150 deletions
+203 -68
View File
@@ -3,19 +3,40 @@
from __future__ import annotations from __future__ import annotations
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, TYPE_CHECKING
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.douyu import DouyuActivityClient, DouyuActivityError from core.douyu import DouyuActivityClient, DouyuActivityError
from ..models import Account, DouyuTask from ..models import Account, DouyuTask
if TYPE_CHECKING:
from .douyu_runner import DouyuBatchRunner
DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX" DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX"
DOUYU_BIND_ROLE_POLL_SECONDS = 65 DOUYU_BIND_ROLE_POLL_SECONDS = 65
DOUYU_BIND_ROLE_POLL_INTERVAL = 5 DOUYU_BIND_ROLE_POLL_INTERVAL = 5
DOUYU_CONFIRM_EFFECT_POLL_TIMES = 3 DOUYU_CONFIRM_EFFECT_POLL_TIMES = 3
DOUYU_CONFIRM_EFFECT_POLL_INTERVAL = 3 DOUYU_CONFIRM_EFFECT_POLL_INTERVAL = 3
class BindMixin: class BindMixin:
"""游戏角色绑定域:绑定状态机、扫码/确认绑定、换绑冷却。""" """游戏角色绑定域:绑定状态机、扫码/确认绑定、换绑冷却。"""
if TYPE_CHECKING:
_stop: Any
payload: dict
@staticmethod
def _to_int(value: Any) -> int | None: ...
@staticmethod
def _format_wait_time(seconds: int | None) -> str: ...
def _push_log(self, level: str, message: str) -> None: ...
def _mark_task(self, *args: Any, **kwargs: Any) -> None: ...
def _update_task_progress(self, *args: Any, **kwargs: Any) -> None: ...
def _client(self, cookie: str) -> DouyuActivityClient: ...
@staticmethod @staticmethod
def _action_act_alias(config: dict, key: str) -> str: def _action_act_alias(config: dict, key: str) -> str:
"""动作类接口用的活动 alias,排除只用于查询最新角色的 legacy alias。""" """动作类接口用的活动 alias,排除只用于查询最新角色的 legacy alias。"""
@@ -30,7 +51,9 @@ class BindMixin:
@classmethod @classmethod
def _bind_qr_act_alias(cls, config: dict) -> str: def _bind_qr_act_alias(cls, config: dict) -> str:
"""生成绑定二维码用的活动 alias。""" """生成绑定二维码用的活动 alias。"""
return cls._action_act_alias(config, "bind_act_alias") or cls._action_act_alias(config, "confirm_act_alias") return cls._action_act_alias(config, "bind_act_alias") or cls._action_act_alias(
config, "confirm_act_alias"
)
@staticmethod @staticmethod
def _query_bind_act_aliases(config: dict) -> list[str]: def _query_bind_act_aliases(config: dict) -> list[str]:
@@ -53,7 +76,9 @@ class BindMixin:
@classmethod @classmethod
def _confirm_act_alias(cls, config: dict) -> str: def _confirm_act_alias(cls, config: dict) -> str:
"""确认绑定接口用的活动 alias。""" """确认绑定接口用的活动 alias。"""
return cls._action_act_alias(config, "confirm_act_alias") or cls._action_act_alias(config, "bind_act_alias") return cls._action_act_alias(
config, "confirm_act_alias"
) or cls._action_act_alias(config, "bind_act_alias")
# 兼容旧调用名 # 兼容旧调用名
@classmethod @classmethod
@@ -73,7 +98,9 @@ class BindMixin:
@staticmethod @staticmethod
def _role_channel(bind_info: dict) -> str: def _role_channel(bind_info: dict) -> str:
return " / ".join( return " / ".join(
part for part in [bind_info.get("area_name"), bind_info.get("plat_name")] if part part
for part in [bind_info.get("area_name"), bind_info.get("plat_name")]
if part
) )
@staticmethod @staticmethod
@@ -177,7 +204,9 @@ class BindMixin:
"bind_info": info, "bind_info": info,
} }
def _format_bind_summary(self, bind_info: dict | None, *, pending: bool | None = None) -> str: def _format_bind_summary(
self, bind_info: dict | None, *, pending: bool | None = None
) -> str:
snap = self._bind_snapshot(bind_info) snap = self._bind_snapshot(bind_info)
role = snap["role_name"] or "-" role = snap["role_name"] or "-"
area = snap["area_name"] or "-" area = snap["area_name"] or "-"
@@ -197,20 +226,30 @@ class BindMixin:
f"{pending_text}" f"{pending_text}"
) )
def _apply_bind_info_to_account(self, account: Account, bind_info: dict, status: str) -> None: def _apply_bind_info_to_account(
self, account: Account, bind_info: dict, status: str
) -> None:
role_name = str(bind_info.get("role_name") or "") role_name = str(bind_info.get("role_name") or "")
account.game_name = role_name or account.game_name account.game_name = role_name or account.game_name
account.game_channel = self._role_channel(bind_info) or account.game_channel account.game_channel = self._role_channel(bind_info) or account.game_channel
account.change_role_wait_time = self._to_int(bind_info.get("change_role_wait_time")) account.change_role_wait_time = self._to_int(
bind_info.get("change_role_wait_time")
)
account.bind_status = status account.bind_status = status
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
def _apply_esports_bind_info_to_account(self, account: Account, bind_info: dict, status: str) -> None: def _apply_esports_bind_info_to_account(
self, account: Account, bind_info: dict, status: str
) -> None:
"""将电竞手册角色状态写入专属字段,避免覆盖精英宝典数据。""" """将电竞手册角色状态写入专属字段,避免覆盖精英宝典数据。"""
role_name = str(bind_info.get("role_name") or "") role_name = str(bind_info.get("role_name") or "")
account.esports_game_name = role_name or account.esports_game_name account.esports_game_name = role_name or account.esports_game_name
account.esports_game_channel = self._role_channel(bind_info) or account.esports_game_channel account.esports_game_channel = (
account.esports_change_role_wait_time = self._to_int(bind_info.get("change_role_wait_time")) self._role_channel(bind_info) or account.esports_game_channel
)
account.esports_change_role_wait_time = self._to_int(
bind_info.get("change_role_wait_time")
)
account.esports_can_change_time = self._to_int(bind_info.get("can_change_time")) account.esports_can_change_time = self._to_int(bind_info.get("can_change_time"))
account.esports_bind_status = status account.esports_bind_status = status
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
@@ -235,7 +274,9 @@ class BindMixin:
"has_selected_role": has_selected_role, "has_selected_role": has_selected_role,
"bind_ready_for_confirm": has_selected_role and not esports_bound, "bind_ready_for_confirm": has_selected_role and not esports_bound,
"bind_confirmed": esports_bound, "bind_confirmed": esports_bound,
"bind_phase": "confirmed" if esports_bound else ("role_ready" if has_selected_role else "waiting_role"), "bind_phase": "confirmed"
if esports_bound
else ("role_ready" if has_selected_role else "waiting_role"),
"activity_bind_info": activity_info, "activity_bind_info": activity_info,
"activity_bind_snapshot": activity_snapshot, "activity_bind_snapshot": activity_snapshot,
"role_source": "activity", "role_source": "activity",
@@ -302,7 +343,9 @@ class BindMixin:
except ValueError: except ValueError:
return len(prefer_aliases) + 1 return len(prefer_aliases) + 1
ranked = sorted(enumerate(candidates), key=lambda item: (_alias_rank(item[1]), item[0])) ranked = sorted(
enumerate(candidates), key=lambda item: (_alias_rank(item[1]), item[0])
)
ordered = [item[1] for item in ranked] ordered = [item[1] for item in ranked]
if prefer_pending: if prefer_pending:
@@ -337,12 +380,15 @@ class BindMixin:
bound = [ bound = [
info info
for info in candidates for info in candidates
if self._is_bound_act(info) and str(info.get("role_name") or "").strip() if self._is_bound_act(info)
and str(info.get("role_name") or "").strip()
and str(info.get("act_alias") or "") in prefer and str(info.get("act_alias") or "") in prefer
] ]
if not bound: if not bound:
return None return None
return sorted(bound, key=lambda info: prefer.index(str(info.get("act_alias") or "")))[0] return sorted(
bound, key=lambda info: prefer.index(str(info.get("act_alias") or ""))
)[0]
def _pick_baseline_bind_info( def _pick_baseline_bind_info(
self, self,
@@ -385,7 +431,9 @@ class BindMixin:
f"aliases={','.join(query_aliases) or '-'} " f"aliases={','.join(query_aliases) or '-'} "
f"baseline={baseline_role_name or '-'} bound={1 if baseline_is_bound_act else 0}", f"baseline={baseline_role_name or '-'} bound={1 if baseline_is_bound_act else 0}",
) )
self._update_task_progress(db, task, "running", "已生成绑定二维码,等待扫码绑定", result) self._update_task_progress(
db, task, "running", "已生成绑定二维码,等待扫码绑定", result
)
poll_count = 0 poll_count = 0
last_summary = "" last_summary = ""
@@ -396,17 +444,22 @@ class BindMixin:
candidates = self._fetch_bind_info_candidates(client, query_aliases) candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not candidates: if not candidates:
result["bind_poll_error"] = "所有 actAlias 查询绑定信息失败" result["bind_poll_error"] = "所有 actAlias 查询绑定信息失败"
self._update_task_progress(db, task, "running", "等待绑定角色同步: 查询失败", result) self._update_task_progress(
db, task, "running", "等待绑定角色同步: 查询失败", result
)
continue continue
poll_count += 1 poll_count += 1
bind_info = self._pick_bind_info( bind_info = (
self._pick_bind_info(
candidates, candidates,
baseline_role_name=baseline_role_name, baseline_role_name=baseline_role_name,
baseline_is_bound_act=baseline_is_bound_act, baseline_is_bound_act=baseline_is_bound_act,
prefer_pending=True, prefer_pending=True,
prefer_aliases=query_aliases, prefer_aliases=query_aliases,
) or candidates[0] )
or candidates[0]
)
snapshot = self._bind_snapshot(bind_info) snapshot = self._bind_snapshot(bind_info)
is_pending_role = self._is_pending_role( is_pending_role = self._is_pending_role(
bind_info, bind_info,
@@ -426,7 +479,8 @@ class BindMixin:
# 注意:未识别到新角色时,不要把当前已绑定角色写进 role_name, # 注意:未识别到新角色时,不要把当前已绑定角色写进 role_name,
# 否则前端会把旧角色误当成“待确认角色/查询结果”。 # 否则前端会把旧角色误当成“待确认角色/查询结果”。
if is_pending_role: if is_pending_role:
result.update({ result.update(
{
**snapshot, **snapshot,
"act_alias": query_alias, "act_alias": query_alias,
"query_act_alias": query_alias, "query_act_alias": query_alias,
@@ -444,12 +498,15 @@ class BindMixin:
} }
for item in candidates for item in candidates
], ],
}) }
)
role_name = snapshot["role_name"] role_name = snapshot["role_name"]
# 待确认角色只回传前端展示,不写入账号表,避免“未换绑成功但角色信息已变新” # 待确认角色只回传前端展示,不写入账号表,避免“未换绑成功但角色信息已变新”
account.bind_status = "game_queried" account.bind_status = "game_queried"
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
self._push_log("success", f"识别到待确认角色: {role_name} (act={query_alias})") self._push_log(
"success", f"识别到待确认角色: {role_name} (act={query_alias})"
)
self._update_task_progress( self._update_task_progress(
db, db,
task, task,
@@ -464,7 +521,8 @@ class BindMixin:
result["current_area_name"] = snapshot["area_name"] result["current_area_name"] = snapshot["area_name"]
result["current_plat_name"] = snapshot["plat_name"] result["current_plat_name"] = snapshot["plat_name"]
result.update({ result.update(
{
"bind_info": bind_info, "bind_info": bind_info,
"act_alias": query_alias, "act_alias": query_alias,
"query_act_alias": query_alias, "query_act_alias": query_alias,
@@ -484,8 +542,11 @@ class BindMixin:
"bind_polling": True, "bind_polling": True,
"poll_count": poll_count, "poll_count": poll_count,
"bind_summary": summary, "bind_summary": summary,
}) }
self._update_task_progress(db, task, "running", f"等待扫码绑定 ({summary})", result) )
self._update_task_progress(
db, task, "running", f"等待扫码绑定 ({summary})", result
)
result["bind_polling"] = False result["bind_polling"] = False
result["bind_ready_for_confirm"] = False result["bind_ready_for_confirm"] = False
@@ -499,7 +560,9 @@ class BindMixin:
) )
return "", result return "", result
def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): def _execute_get_bind_qr(
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
):
client = self._client(cookie) client = self._client(cookie)
qr_act_alias = self._bind_qr_act_alias(config) qr_act_alias = self._bind_qr_act_alias(config)
query_aliases = self._query_bind_act_aliases(config) query_aliases = self._query_bind_act_aliases(config)
@@ -511,19 +574,26 @@ class BindMixin:
before_candidates = self._fetch_bind_info_candidates(client, query_aliases) before_candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not before_candidates: if not before_candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") self._mark_task(
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
)
return return
# baseline 用活动 alias 的“当前已绑定”;pending 检测优先 cjm 的换绑最新态 # baseline 用活动 alias 的“当前已绑定”;pending 检测优先 cjm 的换绑最新态
before = self._pick_baseline_bind_info(before_candidates, config) or {} before = self._pick_baseline_bind_info(before_candidates, config) or {}
# 换绑冷却必须看活动当前绑定(QYOOB),不要用 cjm # 换绑冷却必须看活动当前绑定(QYOOB),不要用 cjm
cooldown_info = self._pick_change_wait_bind_info(before_candidates, config) cooldown_info = self._pick_change_wait_bind_info(before_candidates, config)
pending_before = self._pick_bind_info( cooldown = cooldown_info or {}
pending_before = (
self._pick_bind_info(
before_candidates, before_candidates,
baseline_role_name=str(before.get("role_name") or ""), baseline_role_name=str(before.get("role_name") or ""),
baseline_is_bound_act=self._is_bound_act(before), baseline_is_bound_act=self._is_bound_act(before),
prefer_pending=True, prefer_pending=True,
prefer_aliases=query_aliases, prefer_aliases=query_aliases,
) or before or before_candidates[0] )
or before
or before_candidates[0]
)
before_snapshot = self._bind_snapshot(before) before_snapshot = self._bind_snapshot(before)
cooldown_snapshot = self._bind_snapshot(cooldown_info) cooldown_snapshot = self._bind_snapshot(cooldown_info)
current_role_name = before_snapshot["role_name"] or ( current_role_name = before_snapshot["role_name"] or (
@@ -535,7 +605,7 @@ class BindMixin:
"绑定前状态 " "绑定前状态 "
f"qr_act={qr_act_alias or '-'} query={','.join(query_aliases)} " f"qr_act={qr_act_alias or '-'} query={','.join(query_aliases)} "
f"baseline_hit={before.get('act_alias') or '-'} {self._format_bind_summary(before)} " f"baseline_hit={before.get('act_alias') or '-'} {self._format_bind_summary(before)} "
f"cooldown_hit={cooldown_info.get('act_alias') or '-'} " f"cooldown_hit={cooldown.get('act_alias') or '-'} "
f"{self._format_bind_summary(cooldown_info)} " f"{self._format_bind_summary(cooldown_info)} "
f"pending_hit={pending_before.get('act_alias') or '-'} " f"pending_hit={pending_before.get('act_alias') or '-'} "
f"{self._format_bind_summary(pending_before)}", f"{self._format_bind_summary(pending_before)}",
@@ -543,33 +613,43 @@ class BindMixin:
# 生成二维码前强制检查换绑冷却:冷却中直接失败,绝不发码 # 生成二维码前强制检查换绑冷却:冷却中直接失败,绝不发码
if self._is_change_cooling(cooldown_info): if self._is_change_cooling(cooldown_info):
role_label = current_role_name or cooldown_snapshot["role_name"] or "当前角色" role_label = (
current_role_name or cooldown_snapshot["role_name"] or "当前角色"
)
wait_text = self._format_wait_time(wait_time) or "冷却中" wait_text = self._format_wait_time(wait_time) or "冷却中"
self._push_log( self._push_log(
"warning", "warning",
f"换绑冷却中,跳过生成二维码 role={role_label} wait={wait_time if wait_time is not None else '-'} " f"换绑冷却中,跳过生成二维码 role={role_label} wait={wait_time if wait_time is not None else '-'} "
f"can={cooldown_info.get('can_change_role')}", f"can={cooldown.get('can_change_role')}",
) )
result = { result = {
"act_alias": qr_act_alias or cooldown_info.get("act_alias") or before.get("act_alias"), "act_alias": qr_act_alias
"query_act_alias": cooldown_info.get("act_alias") or before.get("act_alias"), or cooldown.get("act_alias")
or before.get("act_alias"),
"query_act_alias": cooldown.get("act_alias") or before.get("act_alias"),
"query_act_aliases": query_aliases, "query_act_aliases": query_aliases,
"before_bind_info": before, "before_bind_info": before,
"cooldown_bind_info": cooldown_info, "cooldown_bind_info": cooldown_info,
**cooldown_snapshot, **cooldown_snapshot,
"current_role_name": role_label if role_label != "当前角色" else current_role_name, "current_role_name": role_label
"current_area_name": cooldown_snapshot["area_name"] or before_snapshot["area_name"], if role_label != "当前角色"
"current_plat_name": cooldown_snapshot["plat_name"] or before_snapshot["plat_name"], else current_role_name,
"current_area_name": cooldown_snapshot["area_name"]
or before_snapshot["area_name"],
"current_plat_name": cooldown_snapshot["plat_name"]
or before_snapshot["plat_name"],
"change_role_wait_time": wait_time, "change_role_wait_time": wait_time,
"change_role_wait_text": self._format_wait_time(wait_time), "change_role_wait_text": self._format_wait_time(wait_time),
"bind_ready_for_confirm": False, "bind_ready_for_confirm": False,
"bind_confirmed": bool(cooldown_snapshot["is_bound_act"] or before_snapshot["is_bound_act"]), "bind_confirmed": bool(
cooldown_snapshot["is_bound_act"] or before_snapshot["is_bound_act"]
),
"bind_phase": "change_waiting", "bind_phase": "change_waiting",
"bind_polling": False, "bind_polling": False,
} }
self._apply_bind_info_to_account( self._apply_bind_info_to_account(
account, account,
cooldown_info if cooldown_snapshot["role_name"] else before, cooldown if cooldown_snapshot["role_name"] else before,
"bind_confirmed" if result["bind_confirmed"] else "game_queried", "bind_confirmed" if result["bind_confirmed"] else "game_queried",
) )
self._mark_task( self._mark_task(
@@ -610,7 +690,9 @@ class BindMixin:
account.bind_status = "bind_qr_generated" account.bind_status = "bind_qr_generated"
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
# 关键:先把二维码 progress 出去,前端 running 期间即可弹窗扫码。 # 关键:先把二维码 progress 出去,前端 running 期间即可弹窗扫码。
self._update_task_progress(db, task, "running", "已生成绑定二维码,等待扫码绑定", result) self._update_task_progress(
db, task, "running", "已生成绑定二维码,等待扫码绑定", result
)
role_name, result = self._wait_bind_role_result( role_name, result = self._wait_bind_role_result(
client, client,
@@ -623,7 +705,9 @@ class BindMixin:
baseline_is_bound_act=before_snapshot["is_bound_act"], baseline_is_bound_act=before_snapshot["is_bound_act"],
) )
if role_name: if role_name:
self._mark_task(db, task, "success", f"已识别角色: {role_name},待确认绑定", result) self._mark_task(
db, task, "success", f"已识别角色: {role_name},待确认绑定", result
)
return return
if result.get("bind_phase") == "stopped": if result.get("bind_phase") == "stopped":
self._mark_task(db, task, "stopped", "任务已停止", result) self._mark_task(db, task, "stopped", "任务已停止", result)
@@ -636,7 +720,9 @@ class BindMixin:
timeout_msg = f"{timeout_msg} | {last_summary}" timeout_msg = f"{timeout_msg} | {last_summary}"
self._mark_task(db, task, "success", timeout_msg, result) self._mark_task(db, task, "success", timeout_msg, result)
def _execute_confirm_bind(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): def _execute_confirm_bind(
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
):
client = self._client(cookie) client = self._client(cookie)
confirm_alias = self._confirm_act_alias(config) confirm_alias = self._confirm_act_alias(config)
query_aliases = self._query_bind_act_aliases(config) query_aliases = self._query_bind_act_aliases(config)
@@ -651,15 +737,20 @@ class BindMixin:
before_candidates = self._fetch_bind_info_candidates(client, query_aliases) before_candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not before_candidates: if not before_candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") self._mark_task(
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
)
return return
before = self._pick_bind_info( before = (
self._pick_bind_info(
before_candidates, before_candidates,
baseline_role_name="", baseline_role_name="",
baseline_is_bound_act=False, baseline_is_bound_act=False,
prefer_pending=True, prefer_pending=True,
prefer_aliases=query_aliases, prefer_aliases=query_aliases,
) or before_candidates[0] )
or before_candidates[0]
)
before_snapshot = self._bind_snapshot(before) before_snapshot = self._bind_snapshot(before)
role_name = before_snapshot["role_name"] role_name = before_snapshot["role_name"]
query_alias = str(before.get("act_alias") or "") query_alias = str(before.get("act_alias") or "")
@@ -695,10 +786,11 @@ class BindMixin:
before_is_current_bound = ( before_is_current_bound = (
before_bound is not None before_bound is not None
and str(before.get("act_alias") or "") == str(before_bound.get("act_alias") or "") and str(before.get("act_alias") or "")
== str(before_bound.get("act_alias") or "")
and role_name == str(before_bound.get("role_name") or "") and role_name == str(before_bound.get("role_name") or "")
) )
if before_is_current_bound: if before_is_current_bound and before_bound is not None:
self._apply_bind_info_to_account(account, before_bound, "bind_confirmed") self._apply_bind_info_to_account(account, before_bound, "bind_confirmed")
self._mark_task( self._mark_task(
db, db,
@@ -755,6 +847,7 @@ class BindMixin:
f"确认绑定接口返回 act={use_confirm_alias} " f"确认绑定接口返回 act={use_confirm_alias} "
f"error={confirm_raw.get('error')} msg={confirm_raw.get('msg') or '-'}", f"error={confirm_raw.get('error')} msg={confirm_raw.get('msg') or '-'}",
) )
def _pick_bound_after(candidates: list[dict]) -> dict | None: def _pick_bound_after(candidates: list[dict]) -> dict | None:
"""确认后回查:只认活动 alias 上的已生效绑定(bound_act=1)。""" """确认后回查:只认活动 alias 上的已生效绑定(bound_act=1)。"""
return self._pick_current_bound_info( return self._pick_current_bound_info(
@@ -773,7 +866,9 @@ class BindMixin:
for _ in range(DOUYU_CONFIRM_EFFECT_POLL_TIMES): for _ in range(DOUYU_CONFIRM_EFFECT_POLL_TIMES):
if self._stop.wait(DOUYU_CONFIRM_EFFECT_POLL_INTERVAL): if self._stop.wait(DOUYU_CONFIRM_EFFECT_POLL_INTERVAL):
break break
after_candidates = self._fetch_bind_info_candidates(client, query_aliases) after_candidates = self._fetch_bind_info_candidates(
client, query_aliases
)
if not after_candidates: if not after_candidates:
break break
after = _pick_bound_after(after_candidates) after = _pick_bound_after(after_candidates)
@@ -782,7 +877,9 @@ class BindMixin:
if after is None: if after is None:
# 已生效绑定始终未出现:确认未生效(或同步延迟超时),保留原绑定 # 已生效绑定始终未出现:确认未生效(或同步延迟超时),保留原绑定
if before_bound is not None: if before_bound is not None:
self._apply_bind_info_to_account(account, before_bound, "game_queried") self._apply_bind_info_to_account(
account, before_bound, "game_queried"
)
else: else:
account.bind_status = "game_queried" account.bind_status = "game_queried"
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
@@ -815,7 +912,9 @@ class BindMixin:
# 查询接口异常:确认接口已成功时按成功处理,但保留错误信息。 # 查询接口异常:确认接口已成功时按成功处理,但保留错误信息。
# 写库优先确认前已生效绑定,避免待确认角色被误写入。 # 写库优先确认前已生效绑定,避免待确认角色被误写入。
if before_bound is not None: if before_bound is not None:
self._apply_bind_info_to_account(account, before_bound, "bind_confirmed") self._apply_bind_info_to_account(
account, before_bound, "bind_confirmed"
)
else: else:
account.bind_status = "bind_confirmed" account.bind_status = "bind_confirmed"
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
@@ -882,7 +981,9 @@ class BindMixin:
self._apply_esports_bind_info_to_account( self._apply_esports_bind_info_to_account(
account, account,
state, state,
"esports_bound" if esports_bound else ("esports_bind_ready" if role_text else "game_not_bound"), "esports_bound"
if esports_bound
else ("esports_bind_ready" if role_text else "game_not_bound"),
) )
# 二维码用于重新选择角色,不应因为已有角色或换绑冷却而隐藏。 # 二维码用于重新选择角色,不应因为已有角色或换绑冷却而隐藏。
# 冷却是否允许最终由 actBind 返回结果决定。 # 冷却是否允许最终由 actBind 返回结果决定。
@@ -970,10 +1071,14 @@ class BindMixin:
result = { result = {
**after_state, **after_state,
"confirm": confirm_result, "confirm": confirm_result,
"bind_phase": "confirmed" if after_state["esports_bound"] else "confirm_failed", "bind_phase": "confirmed"
if after_state["esports_bound"]
else "confirm_failed",
} }
if not after_state["esports_bound"]: if not after_state["esports_bound"]:
self._apply_esports_bind_info_to_account(account, after_state, "esports_bind_ready") self._apply_esports_bind_info_to_account(
account, after_state, "esports_bind_ready"
)
self._mark_task( self._mark_task(
db, db,
task, task,
@@ -1013,7 +1118,9 @@ class BindMixin:
self._apply_esports_bind_info_to_account( self._apply_esports_bind_info_to_account(
account, account,
state, state,
"esports_bound" if is_bound else ("esports_bind_ready" if role_name else "game_not_bound"), "esports_bound"
if is_bound
else ("esports_bind_ready" if role_name else "game_not_bound"),
) )
result = { result = {
**state, **state,
@@ -1026,7 +1133,9 @@ class BindMixin:
message = "未查询到游戏角色,请先切换角色" message = "未查询到游戏角色,请先切换角色"
self._mark_task(db, task, "success", message, result) self._mark_task(db, task, "success", message, result)
def _execute_query_game_name(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): def _execute_query_game_name(
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
):
client = self._client(cookie) client = self._client(cookie)
query_aliases = self._query_bind_act_aliases(config) query_aliases = self._query_bind_act_aliases(config)
if not query_aliases: if not query_aliases:
@@ -1034,24 +1143,33 @@ class BindMixin:
return return
candidates = self._fetch_bind_info_candidates(client, query_aliases) candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not candidates: if not candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") self._mark_task(
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
)
return return
# 1) 优先“已生效绑定”角色(bound_act=1 且有角色名,通常是活动 alias)。 # 1) 优先“已生效绑定”角色(bound_act=1 且有角色名,通常是活动 alias)。
# 避免把扫码后未确认的新角色当成当前绑定结果。 # 避免把扫码后未确认的新角色当成当前绑定结果。
bound_info = self._pick_current_bound_info(candidates, config) bound_info = self._pick_current_bound_info(candidates, config)
# 2) 待确认角色(cjm 扫码后未确认;无已绑定时也用于首次绑定展示) # 2) 待确认角色(cjm 扫码后未确认;无已绑定时也用于首次绑定展示)
pending_info = self._pick_bind_info( pending_info = (
self._pick_bind_info(
candidates, candidates,
prefer_pending=True, prefer_pending=True,
prefer_aliases=query_aliases, prefer_aliases=query_aliases,
) or candidates[0] )
or candidates[0]
)
pending_role = str(pending_info.get("role_name") or "").strip() pending_role = str(pending_info.get("role_name") or "").strip()
bound_role = str(bound_info.get("role_name") or "") if bound_info else "" 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) 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) 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 {} snapshot = self._bind_snapshot(bind_info) if bind_info else {}
role_name = snapshot.get("role_name") or "" role_name = snapshot.get("role_name") or ""
is_bound = snapshot.get("is_bound_act", False) is_bound = snapshot.get("is_bound_act", False)
@@ -1082,7 +1200,8 @@ class BindMixin:
"bind_ready_for_confirm": has_pending, "bind_ready_for_confirm": has_pending,
"bind_confirmed": is_bound, "bind_confirmed": is_bound,
"bind_phase": ( "bind_phase": (
"confirmed" if is_bound "confirmed"
if is_bound
else ("role_ready" if role_name else "waiting_role") else ("role_ready" if role_name else "waiting_role")
), ),
"bind_summary": summary, "bind_summary": summary,
@@ -1104,14 +1223,18 @@ class BindMixin:
message = f"未获取到游戏名 | {summary}" message = f"未获取到游戏名 | {summary}"
elif is_bound: elif is_bound:
if has_pending and pending_role != bound_role: if has_pending and pending_role != bound_role:
message = f"当前已绑定: {bound_role},待确认: {pending_role} | {summary}" message = (
f"当前已绑定: {bound_role},待确认: {pending_role} | {summary}"
)
else: else:
message = f"当前已绑定: {bound_role} | {summary}" message = f"当前已绑定: {bound_role} | {summary}"
else: else:
message = f"待确认角色: {role_name} | {summary}" message = f"待确认角色: {role_name} | {summary}"
self._mark_task(db, task, "success" if role_name else "failed", message, result) self._mark_task(db, task, "success" if role_name else "failed", message, result)
def _pick_change_wait_bind_info(self, candidates: list[dict], config: dict) -> dict | None: def _pick_change_wait_bind_info(
self, candidates: list[dict], config: dict
) -> dict | None:
"""换绑倒计时优先看活动当前绑定(QYOOB),不是 cjm 换绑最新态。""" """换绑倒计时优先看活动当前绑定(QYOOB),不是 cjm 换绑最新态。"""
if not candidates: if not candidates:
return None return None
@@ -1121,12 +1244,18 @@ class BindMixin:
return current_bound return current_bound
action_aliases = self._action_act_aliases(config) action_aliases = self._action_act_aliases(config)
return self._pick_bind_info( return self._pick_bind_info(
[info for info in candidates if str(info.get("act_alias") or "") in action_aliases], [
info
for info in candidates
if str(info.get("act_alias") or "") in action_aliases
],
prefer_pending=False, prefer_pending=False,
prefer_aliases=action_aliases, prefer_aliases=action_aliases,
) )
def _execute_query_change_bind_time(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): def _execute_query_change_bind_time(
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
):
client = self._client(cookie) client = self._client(cookie)
query_aliases = self._query_bind_act_aliases(config) query_aliases = self._query_bind_act_aliases(config)
if not query_aliases: if not query_aliases:
@@ -1134,7 +1263,9 @@ class BindMixin:
return return
candidates = self._fetch_bind_info_candidates(client, query_aliases) candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not candidates: if not candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") self._mark_task(
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
)
return return
for item in candidates: for item in candidates:
self._push_log( self._push_log(
@@ -1143,7 +1274,9 @@ class BindMixin:
f"{self._format_bind_summary(item)}", f"{self._format_bind_summary(item)}",
) )
# 换绑时间看“当前已绑定”活动态,不要优先 cjm(cjm 常无 changeRoleWaitTime # 换绑时间看“当前已绑定”活动态,不要优先 cjm(cjm 常无 changeRoleWaitTime
bind_info = self._pick_change_wait_bind_info(candidates, config) or candidates[0] bind_info = (
self._pick_change_wait_bind_info(candidates, config) or candidates[0]
)
snapshot = self._bind_snapshot(bind_info) snapshot = self._bind_snapshot(bind_info)
wait_time = snapshot["change_role_wait_time"] wait_time = snapshot["change_role_wait_time"]
account.change_role_wait_time = wait_time account.change_role_wait_time = wait_time
@@ -1171,7 +1304,8 @@ class BindMixin:
"query_act_aliases": query_aliases, "query_act_aliases": query_aliases,
**snapshot, **snapshot,
"change_role_wait_text": wait_text, "change_role_wait_text": wait_text,
"bind_ready_for_confirm": bool(snapshot["role_name"]) and not snapshot["is_bound_act"], "bind_ready_for_confirm": bool(snapshot["role_name"])
and not snapshot["is_bound_act"],
"bind_confirmed": snapshot["is_bound_act"], "bind_confirmed": snapshot["is_bound_act"],
"bind_summary": f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}", "bind_summary": f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}",
"bind_candidates": [ "bind_candidates": [
@@ -1179,7 +1313,9 @@ class BindMixin:
"act_alias": item.get("act_alias"), "act_alias": item.get("act_alias"),
"role_name": item.get("role_name"), "role_name": item.get("role_name"),
"is_bound_act": self._is_bound_act(item), "is_bound_act": self._is_bound_act(item),
"change_role_wait_time": self._to_int(item.get("change_role_wait_time")), "change_role_wait_time": self._to_int(
item.get("change_role_wait_time")
),
"can_change_role": item.get("can_change_role"), "can_change_role": item.get("can_change_role"),
} }
for item in candidates for item in candidates
@@ -1197,4 +1333,3 @@ class BindMixin:
f"{role_name} {status_text} | act={source_alias or '-'}", f"{role_name} {status_text} | act={source_alias or '-'}",
result, result,
) )
+59 -16
View File
@@ -3,14 +3,35 @@
from __future__ import annotations from __future__ import annotations
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, TYPE_CHECKING
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.huya import HuyaHttpClient from core.huya import HuyaHttpClient
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask
if TYPE_CHECKING:
from .huya_runner import HuyaBatchRunner
class GoodsMixin: class GoodsMixin:
"""积分与商城域:积分/兑换记录/商品刷新/兑换。""" """积分与商城域:积分/兑换记录/商品刷新/兑换。"""
if TYPE_CHECKING:
payload: dict
@staticmethod
def _to_int(value: Any) -> int: ...
@staticmethod
def _format_local_time(timestamp: int) -> str: ...
def _resolve_uid(self, account_info: dict) -> int: ...
def _push_log(self, level: str, message: str) -> None: ...
def _mark_task(self, *args: Any, **kwargs: Any) -> None: ...
def _wait_until(self, when: datetime, uid: int) -> bool: ...
def _parse_scheduled_time(self, value: Any) -> datetime | None: ...
def _execute_query_points( def _execute_query_points(
self, self,
worker_db: Session, worker_db: Session,
@@ -39,7 +60,9 @@ class GoodsMixin:
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
return return
client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) client: Any = HuyaHttpClient(
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
)
response = client.query_user_score(uid=uid, cookie=cookie, sid=sid_int) response = client.query_user_score(uid=uid, cookie=cookie, sid=sid_int)
if response is None: if response is None:
self._mark_task(worker_db, task, "error", "虎牙积分接口无响应") self._mark_task(worker_db, task, "error", "虎牙积分接口无响应")
@@ -91,7 +114,9 @@ class GoodsMixin:
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
return return
client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) client: Any = HuyaHttpClient(
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
)
response = client.get_user_prize_records(uid=uid, cookie=cookie, sid=sid_int) response = client.get_user_prize_records(uid=uid, cookie=cookie, sid=sid_int)
if response is None: if response is None:
self._mark_task(worker_db, task, "error", "虎牙兑换记录接口无响应") self._mark_task(worker_db, task, "error", "虎牙兑换记录接口无响应")
@@ -112,7 +137,9 @@ class GoodsMixin:
records = result.get("records", []) records = result.get("records", [])
for index, item in enumerate(records, start=1): for index, item in enumerate(records, start=1):
item["index"] = index item["index"] = index
item["exchange_time_text"] = self._format_local_time(int(item.get("exchange_time") or 0)) item["exchange_time_text"] = self._format_local_time(
int(item.get("exchange_time") or 0)
)
if item.get("score") is not None: if item.get("score") is not None:
item["score_text"] = f"{int(item.get('score') or 0)}积分" item["score_text"] = f"{int(item.get('score') or 0)}积分"
@@ -150,7 +177,9 @@ class GoodsMixin:
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
return return
client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) client: Any = HuyaHttpClient(
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
)
response = client.get_act_prize_list(uid=uid, cookie=cookie, sid=sid_int) response = client.get_act_prize_list(uid=uid, cookie=cookie, sid=sid_int)
if response is None: if response is None:
self._mark_task(worker_db, task, "error", "虎牙商品列表接口无响应") self._mark_task(worker_db, task, "error", "虎牙商品列表接口无响应")
@@ -169,20 +198,23 @@ class GoodsMixin:
return return
goods = [ goods = [
item for item in result.get("goods", []) item
for item in result.get("goods", [])
if item.get("product_id") and item.get("name") if item.get("product_id") and item.get("name")
] ]
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
worker_db.query(HuyaGoodsSnapshot).delete(synchronize_session=False) worker_db.query(HuyaGoodsSnapshot).delete(synchronize_session=False)
for item in goods: for item in goods:
worker_db.add(HuyaGoodsSnapshot( worker_db.add(
HuyaGoodsSnapshot(
product_id=item["product_id"], product_id=item["product_id"],
name=item["name"], name=item["name"],
price=item["price"], price=item["price"],
remain_text=item["remain_text"], remain_text=item["remain_text"],
raw=item, raw=item,
updated_at=now, updated_at=now,
)) )
)
account.status = "goods_refreshed" account.status = "goods_refreshed"
account.updated_at = now account.updated_at = now
@@ -222,10 +254,16 @@ class GoodsMixin:
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
return return
snapshot = worker_db.query(HuyaGoodsSnapshot).filter( snapshot = (
HuyaGoodsSnapshot.product_id == str(product_id) worker_db.query(HuyaGoodsSnapshot)
).first() .filter(HuyaGoodsSnapshot.product_id == str(product_id))
product_name = str(self.payload.get("product_name") or (snapshot.name if snapshot else "") or product_id) .first()
)
product_name = str(
self.payload.get("product_name")
or (snapshot.name if snapshot else "")
or product_id
)
scheduled_at = self._parse_scheduled_time(self.payload.get("scheduled_at")) scheduled_at = self._parse_scheduled_time(self.payload.get("scheduled_at"))
if self.payload.get("scheduled_at") and scheduled_at is None: if self.payload.get("scheduled_at") and scheduled_at is None:
self._mark_task(worker_db, task, "failed", "定时兑换时间格式无效") self._mark_task(worker_db, task, "failed", "定时兑换时间格式无效")
@@ -235,21 +273,27 @@ class GoodsMixin:
self._mark_task(worker_db, task, "stopped", "兑换任务已停止") self._mark_task(worker_db, task, "stopped", "兑换任务已停止")
return return
client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) client: Any = HuyaHttpClient(
response = client.score_exchange_prize(uid=uid, cookie=cookie, sid=sid_int, pid=product_id) logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
)
response = client.score_exchange_prize(
uid=uid, cookie=cookie, sid=sid_int, pid=product_id
)
if response is None: if response is None:
self._mark_task(worker_db, task, "error", "虎牙兑换接口无响应") self._mark_task(worker_db, task, "error", "虎牙兑换接口无响应")
return return
result = response.to_dict() result = response.to_dict()
result.update({ result.update(
{
"sid": sid_int, "sid": sid_int,
"product_id": str(product_id), "product_id": str(product_id),
"product_name": product_name, "product_name": product_name,
"scheduled_at": scheduled_at.isoformat() if scheduled_at else "", "scheduled_at": scheduled_at.isoformat() if scheduled_at else "",
"executed_at": datetime.now(timezone.utc).isoformat(), "executed_at": datetime.now(timezone.utc).isoformat(),
"goods": snapshot.raw if snapshot else None, "goods": snapshot.raw if snapshot else None,
}) }
)
if response.status != 200: if response.status != 200:
self._mark_task( self._mark_task(
worker_db, worker_db,
@@ -264,4 +308,3 @@ class GoodsMixin:
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
message = response.msg or f"兑换成功: {product_name}" message = response.msg or f"兑换成功: {product_name}"
self._mark_task(worker_db, task, "success", message, result) self._mark_task(worker_db, task, "success", message, result)