实现虎牙商品兑换并优化列表展示
This commit is contained in:
@@ -136,6 +136,131 @@ class GetUserPrizeRecordsReq(TafStruct):
|
|||||||
self.sid = ins.read_int32(1, default=self.sid)
|
self.sid = ins.read_int32(1, default=self.sid)
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreExchangePrizeReq(TafStruct):
|
||||||
|
"""webActUI.scoreExchangePrize 请求。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.userId = ActivityUserId()
|
||||||
|
self.sid: int = 0
|
||||||
|
self.pid: int = 0
|
||||||
|
self.ip: str = ""
|
||||||
|
self.clientEnv: dict = {}
|
||||||
|
self.source: str = ""
|
||||||
|
self.isRole: int = 1
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_struct(0, self.userId)
|
||||||
|
os.write_int32(1, self.sid)
|
||||||
|
os.write_int32(2, self.pid)
|
||||||
|
os.write_string(3, self.ip)
|
||||||
|
os.write_map(4, self.clientEnv)
|
||||||
|
os.write_string(5, self.source)
|
||||||
|
os.write_int32(6, self.isRole)
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.userId = ins.read_struct(0, ActivityUserId) or self.userId
|
||||||
|
self.sid = ins.read_int32(1, default=self.sid)
|
||||||
|
self.pid = ins.read_int32(2, default=self.pid)
|
||||||
|
self.ip = ins.read_string(3, default=self.ip)
|
||||||
|
self.clientEnv = ins.read_map(4)
|
||||||
|
self.source = ins.read_string(5, default=self.source)
|
||||||
|
self.isRole = ins.read_int32(6, default=self.isRole)
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeInfo(TafStruct):
|
||||||
|
"""积分兑换结果信息。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.pid: int = 0
|
||||||
|
self.exchangeScore: int = 0
|
||||||
|
self.desc: str = ""
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.pid = ins.read_int32(1, default=self.pid)
|
||||||
|
self.exchangeScore = ins.read_int64(2, default=self.exchangeScore)
|
||||||
|
self.desc = ins.read_string(3, default=self.desc)
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_int32(1, self.pid)
|
||||||
|
os.write_int64(2, self.exchangeScore)
|
||||||
|
os.write_string(3, self.desc)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"pid": self.pid,
|
||||||
|
"exchange_score": self.exchangeScore,
|
||||||
|
"desc": self.desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeActPreCondition(TafStruct):
|
||||||
|
"""兑换前置条件提示。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.conditionType: int = 0
|
||||||
|
self.conditionParams: dict = {}
|
||||||
|
self.tip: str = ""
|
||||||
|
self.jumpUrl: str = ""
|
||||||
|
self.buttonDesc: str = ""
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.conditionType = ins.read_int32(0, default=self.conditionType)
|
||||||
|
self.conditionParams = ins.read_map(1)
|
||||||
|
self.tip = ins.read_string(2, default=self.tip)
|
||||||
|
self.jumpUrl = ins.read_string(3, default=self.jumpUrl)
|
||||||
|
self.buttonDesc = ins.read_string(4, default=self.buttonDesc)
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_int32(0, self.conditionType)
|
||||||
|
os.write_map(1, self.conditionParams)
|
||||||
|
os.write_string(2, self.tip)
|
||||||
|
os.write_string(3, self.jumpUrl)
|
||||||
|
os.write_string(4, self.buttonDesc)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"condition_type": self.conditionType,
|
||||||
|
"condition_params": dict(self.conditionParams),
|
||||||
|
"tip": self.tip,
|
||||||
|
"jump_url": self.jumpUrl,
|
||||||
|
"button_desc": self.buttonDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreExchangePrizeResp(TafStruct):
|
||||||
|
"""webActUI.scoreExchangePrize 响应。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.status: int = 0
|
||||||
|
self.msg: str = ""
|
||||||
|
self.orderId: str = ""
|
||||||
|
self.exchangeInfo = ExchangeInfo()
|
||||||
|
self.actPreCondition = ExchangeActPreCondition()
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.status = ins.read_int32(0, default=self.status)
|
||||||
|
self.msg = ins.read_string(1, default=self.msg)
|
||||||
|
self.orderId = ins.read_string(3, default=self.orderId)
|
||||||
|
self.exchangeInfo = ins.read_struct(4, ExchangeInfo) or self.exchangeInfo
|
||||||
|
self.actPreCondition = ins.read_struct(5, ExchangeActPreCondition) or self.actPreCondition
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_int32(0, self.status)
|
||||||
|
os.write_string(1, self.msg)
|
||||||
|
os.write_string(3, self.orderId)
|
||||||
|
os.write_struct(4, self.exchangeInfo)
|
||||||
|
os.write_struct(5, self.actPreCondition)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"status": self.status,
|
||||||
|
"msg": self.msg,
|
||||||
|
"order_id": self.orderId,
|
||||||
|
"exchange_info": self.exchangeInfo.to_dict(),
|
||||||
|
"act_pre_condition": self.actPreCondition.to_dict(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserPrizeRecordItem(TafStruct):
|
class UserPrizeRecordItem(TafStruct):
|
||||||
"""用户兑换记录项,字段来自 getUserPrizeRecords 实测响应。"""
|
"""用户兑换记录项,字段来自 getUserPrizeRecords 实测响应。"""
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,23 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def score_exchange_prize(self, uid: int, cookie: str, sid: int, pid: int, timeout: float = 15.0):
|
||||||
|
"""兑换活动积分商品。"""
|
||||||
|
from .activity_structs import ScoreExchangePrizeReq, ScoreExchangePrizeResp
|
||||||
|
req = ScoreExchangePrizeReq()
|
||||||
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
|
req.sid = int(sid or 0)
|
||||||
|
req.pid = int(pid or 0)
|
||||||
|
return self.call_rpc(
|
||||||
|
"webActUI",
|
||||||
|
"scoreExchangePrize",
|
||||||
|
req,
|
||||||
|
ScoreExchangePrizeResp,
|
||||||
|
uid=uid,
|
||||||
|
cookie=cookie,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
def get_act_task_detail(self, uid: int, cookie: str, act_id: int, timeout: float = 15.0):
|
def get_act_task_detail(self, uid: int, cookie: str, act_id: int, timeout: float = 15.0):
|
||||||
"""查询活动任务详情,用于发现充值商品 SPU。"""
|
"""查询活动任务详情,用于发现充值商品 SPU。"""
|
||||||
from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp
|
from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""虎牙任务批次执行器。"""
|
"""虎牙任务批次执行器。"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
import threading
|
import threading
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -123,6 +124,31 @@ class HuyaBatchRunner:
|
|||||||
return ""
|
return ""
|
||||||
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_scheduled_time(value) -> datetime | None:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
normalized = text.replace("Z", "+00:00")
|
||||||
|
dt = datetime.fromisoformat(normalized)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
return dt.astimezone()
|
||||||
|
return dt
|
||||||
|
|
||||||
|
def _wait_until(self, when: datetime, uid: int) -> bool:
|
||||||
|
target = when.timestamp()
|
||||||
|
local_text = self._format_local_time(int(target))
|
||||||
|
self._push_log("info", f"[{uid}] 定时兑换等待到 {local_text}")
|
||||||
|
while not self._stop.is_set():
|
||||||
|
remaining = target - time.time()
|
||||||
|
if remaining <= 0:
|
||||||
|
return True
|
||||||
|
time.sleep(min(0.2, max(0.02, remaining)))
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _bind_change_state(cls, bind_status) -> dict:
|
def _bind_change_state(cls, bind_status) -> dict:
|
||||||
account_data = bind_status.accountData
|
account_data = bind_status.accountData
|
||||||
@@ -337,6 +363,82 @@ class HuyaBatchRunner:
|
|||||||
message = f"已刷新商品 {len(goods)} 个"
|
message = f"已刷新商品 {len(goods)} 个"
|
||||||
self._mark_task(worker_db, task, "success", message, {**result, "goods": goods})
|
self._mark_task(worker_db, task, "success", message, {**result, "goods": goods})
|
||||||
|
|
||||||
|
def _execute_exchange_goods(
|
||||||
|
self,
|
||||||
|
worker_db: Session,
|
||||||
|
task: HuyaTask,
|
||||||
|
account: HuyaAccount,
|
||||||
|
account_info: dict,
|
||||||
|
config_info: dict,
|
||||||
|
):
|
||||||
|
sid = str(self.payload.get("sid") or config_info.get("sid") or "").strip()
|
||||||
|
if not sid:
|
||||||
|
self._mark_task(worker_db, task, "failed", "请先配置虎牙活动 SID")
|
||||||
|
return
|
||||||
|
|
||||||
|
sid_int = self._to_int(sid)
|
||||||
|
if not sid_int:
|
||||||
|
self._mark_task(worker_db, task, "failed", f"虎牙活动 SID 无效: {sid}")
|
||||||
|
return
|
||||||
|
|
||||||
|
product_id = self._to_int(self.payload.get("product_id"))
|
||||||
|
if not product_id:
|
||||||
|
self._mark_task(worker_db, task, "failed", "请选择兑换商品")
|
||||||
|
return
|
||||||
|
|
||||||
|
uid = self._resolve_uid(account_info)
|
||||||
|
if not uid:
|
||||||
|
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
|
||||||
|
return
|
||||||
|
|
||||||
|
cookie = account_info.get("cookie") or ""
|
||||||
|
if not cookie:
|
||||||
|
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)
|
||||||
|
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", "定时兑换时间格式无效")
|
||||||
|
return
|
||||||
|
if scheduled_at and scheduled_at.timestamp() > time.time():
|
||||||
|
if not self._wait_until(scheduled_at, uid):
|
||||||
|
self._mark_task(worker_db, task, "failed", "兑换任务已停止")
|
||||||
|
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)
|
||||||
|
if response is None:
|
||||||
|
self._mark_task(worker_db, task, "error", "虎牙兑换接口无响应")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = response.to_dict()
|
||||||
|
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,
|
||||||
|
task,
|
||||||
|
"failed",
|
||||||
|
response.msg or f"虎牙兑换失败: {response.status}",
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
account.status = "goods_exchanged"
|
||||||
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
|
message = response.msg or f"兑换成功: {product_name}"
|
||||||
|
self._mark_task(worker_db, task, "success", message, result)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_pay_channel(value) -> str:
|
def _normalize_pay_channel(value) -> str:
|
||||||
text = str(value or "").strip()
|
text = str(value or "").strip()
|
||||||
@@ -1017,6 +1119,7 @@ class HuyaBatchRunner:
|
|||||||
"query_exchange_records",
|
"query_exchange_records",
|
||||||
"refresh_goods",
|
"refresh_goods",
|
||||||
"refresh_recharge_goods",
|
"refresh_recharge_goods",
|
||||||
|
"exchange_goods",
|
||||||
"create_recharge_order",
|
"create_recharge_order",
|
||||||
}:
|
}:
|
||||||
self._mark_task(worker_db, task, "failed", "该虎牙任务执行器暂未实现")
|
self._mark_task(worker_db, task, "failed", "该虎牙任务执行器暂未实现")
|
||||||
@@ -1030,6 +1133,8 @@ class HuyaBatchRunner:
|
|||||||
self._execute_refresh_goods(worker_db, task, account, account_info, config_info)
|
self._execute_refresh_goods(worker_db, task, account, account_info, config_info)
|
||||||
elif self.task_type == "refresh_recharge_goods":
|
elif self.task_type == "refresh_recharge_goods":
|
||||||
self._execute_refresh_recharge_goods(worker_db, task, account, account_info, config_info)
|
self._execute_refresh_recharge_goods(worker_db, task, account, account_info, config_info)
|
||||||
|
elif self.task_type == "exchange_goods":
|
||||||
|
self._execute_exchange_goods(worker_db, task, account, account_info, config_info)
|
||||||
elif self.task_type == "create_recharge_order":
|
elif self.task_type == "create_recharge_order":
|
||||||
self._execute_create_recharge_order(worker_db, task, account, account_info, config_info)
|
self._execute_create_recharge_order(worker_db, task, account, account_info, config_info)
|
||||||
elif self.task_type == "get_bind_qr":
|
elif self.task_type == "get_bind_qr":
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ SUPPORTED_TASK_TYPES = {
|
|||||||
"confirm_bind": "确认绑定",
|
"confirm_bind": "确认绑定",
|
||||||
"refresh_goods": "刷新商品列表",
|
"refresh_goods": "刷新商品列表",
|
||||||
"refresh_recharge_goods": "刷新充值商品列表",
|
"refresh_recharge_goods": "刷新充值商品列表",
|
||||||
|
"exchange_goods": "兑换商品",
|
||||||
"create_recharge_order": "生成支付二维码",
|
"create_recharge_order": "生成支付二维码",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +173,8 @@ def create_planned_tasks(
|
|||||||
batch_id = uuid.uuid4().hex[:12]
|
batch_id = uuid.uuid4().hex[:12]
|
||||||
payload = payload or {}
|
payload = payload or {}
|
||||||
accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all()
|
accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all()
|
||||||
if task_type in {"refresh_goods", "refresh_recharge_goods", "create_recharge_order"} and accounts:
|
if task_type in {"refresh_goods", "refresh_recharge_goods", "exchange_goods", "create_recharge_order"} and accounts:
|
||||||
# 全局快照和单笔支付二维码都使用一个选中的 CK 即可。
|
# 全局快照、单笔兑换和单笔支付二维码都使用一个选中的 CK 即可。
|
||||||
accounts = accounts[:1]
|
accounts = accounts[:1]
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
db.add(HuyaTask(
|
db.add(HuyaTask(
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button, Card, Col, Form, Input, InputNumber, message, Modal, QRCode, Row, Select, Space, Table, Tabs, Tag, Tooltip, Typography, theme,
|
Button, Card, Col, DatePicker, Form, Input, InputNumber, message, Modal, QRCode, Row, Select, Space, Table, Tabs, Tag, Tooltip, Typography, theme,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
import {
|
import {
|
||||||
AppstoreOutlined, CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined,
|
AppstoreOutlined, CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined,
|
||||||
LinkOutlined, PlayCircleOutlined, QrcodeOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined,
|
LinkOutlined, PlayCircleOutlined, QrcodeOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined,
|
||||||
@@ -31,6 +32,7 @@ const FALLBACK_TASK_TYPES: Record<string, string> = {
|
|||||||
query_exchange_records: '一键查询兑换记录',
|
query_exchange_records: '一键查询兑换记录',
|
||||||
refresh_goods: '刷新商品列表',
|
refresh_goods: '刷新商品列表',
|
||||||
refresh_recharge_goods: '刷新充值商品列表',
|
refresh_recharge_goods: '刷新充值商品列表',
|
||||||
|
exchange_goods: '兑换商品',
|
||||||
create_recharge_order: '生成支付二维码',
|
create_recharge_order: '生成支付二维码',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ const QUICK_ACTIONS = [
|
|||||||
{ key: 'query_exchange_records', icon: <FieldTimeOutlined /> },
|
{ key: 'query_exchange_records', icon: <FieldTimeOutlined /> },
|
||||||
{ key: 'refresh_goods', icon: <ReloadOutlined /> },
|
{ key: 'refresh_goods', icon: <ReloadOutlined /> },
|
||||||
{ key: 'refresh_recharge_goods', icon: <ShoppingOutlined /> },
|
{ key: 'refresh_recharge_goods', icon: <ShoppingOutlined /> },
|
||||||
|
{ key: 'exchange_goods', icon: <ShoppingOutlined /> },
|
||||||
{ key: 'create_recharge_order', icon: <CreditCardOutlined /> },
|
{ key: 'create_recharge_order', icon: <CreditCardOutlined /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -140,6 +143,8 @@ export default function HuyaTasksPage() {
|
|||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
const [selectedTaskType, setSelectedTaskType] = useState('query_points');
|
const [selectedTaskType, setSelectedTaskType] = useState('query_points');
|
||||||
const [selectedGoodsCategory, setSelectedGoodsCategory] = useState('');
|
const [selectedGoodsCategory, setSelectedGoodsCategory] = useState('');
|
||||||
|
const [selectedExchangeGoodsId, setSelectedExchangeGoodsId] = useState('');
|
||||||
|
const [exchangeAt, setExchangeAt] = useState<Dayjs | null>(null);
|
||||||
const [selectedRechargeGoodsId, setSelectedRechargeGoodsId] = useState<string>('');
|
const [selectedRechargeGoodsId, setSelectedRechargeGoodsId] = useState<string>('');
|
||||||
const [rechargeCount, setRechargeCount] = useState(1);
|
const [rechargeCount, setRechargeCount] = useState(1);
|
||||||
const [rechargePayChannel, setRechargePayChannel] = useState('Weixin');
|
const [rechargePayChannel, setRechargePayChannel] = useState('Weixin');
|
||||||
@@ -340,7 +345,35 @@ export default function HuyaTasksPage() {
|
|||||||
return sortedGoods.filter((item) => goodsCategoryKey(item) === selectedGoodsCategory);
|
return sortedGoods.filter((item) => goodsCategoryKey(item) === selectedGoodsCategory);
|
||||||
}, [sortedGoods, selectedGoodsCategory]);
|
}, [sortedGoods, selectedGoodsCategory]);
|
||||||
|
|
||||||
|
const goodsOptions = useMemo(() => {
|
||||||
|
return sortedGoods.map((item) => ({
|
||||||
|
value: item.product_id,
|
||||||
|
label: `${item.name || item.product_id}${item.price ? ` / ${item.price}积分` : ''}`,
|
||||||
|
}));
|
||||||
|
}, [sortedGoods]);
|
||||||
|
|
||||||
|
const selectedExchangeGoods = useMemo(() => {
|
||||||
|
return sortedGoods.find((item) => item.product_id === selectedExchangeGoodsId) || null;
|
||||||
|
}, [selectedExchangeGoodsId, sortedGoods]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sortedGoods.length === 0) {
|
||||||
|
if (selectedExchangeGoodsId) setSelectedExchangeGoodsId('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sortedGoods.some((item) => item.product_id === selectedExchangeGoodsId)) {
|
||||||
|
setSelectedExchangeGoodsId(sortedGoods[0].product_id);
|
||||||
|
}
|
||||||
|
}, [selectedExchangeGoodsId, sortedGoods]);
|
||||||
|
|
||||||
const createPayload = (taskType: string) => {
|
const createPayload = (taskType: string) => {
|
||||||
|
if (taskType === 'exchange_goods') {
|
||||||
|
return {
|
||||||
|
product_id: selectedExchangeGoods?.product_id || selectedExchangeGoodsId,
|
||||||
|
product_name: selectedExchangeGoods?.name || '',
|
||||||
|
scheduled_at: exchangeAt ? exchangeAt.toISOString() : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
if (taskType !== 'create_recharge_order') return {};
|
if (taskType !== 'create_recharge_order') return {};
|
||||||
return {
|
return {
|
||||||
spu_id: selectedRechargeGoods?.spu_id || selectedRechargeGoodsId,
|
spu_id: selectedRechargeGoods?.spu_id || selectedRechargeGoodsId,
|
||||||
@@ -360,6 +393,10 @@ export default function HuyaTasksPage() {
|
|||||||
message.warning('请先选择充值商品');
|
message.warning('请先选择充值商品');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (taskType === 'exchange_goods' && !selectedExchangeGoodsId) {
|
||||||
|
message.warning('请先选择兑换商品');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setStarting(true);
|
setStarting(true);
|
||||||
try {
|
try {
|
||||||
@@ -489,6 +526,20 @@ export default function HuyaTasksPage() {
|
|||||||
if (typeof goodsCount === 'number') return <Tag color="green">商品 {goodsCount} 个</Tag>;
|
if (typeof goodsCount === 'number') return <Tag color="green">商品 {goodsCount} 个</Tag>;
|
||||||
return <Text type="secondary">-</Text>;
|
return <Text type="secondary">-</Text>;
|
||||||
}
|
}
|
||||||
|
if (record.task_type === 'exchange_goods') {
|
||||||
|
const productName = resultText(value, 'product_name');
|
||||||
|
const orderId = resultText(value, 'order_id');
|
||||||
|
if (record.status !== 'success') {
|
||||||
|
return productName ? <Text>{productName}</Text> : <Text type="secondary">-</Text>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Space size={6}>
|
||||||
|
<Tag color="green">已兑换</Tag>
|
||||||
|
{productName ? <Text>{productName}</Text> : null}
|
||||||
|
{orderId ? <Text type="secondary">{orderId}</Text> : null}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (record.task_type === 'refresh_recharge_goods') {
|
if (record.task_type === 'refresh_recharge_goods') {
|
||||||
const goodsCount = value?.goods_count;
|
const goodsCount = value?.goods_count;
|
||||||
const failedCount = value?.failed_count;
|
const failedCount = value?.failed_count;
|
||||||
@@ -560,11 +611,28 @@ export default function HuyaTasksPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const goodsColumns: TableProps<HuyaGoodsItem>['columns'] = [
|
const goodsColumns: TableProps<HuyaGoodsItem>['columns'] = [
|
||||||
{ title: '商品ID', dataIndex: 'product_id', width: 120, ellipsis: true },
|
{ title: '商品ID', dataIndex: 'product_id', width: 88, ellipsis: true },
|
||||||
{ title: '名称', dataIndex: 'name', ellipsis: true },
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 230,
|
||||||
|
render: (value: string) => (
|
||||||
|
<Text
|
||||||
|
title={value}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
lineHeight: '20px',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || '-'}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '分类',
|
title: '分类',
|
||||||
width: 110,
|
width: 96,
|
||||||
render: (_: unknown, record) => {
|
render: (_: unknown, record) => {
|
||||||
const label = goodsCategoryLabel(record);
|
const label = goodsCategoryLabel(record);
|
||||||
return label ? <Tag>{label}</Tag> : <Text type="secondary">-</Text>;
|
return label ? <Tag>{label}</Tag> : <Text type="secondary">-</Text>;
|
||||||
@@ -573,20 +641,20 @@ export default function HuyaTasksPage() {
|
|||||||
{
|
{
|
||||||
title: '价格',
|
title: '价格',
|
||||||
dataIndex: 'price',
|
dataIndex: 'price',
|
||||||
width: 90,
|
width: 78,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (value: number | null) => value ?? <Text type="secondary">-</Text>,
|
render: (value: number | null) => value ?? <Text type="secondary">-</Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '库存',
|
title: '库存',
|
||||||
dataIndex: 'remain_text',
|
dataIndex: 'remain_text',
|
||||||
width: 100,
|
width: 72,
|
||||||
render: (value: string) => formatRemainText(value) || <Text type="secondary">-</Text>,
|
render: (value: string) => formatRemainText(value) || <Text type="secondary">-</Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '更新时间',
|
title: '更新时间',
|
||||||
dataIndex: 'updated_at',
|
dataIndex: 'updated_at',
|
||||||
width: 160,
|
width: 154,
|
||||||
render: (value: string | null) => value ? formatTime(value) : <Text type="secondary">-</Text>,
|
render: (value: string | null) => value ? formatTime(value) : <Text type="secondary">-</Text>,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -728,7 +796,52 @@ export default function HuyaTasksPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card size="small" title={<Space><ShoppingOutlined />兑换商品列表</Space>} style={{ marginBottom: 12 }}>
|
<Card
|
||||||
|
size="small"
|
||||||
|
title={<Space><ShoppingOutlined />兑换商品列表</Space>}
|
||||||
|
extra={(
|
||||||
|
<Space size={6}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={() => startTask('refresh_goods')}
|
||||||
|
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
icon={<CheckCircleOutlined />}
|
||||||
|
onClick={() => startTask('exchange_goods')}
|
||||||
|
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||||
|
>
|
||||||
|
兑换
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
style={{ marginBottom: 12 }}
|
||||||
|
>
|
||||||
|
<Space style={{ marginBottom: 8 }} wrap>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
placeholder="选择兑换商品"
|
||||||
|
value={selectedExchangeGoodsId || undefined}
|
||||||
|
onChange={(value) => setSelectedExchangeGoodsId(value || '')}
|
||||||
|
options={goodsOptions}
|
||||||
|
style={{ minWidth: 220 }}
|
||||||
|
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
showTime
|
||||||
|
allowClear
|
||||||
|
value={exchangeAt}
|
||||||
|
onChange={setExchangeAt}
|
||||||
|
placeholder="立即兑换"
|
||||||
|
style={{ width: 190 }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
{goodsCategories.length > 0 && (
|
{goodsCategories.length > 0 && (
|
||||||
<Tabs
|
<Tabs
|
||||||
size="small"
|
size="small"
|
||||||
@@ -750,6 +863,10 @@ export default function HuyaTasksPage() {
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: 220 }}
|
scroll={{ y: 220 }}
|
||||||
locale={{ emptyText: '暂无兑换商品,请先刷新商品列表' }}
|
locale={{ emptyText: '暂无兑换商品,请先刷新商品列表' }}
|
||||||
|
onRow={(record) => ({
|
||||||
|
onClick: () => setSelectedExchangeGoodsId(record.product_id),
|
||||||
|
})}
|
||||||
|
rowClassName={(record) => record.product_id === selectedExchangeGoodsId ? 'ant-table-row-selected' : ''}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -885,9 +1002,11 @@ export default function HuyaTasksPage() {
|
|||||||
? '使用一个选中的 CK 刷新当前 SID 兑换商品'
|
? '使用一个选中的 CK 刷新当前 SID 兑换商品'
|
||||||
: item.key === 'refresh_recharge_goods'
|
: item.key === 'refresh_recharge_goods'
|
||||||
? '使用一个选中的 CK 刷新充值商品列表'
|
? '使用一个选中的 CK 刷新充值商品列表'
|
||||||
: item.key === 'create_recharge_order'
|
: item.key === 'exchange_goods'
|
||||||
? '按左侧选择生成扫码支付二维码'
|
? '按左侧选择商品,支持立即或定时兑换'
|
||||||
: undefined
|
: item.key === 'create_recharge_order'
|
||||||
|
? '按左侧选择生成扫码支付二维码'
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -902,7 +1021,7 @@ export default function HuyaTasksPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
当前已接入查询积分、绑定、兑换记录、兑换商品、充值商品列表和扫码支付。
|
当前已接入查询积分、绑定、兑换记录、兑换商品列表、商品兑换、充值商品列表和扫码支付。
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user