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
+193
View File
@@ -0,0 +1,193 @@
package models
import (
"fmt"
"time"
"simple_portal/database"
)
// 访问动作类型常量
const (
ActionTypeVisit = "visit" // 主页访问
ActionTypeClick = "click" // 卡片点击
ActionTypeSearch = "search" // 搜索
)
// AccessLog 表示一条访问日志记录。
type AccessLog struct {
ID int `json:"id"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
ActionType string `json:"action_type"` // visit / click / search
Detail string `json:"detail"` // 卡片标题 / 搜索关键词
Referer string `json:"referer"`
CreatedAt time.Time `json:"created_at"`
}
// CreateAccessLog 插入一条访问日志。
func CreateAccessLog(ip, userAgent, actionType, detail, referer string) error {
_, err := database.DB.Exec(
"INSERT INTO access_logs (ip, user_agent, action_type, detail, referer, created_at) VALUES (?, ?, ?, ?, ?, ?)",
ip, userAgent, actionType, detail, referer, time.Now().Format("2006-01-02 15:04:05"),
)
if err != nil {
return fmt.Errorf("failed to create access log: %w", err)
}
return nil
}
// AccessLogStats 访问统计数据
type AccessLogStats struct {
TodayViews int `json:"today_views"` // 今日浏览次数
TotalViews int `json:"total_views"` // 总浏览次数
NewIPs int `json:"new_ips"` // 今日新IP数量
TotalIPs int `json:"total_ips"` // 总IP数量
}
// GetAccessLogStats 获取访问统计数据
func GetAccessLogStats() (*AccessLogStats, error) {
stats := &AccessLogStats{}
// 今日浏览次数
err := database.DB.QueryRow(
"SELECT COUNT(*) FROM access_logs WHERE date(created_at) = date('now', 'localtime')",
).Scan(&stats.TodayViews)
if err != nil {
return nil, fmt.Errorf("failed to count today views: %w", err)
}
// 总浏览次数
err = database.DB.QueryRow("SELECT COUNT(*) FROM access_logs").Scan(&stats.TotalViews)
if err != nil {
return nil, fmt.Errorf("failed to count total views: %w", err)
}
// 总IP数量
err = database.DB.QueryRow("SELECT COUNT(DISTINCT ip) FROM access_logs").Scan(&stats.TotalIPs)
if err != nil {
return nil, fmt.Errorf("failed to count total IPs: %w", err)
}
// 今日新IP数量(今日访问但之前从未出现过的IP)
err = database.DB.QueryRow(`
SELECT COUNT(DISTINCT a1.ip)
FROM access_logs a1
WHERE date(a1.created_at) = date('now', 'localtime')
AND NOT EXISTS (
SELECT 1 FROM access_logs a2
WHERE a2.ip = a1.ip AND date(a2.created_at) < date('now', 'localtime')
)
`).Scan(&stats.NewIPs)
if err != nil {
return nil, fmt.Errorf("failed to count new IPs: %w", err)
}
return stats, nil
}
// GetRecentAccessLogs 获取最近的访问日志(用于实时流量展示)
func GetRecentAccessLogs(limit int) ([]AccessLog, error) {
rows, err := database.DB.Query(
"SELECT id, ip, user_agent, action_type, detail, referer, created_at FROM access_logs ORDER BY created_at DESC LIMIT ?",
limit,
)
if err != nil {
return nil, fmt.Errorf("failed to query recent access logs: %w", err)
}
defer rows.Close()
var logs []AccessLog
for rows.Next() {
var l AccessLog
if err := rows.Scan(&l.ID, &l.IP, &l.UserAgent, &l.ActionType, &l.Detail, &l.Referer, &l.CreatedAt); err != nil {
return nil, fmt.Errorf("failed to scan access log: %w", err)
}
logs = append(logs, l)
}
return logs, rows.Err()
}
// GetAccessLogs 分页获取访问日志,支持按IP和动作类型筛选。
func GetAccessLogs(page, pageSize int, filterIP, filterAction string) ([]AccessLog, int, error) {
// 构建查询条件
where := "WHERE 1=1"
args := []interface{}{}
if filterIP != "" {
where += " AND ip LIKE ?"
args = append(args, "%"+filterIP+"%")
}
if filterAction != "" {
where += " AND action_type = ?"
args = append(args, filterAction)
}
// 获取总数
var total int
countArgs := make([]interface{}, len(args))
copy(countArgs, args)
err := database.DB.QueryRow("SELECT COUNT(*) FROM access_logs "+where, countArgs...).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("failed to count access logs: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
if offset < 0 {
offset = 0
}
queryArgs := append(args, pageSize, offset)
rows, err := database.DB.Query(
"SELECT id, ip, user_agent, action_type, detail, referer, created_at FROM access_logs "+where+" ORDER BY created_at DESC LIMIT ? OFFSET ?",
queryArgs...,
)
if err != nil {
return nil, 0, fmt.Errorf("failed to query access logs: %w", err)
}
defer rows.Close()
var logs []AccessLog
for rows.Next() {
var l AccessLog
if err := rows.Scan(&l.ID, &l.IP, &l.UserAgent, &l.ActionType, &l.Detail, &l.Referer, &l.CreatedAt); err != nil {
return nil, 0, fmt.Errorf("failed to scan access log: %w", err)
}
logs = append(logs, l)
}
return logs, total, rows.Err()
}
// GetAccessLogStatsByIP 获取按IP统计的访问数据
func GetAccessLogStatsByIP(limit int) ([]struct {
IP string `json:"ip"`
Visits int `json:"visits"`
LastSeen string `json:"last_seen"`
}, error) {
rows, err := database.DB.Query(
"SELECT ip, COUNT(*) as visits, MAX(created_at) as last_seen FROM access_logs GROUP BY ip ORDER BY visits DESC LIMIT ?",
limit,
)
if err != nil {
return nil, fmt.Errorf("failed to query IP stats: %w", err)
}
defer rows.Close()
var result []struct {
IP string `json:"ip"`
Visits int `json:"visits"`
LastSeen string `json:"last_seen"`
}
for rows.Next() {
var r struct {
IP string `json:"ip"`
Visits int `json:"visits"`
LastSeen string `json:"last_seen"`
}
if err := rows.Scan(&r.IP, &r.Visits, &r.LastSeen); err != nil {
return nil, fmt.Errorf("failed to scan IP stats: %w", err)
}
result = append(result, r)
}
return result, rows.Err()
}