Files
kefu_cloud/server/internal/handler/page_track.go
T
yml2213 970bae16d1 修复访客浏览轨迹延迟与排序,增加演示站
保留 SPA hash 以正确区分页面;延迟采集 title 避免慢一页;轨迹改为最新在上;补充 demo-shop 测试页与 Vite 静态路由。
2026-07-18 23:22:47 +08:00

83 lines
2.2 KiB
Go

package handler
import (
"net/url"
"strings"
"unicode/utf8"
)
const (
maxPageURLLen = 1000
maxPageTitleLen = 200
// 单会话最多保留轨迹条数(超出删最旧)
maxPageViewsPerSession = 50
)
// 常见敏感 query 参数,上报 URL 时剥离
var sensitiveQueryKeys = map[string]bool{
"token": true, "access_token": true, "refresh_token": true,
"password": true, "passwd": true, "pwd": true,
"phone": true, "mobile": true, "tel": true,
"idcard": true, "id_card": true, "secret": true, "key": true,
"authorization": true, "auth": true, "session": true, "sid": true,
"code": true, "otp": true, "verify": true,
}
func truncateRunes(s string, max int) string {
s = strings.TrimSpace(s)
if max <= 0 || s == "" {
return s
}
if utf8.RuneCountInString(s) <= max {
return s
}
r := []rune(s)
return string(r[:max])
}
// sanitizePageURL 规范化并脱敏 URL,非法则返回空。
func sanitizePageURL(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if utf8.RuneCountInString(raw) > maxPageURLLen*2 {
raw = string([]rune(raw)[:maxPageURLLen*2])
}
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
// 允许无 scheme 的相对路径拼不成绝对时,若已是 http(s) 失败则截断原文
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
return truncateRunes(raw, maxPageURLLen)
}
return ""
}
if u.Scheme != "http" && u.Scheme != "https" {
return ""
}
q := u.Query()
changed := false
for k := range q {
lk := strings.ToLower(k)
if sensitiveQueryKeys[lk] || strings.Contains(lk, "token") || strings.Contains(lk, "password") {
q.Del(k)
changed = true
}
}
if changed {
u.RawQuery = q.Encode()
}
// 保留 hash:大量 SPA(含演示站 demo-shop)靠 #/path 区分页面;
// 若去掉 fragment,换页后 URL 相同会被服务端当成重复而丢弃轨迹。
// 仅截断过长 hash,避免异常数据撑爆字段。
if utf8.RuneCountInString(u.Fragment) > 200 {
u.Fragment = string([]rune(u.Fragment)[:200])
}
out := u.String()
return truncateRunes(out, maxPageURLLen)
}
func sanitizePageTitle(raw string) string {
return truncateRunes(raw, maxPageTitleLen)
}