fix(widget): 优化访客在线续接与历史地区回填
CI / Backend (go vet + test) (push) Failing after 9m33s
CI / Frontend (typecheck + lint + build) (push) Successful in 11m57s

This commit is contained in:
yml2213
2026-11-06 18:51:44 +08:00
parent de1ffbf4f6
commit 93ef9f2040
6 changed files with 178 additions and 10 deletions
+11
View File
@@ -13,6 +13,7 @@ import (
"kefu-cloud/server/internal/metrics"
"kefu-cloud/server/internal/middleware"
"kefu-cloud/server/internal/model"
"kefu-cloud/server/internal/service"
"kefu-cloud/server/internal/storage"
"kefu-cloud/server/internal/ws"
)
@@ -29,6 +30,16 @@ func main() {
} else if geoip.Enabled() {
log.Printf("GeoIP 数据库已启用: %s", cfg.GeoIP.DBPath)
defer geoip.Close()
go func() {
updated, err := service.BackfillVisitorGeoIP(context.Background())
if err != nil {
log.Printf("警告: 历史访客地区回填失败: %v", err)
return
}
if updated > 0 {
log.Printf("历史访客地区回填完成: %d 条", updated)
}
}()
}
// 初始化 JWT(有效期可用 JWT_EXPIRE_HOURS 配置,默认 24 小时)
+74
View File
@@ -0,0 +1,74 @@
package service
import (
"context"
"net"
"strings"
"kefu-cloud/server/internal/geoip"
"kefu-cloud/server/internal/model"
)
const visitorGeoBackfillBatch = 500
var visitorGeoLookup = geoip.Lookup
// BackfillVisitorGeoIP 使用历史会话保存的 IP 补齐旧数据中的国家/省市信息。
func BackfillVisitorGeoIP(ctx context.Context) (int, error) {
var lastID uint
updated := 0
for {
select {
case <-ctx.Done():
return updated, ctx.Err()
default:
}
var sessions []model.Session
if err := model.DB.
Select("id", "visitor_ip").
Where("id > ? AND visitor_ip <> ''", lastID).
Where("visitor_country = '' AND (visitor_region = '' OR visitor_region = ?)", "未知").
Order("id asc").Limit(visitorGeoBackfillBatch).Find(&sessions).Error; err != nil {
return updated, err
}
if len(sessions) == 0 {
return updated, nil
}
for _, session := range sessions {
lastID = session.ID
location, ok := visitorGeoLookup(session.VisitorIP)
if !ok {
if isPrivateIP(session.VisitorIP) {
if err := model.DB.Model(&model.Session{}).Where("id = ?", session.ID).
Update("visitor_region", "内网").Error; err != nil {
return updated, err
}
updated++
}
continue
}
if strings.TrimSpace(location.Region) == "" {
continue
}
if err := model.DB.Model(&model.Session{}).Where("id = ?", session.ID).Updates(map[string]interface{}{
"visitor_region": location.Region,
"visitor_country": location.Country,
"visitor_province": location.Province,
"visitor_city": location.City,
}).Error; err != nil {
return updated, err
}
updated++
}
if len(sessions) < visitorGeoBackfillBatch {
return updated, nil
}
}
}
func isPrivateIP(raw string) bool {
ip := net.ParseIP(strings.TrimSpace(raw))
return ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast())
}
@@ -0,0 +1,66 @@
package service
import (
"context"
"fmt"
"testing"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"kefu-cloud/server/internal/geoip"
"kefu-cloud/server/internal/model"
)
func TestBackfillVisitorGeoIP(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:kefu_geo_backfill_%d?mode=memory&cache=shared", time.Now().UnixNano())), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := model.Migrate(db); err != nil {
t.Fatal(err)
}
oldDB := model.DB
model.DB = db
t.Cleanup(func() { model.DB = oldDB })
originalLookup := visitorGeoLookup
visitorGeoLookup = func(ip string) (geoip.Location, bool) {
if ip == "152.136.121.51" {
return geoip.Location{
Country: "中国", Province: "广东省", City: "深圳市", Region: "中国 广东省 深圳市",
}, true
}
return geoip.Location{}, false
}
t.Cleanup(func() { visitorGeoLookup = originalLookup })
sessions := []model.Session{
{TenantID: 1, ChannelID: 1, CustomerID: 1, VisitorIP: "152.136.121.51", VisitorRegion: "未知", Status: "ended"},
{TenantID: 1, ChannelID: 1, CustomerID: 2, VisitorIP: "192.168.1.10", VisitorRegion: "未知", Status: "ended"},
}
if err := db.Create(&sessions).Error; err != nil {
t.Fatal(err)
}
updated, err := BackfillVisitorGeoIP(context.Background())
if err != nil {
t.Fatal(err)
}
if updated != 2 {
t.Fatalf("updated=%d, want 2", updated)
}
if err := db.First(&sessions[0], sessions[0].ID).Error; err != nil {
t.Fatal(err)
}
if sessions[0].VisitorRegion != "中国 广东省 深圳市" ||
sessions[0].VisitorProvince != "广东省" || sessions[0].VisitorCity != "深圳市" {
t.Fatalf("公网 IP 未正确回填: %+v", sessions[0])
}
if err := db.First(&sessions[1], sessions[1].ID).Error; err != nil {
t.Fatal(err)
}
if sessions[1].VisitorRegion != "内网" {
t.Fatalf("私网 IP 应标记内网: %+v", sessions[1])
}
}