优化和平小店碎片兑换链路

This commit is contained in:
yml2213
2026-08-09 10:07:36 +08:00
parent c6f16dfbee
commit feb72196c1
6 changed files with 54 additions and 18 deletions
+1
View File
@@ -6,6 +6,7 @@ import { LoadProducts } from '@wails/go/app/App'
export interface Product {
commodityId: string
commodityName: string
actionId?: string
category: string
score: number
payType?: number
+6
View File
@@ -13,6 +13,12 @@ const seckillStateEvent = "seckill:state"
// StartSeckill 立即启动抢购。
func (a *App) StartSeckill(accountProducts map[string]string) types.MessageResult {
// 兼容旧前端/旧构建:和平小店只能走 livebuy 专用链路,不能进入 CSRF 兑换流程。
if a.getClient().Activity.IsXiaodian {
return a.startXiaodianSeckill(types.FixedSeckillRequest{
AccountProducts: accountProducts,
}, "和平小店抢购已启动")
}
if len(accountProducts) == 0 {
return types.MessageResult{Error: "请选择账号和商品"}
}
+22 -5
View File
@@ -256,6 +256,7 @@ type xiaodianRecommendation struct {
type xiaodianItem struct {
GoodsID string `json:"iGoodsId"`
GoodsName string `json:"sGoodsName"`
ActionID xiaodianFlexibleString `json:"iActionId"`
Category xiaodianFlexibleString `json:"iCategoryId"`
Price string `json:"iPrice"`
JB2Price string `json:"iJb2Price"`
@@ -264,7 +265,6 @@ type xiaodianItem struct {
GoodsPic string `json:"sGoodsPic"`
ShowBegin string `json:"dtShowBeginTime"`
ShowEnd string `json:"dtShowEndTime"`
ActionID string `json:"iActionId"`
}
type xiaodianFlexibleString string
@@ -328,6 +328,16 @@ type xiaodianBuyInfo struct {
// xiaodianEmbedQuery 获取斗鱼为当前账号签发的短时道具城参数。
func (c *DouyuClient) xiaodianEmbedQuery(cookie string) (url.Values, error) {
return c.xiaodianEmbedQueryWithCache(cookie, true)
}
// xiaodianEmbedQueryFresh 每次调用都重新获取 H5 授权,供会真实扣款的兑换请求使用。
// code/sig/timestamp 是短时凭证,不应跨兑换轮次长期复用。
func (c *DouyuClient) xiaodianEmbedQueryFresh(cookie string) (url.Values, error) {
return c.xiaodianEmbedQueryWithCache(cookie, false)
}
func (c *DouyuClient) xiaodianEmbedQueryWithCache(cookie string, useCache bool) (url.Values, error) {
uid := cookieValue(cookie, "acf_uid")
stk := cookieValue(cookie, "acf_stk")
ltkid := cookieValue(cookie, "acf_ltkid")
@@ -335,8 +345,10 @@ func (c *DouyuClient) xiaodianEmbedQuery(cookie string) (url.Values, error) {
return nil, errors.New("斗鱼 Cookie 缺少 acf_uid、acf_stk 或 acf_ltkid,无法获取和平小店授权")
}
cacheKey := uid + ":" + c.Activity.ActAlias + ":" + c.Activity.RoomID
if cached, ok := c.getXiaodianEmbedCache(cacheKey); ok {
return cached, nil
if useCache {
if cached, ok := c.getXiaodianEmbedCache(cacheKey); ok {
return cached, nil
}
}
endpoint, err := url.Parse(c.Activity.RoleParamURL)
if err != nil {
@@ -539,6 +551,7 @@ func (c *DouyuClient) xiaodianProducts(cookie string, options types.XiaodianProd
products = append(products, types.Product{
CommodityID: item.GoodsID,
CommodityName: item.GoodsName,
ActionID: string(item.ActionID),
Category: string(item.Category),
Score: price,
PayType: payType,
@@ -1231,7 +1244,7 @@ func (c *DouyuClient) xiaodianExchange(cookie string, product types.Product) (st
if strings.TrimSpace(product.CommodityID) == "" {
return "", errors.New("商品 ID 为空")
}
embedQuery, err := c.xiaodianEmbedQuery(cookie)
embedQuery, err := c.xiaodianEmbedQueryFresh(cookie)
if err != nil {
return "", err
}
@@ -1254,7 +1267,11 @@ func (c *DouyuClient) xiaodianExchange(cookie string, product types.Product) (st
query.Set("_jsvar", "buyInfo")
query.Set("acctype", "livelink")
query.Set("_app_id", "2123")
query.Set("iActionId", c.Activity.ActID)
actionID := strings.TrimSpace(product.ActionID)
if actionID == "" {
actionID = c.Activity.ActID
}
query.Set("iActionId", actionID)
query.Set("propid", product.CommodityID)
query.Set("paytype", strconv.Itoa(payType))
query.Set("source", c.Activity.RoomID)
+4 -1
View File
@@ -177,7 +177,7 @@ func TestProductsAutomaticallyResolvesXiaodianRoleAndPaginates(t *testing.T) {
} else {
switch offset {
case "0":
items = []map[string]string{{"iGoodsId": "g1", "sGoodsName": "商品一", "iCategoryId": "1", "iPrice": "10", "sPayType": "qpqb", "iGoodsLeft": "-1"}, {"iGoodsId": "g2", "sGoodsName": "特惠商品", "iCategoryId": "45", "iPrice": "0", "iJb2Price": "20", "sPayType": "jb2", "iGoodsLeft": "3"}}
items = []map[string]string{{"iGoodsId": "g1", "iActionId": "act-g1", "sGoodsName": "商品一", "iCategoryId": "1", "iPrice": "10", "sPayType": "qpqb", "iGoodsLeft": "-1"}, {"iGoodsId": "g2", "sGoodsName": "特惠商品", "iCategoryId": "45", "iPrice": "0", "iJb2Price": "20", "sPayType": "jb2", "iGoodsLeft": "3"}}
case "2":
items = []map[string]string{{"iGoodsId": "g2", "sGoodsName": "重复商品", "iCategoryId": "45", "iPrice": "0", "iJb2Price": "20", "sPayType": "jb2", "iGoodsLeft": "3"}, {"iGoodsId": "g4", "sGoodsName": "商品四", "iCategoryId": "1", "iPrice": "40", "sPayType": "qpqb", "iGoodsLeft": "0"}}
}
@@ -219,6 +219,9 @@ func TestProductsAutomaticallyResolvesXiaodianRoleAndPaginates(t *testing.T) {
if products[1].Category != "45" || products[3].Score != 30 {
t.Fatalf("商品字段 = %#v", products)
}
if products[0].ActionID != "act-g1" {
t.Fatalf("商品动作 ID = %q", products[0].ActionID)
}
if products[0].PayType != 1 || products[0].PayLabel != "点券" || products[1].PayType != 5 || products[1].PayLabel != "扭蛋碎片" || products[1].Score != 20 {
t.Fatalf("支付字段 = %#v", products)
}
+7
View File
@@ -12,6 +12,10 @@ import (
// runAttempt 标准模式兑换:完整执行鉴权 token→BatchLimit→PreExchange→Exchange 链路。
func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResult {
cli := s.clientSnapshot()
// 最终防护:任何旧入口在和平小店活动下都必须使用 livebuy。
if cli.Activity.IsXiaodian {
return s.runXiaodianAttempt(attempt)
}
result := types.ExchangeResult{
Account: attempt.Account.Name,
Product: attempt.Product.CommodityName,
@@ -101,6 +105,9 @@ func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResu
// runAttemptFast 激进模式的兑换逻辑,复用缓存的鉴权 token,跳过 BatchExchangeLimit 和 PreExchange。
func (s *Scheduler) runAttemptFast(attempt types.ExchangeAttempt) types.ExchangeResult {
cli := s.clientSnapshot()
if cli.Activity.IsXiaodian {
return s.runXiaodianAttempt(attempt)
}
result := types.ExchangeResult{
Account: attempt.Account.Name,
Product: attempt.Product.CommodityName,
+14 -12
View File
@@ -96,18 +96,20 @@ type Account struct {
type Product struct {
CommodityID string `json:"commodityId"`
CommodityName string `json:"commodityName"`
Category string `json:"category"`
Score int `json:"score"`
PayType int `json:"payType,omitempty"`
PayLabel string `json:"payLabel,omitempty"`
Count int `json:"count"`
Pic string `json:"pic"`
Status int `json:"status"`
OpenStatus int `json:"openStatus"`
StoreStatus int `json:"storeStatus"`
TimeLimited int `json:"timeLimited"`
CountLimited int `json:"countLimited"`
EliteLimited int `json:"eliteLimited"`
// ActionID 是和平小店商品所属的动作 ID;不同商品可能不属于同一个动作。
ActionID string `json:"actionId,omitempty"`
Category string `json:"category"`
Score int `json:"score"`
PayType int `json:"payType,omitempty"`
PayLabel string `json:"payLabel,omitempty"`
Count int `json:"count"`
Pic string `json:"pic"`
Status int `json:"status"`
OpenStatus int `json:"openStatus"`
StoreStatus int `json:"storeStatus"`
TimeLimited int `json:"timeLimited"`
CountLimited int `json:"countLimited"`
EliteLimited int `json:"eliteLimited"`
}
// ProductPayload 前端商品响应。