更新各项内容

This commit is contained in:
2026-09-11 00:22:49 +08:00
parent 1304e71b7a
commit b9cdabe045
24 changed files with 3109 additions and 469 deletions
+495
View File
@@ -0,0 +1,495 @@
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)
}