扩展客户资料表单:多手机号、固话、微信QQ与备注
添加/编辑客户支持动态多手机、联系电话、备注等字段,详情侧栏同步展示。
This commit is contained in:
@@ -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 的 contacts(source=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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user