Files
go_win_template/internal/payment/client.go
Geliebte 1ea8c9da9c feat: 初始化 Wails + React + Go 桌面应用模板
- Wails v2.16.0 + React 19 + TypeScript + Vite 7
- 插件系统(build tag 控制编译,plugin.json 运行时配置)
- 支付插件(授权、账号、订单、退款、账单)
- 高并发基础设施(任务池、批量处理器、限流器、环形缓冲、管道)
- Windows 系统托盘(Win32 API)
- CodeGraph 索引 + .memory 记忆包
- 默认图标 assets/icon.png
2026-09-17 19:31:17 +08:00

273 lines
7.4 KiB
Go
Raw 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 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())
}