merge: 合并和平小店兑换功能

This commit is contained in:
yml2213
2026-08-08 00:03:16 +08:00
6 changed files with 350 additions and 10 deletions
+78 -1
View File
@@ -826,6 +826,7 @@ class DouyuActivityClient:
# 2026-08 抓包(8.6-和平小店)实证:本期走 xn_live_cjm 变体(旧期为 recommend_live/common) # 2026-08 抓包(8.6-和平小店)实证:本期走 xn_live_cjm 变体(旧期为 recommend_live/common)
XPD_RECOMMEND_API = "https://apps.game.qq.com/daoju/v3/recommend_xn_live_cjm/common" XPD_RECOMMEND_API = "https://apps.game.qq.com/daoju/v3/recommend_xn_live_cjm/common"
XPD_BALANCE_API = "https://apps.game.qq.com/daoju/igw/live/" XPD_BALANCE_API = "https://apps.game.qq.com/daoju/igw/live/"
XPD_LIVE_BUY_API = "https://apps.game.qq.com/daoju/igw/livebuy/"
XPD_UA = ( XPD_UA = (
"Mozilla/5.0 (Linux; Android 12; HBN-AL00 Build/V417IR; wv) " "Mozilla/5.0 (Linux; Android 12; HBN-AL00 Build/V417IR; wv) "
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 " "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 "
@@ -965,7 +966,7 @@ class DouyuActivityClient:
"iRelationGoodsid,dtModifyTime,sBuyLimitInfo,sGoodsWaterMark,dtShowBeginTime," "iRelationGoodsid,dtModifyTime,sBuyLimitInfo,sGoodsWaterMark,dtShowBeginTime,"
"dtShowEndTime,iSort,iGoodsId,sGoodsName,iJbPrice,iJbOrgPrice,iJb2Price," "dtShowEndTime,iSort,iGoodsId,sGoodsName,iJbPrice,iJbOrgPrice,iJb2Price,"
"iJb2OrgPrice,iPrice,iOrgPrice,sGoodsPic,sGoodsDesc,sPayType,dtBeginTime," "iJb2OrgPrice,iPrice,iOrgPrice,sGoodsPic,sGoodsDesc,sPayType,dtBeginTime,"
"dtEndTime,iActionId,sExtShowInfo,sExtInfo,iCategoryId,dtRushBegin,dtRushEnd" "dtEndTime,iActionId,sExtShowInfo,sExtInfo,iCategoryId,iGoodsLeft,dtRushBegin,dtRushEnd"
), ),
"actionFields": "iActionId,sActionName,dtBeginTime,dtEndTime", "actionFields": "iActionId,sActionName,dtBeginTime,dtEndTime",
"order_by": "dtShowBeginTime", "order_by": "dtShowBeginTime",
@@ -1146,3 +1147,79 @@ class DouyuActivityClient:
) )
info = self._xpd_parse_var(response.text, "jbInfos") info = self._xpd_parse_var(response.text, "jbInfos")
return {"fragments": self._xpd_int(info.get("jb2")), "raw": info} return {"fragments": self._xpd_int(info.get("jb2")), "raw": info}
def xpd_exchange_goods(
self,
*,
embed_query: dict[str, str],
act_id: str,
rid: str,
commodity_id: str,
price: int,
picture: str = "",
pay_type: int = 1,
action_id: str = "",
) -> dict[str, Any]:
"""兑换和平小店商品。
道聚城的 code/sig 为短时授权参数,调用方必须在每次兑换前重新获取。
该接口会实际扣除点券或扭蛋碎片,因此遇到网络错误不做自动重试。
"""
if pay_type not in (1, 5):
raise DouyuActivityError("小店兑换仅支持点券或扭蛋碎片")
if not commodity_id:
raise DouyuActivityError("小店兑换缺少商品 ID")
if price < 0:
raise DouyuActivityError("小店兑换商品价格无效")
params = {
"_service": "buy.plug.svr.web",
"_plug_id": "7200",
"_jsvar": "buyInfo",
"acctype": "livelink",
"_app_id": "2123",
"iActionId": action_id or act_id,
"propid": commodity_id,
"paytype": str(pay_type),
"source": rid,
"sAnchorId": rid,
"attach": json.dumps({"ch": "0"}, separators=(",", ":")),
"apptype": "3",
"_biz_code": "cjm",
"_act_id": act_id,
"sLiveUserInfo": json.dumps(
{"sAnchorId": rid, "sVideoId": 0}, separators=(",", ":"),
),
"appext": json.dumps(
{"user_price": price, "sGoodsPic": picture, "paytype": pay_type},
separators=(",", ":"),
),
"isCode": "1",
"authType": "delegate",
"_sid": "8",
"set": "8",
}
for key in ("gameId", "actId", "appId", "livePlatId", "code", "timestamp", "v", "sig"):
value = str(embed_query.get(key) or "")
if value:
params[key] = value
response = self._request(
"get",
self.XPD_LIVE_BUY_API,
source="兑换小店商品",
params=params,
headers=self._xpd_daoju_headers(),
)
info = self._xpd_parse_var(response.text, "buyInfo")
if str(info.get("ret")) != "0" or str(info.get("msg") or "").lower() != "ok":
raise DouyuActivityError(str(info.get("msg") or "小店兑换失败"))
return {
"commodity_id": commodity_id,
"pay_type": pay_type,
"price": price,
"serial": str(info.get("serial") or info.get("sSerialNum") or ""),
"event_id": str(info.get("event_id") or ""),
"new_balance": self._xpd_int(info.get("newBalance")),
"raw": info,
}
+84
View File
@@ -0,0 +1,84 @@
import json
import unittest
from types import SimpleNamespace
from unittest.mock import Mock
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
class XpdExchangeTests(unittest.TestCase):
def setUp(self):
self.client = DouyuActivityClient(
'acf_uid=100; acf_stk=stk; acf_ltkid=ltkid',
)
def test_exchange_uses_fresh_embed_authorization_and_point_price(self):
self.client._request = Mock(return_value=SimpleNamespace(
text='var buyInfo={"ret":"0","msg":"ok","serial":"order-1","newBalance":88};',
))
result = self.client.xpd_exchange_goods(
embed_query={
'gameId': 'cjm', 'actId': '46195', 'appId': 'bp_cf',
'livePlatId': 'douyu', 'code': 'fresh-code',
'timestamp': '123', 'v': '2.0', 'sig': 'fresh-signature',
},
act_id='46195',
rid='9263298',
commodity_id='goods-1',
price=1200,
picture='https://cdn.example/item.jpg',
pay_type=1,
action_id='46195',
)
self.assertEqual(result['serial'], 'order-1')
self.assertEqual(result['new_balance'], 88)
call = self.client._request.call_args
self.assertEqual(call.args[:2], ('get', self.client.XPD_LIVE_BUY_API))
params = call.kwargs['params']
self.assertEqual(params['code'], 'fresh-code')
self.assertEqual(params['sig'], 'fresh-signature')
self.assertEqual(params['propid'], 'goods-1')
self.assertEqual(params['paytype'], '1')
self.assertEqual(json.loads(params['appext']), {
'user_price': 1200,
'sGoodsPic': 'https://cdn.example/item.jpg',
'paytype': 1,
})
def test_exchange_surfaces_daoju_failure_message(self):
self.client._request = Mock(return_value=SimpleNamespace(
text='var buyInfo={"ret":"-1","msg":"点券不足"};',
))
with self.assertRaisesRegex(DouyuActivityError, '点券不足'):
self.client.xpd_exchange_goods(
embed_query={'code': 'fresh-code'},
act_id='46195',
rid='9263298',
commodity_id='goods-1',
price=1200,
)
def test_list_goods_requests_and_preserves_inventory(self):
self.client._request = Mock(return_value=SimpleNamespace(
text=(
'var recommend={"data":{"client_data":{"itemsdetail":[{'
'"iGoodsId":"goods-1","sGoodsName":"测试商品","iPrice":"6800",'
'"iJb2Price":"0","iGoodsLeft":"0"}]}}};'
),
))
result = self.client.xpd_list_goods(
embed_query={}, act_id='46195', openid='openid', roleid='roleid',
)
self.assertEqual(result['goods'][0]['price'], 6800)
self.assertEqual(result['goods'][0]['goods_left'], 0)
fields = self.client._request.call_args.kwargs['params']['fields']
self.assertIn('iGoodsLeft', fields.split(','))
if __name__ == '__main__':
unittest.main()
+100 -1
View File
@@ -455,7 +455,10 @@ class DouyuBatchRunner:
row.price = self._to_int(raw.get("price") or raw.get("iPrice")) row.price = self._to_int(raw.get("price") or raw.get("iPrice"))
row.org_price = self._to_int(raw.get("org_price") or raw.get("iOrgPrice")) row.org_price = self._to_int(raw.get("org_price") or raw.get("iOrgPrice"))
row.category = str(raw.get("category") or raw.get("iCategoryId") or "") row.category = str(raw.get("category") or raw.get("iCategoryId") or "")
row.goods_left = self._to_int(raw.get("goods_left") or raw.get("iGoodsLeft")) goods_left = raw.get("goods_left")
if goods_left is None:
goods_left = raw.get("iGoodsLeft")
row.goods_left = self._to_int(goods_left)
row.raw = raw row.raw = raw
row.updated_at = now row.updated_at = now
db.commit() db.commit()
@@ -1414,6 +1417,101 @@ class DouyuBatchRunner:
{"fragments": fragments, "role": role, "area_id": int(areaid)}, {"fragments": fragments, "role": role, "area_id": int(areaid)},
) )
def _execute_exchange_xpd_goods(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
):
"""兑换和平小店商品,使用本次签发的道聚城短时授权。"""
payload = self._task_payload(task)
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
if not commodity_id:
self._mark_task(db, task, "failed", "请选择小店商品")
return
try:
pay_type = int(payload.get("pay_type") or 1)
except (TypeError, ValueError):
self._mark_task(db, task, "failed", "兑换货币参数无效")
return
if pay_type not in (1, 5):
self._mark_task(db, task, "failed", "小店兑换仅支持点券或扭蛋碎片")
return
goods = (
db.query(DouyuXpdGoodsSnapshot)
.filter(DouyuXpdGoodsSnapshot.commodity_id == commodity_id)
.first()
)
if not goods:
self._mark_task(db, task, "failed", "未找到小店商品快照,请先刷新商品列表")
return
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
price_key = "iPrice" if pay_type == 1 else "iJb2Price"
price = self._to_int(goods_raw.get(price_key))
if price is None:
price = goods.price if pay_type == 1 else None
if price is None or price <= 0:
currency = "点券" if pay_type == 1 else "扭蛋碎片"
self._mark_task(db, task, "failed", f"该商品不支持使用{currency}兑换")
return
if goods.goods_left is not None and goods.goods_left <= 0:
self._mark_task(db, task, "failed", "该商品库存不足,请刷新商品列表后重试")
return
client = self._client(cookie)
embed = client.xpd_embed_query(
act_alias=str(config["xpd_act_alias"]),
rid=str(config["xpd_rid"]),
)
role: dict = {}
try:
role = client.xpd_get_role(
embed_query=embed["query"],
act_id=str(config["xpd_act_id"]),
rid=str(config["xpd_rid"]),
)
if role.get("role_id"):
self._apply_xpd_role_to_account(account, role, self._xpd_area_id(role, account))
except DouyuActivityError as exc:
self._push_log("warning", f"小店兑换前刷新角色失败,使用已保存角色: {exc}")
if not role.get("role_id") and not account.xpd_role_id:
self._mark_task(db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定")
return
result = client.xpd_exchange_goods(
embed_query=embed["query"],
act_id=str(config["xpd_act_id"]),
rid=str(config["xpd_rid"]),
commodity_id=commodity_id,
price=price,
picture=str(goods_raw.get("sGoodsPic") or ""),
pay_type=pay_type,
action_id=str(goods_raw.get("iActionId") or ""),
)
if pay_type == 1 and result.get("new_balance") is not None:
account.xpd_balance = result["new_balance"]
if pay_type == 5 and result.get("new_balance") is not None:
account.xpd_fragments = result["new_balance"]
account.xpd_bind_status = "xpd_goods_exchanged"
account.updated_at = datetime.now(timezone.utc)
db.commit()
currency = "点券" if pay_type == 1 else "扭蛋碎片"
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"
result.update({
"goods": goods_raw,
"game_name": display_role,
"game_channel": channel,
"account_name": account.nickname or account.username or account.uid or f"#{account.id}",
"currency": currency,
})
self._mark_task(db, task, "success", f"兑换小店商品成功: {goods.name or commodity_id}{price}{currency}", 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) client = self._client(cookie)
qr_act_alias = self._bind_qr_act_alias(config) qr_act_alias = self._bind_qr_act_alias(config)
@@ -2710,6 +2808,7 @@ class DouyuBatchRunner:
"refresh_xpd_goods": self._execute_refresh_xpd_goods, "refresh_xpd_goods": self._execute_refresh_xpd_goods,
"query_xpd_balance": self._execute_query_xpd_balance, "query_xpd_balance": self._execute_query_xpd_balance,
"query_xpd_fragments": self._execute_query_xpd_fragments, "query_xpd_fragments": self._execute_query_xpd_fragments,
"exchange_xpd_goods": self._execute_exchange_xpd_goods,
}.get(task.task_type) }.get(task.task_type)
if handler is None: if handler is None:
self._mark_task(worker_db, task, "failed", "不支持的任务类型") self._mark_task(worker_db, task, "failed", "不支持的任务类型")
+1
View File
@@ -44,6 +44,7 @@ SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码",
"refresh_xpd_goods": "刷新小店商品列表", "refresh_xpd_goods": "刷新小店商品列表",
"query_xpd_balance": "查询小店点券余额", "query_xpd_balance": "查询小店点券余额",
"query_xpd_fragments": "查询小店扭蛋碎片", "query_xpd_fragments": "查询小店扭蛋碎片",
"exchange_xpd_goods": "兑换小店商品",
} }
+84 -6
View File
@@ -91,6 +91,7 @@ const PEACE_QUICK_ACTIONS = [
{ key: 'query_xpd_balance', icon: <SearchOutlined /> }, { key: 'query_xpd_balance', icon: <SearchOutlined /> },
{ key: 'query_xpd_fragments', icon: <SearchOutlined /> }, { key: 'query_xpd_fragments', icon: <SearchOutlined /> },
{ key: 'refresh_xpd_goods', icon: <ReloadOutlined /> }, { key: 'refresh_xpd_goods', icon: <ReloadOutlined /> },
{ key: 'exchange_xpd_goods', icon: <ShoppingOutlined /> },
]; ];
const ELITE_TASK_TYPES = new Set([ const ELITE_TASK_TYPES = new Set([
@@ -133,6 +134,7 @@ const PEACE_TASK_TYPES = new Set([
'refresh_xpd_goods', 'refresh_xpd_goods',
'query_xpd_balance', 'query_xpd_balance',
'query_xpd_fragments', 'query_xpd_fragments',
'exchange_xpd_goods',
]); ]);
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount'; const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count'; const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
@@ -231,9 +233,34 @@ function goodsLabel(item: DouyuGoodsItem): string {
function xpdGoodsLabel(item: DouyuGoodsItem): string { function xpdGoodsLabel(item: DouyuGoodsItem): string {
const xpd = item as DouyuGoodsItem & { price?: number | null; goods_left?: number | null }; const xpd = item as DouyuGoodsItem & { price?: number | null; goods_left?: number | null };
const price = xpd.price != null ? ` / ${xpd.price}点券` : ''; const raw = item.raw?.raw as Record<string, unknown> | undefined;
const left = xpd.goods_left != null ? ` / 剩${xpd.goods_left}` : ''; const pointPrice = Number(xpd.price ?? raw?.iPrice);
return `${item.name || item.commodity_id}${price}${left}`; const fragmentPrice = Number(raw?.iJb2Price);
const prices = [
Number.isFinite(pointPrice) && pointPrice > 0 ? `${pointPrice}点券` : '',
Number.isFinite(fragmentPrice) && fragmentPrice > 0 ? `${fragmentPrice}碎片` : '',
].filter(Boolean);
const stock = xpd.goods_left;
const stockText = stock != null && Number.isFinite(Number(stock))
? `库存${stock}`
: '库存未知';
return `${item.name || item.commodity_id}${prices.length ? ` / ${prices.join(' 或 ')}` : ' / 暂不可兑换'} / ${stockText}`;
}
function xpdGoodsPaymentOptions(item: DouyuGoodsItem | undefined): Array<{ value: '1' | '5'; label: string }> {
if (!item) return [];
const xpd = item as DouyuGoodsItem & { price?: number | null };
const raw = item.raw?.raw as Record<string, unknown> | undefined;
const pointPrice = Number(xpd.price ?? raw?.iPrice);
const fragmentPrice = Number(raw?.iJb2Price);
return [
Number.isFinite(pointPrice) && pointPrice > 0
? { value: '1' as const, label: `使用点券兑换(${pointPrice}点券)` }
: null,
Number.isFinite(fragmentPrice) && fragmentPrice > 0
? { value: '5' as const, label: `使用扭蛋碎片兑换(${fragmentPrice}碎片)` }
: null,
].filter((option): option is { value: '1' | '5'; label: string } => option !== null);
} }
function formatWaitSeconds(seconds: number | null | undefined): string { function formatWaitSeconds(seconds: number | null | undefined): string {
@@ -337,6 +364,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY)); const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY)); const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY));
const [selectedGoodsId, setSelectedGoodsId] = useState(''); const [selectedGoodsId, setSelectedGoodsId] = useState('');
const [xpdPayType, setXpdPayType] = useState<'1' | '5'>('1');
const [accountSearch, setAccountSearch] = useState(''); const [accountSearch, setAccountSearch] = useState('');
const [layoutMode, setLayoutMode] = useState<'stack' | 'split'>(() => { const [layoutMode, setLayoutMode] = useState<'stack' | 'split'>(() => {
const v = localStorage.getItem(DOUYU_LAYOUT_MODE_STORAGE_KEY); const v = localStorage.getItem(DOUYU_LAYOUT_MODE_STORAGE_KEY);
@@ -847,6 +875,26 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
// 多批次并发:不再用全局 busy 标志阻塞 UI,刷新频率直接用各子条件判断 // 多批次并发:不再用全局 busy 标志阻塞 UI,刷新频率直接用各子条件判断
const selectedXpdGoods = useMemo(
() => goods.find((item) => item.commodity_id === selectedGoodsId),
[goods, selectedGoodsId],
);
const xpdPaymentOptions = useMemo(
() => xpdGoodsPaymentOptions(selectedXpdGoods),
[selectedXpdGoods],
);
const selectedXpdGoodsSoldOut = (() => {
const stock = (selectedXpdGoods as (DouyuGoodsItem & { goods_left?: number | null }) | undefined)?.goods_left;
return stock != null && Number.isFinite(Number(stock)) && Number(stock) <= 0;
})();
useEffect(() => {
if (xpdPaymentOptions.length === 0) return;
if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
setXpdPayType(xpdPaymentOptions[0].value);
}
}, [xpdPayType, xpdPaymentOptions]);
const startTask = async ( const startTask = async (
taskType: string, taskType: string,
accountIds?: number[], accountIds?: number[],
@@ -858,12 +906,25 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return; return;
} }
// 多批次并发:不再拦截,允许在已有批次运行时继续创建新批次 // 多批次并发:不再拦截,允许在已有批次运行时继续创建新批次
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) { if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType) && !selectedGoodsId) {
message.warning('请先选择兑换商品'); message.warning('请先选择兑换商品');
return; return;
} }
if (taskType === 'exchange_xpd_goods') {
if (selectedXpdGoodsSoldOut) {
message.warning('该商品库存不足,请刷新商品列表后重试');
return;
}
if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
message.warning('该商品不支持当前兑换方式');
return;
}
}
const payload: Record<string, unknown> = { ...extraPayload }; const payload: Record<string, unknown> = { ...extraPayload };
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) payload.commodity_id = selectedGoodsId; if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType)) {
payload.commodity_id = selectedGoodsId;
}
if (taskType === 'exchange_xpd_goods') payload.pay_type = xpdPayType;
if (taskType === 'create_gold_qr') payload.amount = goldAmount; if (taskType === 'create_gold_qr') payload.amount = goldAmount;
if (['donate_elite_gift', 'donate_esports_chicken_gift', 'donate_esports_firework_gift'].includes(taskType)) { if (['donate_elite_gift', 'donate_esports_chicken_gift', 'donate_esports_firework_gift'].includes(taskType)) {
payload.gift_count = giftCount; payload.gift_count = giftCount;
@@ -1484,6 +1545,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]); const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
const actionDisabled = (taskType: string) => { const actionDisabled = (taskType: string) => {
if (selectedIds.length === 0) return true; if (selectedIds.length === 0) return true;
if (taskType === 'exchange_xpd_goods') {
return !selectedGoodsId
|| selectedXpdGoodsSoldOut
|| !xpdPaymentOptions.some((option) => option.value === xpdPayType);
}
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId; if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
// 选中账号中有正在执行的任务时禁用,避免同账号并发不同类型任务(并发写状态字段) // 选中账号中有正在执行的任务时禁用,避免同账号并发不同类型任务(并发写状态字段)
return selectedHasRunning; return selectedHasRunning;
@@ -1578,7 +1644,19 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
optionFilterProp="label" optionFilterProp="label"
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
{renderActionButton('refresh_xpd_goods')} <Select
size="small"
value={xpdPaymentOptions.some((option) => option.value === xpdPayType) ? xpdPayType : undefined}
onChange={setXpdPayType}
options={xpdPaymentOptions}
placeholder={selectedGoodsId ? '该商品暂无可用兑换方式' : '先选择商品'}
disabled={!selectedGoodsId || xpdPaymentOptions.length === 0}
style={{ width: '100%' }}
/>
<div style={actionGridStyle}>
{renderActionButton('refresh_xpd_goods')}
{renderActionButton('exchange_xpd_goods', 'primary')}
</div>
</Space> </Space>
</div> </div>
</div> </div>
+3 -2
View File
@@ -1,6 +1,6 @@
import type { DouyuTaskItem } from '../api/types'; import type { DouyuTaskItem } from '../api/types';
const EXCHANGE_TASK_TYPES = new Set(['exchange_goods', 'exchange_esports_goods']); const EXCHANGE_TASK_TYPES = new Set(['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods']);
const CARD_W = 320; const CARD_W = 320;
const CARD_H = 470; const CARD_H = 470;
const FONT_FAMILY = '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif'; const FONT_FAMILY = '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif';
@@ -28,9 +28,10 @@ function goodsOf(task: DouyuTaskItem): {
const goods = (task.result?.goods ?? null) as Record<string, unknown> | null; const goods = (task.result?.goods ?? null) as Record<string, unknown> | null;
const name = rawString(goods, 'commodityName') const name = rawString(goods, 'commodityName')
|| rawString(goods, 'name') || rawString(goods, 'name')
|| rawString(goods, 'sGoodsName')
|| task.message.replace(/^兑换[^:]*[:]?\s*/, '').trim() || task.message.replace(/^兑换[^:]*[:]?\s*/, '').trim()
|| rawString(task.result, 'commodity_id'); || rawString(task.result, 'commodity_id');
const pic = rawString(goods, 'webPic') || rawString(goods, 'pic'); const pic = rawString(goods, 'webPic') || rawString(goods, 'pic') || rawString(goods, 'sGoodsPic');
const roleName = rawString(task.result, 'game_name'); const roleName = rawString(task.result, 'game_name');
const channel = rawString(task.result, 'game_channel'); const channel = rawString(task.result, 'game_channel');
const accountName = rawString(task.result, 'account_name') const accountName = rawString(task.result, 'account_name')