Files

339 lines
9.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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 (
"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 的 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 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": "已删除"})
}