新增斗鱼活动任务模块:绑定、宝典、鱼翅、积分、兑换等功能
- 新增 activity_client.py:封装斗鱼活动/兑换/充值/送礼接口 - 新增 cookie_utils.py:Cookie 解析与规范化工具 - 新增 douyu_service/douyu_runner:斗鱼任务服务层与批量执行器 - 新增 douyu 路由:任务类型查询、账号列表、配置管理、商品管理、批量任务、WebSocket 日志 - 新增 models/schemas:DouyuTask/DouyuConfig/DouyuGoodsSnapshot 模型,Account 扩展点数/鱼翅/绑定状态等字段 - 新增数据库迁移:斗鱼活动相关表与 accounts 字段补充 - 新增前端 DouyuTasksPage 任务操作台页面 - 兑换商品请求添加 sec-ch-ua 反检测头 - 兑换商品支持最多 8 次重试 + csrf_token 自动刷新 - 注册 douyu:task / douyu:config 权限点 - 侧边栏新增斗鱼分组与任务操作台菜单入口
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
"""斗鱼活动任务服务。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.douyu.activity_client import DouyuActivityClient
|
||||
from core.douyu.cookie_utils import cookie_value
|
||||
|
||||
from ..models import Account, DouyuConfig, DouyuTask, LoginTask
|
||||
|
||||
|
||||
SUPPORTED_DOUYU_TASK_TYPES = {
|
||||
"get_bind_qr": "获取绑定二维码",
|
||||
"confirm_bind": "确认绑定",
|
||||
"create_elite_qr": "开通精英宝典30",
|
||||
"create_gold_qr": "充值鱼翅",
|
||||
"donate_elite_gift": "赠送精英令",
|
||||
"query_points": "一键查询积分",
|
||||
"exchange_goods": "兑换商品",
|
||||
"query_game_name": "一键获取游戏名",
|
||||
"query_change_bind_time": "一键查询换绑时间",
|
||||
"query_limited_goods": "一键查询限兑商品",
|
||||
"query_gold_balance": "一键查询鱼刺余额",
|
||||
"refresh_goods": "刷新商品列表",
|
||||
"query_exchange_records": "一键查询兑换记录",
|
||||
"prefetch_csrf_token": "一键获取兑换 CSRF Token",
|
||||
}
|
||||
|
||||
|
||||
DOUYU_CONFIG_DEFAULTS = {
|
||||
"manual_id": "G4KA4Qnz4LDp7",
|
||||
"rid": "9263298",
|
||||
"bind_act_alias": "20250213NQCYX",
|
||||
"confirm_act_alias": "20260120QYOOB",
|
||||
"legacy_act_alias": "cjm",
|
||||
"room_id": "9263298",
|
||||
"elite_amount": 3000,
|
||||
"gold_pay_type": 1,
|
||||
"gift_id": "23643",
|
||||
"skin_id": "2942",
|
||||
}
|
||||
|
||||
DOUYU_CONFIG_FIELDS = tuple(DOUYU_CONFIG_DEFAULTS.keys())
|
||||
DOUYU_ACTIVE_TASK_STATUSES = ("planned", "pending", "running")
|
||||
|
||||
|
||||
def douyu_config_value(field: str, value):
|
||||
"""读取配置值;空值自动回退到默认值。"""
|
||||
default = DOUYU_CONFIG_DEFAULTS[field]
|
||||
if isinstance(default, int):
|
||||
try:
|
||||
return int(value if value is not None else default)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
text = str(value or "").strip()
|
||||
return text or str(default)
|
||||
|
||||
|
||||
def apply_douyu_config_defaults(config: DouyuConfig) -> bool:
|
||||
"""补齐斗鱼配置默认值,返回是否发生变更。"""
|
||||
changed = False
|
||||
for field in DOUYU_CONFIG_FIELDS:
|
||||
normalized = douyu_config_value(field, getattr(config, field, None))
|
||||
if getattr(config, field, None) != normalized:
|
||||
setattr(config, field, normalized)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def ensure_douyu_config(db: Session) -> DouyuConfig:
|
||||
"""获取单条斗鱼配置,不存在则创建。"""
|
||||
config = db.query(DouyuConfig).first()
|
||||
if config:
|
||||
if apply_douyu_config_defaults(config):
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return config
|
||||
config = DouyuConfig(**DOUYU_CONFIG_DEFAULTS)
|
||||
db.add(config)
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return config
|
||||
|
||||
|
||||
def latest_success_cookie(db: Session, account_id: int) -> str:
|
||||
"""读取账号最近一次成功登录 Cookie。"""
|
||||
task = (
|
||||
db.query(LoginTask)
|
||||
.filter(
|
||||
LoginTask.account_id == account_id,
|
||||
LoginTask.status == "success",
|
||||
LoginTask.cookie != "",
|
||||
)
|
||||
.order_by(LoginTask.id.desc())
|
||||
.first()
|
||||
)
|
||||
return task.cookie if task else ""
|
||||
|
||||
|
||||
def cookie_account_ids_query(db: Session):
|
||||
"""返回拥有成功 Cookie 的斗鱼账号 ID 查询。"""
|
||||
return (
|
||||
db.query(LoginTask.account_id)
|
||||
.filter(LoginTask.status == "success", LoginTask.cookie != "")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
def visible_douyu_task_accounts(db: Session, account_ids: list[int]) -> list[Account]:
|
||||
"""只保留存在成功 Cookie 的斗鱼账号。"""
|
||||
if not account_ids:
|
||||
return []
|
||||
cookie_ids = cookie_account_ids_query(db).subquery()
|
||||
return (
|
||||
db.query(Account)
|
||||
.filter(Account.id.in_(account_ids), Account.id.in_(cookie_ids))
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def update_account_profile_from_cookie(account: Account, cookie: str) -> None:
|
||||
"""从 Cookie 回填 uid/nickname。"""
|
||||
profile = DouyuActivityClient.profile_from_cookie(cookie)
|
||||
if profile["uid"]:
|
||||
account.uid = profile["uid"]
|
||||
if profile["nickname"]:
|
||||
account.nickname = profile["nickname"]
|
||||
|
||||
|
||||
def account_uid(account: Account, cookie: str) -> str:
|
||||
"""优先从账号字段读取 uid,缺失时从 Cookie 中取。"""
|
||||
return account.uid or cookie_value(cookie, "acf_uid")
|
||||
|
||||
|
||||
def create_douyu_planned_tasks(
|
||||
db: Session,
|
||||
account_ids: list[int],
|
||||
task_type: str,
|
||||
created_by: int,
|
||||
payload: dict | None = None,
|
||||
) -> tuple[str, int]:
|
||||
"""创建斗鱼任务记录,等待后台执行器消费。"""
|
||||
if task_type not in SUPPORTED_DOUYU_TASK_TYPES:
|
||||
raise ValueError("不支持的任务类型")
|
||||
|
||||
accounts = visible_douyu_task_accounts(db, account_ids)
|
||||
if task_type == "refresh_goods" and accounts:
|
||||
# 商品快照是全局数据,一个可用 CK 足够;没有 CK 时前端无法选账号创建任务。
|
||||
accounts = accounts[:1]
|
||||
|
||||
batch_id = uuid.uuid4().hex[:12]
|
||||
payload = payload or {}
|
||||
for account in accounts:
|
||||
db.add(DouyuTask(
|
||||
batch_id=batch_id,
|
||||
account_id=account.id,
|
||||
task_type=task_type,
|
||||
status="planned",
|
||||
message="任务已创建,等待执行",
|
||||
result={"payload": payload} if payload else None,
|
||||
created_by=created_by,
|
||||
))
|
||||
db.commit()
|
||||
return batch_id, len(accounts)
|
||||
|
||||
|
||||
def cleanup_orphan_douyu_tasks(
|
||||
db: Session,
|
||||
*,
|
||||
active_batch_ids: set[str] | None = None,
|
||||
batch_id: str | None = None,
|
||||
statuses: tuple[str, ...] = DOUYU_ACTIVE_TASK_STATUSES,
|
||||
message: str = "任务已中断(服务重启或批次丢失)",
|
||||
) -> int:
|
||||
"""清理没有执行器接管的斗鱼任务。"""
|
||||
query = db.query(DouyuTask).filter(DouyuTask.status.in_(statuses))
|
||||
if batch_id:
|
||||
query = query.filter(DouyuTask.batch_id == batch_id)
|
||||
elif active_batch_ids is not None and active_batch_ids:
|
||||
query = query.filter(~DouyuTask.batch_id.in_(list(active_batch_ids)))
|
||||
|
||||
tasks = query.all()
|
||||
if not tasks:
|
||||
return 0
|
||||
now = datetime.now(timezone.utc)
|
||||
for task in tasks:
|
||||
task.status = "stopped"
|
||||
task.message = message
|
||||
task.finished_at = now
|
||||
db.commit()
|
||||
return len(tasks)
|
||||
Reference in New Issue
Block a user