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:
48
internal/app/app.go
Normal file
48
internal/app/app.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Instance 全局应用单例,持有 ctx 和托盘引用
|
||||
type Instance struct {
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
name string
|
||||
}
|
||||
|
||||
// App 应用上下文入口
|
||||
type App struct {
|
||||
instance *Instance
|
||||
}
|
||||
|
||||
var singleton *Instance
|
||||
var once sync.Once
|
||||
|
||||
// Init 初始化应用全局上下文(只调用一次)
|
||||
func Init() *App {
|
||||
once.Do(func() {
|
||||
singleton = &Instance{name: "go_win_template"}
|
||||
})
|
||||
return &App{instance: singleton}
|
||||
}
|
||||
|
||||
// GetSingleton 获取全局单例
|
||||
func GetSingleton() *Instance {
|
||||
return singleton
|
||||
}
|
||||
|
||||
// SetContext 设置 Wails context
|
||||
func (s *Instance) SetContext(ctx context.Context) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.ctx = ctx
|
||||
}
|
||||
|
||||
// Context 获取当前 context
|
||||
func (s *Instance) Context() context.Context {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.ctx
|
||||
}
|
||||
62
internal/bus/bus.go
Normal file
62
internal/bus/bus.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Handler 事件处理器类型
|
||||
type Handler func(payload any)
|
||||
|
||||
// EventBus 应用内事件总线,用于模块间解耦通信
|
||||
type EventBus struct {
|
||||
mu sync.RWMutex
|
||||
handlers map[string][]Handler
|
||||
}
|
||||
|
||||
// NewEventBus 创建事件总线实例
|
||||
func NewEventBus() *EventBus {
|
||||
return &EventBus{
|
||||
handlers: make(map[string][]Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe 订阅事件,返回取消函数
|
||||
func (b *EventBus) Subscribe(event string, handler Handler) func() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.handlers[event] = append(b.handlers[event], handler)
|
||||
return func() { b.Unsubscribe(event, handler) }
|
||||
}
|
||||
|
||||
// Unsubscribe 取消订阅
|
||||
func (b *EventBus) Unsubscribe(event string, handler Handler) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
handlers := b.handlers[event]
|
||||
for i, h := range handlers {
|
||||
// 函数不能直接比较,用索引追踪
|
||||
_ = i
|
||||
_ = h
|
||||
// 简化:直接清空该事件所有处理器(实际项目可用 handler ID)
|
||||
b.handlers[event] = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Publish 同步发布事件
|
||||
func (b *EventBus) Publish(event string, payload any) {
|
||||
b.mu.RLock()
|
||||
handlers := make([]Handler, len(b.handlers[event]))
|
||||
copy(handlers, b.handlers[event])
|
||||
b.mu.RUnlock()
|
||||
for _, h := range handlers {
|
||||
if h != nil {
|
||||
h(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PublishAsync 异步发布事件,不阻塞调用方
|
||||
func (b *EventBus) PublishAsync(event string, payload any) {
|
||||
b.Publish(event, payload) // 当前同步调用,后续可扩展为 goroutine
|
||||
}
|
||||
119
internal/cache/cache.go
vendored
Normal file
119
internal/cache/cache.go
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Item 缓存项
|
||||
type Item struct {
|
||||
Value any
|
||||
Expiration int64 // Unix 纳秒,0 表示永不过期
|
||||
}
|
||||
|
||||
// IsExpired 是否已过期
|
||||
func (i *Item) IsExpired() bool {
|
||||
if i.Expiration == 0 {
|
||||
return false
|
||||
}
|
||||
return time.Now().UnixNano() > i.Expiration
|
||||
}
|
||||
|
||||
// Cache 线程安全的内存缓存,支持 TTL
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]*Item
|
||||
ttl time.Duration
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
// Option 缓存配置选项
|
||||
type Option func(*Cache)
|
||||
|
||||
// WithTTL 设置默认过期时间
|
||||
func WithTTL(ttl time.Duration) Option {
|
||||
return func(c *Cache) {
|
||||
c.ttl = ttl
|
||||
}
|
||||
}
|
||||
|
||||
// WithCleanupInterval 设置后台清理间隔
|
||||
func WithCleanupInterval(d time.Duration) Option {
|
||||
return func(c *Cache) {
|
||||
go c.cleanupLoop(d)
|
||||
}
|
||||
}
|
||||
|
||||
// NewCache 创建缓存实例
|
||||
func NewCache(opts ...Option) *Cache {
|
||||
c := &Cache{
|
||||
items: make(map[string]*Item),
|
||||
ttl: 5 * time.Minute,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Set 设置缓存项
|
||||
func (c *Cache) Set(key string, value any) {
|
||||
c.SetWithTTL(key, value, c.ttl)
|
||||
}
|
||||
|
||||
// SetWithTTL 设置缓存项并指定过期时间
|
||||
func (c *Cache) SetWithTTL(key string, value any, ttl time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
exp := int64(0)
|
||||
if ttl > 0 {
|
||||
exp = time.Now().Add(ttl).UnixNano()
|
||||
}
|
||||
c.items[key] = &Item{Value: value, Expiration: exp}
|
||||
}
|
||||
|
||||
// Get 获取缓存项
|
||||
func (c *Cache) Get(key string) (any, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
item, ok := c.items[key]
|
||||
if !ok || item.IsExpired() {
|
||||
return nil, false
|
||||
}
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
// Delete 删除缓存项
|
||||
func (c *Cache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, key)
|
||||
}
|
||||
|
||||
// Len 返回缓存项数量
|
||||
func (c *Cache) Len() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.items)
|
||||
}
|
||||
|
||||
// Clear 清空缓存
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.items = make(map[string]*Item)
|
||||
}
|
||||
|
||||
func (c *Cache) cleanupLoop(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
c.mu.Lock()
|
||||
for k, v := range c.items {
|
||||
if v.IsExpired() {
|
||||
delete(c.items, k)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
272
internal/payment/client.go
Normal file
272
internal/payment/client.go
Normal 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())
|
||||
}
|
||||
149
internal/payment/service.go
Normal file
149
internal/payment/service.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Service 支付业务层,内部封装 Client,对外暴露方法
|
||||
type Service struct {
|
||||
client *Client
|
||||
cfg Config
|
||||
}
|
||||
|
||||
// NewService 创建支付服务
|
||||
func NewService(cfg Config) *Service {
|
||||
return &Service{
|
||||
client: NewClient(cfg),
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// GetClient 获取底层 HTTP 客户端(供高级用法)
|
||||
func (s *Service) GetClient() *Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// Login 登录授权,获取 access_token
|
||||
func (s *Service) Login(ctx context.Context) (map[string]any, error) {
|
||||
token, err := s.client.GetToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"access_token": token.AccessToken,
|
||||
"expires_in": token.ExpiresIn,
|
||||
"message": "授权成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckAuth 查询授权状态
|
||||
func (s *Service) CheckAuth(ctx context.Context) (map[string]any, error) {
|
||||
token, err := s.client.GetToken(ctx)
|
||||
if err != nil {
|
||||
return map[string]any{"success": false, "message": "授权失效,请重新登录"}, err
|
||||
}
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"access_token": token.AccessToken,
|
||||
"expires_in": token.ExpiresIn,
|
||||
"message": "授权有效",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refresh 主动刷新令牌
|
||||
func (s *Service) Refresh(ctx context.Context) (map[string]any, error) {
|
||||
token, err := s.client.RefreshToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"access_token": token.AccessToken,
|
||||
"expires_in": token.ExpiresIn,
|
||||
"message": "续期成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccountInfo 获取授权账号信息
|
||||
func (s *Service) GetAccountInfo(ctx context.Context) (map[string]any, error) {
|
||||
raw, err := s.client.Get(ctx, "/v1/account/info", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get account info: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// QueryOrder 通过 order_id 查询订单
|
||||
func (s *Service) QueryOrder(ctx context.Context, orderID string) (map[string]any, error) {
|
||||
raw, err := s.client.Get(ctx, "/v1/orders/"+orderID, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query order: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// QueryOrderByOutNo 通过商户订单号查询
|
||||
func (s *Service) QueryOrderByOutNo(ctx context.Context, outTradeNo string) (map[string]any, error) {
|
||||
raw, err := s.client.Get(ctx, "/v1/orders", map[string]string{"out_trade_no": outTradeNo})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query order by out_no: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// RequestRefund 申请退款
|
||||
func (s *Service) RequestRefund(ctx context.Context, req map[string]any) (map[string]any, error) {
|
||||
raw, err := s.client.Post(ctx, "/v1/refunds", nil, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request refund: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// GetBill 分页获取账单
|
||||
func (s *Service) GetBill(ctx context.Context, page, pageSize int) (map[string]any, error) {
|
||||
params := map[string]string{
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page_size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
raw, err := s.client.Get(ctx, "/v1/bills", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get bill: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// GetBillByDate 按日期范围查账单
|
||||
func (s *Service) GetBillByDate(ctx context.Context, startDate, endDate string) (map[string]any, error) {
|
||||
params := map[string]string{
|
||||
"start_date": startDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
raw, err := s.client.Get(ctx, "/v1/bills/range", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get bill by date: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 内部辅助
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
func parseAPIResponse(raw []byte) (map[string]any, error) {
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("api error code=%d msg=%s", result.Code, result.Message)
|
||||
}
|
||||
return map[string]any{"success": true, "data": result.Data, "message": result.Message}, nil
|
||||
}
|
||||
26
internal/plugins/ai_impl.go
Normal file
26
internal/plugins/ai_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build ai
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type aiPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *aiPlugin) Name() string { return "ai" }
|
||||
func (p *aiPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *aiPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[ai] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *aiPlugin) Register(app *App) error { return nil }
|
||||
func (p *aiPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&aiPlugin{enabled: cfg.Plugins["ai"].Enabled})
|
||||
}
|
||||
26
internal/plugins/concurrent_impl.go
Normal file
26
internal/plugins/concurrent_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build concurrent
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type concurrentPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *concurrentPlugin) Name() string { return "concurrent" }
|
||||
func (p *concurrentPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *concurrentPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[concurrent] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *concurrentPlugin) Register(app *App) error { return nil }
|
||||
func (p *concurrentPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&concurrentPlugin{enabled: cfg.Plugins["concurrent"].Enabled})
|
||||
}
|
||||
26
internal/plugins/iconfont_impl.go
Normal file
26
internal/plugins/iconfont_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build iconfont
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type iconfontPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *iconfontPlugin) Name() string { return "iconfont" }
|
||||
func (p *iconfontPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *iconfontPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[iconfont] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *iconfontPlugin) Register(app *App) error { return nil }
|
||||
func (p *iconfontPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&iconfontPlugin{enabled: cfg.Plugins["iconfont"].Enabled})
|
||||
}
|
||||
153
internal/plugins/loader.go
Normal file
153
internal/plugins/loader.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Config 插件配置文件结构
|
||||
type Config struct {
|
||||
Version string `json:"version"`
|
||||
Plugins map[string]PluginOption `json:"plugins"`
|
||||
}
|
||||
|
||||
// PluginOption 单个插件配置项
|
||||
type PluginOption struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
// Params 插件专用参数,由插件自己解析
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// Loader 插件配置加载器
|
||||
type Loader struct {
|
||||
mu sync.RWMutex
|
||||
cfg *Config
|
||||
path string
|
||||
}
|
||||
|
||||
// NewLoader 创建配置加载器
|
||||
func NewLoader(path string) *Loader {
|
||||
return &Loader{path: path}
|
||||
}
|
||||
|
||||
// Load 从文件加载配置
|
||||
func (l *Loader) Load() (*Config, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(l.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 环境变量覆盖优先级最高
|
||||
if v := os.Getenv("PLUGIN_ICONFONT"); v == "false" || v == "0" {
|
||||
tmp := cfg.Plugins["iconfont"]; tmp.Enabled = false; cfg.Plugins["iconfont"] = tmp
|
||||
}
|
||||
if v := os.Getenv("PLUGIN_AI"); v == "true" || v == "1" {
|
||||
tmp := cfg.Plugins["ai"]; tmp.Enabled = true; cfg.Plugins["ai"] = tmp
|
||||
}
|
||||
if v := os.Getenv("PLUGIN_CONCURRENT"); v == "false" || v == "0" {
|
||||
tmp := cfg.Plugins["concurrent"]; tmp.Enabled = false; cfg.Plugins["concurrent"] = tmp
|
||||
}
|
||||
|
||||
l.cfg = &cfg
|
||||
return l.cfg, nil
|
||||
}
|
||||
|
||||
// IsEnabled 查询插件启用状态
|
||||
func (l *Loader) IsEnabled(name string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
if l.cfg == nil {
|
||||
return false
|
||||
}
|
||||
pc, ok := l.cfg.Plugins[name]
|
||||
return ok && pc.Enabled
|
||||
}
|
||||
|
||||
// GetEnabledNames 返回所有已启用插件名
|
||||
func (l *Loader) GetEnabledNames() []string {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
if l.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0)
|
||||
for name, pc := range l.cfg.Plugins {
|
||||
if pc.Enabled {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Save 保存配置
|
||||
func (l *Loader) Save(cfg *Config) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(l.path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(l.path, data, 0644)
|
||||
}
|
||||
|
||||
// LoadOrDefault 加载配置,找不到则返回默认值
|
||||
func LoadOrDefault() (*Config, error) {
|
||||
paths := []string{"config/plugin.json", "./plugin.json"}
|
||||
exe, _ := os.Executable()
|
||||
if exe != "" {
|
||||
dir := filepath.Dir(exe)
|
||||
paths = append([]string{
|
||||
filepath.Join(dir, "config", "plugin.json"),
|
||||
filepath.Join(dir, "plugin.json"),
|
||||
}, paths...)
|
||||
}
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
loader := NewLoader(p)
|
||||
if cfg, err := loader.Load(); err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultConfig(), nil
|
||||
}
|
||||
|
||||
// EnsureConfig 确保配置文件存在
|
||||
func EnsureConfig() error {
|
||||
_, err := os.Stat("config/plugin.json")
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
cfg, _ := LoadOrDefault()
|
||||
loader := NewLoader("config/plugin.json")
|
||||
if mkErr := os.MkdirAll("config", 0755); mkErr != nil {
|
||||
return mkErr
|
||||
}
|
||||
return loader.Save(cfg)
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
Version: "1.0.0",
|
||||
Plugins: map[string]PluginOption{
|
||||
"iconfont": {Enabled: false, Description: "阿里IconFont图标库,支持字体图标"},
|
||||
"ai": {Enabled: false, Description: "AI大模型集成,支持对话、分析等功能"},
|
||||
"concurrent": {Enabled: true, Description: "高并发工具集,任务池、限流器等"},
|
||||
},
|
||||
}
|
||||
}
|
||||
54
internal/plugins/payment_impl.go
Normal file
54
internal/plugins/payment_impl.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Package plugins — payment 插件实现(带 build tag,未启用时不编译)
|
||||
//go:build payment
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go_win_template/internal/payment"
|
||||
)
|
||||
|
||||
// paymentPluginImpl 支付插件
|
||||
type paymentPluginImpl struct {
|
||||
enabled bool
|
||||
svc *payment.Service
|
||||
}
|
||||
|
||||
func (p *paymentPluginImpl) Name() string { return "payment" }
|
||||
func (p *paymentPluginImpl) Enabled() bool { return p.enabled }
|
||||
func (p *paymentPluginImpl) Init(ctx context.Context) error {
|
||||
cfg, _ := LoadOrDefault()
|
||||
pcfg := payment.Config{
|
||||
AppID: getStringParam(cfg, "payment", "app_id"),
|
||||
MchID: getStringParam(cfg, "payment", "mch_id"),
|
||||
APIKey: getStringParam(cfg, "payment", "api_key"),
|
||||
BaseURL: getStringParam(cfg, "payment", "base_url"),
|
||||
AutoRefreshToken: true,
|
||||
}
|
||||
p.svc = payment.NewService(pcfg)
|
||||
fmt.Printf("[payment] initialized, base_url=%s\n", pcfg.BaseURL)
|
||||
return nil
|
||||
}
|
||||
func (p *paymentPluginImpl) Register(app *App) error { return nil }
|
||||
func (p *paymentPluginImpl) Shutdown() error { return nil }
|
||||
|
||||
// GetPaymentService 返回支付服务(any 类型,供 App 层类型断言)
|
||||
func (p *paymentPluginImpl) GetPaymentService() any {
|
||||
return p.svc
|
||||
}
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&paymentPluginImpl{enabled: cfg.Plugins["payment"].Enabled})
|
||||
}
|
||||
|
||||
func getStringParam(cfg *Config, plugin, key string) string {
|
||||
if opts, ok := cfg.Plugins[plugin]; ok {
|
||||
if v, ok := opts.Params[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
150
internal/plugins/plugins.go
Normal file
150
internal/plugins/plugins.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Package plugins 插件系统核心接口与注册中心
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Plugin 插件接口,所有插件必须实现
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Init(ctx context.Context) error
|
||||
Register(app *App) error
|
||||
Shutdown() error
|
||||
Enabled() bool
|
||||
}
|
||||
|
||||
// App 插件可见的应用上下文
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
config map[string]interface{}
|
||||
}
|
||||
|
||||
// NewApp 创建应用上下文
|
||||
func NewApp(ctx context.Context, config map[string]interface{}) *App {
|
||||
return &App{ctx: ctx, config: config}
|
||||
}
|
||||
|
||||
// Registry 插件注册中心(线程安全)
|
||||
type Registry struct {
|
||||
mu sync.Mutex
|
||||
plugins map[string]Plugin
|
||||
}
|
||||
|
||||
var globalRegistry *Registry
|
||||
var once sync.Once
|
||||
|
||||
func getGlobalRegistry() *Registry {
|
||||
once.Do(func() {
|
||||
globalRegistry = &Registry{plugins: make(map[string]Plugin)}
|
||||
})
|
||||
return globalRegistry
|
||||
}
|
||||
|
||||
// Register 全局注册插件(由各插件 build.go 的 init() 调用)
|
||||
func Register(p Plugin) {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.plugins[p.Name()] = p
|
||||
log.Printf("[plugins] registered: %s (enabled=%v)", p.Name(), p.Enabled())
|
||||
}
|
||||
|
||||
// Get 获取已注册插件
|
||||
func Get(name string) Plugin {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.plugins[name]
|
||||
}
|
||||
|
||||
// List 列出所有已注册插件名
|
||||
func List() []string {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
names := make([]string, 0, len(r.plugins))
|
||||
for name := range r.plugins {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Count 返回已注册插件数
|
||||
func Count() int {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return len(r.plugins)
|
||||
}
|
||||
|
||||
// Start 启动所有已启用的插件
|
||||
func Start(ctx context.Context) error {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for name, p := range r.plugins {
|
||||
if !p.Enabled() {
|
||||
log.Printf("[plugins] skip disabled: %s", name)
|
||||
continue
|
||||
}
|
||||
if err := p.Init(ctx); err != nil {
|
||||
log.Printf("[plugins] init %s failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if err := p.Register(NewApp(ctx, nil)); err != nil {
|
||||
log.Printf("[plugins] register %s failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[plugins] started: %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭所有插件
|
||||
func Shutdown() error {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for name, p := range r.plugins {
|
||||
if err := p.Shutdown(); err != nil {
|
||||
log.Printf("[plugins] shutdown %s error: %v", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnabledPlugins 返回所有已启用插件列表
|
||||
func EnabledPlugins() []Plugin {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
result := make([]Plugin, 0)
|
||||
for _, p := range r.plugins {
|
||||
if p.Enabled() {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PrintStatus 打印当前插件状态(调试用)
|
||||
func PrintStatus() {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
fmt.Println("[plugins] current status:")
|
||||
for name, p := range r.plugins {
|
||||
status := "disabled"
|
||||
if p.Enabled() {
|
||||
status = "enabled"
|
||||
}
|
||||
fmt.Printf(" - %s: %s\n", name, status)
|
||||
}
|
||||
if len(r.plugins) == 0 {
|
||||
fmt.Println(" (no plugins registered — check build tags)")
|
||||
}
|
||||
}
|
||||
369
internal/service/concurrent.go
Normal file
369
internal/service/concurrent.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BatchResult 批量任务结果
|
||||
type BatchResult struct {
|
||||
Success []any
|
||||
Fail []BatchError
|
||||
}
|
||||
|
||||
// BatchError 批量任务失败记录
|
||||
type BatchError struct {
|
||||
Index int
|
||||
Item any
|
||||
Err error
|
||||
}
|
||||
|
||||
// BatchWorker 批量处理器,适合文件处理、数据转换等高并发场景
|
||||
type BatchWorker struct {
|
||||
mu sync.Mutex
|
||||
running atomic.Int64
|
||||
total atomic.Int64
|
||||
success atomic.Int64
|
||||
failed atomic.Int64
|
||||
maxConcurrency int
|
||||
}
|
||||
|
||||
// NewBatchWorker 创建批量处理器
|
||||
func NewBatchWorker(maxConcurrency int) *BatchWorker {
|
||||
if maxConcurrency <= 0 {
|
||||
maxConcurrency = 10
|
||||
}
|
||||
return &BatchWorker{
|
||||
maxConcurrency: maxConcurrency,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessConcurrent 并发处理一批数据
|
||||
func (bw *BatchWorker) ProcessConcurrent(
|
||||
ctx context.Context,
|
||||
items []any,
|
||||
handler func(ctx context.Context, item any) (any, error),
|
||||
) BatchResult {
|
||||
bw.total.Add(int64(len(items)))
|
||||
results := BatchResult{}
|
||||
sem := make(chan struct{}, bw.maxConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for i, item := range items {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
default:
|
||||
}
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(index int, data any) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
bw.running.Add(1)
|
||||
defer bw.running.Add(-1)
|
||||
|
||||
result, err := handler(ctx, data)
|
||||
bw.success.Add(1)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
bw.failed.Add(1)
|
||||
results.Fail = append(results.Fail, BatchError{
|
||||
Index: index,
|
||||
Item: data,
|
||||
Err: err,
|
||||
})
|
||||
} else {
|
||||
results.Success = append(results.Success, result)
|
||||
}
|
||||
}(i, item)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// ProcessSequential 顺序处理(用于需要严格顺序的场景)
|
||||
func (bw *BatchWorker) ProcessSequential(
|
||||
ctx context.Context,
|
||||
items []any,
|
||||
handler func(ctx context.Context, item any) (any, error),
|
||||
) BatchResult {
|
||||
results := BatchResult{}
|
||||
for i, item := range items {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return results
|
||||
default:
|
||||
}
|
||||
result, err := handler(ctx, item)
|
||||
if err != nil {
|
||||
results.Fail = append(results.Fail, BatchError{Index: i, Item: item, Err: err})
|
||||
} else {
|
||||
results.Success = append(results.Success, result)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Stats 当前处理统计
|
||||
func (bw *BatchWorker) Stats() (running, total, success, failed int64) {
|
||||
return bw.running.Load(), bw.total.Load(), bw.success.Load(), bw.failed.Load()
|
||||
}
|
||||
|
||||
// RingBuffer 无锁环形缓冲区,适合高频消息传递
|
||||
type RingBuffer struct {
|
||||
data []any
|
||||
head int64
|
||||
tail int64
|
||||
capacity int
|
||||
size atomic.Int64
|
||||
}
|
||||
|
||||
// NewRingBuffer 创建环形缓冲区
|
||||
func NewRingBuffer(capacity int) *RingBuffer {
|
||||
if capacity <= 0 {
|
||||
capacity = 1024
|
||||
}
|
||||
rb := &RingBuffer{
|
||||
data: make([]any, capacity),
|
||||
capacity: capacity,
|
||||
}
|
||||
return rb
|
||||
}
|
||||
|
||||
// Enqueue 入队(阻塞直到有空位)
|
||||
func (rb *RingBuffer) Enqueue(item any) {
|
||||
for {
|
||||
tail := atomic.LoadInt64(&rb.tail)
|
||||
head := atomic.LoadInt64(&rb.head)
|
||||
if tail-head >= int64(rb.capacity) {
|
||||
time.Sleep(time.Microsecond)
|
||||
continue
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&rb.tail, tail, tail+1) {
|
||||
rb.data[tail%int64(rb.capacity)] = item
|
||||
rb.size.Add(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dequeue 出队(阻塞直到有数据)
|
||||
func (rb *RingBuffer) Dequeue() (any, bool) {
|
||||
for {
|
||||
head := atomic.LoadInt64(&rb.head)
|
||||
tail := atomic.LoadInt64(&rb.tail)
|
||||
if head >= tail {
|
||||
return nil, false
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&rb.head, head, head+1) {
|
||||
item := rb.data[head%int64(rb.capacity)]
|
||||
rb.data[head%int64(rb.capacity)] = nil
|
||||
rb.size.Add(-1)
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size 当前元素数量
|
||||
func (rb *RingBuffer) Size() int64 {
|
||||
return rb.size.Load()
|
||||
}
|
||||
|
||||
// RateLimiter 令牌桶限流器
|
||||
type RateLimiter struct {
|
||||
tokens int64
|
||||
maxTokens int64
|
||||
refillRate int64 // 每秒补充数
|
||||
lastRefill time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewRateLimiter 创建限流器
|
||||
func NewRateLimiter(maxTokens, refillRate int64) *RateLimiter {
|
||||
rl := &RateLimiter{
|
||||
tokens: maxTokens,
|
||||
maxTokens: maxTokens,
|
||||
refillRate: refillRate,
|
||||
lastRefill: time.Now(),
|
||||
}
|
||||
go rl.refillLoop()
|
||||
return rl
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) refillLoop() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
rl.mu.Lock()
|
||||
elapsed := time.Since(rl.lastRefill).Seconds()
|
||||
add := int64(elapsed * float64(rl.refillRate))
|
||||
current := rl.tokens
|
||||
if current+add > rl.maxTokens {
|
||||
add = rl.maxTokens - current
|
||||
}
|
||||
rl.tokens += add
|
||||
rl.lastRefill = time.Now()
|
||||
rl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire 获取一个令牌,返回是否成功
|
||||
func (rl *RateLimiter) Acquire() bool {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
if rl.tokens <= 0 {
|
||||
return false
|
||||
}
|
||||
rl.tokens--
|
||||
return true
|
||||
}
|
||||
|
||||
// Limit 带限制的包装函数
|
||||
func (rl *RateLimiter) Limit(fn func() error) error {
|
||||
for !rl.Acquire() {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
}
|
||||
return fn()
|
||||
}
|
||||
|
||||
// ConcurrentMap 线程安全的 map,支持并发读写
|
||||
type ConcurrentMap struct {
|
||||
mu sync.RWMutex
|
||||
items map[any]any
|
||||
}
|
||||
|
||||
// NewConcurrentMap 创建线程安全 map
|
||||
func NewConcurrentMap() *ConcurrentMap {
|
||||
return &ConcurrentMap{items: make(map[any]any)}
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Set(k, v any) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
cm.items[k] = v
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Get(k any) (any, bool) {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
v, ok := cm.items[k]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Delete(k any) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
delete(cm.items, k)
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Len() int {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
return len(cm.items)
|
||||
}
|
||||
|
||||
// Pipeline 函数式管道,链式处理数据流
|
||||
type Pipeline struct {
|
||||
stages []func(any) any
|
||||
input chan any
|
||||
output chan any
|
||||
}
|
||||
|
||||
// NewPipeline 创建管道
|
||||
func NewPipeline() *Pipeline {
|
||||
return &Pipeline{
|
||||
stages: make([]func(any) any, 0),
|
||||
input: make(chan any, 256),
|
||||
output: make(chan any, 256),
|
||||
}
|
||||
}
|
||||
|
||||
// AddStage 添加处理阶段
|
||||
func (p *Pipeline) AddStage(fn func(any) any) *Pipeline {
|
||||
p.stages = append(p.stages, fn)
|
||||
return p
|
||||
}
|
||||
|
||||
// Run 启动管道(阻塞,直到 input 关闭)
|
||||
func (p *Pipeline) Run(items []any) []any {
|
||||
go func() {
|
||||
for _, item := range items {
|
||||
p.input <- item
|
||||
}
|
||||
close(p.input)
|
||||
}()
|
||||
|
||||
go p.process()
|
||||
results := make([]any, 0)
|
||||
for v := range p.output {
|
||||
results = append(results, v)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (p *Pipeline) process() {
|
||||
defer close(p.output)
|
||||
for item := range p.input {
|
||||
for _, stage := range p.stages {
|
||||
item = stage(item)
|
||||
}
|
||||
p.output <- item
|
||||
}
|
||||
}
|
||||
|
||||
// GoCtx 便捷并发模式
|
||||
type GoCtx struct {
|
||||
wg sync.WaitGroup
|
||||
errs sync.Map
|
||||
results sync.Map
|
||||
}
|
||||
|
||||
// Go 启动一个并发任务
|
||||
func (gc *GoCtx) Go(id string, fn func() error) {
|
||||
gc.wg.Add(1)
|
||||
go func() {
|
||||
defer gc.wg.Done()
|
||||
if err := fn(); err != nil {
|
||||
gc.errs.Store(id, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait 等待所有任务完成,返回错误列表
|
||||
func (gc *GoCtx) Wait() []error {
|
||||
gc.wg.Wait()
|
||||
var errs []error
|
||||
gc.errs.Range(func(_, v any) bool {
|
||||
errs = append(errs, v.(error))
|
||||
return true
|
||||
})
|
||||
return errs
|
||||
}
|
||||
|
||||
// WaitWithTimeout 带超时的等待
|
||||
func (gc *GoCtx) WaitWithTimeout(timeout time.Duration) []error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
gc.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
var errs []error
|
||||
gc.errs.Range(func(_, v any) bool {
|
||||
errs = append(errs, v.(error))
|
||||
return true
|
||||
})
|
||||
return errs
|
||||
case <-time.After(timeout):
|
||||
return []error{fmt.Errorf("并发任务超时: %v", timeout)}
|
||||
}
|
||||
}
|
||||
167
internal/tray/win/tray.go
Normal file
167
internal/tray/win/tray.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// Package tray Windows 系统托盘实现(使用 win32 API)
|
||||
package tray
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Hook 托盘图标句柄
|
||||
var hook windows.Handle
|
||||
|
||||
// MenuHandle 菜单句柄
|
||||
type MenuHandle uintptr
|
||||
|
||||
// Tray 系统托盘管理器
|
||||
type Tray struct {
|
||||
mu sync.Mutex
|
||||
visible bool
|
||||
callback uintptr // WNDPROC 回调
|
||||
hWnd windows.Handle
|
||||
}
|
||||
|
||||
// MenuItem 菜单项
|
||||
type MenuItem struct {
|
||||
ID uint32
|
||||
Text string
|
||||
}
|
||||
|
||||
// NewTray 创建系统托盘
|
||||
func NewTray(iconData []byte, tooltip string) *Tray {
|
||||
t := &Tray{}
|
||||
if err := t.init(iconData, tooltip); err != nil {
|
||||
log.Printf("[tray] init failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Tray) init(iconData []byte, tooltip string) error {
|
||||
// 获取当前进程窗口
|
||||
hWnd, _ := windows.GetForegroundWindow()
|
||||
if hWnd == 0 {
|
||||
return windows.ERROR_ACCESS_DENIED
|
||||
}
|
||||
t.hWnd = hWnd
|
||||
|
||||
// 注册托盘图标
|
||||
// NOTIFYICONDATA structure
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
|
||||
data := ¬ifyIconData{
|
||||
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||
HWnd: uint32(hWnd),
|
||||
UID: 1000,
|
||||
UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP,
|
||||
UCallbackMessage: WM_USER + 1000,
|
||||
HIcon: 0, // 使用默认图标
|
||||
}
|
||||
|
||||
// 设置 tooltip
|
||||
for i := 0; i < len(tooltip) && i < 128; i++ {
|
||||
data.Tip[i] = uint8(tooltip[i])
|
||||
}
|
||||
data.Tip[len(tooltip)] = 0
|
||||
|
||||
// 添加托盘图标
|
||||
ret, _, _ := procShellNotifyIcon.Call(
|
||||
SHM_ADD,
|
||||
uintptr(unsafe.Pointer(data)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return windows.GetLastError()
|
||||
}
|
||||
|
||||
t.visible = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy 销毁托盘图标
|
||||
func (t *Tray) Destroy() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if !t.visible {
|
||||
return
|
||||
}
|
||||
data := ¬ifyIconData{
|
||||
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||
HWnd: uint32(t.hWnd),
|
||||
UID: 1000,
|
||||
UFlags: NIF_MESSAGE,
|
||||
}
|
||||
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
|
||||
t.visible = false
|
||||
}
|
||||
|
||||
// Show 显示托盘
|
||||
func (t *Tray) Show(iconData []byte, tooltip string) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.visible {
|
||||
return nil
|
||||
}
|
||||
return t.init(iconData, tooltip)
|
||||
}
|
||||
|
||||
// Hide 隐藏托盘
|
||||
func (t *Tray) Hide() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if !t.visible {
|
||||
return
|
||||
}
|
||||
data := ¬ifyIconData{
|
||||
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||
HWnd: uint32(t.hWnd),
|
||||
UID: 1000,
|
||||
UFlags: NIF_MESSAGE,
|
||||
}
|
||||
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
|
||||
t.visible = false
|
||||
}
|
||||
|
||||
// IsVisible 是否可见
|
||||
func (t *Tray) IsVisible() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.visible
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Win32 API 定义
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
WM_USER = 0x0400
|
||||
SHM_ADD uintptr = 0x00000000
|
||||
SHM_DELETE uintptr = 0x00000001
|
||||
|
||||
NIF_MESSAGE = 0x00000001
|
||||
NIF_ICON = 0x00000002
|
||||
NIF_TIP = 0x00000004
|
||||
)
|
||||
|
||||
// NOTIFYICONDATA struct
|
||||
type notifyIconData struct {
|
||||
CbSize uint32
|
||||
HWnd uint32
|
||||
UID uint32
|
||||
UFlags uint32
|
||||
UCallbackMessage uint32
|
||||
HIcon uintptr
|
||||
Tip [128]uint8
|
||||
}
|
||||
|
||||
var (
|
||||
procShellNotifyIcon = mustLoad("shell32.dll").MustFindProc("Shell_NotifyIconA")
|
||||
)
|
||||
|
||||
func mustLoad(dllName string) *windows.DynLib {
|
||||
dll, err := windows.LoadDLL(dllName)
|
||||
if err != nil {
|
||||
log.Fatalf("[tray] failed to load %s: %v", dllName, err)
|
||||
}
|
||||
return dll
|
||||
}
|
||||
146
internal/worker/pool.go
Normal file
146
internal/worker/pool.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Job 任务接口,所有后台任务需实现此接口
|
||||
type Job interface {
|
||||
Execute(ctx context.Context) error
|
||||
JobID() string
|
||||
}
|
||||
|
||||
// TaskResult 任务执行结果
|
||||
type TaskResult struct {
|
||||
JobID string
|
||||
Output any
|
||||
Error error
|
||||
}
|
||||
|
||||
// Pool 任务并发池,管理固定数量工作协程
|
||||
type Pool struct {
|
||||
jobs chan Job
|
||||
results chan TaskResult
|
||||
wg sync.WaitGroup
|
||||
stopOnce sync.Once
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running atomic.Int64
|
||||
total atomic.Int64
|
||||
failed atomic.Int64
|
||||
}
|
||||
|
||||
// Options 池配置
|
||||
type Options struct {
|
||||
Workers int // 工作协程数,默认 4
|
||||
BufSize int // 队列缓冲,默认 1000
|
||||
}
|
||||
|
||||
// NewPool 创建任务池
|
||||
func NewPool(opts ...Options) *Pool {
|
||||
o := Options{Workers: 4, BufSize: 1000}
|
||||
if len(opts) > 0 {
|
||||
o = opts[0]
|
||||
}
|
||||
if o.Workers <= 0 {
|
||||
o.Workers = 4
|
||||
}
|
||||
if o.BufSize <= 0 {
|
||||
o.BufSize = 1000
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p := &Pool{
|
||||
jobs: make(chan Job, o.BufSize),
|
||||
results: make(chan TaskResult, o.BufSize),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
p.startWorkers()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Pool) startWorkers() {
|
||||
for i := 0; i < 4; i++ {
|
||||
p.wg.Add(1)
|
||||
go p.worker()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) worker() {
|
||||
defer p.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
case job, ok := <-p.jobs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.running.Add(1)
|
||||
p.total.Add(1)
|
||||
err := job.Execute(p.ctx)
|
||||
result := TaskResult{JobID: job.JobID(), Error: err}
|
||||
if err != nil {
|
||||
p.failed.Add(1)
|
||||
}
|
||||
p.running.Add(-1)
|
||||
select {
|
||||
case p.results <- result:
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit 提交任务到队列(非阻塞,队列满时返回 false)
|
||||
func (p *Pool) Submit(job Job) bool {
|
||||
select {
|
||||
case p.jobs <- job:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitAndWait 提交任务并等待结果
|
||||
func (p *Pool) SubmitAndWait(ctx context.Context, job Job) (*TaskResult, error) {
|
||||
if !p.Submit(job) {
|
||||
return nil, ErrQueueFull
|
||||
}
|
||||
select {
|
||||
case r := <-p.results:
|
||||
return &r, r.Error
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Results 返回结果通道,供外部 range 消费
|
||||
func (p *Pool) Results() <-chan TaskResult {
|
||||
return p.results
|
||||
}
|
||||
|
||||
// Stats 当前池状态
|
||||
func (p *Pool) Stats() (running, total, failed int64) {
|
||||
return p.running.Load(), p.total.Load(), p.failed.Load()
|
||||
}
|
||||
|
||||
// Stop 优雅停止池
|
||||
func (p *Pool) Stop() {
|
||||
p.stopOnce.Do(func() {
|
||||
p.cancel()
|
||||
close(p.jobs)
|
||||
p.wg.Wait()
|
||||
close(p.results)
|
||||
})
|
||||
}
|
||||
|
||||
var ErrQueueFull = SyncError("任务队列已满,请降低提交频率或增大 BufSize")
|
||||
|
||||
// SyncError 用于类型断言
|
||||
type SyncError string
|
||||
|
||||
func (e SyncError) Error() string { return string(e) }
|
||||
Reference in New Issue
Block a user