补全过滤入口与直接兑换,优化兑换分类和 WSS 建连
- GUI 接线 Exchange 绑定:新增「直接兑换」按钮,结果带 category/message 并进入结果面板 - 补 -max-price/-min-left(CLI 与 GUI 设置),Target 的价格与库存过滤不再悬空 - 删除死代码 sendUpdateUserInfo 及其请求结构体(浏览器流程不含该帧) - ClassifyExchange 改为多路判定:合并 msg/actPreCondition/exchangeInfo 文案,status=200 且有 orderId 视为成功,新增 condition 分类(precondition 门禁) - WSS 建连改为锁外 dial、锁内注册,同 uid 并发调用共享一次握手 - 默认传输层统一为 http(CLI/GUI/README),.gitignore 忽略 .DS_Store - 同步 Wails 依赖升级(2.12.0 -> 2.15.0)并重新生成前端绑定
This commit is contained in:
+21
-6
@@ -48,6 +48,8 @@ type Config struct {
|
||||
PID int32 `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
TargetText string `json:"targetText"`
|
||||
MaxPrice int64 `json:"maxPrice"`
|
||||
MinLeft int64 `json:"minLeft"`
|
||||
CookieText string `json:"cookieText"`
|
||||
SelectedAccountLines []int `json:"selectedAccountLines"`
|
||||
Loops int `json:"loops"`
|
||||
@@ -103,9 +105,11 @@ type RecordResult struct {
|
||||
}
|
||||
|
||||
type ExchangeResult struct {
|
||||
UID int64 `json:"uid"`
|
||||
Resp *huya.ScoreExchangePrizeResp `json:"resp,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
UID int64 `json:"uid"`
|
||||
Resp *huya.ScoreExchangePrizeResp `json:"resp,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type MonitorState struct {
|
||||
@@ -138,7 +142,7 @@ func (a *App) Shutdown(ctx context.Context) {
|
||||
|
||||
func (a *App) DefaultConfig() Config {
|
||||
return Config{
|
||||
Transport: "wss",
|
||||
Transport: "http",
|
||||
WSSHost: huya.DefaultWSSHost,
|
||||
SID: 2203,
|
||||
Loops: 20,
|
||||
@@ -296,7 +300,13 @@ func (a *App) Exchange(cfg Config) ([]ExchangeResult, error) {
|
||||
ctx, cancel := requestContext(cfg.TimeoutSec)
|
||||
defer cancel()
|
||||
resp, err := client.ScoreExchangePrize(ctx, account.UID, account.Cookie, cfg.SID, cfg.PID)
|
||||
results[i] = ExchangeResult{UID: account.UID, Resp: resp, Error: errorString(err)}
|
||||
results[i] = ExchangeResult{
|
||||
UID: account.UID,
|
||||
Resp: resp,
|
||||
Category: sniper.ClassifyExchange(resp, err),
|
||||
Message: sniper.ExchangeMessage(resp),
|
||||
Error: errorString(err),
|
||||
}
|
||||
})
|
||||
a.addLog("info", "直接兑换完成: sid=%d pid=%d accounts=%d", cfg.SID, cfg.PID, len(accounts))
|
||||
return results, nil
|
||||
@@ -530,7 +540,7 @@ func (a *App) emit(event string, data any) {
|
||||
|
||||
func normalizeConfig(cfg Config) Config {
|
||||
if strings.TrimSpace(cfg.Transport) == "" {
|
||||
cfg.Transport = "wss"
|
||||
cfg.Transport = "http"
|
||||
}
|
||||
if strings.TrimSpace(cfg.WSSHost) == "" {
|
||||
cfg.WSSHost = huya.DefaultWSSHost
|
||||
@@ -582,6 +592,11 @@ func parseTargets(cfg Config) []sniper.Target {
|
||||
}
|
||||
targets = append(targets, sniper.Target{Name: item})
|
||||
}
|
||||
// 价格/库存过滤对所有目标统一生效,避免逐处重复赋值。
|
||||
for i := range targets {
|
||||
targets[i].MaxPrice = cfg.MaxPrice
|
||||
targets[i].MinLeft = cfg.MinLeft
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
|
||||
@@ -98,3 +98,35 @@ func TestAddLogDoesNotBlockWhenQueueIsFull(t *testing.T) {
|
||||
t.Fatalf("droppedLogs = %d, want 1", dropped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetsAppliesPriceAndInventoryFilters(t *testing.T) {
|
||||
targets := parseTargets(Config{
|
||||
PID: 12889,
|
||||
Name: "小黄鸭",
|
||||
TargetText: "13001\nauto",
|
||||
AutoSelect: true,
|
||||
MaxPrice: 1000,
|
||||
MinLeft: 2,
|
||||
})
|
||||
if len(targets) != 4 {
|
||||
t.Fatalf("targets=%d, want 4", len(targets))
|
||||
}
|
||||
for _, target := range targets {
|
||||
if target.MaxPrice != 1000 {
|
||||
t.Fatalf("target %+v maxPrice=%d, want 1000", target, target.MaxPrice)
|
||||
}
|
||||
if target.MinLeft != 2 {
|
||||
t.Fatalf("target %+v minLeft=%d, want 2", target, target.MinLeft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetsWithoutFiltersKeepsZeroValues(t *testing.T) {
|
||||
targets := parseTargets(Config{PID: 12889})
|
||||
if len(targets) != 1 {
|
||||
t.Fatalf("targets=%d, want 1", len(targets))
|
||||
}
|
||||
if targets[0].MaxPrice != 0 || targets[0].MinLeft != 0 {
|
||||
t.Fatalf("unexpected filters: %+v", targets[0])
|
||||
}
|
||||
}
|
||||
|
||||
+49
-63
@@ -35,8 +35,13 @@ type WSSClient struct {
|
||||
Logger func(string)
|
||||
CallTimeout time.Duration
|
||||
|
||||
// dialFunc 仅用于测试注入,为 nil 时走 dialSession。
|
||||
dialFunc func(ctx context.Context, uid int64, cookie string) (*WSSSession, error)
|
||||
|
||||
mu sync.Mutex
|
||||
sessions map[string]*WSSSession
|
||||
// dialing 记录正在建连的 uid,避免并发调用重复握手。
|
||||
dialing map[string]chan struct{}
|
||||
}
|
||||
|
||||
type WSSSession struct {
|
||||
@@ -73,6 +78,7 @@ func NewWSSClient(logger func(string)) *WSSClient {
|
||||
Logger: logger,
|
||||
CallTimeout: 10 * time.Second,
|
||||
sessions: map[string]*WSSSession{},
|
||||
dialing: map[string]chan struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,26 +111,54 @@ func (c *WSSClient) PrewarmActivity(ctx context.Context, uid int64, cookie strin
|
||||
return err
|
||||
}
|
||||
|
||||
// session 返回 uid 对应的连接,必要时新建。
|
||||
//
|
||||
// 握手与初始化在锁外完成,同一 uid 的并发调用共享同一次建连(single-flight),
|
||||
// 因此多账号预热/抢购时不会因为持有全局锁而串行化。
|
||||
func (c *WSSClient) session(ctx context.Context, uid int64, cookie string) (*WSSSession, error) {
|
||||
key := strconv.FormatInt(uid, 10)
|
||||
c.mu.Lock()
|
||||
if session := c.sessions[key]; session != nil && !session.isClosed() {
|
||||
c.mu.Unlock()
|
||||
return session, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
for {
|
||||
c.mu.Lock()
|
||||
if session := c.sessions[key]; session != nil && !session.isClosed() {
|
||||
c.mu.Unlock()
|
||||
return session, nil
|
||||
}
|
||||
dialing := c.dialing[key]
|
||||
if dialing == nil {
|
||||
dialing = make(chan struct{})
|
||||
c.dialing[key] = dialing
|
||||
c.mu.Unlock()
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if session := c.sessions[key]; session != nil && !session.isClosed() {
|
||||
return session, nil
|
||||
session, err := c.dial(ctx, uid, cookie)
|
||||
|
||||
c.mu.Lock()
|
||||
delete(c.dialing, key)
|
||||
if err == nil {
|
||||
c.sessions[key] = session
|
||||
}
|
||||
c.mu.Unlock()
|
||||
close(dialing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
// 已有同 uid 的建连在途,等它结束后复用结果或自行重试。
|
||||
select {
|
||||
case <-dialing:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
session, err := c.dialSession(ctx, uid, cookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (c *WSSClient) dial(ctx context.Context, uid int64, cookie string) (*WSSSession, error) {
|
||||
if c.dialFunc != nil {
|
||||
return c.dialFunc(ctx, uid, cookie)
|
||||
}
|
||||
c.sessions[key] = session
|
||||
return session, nil
|
||||
return c.dialSession(ctx, uid, cookie)
|
||||
}
|
||||
|
||||
func (c *WSSClient) dialSession(ctx context.Context, uid int64, cookie string) (*WSSSession, error) {
|
||||
@@ -383,15 +417,6 @@ func (s *WSSSession) callGetConfig(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendUpdateUserInfo(ctx context.Context, launchGUID string) error {
|
||||
req := &wsUpdateUserInfoReq{AppSrc: "HUYA&ZH&2052", GUID: launchGUID}
|
||||
payload, err := encodeWSSStructCommand(WSSCmdUpdateUserInfoReq, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendUpdateUserExps(ctx context.Context) error {
|
||||
payload, err := encodeWSSStructCommand(WSSCmdUpdateUserExpsReq, &wsUpdateUserExpsReq{})
|
||||
if err != nil {
|
||||
@@ -631,45 +656,6 @@ func (r *wsLaunchReq) WriteTo(w *taf.Writer) error {
|
||||
return w.WriteStructEnd()
|
||||
}
|
||||
|
||||
type wsUpdateUserInfoReq struct {
|
||||
AppSrc string
|
||||
GUID string
|
||||
}
|
||||
|
||||
func (r *wsUpdateUserInfoReq) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteString(0, r.AppSrc); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(1, r.GUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(2, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(3, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteStructBegin(6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(0, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(1, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(2, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteStructEnd(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(7, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteInt32(8, 0)
|
||||
}
|
||||
|
||||
type wsUpdateUserExpsReq struct{}
|
||||
|
||||
func (r *wsUpdateUserExpsReq) WriteTo(w *taf.Writer) error {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package huya
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"huya-go/internal/taf"
|
||||
)
|
||||
@@ -96,3 +100,63 @@ func TestNewWSSCommandTraceMatchesBrowserShape(t *testing.T) {
|
||||
t.Fatalf("trace suffix = %q, want :0:0", trace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionSharesSingleDialPerUID(t *testing.T) {
|
||||
client := NewWSSClient(nil)
|
||||
dialStarted := make(chan struct{}, 1)
|
||||
release := make(chan struct{})
|
||||
var dials atomic.Int32
|
||||
client.dialFunc = func(ctx context.Context, uid int64, cookie string) (*WSSSession, error) {
|
||||
dials.Add(1)
|
||||
select {
|
||||
case dialStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
<-release
|
||||
return &WSSSession{
|
||||
client: client,
|
||||
uid: uid,
|
||||
cookie: cookie,
|
||||
closed: make(chan struct{}),
|
||||
pending: map[int32]chan rpcResult{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const callers = 4
|
||||
sessions := make([]*WSSSession, callers)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < callers; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
session, err := client.session(context.Background(), 42, "yyuid=42")
|
||||
if err != nil {
|
||||
t.Errorf("session: %v", err)
|
||||
return
|
||||
}
|
||||
sessions[i] = session
|
||||
}(i)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-dialStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("dial was not started")
|
||||
}
|
||||
// 留出时间让其余调用进入等待分支。
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
close(release)
|
||||
wg.Wait()
|
||||
|
||||
if got := dials.Load(); got != 1 {
|
||||
t.Fatalf("dials=%d, want 1", got)
|
||||
}
|
||||
for i, session := range sessions {
|
||||
if session == nil {
|
||||
t.Fatalf("caller %d got nil session", i)
|
||||
}
|
||||
if session != sessions[0] {
|
||||
t.Fatalf("caller %d got a different session", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+77
-25
@@ -24,6 +24,9 @@ const (
|
||||
CategorySoldOut = "sold_out"
|
||||
CategoryFailed = "failed"
|
||||
CategoryError = "error"
|
||||
// CategoryCondition 表示服务端返回了 precondition 门禁(条件未达成),
|
||||
// 没有可识别文案,按可重试失败处理但单独分类便于观察。
|
||||
CategoryCondition = "condition"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
@@ -314,7 +317,7 @@ func (r *Runner) exchangeTarget(ctx context.Context, client huya.ActivityClient,
|
||||
exchange, err := client.ScoreExchangePrize(ctx, account.UID, account.Cookie, sid, pid)
|
||||
result.Exchange = exchange
|
||||
result.Category = ClassifyExchange(exchange, err)
|
||||
result.Message = exchangeMessage(exchange)
|
||||
result.Message = ExchangeMessage(exchange)
|
||||
if err != nil {
|
||||
result.Action = "exchange_error"
|
||||
result.Error = err.Error()
|
||||
@@ -354,11 +357,7 @@ func (r *Runner) applyExchangeDecision(state *runState, account Account, index i
|
||||
retryAt := now.Add(cfg.TooFrequentBackoff)
|
||||
state.setRetryAfter(account.UID, index, retryAt)
|
||||
result.RetryAfter = retryAt.UnixMilli()
|
||||
case CategoryError:
|
||||
retryAt := now.Add(cfg.ErrorBackoff)
|
||||
state.setRetryAfter(account.UID, index, retryAt)
|
||||
result.RetryAfter = retryAt.UnixMilli()
|
||||
case CategoryFailed:
|
||||
case CategoryError, CategoryFailed, CategoryCondition:
|
||||
retryAt := now.Add(cfg.ErrorBackoff)
|
||||
state.setRetryAfter(account.UID, index, retryAt)
|
||||
result.RetryAfter = retryAt.UnixMilli()
|
||||
@@ -609,6 +608,64 @@ func autoPrize(prizes []huya.PrizeInfo, target Target) *huya.PrizeInfo {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// exchangeSignals 汇总一次兑换响应里可用于判定的信号。
|
||||
//
|
||||
// 虎牙把失败原因分散在 msg / actPreCondition(Tip/ButtonDesc/ConditionParams)
|
||||
// 和 exchangeInfo.desc 里,status 除 200 外没有可依据的公开语义,
|
||||
// 所以判定顺序是:文案 -> 成功结构(status=200 且有 orderId)-> 结构门禁 -> 兜底失败。
|
||||
type exchangeSignals struct {
|
||||
text string
|
||||
status int32
|
||||
hasOrderID bool
|
||||
conditionType int32
|
||||
}
|
||||
|
||||
func buildExchangeSignals(resp *huya.ScoreExchangePrizeResp) exchangeSignals {
|
||||
if resp == nil {
|
||||
return exchangeSignals{}
|
||||
}
|
||||
parts := []string{
|
||||
resp.Msg,
|
||||
resp.ActPreCondition.Tip,
|
||||
resp.ActPreCondition.ButtonDesc,
|
||||
resp.ExchangeInfo.Desc,
|
||||
}
|
||||
for _, value := range resp.ActPreCondition.ConditionParams {
|
||||
parts = append(parts, value)
|
||||
}
|
||||
texts := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if item := strings.TrimSpace(part); item != "" {
|
||||
texts = append(texts, item)
|
||||
}
|
||||
}
|
||||
return exchangeSignals{
|
||||
text: strings.Join(texts, " "),
|
||||
status: resp.Status,
|
||||
hasOrderID: strings.TrimSpace(resp.OrderID) != "",
|
||||
conditionType: resp.ActPreCondition.ConditionType,
|
||||
}
|
||||
}
|
||||
|
||||
// classifyExchangeText 只依据文案判定失败原因,命中时返回分类。
|
||||
func classifyExchangeText(text string) (string, bool) {
|
||||
switch {
|
||||
case containsAny(text, "操作频繁", "太频繁", "稍后再试"):
|
||||
return CategoryTooFrequent, true
|
||||
case containsAny(text, "暂未开放", "未开放", "未开始", "活动未开始"):
|
||||
return CategoryNotOpen, true
|
||||
case containsAny(text, "已抢完", "已兑完", "库存不足", "没有库存", "领完了"):
|
||||
return CategorySoldOut, true
|
||||
case containsAny(text, "货币不足", "积分不足", "余额不足", "点券不足", "不足"):
|
||||
return CategoryNotEnough, true
|
||||
case containsAny(text, "已兑换", "重复兑换", "已领取", "超过兑换次数", "达到上限", "限购"):
|
||||
return CategoryAlreadyDone, true
|
||||
case containsAny(text, "奖品已下架", "已下架", "奖品不存在", "活动不存在"):
|
||||
return CategoryOffShelf, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func ClassifyExchange(resp *huya.ScoreExchangePrizeResp, err error) string {
|
||||
if err != nil {
|
||||
return CategoryError
|
||||
@@ -619,28 +676,23 @@ func ClassifyExchange(resp *huya.ScoreExchangePrizeResp, err error) string {
|
||||
if resp.Success() {
|
||||
return CategorySuccess
|
||||
}
|
||||
msg := exchangeMessage(resp)
|
||||
switch {
|
||||
case containsAny(msg, "操作频繁", "太频繁", "稍后再试"):
|
||||
return CategoryTooFrequent
|
||||
case containsAny(msg, "暂未开放", "未开放", "未开始", "活动未开始"):
|
||||
return CategoryNotOpen
|
||||
case containsAny(msg, "已抢完", "已兑完", "库存不足", "没有库存", "领完了"):
|
||||
return CategorySoldOut
|
||||
case containsAny(msg, "货币不足", "积分不足", "余额不足", "点券不足", "不足"):
|
||||
return CategoryNotEnough
|
||||
case containsAny(msg, "已兑换", "重复兑换", "已领取", "超过兑换次数", "达到上限", "限购"):
|
||||
return CategoryAlreadyDone
|
||||
case containsAny(msg, "奖品已下架", "已下架", "奖品不存在", "活动不存在"):
|
||||
return CategoryOffShelf
|
||||
case resp.Status != 0:
|
||||
return CategoryFailed
|
||||
default:
|
||||
return CategoryFailed
|
||||
signals := buildExchangeSignals(resp)
|
||||
if category, ok := classifyExchangeText(signals.text); ok {
|
||||
return category
|
||||
}
|
||||
// 成功回包也可能不带中文文案:status=200 且有 orderId 视为成功。
|
||||
if signals.status == 200 && signals.hasOrderID {
|
||||
return CategorySuccess
|
||||
}
|
||||
// 没有文案但服务端回了 precondition 门禁,归为条件未达成,按可重试失败处理。
|
||||
if signals.conditionType != 0 {
|
||||
return CategoryCondition
|
||||
}
|
||||
return CategoryFailed
|
||||
}
|
||||
|
||||
func exchangeMessage(resp *huya.ScoreExchangePrizeResp) string {
|
||||
// ExchangeMessage 返回响应里最有代表性的提示文案,供日志与 GUI 展示。
|
||||
func ExchangeMessage(resp *huya.ScoreExchangePrizeResp) string {
|
||||
if resp == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -83,6 +83,44 @@ func TestClassifyExchange(t *testing.T) {
|
||||
{name: "off shelf", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品已下架"}, want: CategoryOffShelf},
|
||||
{name: "sold out", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "库存不足"}, want: CategorySoldOut},
|
||||
{name: "error", err: errors.New("timeout"), want: CategoryError},
|
||||
{
|
||||
name: "success with order id",
|
||||
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "sOrderId_2203_12889_1_1783328404_957"},
|
||||
want: CategorySuccess,
|
||||
},
|
||||
{
|
||||
name: "not enough in precondition tip",
|
||||
resp: &huya.ScoreExchangePrizeResp{
|
||||
Status: 500,
|
||||
Msg: "兑换失败",
|
||||
ActPreCondition: huya.ActPreCondition{Tip: "积分不足,无法兑换"},
|
||||
},
|
||||
want: CategoryNotEnough,
|
||||
},
|
||||
{
|
||||
name: "not enough in condition params",
|
||||
resp: &huya.ScoreExchangePrizeResp{
|
||||
Status: 500,
|
||||
Msg: "兑换失败",
|
||||
ActPreCondition: huya.ActPreCondition{ConditionParams: map[string]string{"tip": "余额不足"}},
|
||||
},
|
||||
want: CategoryNotEnough,
|
||||
},
|
||||
{
|
||||
name: "condition gate without text",
|
||||
resp: &huya.ScoreExchangePrizeResp{
|
||||
Status: 500,
|
||||
ActPreCondition: huya.ActPreCondition{ConditionType: 2},
|
||||
},
|
||||
want: CategoryCondition,
|
||||
},
|
||||
{name: "empty response", resp: &huya.ScoreExchangePrizeResp{Status: 500}, want: CategoryFailed},
|
||||
{name: "nil response", resp: nil, want: CategoryFailed},
|
||||
{
|
||||
name: "text wins over order id",
|
||||
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "order", Msg: "库存不足"},
|
||||
want: CategorySoldOut,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -253,3 +291,29 @@ func TestRunTargetsRefreshAfterSuccessEmitsRefreshResults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunTargetsConditionGateKeepsRetrying(t *testing.T) {
|
||||
client := &fakeActivityClient{
|
||||
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
|
||||
return &huya.ScoreExchangePrizeResp{
|
||||
Status: 500,
|
||||
ActPreCondition: huya.ActPreCondition{ConditionType: 3},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
runner := Runner{Client: client}
|
||||
|
||||
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
|
||||
SID: 2203,
|
||||
Loops: 3,
|
||||
Delay: 10 * time.Millisecond,
|
||||
ErrorBackoff: time.Millisecond,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, exchangeCount, _ := client.counts()
|
||||
if exchangeCount != 3 {
|
||||
t.Fatalf("exchangeCount=%d, want 3", exchangeCount)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user