type: 收窄斗鱼商城与小店执行器类型
This commit is contained in:
@@ -4,29 +4,75 @@ from __future__ import annotations
|
|||||||
import random
|
import random
|
||||||
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, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask
|
from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .douyu_runner import DouyuBatchRunner
|
||||||
|
|
||||||
# 兑换节奏与重试(对齐 8.30 浏览器抓包:锁单->支付间隔约 2.2~4.2s;火爆类错误要长退避而不是秒级连打)
|
# 兑换节奏与重试(对齐 8.30 浏览器抓包:锁单->支付间隔约 2.2~4.2s;火爆类错误要长退避而不是秒级连打)
|
||||||
DOUYU_EXCHANGE_PRE_CREATE_JITTER = (0.3, 1.2)
|
DOUYU_EXCHANGE_PRE_CREATE_JITTER = (0.3, 1.2)
|
||||||
DOUYU_EXCHANGE_LOCK_PAY_DELAY_RANGE = (2.0, 4.0)
|
DOUYU_EXCHANGE_LOCK_PAY_DELAY_RANGE = (2.0, 4.0)
|
||||||
DOUYU_EXCHANGE_LOCK_TTL_SECONDS = 300
|
DOUYU_EXCHANGE_LOCK_TTL_SECONDS = 300
|
||||||
DOUYU_EXCHANGE_RATE_LIMIT_HINTS = ("火爆", "频繁", "稍后", "太热", "限流", "繁忙", "人多", "排队", "手慢")
|
DOUYU_EXCHANGE_RATE_LIMIT_HINTS = (
|
||||||
|
"火爆",
|
||||||
|
"频繁",
|
||||||
|
"稍后",
|
||||||
|
"太热",
|
||||||
|
"限流",
|
||||||
|
"繁忙",
|
||||||
|
"人多",
|
||||||
|
"排队",
|
||||||
|
"手慢",
|
||||||
|
)
|
||||||
DOUYU_EXCHANGE_RATE_LIMIT_BACKOFFS = (30, 60, 90)
|
DOUYU_EXCHANGE_RATE_LIMIT_BACKOFFS = (30, 60, 90)
|
||||||
DOUYU_EXCHANGE_GENERIC_BACKOFFS = (2, 5, 10, 20)
|
DOUYU_EXCHANGE_GENERIC_BACKOFFS = (2, 5, 10, 20)
|
||||||
|
|
||||||
|
|
||||||
class GoodsMixin:
|
class GoodsMixin:
|
||||||
"""商城兑换域:商品刷新、锁单/支付/兑换、兑换节奏与退避重试。"""
|
"""商城兑换域:商品刷新、锁单/支付/兑换、兑换节奏与退避重试。"""
|
||||||
def _execute_refresh_goods(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_stop: Any
|
||||||
|
|
||||||
|
def _client(self, cookie: str) -> DouyuActivityClient: ...
|
||||||
|
def _task_payload(self, task: DouyuTask) -> dict: ...
|
||||||
|
def _upsert_goods(self, db: Session, goods: list[dict]) -> None: ...
|
||||||
|
def _upsert_esports_goods(self, db: Session, goods: list[dict]) -> None: ...
|
||||||
|
def _refresh_account_points(
|
||||||
|
self,
|
||||||
|
client: DouyuActivityClient,
|
||||||
|
account: Account,
|
||||||
|
cookie: str,
|
||||||
|
*,
|
||||||
|
ctn: str = "",
|
||||||
|
) -> dict: ...
|
||||||
|
def _refresh_esports_handbook(
|
||||||
|
self, client: DouyuActivityClient, account: Account, *, manual_id: str
|
||||||
|
) -> dict: ...
|
||||||
|
def _sleep_interruptible(self, seconds: float) -> bool: ...
|
||||||
|
def _push_log(self, level: str, message: str) -> None: ...
|
||||||
|
def _mark_task(self, *args: Any, **kwargs: Any) -> None: ...
|
||||||
|
|
||||||
|
def _execute_refresh_goods(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
client = self._client(cookie)
|
client = self._client(cookie)
|
||||||
result = client.list_goods(manual_id=config["manual_id"], rid=config["rid"])
|
result = client.list_goods(manual_id=config["manual_id"], rid=config["rid"])
|
||||||
goods = result["goods"]
|
goods = result["goods"]
|
||||||
self._upsert_goods(db, goods)
|
self._upsert_goods(db, goods)
|
||||||
account.bind_status = account.bind_status or "active"
|
account.bind_status = account.bind_status or "active"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(db, task, "success", f"已刷新商品 {len(goods)} 个", {"goods_count": len(goods), "goods": goods})
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"success",
|
||||||
|
f"已刷新商品 {len(goods)} 个",
|
||||||
|
{"goods_count": len(goods), "goods": goods},
|
||||||
|
)
|
||||||
|
|
||||||
def _execute_refresh_esports_goods(
|
def _execute_refresh_esports_goods(
|
||||||
self,
|
self,
|
||||||
@@ -51,12 +97,20 @@ class GoodsMixin:
|
|||||||
task,
|
task,
|
||||||
"success",
|
"success",
|
||||||
f"已刷新电竞皮肤 {len(goods)} 个",
|
f"已刷新电竞皮肤 {len(goods)} 个",
|
||||||
{"goods_count": len(goods), "esports_store_score": result["score"], "goods": goods},
|
{
|
||||||
|
"goods_count": len(goods),
|
||||||
|
"esports_store_score": result["score"],
|
||||||
|
"goods": goods,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _execute_lock_goods(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
def _execute_lock_goods(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
payload = self._task_payload(task)
|
payload = self._task_payload(task)
|
||||||
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
|
commodity_id = str(
|
||||||
|
payload.get("commodity_id") or payload.get("commodityId") or ""
|
||||||
|
).strip()
|
||||||
if not commodity_id:
|
if not commodity_id:
|
||||||
self._mark_task(db, task, "failed", "请选择锁定商品")
|
self._mark_task(db, task, "failed", "请选择锁定商品")
|
||||||
return
|
return
|
||||||
@@ -89,7 +143,10 @@ class GoodsMixin:
|
|||||||
"goods": goods.raw if goods else None,
|
"goods": goods.raw if goods else None,
|
||||||
"game_name": account.game_name or "",
|
"game_name": account.game_name or "",
|
||||||
"game_channel": account.game_channel or "",
|
"game_channel": account.game_channel or "",
|
||||||
"account_name": account.nickname or account.username or account.uid or f"#{account.id}",
|
"account_name": account.nickname
|
||||||
|
or account.username
|
||||||
|
or account.uid
|
||||||
|
or f"#{account.id}",
|
||||||
**result,
|
**result,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -104,7 +161,9 @@ class GoodsMixin:
|
|||||||
):
|
):
|
||||||
payload = self._task_payload(task)
|
payload = self._task_payload(task)
|
||||||
order_id = str(payload.get("order_id") or payload.get("orderId") or "").strip()
|
order_id = str(payload.get("order_id") or payload.get("orderId") or "").strip()
|
||||||
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
|
commodity_id = str(
|
||||||
|
payload.get("commodity_id") or payload.get("commodityId") or ""
|
||||||
|
).strip()
|
||||||
if not order_id:
|
if not order_id:
|
||||||
self._mark_task(db, task, "failed", "锁单订单号不能为空")
|
self._mark_task(db, task, "failed", "锁单订单号不能为空")
|
||||||
return
|
return
|
||||||
@@ -133,11 +192,16 @@ class GoodsMixin:
|
|||||||
"goods": goods.raw if goods else None,
|
"goods": goods.raw if goods else None,
|
||||||
"game_name": account.game_name or "",
|
"game_name": account.game_name or "",
|
||||||
"game_channel": account.game_channel or "",
|
"game_channel": account.game_channel or "",
|
||||||
"account_name": account.nickname or account.username or account.uid or f"#{account.id}",
|
"account_name": account.nickname
|
||||||
|
or account.username
|
||||||
|
or account.uid
|
||||||
|
or f"#{account.id}",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
ctn = client.acf_ccn(refresh_subscribe=False)
|
ctn = client.acf_ccn(refresh_subscribe=False)
|
||||||
result.update(self._refresh_account_points(client, account, cookie, ctn=ctn))
|
result.update(
|
||||||
|
self._refresh_account_points(client, account, cookie, ctn=ctn)
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._push_log("warning", f"支付锁单后刷新积分失败: {exc}")
|
self._push_log("warning", f"支付锁单后刷新积分失败: {exc}")
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
@@ -206,9 +270,13 @@ class GoodsMixin:
|
|||||||
return None
|
return None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _execute_exchange_goods(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
def _execute_exchange_goods(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
payload = self._task_payload(task)
|
payload = self._task_payload(task)
|
||||||
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
|
commodity_id = str(
|
||||||
|
payload.get("commodity_id") or payload.get("commodityId") or ""
|
||||||
|
).strip()
|
||||||
if not commodity_id:
|
if not commodity_id:
|
||||||
self._mark_task(db, task, "failed", "请选择兑换商品")
|
self._mark_task(db, task, "failed", "请选择兑换商品")
|
||||||
return
|
return
|
||||||
@@ -226,7 +294,9 @@ class GoodsMixin:
|
|||||||
self._push_log("warning", f"查询手册状态失败,按经典流程继续: {exc}")
|
self._push_log("warning", f"查询手册状态失败,按经典流程继续: {exc}")
|
||||||
detail = None
|
detail = None
|
||||||
try:
|
try:
|
||||||
detail = client.query_goods_detail(manual_id=manual_id, commodity_id=commodity_id, rid=rid)["detail"]
|
detail = client.query_goods_detail(
|
||||||
|
manual_id=manual_id, commodity_id=commodity_id, rid=rid
|
||||||
|
)["detail"]
|
||||||
except DouyuActivityError as exc:
|
except DouyuActivityError as exc:
|
||||||
self._push_log("warning", f"查询商品详情失败,按经典流程继续: {exc}")
|
self._push_log("warning", f"查询商品详情失败,按经典流程继续: {exc}")
|
||||||
|
|
||||||
@@ -249,7 +319,10 @@ class GoodsMixin:
|
|||||||
|
|
||||||
def finish_failed(message: str) -> None:
|
def finish_failed(message: str) -> None:
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
db, task, "failed", message,
|
db,
|
||||||
|
task,
|
||||||
|
"failed",
|
||||||
|
message,
|
||||||
{"commodity_id": commodity_id, "plan": plan, "detail": detail},
|
{"commodity_id": commodity_id, "plan": plan, "detail": detail},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -266,37 +339,63 @@ class GoodsMixin:
|
|||||||
|
|
||||||
if action == "subscribe":
|
if action == "subscribe":
|
||||||
try:
|
try:
|
||||||
client.subscribe_commodity(manual_id=manual_id, commodity_id=commodity_id)
|
client.subscribe_commodity(
|
||||||
|
manual_id=manual_id, commodity_id=commodity_id
|
||||||
|
)
|
||||||
except DouyuActivityError as exc:
|
except DouyuActivityError as exc:
|
||||||
finish_failed(f"预约到货失败: {exc}")
|
finish_failed(f"预约到货失败: {exc}")
|
||||||
return
|
return
|
||||||
account.bind_status = "goods_subscribed"
|
account.bind_status = "goods_subscribed"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
db, task, "success", f"已预约到货: {name} ({plan['text']})",
|
db,
|
||||||
|
task,
|
||||||
|
"success",
|
||||||
|
f"已预约到货: {name} ({plan['text']})",
|
||||||
{"commodity_id": commodity_id, "plan": plan, "detail": detail},
|
{"commodity_id": commodity_id, "plan": plan, "detail": detail},
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if action == "pre_exchange":
|
if action == "pre_exchange":
|
||||||
try:
|
try:
|
||||||
check = client.pre_exchange_check(manual_id=manual_id, commodity_id=commodity_id)
|
check = client.pre_exchange_check(
|
||||||
|
manual_id=manual_id, commodity_id=commodity_id
|
||||||
|
)
|
||||||
user_status = int((check.get("data") or {}).get("userStatus") or 0)
|
user_status = int((check.get("data") or {}).get("userStatus") or 0)
|
||||||
if user_status == 1 and not plan["pre_exchange"]:
|
if user_status == 1 and not plan["pre_exchange"]:
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
db, task, "success", f"已预兑: {name},等待开放后继续兑换",
|
db,
|
||||||
{"commodity_id": commodity_id, "plan": plan, "detail": detail, "check": check},
|
task,
|
||||||
|
"success",
|
||||||
|
f"已预兑: {name},等待开放后继续兑换",
|
||||||
|
{
|
||||||
|
"commodity_id": commodity_id,
|
||||||
|
"plan": plan,
|
||||||
|
"detail": detail,
|
||||||
|
"check": check,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
confirm = client.confirm_pre_exchange(manual_id=manual_id, commodity_id=commodity_id, rid=rid)
|
confirm = client.confirm_pre_exchange(
|
||||||
|
manual_id=manual_id, commodity_id=commodity_id, rid=rid
|
||||||
|
)
|
||||||
except DouyuActivityError as exc:
|
except DouyuActivityError as exc:
|
||||||
finish_failed(f"预兑失败: {exc}")
|
finish_failed(f"预兑失败: {exc}")
|
||||||
return
|
return
|
||||||
account.bind_status = "goods_pre_exchanged"
|
account.bind_status = "goods_pre_exchanged"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
db, task, "success", f"预兑成功: {name},开放后可继续兑换",
|
db,
|
||||||
{"commodity_id": commodity_id, "plan": plan, "detail": detail, "check": check, "confirm": confirm},
|
task,
|
||||||
|
"success",
|
||||||
|
f"预兑成功: {name},开放后可继续兑换",
|
||||||
|
{
|
||||||
|
"commodity_id": commodity_id,
|
||||||
|
"plan": plan,
|
||||||
|
"detail": detail,
|
||||||
|
"check": check,
|
||||||
|
"confirm": confirm,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -309,7 +408,9 @@ class GoodsMixin:
|
|||||||
|
|
||||||
# 少量随机抖动(浏览器点击商品到锁单之间有人工间隔)
|
# 少量随机抖动(浏览器点击商品到锁单之间有人工间隔)
|
||||||
self._push_log("info", f" 兑换 {name}: {plan['text']}")
|
self._push_log("info", f" 兑换 {name}: {plan['text']}")
|
||||||
if not self._sleep_interruptible(random.uniform(*DOUYU_EXCHANGE_PRE_CREATE_JITTER)):
|
if not self._sleep_interruptible(
|
||||||
|
random.uniform(*DOUYU_EXCHANGE_PRE_CREATE_JITTER)
|
||||||
|
):
|
||||||
self._mark_task(db, task, "stopped", "任务已停止", result)
|
self._mark_task(db, task, "stopped", "任务已停止", result)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -321,8 +422,14 @@ class GoodsMixin:
|
|||||||
)
|
)
|
||||||
except DouyuActivityError as exc:
|
except DouyuActivityError as exc:
|
||||||
# 锁单本身被频控时做一次长退避重试,避免无脑连打
|
# 锁单本身被频控时做一次长退避重试,避免无脑连打
|
||||||
backoff = DOUYU_EXCHANGE_RATE_LIMIT_BACKOFFS[0] if self._is_rate_limited_error(str(exc)) else 3
|
backoff = (
|
||||||
self._push_log("info", f" 锁定兑换商品失败({exc}),{backoff:.0f}s 后重试一次")
|
DOUYU_EXCHANGE_RATE_LIMIT_BACKOFFS[0]
|
||||||
|
if self._is_rate_limited_error(str(exc))
|
||||||
|
else 3
|
||||||
|
)
|
||||||
|
self._push_log(
|
||||||
|
"info", f" 锁定兑换商品失败({exc}),{backoff:.0f}s 后重试一次"
|
||||||
|
)
|
||||||
if not self._sleep_interruptible(backoff):
|
if not self._sleep_interruptible(backoff):
|
||||||
self._mark_task(db, task, "stopped", "任务已停止", result)
|
self._mark_task(db, task, "stopped", "任务已停止", result)
|
||||||
return
|
return
|
||||||
@@ -336,13 +443,19 @@ class GoodsMixin:
|
|||||||
except DouyuActivityError as exc2:
|
except DouyuActivityError as exc2:
|
||||||
finish_failed(f"锁定兑换商品失败: {exc2}")
|
finish_failed(f"锁定兑换商品失败: {exc2}")
|
||||||
return
|
return
|
||||||
result.update({**locked, "lock_order": locked, "goods": goods.raw if goods else None})
|
result.update(
|
||||||
|
{**locked, "lock_order": locked, "goods": goods.raw if goods else None}
|
||||||
|
)
|
||||||
|
|
||||||
# 锁单成功后按浏览器节奏(抓包实测 2.24s~4.2s)等待再支付
|
# 锁单成功后按浏览器节奏(抓包实测 2.24s~4.2s)等待再支付
|
||||||
delay = random.uniform(*DOUYU_EXCHANGE_LOCK_PAY_DELAY_RANGE)
|
delay = random.uniform(*DOUYU_EXCHANGE_LOCK_PAY_DELAY_RANGE)
|
||||||
self._push_log("info", f" 锁单成功(订单 {locked['order_id']}),{delay:.1f}s 后支付")
|
self._push_log(
|
||||||
|
"info", f" 锁单成功(订单 {locked['order_id']}),{delay:.1f}s 后支付"
|
||||||
|
)
|
||||||
if not self._sleep_interruptible(delay):
|
if not self._sleep_interruptible(delay):
|
||||||
self._mark_task(db, task, "stopped", "任务已停止,商品锁单仍可能有效", locked)
|
self._mark_task(
|
||||||
|
db, task, "stopped", "任务已停止,商品锁单仍可能有效", locked
|
||||||
|
)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
payment = self._pay_exchange_with_backoff(
|
payment = self._pay_exchange_with_backoff(
|
||||||
@@ -356,23 +469,30 @@ class GoodsMixin:
|
|||||||
finish_failed(str(exc))
|
finish_failed(str(exc))
|
||||||
return
|
return
|
||||||
if payment is None:
|
if payment is None:
|
||||||
self._mark_task(db, task, "stopped", "任务已停止,商品锁单仍可能有效", locked)
|
self._mark_task(
|
||||||
|
db, task, "stopped", "任务已停止,商品锁单仍可能有效", locked
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
result.update({
|
result.update(
|
||||||
|
{
|
||||||
"order_id": locked["order_id"],
|
"order_id": locked["order_id"],
|
||||||
"exchange_id": payment["exchange_id"],
|
"exchange_id": payment["exchange_id"],
|
||||||
"commodity_image": payment["commodity_image"] or locked["commodity_image"],
|
"commodity_image": payment["commodity_image"]
|
||||||
|
or locked["commodity_image"],
|
||||||
"exchange_num": payment["exchange_num"],
|
"exchange_num": payment["exchange_num"],
|
||||||
"payment": payment,
|
"payment": payment,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
account.bind_status = "goods_exchanged"
|
account.bind_status = "goods_exchanged"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
# 兑换成功后自动刷新积分,更新账号最新积分信息(失败不阻断兑换成功)
|
# 兑换成功后自动刷新积分,更新账号最新积分信息(失败不阻断兑换成功)
|
||||||
points_refresh = None
|
points_refresh = None
|
||||||
try:
|
try:
|
||||||
ctn = client.acf_ccn(refresh_subscribe=False)
|
ctn = client.acf_ccn(refresh_subscribe=False)
|
||||||
points_refresh = self._refresh_account_points(client, account, cookie, ctn=ctn)
|
points_refresh = self._refresh_account_points(
|
||||||
|
client, account, cookie, ctn=ctn
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._push_log("warning", f"兑换后刷新积分失败: {exc}")
|
self._push_log("warning", f"兑换后刷新积分失败: {exc}")
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
@@ -384,7 +504,10 @@ class GoodsMixin:
|
|||||||
"goods": goods.raw if goods else None,
|
"goods": goods.raw if goods else None,
|
||||||
"game_name": account.game_name or "",
|
"game_name": account.game_name or "",
|
||||||
"game_channel": account.game_channel or "",
|
"game_channel": account.game_channel or "",
|
||||||
"account_name": account.nickname or account.username or account.uid or f"#{account.id}",
|
"account_name": account.nickname
|
||||||
|
or account.username
|
||||||
|
or account.uid
|
||||||
|
or f"#{account.id}",
|
||||||
**result,
|
**result,
|
||||||
**(points_refresh or {}),
|
**(points_refresh or {}),
|
||||||
},
|
},
|
||||||
@@ -400,7 +523,9 @@ class GoodsMixin:
|
|||||||
):
|
):
|
||||||
"""兑换电竞手册皮肤,并同步电竞积分。"""
|
"""兑换电竞手册皮肤,并同步电竞积分。"""
|
||||||
payload = self._task_payload(task)
|
payload = self._task_payload(task)
|
||||||
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
|
commodity_id = str(
|
||||||
|
payload.get("commodity_id") or payload.get("commodityId") or ""
|
||||||
|
).strip()
|
||||||
if not commodity_id:
|
if not commodity_id:
|
||||||
self._mark_task(db, task, "failed", "请选择电竞皮肤")
|
self._mark_task(db, task, "failed", "请选择电竞皮肤")
|
||||||
return
|
return
|
||||||
@@ -419,7 +544,9 @@ class GoodsMixin:
|
|||||||
client = self._client(cookie)
|
client = self._client(cookie)
|
||||||
baseline_points = account.esports_points
|
baseline_points = account.esports_points
|
||||||
try:
|
try:
|
||||||
baseline = self._refresh_esports_handbook(client, account, manual_id=manual_id)
|
baseline = self._refresh_esports_handbook(
|
||||||
|
client, account, manual_id=manual_id
|
||||||
|
)
|
||||||
baseline_points = baseline["esports_points"]
|
baseline_points = baseline["esports_points"]
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -439,7 +566,9 @@ class GoodsMixin:
|
|||||||
result["goods"] = goods.raw if goods else None
|
result["goods"] = goods.raw if goods else None
|
||||||
result["esports_points_baseline"] = baseline_points
|
result["esports_points_baseline"] = baseline_points
|
||||||
try:
|
try:
|
||||||
points_result = self._refresh_esports_handbook(client, account, manual_id=manual_id)
|
points_result = self._refresh_esports_handbook(
|
||||||
|
client, account, manual_id=manual_id
|
||||||
|
)
|
||||||
result.update(points_result)
|
result.update(points_result)
|
||||||
result["esports_points_after_exchange"] = points_result["esports_points"]
|
result["esports_points_after_exchange"] = points_result["esports_points"]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -455,31 +584,66 @@ class GoodsMixin:
|
|||||||
message += f",电竞积分: {account.esports_points}"
|
message += f",电竞积分: {account.esports_points}"
|
||||||
result["game_name"] = account.esports_game_name or ""
|
result["game_name"] = account.esports_game_name or ""
|
||||||
result["game_channel"] = account.esports_game_channel or ""
|
result["game_channel"] = account.esports_game_channel or ""
|
||||||
result["account_name"] = account.nickname or account.username or account.uid or f"#{account.id}"
|
result["account_name"] = (
|
||||||
|
account.nickname or account.username or account.uid or f"#{account.id}"
|
||||||
|
)
|
||||||
self._mark_task(db, task, "success", message, result)
|
self._mark_task(db, task, "success", message, result)
|
||||||
|
|
||||||
def _execute_query_limited_goods(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
def _execute_query_limited_goods(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
client = self._client(cookie)
|
client = self._client(cookie)
|
||||||
result = client.query_limited_goods(manual_id=str(config["manual_id"]), rid=str(config["rid"]))
|
result = client.query_limited_goods(
|
||||||
|
manual_id=str(config["manual_id"]), rid=str(config["rid"])
|
||||||
|
)
|
||||||
limited = result["limited_goods"]
|
limited = result["limited_goods"]
|
||||||
names = [str(item.get("commodityName") or "") for item in limited if item.get("commodityName")]
|
names = [
|
||||||
message = "无限制商品" if not names else f"限兑 {len(names)} 个: {', '.join(names[:5])}"
|
str(item.get("commodityName") or "")
|
||||||
|
for item in limited
|
||||||
|
if item.get("commodityName")
|
||||||
|
]
|
||||||
|
message = (
|
||||||
|
"无限制商品"
|
||||||
|
if not names
|
||||||
|
else f"限兑 {len(names)} 个: {', '.join(names[:5])}"
|
||||||
|
)
|
||||||
account.bind_status = "limited_goods_queried"
|
account.bind_status = "limited_goods_queried"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(db, task, "success", message, {"limited_count": len(limited), "limited_goods": limited})
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"success",
|
||||||
|
message,
|
||||||
|
{"limited_count": len(limited), "limited_goods": limited},
|
||||||
|
)
|
||||||
|
|
||||||
def _execute_query_exchange_records(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
def _execute_query_exchange_records(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
client = self._client(cookie)
|
client = self._client(cookie)
|
||||||
result = client.exchange_records(manual_id=str(config["manual_id"]))
|
result = client.exchange_records(manual_id=str(config["manual_id"]))
|
||||||
records = result["records"]
|
records = result["records"]
|
||||||
account.bind_status = "exchange_records_queried"
|
account.bind_status = "exchange_records_queried"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(db, task, "success", f"兑换记录 {len(records)} 条" if records else "暂无兑换记录", result)
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"success",
|
||||||
|
f"兑换记录 {len(records)} 条" if records else "暂无兑换记录",
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
|
||||||
def _execute_prefetch_csrf_token(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
def _execute_prefetch_csrf_token(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
client = self._client(cookie)
|
client = self._client(cookie)
|
||||||
token = client.csrf_token()
|
token = client.csrf_token()
|
||||||
account.bind_status = "csrf_token_ready"
|
account.bind_status = "csrf_token_ready"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(db, task, "success", "获取 csrf_token 成功", {"csrf_token": token, "cookie": client.cookie})
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"success",
|
||||||
|
"获取 csrf_token 成功",
|
||||||
|
{"csrf_token": token, "cookie": client.cookie},
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,16 +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, cast
|
||||||
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, DouyuXpdGoodsSnapshot
|
from ..models import Account, DouyuTask, DouyuXpdGoodsSnapshot
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .douyu_runner import DouyuBatchRunner
|
||||||
|
|
||||||
DOUYU_XPD_BIND_POLL_SECONDS = 300
|
DOUYU_XPD_BIND_POLL_SECONDS = 300
|
||||||
DOUYU_XPD_BIND_POLL_INTERVAL = 5
|
DOUYU_XPD_BIND_POLL_INTERVAL = 5
|
||||||
|
|
||||||
|
|
||||||
class XpdMixin:
|
class XpdMixin:
|
||||||
"""和平小店域:绑定、商品、余额、碎片与兑换。"""
|
"""和平小店域:绑定、商品、余额、碎片与兑换。"""
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_stop: Any
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_int(value: Any) -> int | None: ...
|
||||||
|
|
||||||
|
def _client(self, cookie: str) -> DouyuActivityClient: ...
|
||||||
|
def _task_payload(self, task: DouyuTask) -> dict: ...
|
||||||
|
def _upsert_xpd_goods(self, db: Session, goods: list[dict]) -> None: ...
|
||||||
|
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 _xpd_role_context(self, client: DouyuActivityClient, config: dict) -> dict:
|
def _xpd_role_context(self, client: DouyuActivityClient, config: dict) -> dict:
|
||||||
"""获取小店 H5 参数 + 绑定角色信息,小店任务共用。"""
|
"""获取小店 H5 参数 + 绑定角色信息,小店任务共用。"""
|
||||||
embed = client.xpd_embed_query(
|
embed = client.xpd_embed_query(
|
||||||
@@ -43,8 +62,12 @@ class XpdMixin:
|
|||||||
return 2
|
return 2
|
||||||
return account.xpd_area_id or 1
|
return account.xpd_area_id or 1
|
||||||
|
|
||||||
def _apply_xpd_role_to_account(self, account: Account, role: dict, area_id: int) -> None:
|
def _apply_xpd_role_to_account(
|
||||||
account.xpd_game_name = str(role.get("role_name") or "") or account.xpd_game_name
|
self, account: Account, role: dict, area_id: int
|
||||||
|
) -> None:
|
||||||
|
account.xpd_game_name = (
|
||||||
|
str(role.get("role_name") or "") or account.xpd_game_name
|
||||||
|
)
|
||||||
account.xpd_openid = str(role.get("game_open_id") or "") or account.xpd_openid
|
account.xpd_openid = str(role.get("game_open_id") or "") or account.xpd_openid
|
||||||
account.xpd_role_id = str(role.get("role_id") or "") or account.xpd_role_id
|
account.xpd_role_id = str(role.get("role_id") or "") or account.xpd_role_id
|
||||||
account.xpd_plat_id = self._to_int(role.get("plat_id"))
|
account.xpd_plat_id = self._to_int(role.get("plat_id"))
|
||||||
@@ -71,7 +94,11 @@ class XpdMixin:
|
|||||||
account.xpd_bind_status = "xpd_bound"
|
account.xpd_bind_status = "xpd_bound"
|
||||||
db.commit()
|
db.commit()
|
||||||
role_text = str(role.get("role_name") or "-")
|
role_text = str(role.get("role_name") or "-")
|
||||||
channel = "微信" if role.get("type") == "wx" else ("手Q" if role.get("type") == "qq" else str(role.get("type") or "-"))
|
channel = (
|
||||||
|
"微信"
|
||||||
|
if role.get("type") == "wx"
|
||||||
|
else ("手Q" if role.get("type") == "qq" else str(role.get("type") or "-"))
|
||||||
|
)
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
db,
|
db,
|
||||||
task,
|
task,
|
||||||
@@ -126,7 +153,9 @@ class XpdMixin:
|
|||||||
return
|
return
|
||||||
if state == "pending":
|
if state == "pending":
|
||||||
role_text = str(result.get("role_name") or "-")
|
role_text = str(result.get("role_name") or "-")
|
||||||
self._mark_task(db, task, "success", f"已识别角色: {role_text},待确认绑定", result)
|
self._mark_task(
|
||||||
|
db, task, "success", f"已识别角色: {role_text},待确认绑定", result
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
db,
|
db,
|
||||||
@@ -162,9 +191,13 @@ class XpdMixin:
|
|||||||
result["bind_polling"] = True
|
result["bind_polling"] = True
|
||||||
role_name = str(info.get("role_name") or "")
|
role_name = str(info.get("role_name") or "")
|
||||||
bound_now = bool(info.get("bind_role"))
|
bound_now = bool(info.get("bind_role"))
|
||||||
changed = before_bound and bool(role_name) and role_name != before_role_name
|
changed = (
|
||||||
|
before_bound and bool(role_name) and role_name != before_role_name
|
||||||
|
)
|
||||||
if (not before_bound and bound_now and role_name) or changed:
|
if (not before_bound and bound_now and role_name) or changed:
|
||||||
result.update({key: value for key, value in info.items() if key != "raw"})
|
result.update(
|
||||||
|
{key: value for key, value in info.items() if key != "raw"}
|
||||||
|
)
|
||||||
result["bind_polling"] = False
|
result["bind_polling"] = False
|
||||||
result["xpd_pending_confirm"] = True
|
result["xpd_pending_confirm"] = True
|
||||||
return "pending"
|
return "pending"
|
||||||
@@ -211,14 +244,18 @@ class XpdMixin:
|
|||||||
account.xpd_bind_status = "xpd_not_bound"
|
account.xpd_bind_status = "xpd_not_bound"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
self._mark_task(db, task, "failed", "尚未检测到小店绑定角色,请先扫码绑定", result)
|
self._mark_task(
|
||||||
|
db, task, "failed", "尚未检测到小店绑定角色,请先扫码绑定", result
|
||||||
|
)
|
||||||
return
|
return
|
||||||
# 优先用完整角色信息回写(与查询角色一致),失败时回退 bindInfo 角色名
|
# 优先用完整角色信息回写(与查询角色一致),失败时回退 bindInfo 角色名
|
||||||
try:
|
try:
|
||||||
ctx = self._xpd_role_context(client, config)
|
ctx = self._xpd_role_context(client, config)
|
||||||
role = ctx["role"]
|
role = ctx["role"]
|
||||||
if role.get("role_id"):
|
if role.get("role_id"):
|
||||||
self._apply_xpd_role_to_account(account, role, self._xpd_area_id(role, account))
|
self._apply_xpd_role_to_account(
|
||||||
|
account, role, self._xpd_area_id(role, account)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
account.xpd_game_name = role_name
|
account.xpd_game_name = role_name
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
@@ -375,7 +412,9 @@ class XpdMixin:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if not openid or not roleid:
|
if not openid or not roleid:
|
||||||
self._mark_task(db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定")
|
self._mark_task(
|
||||||
|
db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
result = client.xpd_fragments(
|
result = client.xpd_fragments(
|
||||||
embed_query=embed_query,
|
embed_query=embed_query,
|
||||||
@@ -440,7 +479,9 @@ class XpdMixin:
|
|||||||
):
|
):
|
||||||
"""兑换和平小店商品,使用本次签发的道聚城短时授权。"""
|
"""兑换和平小店商品,使用本次签发的道聚城短时授权。"""
|
||||||
payload = self._task_payload(task)
|
payload = self._task_payload(task)
|
||||||
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
|
commodity_id = str(
|
||||||
|
payload.get("commodity_id") or payload.get("commodityId") or ""
|
||||||
|
).strip()
|
||||||
if not commodity_id:
|
if not commodity_id:
|
||||||
self._mark_task(db, task, "failed", "请选择小店商品")
|
self._mark_task(db, task, "failed", "请选择小店商品")
|
||||||
return
|
return
|
||||||
@@ -462,7 +503,12 @@ class XpdMixin:
|
|||||||
self._mark_task(db, task, "failed", "未找到小店商品快照,请先刷新商品列表")
|
self._mark_task(db, task, "failed", "未找到小店商品快照,请先刷新商品列表")
|
||||||
return
|
return
|
||||||
goods_snapshot = goods.raw if isinstance(goods.raw, dict) else {}
|
goods_snapshot = goods.raw if isinstance(goods.raw, dict) else {}
|
||||||
goods_raw = goods_snapshot.get("raw") if isinstance(goods_snapshot.get("raw"), dict) else goods_snapshot
|
nested_raw = goods_snapshot.get("raw")
|
||||||
|
goods_raw = (
|
||||||
|
cast(dict[str, Any], nested_raw)
|
||||||
|
if isinstance(nested_raw, dict)
|
||||||
|
else cast(dict[str, Any], goods_snapshot)
|
||||||
|
)
|
||||||
price_key = "iPrice" if pay_type == 1 else "iJb2Price"
|
price_key = "iPrice" if pay_type == 1 else "iJb2Price"
|
||||||
price = self._to_int(goods_raw.get(price_key))
|
price = self._to_int(goods_raw.get(price_key))
|
||||||
if price is None:
|
if price is None:
|
||||||
@@ -489,11 +535,15 @@ class XpdMixin:
|
|||||||
rid=str(config["xpd_rid"]),
|
rid=str(config["xpd_rid"]),
|
||||||
)
|
)
|
||||||
if role.get("role_id"):
|
if role.get("role_id"):
|
||||||
self._apply_xpd_role_to_account(account, role, self._xpd_area_id(role, account))
|
self._apply_xpd_role_to_account(
|
||||||
|
account, role, self._xpd_area_id(role, account)
|
||||||
|
)
|
||||||
except DouyuActivityError as exc:
|
except DouyuActivityError as exc:
|
||||||
self._push_log("warning", f"小店兑换前刷新角色失败,使用已保存角色: {exc}")
|
self._push_log("warning", f"小店兑换前刷新角色失败,使用已保存角色: {exc}")
|
||||||
if not role.get("role_id") and not account.xpd_role_id:
|
if not role.get("role_id") and not account.xpd_role_id:
|
||||||
self._mark_task(db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定")
|
self._mark_task(
|
||||||
|
db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
result = client.xpd_exchange_goods(
|
result = client.xpd_exchange_goods(
|
||||||
@@ -516,13 +566,25 @@ class XpdMixin:
|
|||||||
|
|
||||||
currency = "点券" if pay_type == 1 else "扭蛋碎片"
|
currency = "点券" if pay_type == 1 else "扭蛋碎片"
|
||||||
display_role = role.get("role_name") or account.xpd_game_name or ""
|
display_role = role.get("role_name") or account.xpd_game_name or ""
|
||||||
channel = "微信" if (role.get("type") == "wx" or account.xpd_area_id == 1) else "手Q"
|
channel = (
|
||||||
result.update({
|
"微信" if (role.get("type") == "wx" or account.xpd_area_id == 1) else "手Q"
|
||||||
|
)
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
"goods": {**goods_raw, "commodityName": goods.name or ""},
|
"goods": {**goods_raw, "commodityName": goods.name or ""},
|
||||||
"game_name": display_role,
|
"game_name": display_role,
|
||||||
"game_channel": channel,
|
"game_channel": channel,
|
||||||
"account_name": account.nickname or account.username or account.uid or f"#{account.id}",
|
"account_name": account.nickname
|
||||||
|
or account.username
|
||||||
|
or account.uid
|
||||||
|
or f"#{account.id}",
|
||||||
"currency": currency,
|
"currency": currency,
|
||||||
})
|
}
|
||||||
self._mark_task(db, task, "success", f"兑换小店商品成功: {goods.name or commodity_id}({price}{currency})", result)
|
)
|
||||||
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"success",
|
||||||
|
f"兑换小店商品成功: {goods.name or commodity_id}({price}{currency})",
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user