优化线索、商机、项目相关开发

This commit is contained in:
2026-09-11 17:00:23 +08:00
parent 9c07af2b59
commit 840673bda5
55 changed files with 4866 additions and 625 deletions
+133 -1
View File
@@ -7,10 +7,28 @@ import (
"server/models"
"server/pkg/jwtutil"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
// resolveUserName 按租户 + 用户ID 解析用户显示名(优先真实姓名,退化到账号,最后退化到登录名)。
// 注意:JWT 里的 Username 是登录账号,直接存会导致前端"负责人"显示成账号。
func resolveUserName(claims *jwtutil.Claims) string {
if claims == nil {
return ""
}
tid := uint64(claims.TenantId)
account, name := services.OperationLogUser(&tid, uint64(claims.UserID))
if strings.TrimSpace(name) != "" {
return strings.TrimSpace(name)
}
if strings.TrimSpace(account) != "" {
return strings.TrimSpace(account)
}
return claims.Username
}
// CRM 业务管线(线索/商机/项目/回访)公共辅助方法。
// related_type 约定:1=线索 2=商机 3=项目
@@ -98,7 +116,7 @@ func crmWriteLog(tenantID string, relatedType int8, relatedID uint64, action, co
operatorID, operatorName := "", ""
if claims != nil {
operatorID = pipelineUID(claims)
operatorName = claims.Username
operatorName = resolveUserName(claims)
}
log := models.TenantCrmOperateLog{
TenantID: tenantID,
@@ -245,3 +263,117 @@ func syncContactsToCompany(tenantID string, customerID uint64, contacts []models
}
return inserted, merged
}
// relatedRef 关联对象引用(related_type + related_id)
type relatedRef struct {
Type int8
ID uint64
}
// relatedChain 返回详情页需要展示的关联链路(上游 + 自身),顺序:线索 → 商机 → 项目。
// 线索 → [线索]
// 商机 → [线索, 商机]
// 项目 → [线索, 商机, 项目]
// 过程数据(联系人 / 跟进记录 / 附件)只存在创建时的对象上、不做搬迁,
// 由上层按链路累积查询,从而在商机里能看到线索阶段的数据,在项目里能看到全部阶段的数据。
func relatedChain(tenantID string, relatedType int8, relatedID uint64) []relatedRef {
if models.Orm == nil || relatedID == 0 {
return []relatedRef{{Type: relatedType, ID: relatedID}}
}
refs := make([]relatedRef, 0, 3)
switch relatedType {
case 2: // 商机:来源线索 + 自身
var biz models.TenantCrmBusiness
if err := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
Filter("id", relatedID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&biz); err == nil {
if biz.ClueID != nil && *biz.ClueID > 0 {
refs = append(refs, relatedRef{Type: 1, ID: *biz.ClueID})
}
}
refs = append(refs, relatedRef{Type: 2, ID: relatedID})
case 3: // 项目:来源商机 + 商机的来源线索 + 自身
var proj models.TenantCrmProject
if err := models.Orm.QueryTable(new(models.TenantCrmProject)).
Filter("id", relatedID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&proj); err == nil {
if proj.BusinessID != nil && *proj.BusinessID > 0 {
var biz models.TenantCrmBusiness
if err2 := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
Filter("id", *proj.BusinessID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&biz); err2 == nil {
if biz.ClueID != nil && *biz.ClueID > 0 {
refs = append(refs, relatedRef{Type: 1, ID: *biz.ClueID})
}
refs = append(refs, relatedRef{Type: 2, ID: biz.ID})
}
}
}
refs = append(refs, relatedRef{Type: 3, ID: relatedID})
default: // 线索:仅自身
refs = append(refs, relatedRef{Type: relatedType, ID: relatedID})
}
return refs
}
// attachBusinessName 批量填充项目的「来源商机名称」(非表字段,仅用于展示)。
func attachBusinessName(tenantID string, list []models.TenantCrmProject) {
ids := make([]uint64, 0, len(list))
seen := map[uint64]bool{}
for _, p := range list {
if p.BusinessID != nil && *p.BusinessID > 0 && !seen[*p.BusinessID] {
seen[*p.BusinessID] = true
ids = append(ids, *p.BusinessID)
}
}
if len(ids) == 0 {
return
}
var rows []models.TenantCrmBusiness
_, _ = models.Orm.QueryTable(new(models.TenantCrmBusiness)).
Filter("tenant_id", tenantID).
Filter("id__in", ids).
Filter("delete_time__isnull", true).
All(&rows)
nameMap := make(map[uint64]string, len(rows))
for _, r := range rows {
nameMap[r.ID] = r.BusinessName
}
for i := range list {
if list[i].BusinessID != nil {
list[i].BusinessName = nameMap[*list[i].BusinessID]
}
}
}
// attachClueName 批量填充商机的「来源线索名称」(非表字段,仅用于展示)。
func attachClueName(tenantID string, list []models.TenantCrmBusiness) {
ids := make([]uint64, 0, len(list))
seen := map[uint64]bool{}
for _, b := range list {
if b.ClueID != nil && *b.ClueID > 0 && !seen[*b.ClueID] {
seen[*b.ClueID] = true
ids = append(ids, *b.ClueID)
}
}
if len(ids) == 0 {
return
}
var rows []models.TenantCrmClue
_, _ = models.Orm.QueryTable(new(models.TenantCrmClue)).
Filter("tenant_id", tenantID).
Filter("id__in", ids).
Filter("delete_time__isnull", true).
All(&rows)
nameMap := make(map[uint64]string, len(rows))
for _, r := range rows {
nameMap[r.ID] = r.ClueName
}
for i := range list {
if list[i].ClueID != nil {
list[i].ClueName = nameMap[*list[i].ClueID]
}
}
}