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
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,
)
+59 -16
View File
@@ -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)