1. 海报

This commit is contained in:
李琦
2026-08-03 10:27:56 +08:00
parent 2d5fbbe62a
commit 76b894c446
2 changed files with 338 additions and 66 deletions

View File

@@ -6,6 +6,7 @@ import (
"io"
"log"
"net/http"
"sort"
"strconv"
"strings"
"sync"
@@ -251,6 +252,117 @@ func truncateRunes(s string, max int) string {
return string(r[:max])
}
// parseDeviceFromUA 从 UA 解析机型/平台,用于用户画像统计
func parseDeviceFromUA(ua string) string {
ua = strings.TrimSpace(ua)
if ua == "" {
return "未知"
}
lower := strings.ToLower(ua)
if strings.Contains(ua, "iPhone") {
return "iPhone"
}
if strings.Contains(ua, "iPad") {
return "iPad"
}
if strings.Contains(lower, "android") {
if model := extractAndroidModel(ua); model != "" {
return model
}
return "Android"
}
if strings.Contains(lower, "windows phone") {
return "Windows Phone"
}
if strings.Contains(lower, "windows") {
return "Windows"
}
if strings.Contains(ua, "Macintosh") || strings.Contains(lower, "mac os") {
return "Mac"
}
if strings.Contains(lower, "linux") {
return "Linux"
}
return "其他"
}
func extractAndroidModel(ua string) string {
// 常见形态Linux; Android 13; SM-S9180 Build/... 或 Android 12; Pixel 7)
idx := strings.Index(ua, "Android")
if idx < 0 {
return ""
}
rest := ua[idx:]
semi := strings.Index(rest, ";")
if semi < 0 {
return ""
}
rest = strings.TrimSpace(rest[semi+1:])
end := len(rest)
for _, sep := range []string{" Build", ")", "; HMSCore", "; HarmonyOS"} {
if p := strings.Index(rest, sep); p >= 0 && p < end {
end = p
}
}
model := strings.TrimSpace(rest[:end])
model = strings.Trim(model, "; ")
if model == "" || strings.EqualFold(model, "wv") || strings.HasPrefix(strings.ToLower(model), "linux") {
return ""
}
// 过长型号截断
r := []rune(model)
if len(r) > 40 {
model = string(r[:40])
}
return model
}
func aggregateDevices() []map[string]interface{} {
type uaRow struct {
Fingerprint string
UserAgent string
LastSeenAt time.Time
}
var rows []uaRow
db.Model(&SiteVisit{}).
Select("fingerprint, user_agent, last_seen_at").
Order("last_seen_at desc").
Find(&rows)
seen := map[string]struct{}{}
counts := map[string]int64{}
for _, r := range rows {
if _, ok := seen[r.Fingerprint]; ok {
continue
}
seen[r.Fingerprint] = struct{}{}
name := parseDeviceFromUA(r.UserAgent)
counts[name]++
}
type kv struct {
Name string
Value int64
}
list := make([]kv, 0, len(counts))
for name, value := range counts {
list = append(list, kv{Name: name, Value: value})
}
sort.Slice(list, func(i, j int) bool { return list[i].Value > list[j].Value })
if len(list) > 30 {
list = list[:30]
}
out := make([]map[string]interface{}, 0, len(list))
for _, item := range list {
out = append(out, map[string]interface{}{
"name": item.Name,
"value": item.Value,
})
}
return out
}
func handleTrackVisit(c *gin.Context) {
var body struct {
Fingerprint string `json:"fingerprint"`
@@ -372,6 +484,8 @@ func handleVisitStats(c *gin.Context) {
regions = []regionRow{}
}
devices := aggregateDevices()
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
@@ -379,6 +493,7 @@ func handleVisitStats(c *gin.Context) {
"pv": pv,
"today_uv": todayUV,
"regions": regions,
"devices": devices,
},
})
}
@@ -408,10 +523,31 @@ func handleVisitList(c *gin.Context) {
list = []SiteVisit{}
}
enriched := make([]gin.H, 0, len(list))
for _, row := range list {
enriched = append(enriched, gin.H{
"id": row.ID,
"fingerprint": row.Fingerprint,
"client_id": row.ClientID,
"ip": row.IP,
"country": row.Country,
"region": row.Region,
"city": row.City,
"isp": row.ISP,
"raw_location": row.RawLocation,
"user_agent": row.UserAgent,
"device": parseDeviceFromUA(row.UserAgent),
"referer": row.Referer,
"visit_count": row.VisitCount,
"first_seen_at": row.FirstSeenAt,
"last_seen_at": row.LastSeenAt,
})
}
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"list": list,
"list": enriched,
"total": total,
"page": page,
"pageSize": pageSize,