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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user