feat: 门户网站初始提交
- Go + Gin + html/template 服务端渲染 - 主页:Google 风格搜索框 + 导航卡片 - 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置 - 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1 - 安全:登录日志、修改密码、IP 自动封禁、IP 白名单 - 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计 - SQLite 存储(modernc.org/sqlite,纯 Go) - 内存 Session + bcrypt 密码哈希
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user