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