9c843b79ba
- 新增 users 和 user_settings 表,repositories/analyses/review_notes 添加 user_id 列 - 实现基于邮箱+密码的注册登录,密码使用 bcrypt 哈希 - 使用 gin-contrib/sessions cookie-based session 管理 - 所有仓库、分析记录、review notes 按用户隔离 - 用户设置(LLM 配置、review 参数、缓存配置)独立存储 - 新增登录/注册页面,导航栏显示用户邮箱和退出按钮 - 前端 fetch 请求统一添加 credentials: 'same-origin' - 支持 SESSION_SECRET 环境变量配置会话密钥
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
"github.com/HoHD/PR-Helper/models"
|
|
)
|
|
|
|
type DB struct {
|
|
conn *sql.DB
|
|
}
|
|
|
|
func New(dbPath string) (*DB, error) {
|
|
conn, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db := &DB{conn: conn}
|
|
if err := db.migrate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := db.seedDefaults(); err != nil {
|
|
return nil, err
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
func (db *DB) Close() error {
|
|
return db.conn.Close()
|
|
}
|
|
|
|
func (db *DB) Conn() *sql.DB {
|
|
return db.conn
|
|
}
|
|
|
|
func (db *DB) migrate() error {
|
|
stmts := []string{
|
|
`CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS user_settings (
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
key TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
PRIMARY KEY (user_id, key)
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS repositories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
url TEXT NOT NULL,
|
|
local_path TEXT NOT NULL,
|
|
size_bytes INTEGER DEFAULT 0,
|
|
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS analyses (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
repo_id INTEGER REFERENCES repositories(id),
|
|
type TEXT NOT NULL,
|
|
base_ref TEXT NOT NULL,
|
|
head_ref TEXT NOT NULL,
|
|
result TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS review_notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
analysis_id INTEGER REFERENCES analyses(id),
|
|
scope TEXT NOT NULL,
|
|
scope_key TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
}
|
|
for _, s := range stmts {
|
|
if _, err := db.conn.Exec(s); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Run incremental migrations for existing databases
|
|
if err := db.migrateV2(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrateV2 adds user_id columns to existing tables and removes the old UNIQUE constraint on url.
|
|
func (db *DB) migrateV2() error {
|
|
migrations := []string{
|
|
`ALTER TABLE repositories ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
|
|
`ALTER TABLE analyses ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
|
|
`ALTER TABLE review_notes ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
|
|
}
|
|
for _, m := range migrations {
|
|
// ALTER TABLE ADD COLUMN will fail if column already exists; ignore the error
|
|
db.conn.Exec(m)
|
|
}
|
|
// Recreate repositories table without the UNIQUE constraint on url (same url can be cloned by different users)
|
|
// SQLite doesn't support DROP CONSTRAINT, so we use a workaround
|
|
var hasOldUnique int
|
|
db.conn.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='repositories_old'`).Scan(&hasOldUnique)
|
|
if hasOldUnique == 0 {
|
|
// Check if the UNIQUE constraint exists by trying to detect the old schema
|
|
// We'll just do the migration idempotently
|
|
db.conn.Exec(`CREATE TABLE IF NOT EXISTS repositories_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
url TEXT NOT NULL,
|
|
local_path TEXT NOT NULL,
|
|
size_bytes INTEGER DEFAULT 0,
|
|
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`)
|
|
db.conn.Exec(`INSERT OR IGNORE INTO repositories_new (id, user_id, url, local_path, size_bytes, cloned_at, last_used)
|
|
SELECT id, user_id, url, local_path, size_bytes, cloned_at, last_used FROM repositories`)
|
|
// Only swap if the new table has data or old table is empty
|
|
var newCount, oldCount int
|
|
db.conn.QueryRow(`SELECT COUNT(*) FROM repositories_new`).Scan(&newCount)
|
|
db.conn.QueryRow(`SELECT COUNT(*) FROM repositories`).Scan(&oldCount)
|
|
if newCount >= oldCount {
|
|
db.conn.Exec(`DROP TABLE repositories`)
|
|
db.conn.Exec(`ALTER TABLE repositories_new RENAME TO repositories`)
|
|
} else {
|
|
db.conn.Exec(`DROP TABLE repositories_new`)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (db *DB) seedDefaults() error {
|
|
for key, val := range models.DefaultSettings {
|
|
_, err := db.conn.Exec(
|
|
`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, key, val,
|
|
)
|
|
if err != nil {
|
|
log.Printf("warning: failed to seed setting %s: %v", key, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|