feat(server/db): 多库(3/4)— Dialect 抽象(upsert/锁)+ 合并 directory_version

- db.Dialect:LockForUpdate(mysql "FOR UPDATE" / sqlite "")、Upsert(中性
  EXCLUDED.col → mysql VALUES()/ sqlite excluded.);DialectForDB 从连接驱动推导
- 9 处 ON DUPLICATE KEY、11 处 FOR UPDATE 全走 dialect;sqlite 靠 _txlock=
  immediate 取得 BEGIN IMMEDIATE 悲观锁等价语义
- directory_version 三处重复合并为 store.BumpDirectoryVersion(dialect 感知)
- 这些文件同时含(2/4)的 UTC→Go 改动(与 upsert/锁同语句交错,无法拆分)
- 顺带:usage 的 FIELD()、codes 的 DATE_ADD/GREATEST 续期、nodes 的
  UNIX_TIMESTAMP、NULLIF 等 MySQL 专属构造一并退回 Go/可移植写法

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-18 00:01:26 +08:00
parent ece51d7e3d
commit 86b67c557b
9 changed files with 265 additions and 113 deletions
+19 -12
View File
@@ -6,15 +6,22 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
dbx "github.com/wangjia/pangolin/server/internal/db"
)
// MySQLStore is the production Store backed by the shared *sql.DB pool.
// (Despite the name it is dialect-aware and also works against SQLite.)
type MySQLStore struct {
db *sql.DB
db *sql.DB
dialect dbx.Dialect
}
// NewMySQLStore wraps a MySQL connection pool.
func NewMySQLStore(db *sql.DB) *MySQLStore { return &MySQLStore{db: db} }
// NewMySQLStore wraps a database connection pool (MySQL or SQLite).
func NewMySQLStore(db *sql.DB) *MySQLStore {
return &MySQLStore{db: db, dialect: dbx.DialectForDB(db)}
}
var _ Store = (*MySQLStore)(nil)
@@ -54,10 +61,10 @@ func (s *MySQLStore) InsertNode(ctx context.Context, n *Node) (int64, error) {
`INSERT INTO nodes
(uuid, region, name_zh, name_en, role, tier, endpoint, hy2_port,
reality_pbk, reality_sni, provider_id, tags, status, weight, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(6))`,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
n.UUID, n.Region, n.NameZH, n.NameEn, string(n.Role), string(n.Tier),
endpoint, nullInt(n.HY2Port), n.RealityPBK, n.RealitySNI, n.ProviderID,
marshalJSONList(n.Tags), string(status), weight)
marshalJSONList(n.Tags), string(status), weight, time.Now().UTC())
if err != nil {
return 0, fmt.Errorf("store.InsertNode: %w", err)
}
@@ -236,8 +243,8 @@ func (s *MySQLStore) WriteNodeEvent(ctx context.Context, nodeID int64, event Eve
detailJSON = "null"
}
_, err := s.db.ExecContext(ctx,
`INSERT INTO node_events (node_id, event, detail, at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))`,
nodeID, string(event), detailJSON)
`INSERT INTO node_events (node_id, event, detail, at) VALUES (?, ?, ?, ?)`,
nodeID, string(event), detailJSON, time.Now().UTC())
if err != nil {
return fmt.Errorf("store.WriteNodeEvent: %w", err)
}
@@ -249,8 +256,8 @@ func (s *MySQLStore) WriteAuditLog(ctx context.Context, actor, action, target, m
metaJSON = "null"
}
_, err := s.db.ExecContext(ctx,
`INSERT INTO audit_log (actor, action, target, meta, at) VALUES (?, ?, ?, ?, UTC_TIMESTAMP(6))`,
actor, action, target, metaJSON)
`INSERT INTO audit_log (actor, action, target, meta, at) VALUES (?, ?, ?, ?, ?)`,
actor, action, target, metaJSON, time.Now().UTC())
if err != nil {
return fmt.Errorf("store.WriteAuditLog: %w", err)
}
@@ -288,9 +295,9 @@ func (s *MySQLStore) LookupIdempotency(ctx context.Context, key string) (string,
func (s *MySQLStore) SaveIdempotency(ctx context.Context, key, nodeUUID string) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO provision_idempotency (idempotency_key, node_uuid, created_at)
VALUES (?, ?, UTC_TIMESTAMP(6))
ON DUPLICATE KEY UPDATE idempotency_key = idempotency_key`,
key, nodeUUID)
VALUES (?, ?, ?) `+
s.dialect.Upsert([]string{"idempotency_key"}),
key, nodeUUID, time.Now().UTC())
if err != nil {
return fmt.Errorf("store.SaveIdempotency: %w", err)
}