mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 07:29:40 +08:00
- queryWithContext 中 find/count 命令改用原生 Collection.Find()和 CountDocuments() API,替代RunCommand 的 firstBatch 模式
- 新增 convertBsonValue 将 ObjectID/bson.M/bson.D/bson.A 转为JSON 友好类型,_id 列自动置首
- DBQuery 增加 MongoDB JSON 命令识别,避免 find 命令误走 Exec 分支
⚡️perf(macos): 动态控制 NSVisualEffectView 降低 MacOS GPU 持续消耗,Windows不受影响
- NSVisualEffectView 启动默认 alpha 由 0.72 改为 0,窗口默认 opaque
- 新增 gonaviSetEffectViewAlpha ObjC 函数支持运行时动态切换
- 新增 SetWindowTranslucency Wails 绑定方法供前端调用
- 启动重试次数由 24 次缩减至 8 次
- opacity=1.0 且 blur=0 时窗口标记 opaque,GPU 不再持续计算模糊合成
- App.tsx 仅保留最外层 Layout 的 backdropFilter,移除 TitleBar/MenuBar/Content/DataGrid/LogPanel 冗余嵌套
- App.css 移除暗色模式全局 text-shadow 减少 compositing 开销
270 lines
7.4 KiB
Go
270 lines
7.4 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"GoNavi-Wails/internal/connection"
|
||
"GoNavi-Wails/internal/db"
|
||
"GoNavi-Wails/internal/logger"
|
||
)
|
||
|
||
const dbCachePingInterval = 30 * time.Second
|
||
|
||
type cachedDatabase struct {
|
||
inst db.Database
|
||
lastPing time.Time
|
||
}
|
||
|
||
// App struct
|
||
type App struct {
|
||
ctx context.Context
|
||
dbCache map[string]cachedDatabase // Cache for DB connections
|
||
mu sync.RWMutex // Mutex for cache access
|
||
updateMu sync.Mutex
|
||
updateState updateState
|
||
}
|
||
|
||
// NewApp creates a new App application struct
|
||
func NewApp() *App {
|
||
return &App{
|
||
dbCache: make(map[string]cachedDatabase),
|
||
}
|
||
}
|
||
|
||
// Startup is called when the app starts. The context is saved
|
||
// so we can call the runtime methods
|
||
func (a *App) Startup(ctx context.Context) {
|
||
a.ctx = ctx
|
||
logger.Init()
|
||
applyMacWindowTranslucencyFix()
|
||
logger.Infof("应用启动完成")
|
||
}
|
||
|
||
// SetWindowTranslucency 动态调整 macOS 窗口透明度。
|
||
// 前端在加载用户外观设置后、以及用户修改外观时调用此方法。
|
||
// opacity=1.0 且 blur=0 时窗口标记为 opaque,GPU 不再持续计算窗口背后的模糊合成。
|
||
func (a *App) SetWindowTranslucency(opacity float64, blur float64) {
|
||
setMacWindowTranslucency(opacity, blur)
|
||
}
|
||
|
||
// Shutdown is called when the app terminates
|
||
func (a *App) Shutdown(ctx context.Context) {
|
||
logger.Infof("应用开始关闭,准备释放资源")
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
for _, dbInst := range a.dbCache {
|
||
if err := dbInst.inst.Close(); err != nil {
|
||
logger.Error(err, "关闭数据库连接失败")
|
||
}
|
||
}
|
||
// Close all Redis connections
|
||
CloseAllRedisClients()
|
||
logger.Infof("资源释放完成,应用已关闭")
|
||
logger.Close()
|
||
}
|
||
|
||
// Helper: Generate a unique key for the connection config
|
||
func getCacheKey(config connection.ConnectionConfig) string {
|
||
if !config.UseSSH {
|
||
config.SSH = connection.SSHConfig{}
|
||
}
|
||
// 保持与驱动默认一致,避免同一连接被重复缓存
|
||
if config.Type == "postgres" && config.Database == "" {
|
||
config.Database = "postgres"
|
||
}
|
||
|
||
b, _ := json.Marshal(config)
|
||
sum := sha256.Sum256(b)
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
func wrapConnectError(config connection.ConnectionConfig, err error) error {
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
|
||
var netErr net.Error
|
||
if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &netErr) && netErr.Timeout()) {
|
||
dbName := config.Database
|
||
if dbName == "" {
|
||
dbName = "(default)"
|
||
}
|
||
err = fmt.Errorf("数据库连接超时:%s %s:%d/%s:%w", config.Type, config.Host, config.Port, dbName, err)
|
||
}
|
||
|
||
return withLogHint{err: err, logPath: logger.Path()}
|
||
}
|
||
|
||
type withLogHint struct {
|
||
err error
|
||
logPath string
|
||
}
|
||
|
||
func (e withLogHint) Error() string {
|
||
message := normalizeErrorMessage(e.err)
|
||
if strings.TrimSpace(e.logPath) == "" {
|
||
return message
|
||
}
|
||
return fmt.Sprintf("%s(详细日志:%s)", message, e.logPath)
|
||
}
|
||
|
||
func (e withLogHint) Unwrap() error {
|
||
return e.err
|
||
}
|
||
|
||
func formatConnSummary(config connection.ConnectionConfig) string {
|
||
timeoutSeconds := config.Timeout
|
||
if timeoutSeconds <= 0 {
|
||
timeoutSeconds = 30
|
||
}
|
||
|
||
dbName := config.Database
|
||
if strings.TrimSpace(dbName) == "" {
|
||
dbName = "(default)"
|
||
}
|
||
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("类型=%s 地址=%s:%d 数据库=%s 用户=%s 超时=%ds",
|
||
config.Type, config.Host, config.Port, dbName, config.User, timeoutSeconds))
|
||
|
||
if len(config.Hosts) > 0 {
|
||
b.WriteString(fmt.Sprintf(" 节点数=%d", len(config.Hosts)))
|
||
}
|
||
if strings.TrimSpace(config.Topology) != "" {
|
||
b.WriteString(fmt.Sprintf(" 拓扑=%s", strings.TrimSpace(config.Topology)))
|
||
}
|
||
if strings.TrimSpace(config.URI) != "" {
|
||
b.WriteString(fmt.Sprintf(" URI=已配置(长度=%d)", len(config.URI)))
|
||
}
|
||
if strings.TrimSpace(config.MySQLReplicaUser) != "" {
|
||
b.WriteString(" MySQL从库凭据=已配置")
|
||
}
|
||
if strings.EqualFold(strings.TrimSpace(config.Type), "mongodb") {
|
||
if strings.TrimSpace(config.MongoReplicaUser) != "" {
|
||
b.WriteString(" Mongo从库凭据=已配置")
|
||
}
|
||
if strings.TrimSpace(config.ReplicaSet) != "" {
|
||
b.WriteString(fmt.Sprintf(" 副本集=%s", strings.TrimSpace(config.ReplicaSet)))
|
||
}
|
||
if strings.TrimSpace(config.ReadPreference) != "" {
|
||
b.WriteString(fmt.Sprintf(" 读偏好=%s", strings.TrimSpace(config.ReadPreference)))
|
||
}
|
||
if strings.TrimSpace(config.AuthSource) != "" {
|
||
b.WriteString(fmt.Sprintf(" 认证库=%s", strings.TrimSpace(config.AuthSource)))
|
||
}
|
||
}
|
||
|
||
if config.UseSSH {
|
||
b.WriteString(fmt.Sprintf(" SSH=%s:%d 用户=%s", config.SSH.Host, config.SSH.Port, config.SSH.User))
|
||
}
|
||
|
||
if config.Type == "custom" {
|
||
driver := strings.TrimSpace(config.Driver)
|
||
if driver == "" {
|
||
driver = "(未配置)"
|
||
}
|
||
dsnState := "未配置"
|
||
if strings.TrimSpace(config.DSN) != "" {
|
||
dsnState = fmt.Sprintf("已配置(长度=%d)", len(config.DSN))
|
||
}
|
||
b.WriteString(fmt.Sprintf(" 驱动=%s DSN=%s", driver, dsnState))
|
||
}
|
||
|
||
return b.String()
|
||
}
|
||
|
||
func (a *App) getDatabaseForcePing(config connection.ConnectionConfig) (db.Database, error) {
|
||
return a.getDatabaseWithPing(config, true)
|
||
}
|
||
|
||
// Helper: Get or create a database connection
|
||
func (a *App) getDatabase(config connection.ConnectionConfig) (db.Database, error) {
|
||
return a.getDatabaseWithPing(config, false)
|
||
}
|
||
|
||
func (a *App) getDatabaseWithPing(config connection.ConnectionConfig, forcePing bool) (db.Database, error) {
|
||
key := getCacheKey(config)
|
||
shortKey := key
|
||
if len(shortKey) > 12 {
|
||
shortKey = shortKey[:12]
|
||
}
|
||
|
||
a.mu.RLock()
|
||
entry, ok := a.dbCache[key]
|
||
a.mu.RUnlock()
|
||
if ok {
|
||
needPing := forcePing
|
||
if !needPing {
|
||
lastPing := entry.lastPing
|
||
if lastPing.IsZero() || time.Since(lastPing) >= dbCachePingInterval {
|
||
needPing = true
|
||
}
|
||
}
|
||
|
||
if !needPing {
|
||
return entry.inst, nil
|
||
}
|
||
|
||
if err := entry.inst.Ping(); err == nil {
|
||
// Update lastPing (best effort)
|
||
a.mu.Lock()
|
||
if cur, exists := a.dbCache[key]; exists && cur.inst == entry.inst {
|
||
cur.lastPing = time.Now()
|
||
a.dbCache[key] = cur
|
||
}
|
||
a.mu.Unlock()
|
||
return entry.inst, nil
|
||
} else {
|
||
logger.Error(err, "缓存连接不可用,准备重建:%s 缓存Key=%s", formatConnSummary(config), shortKey)
|
||
}
|
||
|
||
// Ping failed: remove cached instance (best effort)
|
||
a.mu.Lock()
|
||
if cur, exists := a.dbCache[key]; exists && cur.inst == entry.inst {
|
||
if err := cur.inst.Close(); err != nil {
|
||
logger.Error(err, "关闭失效缓存连接失败:缓存Key=%s", shortKey)
|
||
}
|
||
delete(a.dbCache, key)
|
||
}
|
||
a.mu.Unlock()
|
||
}
|
||
|
||
logger.Infof("获取数据库连接:%s 缓存Key=%s", formatConnSummary(config), shortKey)
|
||
logger.Infof("创建数据库驱动实例:类型=%s 缓存Key=%s", config.Type, shortKey)
|
||
dbInst, err := db.NewDatabase(config.Type)
|
||
if err != nil {
|
||
logger.Error(err, "创建数据库驱动实例失败:类型=%s 缓存Key=%s", config.Type, shortKey)
|
||
return nil, err
|
||
}
|
||
|
||
if err := dbInst.Connect(config); err != nil {
|
||
wrapped := wrapConnectError(config, err)
|
||
logger.Error(wrapped, "建立数据库连接失败:%s 缓存Key=%s", formatConnSummary(config), shortKey)
|
||
return nil, wrapped
|
||
}
|
||
|
||
now := time.Now()
|
||
|
||
a.mu.Lock()
|
||
if existing, exists := a.dbCache[key]; exists && existing.inst != nil {
|
||
a.mu.Unlock()
|
||
// Prefer existing cached connection to avoid cache racing duplicates.
|
||
_ = dbInst.Close()
|
||
return existing.inst, nil
|
||
}
|
||
a.dbCache[key] = cachedDatabase{inst: dbInst, lastPing: now}
|
||
a.mu.Unlock()
|
||
|
||
logger.Infof("数据库连接成功并写入缓存:%s 缓存Key=%s", formatConnSummary(config), shortKey)
|
||
return dbInst, nil
|
||
}
|