Files
kefu_cloud/server/internal/handler/contact_extract.go
T
yml2213 6070cf47dc 支持访客实时输入草稿展示与联系方式自动识别
坐席输入框上方显示「对方正在输入」草稿;从草稿/消息提取手机微信邮箱QQ并追加入库,不覆盖已有联系方式。
2026-07-19 00:19:24 +08:00

178 lines
5.0 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}