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
+224
View File
@@ -0,0 +1,224 @@
package models
import (
"database/sql"
"fmt"
"simple_portal/database"
)
// Card represents a navigation card on the portal home page.
type Card struct {
ID int `json:"id"`
Icon string `json:"icon"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
URL string `json:"url"`
Sort int `json:"sort"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"created_at"`
}
// GetAllCards returns all cards ordered by sort ascending.
func GetAllCards() ([]Card, error) {
rows, err := database.DB.Query("SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards ORDER BY sort ASC")
if err != nil {
return nil, fmt.Errorf("failed to query cards: %w", err)
}
defer rows.Close()
var cards []Card
for rows.Next() {
var c Card
var enabled int
if err := rows.Scan(&c.ID, &c.Icon, &c.Title, &c.Subtitle, &c.URL, &c.Sort, &enabled, &c.CreatedAt); err != nil {
return nil, fmt.Errorf("failed to scan card: %w", err)
}
c.Enabled = enabled == 1
cards = append(cards, c)
}
return cards, rows.Err()
}
// GetEnabledCards returns only enabled cards ordered by sort ascending.
func GetEnabledCards() ([]Card, error) {
rows, err := database.DB.Query("SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE enabled = 1 ORDER BY sort ASC")
if err != nil {
return nil, fmt.Errorf("failed to query enabled cards: %w", err)
}
defer rows.Close()
var cards []Card
for rows.Next() {
var c Card
var enabled int
if err := rows.Scan(&c.ID, &c.Icon, &c.Title, &c.Subtitle, &c.URL, &c.Sort, &enabled, &c.CreatedAt); err != nil {
return nil, fmt.Errorf("failed to scan card: %w", err)
}
c.Enabled = enabled == 1
cards = append(cards, c)
}
return cards, rows.Err()
}
// GetCardByID returns a single card by its ID.
func GetCardByID(id int) (*Card, error) {
var c Card
var enabled int
err := database.DB.QueryRow(
"SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE id = ?",
id,
).Scan(&c.ID, &c.Icon, &c.Title, &c.Subtitle, &c.URL, &c.Sort, &enabled, &c.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to query card by id: %w", err)
}
c.Enabled = enabled == 1
return &c, nil
}
// CreateCard inserts a new card with sort = MAX(sort) + 1.
func CreateCard(card *Card) error {
var maxSort sql.NullInt64
err := database.DB.QueryRow("SELECT MAX(sort) FROM cards").Scan(&maxSort)
if err != nil {
return fmt.Errorf("failed to get max sort: %w", err)
}
newSort := 0
if maxSort.Valid {
newSort = int(maxSort.Int64) + 1
}
enabledInt := 0
if card.Enabled {
enabledInt = 1
}
result, err := database.DB.Exec(
"INSERT INTO cards (icon, title, subtitle, url, sort, enabled) VALUES (?, ?, ?, ?, ?, ?)",
card.Icon, card.Title, card.Subtitle, card.URL, newSort, enabledInt,
)
if err != nil {
return fmt.Errorf("failed to insert card: %w", err)
}
lastID, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("failed to get last insert id: %w", err)
}
card.ID = int(lastID)
card.Sort = newSort
return nil
}
// UpdateCard updates an existing card by ID.
func UpdateCard(card *Card) error {
enabledInt := 0
if card.Enabled {
enabledInt = 1
}
_, err := database.DB.Exec(
"UPDATE cards SET icon = ?, title = ?, subtitle = ?, url = ?, sort = ?, enabled = ? WHERE id = ?",
card.Icon, card.Title, card.Subtitle, card.URL, card.Sort, enabledInt, card.ID,
)
if err != nil {
return fmt.Errorf("failed to update card: %w", err)
}
return nil
}
// DeleteCard deletes a card by ID.
func DeleteCard(id int) error {
_, err := database.DB.Exec("DELETE FROM cards WHERE id = ?", id)
if err != nil {
return fmt.Errorf("failed to delete card: %w", err)
}
return nil
}
// ToggleCard toggles the enabled status of a card.
func ToggleCard(id int) error {
_, err := database.DB.Exec("UPDATE cards SET enabled = CASE WHEN enabled = 1 THEN 0 ELSE 1 END WHERE id = ?", id)
if err != nil {
return fmt.Errorf("failed to toggle card: %w", err)
}
return nil
}
// MoveCardUp swaps the sort value of the card with the one above it.
func MoveCardUp(id int) error {
// Get the current card
card, err := GetCardByID(id)
if err != nil || card == nil {
return fmt.Errorf("card not found: %d", id)
}
// Find the card with the next lower sort value (the one above)
var aboveCard Card
var aboveEnabled int
err = database.DB.QueryRow(
"SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE sort < ? ORDER BY sort DESC LIMIT 1",
card.Sort,
).Scan(&aboveCard.ID, &aboveCard.Icon, &aboveCard.Title, &aboveCard.Subtitle, &aboveCard.URL, &aboveCard.Sort, &aboveEnabled, &aboveCard.CreatedAt)
if err == sql.ErrNoRows {
// Already at the top
return nil
}
if err != nil {
return fmt.Errorf("failed to find card above: %w", err)
}
aboveCard.Enabled = aboveEnabled == 1
// Swap sort values
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", aboveCard.Sort, card.ID)
if err != nil {
return fmt.Errorf("failed to swap sort (current): %w", err)
}
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", card.Sort, aboveCard.ID)
if err != nil {
return fmt.Errorf("failed to swap sort (above): %w", err)
}
return nil
}
// MoveCardDown swaps the sort value of the card with the one below it.
func MoveCardDown(id int) error {
// Get the current card
card, err := GetCardByID(id)
if err != nil || card == nil {
return fmt.Errorf("card not found: %d", id)
}
// Find the card with the next higher sort value (the one below)
var belowCard Card
var belowEnabled int
err = database.DB.QueryRow(
"SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE sort > ? ORDER BY sort ASC LIMIT 1",
card.Sort,
).Scan(&belowCard.ID, &belowCard.Icon, &belowCard.Title, &belowCard.Subtitle, &belowCard.URL, &belowCard.Sort, &belowEnabled, &belowCard.CreatedAt)
if err == sql.ErrNoRows {
// Already at the bottom
return nil
}
if err != nil {
return fmt.Errorf("failed to find card below: %w", err)
}
belowCard.Enabled = belowEnabled == 1
// Swap sort values
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", belowCard.Sort, card.ID)
if err != nil {
return fmt.Errorf("failed to swap sort (current): %w", err)
}
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", card.Sort, belowCard.ID)
if err != nil {
return fmt.Errorf("failed to swap sort (below): %w", err)
}
return nil
}