package service import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "sync" "time" "affiliate_dash/internal/model" ) // FlexibleFloat64 兼容反序列化 JSON 中的数字或字符串(如 "8271.2") type FlexibleFloat64 float64 func (f *FlexibleFloat64) UnmarshalJSON(b []byte) error { if len(b) == 0 || string(b) == "null" { *f = 0 return nil } var n float64 if err := json.Unmarshal(b, &n); err == nil { *f = FlexibleFloat64(n) return nil } var s string if err := json.Unmarshal(b, &s); err == nil { s = strings.TrimSpace(s) if s == "" { *f = 0 return nil } val, err := strconv.ParseFloat(s, 64) if err != nil { *f = 0 return nil } *f = FlexibleFloat64(val) return nil } *f = 0 return nil } func (f FlexibleFloat64) MarshalJSON() ([]byte, error) { return json.Marshal(float64(f)) } func (f FlexibleFloat64) Float64() float64 { return float64(f) } // RockClient 负责与 Rock 上游平台进行 HTTP 通信。 // // baseURL 在运行期可被后台配置改动(见 RockService.UpdateConfig),而读取方 // (HTTP 请求、库存巡检 goroutine)是并发的,因此必须加锁访问。 type RockClient struct { mu sync.RWMutex baseURL string httpClient *http.Client } func NewRockClient(baseURL string) *RockClient { c := &RockClient{ httpClient: &http.Client{ Timeout: 15 * time.Second, }, } c.SetBaseURL(baseURL) return c } // BaseURL 返回当前生效的上游地址。 func (c *RockClient) BaseURL() string { if c == nil { return "" } c.mu.RLock() defer c.mu.RUnlock() return c.baseURL } // SetBaseURL 更新上游地址;传入空值或只有空白时回落到内置默认地址。 func (c *RockClient) SetBaseURL(baseURL string) { if c == nil { return } base := strings.TrimRight(strings.TrimSpace(baseURL), "/") if base == "" { base = model.RockDefaultBaseURL } c.mu.Lock() c.baseURL = base c.mu.Unlock() } type RockMeResponse struct { User struct { ID uint64 `json:"id"` Username string `json:"username"` Role string `json:"role"` Points FlexibleFloat64 `json:"points"` Status string `json:"status"` } `json:"user"` Error string `json:"error,omitempty"` } type RockClaimedToken struct { ID int64 `json:"id"` Token string `json:"token"` Color string `json:"color"` Region string `json:"region"` Status string `json:"status"` RedeemCode string `json:"redeem_code"` ClaimedAt string `json:"claimed_at"` } type RockClaimResponse struct { Tokens []RockClaimedToken `json:"tokens"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` } type RockTokenData struct { ID int64 `json:"id"` RedeemCode string `json:"redeem_code"` Token string `json:"token"` Color string `json:"color"` Region string `json:"region"` Phone string `json:"phone"` Status string `json:"status"` BoundAccount string `json:"bound_account"` BoundAccountID string `json:"bound_account_id"` PreviousBoundAccount string `json:"previous_bound_account"` PreviousBoundAccountID string `json:"previous_bound_account_id"` BindChangedAt string `json:"bind_changed_at"` BindChangeType string `json:"bind_change_type"` RedeemSkin string `json:"redeem_skin"` RedeemCurrency string `json:"redeem_currency"` RedeemedAt string `json:"redeemed_at"` } func (t *RockTokenData) IsBound() bool { if t.Status == "bound" || t.Status == "redeemed" { return true } return strings.TrimSpace(t.BoundAccountID) != "" } func (t *RockTokenData) GetRoleName() string { return strings.TrimSpace(t.BoundAccount) } func (t *RockTokenData) GetRoleID() string { return strings.TrimSpace(t.BoundAccountID) } func (t *RockTokenData) GetChannel() string { return strings.TrimSpace(t.Region) } type RockRedeemDetailResponse struct { ID int64 `json:"id"` RedeemCode string `json:"redeem_code"` Color string `json:"color"` Status string `json:"status"` BindURL string `json:"bindUrl"` BindError string `json:"bindError"` QRURL string `json:"qrUrl"` Token RockTokenData `json:"token"` ExpiresAt string `json:"expires_at"` Error string `json:"error,omitempty"` } type RockDeliveryItem struct { SkinID int `json:"skinId"` Skin string `json:"skin"` CurrencyID int `json:"currencyId"` Currency string `json:"currency"` CurrencyQuantity int `json:"currencyQuantity"` Time string `json:"time"` Role string `json:"role"` Message string `json:"message"` FulfillID int64 `json:"fulfillId"` Membership int `json:"membership"` } type RockBindingInfo struct { Bound bool `json:"bound"` Role string `json:"role"` RoleID string `json:"roleId"` Region string `json:"region"` BindTime string `json:"bindTime"` } type RockRefreshResponse struct { Token RockTokenData `json:"token"` Binding RockBindingInfo `json:"binding"` DeliveryList []RockDeliveryItem `json:"deliveryList"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` } func (r *RockRefreshResponse) IsBound() bool { if r == nil { return false } if r.Token.IsBound() { return true } return r.Binding.Bound || strings.TrimSpace(r.Binding.RoleID) != "" } func (r *RockRefreshResponse) GetRoleName() string { if r == nil { return "" } if name := r.Token.GetRoleName(); name != "" { return name } return strings.TrimSpace(r.Binding.Role) } func (r *RockRefreshResponse) GetRoleID() string { if r == nil { return "" } if id := r.Token.GetRoleID(); id != "" { return id } return strings.TrimSpace(r.Binding.RoleID) } func (r *RockRefreshResponse) GetChannel() string { if r == nil { return "" } if channel := r.Token.GetChannel(); channel != "" { return channel } return strings.TrimSpace(r.Binding.Region) } type RockRedeemResponse struct { Token RockTokenData `json:"token"` Status string `json:"status"` Message string `json:"message"` Error string `json:"error,omitempty"` } func (r *RockRedeemResponse) IsSuccess() bool { return r.Status == "success" || r.Token.Status == "redeemed" } type RockExchangeRecordsResponse struct { Records []struct { ID int64 `json:"id"` TokenID int64 `json:"token_id"` Skin string `json:"skin"` Currency string `json:"currency"` Quantity int `json:"quantity"` Role string `json:"role"` RoleID string `json:"role_id"` FulfillID int64 `json:"fulfill_id"` RedeemedAt string `json:"redeemed_at"` ClaimedAt string `json:"claimed_at"` RedeemCode string `json:"redeem_code"` DeliveryUser string `json:"delivery_user"` } `json:"records"` Pagination struct { Page int `json:"page"` PageSize int `json:"pageSize"` Total int `json:"total"` TotalPages int `json:"totalPages"` } `json:"pagination"` Error string `json:"error,omitempty"` } // RockPackage 是上游打包记录(/api/user/packages)的列表项。 type RockPackage struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` TokenCount int `json:"token_count"` ShareCode string `json:"share_code"` Status string `json:"status"` CreatedAt string `json:"created_at"` Note string `json:"note"` } // RockPackageToken 是打包详情(/api/package/{share_code})里的 Token 原始结构。 // 字段命名与 0666.pro 返回保持一致,便于把整条 JSON 原样存入 raw_data。 type RockPackageToken struct { ID int64 `json:"id"` RedeemCode string `json:"redeem_code"` Token string `json:"token"` Color string `json:"color"` Region string `json:"region"` Phone string `json:"phone"` Status string `json:"status"` BoundAccount string `json:"bound_account"` BoundAccountID string `json:"bound_account_id"` PreviousBoundAccount string `json:"previous_bound_account"` PreviousBoundAccountID string `json:"previous_bound_account_id"` BindChangedAt string `json:"bind_changed_at"` BindChangeType string `json:"bind_change_type"` RedeemSkin string `json:"redeem_skin"` RedeemCurrency string `json:"redeem_currency"` RedeemedAt string `json:"redeemed_at"` ClaimGroup string `json:"claim_group"` ClaimRound int `json:"claim_round"` ClaimNo int `json:"claim_no"` ExpiresAt string `json:"expires_at"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type RockPackageDetailResponse struct { Package RockPackage `json:"package"` Owner RockPackageOwner `json:"owner"` Tokens []RockPackageToken `json:"tokens"` Config map[string]any `json:"config"` Settings map[string]any `json:"settings"` Error string `json:"error,omitempty"` } type RockPackageOwner struct { Username string `json:"username"` Phone string `json:"phone"` } // RockPackageCreateResponse 是 POST /api/user/packages 的返回结构。 // 上游示例:{"package":{"id":1512,"token_count":2,"share_code":"1448fffa85bb3d4f913c",...}} type RockPackageCreateResponse struct { Package *RockPackage `json:"package"` Packages []RockPackage `json:"packages"` ShareCode string `json:"share_code"` URL string `json:"url"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` } // ResolvePackage 兼容 package / packages / share_code / url 四种返回形态,返回包 ID 与 share_code。 func (r *RockPackageCreateResponse) ResolvePackage() (int64, string) { if r == nil { return 0, "" } if r.Package != nil { if code := strings.TrimSpace(r.Package.ShareCode); code != "" { return r.Package.ID, code } } for _, p := range r.Packages { if code := strings.TrimSpace(p.ShareCode); code != "" { return p.ID, code } } if code := strings.TrimSpace(r.ShareCode); code != "" { return 0, code } if rawURL := strings.TrimSpace(r.URL); rawURL != "" { if idx := strings.LastIndex(rawURL, "/"); idx >= 0 && idx < len(rawURL)-1 { return 0, strings.TrimSpace(rawURL[idx+1:]) } } return 0, "" } // RockPackageListResponse 是 GET /api/user/packages 的返回结构。 type RockPackageListResponse struct { Packages []RockPackage `json:"packages"` Pagination struct { Page int `json:"page"` PageSize int `json:"pageSize"` Total int `json:"total"` TotalPages int `json:"totalPages"` Offset int `json:"offset"` } `json:"pagination"` Error string `json:"error,omitempty"` } // ListPackages 分页查询上游打包记录(需 Bearer Token)。 func (c *RockClient) ListPackages(ctx context.Context, bearerToken string, page, pageSize int) (*RockPackageListResponse, error) { if page <= 0 { page = 1 } if pageSize <= 0 || pageSize > 100 { pageSize = 20 } reqURL := fmt.Sprintf("%s/api/user/packages?page=%d&pageSize=%d", c.BaseURL(), page, pageSize) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Origin", c.BaseURL()) req.Header.Set("Referer", c.BaseURL()+"/") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 packages 列表失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 packages 列表响应失败: %w", err) } if resp.StatusCode == http.StatusUnauthorized { return nil, fmt.Errorf("上游 Bearer Token 已失效或未授权 (401)") } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游 packages 列表接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockPackageListResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 packages 列表响应失败: %w", err) } if res.Error != "" { return nil, fmt.Errorf("上游 packages 列表业务错误: %s", res.Error) } return &res, nil } // CreatePackage 把一批已提取 Token 打包成公开分享包(需 Bearer Token)。 // 注意:上游未返回 share_code 时不会在这里报错,由调用方决定兜底策略。 func (c *RockClient) CreatePackage(ctx context.Context, bearerToken string, ids []string, note string) (*RockPackageCreateResponse, error) { startedAt := time.Now() if len(ids) == 0 { return nil, fmt.Errorf("打包 ids 不能为空") } reqURL := fmt.Sprintf("%s/api/user/packages", c.BaseURL()) payload := map[string]interface{}{ "ids": ids, "note": note, } jsonBytes, _ := json.Marshal(payload) req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(jsonBytes)) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Origin", c.BaseURL()) req.Header.Set("Referer", c.BaseURL()+"/") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 packages 创建接口失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 packages 创建响应失败: %w", err) } traceRockRequest(ctx, "package", http.MethodPost, reqURL, jsonBytes, resp.StatusCode, body, time.Since(startedAt), err) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游 packages 创建接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockPackageCreateResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 packages 创建响应失败: %w", err) } if res.Error != "" { return nil, fmt.Errorf("上游 packages 创建业务错误: %s", res.Error) } return &res, nil } // GetPackageDetail 按 share_code 查询打包详情(公开接口,无需 Bearer)。 func (c *RockClient) GetPackageDetail(ctx context.Context, shareCode string) (*RockPackageDetailResponse, error) { startedAt := time.Now() shareCode = strings.TrimSpace(shareCode) if shareCode == "" { return nil, fmt.Errorf("share_code 不能为空") } reqURL := fmt.Sprintf("%s/api/package/%s", c.BaseURL(), url.PathEscape(shareCode)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 package 详情失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 package 详情响应失败: %w", err) } traceRockRequest(ctx, "detail", http.MethodGet, reqURL, nil, resp.StatusCode, body, time.Since(startedAt), err) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游 package 详情接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockPackageDetailResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 package 详情失败: %w", err) } if res.Error != "" { return nil, fmt.Errorf("上游 package 详情业务错误: %s", res.Error) } return &res, nil } // GetMe 获取商户个人信息及积分 func (c *RockClient) GetMe(ctx context.Context, bearerToken string) (*RockMeResponse, error) { startedAt := time.Now() reqURL := fmt.Sprintf("%s/api/me", c.BaseURL()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 /api/me 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } traceRockRequest(ctx, "me", http.MethodGet, reqURL, nil, resp.StatusCode, body, time.Since(startedAt), err) if resp.StatusCode == http.StatusUnauthorized { return nil, fmt.Errorf("上游 Bearer Token 已失效或未授权 (401)") } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游返回错误状态码 %d: %s", resp.StatusCode, string(body)) } var res RockMeResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 /api/me 响应失败: %w", err) } if res.User.Username == "" && res.Error != "" { return nil, fmt.Errorf("上游业务错误: %s", res.Error) } return &res, nil } // RockDashboardResponse 是 GET /api/user/dashboard 的返回结构。 type RockDashboardResponse struct { Available struct { Black int `json:"black"` Blue int `json:"blue"` } `json:"available"` EffectiveTokenCosts struct { BlackTokenCost FlexibleFloat64 `json:"blackTokenCost"` BlueTokenCost FlexibleFloat64 `json:"blueTokenCost"` } `json:"effectiveTokenCosts"` Config struct { BlackTokenCost FlexibleFloat64 `json:"blackTokenCost"` BlueTokenCost FlexibleFloat64 `json:"blueTokenCost"` } `json:"config"` Error string `json:"error,omitempty"` } // ResolveBlackTokenCost 优先取 effectiveTokenCosts,其次 config。 func (r *RockDashboardResponse) ResolveBlackTokenCost() float64 { if r == nil { return 0 } if cost := float64(r.EffectiveTokenCosts.BlackTokenCost); cost > 0 { return cost } return float64(r.Config.BlackTokenCost) } func (r *RockDashboardResponse) ResolveBlueTokenCost() float64 { if r == nil { return 0 } if cost := float64(r.EffectiveTokenCosts.BlueTokenCost); cost > 0 { return cost } return float64(r.Config.BlueTokenCost) } // GetDashboard 获取上游可用库存与实时采购单价(需 Bearer Token)。 func (c *RockClient) GetDashboard(ctx context.Context, bearerToken string) (*RockDashboardResponse, error) { startedAt := time.Now() reqURL := fmt.Sprintf("%s/api/user/dashboard", c.BaseURL()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Origin", c.BaseURL()) req.Header.Set("Referer", c.BaseURL()+"/") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 dashboard 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 dashboard 响应失败: %w", err) } traceRockRequest(ctx, "dashboard", http.MethodGet, reqURL, nil, resp.StatusCode, body, time.Since(startedAt), err) if resp.StatusCode == http.StatusUnauthorized { return nil, fmt.Errorf("上游 Bearer Token 已失效或未授权 (401)") } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游 dashboard 接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockDashboardResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 dashboard 响应失败: %w", err) } if res.Error != "" { return nil, fmt.Errorf("上游 dashboard 业务错误: %s", res.Error) } return &res, nil } // ClaimTokens 批量提取采购 Token func (c *RockClient) ClaimTokens(ctx context.Context, bearerToken string, color string, count int) ([]RockClaimedToken, error) { startedAt := time.Now() if count <= 0 { return nil, fmt.Errorf("提取数量必须大于 0") } reqURL := fmt.Sprintf("%s/api/user/tokens/claim", c.BaseURL()) payload := map[string]interface{}{ "color": color, "count": fmt.Sprintf("%d", count), } jsonBytes, _ := json.Marshal(payload) req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(jsonBytes)) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Origin", c.BaseURL()) req.Header.Set("Referer", c.BaseURL()+"/") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 claim 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 claim 响应失败: %w", err) } traceRockRequest(ctx, "claim", http.MethodPost, reqURL, jsonBytes, resp.StatusCode, body, time.Since(startedAt), err) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游 claim 接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockClaimResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 claim 响应失败: %w", err) } if len(res.Tokens) == 0 && res.Error != "" { return nil, fmt.Errorf("上游 claim 失败: %s", res.Error) } return res.Tokens, nil } // GetRedeemDetail 根据 redeem_code 免登录获取腾讯官方绑定链接与状态 func (c *RockClient) GetRedeemDetail(ctx context.Context, redeemCode string) (*RockRedeemDetailResponse, error) { reqURL := fmt.Sprintf("%s/api/redeem/%s", c.BaseURL(), url.PathEscape(redeemCode)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 /api/redeem/{code} 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 redeem 详情响应失败: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游 redeem 详情接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockRedeemDetailResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 redeem 详情失败: %w", err) } return &res, nil } // RefreshToken 刷新 Token 绑定状态并获取官方履约结果 func (c *RockClient) RefreshToken(ctx context.Context, bearerToken string, upstreamTokenID string) (*RockRefreshResponse, error) { reqURL := fmt.Sprintf("%s/api/user/tokens/%s/refresh", c.BaseURL(), url.PathEscape(upstreamTokenID)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, nil) if err != nil { return nil, err } if bearerToken != "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) } req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Origin", c.BaseURL()) req.Header.Set("Referer", c.BaseURL()+"/") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 refresh 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 refresh 响应失败: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游 refresh 接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockRefreshResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 refresh 响应失败: %w", err) } return &res, nil } // RockUpstreamStatusError 保留上游 HTTP 状态码,调用方可据此决定是否降级到备用接口。 type RockUpstreamStatusError struct { StatusCode int Body string } func (e *RockUpstreamStatusError) Error() string { return fmt.Sprintf("上游返回错误码 %d: %s", e.StatusCode, e.Body) } // RefreshRedeemBinding 通过兑换码刷新绑定状态(0666.pro 公开端接口,无需 Bearer)。 func (c *RockClient) RefreshRedeemBinding(ctx context.Context, redeemCode string) (*RockRefreshResponse, error) { code := strings.TrimSpace(redeemCode) if code == "" { return nil, fmt.Errorf("兑换码为空,无法刷新绑定状态") } reqURL := fmt.Sprintf("%s/api/redeem/%s/refresh", c.BaseURL(), url.PathEscape(code)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Origin", c.BaseURL()) req.Header.Set("Referer", c.BaseURL()+"/") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 redeem refresh 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 redeem refresh 响应失败: %w", err) } if resp.StatusCode != http.StatusOK { return nil, &RockUpstreamStatusError{StatusCode: resp.StatusCode, Body: string(body)} } var res RockRefreshResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析 redeem refresh 响应失败: %w", err) } return &res, nil } // RedeemSkin 执行最终皮肤兑换发货(需携带商户 Bearer Token 鉴权) func (c *RockClient) RedeemSkin(ctx context.Context, bearerToken, upstreamTokenID, skin, currency, account string) (*RockRedeemResponse, error) { reqURL := fmt.Sprintf("%s/api/redeem/%s", c.BaseURL(), url.PathEscape(upstreamTokenID)) payload := map[string]interface{}{ "skin": skin, "currency": currency, "account": account, } jsonBytes, _ := json.Marshal(payload) req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(jsonBytes)) if err != nil { return nil, err } if bearerToken != "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Origin", c.BaseURL()) req.Header.Set("Referer", c.BaseURL()+"/") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 兑换发货接口失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取兑换响应失败: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游兑换发货接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockRedeemResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析兑换响应失败: %w", err) } if !res.IsSuccess() && res.Error != "" { return nil, fmt.Errorf("上游兑换失败: %s", res.Error) } return &res, nil } // QueryExchangeRecords 异步查询兑换对账记录 func (c *RockClient) QueryExchangeRecords(ctx context.Context, bearerToken string, page, pageSize int, search string) (*RockExchangeRecordsResponse, error) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } reqURL := fmt.Sprintf("%s/api/user/exchange-records?page=%d&pageSize=%d&search=%s", c.BaseURL(), page, pageSize, url.QueryEscape(search)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求 0666 exchange-records 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取对账响应失败: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("上游对账接口返回错误码 %d: %s", resp.StatusCode, string(body)) } var res RockExchangeRecordsResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("解析对账响应失败: %w", err) } return &res, nil } // ReportAbnormal 上报异常 Token func (c *RockClient) ReportAbnormal(ctx context.Context, bearerToken, upstreamTokenID, reason string) error { reqURL := fmt.Sprintf("%s/api/user/tokens/%s/report", c.BaseURL(), url.PathEscape(upstreamTokenID)) payload := map[string]string{"reason": reason} jsonBytes, _ := json.Marshal(payload) req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(jsonBytes)) if err != nil { return err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", strings.TrimSpace(bearerToken))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("请求 0666 report 失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("上报异常失败(%d): %s", resp.StatusCode, string(body)) } return nil }