修复访客浏览轨迹延迟与排序,增加演示站
保留 SPA hash 以正确区分页面;延迟采集 title 避免慢一页;轨迹改为最新在上;补充 demo-shop 测试页与 Vite 静态路由。
This commit is contained in:
@@ -67,8 +67,12 @@ func sanitizePageURL(raw string) string {
|
||||
if changed {
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
// 去掉 fragment
|
||||
u.Fragment = ""
|
||||
// 保留 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)
|
||||
}
|
||||
|
||||
@@ -7,13 +7,22 @@ import (
|
||||
)
|
||||
|
||||
func TestSanitizePageURL(t *testing.T) {
|
||||
got := sanitizePageURL("https://shop.example.com/p/1?token=secret&ok=1#hash")
|
||||
if got == "" || strings.Contains(got, "token=") || strings.Contains(got, "#") {
|
||||
got := sanitizePageURL("https://shop.example.com/p/1?token=secret&ok=1#/cart")
|
||||
if got == "" || strings.Contains(got, "token=") {
|
||||
t.Fatalf("sanitize failed: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "ok=1") {
|
||||
t.Fatalf("should keep ok param: %q", got)
|
||||
}
|
||||
// SPA hash 路由必须保留,否则换页轨迹会被去重丢掉
|
||||
if !strings.Contains(got, "#/cart") {
|
||||
t.Fatalf("should keep hash fragment for SPA: %q", got)
|
||||
}
|
||||
home := sanitizePageURL("http://localhost:5173/demo-shop/index.html#/")
|
||||
cart := sanitizePageURL("http://localhost:5173/demo-shop/index.html#/cart")
|
||||
if home == "" || cart == "" || home == cart {
|
||||
t.Fatalf("hash pages must differ: home=%q cart=%q", home, cart)
|
||||
}
|
||||
if sanitizePageURL("javascript:alert(1)") != "" {
|
||||
t.Fatal("reject javascript")
|
||||
}
|
||||
|
||||
@@ -353,7 +353,8 @@ func (h *SessionHandler) Get(c *gin.Context) {
|
||||
var events []model.SessionEvent
|
||||
model.DB.Where("session_id = ?", session.ID).Order("created_at asc").Find(&events)
|
||||
var pageViews []model.VisitorPageView
|
||||
model.DB.Where("session_id = ?", session.ID).Order("entered_at asc").Limit(maxPageViewsPerSession).Find(&pageViews)
|
||||
// 最新在前,坐席侧无需滚到底
|
||||
model.DB.Where("session_id = ?", session.ID).Order("entered_at desc").Limit(maxPageViewsPerSession).Find(&pageViews)
|
||||
var pendingCount int64
|
||||
model.DB.Model(&model.Session{}).Where("customer_id = ? AND tenant_id = ? AND status = ?", session.CustomerID, session.TenantID, "waiting").Count(&pendingCount)
|
||||
|
||||
|
||||
@@ -274,13 +274,34 @@ func (h *WidgetHandler) PageView(c *gin.Context) {
|
||||
pageTitle := sanitizePageTitle(req.PageTitle)
|
||||
now := time.Now()
|
||||
|
||||
// 与当前页相同则只刷新 last_seen,不重复插轨迹
|
||||
// 与当前页相同:只刷新活跃时间/标题,不插重复轨迹
|
||||
if strings.TrimSpace(session.CurrentURL) == pageURL {
|
||||
_ = model.DB.Model(session).Updates(map[string]interface{}{
|
||||
"last_seen_at": now,
|
||||
"current_title": pageTitle,
|
||||
}).Error
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"deduped": true}})
|
||||
updates := map[string]interface{}{
|
||||
"last_seen_at": now,
|
||||
}
|
||||
titleFixed := false
|
||||
if pageTitle != "" && pageTitle != strings.TrimSpace(session.CurrentTitle) {
|
||||
updates["current_title"] = pageTitle
|
||||
// 同步最新一条轨迹的标题(纠正「慢一页」时写入的旧 title)
|
||||
var last model.VisitorPageView
|
||||
if err := model.DB.Where("session_id = ? AND url = ?", session.ID, pageURL).
|
||||
Order("entered_at desc").First(&last).Error; err == nil {
|
||||
_ = model.DB.Model(&last).Update("title", pageTitle).Error
|
||||
titleFixed = true
|
||||
if payload, err := ws.NewEvent("page_view", session.ID, gin.H{
|
||||
"id": last.ID,
|
||||
"session_id": last.SessionID,
|
||||
"url": pageURL,
|
||||
"title": pageTitle,
|
||||
"entered_at": last.EnteredAt,
|
||||
"title_fix": true,
|
||||
}); err == nil {
|
||||
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = model.DB.Model(session).Updates(updates).Error
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"deduped": true, "title_updated": titleFixed}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -324,7 +345,9 @@ func (h *WidgetHandler) PageView(c *gin.Context) {
|
||||
"title": pv.Title,
|
||||
"entered_at": pv.EnteredAt,
|
||||
}); err == nil {
|
||||
ws.DefaultHub.BroadcastToSessionStaff(session.TenantID, session.AgentID, payload)
|
||||
// 排队中 AgentID 为空时,BroadcastToSessionStaff 只会推给 admin/supervisor,
|
||||
// 一线坐席看不到实时轨迹;轨迹对租户内工作人员统一广播。
|
||||
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": pv})
|
||||
|
||||
Reference in New Issue
Block a user