将 auth 数据库从 SQLite 迁移到 PostgreSQL

This commit is contained in:
fish
2026-05-03 20:50:28 +08:00
parent fbcde3cc71
commit 79d2f292f1
8 changed files with 23 additions and 59 deletions

View File

@@ -36,13 +36,10 @@ services:
environment: environment:
- LISTEN_ADDR=:8080 - LISTEN_ADDR=:8080
- DATABASE_URL=postgres://trade:trade@postgres:5432/futures?sslmode=disable - DATABASE_URL=postgres://trade:trade@postgres:5432/futures?sslmode=disable
- AUTH_DB_PATH=/app/auth/auth.db
depends_on: depends_on:
- postgres - postgres
ports: ports:
- "4000:8080" - "4000:8080"
volumes:
- ./data:/app/auth
restart: unless-stopped restart: unless-stopped
volumes: volumes:

View File

@@ -21,7 +21,6 @@ WORKDIR /src
COPY backend ./ COPY backend ./
COPY --from=ui /ui/dist ./dist COPY --from=ui /ui/dist ./dist
# 用 modernc.org/sqlite 纯 Go 驱动,无 CGO,无需 gcc/musl-dev
ENV CGO_ENABLED=0 GOOS=linux ENV CGO_ENABLED=0 GOOS=linux
RUN go mod tidy && \ RUN go mod tidy && \
@@ -36,7 +35,7 @@ RUN apk add --no-cache tzdata ca-certificates && \
echo "Asia/Shanghai" > /etc/timezone && \ echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata && \ apk del tzdata && \
adduser -D -u 1000 app && \ adduser -D -u 1000 app && \
mkdir -p /app/data /app/auth && \ mkdir -p /app/data && \
chown -R app:app /app chown -R app:app /app
WORKDIR /app WORKDIR /app
@@ -45,8 +44,7 @@ USER app
COPY --from=api --chown=app:app /out/web /app/web COPY --from=api --chown=app:app /out/web /app/web
ENV TZ=Asia/Shanghai \ ENV TZ=Asia/Shanghai \
LISTEN_ADDR=:8080 \ LISTEN_ADDR=:8080
AUTH_DB_PATH=/app/auth/auth.db
EXPOSE 8080 EXPOSE 8080

View File

@@ -7,5 +7,4 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
golang.org/x/crypto v0.27.0 golang.org/x/crypto v0.27.0
modernc.org/sqlite v1.32.0
) )

View File

@@ -2,4 +2,3 @@ github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=

View File

@@ -8,7 +8,6 @@ import (
type Config struct { type Config struct {
ListenAddr string ListenAddr string
DatabaseURL string DatabaseURL string
AuthDBPath string
TushareAPIURL string TushareAPIURL string
} }
@@ -16,7 +15,6 @@ func Load() (*Config, error) {
cfg := &Config{ cfg := &Config{
ListenAddr: getenv("LISTEN_ADDR", ":8080"), ListenAddr: getenv("LISTEN_ADDR", ":8080"),
DatabaseURL: os.Getenv("DATABASE_URL"), DatabaseURL: os.Getenv("DATABASE_URL"),
AuthDBPath: getenv("AUTH_DB_PATH", "/app/auth/auth.db"),
TushareAPIURL: getenv("TUSHARE_API_URL", "http://tushare:8000"), TushareAPIURL: getenv("TUSHARE_API_URL", "http://tushare:8000"),
} }
if cfg.DatabaseURL == "" { if cfg.DatabaseURL == "" {

View File

@@ -4,10 +4,9 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"path/filepath"
"time" "time"
_ "modernc.org/sqlite" _ "github.com/lib/pq"
) )
type AuthStore struct{ db *sql.DB } type AuthStore struct{ db *sql.DB }
@@ -30,18 +29,14 @@ const (
var ErrNotFound = errors.New("user not found") var ErrNotFound = errors.New("user not found")
func OpenAuth(path string) (*AuthStore, error) { func OpenAuth(databaseURL string) (*AuthStore, error) {
if dir := filepath.Dir(path); dir != "" { db, err := sql.Open("postgres", databaseURL)
_ = ensureDir(dir)
}
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)", path)
db, err := sql.Open("sqlite", dsn)
if err != nil { if err != nil {
return nil, fmt.Errorf("open auth.db: %w", err) return nil, fmt.Errorf("open auth db: %w", err)
} }
db.SetMaxOpenConns(1) // sqlite write 单连接更稳 db.SetMaxOpenConns(8)
if err := db.Ping(); err != nil { if err := db.Ping(); err != nil {
return nil, fmt.Errorf("ping auth.db: %w", err) return nil, fmt.Errorf("ping auth db: %w", err)
} }
s := &AuthStore{db: db} s := &AuthStore{db: db}
if err := s.init(); err != nil { if err := s.init(); err != nil {
@@ -55,11 +50,11 @@ func (s *AuthStore) Close() error { return s.db.Close() }
func (s *AuthStore) init() error { func (s *AuthStore) init() error {
_, err := s.db.Exec(` _, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin','user')), role TEXT NOT NULL CHECK(role IN ('admin','user')),
disabled INTEGER NOT NULL DEFAULT 0, disabled BOOLEAN NOT NULL DEFAULT FALSE,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL updated_at TEXT NOT NULL
); );
@@ -69,7 +64,7 @@ func (s *AuthStore) init() error {
return err return err
} }
// 兼容旧表:添加 force_password_change 列(已存在则忽略错误) // 兼容旧表:添加 force_password_change 列(已存在则忽略错误)
_, _ = s.db.Exec(`ALTER TABLE users ADD COLUMN force_password_change INTEGER NOT NULL DEFAULT 0`) _, _ = s.db.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS force_password_change BOOLEAN NOT NULL DEFAULT FALSE`)
return nil return nil
} }
@@ -81,28 +76,28 @@ func (s *AuthStore) CountAdmins() (int, error) {
func (s *AuthStore) CreateUser(username, passwordHash, role string) (*User, error) { func (s *AuthStore) CreateUser(username, passwordHash, role string) (*User, error) {
now := time.Now().Format("2006-01-02 15:04:05") now := time.Now().Format("2006-01-02 15:04:05")
res, err := s.db.Exec( var id int64
err := s.db.QueryRow(
`INSERT INTO users(username, password_hash, role, disabled, created_at, updated_at) `INSERT INTO users(username, password_hash, role, disabled, created_at, updated_at)
VALUES (?, ?, ?, 0, ?, ?)`, VALUES ($1, $2, $3, FALSE, $4, $5) RETURNING id`,
username, passwordHash, role, now, now, username, passwordHash, role, now, now,
) ).Scan(&id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
id, _ := res.LastInsertId()
return &User{ID: id, Username: username, PasswordHash: passwordHash, Role: role, return &User{ID: id, Username: username, PasswordHash: passwordHash, Role: role,
CreatedAt: now, UpdatedAt: now}, nil CreatedAt: now, UpdatedAt: now}, nil
} }
func (s *AuthStore) GetByUsername(username string) (*User, error) { func (s *AuthStore) GetByUsername(username string) (*User, error) {
row := s.db.QueryRow(`SELECT id, username, password_hash, role, disabled, force_password_change, created_at, updated_at row := s.db.QueryRow(`SELECT id, username, password_hash, role, disabled, force_password_change, created_at, updated_at
FROM users WHERE username = ?`, username) FROM users WHERE username = $1`, username)
return scanUser(row) return scanUser(row)
} }
func (s *AuthStore) GetByID(id int64) (*User, error) { func (s *AuthStore) GetByID(id int64) (*User, error) {
row := s.db.QueryRow(`SELECT id, username, password_hash, role, disabled, force_password_change, created_at, updated_at row := s.db.QueryRow(`SELECT id, username, password_hash, role, disabled, force_password_change, created_at, updated_at
FROM users WHERE id = ?`, id) FROM users WHERE id = $1`, id)
return scanUser(row) return scanUser(row)
} }
@@ -126,7 +121,7 @@ func (s *AuthStore) ListUsers() ([]User, error) {
func (s *AuthStore) UpdatePassword(id int64, hash string) error { func (s *AuthStore) UpdatePassword(id int64, hash string) error {
now := time.Now().Format("2006-01-02 15:04:05") now := time.Now().Format("2006-01-02 15:04:05")
res, err := s.db.Exec(`UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?`, hash, now, id) res, err := s.db.Exec(`UPDATE users SET password_hash = $1, updated_at = $2 WHERE id = $3`, hash, now, id)
if err != nil { if err != nil {
return err return err
} }
@@ -139,11 +134,7 @@ func (s *AuthStore) UpdatePassword(id int64, hash string) error {
func (s *AuthStore) SetForcePasswordChange(id int64, v bool) error { func (s *AuthStore) SetForcePasswordChange(id int64, v bool) error {
now := time.Now().Format("2006-01-02 15:04:05") now := time.Now().Format("2006-01-02 15:04:05")
val := 0 res, err := s.db.Exec(`UPDATE users SET force_password_change = $1, updated_at = $2 WHERE id = $3`, v, now, id)
if v {
val = 1
}
res, err := s.db.Exec(`UPDATE users SET force_password_change = ?, updated_at = ? WHERE id = ?`, val, now, id)
if err != nil { if err != nil {
return err return err
} }
@@ -156,11 +147,7 @@ func (s *AuthStore) SetForcePasswordChange(id int64, v bool) error {
func (s *AuthStore) SetDisabled(id int64, disabled bool) error { func (s *AuthStore) SetDisabled(id int64, disabled bool) error {
now := time.Now().Format("2006-01-02 15:04:05") now := time.Now().Format("2006-01-02 15:04:05")
v := 0 res, err := s.db.Exec(`UPDATE users SET disabled = $1, updated_at = $2 WHERE id = $3`, disabled, now, id)
if disabled {
v = 1
}
res, err := s.db.Exec(`UPDATE users SET disabled = ?, updated_at = ? WHERE id = ?`, v, now, id)
if err != nil { if err != nil {
return err return err
} }
@@ -172,7 +159,7 @@ func (s *AuthStore) SetDisabled(id int64, disabled bool) error {
} }
func (s *AuthStore) DeleteUser(id int64) error { func (s *AuthStore) DeleteUser(id int64) error {
res, err := s.db.Exec(`DELETE FROM users WHERE id = ?`, id) res, err := s.db.Exec(`DELETE FROM users WHERE id = $1`, id)
if err != nil { if err != nil {
return err return err
} }
@@ -189,16 +176,12 @@ type rowScanner interface {
func scanUser(r rowScanner) (*User, error) { func scanUser(r rowScanner) (*User, error) {
var u User var u User
var disabled int if err := r.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &u.Disabled, &u.ForcePasswordChange, &u.CreatedAt, &u.UpdatedAt); err != nil {
var forceChange int
if err := r.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &disabled, &forceChange, &u.CreatedAt, &u.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound return nil, ErrNotFound
} }
return nil, err return nil, err
} }
u.Disabled = disabled != 0
u.ForcePasswordChange = forceChange != 0
return &u, nil return &u, nil
} }

View File

@@ -1,10 +0,0 @@
package store
import "os"
func ensureDir(dir string) error {
if _, err := os.Stat(dir); err == nil {
return nil
}
return os.MkdirAll(dir, 0o755)
}

View File

@@ -30,7 +30,7 @@ func main() {
} }
defer futures.Close() defer futures.Close()
authDB, err := store.OpenAuth(cfg.AuthDBPath) authDB, err := store.OpenAuth(cfg.DatabaseURL)
if err != nil { if err != nil {
log.Fatalf("open auth: %v", err) log.Fatalf("open auth: %v", err)
} }