type: 收窄商城与斗鱼绑定执行器类型
This commit is contained in:
@@ -3,19 +3,40 @@
|
||||
from __future__ import annotations
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.douyu import DouyuActivityClient, DouyuActivityError
|
||||
from ..models import Account, DouyuTask
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .douyu_runner import DouyuBatchRunner
|
||||
|
||||
DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX"
|
||||
DOUYU_BIND_ROLE_POLL_SECONDS = 65
|
||||
DOUYU_BIND_ROLE_POLL_INTERVAL = 5
|
||||
DOUYU_CONFIRM_EFFECT_POLL_TIMES = 3
|
||||
DOUYU_CONFIRM_EFFECT_POLL_INTERVAL = 3
|
||||
|
||||
|
||||
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
|
||||
def _action_act_alias(config: dict, key: str) -> str:
|
||||
"""动作类接口用的活动 alias,排除只用于查询最新角色的 legacy alias。"""
|
||||
@@ -30,7 +51,9 @@ class BindMixin:
|
||||
@classmethod
|
||||
def _bind_qr_act_alias(cls, config: dict) -> str:
|
||||
"""生成绑定二维码用的活动 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
|
||||
def _query_bind_act_aliases(config: dict) -> list[str]:
|
||||
@@ -53,7 +76,9 @@ class BindMixin:
|
||||
@classmethod
|
||||
def _confirm_act_alias(cls, config: dict) -> str:
|
||||
"""确认绑定接口用的活动 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
|
||||
@@ -73,7 +98,9 @@ class BindMixin:
|
||||
@staticmethod
|
||||
def _role_channel(bind_info: dict) -> str:
|
||||
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
|
||||
@@ -177,7 +204,9 @@ class BindMixin:
|
||||
"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)
|
||||
role = snap["role_name"] or "-"
|
||||
area = snap["area_name"] or "-"
|
||||
@@ -197,20 +226,30 @@ class BindMixin:
|
||||
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 "")
|
||||
account.game_name = role_name or account.game_name
|
||||
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.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 "")
|
||||
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_change_role_wait_time = self._to_int(bind_info.get("change_role_wait_time"))
|
||||
account.esports_game_channel = (
|
||||
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_bind_status = status
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
@@ -235,7 +274,9 @@ class BindMixin:
|
||||
"has_selected_role": has_selected_role,
|
||||
"bind_ready_for_confirm": has_selected_role and not 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_snapshot": activity_snapshot,
|
||||
"role_source": "activity",
|
||||
@@ -302,7 +343,9 @@ class BindMixin:
|
||||
except ValueError:
|
||||
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]
|
||||
|
||||
if prefer_pending:
|
||||
@@ -337,12 +380,15 @@ class BindMixin:
|
||||
bound = [
|
||||
info
|
||||
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
|
||||
]
|
||||
if not bound:
|
||||
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(
|
||||
self,
|
||||
@@ -385,7 +431,9 @@ class BindMixin:
|
||||
f"aliases={','.join(query_aliases) or '-'} "
|
||||
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
|
||||
last_summary = ""
|
||||
@@ -396,17 +444,22 @@ class BindMixin:
|
||||
candidates = self._fetch_bind_info_candidates(client, query_aliases)
|
||||
if not candidates:
|
||||
result["bind_poll_error"] = "所有 actAlias 查询绑定信息失败"
|
||||
self._update_task_progress(db, task, "running", "等待绑定角色同步: 查询失败", result)
|
||||
self._update_task_progress(
|
||||
db, task, "running", "等待绑定角色同步: 查询失败", result
|
||||
)
|
||||
continue
|
||||
|
||||
poll_count += 1
|
||||
bind_info = self._pick_bind_info(
|
||||
bind_info = (
|
||||
self._pick_bind_info(
|
||||
candidates,
|
||||
baseline_role_name=baseline_role_name,
|
||||
baseline_is_bound_act=baseline_is_bound_act,
|
||||
prefer_pending=True,
|
||||
prefer_aliases=query_aliases,
|
||||
) or candidates[0]
|
||||
)
|
||||
or candidates[0]
|
||||
)
|
||||
snapshot = self._bind_snapshot(bind_info)
|
||||
is_pending_role = self._is_pending_role(
|
||||
bind_info,
|
||||
@@ -426,7 +479,8 @@ class BindMixin:
|
||||
# 注意:未识别到新角色时,不要把当前已绑定角色写进 role_name,
|
||||
# 否则前端会把旧角色误当成“待确认角色/查询结果”。
|
||||
if is_pending_role:
|
||||
result.update({
|
||||
result.update(
|
||||
{
|
||||
**snapshot,
|
||||
"act_alias": query_alias,
|
||||
"query_act_alias": query_alias,
|
||||
@@ -444,12 +498,15 @@ class BindMixin:
|
||||
}
|
||||
for item in candidates
|
||||
],
|
||||
})
|
||||
}
|
||||
)
|
||||
role_name = snapshot["role_name"]
|
||||
# 待确认角色只回传前端展示,不写入账号表,避免“未换绑成功但角色信息已变新”
|
||||
account.bind_status = "game_queried"
|
||||
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(
|
||||
db,
|
||||
task,
|
||||
@@ -464,7 +521,8 @@ class BindMixin:
|
||||
result["current_area_name"] = snapshot["area_name"]
|
||||
result["current_plat_name"] = snapshot["plat_name"]
|
||||
|
||||
result.update({
|
||||
result.update(
|
||||
{
|
||||
"bind_info": bind_info,
|
||||
"act_alias": query_alias,
|
||||
"query_act_alias": query_alias,
|
||||
@@ -484,8 +542,11 @@ class BindMixin:
|
||||
"bind_polling": True,
|
||||
"poll_count": poll_count,
|
||||
"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_ready_for_confirm"] = False
|
||||
@@ -499,7 +560,9 @@ class BindMixin:
|
||||
)
|
||||
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)
|
||||
qr_act_alias = self._bind_qr_act_alias(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)
|
||||
if not before_candidates:
|
||||
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
|
||||
self._mark_task(
|
||||
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
|
||||
)
|
||||
return
|
||||
# baseline 用活动 alias 的“当前已绑定”;pending 检测优先 cjm 的换绑最新态
|
||||
before = self._pick_baseline_bind_info(before_candidates, config) or {}
|
||||
# 换绑冷却必须看活动当前绑定(QYOOB),不要用 cjm
|
||||
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,
|
||||
baseline_role_name=str(before.get("role_name") or ""),
|
||||
baseline_is_bound_act=self._is_bound_act(before),
|
||||
prefer_pending=True,
|
||||
prefer_aliases=query_aliases,
|
||||
) or before or before_candidates[0]
|
||||
)
|
||||
or before
|
||||
or before_candidates[0]
|
||||
)
|
||||
before_snapshot = self._bind_snapshot(before)
|
||||
cooldown_snapshot = self._bind_snapshot(cooldown_info)
|
||||
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"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"pending_hit={pending_before.get('act_alias') or '-'} "
|
||||
f"{self._format_bind_summary(pending_before)}",
|
||||
@@ -543,33 +613,43 @@ class BindMixin:
|
||||
|
||||
# 生成二维码前强制检查换绑冷却:冷却中直接失败,绝不发码
|
||||
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 "冷却中"
|
||||
self._push_log(
|
||||
"warning",
|
||||
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 = {
|
||||
"act_alias": qr_act_alias or cooldown_info.get("act_alias") or before.get("act_alias"),
|
||||
"query_act_alias": cooldown_info.get("act_alias") or before.get("act_alias"),
|
||||
"act_alias": qr_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,
|
||||
"before_bind_info": before,
|
||||
"cooldown_bind_info": cooldown_info,
|
||||
**cooldown_snapshot,
|
||||
"current_role_name": role_label if role_label != "当前角色" 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"],
|
||||
"current_role_name": role_label
|
||||
if role_label != "当前角色"
|
||||
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_text": self._format_wait_time(wait_time),
|
||||
"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_polling": False,
|
||||
}
|
||||
self._apply_bind_info_to_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",
|
||||
)
|
||||
self._mark_task(
|
||||
@@ -610,7 +690,9 @@ class BindMixin:
|
||||
account.bind_status = "bind_qr_generated"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
# 关键:先把二维码 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(
|
||||
client,
|
||||
@@ -623,7 +705,9 @@ class BindMixin:
|
||||
baseline_is_bound_act=before_snapshot["is_bound_act"],
|
||||
)
|
||||
if role_name:
|
||||
self._mark_task(db, task, "success", f"已识别角色: {role_name},待确认绑定", result)
|
||||
self._mark_task(
|
||||
db, task, "success", f"已识别角色: {role_name},待确认绑定", result
|
||||
)
|
||||
return
|
||||
if result.get("bind_phase") == "stopped":
|
||||
self._mark_task(db, task, "stopped", "任务已停止", result)
|
||||
@@ -636,7 +720,9 @@ class BindMixin:
|
||||
timeout_msg = f"{timeout_msg} | {last_summary}"
|
||||
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)
|
||||
confirm_alias = self._confirm_act_alias(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)
|
||||
if not before_candidates:
|
||||
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
|
||||
self._mark_task(
|
||||
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
|
||||
)
|
||||
return
|
||||
before = self._pick_bind_info(
|
||||
before = (
|
||||
self._pick_bind_info(
|
||||
before_candidates,
|
||||
baseline_role_name="",
|
||||
baseline_is_bound_act=False,
|
||||
prefer_pending=True,
|
||||
prefer_aliases=query_aliases,
|
||||
) or before_candidates[0]
|
||||
)
|
||||
or before_candidates[0]
|
||||
)
|
||||
before_snapshot = self._bind_snapshot(before)
|
||||
role_name = before_snapshot["role_name"]
|
||||
query_alias = str(before.get("act_alias") or "")
|
||||
@@ -695,10 +786,11 @@ class BindMixin:
|
||||
|
||||
before_is_current_bound = (
|
||||
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 "")
|
||||
)
|
||||
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._mark_task(
|
||||
db,
|
||||
@@ -755,6 +847,7 @@ class BindMixin:
|
||||
f"确认绑定接口返回 act={use_confirm_alias} "
|
||||
f"error={confirm_raw.get('error')} msg={confirm_raw.get('msg') or '-'}",
|
||||
)
|
||||
|
||||
def _pick_bound_after(candidates: list[dict]) -> dict | None:
|
||||
"""确认后回查:只认活动 alias 上的已生效绑定(bound_act=1)。"""
|
||||
return self._pick_current_bound_info(
|
||||
@@ -773,7 +866,9 @@ class BindMixin:
|
||||
for _ in range(DOUYU_CONFIRM_EFFECT_POLL_TIMES):
|
||||
if self._stop.wait(DOUYU_CONFIRM_EFFECT_POLL_INTERVAL):
|
||||
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:
|
||||
break
|
||||
after = _pick_bound_after(after_candidates)
|
||||
@@ -782,7 +877,9 @@ class BindMixin:
|
||||
if after is 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:
|
||||
account.bind_status = "game_queried"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
@@ -815,7 +912,9 @@ class BindMixin:
|
||||
# 查询接口异常:确认接口已成功时按成功处理,但保留错误信息。
|
||||
# 写库优先确认前已生效绑定,避免待确认角色被误写入。
|
||||
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:
|
||||
account.bind_status = "bind_confirmed"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
@@ -882,7 +981,9 @@ class BindMixin:
|
||||
self._apply_esports_bind_info_to_account(
|
||||
account,
|
||||
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 返回结果决定。
|
||||
@@ -970,10 +1071,14 @@ class BindMixin:
|
||||
result = {
|
||||
**after_state,
|
||||
"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"]:
|
||||
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(
|
||||
db,
|
||||
task,
|
||||
@@ -1013,7 +1118,9 @@ class BindMixin:
|
||||
self._apply_esports_bind_info_to_account(
|
||||
account,
|
||||
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 = {
|
||||
**state,
|
||||
@@ -1026,7 +1133,9 @@ class BindMixin:
|
||||
message = "未查询到游戏角色,请先切换角色"
|
||||
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)
|
||||
query_aliases = self._query_bind_act_aliases(config)
|
||||
if not query_aliases:
|
||||
@@ -1034,24 +1143,33 @@ class BindMixin:
|
||||
return
|
||||
candidates = self._fetch_bind_info_candidates(client, query_aliases)
|
||||
if not candidates:
|
||||
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
|
||||
self._mark_task(
|
||||
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
|
||||
)
|
||||
return
|
||||
|
||||
# 1) 优先“已生效绑定”角色(bound_act=1 且有角色名,通常是活动 alias)。
|
||||
# 避免把扫码后未确认的新角色当成当前绑定结果。
|
||||
bound_info = self._pick_current_bound_info(candidates, config)
|
||||
# 2) 待确认角色(cjm 扫码后未确认;无已绑定时也用于首次绑定展示)
|
||||
pending_info = self._pick_bind_info(
|
||||
pending_info = (
|
||||
self._pick_bind_info(
|
||||
candidates,
|
||||
prefer_pending=True,
|
||||
prefer_aliases=query_aliases,
|
||||
) or candidates[0]
|
||||
)
|
||||
or candidates[0]
|
||||
)
|
||||
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)
|
||||
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 {}
|
||||
role_name = snapshot.get("role_name") or ""
|
||||
is_bound = snapshot.get("is_bound_act", False)
|
||||
@@ -1082,7 +1200,8 @@ class BindMixin:
|
||||
"bind_ready_for_confirm": has_pending,
|
||||
"bind_confirmed": is_bound,
|
||||
"bind_phase": (
|
||||
"confirmed" if is_bound
|
||||
"confirmed"
|
||||
if is_bound
|
||||
else ("role_ready" if role_name else "waiting_role")
|
||||
),
|
||||
"bind_summary": summary,
|
||||
@@ -1104,14 +1223,18 @@ class BindMixin:
|
||||
message = f"未获取到游戏名 | {summary}"
|
||||
elif is_bound:
|
||||
if has_pending and pending_role != bound_role:
|
||||
message = f"当前已绑定: {bound_role},待确认: {pending_role} | {summary}"
|
||||
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)
|
||||
|
||||
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 换绑最新态。"""
|
||||
if not candidates:
|
||||
return None
|
||||
@@ -1121,12 +1244,18 @@ class BindMixin:
|
||||
return current_bound
|
||||
action_aliases = self._action_act_aliases(config)
|
||||
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_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)
|
||||
query_aliases = self._query_bind_act_aliases(config)
|
||||
if not query_aliases:
|
||||
@@ -1134,7 +1263,9 @@ class BindMixin:
|
||||
return
|
||||
candidates = self._fetch_bind_info_candidates(client, query_aliases)
|
||||
if not candidates:
|
||||
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
|
||||
self._mark_task(
|
||||
db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias"
|
||||
)
|
||||
return
|
||||
for item in candidates:
|
||||
self._push_log(
|
||||
@@ -1143,7 +1274,9 @@ class BindMixin:
|
||||
f"{self._format_bind_summary(item)}",
|
||||
)
|
||||
# 换绑时间看“当前已绑定”活动态,不要优先 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)
|
||||
wait_time = snapshot["change_role_wait_time"]
|
||||
account.change_role_wait_time = wait_time
|
||||
@@ -1171,7 +1304,8 @@ class BindMixin:
|
||||
"query_act_aliases": query_aliases,
|
||||
**snapshot,
|
||||
"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_summary": f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}",
|
||||
"bind_candidates": [
|
||||
@@ -1179,7 +1313,9 @@ class BindMixin:
|
||||
"act_alias": item.get("act_alias"),
|
||||
"role_name": item.get("role_name"),
|
||||
"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"),
|
||||
}
|
||||
for item in candidates
|
||||
@@ -1197,4 +1333,3 @@ class BindMixin:
|
||||
f"{role_name} {status_text} | act={source_alias or '-'}",
|
||||
result,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,14 +3,35 @@
|
||||
from __future__ import annotations
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.huya import HuyaHttpClient
|
||||
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .huya_runner import HuyaBatchRunner
|
||||
|
||||
|
||||
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(
|
||||
self,
|
||||
worker_db: Session,
|
||||
@@ -39,7 +60,9 @@ class GoodsMixin:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
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)
|
||||
if response is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙积分接口无响应")
|
||||
@@ -91,7 +114,9 @@ class GoodsMixin:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
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)
|
||||
if response is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙兑换记录接口无响应")
|
||||
@@ -112,7 +137,9 @@ class GoodsMixin:
|
||||
records = result.get("records", [])
|
||||
for index, item in enumerate(records, start=1):
|
||||
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:
|
||||
item["score_text"] = f"{int(item.get('score') or 0)}积分"
|
||||
|
||||
@@ -150,7 +177,9 @@ class GoodsMixin:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
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)
|
||||
if response is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙商品列表接口无响应")
|
||||
@@ -169,20 +198,23 @@ class GoodsMixin:
|
||||
return
|
||||
|
||||
goods = [
|
||||
item for item in result.get("goods", [])
|
||||
item
|
||||
for item in result.get("goods", [])
|
||||
if item.get("product_id") and item.get("name")
|
||||
]
|
||||
now = datetime.now(timezone.utc)
|
||||
worker_db.query(HuyaGoodsSnapshot).delete(synchronize_session=False)
|
||||
for item in goods:
|
||||
worker_db.add(HuyaGoodsSnapshot(
|
||||
worker_db.add(
|
||||
HuyaGoodsSnapshot(
|
||||
product_id=item["product_id"],
|
||||
name=item["name"],
|
||||
price=item["price"],
|
||||
remain_text=item["remain_text"],
|
||||
raw=item,
|
||||
updated_at=now,
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
account.status = "goods_refreshed"
|
||||
account.updated_at = now
|
||||
@@ -222,10 +254,16 @@ class GoodsMixin:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
return
|
||||
|
||||
snapshot = worker_db.query(HuyaGoodsSnapshot).filter(
|
||||
HuyaGoodsSnapshot.product_id == str(product_id)
|
||||
).first()
|
||||
product_name = str(self.payload.get("product_name") or (snapshot.name if snapshot else "") or product_id)
|
||||
snapshot = (
|
||||
worker_db.query(HuyaGoodsSnapshot)
|
||||
.filter(HuyaGoodsSnapshot.product_id == str(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"))
|
||||
if self.payload.get("scheduled_at") and scheduled_at is None:
|
||||
self._mark_task(worker_db, task, "failed", "定时兑换时间格式无效")
|
||||
@@ -235,21 +273,27 @@ class GoodsMixin:
|
||||
self._mark_task(worker_db, task, "stopped", "兑换任务已停止")
|
||||
return
|
||||
|
||||
client = HuyaHttpClient(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)
|
||||
client: Any = HuyaHttpClient(
|
||||
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:
|
||||
self._mark_task(worker_db, task, "error", "虎牙兑换接口无响应")
|
||||
return
|
||||
|
||||
result = response.to_dict()
|
||||
result.update({
|
||||
result.update(
|
||||
{
|
||||
"sid": sid_int,
|
||||
"product_id": str(product_id),
|
||||
"product_name": product_name,
|
||||
"scheduled_at": scheduled_at.isoformat() if scheduled_at else "",
|
||||
"executed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"goods": snapshot.raw if snapshot else None,
|
||||
})
|
||||
}
|
||||
)
|
||||
if response.status != 200:
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
@@ -264,4 +308,3 @@ class GoodsMixin:
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
message = response.msg or f"兑换成功: {product_name}"
|
||||
self._mark_task(worker_db, task, "success", message, result)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user