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:
+224
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user