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:
@@ -5,6 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
dbx "github.com/wangjia/pangolin/server/internal/db"
|
||||
)
|
||||
|
||||
// DeviceRow mirrors a `devices` table row.
|
||||
@@ -33,11 +35,12 @@ type effSub struct {
|
||||
// Store wraps a *sql.DB and exposes the database operations the devices module
|
||||
// needs. Methods that take a *sql.Tx run within that transaction.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
dialect dbx.Dialect
|
||||
}
|
||||
|
||||
// NewStore creates a Store backed by the given MySQL connection pool.
|
||||
func NewStore(db *sql.DB) *Store { return &Store{db: db} }
|
||||
// NewStore creates a Store backed by the given connection pool (MySQL or SQLite).
|
||||
func NewStore(db *sql.DB) *Store { return &Store{db: db, dialect: dbx.DialectForDB(db)} }
|
||||
|
||||
// BeginTx starts a transaction at Read Committed isolation. Per-user
|
||||
// serialization for device mutations is achieved by locking the users row
|
||||
@@ -77,7 +80,7 @@ func (s *Store) ListByUser(ctx context.Context, userID int64) ([]DeviceRow, erro
|
||||
func (s *Store) findDeviceByUUIDTx(ctx context.Context, tx *sql.Tx, uuid string) (*DeviceRow, error) {
|
||||
row := tx.QueryRowContext(ctx,
|
||||
`SELECT id, uuid, user_id, name, platform, last_seen, created_at
|
||||
FROM devices WHERE uuid=? FOR UPDATE`, uuid)
|
||||
FROM devices WHERE uuid=? `+s.dialect.LockForUpdate(), uuid)
|
||||
var d DeviceRow
|
||||
if err := row.Scan(&d.ID, &d.UUID, &d.UserID, &d.Name, &d.Platform, &d.LastSeen, &d.CreatedAt); err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -90,7 +93,7 @@ func (s *Store) findDeviceByUUIDTx(ctx context.Context, tx *sql.Tx, uuid string)
|
||||
// lockUser locks the users row to serialize per-user device mutations and
|
||||
// returns the user's status. Returns (false, "", nil) when the user is absent.
|
||||
func (s *Store) lockUser(ctx context.Context, tx *sql.Tx, userID int64) (exists bool, status string, err error) {
|
||||
row := tx.QueryRowContext(ctx, `SELECT status FROM users WHERE id=? FOR UPDATE`, userID)
|
||||
row := tx.QueryRowContext(ctx, `SELECT status FROM users WHERE id=? `+s.dialect.LockForUpdate(), userID)
|
||||
if e := row.Scan(&status); e == sql.ErrNoRows {
|
||||
return false, "", nil
|
||||
} else if e != nil {
|
||||
@@ -110,10 +113,11 @@ func (s *Store) countDevicesTx(ctx context.Context, tx *sql.Tx, userID int64) (i
|
||||
|
||||
// insertDeviceTx inserts a new device row inside tx and returns it.
|
||||
func (s *Store) insertDeviceTx(ctx context.Context, tx *sql.Tx, uuid string, userID int64, name, platform string) (*DeviceRow, error) {
|
||||
now := time.Now().UTC()
|
||||
res, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO devices (uuid, user_id, name, platform, last_seen, created_at)
|
||||
VALUES (?, ?, ?, ?, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6))`,
|
||||
uuid, userID, name, platform)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
uuid, userID, name, platform, now, now)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store.insertDeviceTx: %w", err)
|
||||
}
|
||||
@@ -124,7 +128,7 @@ func (s *Store) insertDeviceTx(ctx context.Context, tx *sql.Tx, uuid string, use
|
||||
// touchLastSeenTx updates a device's last_seen to now inside tx.
|
||||
func (s *Store) touchLastSeenTx(ctx context.Context, tx *sql.Tx, deviceID int64) error {
|
||||
_, err := tx.ExecContext(ctx,
|
||||
`UPDATE devices SET last_seen=UTC_TIMESTAMP(6) WHERE id=?`, deviceID)
|
||||
`UPDATE devices SET last_seen=? WHERE id=?`, time.Now().UTC(), deviceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("store.touchLastSeenTx: %w", err)
|
||||
}
|
||||
@@ -212,8 +216,8 @@ func (s *Store) writeAuditLogTx(ctx context.Context, tx *sql.Tx, actor, action,
|
||||
}
|
||||
_, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO audit_log (actor, action, target, meta, at)
|
||||
VALUES (?, ?, ?, ?, UTC_TIMESTAMP(6))`,
|
||||
actor, action, target, metaJSON)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
actor, action, target, metaJSON, time.Now().UTC())
|
||||
if err != nil {
|
||||
return fmt.Errorf("store.writeAuditLogTx: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user