Files
yunzerwebsiteallinone/go/controllers/backend_crm_contact.go
T
2026-09-11 00:22:49 +08:00

496 lines
14 KiB
Go
Raw 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 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)
}