package handler import ( "regexp" "strings" "unicode/utf8" "kefu-cloud/server/internal/model" "kefu-cloud/server/internal/ws" ) var ( reMobileCN = regexp.MustCompile(`(?i)(?:手机|电话|联系|号|tel|phone|mobile)?[::\s]*((?:\+?86[-\s]?)?1[3-9]\d{9})`) reMobileStrict = regexp.MustCompile(`(?:^|[^\d])(1[3-9]\d{9})(?:[^\d]|$)`) reEmail = regexp.MustCompile(`(?i)[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}`) // 允许「微信是 xxx / 微信:xxx / 微信号 xxx」等口语写法 reWechat = regexp.MustCompile(`(?i)(?:微信|微信号|vx|v信|wx)[号]?(?:是|为|:|:|[\s ]+)[\s ]*([a-zA-Z][-_a-zA-Z0-9]{5,19})`) reWechatID = regexp.MustCompile(`(?i)\b(wxid_[a-zA-Z0-9]{5,20})\b`) reQQ = regexp.MustCompile(`(?i)(?:QQ|扣扣)[号]?(?:是|为|:|:|[\s ]+)[\s ]*([1-9]\d{4,11})`) ) type extractedContact struct { Kind string Value string } // extractContactsFromText 从文本中提取联系方式(手机/微信/邮箱/QQ)。 func extractContactsFromText(text string) []extractedContact { text = strings.TrimSpace(text) if text == "" { return nil } // 限制扫描长度 if utf8.RuneCountInString(text) > 2000 { text = string([]rune(text)[:2000]) } seen := map[string]bool{} var out []extractedContact add := func(kind, val string) { val = strings.TrimSpace(val) if val == "" { return } // 规范化手机号 if kind == "phone" { val = regexp.MustCompile(`[^\d]`).ReplaceAllString(val, "") if strings.HasPrefix(val, "86") && len(val) == 13 { val = val[2:] } if len(val) != 11 || val[0] != '1' { return } } key := kind + ":" + strings.ToLower(val) if seen[key] { return } seen[key] = true out = append(out, extractedContact{Kind: kind, Value: val}) } for _, m := range reMobileCN.FindAllStringSubmatch(text, -1) { if len(m) > 1 { add("phone", m[1]) } } for _, m := range reMobileStrict.FindAllStringSubmatch(text, -1) { if len(m) > 1 { add("phone", m[1]) } } for _, m := range reEmail.FindAllString(text, -1) { add("email", strings.ToLower(m)) } for _, m := range reWechat.FindAllStringSubmatch(text, -1) { if len(m) > 1 { add("wechat", m[1]) } } for _, m := range reWechatID.FindAllStringSubmatch(text, -1) { if len(m) > 1 { add("wechat", m[1]) } } for _, m := range reQQ.FindAllStringSubmatch(text, -1) { if len(m) > 1 { add("qq", m[1]) } } return out } // mergeCustomerContacts 将提取结果写入客户联系方式表(已存在则跳过,不覆盖)。 // 同时:若 customers.phone/email 为空,用第一条补全(兼容旧列表搜索)。 // 返回新插入的条数。 func mergeCustomerContacts(tenantID, customerID uint, contacts []extractedContact, source string) (int, []model.CustomerContact) { if customerID == 0 || len(contacts) == 0 { return 0, nil } var customer model.Customer if err := model.DB.Where("id = ? AND tenant_id = ?", customerID, tenantID).First(&customer).Error; err != nil { return 0, nil } added := 0 var inserted []model.CustomerContact updates := map[string]interface{}{} for _, c := range contacts { var exists int64 model.DB.Model(&model.CustomerContact{}). Where("customer_id = ? AND kind = ? AND value = ?", customerID, c.Kind, c.Value). Count(&exists) if exists > 0 { continue } row := model.CustomerContact{ TenantID: tenantID, CustomerID: customerID, Kind: c.Kind, Value: c.Value, Source: source, } if err := model.DB.Create(&row).Error; err != nil { continue } added++ inserted = append(inserted, row) // 主字段仅在为空时填充,绝不覆盖已有值 if c.Kind == "phone" && strings.TrimSpace(customer.Phone) == "" && updates["phone"] == nil { updates["phone"] = c.Value customer.Phone = c.Value } if c.Kind == "email" && strings.TrimSpace(customer.Email) == "" && updates["email"] == nil { updates["email"] = c.Value customer.Email = c.Value } } if len(updates) > 0 { model.DB.Model(&model.Customer{}).Where("id = ?", customerID).Updates(updates) } return added, inserted } func listCustomerContacts(customerID uint) []model.CustomerContact { var list []model.CustomerContact model.DB.Where("customer_id = ?", customerID).Order("id asc").Find(&list) return list } // onVisitorDraftContacts 草稿文本提取联系方式并通知坐席刷新客户资料。 func onVisitorDraftContacts(tenantID, customerID, sessionID uint, text string) { contacts := extractContactsFromText(text) if len(contacts) == 0 { return } n, inserted := mergeCustomerContacts(tenantID, customerID, contacts, "draft") if n == 0 { return } // 推送客户资料更新(含全部联系方式) var customer model.Customer if err := model.DB.First(&customer, customerID).Error; err != nil { return } all := listCustomerContacts(customerID) payload, err := ws.NewEvent("customer_updated", sessionID, map[string]interface{}{ "customer": customer, "contacts": all, "added_contacts": inserted, }) if err == nil { ws.DefaultHub.BroadcastToTenantStaff(tenantID, payload) } }