1. 海报
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user