Files
2026-10-31 14:03:04 +08:00

909 lines
29 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}