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