339 lines
9.8 KiB
Go
339 lines
9.8 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"kefu-cloud/server/internal/middleware"
|
||
"kefu-cloud/server/internal/model"
|
||
)
|
||
|
||
type CustomerHandler struct{}
|
||
|
||
func NewCustomerHandler() *CustomerHandler { return &CustomerHandler{} }
|
||
|
||
func canAccessCustomer(c *gin.Context, customerID uint) bool {
|
||
if !middleware.HasPermission(c, "customer.view") {
|
||
return false
|
||
}
|
||
if middleware.CanAccessAllData(c, "customer") {
|
||
return true
|
||
}
|
||
var count int64
|
||
model.DB.Model(&model.Session{}).
|
||
Where("tenant_id = ? AND customer_id = ? AND agent_id = ?", middleware.GetTenantID(c), customerID, middleware.GetUserID(c)).
|
||
Count(&count)
|
||
return count > 0
|
||
}
|
||
|
||
func (h *CustomerHandler) List(c *gin.Context) {
|
||
tenantID := middleware.GetTenantID(c)
|
||
page, pageSize := middleware.GetPageParams(c)
|
||
search := c.Query("search")
|
||
status := c.Query("status")
|
||
source := c.Query("source")
|
||
|
||
var customers []model.Customer
|
||
var total int64
|
||
|
||
query := model.DB.Where("tenant_id = ?", tenantID)
|
||
if !middleware.CanAccessAllData(c, "customer") {
|
||
assignedCustomers := model.DB.Model(&model.Session{}).
|
||
Select("customer_id").
|
||
Where("tenant_id = ? AND agent_id = ?", tenantID, middleware.GetUserID(c))
|
||
query = query.Where("id IN (?)", assignedCustomers)
|
||
}
|
||
if 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)
|
||
}
|
||
if source != "" {
|
||
query = query.Where("source = ?", source)
|
||
}
|
||
|
||
query.Model(&model.Customer{}).Count(&total)
|
||
query.Order("updated_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&customers)
|
||
|
||
middleware.JSONList(c, customers, total, page, pageSize)
|
||
}
|
||
|
||
func (h *CustomerHandler) Get(c *gin.Context) {
|
||
tenantID := middleware.GetTenantID(c)
|
||
id := c.Param("id")
|
||
|
||
var customer model.Customer
|
||
if err := model.DB.Where("id = ? AND tenant_id = ?", id, tenantID).First(&customer).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "客户不存在"})
|
||
return
|
||
}
|
||
if !canAccessCustomer(c, customer.ID) {
|
||
middleware.AuditDataAccessDenied(c, "customer", &customer.ID)
|
||
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权查看该客户"})
|
||
return
|
||
}
|
||
|
||
var sessions []model.Session
|
||
sessionQuery := model.DB.Where("customer_id = ? AND tenant_id = ?", customer.ID, tenantID)
|
||
if !middleware.CanAccessAllData(c, "customer") {
|
||
sessionQuery = sessionQuery.Where("agent_id = ?", middleware.GetUserID(c))
|
||
}
|
||
sessionQuery.Order("created_at desc").Limit(20).Find(&sessions)
|
||
contacts := listCustomerContacts(customer.ID)
|
||
|
||
middleware.JSON(c, gin.H{
|
||
"customer": customer,
|
||
"sessions": sessions,
|
||
"contacts": contacts,
|
||
})
|
||
}
|
||
|
||
// 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 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 = "手动录入"
|
||
}
|
||
|
||
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
|
||
}
|
||
tags = normalized
|
||
} else {
|
||
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)
|
||
}
|
||
|
||
func (h *CustomerHandler) Update(c *gin.Context) {
|
||
tenantID := middleware.GetTenantID(c)
|
||
id := c.Param("id")
|
||
|
||
var customer model.Customer
|
||
if err := model.DB.Where("id = ? AND tenant_id = ?", id, tenantID).First(&customer).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "客户不存在"})
|
||
return
|
||
}
|
||
if !canAccessCustomer(c, customer.ID) {
|
||
middleware.AuditDataAccessDenied(c, "customer", &customer.ID)
|
||
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权编辑该客户"})
|
||
return
|
||
}
|
||
|
||
var raw map[string]interface{}
|
||
if err := c.ShouldBindJSON(&raw); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||
return
|
||
}
|
||
|
||
// 多手机号完整同步
|
||
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")
|
||
}
|
||
|
||
allowed := map[string]bool{
|
||
"name": true, "phone": true, "tel": true, "email": true, "wechat": true, "qq": true,
|
||
"remark": true, "tags": true, "source": true, "status": true,
|
||
}
|
||
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
|
||
}
|
||
updates["tags"] = normalized
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
func (h *CustomerHandler) Delete(c *gin.Context) {
|
||
if !middleware.HasAnyPermission(c, "customer.export") {
|
||
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅主管或管理员可删除客户"})
|
||
return
|
||
}
|
||
tenantID := middleware.GetTenantID(c)
|
||
id := c.Param("id")
|
||
|
||
result := model.DB.Where("id = ? AND tenant_id = ?", id, tenantID).Delete(&model.Customer{})
|
||
if result.RowsAffected == 0 {
|
||
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "客户不存在"})
|
||
return
|
||
}
|
||
|
||
middleware.JSON(c, gin.H{"message": "已删除"})
|
||
}
|