Build and publish Docker images / Build and publish images (push) Successful in 2m10s
159 lines
4.4 KiB
Go
159 lines
4.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
const (
|
|
legacyAnonymousVisitorName = "Anonymous Visitor"
|
|
anonymousVisitorName = "匿名客户"
|
|
|
|
visitorNameSourceKey = "visitor_name_source"
|
|
visitorProvinceKey = "visitor_province"
|
|
visitorCityKey = "visitor_city"
|
|
visitorGeoAtKey = "visitor_geo_at"
|
|
visitorNameSourceGeoIP = "ip_geolocation"
|
|
visitorNameSourceFallback = "anonymous_fallback"
|
|
)
|
|
|
|
type visitorIdentity struct {
|
|
name string
|
|
attributes map[string]any
|
|
}
|
|
|
|
func (s *WidgetService) anonymousVisitorIdentity(clientIP string) visitorIdentity {
|
|
identity := visitorIdentity{
|
|
name: anonymousVisitorName,
|
|
attributes: map[string]any{
|
|
visitorNameSourceKey: visitorNameSourceFallback,
|
|
},
|
|
}
|
|
if s.visitorGeoResolver == nil {
|
|
return identity
|
|
}
|
|
|
|
ip := net.ParseIP(strings.TrimSpace(clientIP))
|
|
if ip == nil {
|
|
return identity
|
|
}
|
|
location, ok := s.visitorGeoResolver.Lookup(ip)
|
|
if !ok {
|
|
return identity
|
|
}
|
|
name := formatVisitorName(location.Province, location.City)
|
|
if name == "" {
|
|
return identity
|
|
}
|
|
|
|
identity.name = name
|
|
identity.attributes = map[string]any{
|
|
visitorNameSourceKey: visitorNameSourceGeoIP,
|
|
visitorGeoAtKey: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
if province := normalizeRegionPart(location.Province); province != "" {
|
|
identity.attributes[visitorProvinceKey] = province
|
|
}
|
|
if city := normalizeRegionPart(location.City); city != "" {
|
|
identity.attributes[visitorCityKey] = city
|
|
}
|
|
return identity
|
|
}
|
|
|
|
func (s *WidgetService) enrichAnonymousWidgetContact(ctx context.Context, contact *model.Contact, clientIP string) (*model.Contact, error) {
|
|
if contact == nil || !isAnonymousWidgetContact(contact) {
|
|
return contact, nil
|
|
}
|
|
|
|
attributes := jsonMap(contact.AdditionalAttributes)
|
|
if source, _ := attributes[visitorNameSourceKey].(string); source == visitorNameSourceGeoIP {
|
|
return contact, nil
|
|
}
|
|
identity := s.anonymousVisitorIdentity(clientIP)
|
|
if contact.Name == identity.name && reflect.DeepEqual(attributes[visitorNameSourceKey], identity.attributes[visitorNameSourceKey]) {
|
|
return contact, nil
|
|
}
|
|
|
|
contact.Name = identity.name
|
|
for key, value := range identity.attributes {
|
|
attributes[key] = value
|
|
}
|
|
contact.AdditionalAttributes = mustJSON(attributes)
|
|
if err := s.contactRepo.Update(ctx, contact); err != nil {
|
|
return nil, err
|
|
}
|
|
return contact, nil
|
|
}
|
|
|
|
func isAnonymousWidgetContact(contact *model.Contact) bool {
|
|
if contact == nil {
|
|
return false
|
|
}
|
|
name := strings.TrimSpace(contact.Name)
|
|
if name == "" || name == legacyAnonymousVisitorName || name == anonymousVisitorName {
|
|
return true
|
|
}
|
|
attributes := jsonMap(contact.AdditionalAttributes)
|
|
source, _ := attributes[visitorNameSourceKey].(string)
|
|
if source == visitorNameSourceGeoIP {
|
|
return true
|
|
}
|
|
return source == visitorNameSourceFallback && (name == "" || name == legacyAnonymousVisitorName || name == anonymousVisitorName)
|
|
}
|
|
|
|
func formatVisitorName(province, city string) string {
|
|
province = normalizeRegionPart(province)
|
|
city = normalizeRegionPart(city)
|
|
base := city
|
|
if base == "" {
|
|
base = province
|
|
} else if province != "" && city != province && !strings.HasPrefix(city, province) {
|
|
base = province + city
|
|
}
|
|
if base == "" {
|
|
return ""
|
|
}
|
|
return base + "客户"
|
|
}
|
|
|
|
func normalizeRegionPart(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
for _, suffix := range []string{"特别行政区", "自治区", "自治州", "省", "市", "地区", "盟"} {
|
|
if strings.HasSuffix(value, suffix) && len([]rune(value)) > len([]rune(suffix)) {
|
|
return strings.TrimSpace(strings.TrimSuffix(value, suffix))
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
func isServerManagedVisitorAttribute(key string) bool {
|
|
switch key {
|
|
case visitorNameSourceKey, visitorProvinceKey, visitorCityKey, visitorGeoAtKey, "created_at_ip":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func filterWidgetAdditionalAttributes(attributes map[string]any) map[string]any {
|
|
filtered := make(map[string]any, len(attributes))
|
|
for key, value := range attributes {
|
|
if !isServerManagedVisitorAttribute(key) {
|
|
filtered[key] = value
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// PublicWidgetAdditionalAttributes removes server-owned visitor metadata from
|
|
// the public widget payload while retaining customer-provided attributes.
|
|
func PublicWidgetAdditionalAttributes(raw datatypes.JSON) map[string]any {
|
|
return filterWidgetAdditionalAttributes(jsonMap(raw))
|
|
}
|