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

48
internal/app/app.go Normal file
View 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
View 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
View 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
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())
}

149
internal/payment/service.go Normal file
View 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
}

View 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})
}

View 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})
}

View 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
View 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: "高并发工具集,任务池、限流器等"},
},
}
}

View 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
View 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)")
}
}

View 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
View 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 := &notifyIconData{
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 := &notifyIconData{
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 := &notifyIconData{
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
View 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) }