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
This commit is contained in:
2026-09-17 19:31:17 +08:00
commit 1ea8c9da9c
57 changed files with 6305 additions and 0 deletions

272
internal/payment/client.go Normal file
View File

@@ -0,0 +1,272 @@
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())
}