扩展客户资料表单:多手机号、固话、微信QQ与备注

添加/编辑客户支持动态多手机、联系电话、备注等字段,详情侧栏同步展示。
This commit is contained in:
yml2213
2026-07-19 00:49:56 +08:00
parent 1f92c35963
commit 4b7d8b04ea
4 changed files with 408 additions and 78 deletions
+175 -31
View File
@@ -45,7 +45,11 @@ func (h *CustomerHandler) List(c *gin.Context) {
query = query.Where("id IN (?)", assignedCustomers)
}
if search != "" {
query = query.Where("name LIKE ? OR phone LIKE ? OR email LIKE ?", "%"+search+"%", "%"+search+"%", "%"+search+"%")
like := "%" + search + "%"
query = query.Where(
"name LIKE ? OR phone LIKE ? OR tel LIKE ? OR email LIKE ? OR wechat LIKE ? OR qq LIKE ? OR remark LIKE ?",
like, like, like, like, like, like, like,
)
}
if status != "" {
query = query.Where("status = ?", status)
@@ -89,29 +93,132 @@ func (h *CustomerHandler) Get(c *gin.Context) {
})
}
// SaveCustomerReq 创建/更新客户(支持多手机号)。
// phones:多个手机号;第一项写入主字段 phone,全部写入 contacts。
type SaveCustomerReq struct {
Name string `json:"name"`
Phone string `json:"phone"` // 兼容:单手机
Phones []string `json:"phones"` // 多手机
Tel string `json:"tel"` // 联系电话/固话
Email string `json:"email"`
Wechat string `json:"wechat"`
QQ string `json:"qq"`
Remark string `json:"remark"`
Tags string `json:"tags"`
Source string `json:"source"`
Status string `json:"status"`
}
func normalizePhoneList(primary string, phones []string) []string {
seen := map[string]bool{}
var out []string
add := func(raw string) {
v := strings.TrimSpace(raw)
if v == "" || seen[v] {
return
}
seen[v] = true
out = append(out, v)
}
add(primary)
for _, p := range phones {
add(p)
}
return out
}
// syncCustomerPhones 以 phones 列表为准重写 kind=phone 的 contactssource=agent),并补主字段。
func syncCustomerPhones(tenantID, customerID uint, phones []string) {
// 删除坐席维护的手机号记录后重建;自动识别(draft/message)中不在新列表的保留
model.DB.Where("customer_id = ? AND kind = ? AND source = ?", customerID, "phone", "agent").Delete(&model.CustomerContact{})
for _, p := range phones {
var exists int64
model.DB.Model(&model.CustomerContact{}).
Where("customer_id = ? AND kind = ? AND value = ?", customerID, "phone", p).
Count(&exists)
if exists > 0 {
continue
}
_ = model.DB.Create(&model.CustomerContact{
TenantID: tenantID,
CustomerID: customerID,
Kind: "phone",
Value: p,
Source: "agent",
}).Error
}
}
func (h *CustomerHandler) Create(c *gin.Context) {
var customer model.Customer
if err := c.ShouldBindJSON(&customer); err != nil {
var req SaveCustomerReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
name := strings.TrimSpace(req.Name)
if name == "" || len([]rune(name)) < 2 || len([]rune(name)) > 50 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "客户姓名需 2-50 个字符"})
return
}
phones := normalizePhoneList(req.Phone, req.Phones)
primaryPhone := ""
if len(phones) > 0 {
primaryPhone = phones[0]
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "offline"
}
source := strings.TrimSpace(req.Source)
if source == "" {
source = "手动录入"
}
customer.TenantID = middleware.GetTenantID(c)
if customer.Tags != "" {
normalized, err := normalizeCustomerTagsForTenant(customer.TenantID, customer.Tags, nil)
tenantID := middleware.GetTenantID(c)
tags := req.Tags
if tags != "" {
normalized, err := normalizeCustomerTagsForTenant(tenantID, tags, nil)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
customer.Tags = normalized
tags = normalized
} else {
customer.Tags = "[]"
tags = "[]"
}
customer := model.Customer{
TenantID: tenantID,
Name: name,
Phone: primaryPhone,
Tel: strings.TrimSpace(req.Tel),
Email: strings.TrimSpace(req.Email),
Wechat: strings.TrimSpace(req.Wechat),
QQ: strings.TrimSpace(req.QQ),
Remark: strings.TrimSpace(req.Remark),
Tags: tags,
Source: source,
Status: status,
}
if err := model.DB.Create(&customer).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "创建失败"})
return
}
syncCustomerPhones(tenantID, customer.ID, phones)
// 主字段微信/邮箱/QQ 也写入 contacts(便于统一展示)
var extras []extractedContact
if customer.Wechat != "" {
extras = append(extras, extractedContact{Kind: "wechat", Value: customer.Wechat})
}
if customer.QQ != "" {
extras = append(extras, extractedContact{Kind: "qq", Value: customer.QQ})
}
if customer.Email != "" {
extras = append(extras, extractedContact{Kind: "email", Value: customer.Email})
}
if len(extras) > 0 {
_, _ = mergeCustomerContacts(tenantID, customer.ID, extras, "agent")
}
middleware.JSON(c, customer)
}
@@ -130,35 +237,63 @@ func (h *CustomerHandler) Update(c *gin.Context) {
return
}
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
var raw map[string]interface{}
if err := c.ShouldBindJSON(&raw); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
allowed := map[string]bool{
"name": true, "phone": true, "email": true, "wechat": true, "qq": true,
"tags": true, "source": true, "status": true,
}
for key := range updates {
if !allowed[key] {
delete(updates, key)
}
}
// 字符串字段统一 trim
for _, k := range []string{"name", "phone", "email", "wechat", "qq", "source", "status"} {
if v, ok := updates[k]; ok {
if s, ok := v.(string); ok {
updates[k] = strings.TrimSpace(s)
// 多手机号完整同步
var phones []string
hasPhones := false
if v, ok := raw["phones"]; ok {
hasPhones = true
if arr, ok := v.([]interface{}); ok {
for _, item := range arr {
if s, ok := item.(string); ok {
phones = append(phones, s)
}
}
}
primaryExtra := ""
if p, ok := raw["phone"].(string); ok {
primaryExtra = p
}
phones = normalizePhoneList(primaryExtra, phones)
delete(raw, "phones")
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "没有可更新字段"})
return
allowed := map[string]bool{
"name": true, "phone": true, "tel": true, "email": true, "wechat": true, "qq": true,
"remark": true, "tags": true, "source": true, "status": true,
}
if raw, ok := updates["tags"]; ok {
normalized, err := normalizeCustomerTagsForTenant(tenantID, raw, parseCustomerTagsJSON(customer.Tags))
updates := map[string]interface{}{}
for key, val := range raw {
if !allowed[key] {
continue
}
if s, ok := val.(string); ok {
updates[key] = strings.TrimSpace(s)
} else {
updates[key] = val
}
}
if hasPhones {
primary := ""
if len(phones) > 0 {
primary = phones[0]
}
updates["phone"] = primary
}
if rawName, ok := updates["name"].(string); ok {
if rawName == "" || len([]rune(rawName)) < 2 || len([]rune(rawName)) > 50 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "客户姓名需 2-50 个字符"})
return
}
}
if rawTags, ok := updates["tags"]; ok {
normalized, err := normalizeCustomerTagsForTenant(tenantID, rawTags, parseCustomerTagsJSON(customer.Tags))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
@@ -166,10 +301,19 @@ func (h *CustomerHandler) Update(c *gin.Context) {
updates["tags"] = normalized
}
if err := model.DB.Model(&customer).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "更新失败"})
if len(updates) == 0 && !hasPhones {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "没有可更新字段"})
return
}
if len(updates) > 0 {
if err := model.DB.Model(&customer).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "更新失败"})
return
}
}
if hasPhones {
syncCustomerPhones(tenantID, customer.ID, phones)
}
model.DB.First(&customer, customer.ID)
middleware.JSON(c, customer)
}