和平小店可以返回所有商品

This commit is contained in:
yml2213
2026-08-08 00:30:58 +08:00
parent 85813a5095
commit de23d87087
5 changed files with 107 additions and 35 deletions
+1
View File
@@ -26,3 +26,4 @@ web/frontend/dist/
data/web.db
.DS_Store
.reasonix/
.tmp_reverse/
+60 -29
View File
@@ -827,6 +827,7 @@ class DouyuActivityClient:
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_LIVE_BUY_API = "https://apps.game.qq.com/daoju/igw/livebuy/"
XPD_GOODS_PAGE_SIZE = 10
XPD_UA = (
"Mozilla/5.0 (Linux; Android 12; HBN-AL00 Build/V417IR; wv) "
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 "
@@ -953,20 +954,17 @@ class DouyuActivityClient:
roleid: str,
areaid: str = "1",
) -> dict[str, Any]:
"""查询和平小店推荐商品(category=76 全部商品)"""
params = {
"""查询和平小店全部商品,排除热门抢购分类并拉取全部分页"""
base_params = {
"_service": "act.recommend.query",
"_biz_code": "cjm",
"_app_id": "2123",
"app_id": "2123",
"page_begin": "0",
"page_num": "1",
"page_size": "200",
"fields": (
"iRelationGoodsid,dtModifyTime,sBuyLimitInfo,sGoodsWaterMark,dtShowBeginTime,"
"dtShowEndTime,iSort,iGoodsId,sGoodsName,iJbPrice,iJbOrgPrice,iJb2Price,"
"iJb2OrgPrice,iPrice,iOrgPrice,sGoodsPic,sGoodsDesc,sPayType,dtBeginTime,"
"dtEndTime,iActionId,sExtShowInfo,sExtInfo,iCategoryId,iGoodsLeft,dtRushBegin,dtRushEnd"
"dtEndTime,iActionId,sExtShowInfo,sExtInfo,iCategoryId,dtRushBegin,dtRushEnd"
),
"actionFields": "iActionId,sActionName,dtBeginTime,dtEndTime",
"order_by": "dtShowBeginTime",
@@ -974,36 +972,69 @@ class DouyuActivityClient:
"desc": "1",
"_act_id": act_id,
"actid": act_id,
"category": "76",
# category=76 是“热门抢购”;全部商品页明确将其排除。
"excludeFields": "iCategoryId_76",
"userid": openid,
"roleid": roleid,
"area": "1",
"areaid": str(areaid),
"need_limit": "1",
}
response = self._request(
"get",
self.XPD_RECOMMEND_API,
source="刷新小店商品",
params=params,
headers=self._xpd_daoju_headers(),
)
recommend = self._xpd_parse_var(response.text, "recommend")
items = (((recommend.get("data") or {}).get("client_data") or {}).get("itemsdetail")) or []
goods = []
for item in items:
goods.append(
{
"commodity_id": str(item.get("iGoodsId") or ""),
"name": str(item.get("sGoodsName") or ""),
"price": self._xpd_int(item.get("iPrice")),
"org_price": self._xpd_int(item.get("iOrgPrice")),
"category": str(item.get("iCategoryId") or ""),
"goods_left": self._xpd_int(item.get("iGoodsLeft")),
"raw": item,
}
goods: list[dict[str, Any]] = []
seen_ids: set[str] = set()
pages: list[dict[str, Any]] = []
offset = 0
page_num = 1
while True:
params = {
**base_params,
"page_begin": str(offset),
"page_num": str(page_num),
"page_size": str(self.XPD_GOODS_PAGE_SIZE),
}
response = self._request(
"get",
self.XPD_RECOMMEND_API,
source=f"刷新小店全部商品第 {page_num}",
params=params,
headers=self._xpd_daoju_headers(),
)
return {"goods": goods, "raw": recommend}
recommend = self._xpd_parse_var(response.text, "recommend")
if str(recommend.get("errcode") or "0") != "0":
raise DouyuActivityError(str(recommend.get("errdesc") or "小店商品查询失败"))
pages.append(recommend)
items = (((recommend.get("data") or {}).get("client_data") or {}).get("itemsdetail")) or []
if not isinstance(items, list):
raise DouyuActivityError("小店商品响应格式异常")
for item in items:
if not isinstance(item, dict):
continue
commodity_id = str(item.get("iGoodsId") or "")
if not commodity_id or commodity_id in seen_ids:
continue
seen_ids.add(commodity_id)
goods.append(
{
"commodity_id": commodity_id,
"name": str(item.get("sGoodsName") or ""),
"price": self._xpd_int(item.get("iPrice")),
"org_price": self._xpd_int(item.get("iOrgPrice")),
"category": str(item.get("iCategoryId") or ""),
"goods_left": self._xpd_int(item.get("iGoodsLeft")),
"raw": item,
}
)
if len(items) < self.XPD_GOODS_PAGE_SIZE:
break
offset += self.XPD_GOODS_PAGE_SIZE
page_num += 1
if page_num > 100:
raise DouyuActivityError("小店商品分页超出安全上限")
return {"goods": goods, "raw": {"pages": pages}}
def xpd_bind_qr(self, *, act_alias: str) -> dict[str, Any]:
"""生成和平小店绑定二维码链接(微信扫码进入 livelink 小程序绑定角色)。
+29 -2
View File
@@ -76,8 +76,35 @@ class XpdExchangeTests(unittest.TestCase):
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(','))
params = self.client._request.call_args.kwargs['params']
self.assertEqual(params['excludeFields'], 'iCategoryId_76')
self.assertNotIn('category', params)
def test_list_goods_fetches_every_page_and_deduplicates_goods(self):
first_page = [
{'iGoodsId': f'goods-{index}', 'sGoodsName': f'商品 {index}'}
for index in range(10)
]
second_page = [
{'iGoodsId': 'goods-9', 'sGoodsName': '重复商品'},
{'iGoodsId': 'goods-10', 'sGoodsName': '商品 10'},
]
self.client._request = Mock(side_effect=[
SimpleNamespace(text=f'var recommend={{"errcode":0,"data":{{"client_data":{{"itemsdetail":{json.dumps(first_page)}}}}}}};'),
SimpleNamespace(text=f'var recommend={{"errcode":0,"data":{{"client_data":{{"itemsdetail":{json.dumps(second_page)}}}}}}};'),
])
result = self.client.xpd_list_goods(
embed_query={}, act_id='46195', openid='openid', roleid='roleid',
)
self.assertEqual(len(result['goods']), 11)
self.assertEqual([item['commodity_id'] for item in result['goods']][-1], 'goods-10')
self.assertEqual(self.client._request.call_count, 2)
first_params = self.client._request.call_args_list[0].kwargs['params']
second_params = self.client._request.call_args_list[1].kwargs['params']
self.assertEqual((first_params['page_begin'], first_params['page_num']), ('0', '1'))
self.assertEqual((second_params['page_begin'], second_params['page_num']), ('10', '2'))
if __name__ == '__main__':
+15 -2
View File
@@ -437,8 +437,20 @@ class DouyuBatchRunner:
db.commit()
def _upsert_xpd_goods(self, db: Session, goods: list[dict]) -> None:
"""写入和平小店商品快照。"""
"""同步和平小店商品快照,移除上一次热门抢购等遗留商品"""
now = datetime.now(timezone.utc)
commodity_ids = {
str(raw.get("commodity_id") or raw.get("iGoodsId") or "")
for raw in goods
}
commodity_ids.discard("")
query = db.query(DouyuXpdGoodsSnapshot)
if commodity_ids:
query.filter(~DouyuXpdGoodsSnapshot.commodity_id.in_(commodity_ids)).delete(
synchronize_session=False,
)
else:
query.delete(synchronize_session=False)
for raw in goods:
commodity_id = str(raw.get("commodity_id") or raw.get("iGoodsId") or "")
if not commodity_id:
@@ -1458,7 +1470,8 @@ class DouyuBatchRunner:
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:
# iGoodsLeft=-1 表示活动未公开库存,不是售罄;只有 0 才阻止兑换。
if goods.goods_left == 0:
self._mark_task(db, task, "failed", "该商品库存不足,请刷新商品列表后重试")
return
+2 -2
View File
@@ -242,7 +242,7 @@ function xpdGoodsLabel(item: DouyuGoodsItem): string {
].filter(Boolean);
const stock = xpd.goods_left;
const stockText = stock != null && Number.isFinite(Number(stock))
? `库存${stock}`
? (Number(stock) < 0 ? '库存未公开' : `库存${stock}`)
: '库存未知';
return `${item.name || item.commodity_id}${prices.length ? ` / ${prices.join(' 或 ')}` : ' / 暂不可兑换'} / ${stockText}`;
}
@@ -885,7 +885,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
);
const selectedXpdGoodsSoldOut = (() => {
const stock = (selectedXpdGoods as (DouyuGoodsItem & { goods_left?: number | null }) | undefined)?.goods_left;
return stock != null && Number.isFinite(Number(stock)) && Number(stock) <= 0;
return stock != null && Number.isFinite(Number(stock)) && Number(stock) === 0;
})();
useEffect(() => {