package controllers import ( "encoding/json" "fmt" "io" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // BackendCrmContactController CRM 联系人管理(客户 / 供应商的对接人) // // 数据复用已有的「公司联系人」表 models.ErpCompanyContact: // - related_type(1=客户,2=供应商) <-> company_type(customer/supplier) // - related_id <-> company_id // - contact_name <-> name // - mobile(手机号) <-> mobiles(JSON数组,取第一个) // - phone(座机) <-> phone // - wechat / qq / dingtalk / home_address / status <-> 同名字段 // // 这样 CRM 联系人与 ERP 公司联系人是同一份数据,不会分裂。 type BackendCrmContactController struct { beego.Controller } // crmContactDTO 返回给前端的联系人结构 type crmContactDTO struct { ID uint64 `json:"id"` RelatedType int `json:"related_type"` RelatedID uint64 `json:"related_id"` RelatedName string `json:"related_name"` ContactName string `json:"contact_name"` Gender int8 `json:"gender"` Mobile string `json:"mobile"` // 手机号(mobiles 数组第一个) Mobiles string `json:"mobiles"` // 手机号原始 JSON 数组 Phone string `json:"phone"` // 座机 Email string `json:"email"` Wechat string `json:"wechat"` // 微信 QQ string `json:"qq"` // QQ Dingtalk string `json:"dingtalk"` // 钉钉 Department string `json:"department"` Position string `json:"position"` HomeAddress string `json:"home_address"` // 家庭住址 IsPrimary int8 `json:"is_primary"` Remark string `json:"remark"` Status int8 `json:"status"` // 1-在职 0-离职 CreateTime time.Time `json:"create_time"` UpdateTime time.Time `json:"update_time"` } // buildMobiles 将单个手机号转为 mobiles 的 JSON 数组字符串 func buildMobiles(mobile string) string { mobile = strings.TrimSpace(mobile) if mobile == "" { return "" } arr := []string{mobile} b, err := json.Marshal(arr) if err != nil { return "" } return string(b) } // firstMobile 从 mobiles(JSON 数组或普通字符串)中取出第一个手机号 func firstMobile(mobiles string) string { mobiles = strings.TrimSpace(mobiles) if mobiles == "" { return "" } var arr []string if err := json.Unmarshal([]byte(mobiles), &arr); err == nil { for _, m := range arr { if strings.TrimSpace(m) != "" { return strings.TrimSpace(m) } } return "" } // 非 JSON,按原样返回 return mobiles } func crmContactCompanyType(relatedType int) string { if relatedType == 2 { return "supplier" } return "customer" } func crmContactRelatedType(companyType string) int { if companyType == "supplier" { return 2 } return 1 } // crmContactAtoi 将查询参数安全转为 int,非法值返回 0 func crmContactAtoi(s string) int { n, _ := strconv.Atoi(strings.TrimSpace(s)) return n } func (c *BackendCrmContactController) contactClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return nil, fmt.Errorf("未登录") } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, fmt.Errorf("认证信息格式错误") } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { return nil, fmt.Errorf("无效的token") } if claims.UserType != "backend" { return nil, fmt.Errorf("无权访问") } return claims, nil } func (c *BackendCrmContactController) crmContactJsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } func (c *BackendCrmContactController) crmContactOk(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } // toCrmContactDTO 模型转 DTO,names 为关联对象名称映射(key: "customer:123") func toCrmContactDTO(row models.ErpCompanyContact, names map[string]string) crmContactDTO { relatedType := crmContactRelatedType(row.CompanyType) key := fmt.Sprintf("%s:%d", row.CompanyType, row.CompanyID) return crmContactDTO{ ID: row.ID, RelatedType: relatedType, RelatedID: row.CompanyID, RelatedName: names[key], ContactName: row.Name, Gender: row.Gender, Mobile: firstMobile(row.Mobiles), Mobiles: row.Mobiles, Phone: row.Phone, Email: row.Email, Wechat: row.Wechat, QQ: row.QQ, Dingtalk: row.Dingtalk, Department: row.Department, Position: row.Position, HomeAddress: row.HomeAddress, IsPrimary: row.IsPrimary, Remark: row.Remark, Status: row.Status, CreateTime: row.CreateTime, UpdateTime: row.UpdateTime, } } // loadRelatedNames 批量查询关联的客户/供应商名称,避免逐条查询 func loadRelatedNames(tenantID string, rows []models.ErpCompanyContact) map[string]string { names := map[string]string{} if len(rows) == 0 || models.Orm == nil { return names } customerIDs := make([]uint64, 0) supplierIDs := make([]uint64, 0) for _, r := range rows { if r.CompanyType == "supplier" { supplierIDs = append(supplierIDs, r.CompanyID) } else { customerIDs = append(customerIDs, r.CompanyID) } } if len(customerIDs) > 0 { var list []models.TenantCrmCustomer if _, err := models.Orm.QueryTable(new(models.TenantCrmCustomer)). Filter("tenant_id", tenantID). Filter("id__in", customerIDs). All(&list); err == nil { for _, r := range list { names[fmt.Sprintf("customer:%d", r.ID)] = r.CustomerName } } } if len(supplierIDs) > 0 { var list []models.TenantCrmSupplier if _, err := models.Orm.QueryTable(new(models.TenantCrmSupplier)). Filter("tenant_id", tenantID). Filter("id__in", supplierIDs). All(&list); err == nil { for _, r := range list { names[fmt.Sprintf("supplier:%d", r.ID)] = r.SupplierName } } } return names } // clearOtherPrimary 将同一公司下的其他联系人取消主联系人,保证只有一个主联系人 func clearOtherPrimary(tenantID, companyType string, companyID, excludeID uint64) { if models.Orm == nil || companyID == 0 { return } qs := models.Orm.QueryTable(new(models.ErpCompanyContact)). Filter("tenant_id", tenantID). Filter("company_type", companyType). Filter("company_id", companyID). Filter("delete_time__isnull", true). Exclude("id", excludeID) _, _ = qs.Update(map[string]interface{}{"is_primary": 0}) } // List GET /backend/crm/contact/list?related_type=1&related_id=7&keyword=&is_primary= func (c *BackendCrmContactController) List() { claims, err := c.contactClaims() if err != nil { c.crmContactJsonErr(401, 401, err.Error()) return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) keyword := strings.TrimSpace(c.GetString("keyword")) relatedTypeStr := strings.TrimSpace(c.GetString("related_type")) relatedIDStr := strings.TrimSpace(c.GetString("related_id")) primaryStr := strings.TrimSpace(c.GetString("is_primary")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } tenantID := fmt.Sprintf("%d", claims.TenantId) cond := orm.NewCondition(). And("tenant_id", tenantID). And("delete_time__isnull", true) if relatedTypeStr != "" { cond = cond.And("company_type", crmContactCompanyType(crmContactAtoi(relatedTypeStr))) } if relatedIDStr != "" && relatedIDStr != "0" { cond = cond.And("company_id", relatedIDStr) } if primaryStr != "" { cond = cond.And("is_primary", crmContactAtoi(primaryStr)) } if keyword != "" { kw := orm.NewCondition(). Or("name__contains", keyword). Or("phone__contains", keyword). Or("email__contains", keyword) cond = cond.AndCond(kw) } qs := models.Orm.QueryTable(new(models.ErpCompanyContact)).SetCond(cond) total, _ := qs.Count() var rows []models.ErpCompanyContact if total > 0 { _, _ = qs.OrderBy("-is_primary", "-update_time"). Offset((page - 1) * pageSize). Limit(pageSize). All(&rows) } names := loadRelatedNames(tenantID, rows) list := make([]crmContactDTO, 0, len(rows)) for _, r := range rows { list = append(list, toCrmContactDTO(r, names)) } c.crmContactOk(map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } // Add POST /backend/crm/contact/add func (c *BackendCrmContactController) Add() { claims, err := c.contactClaims() if err != nil { c.crmContactJsonErr(401, 401, err.Error()) return } var p struct { RelatedType int `json:"related_type"` RelatedID uint64 `json:"related_id"` ContactName string `json:"contact_name"` Gender int8 `json:"gender"` Mobile string `json:"mobile"` Phone string `json:"phone"` Email string `json:"email"` Wechat string `json:"wechat"` QQ string `json:"qq"` Dingtalk string `json:"dingtalk"` Department string `json:"department"` Position string `json:"position"` HomeAddress string `json:"home_address"` IsPrimary int8 `json:"is_primary"` Status int8 `json:"status"` Remark string `json:"remark"` } raw, _ := io.ReadAll(c.Ctx.Request.Body) if err := json.Unmarshal(raw, &p); err != nil { c.crmContactJsonErr(400, 400, "参数错误") return } if strings.TrimSpace(p.ContactName) == "" { c.crmContactJsonErr(400, 400, "联系人姓名不能为空") return } if p.RelatedID == 0 { c.crmContactJsonErr(400, 400, "请选择关联对象") return } companyType := crmContactCompanyType(p.RelatedType) now := time.Now() contact := models.ErpCompanyContact{ TenantID: fmt.Sprintf("%d", claims.TenantId), CompanyType: companyType, CompanyID: p.RelatedID, Name: strings.TrimSpace(p.ContactName), Gender: p.Gender, Phone: strings.TrimSpace(p.Phone), // 座机 Mobiles: buildMobiles(p.Mobile), // 手机号(JSON数组) Email: strings.TrimSpace(p.Email), Wechat: strings.TrimSpace(p.Wechat), QQ: strings.TrimSpace(p.QQ), Dingtalk: strings.TrimSpace(p.Dingtalk), Department: strings.TrimSpace(p.Department), Position: strings.TrimSpace(p.Position), HomeAddress: strings.TrimSpace(p.HomeAddress), IsPrimary: p.IsPrimary, Status: 1, Remark: p.Remark, CreateTime: now, UpdateTime: now, } // 新增联系人默认在职 if p.Status == 0 { contact.Status = 1 } else { contact.Status = p.Status } id, err := models.Orm.Insert(&contact) if err != nil { c.crmContactJsonErr(500, 500, "新增失败: "+err.Error()) return } if p.IsPrimary == 1 { clearOtherPrimary(fmt.Sprintf("%d", claims.TenantId), companyType, p.RelatedID, uint64(id)) } c.crmContactOk(map[string]interface{}{"id": id}) } // Edit POST /backend/crm/contact/edit func (c *BackendCrmContactController) Edit() { claims, err := c.contactClaims() if err != nil { c.crmContactJsonErr(401, 401, err.Error()) return } var p struct { ID uint64 `json:"id"` RelatedType int `json:"related_type"` RelatedID uint64 `json:"related_id"` ContactName string `json:"contact_name"` Gender int8 `json:"gender"` Mobile string `json:"mobile"` Phone string `json:"phone"` Email string `json:"email"` Wechat string `json:"wechat"` QQ string `json:"qq"` Dingtalk string `json:"dingtalk"` Department string `json:"department"` Position string `json:"position"` HomeAddress string `json:"home_address"` IsPrimary int8 `json:"is_primary"` Status int8 `json:"status"` Remark string `json:"remark"` } raw, _ := io.ReadAll(c.Ctx.Request.Body) if err := json.Unmarshal(raw, &p); err != nil { c.crmContactJsonErr(400, 400, "参数错误") return } if p.ID == 0 { c.crmContactJsonErr(400, 400, "缺少ID") return } if strings.TrimSpace(p.ContactName) == "" { c.crmContactJsonErr(400, 400, "联系人姓名不能为空") return } tenantID := fmt.Sprintf("%d", claims.TenantId) var contact models.ErpCompanyContact if err := models.Orm.QueryTable(new(models.ErpCompanyContact)). Filter("id", p.ID). Filter("tenant_id", tenantID). Filter("delete_time__isnull", true). One(&contact); err != nil { c.crmContactJsonErr(404, 404, "联系人未找到") return } // 允许调整关联对象;未传则保持原值 companyType := contact.CompanyType companyID := contact.CompanyID if p.RelatedID > 0 { companyType = crmContactCompanyType(p.RelatedType) companyID = p.RelatedID } contact.CompanyType = companyType contact.CompanyID = companyID contact.Name = strings.TrimSpace(p.ContactName) contact.Gender = p.Gender contact.Phone = strings.TrimSpace(p.Phone) // 座机 contact.Mobiles = buildMobiles(p.Mobile) // 手机号(JSON数组) contact.Email = strings.TrimSpace(p.Email) contact.Wechat = strings.TrimSpace(p.Wechat) contact.QQ = strings.TrimSpace(p.QQ) contact.Dingtalk = strings.TrimSpace(p.Dingtalk) contact.Department = strings.TrimSpace(p.Department) contact.Position = strings.TrimSpace(p.Position) contact.HomeAddress = strings.TrimSpace(p.HomeAddress) contact.IsPrimary = p.IsPrimary contact.Status = p.Status contact.Remark = p.Remark contact.UpdateTime = time.Now() if _, err := models.Orm.Update(&contact); err != nil { c.crmContactJsonErr(500, 500, "更新失败: "+err.Error()) return } if p.IsPrimary == 1 { clearOtherPrimary(tenantID, companyType, companyID, contact.ID) } c.crmContactOk(map[string]interface{}{"id": contact.ID}) } // Delete POST /backend/crm/contact/delete 软删除 func (c *BackendCrmContactController) Delete() { claims, err := c.contactClaims() if err != nil { c.crmContactJsonErr(401, 401, err.Error()) return } var p struct { ID uint64 `json:"id"` } raw, _ := io.ReadAll(c.Ctx.Request.Body) if err := json.Unmarshal(raw, &p); err != nil { c.crmContactJsonErr(400, 400, "参数错误") return } if p.ID == 0 { c.crmContactJsonErr(400, 400, "缺少ID") return } now := time.Now() num, err := models.Orm.QueryTable(new(models.ErpCompanyContact)). Filter("id", p.ID). Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). Filter("delete_time__isnull", true). Update(map[string]interface{}{ "delete_time": now, "update_time": now, }) if err != nil { c.crmContactJsonErr(500, 500, "删除失败: "+err.Error()) return } if num == 0 { c.crmContactJsonErr(404, 404, "联系人未找到") return } c.crmContactOk(nil) }