package payment import ( "context" "crypto/hmac" "crypto/sha256" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" ) // ───────────────────────────────────────────── // 配置与数据结构 // ───────────────────────────────────────────── // Config 支付平台配置,可接入任意支付网关 type Config struct { AppID string `json:"app_id"` MchID string `json:"mch_id"` // 商户号(可选) APIKey string `json:"api_key"` // 签名密钥 BaseURL string `json:"base_url"` // 网关地址 Timeout time.Duration `json:"timeout"` AutoRefreshToken bool `json:"auto_refresh_token"` // 是否自动续期 } // Token 授权令牌 type Token struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` ExpiresAt time.Time `json:"expires_at"` IsExpired func() bool } func (t *Token) update(expiresIn int) { t.ExpiresIn = expiresIn t.ExpiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second) t.IsExpired = func() bool { return time.Now().After(t.ExpiresAt.Add(-30 * time.Second)) // 提前30秒判定过期 } } // ───────────────────────────────────────────── // HTTP 客户端(带自动重填和签名) // ───────────────────────────────────────────── // Client 支付 HTTP 客户端 type Client struct { mu sync.Mutex cfg Config httpClient *http.Client token *Token tokenMu sync.RWMutex } // NewClient 创建支付客户端 func NewClient(cfg Config) *Client { if cfg.Timeout == 0 { cfg.Timeout = 10 * time.Second } if cfg.BaseURL == "" { cfg.BaseURL = "https://api.example.com" } c := &Client{ cfg: cfg, httpClient: &http.Client{Timeout: cfg.Timeout}, } return c } // GetToken 获取访问令牌 func (c *Client) GetToken(ctx context.Context) (*Token, error) { c.tokenMu.RLock() if c.token != nil && !c.token.IsExpired() { c.tokenMu.RUnlock() return c.token, nil } c.tokenMu.RUnlock() c.mu.Lock() defer c.mu.Unlock() // double-check c.tokenMu.RLock() if c.token != nil && !c.token.IsExpired() { c.tokenMu.RUnlock() return c.token, nil } c.tokenMu.RUnlock() reqURL := fmt.Sprintf("%s/oauth/token", c.cfg.BaseURL) body := map[string]string{ "app_id": c.cfg.AppID, "timestamp": fmt.Sprintf("%d", time.Now().Unix()), "nonce": generateNonce(), } reqBody, _ := json.Marshal(body) req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody))) req.Header.Set("Content-Type", "application/json") addSignature(req, body, c.cfg.APIKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("get token: %w", err) } defer resp.Body.Close() var result struct { Code int `json:"code"` Message string `json:"message"` Data struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } `json:"data"` } json.NewDecoder(resp.Body).Decode(&result) if result.Code != 0 { return nil, fmt.Errorf("get token failed: %s", result.Message) } c.token = &Token{} c.token.update(result.Data.ExpiresIn) c.token.AccessToken = result.Data.AccessToken return c.token, nil } // RefreshToken 主动刷新令牌 func (c *Client) RefreshToken(ctx context.Context) (*Token, error) { reqURL := fmt.Sprintf("%s/oauth/refresh", c.cfg.BaseURL) body := map[string]string{ "app_id": c.cfg.AppID, "timestamp": fmt.Sprintf("%d", time.Now().Unix()), } reqBody, _ := json.Marshal(body) req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody))) req.Header.Set("Content-Type", "application/json") addSignature(req, body, c.cfg.APIKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result struct { Code int `json:"code"` Message string `json:"message"` Data struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } `json:"data"` } json.NewDecoder(resp.Body).Decode(&result) if result.Code != 0 { return nil, fmt.Errorf("refresh token failed: %s", result.Message) } c.mu.Lock() c.token = &Token{} c.token.update(result.Data.ExpiresIn) c.token.AccessToken = result.Data.AccessToken c.mu.Unlock() return c.token, nil } // Get 发送 GET 请求(自动携带 token) func (c *Client) Get(ctx context.Context, path string, params map[string]string) ([]byte, error) { token, err := c.GetToken(ctx) if err != nil { return nil, err } return c.doRequest(ctx, http.MethodGet, path, params, token.AccessToken, nil) } // Post 发送 POST 请求(自动携带 token) func (c *Client) Post(ctx context.Context, path string, params map[string]string, body any) ([]byte, error) { token, err := c.GetToken(ctx) if err != nil { return nil, err } return c.doRequest(ctx, http.MethodPost, path, params, token.AccessToken, body) } func (c *Client) doRequest(ctx context.Context, method, path string, params map[string]string, token string, body any) ([]byte, error) { reqURL := c.cfg.BaseURL + path values := url.Values{} for k, v := range params { values.Set(k, v) } if values.Encode() != "" { reqURL += "?" + values.Encode() } var reqBody io.Reader var contentType string if body != nil { data, _ := json.Marshal(body) reqBody = strings.NewReader(string(data)) contentType = "application/json" } req, _ := http.NewRequestWithContext(ctx, method, reqURL, reqBody) if contentType != "" { req.Header.Set("Content-Type", contentType) } req.Header.Set("Authorization", "Bearer "+token) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("request %s %s: %w", method, path, err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)) } return respBody, nil } // ───────────────────────────────────────────── // 签名辅助 // ───────────────────────────────────────────── func addSignature(req *http.Request, body map[string]string, apiKey string) { signStr := buildSignString(body) sig := hmacSHA256(signStr, apiKey) req.Header.Set("X-Signature", sig) } func buildSignString(params map[string]string) string { keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sortStrings(keys) var sb strings.Builder for _, k := range keys { sb.WriteString(k) sb.WriteString("=") sb.WriteString(params[k]) sb.WriteString("&") } return strings.TrimRight(sb.String(), "&") } func hmacSHA256(data, key string) string { m := hmac.New(sha256.New, []byte(key)) m.Write([]byte(data)) return fmt.Sprintf("%x", m.Sum(nil)) } func sortStrings(s []string) { for i := 1; i < len(s); i++ { for j := i; j > 0 && s[j] < s[j-1]; j-- { s[j], s[j-1] = s[j-1], s[j] } } } func generateNonce() string { // 简单 nonce,生产环境建议使用 crypto/rand return fmt.Sprintf("%d", time.Now().UnixNano()) }