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:
2026-05-28 13:54:07 +08:00
commit c16a8dfbc4
42 changed files with 5295 additions and 0 deletions
+181
View File
@@ -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
}