- Go + Gin + html/template 服务端渲染 - 主页:Google 风格搜索框 + 导航卡片 - 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置 - 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1 - 安全:登录日志、修改密码、IP 自动封禁、IP 白名单 - 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计 - SQLite 存储(modernc.org/sqlite,纯 Go) - 内存 Session + bcrypt 密码哈希
182 lines
4.5 KiB
Go
182 lines
4.5 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// DB is the global database connection pointer.
|
|
var DB *sql.DB
|
|
|
|
// InitDB initializes the SQLite database, creates the data directory,
|
|
// opens the connection, sets WAL mode, and creates default tables and data.
|
|
func InitDB() error {
|
|
dbPath := filepath.Join(".", "data", "portal.db")
|
|
|
|
// Create data directory if not exists
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create data directory: %w", err)
|
|
}
|
|
|
|
var err error
|
|
DB, err = sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
// SQLite requires max open conns = 1 for safe writes
|
|
DB.SetMaxOpenConns(1)
|
|
|
|
// Enable WAL mode for better concurrent read performance
|
|
if _, err := DB.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
|
return fmt.Errorf("failed to set WAL mode: %w", err)
|
|
}
|
|
|
|
// Create tables
|
|
if err := createTables(); err != nil {
|
|
return fmt.Errorf("failed to create tables: %w", err)
|
|
}
|
|
|
|
// Seed default data
|
|
if err := seedData(); err != nil {
|
|
return fmt.Errorf("failed to seed data: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createTables creates the cards, settings, and admins tables.
|
|
func createTables() error {
|
|
_, err := DB.Exec(`
|
|
CREATE TABLE IF NOT EXISTS cards (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
icon TEXT,
|
|
title TEXT NOT NULL,
|
|
subtitle TEXT,
|
|
url TEXT NOT NULL,
|
|
sort INTEGER DEFAULT 0,
|
|
enabled INTEGER DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS admins (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS login_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
admin_id INTEGER,
|
|
username TEXT NOT NULL,
|
|
ip TEXT NOT NULL,
|
|
user_agent TEXT,
|
|
success INTEGER NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ip_bans (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ip TEXT NOT NULL,
|
|
reason TEXT,
|
|
fail_count INTEGER DEFAULT 0,
|
|
banned_until DATETIME NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ip_whitelist (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ip TEXT NOT NULL,
|
|
comment TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS access_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ip TEXT NOT NULL,
|
|
user_agent TEXT,
|
|
action_type TEXT NOT NULL,
|
|
detail TEXT DEFAULT '',
|
|
referer TEXT DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_access_logs_ip ON access_logs(ip);
|
|
CREATE INDEX IF NOT EXISTS idx_access_logs_action_type ON access_logs(action_type);
|
|
CREATE INDEX IF NOT EXISTS idx_access_logs_created_at ON access_logs(created_at);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
// seedData inserts default admin account and search engine setting if not present.
|
|
func seedData() error {
|
|
// Insert default admin if not exists
|
|
var count int
|
|
err := DB.QueryRow("SELECT COUNT(*) FROM admins WHERE username = ?", "admin").Scan(&count)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
_, err = DB.Exec(
|
|
"INSERT INTO admins (username, password) VALUES (?, ?)",
|
|
"admin",
|
|
"$2a$10$h3Csm2HmWUtvim3MJ8VG0OHx/tevZorlUXQVDtN2EgWhROtiM3Sg.", // bcrypt hash for "admin123"
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Insert default search engine setting if not exists
|
|
var settingCount int
|
|
err = DB.QueryRow("SELECT COUNT(*) FROM settings WHERE key = ?", "search_engine").Scan(&settingCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if settingCount == 0 {
|
|
_, err = DB.Exec(
|
|
"INSERT INTO settings (key, value) VALUES (?, ?)",
|
|
"search_engine",
|
|
"https://www.google.com/search?q=%s",
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Insert default homepage settings if not exists
|
|
defaultSettings := []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{"homepage_title", "Portal"},
|
|
{"homepage_subtitle", ""},
|
|
{"homepage_background", ""},
|
|
}
|
|
for _, s := range defaultSettings {
|
|
var cnt int
|
|
err = DB.QueryRow("SELECT COUNT(*) FROM settings WHERE key = ?", s.key).Scan(&cnt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cnt == 0 {
|
|
_, err = DB.Exec("INSERT INTO settings (key, value) VALUES (?, ?)", s.key, s.value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CloseDB closes the database connection.
|
|
func CloseDB() error {
|
|
if DB != nil {
|
|
return DB.Close()
|
|
}
|
|
return nil
|
|
}
|