package app import ( "fmt" "strconv" "strings" "sync" "time" "douyu_go/internal/logger" "douyu_go/internal/types" ) const exchangeLookupConcurrency = 8 const xiaodianExchangeLookupConcurrency = 4 type exchangeLookupResult struct { Account types.Account Records []types.ExchangeRecord Err error EmptyCookie bool } // QueryScore 查询单个账号的积分。 func (a *App) QueryScore(accountName string) types.ScoreResult { if accountName == "" { return types.ScoreResult{Score: "缺少账号名"} } cookie, ok := a.scheduler.AccountCookieByName(accountName) if !ok || cookie == "" { return types.ScoreResult{Score: fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)} } score, err := a.getClient().Score(cookie) if err != nil { return types.ScoreResult{Score: "查询失败: " + err.Error()} } return types.ScoreResult{Score: strconv.Itoa(score)} } // QueryXiaodianAssets 查询勾选账号的和平小店点券和扭蛋碎片。 func (a *App) QueryXiaodianAssets(accountNames []string) types.XiaodianAssetsResult { results := make(map[string]types.XiaodianAssets) if !a.getClient().Activity.IsXiaodian { return types.XiaodianAssetsResult{Results: results} } selected := make(map[string]struct{}, len(accountNames)) for _, name := range accountNames { if name = strings.TrimSpace(name); name != "" { selected[name] = struct{}{} } } for _, account := range a.scheduler.AccountSnapshot() { if _, ok := selected[account.Name]; !ok { continue } if strings.TrimSpace(account.Cookie) == "" { results[account.Name] = types.XiaodianAssets{Error: "Cookie 为空"} continue } assets, err := a.getClient().XiaodianAssets(account.Cookie) if err != nil { results[account.Name] = types.XiaodianAssets{Error: "查询失败: " + err.Error()} continue } results[account.Name] = assets } return types.XiaodianAssetsResult{Results: results} } // QueryXiaodianRoles 查询勾选账号当前绑定的和平小店角色。 func (a *App) QueryXiaodianRoles(accountNames []string) types.XiaodianRoleResult { results := make(map[string]types.XiaodianRole) if !a.getClient().Activity.IsXiaodian { return types.XiaodianRoleResult{Results: results} } selected := make(map[string]struct{}, len(accountNames)) for _, name := range accountNames { if name = strings.TrimSpace(name); name != "" { selected[name] = struct{}{} } } for _, account := range a.scheduler.AccountSnapshot() { if _, ok := selected[account.Name]; !ok { continue } if strings.TrimSpace(account.Cookie) == "" { results[account.Name] = types.XiaodianRole{Error: "Cookie 为空"} continue } role, err := a.getClient().XiaodianRole(account.Cookie) if err != nil { results[account.Name] = types.XiaodianRole{Error: "查询失败: " + err.Error()} continue } results[account.Name] = role } return types.XiaodianRoleResult{Results: results} } // GetXiaodianBindQRCode 为指定账号生成和平小店角色绑定二维码。 func (a *App) GetXiaodianBindQRCode(accountName string) types.XiaodianBindQRCode { accountName = strings.TrimSpace(accountName) if !a.getClient().Activity.IsXiaodian { return types.XiaodianBindQRCode{Error: "当前活动不是和平小店"} } cookie, ok := a.scheduler.AccountCookieByName(accountName) if !ok || strings.TrimSpace(cookie) == "" { return types.XiaodianBindQRCode{Error: "账号不存在或 Cookie 为空"} } urlValue, bindURL, err := a.getClient().XiaodianBindQRCode(cookie) if err != nil { return types.XiaodianBindQRCode{Error: "获取绑定二维码失败: " + err.Error()} } return types.XiaodianBindQRCode{ Success: true, AccountName: accountName, URL: urlValue, GameRoleBindURL: bindURL, } } // BindXiaodianRole 确认扫码后的绑定,并返回最新角色信息。 func (a *App) BindXiaodianRole(accountName string) types.XiaodianRole { accountName = strings.TrimSpace(accountName) if !a.getClient().Activity.IsXiaodian { return types.XiaodianRole{Error: "当前活动不是和平小店"} } cookie, ok := a.scheduler.AccountCookieByName(accountName) if !ok || strings.TrimSpace(cookie) == "" { return types.XiaodianRole{Error: "账号不存在或 Cookie 为空"} } role, err := a.getClient().XiaodianBindRole(cookie) if err != nil { return types.XiaodianRole{Error: "绑定失败: " + err.Error()} } return role } // VerifyExchange 查询兑换记录,用于复验抢购结果。 func (a *App) VerifyExchange(accountName string) types.ExchangeRecordsResult { if accountName == "" { return types.ExchangeRecordsResult{Error: "缺少账号名"} } cookie, ok := a.scheduler.AccountCookieByName(accountName) if !ok || cookie == "" { return types.ExchangeRecordsResult{Error: fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)} } records, err := a.getClient().ExchangeList(cookie) if err != nil { return types.ExchangeRecordsResult{Error: "查询失败: " + err.Error()} } matched := filterExchangeRecords(records) return types.ExchangeRecordsResult{ Success: true, Records: matched, Count: len(matched), } } // VerifyAllExchange 批量查询兑换记录,使用有限并发减少等待时间。 func (a *App) VerifyAllExchange() types.VerifyAllExchangeResult { accounts := a.scheduler.AccountSnapshot() results := make(map[string]types.AccountExchangeResult) lookups := a.lookupExchangeRecords("复验", accounts) for _, lookup := range lookups { if lookup.EmptyCookie { results[lookup.Account.Name] = types.AccountExchangeResult{Error: "Cookie 为空"} continue } if lookup.Err != nil { results[lookup.Account.Name] = types.AccountExchangeResult{Error: "查询失败: " + lookup.Err.Error()} continue } matched := filterExchangeRecords(lookup.Records) results[lookup.Account.Name] = types.AccountExchangeResult{ Success: true, Records: matched, Count: len(matched), } } return types.VerifyAllExchangeResult{Results: results} } // QueryXiaodianExchangeRecords 查询所有账号的和平小店真实订单,并标记当前商品是否已兑换。 func (a *App) QueryXiaodianExchangeRecords(commodityID, commodityName string) types.XiaodianExchangeRecordsResult { result := types.XiaodianExchangeRecordsResult{ ProductID: strings.TrimSpace(commodityID), ProductName: strings.TrimSpace(commodityName), Results: make(map[string]types.XiaodianExchangeAccountResult), } if !a.getClient().Activity.IsXiaodian { result.Error = "当前活动不是和平小店" return result } if result.ProductID == "" { result.Error = "请先选择商品" return result } accounts := a.scheduler.AccountSnapshot() if len(accounts) == 0 { result.Success = true return result } start := time.Now() logger.LogEvent("兑换记录", "info", "和平小店兑换记录批量查询开始: 账号=%d 并发=%d", len(accounts), xiaodianExchangeLookupConcurrency) type lookup struct { account types.Account records []types.XiaodianExchangeRecord err error } lookups := make([]lookup, len(accounts)) jobs := make(chan int) workerCount := xiaodianExchangeLookupConcurrency if len(accounts) < workerCount { workerCount = len(accounts) } var wg sync.WaitGroup for worker := 0; worker < workerCount; worker++ { wg.Add(1) go func() { defer wg.Done() for index := range jobs { account := accounts[index] item := lookup{account: account} if strings.TrimSpace(account.Cookie) == "" { item.err = fmt.Errorf("Cookie 为空") } else { item.records, item.err = a.getClient().XiaodianExchangeList(account.Cookie) } lookups[index] = item } }() } for index := range accounts { jobs <- index } close(jobs) wg.Wait() failed := 0 matched := 0 for _, lookup := range lookups { if lookup.err != nil { failed++ result.Results[lookup.account.Name] = types.XiaodianExchangeAccountResult{Error: lookup.err.Error()} continue } isMatched := false for _, record := range lookup.records { if record.CommodityID == result.ProductID { isMatched = true break } } if isMatched { matched++ } result.Results[lookup.account.Name] = types.XiaodianExchangeAccountResult{ Success: true, Records: lookup.records, Matched: isMatched, } } result.Success = true level := "success" if failed > 0 { level = "warn" } logger.LogEvent("兑换记录", level, "和平小店兑换记录批量查询完成: 账号=%d 命中=%d 失败=%d 耗时=%s", len(accounts), matched, failed, time.Since(start).Round(time.Millisecond)) return result } // VerifyAndExport 复验并导出结果。 func (a *App) VerifyAndExport(commodityID string) types.VerifyAndExportResult { accounts := a.scheduler.AccountSnapshot() results := make(map[string]types.AccountHitResult) hitAccounts := make([]string, 0) missAccounts := make([]string, 0) lookups := a.lookupExchangeRecords("复验", accounts) for _, lookup := range lookups { if lookup.EmptyCookie { missAccounts = append(missAccounts, lookup.Account.Name) results[lookup.Account.Name] = types.AccountHitResult{Error: "Cookie 为空"} continue } if lookup.Err != nil { missAccounts = append(missAccounts, lookup.Account.Name) results[lookup.Account.Name] = types.AccountHitResult{Error: "查询失败: " + lookup.Err.Error()} continue } // 检查是否抢到了指定商品 hit := hasCommodityRecord(lookup.Records, commodityID, 0, 0) if hit { hitAccounts = append(hitAccounts, lookup.Account.Name) results[lookup.Account.Name] = types.AccountHitResult{Success: true, Hit: true} } else { missAccounts = append(missAccounts, lookup.Account.Name) results[lookup.Account.Name] = types.AccountHitResult{Success: true} } } return types.VerifyAndExportResult{ Success: true, HitCount: len(hitAccounts), MissCount: len(missAccounts), HitAccounts: hitAccounts, MissAccounts: missAccounts, Details: results, } } func (a *App) lookupExchangeRecords(category string, accounts []types.Account) []exchangeLookupResult { results := make([]exchangeLookupResult, len(accounts)) if len(accounts) == 0 { return results } workerCount := exchangeLookupConcurrency if len(accounts) < workerCount { workerCount = len(accounts) } start := time.Now() logger.LogEvent(category, "info", "兑换记录批量查询开始: 账号=%d 并发=%d", len(accounts), workerCount) jobs := make(chan int) var wg sync.WaitGroup for worker := 0; worker < workerCount; worker++ { wg.Add(1) go func() { defer wg.Done() for idx := range jobs { account := accounts[idx] lookup := exchangeLookupResult{Account: account} if strings.TrimSpace(account.Cookie) == "" { lookup.EmptyCookie = true results[idx] = lookup continue } records, err := a.getClient().ExchangeList(account.Cookie) lookup.Records = records lookup.Err = err results[idx] = lookup } }() } for idx := range accounts { jobs <- idx } close(jobs) wg.Wait() errors := 0 empty := 0 for _, result := range results { if result.EmptyCookie { empty++ } if result.Err != nil { errors++ } } level := "success" if errors > 0 { level = "warn" } logger.LogEvent(category, level, "兑换记录批量查询完成: 账号=%d 空CK=%d 失败=%d 耗时=%s", len(accounts), empty, errors, time.Since(start).Round(time.Millisecond)) return results } func filterExchangeRecords(records []types.ExchangeRecord) []types.ExchangeRecord { matched := make([]types.ExchangeRecord, 0, len(records)) for _, r := range records { if r.CommodityID != "" { matched = append(matched, r) } } return matched } func hasCommodityRecord(records []types.ExchangeRecord, commodityID string, startTime, endTime int64) bool { if commodityID == "" { return false } for _, r := range records { if r.CommodityID != commodityID { continue } if startTime == 0 && endTime == 0 { return true } recordTime, err := strconv.ParseInt(r.Time, 10, 64) if err != nil { continue } if startTime > 0 && recordTime < startTime { continue } if endTime > 0 && recordTime > endTime { continue } return true } return false }