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

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
+111 -23
View File
@@ -3,6 +3,8 @@ package controllers
import (
"encoding/json"
"io"
"sort"
"strconv"
"strings"
"time"
@@ -39,6 +41,39 @@ func (c *BackendCrmFollowController) List() {
relatedID := strings.TrimSpace(c.GetString("related_id"))
followType := strings.TrimSpace(c.GetString("follow_type"))
keyword := strings.TrimSpace(c.GetString("keyword"))
includeSource := strings.TrimSpace(c.GetString("include_source")) == "1"
// 详情页链路模式(include_source=1):按 线索→商机→项目 累积返回上游 + 自身的数据
if includeSource && relatedType != "" && relatedID != "" && relatedID != "0" {
rid, _ := strconv.ParseUint(relatedID, 10, 64)
rt, _ := strconv.Atoi(relatedType)
refs := relatedChain(tenantID, int8(rt), rid)
var merged []models.TenantCrmFollow
for _, r := range refs {
var rows []models.TenantCrmFollow
sub := orm.NewCondition().
And("tenant_id", tenantID).
And("delete_time__isnull", true).
And("related_type", r.Type).
And("related_id", r.ID)
_, _ = models.Orm.QueryTable(new(models.TenantCrmFollow)).SetCond(sub).All(&rows)
merged = append(merged, rows...)
}
sort.Slice(merged, func(i, j int) bool {
ti, tj := merged[i].FollowTime, merged[j].FollowTime
if ti != nil && tj != nil && !ti.Equal(*tj) {
return ti.After(*tj)
}
if (ti == nil) != (tj == nil) {
return ti != nil
}
return merged[i].ID > merged[j].ID
})
pipelineOk(&c.Controller, map[string]interface{}{
"list": merged, "total": len(merged), "page": 1, "pageSize": len(merged),
})
return
}
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
if relatedType != "" {
@@ -72,6 +107,7 @@ type followPayload struct {
RelatedType int8 `json:"related_type"`
RelatedID uint64 `json:"related_id"`
RelatedName string `json:"related_name"`
Stage int8 `json:"stage"` // 创建阶段:不传时默认取 related_type
FollowType string `json:"follow_type"`
FollowTime string `json:"follow_time"`
Content string `json:"content"`
@@ -106,17 +142,23 @@ func (c *BackendCrmFollowController) Add() {
followTime = &t
}
nextContact := parsePipelineDateTime(p.NextContactTime)
// 创建阶段:默认取当前关联类型(线索/商机/项目)
stage := p.Stage
if stage != 1 && stage != 2 && stage != 3 {
stage = p.RelatedType
}
row := models.TenantCrmFollow{
TenantID: tenantID,
RelatedType: p.RelatedType,
RelatedID: p.RelatedID,
RelatedName: strings.TrimSpace(p.RelatedName),
Stage: stage,
FollowType: firstNonEmpty(p.FollowType, "1"),
FollowTime: followTime,
Content: p.Content,
NextContactTime: nextContact,
OwnerUserID: pipelineUID(claims),
OwnerUserName: claims.Username,
OwnerUserName: resolveUserName(claims),
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
@@ -159,6 +201,11 @@ func (c *BackendCrmFollowController) Edit() {
return
}
row.FollowType = firstNonEmpty(p.FollowType, row.FollowType)
// 回访阶段(所属阶段)允许修正:仅在传入合法阶段(1线索/2商机/3项目)时更新,
// 保证"记录创建时所在阶段"可被纠正为沟通实际发生的阶段。
if p.Stage == 1 || p.Stage == 2 || p.Stage == 3 {
row.Stage = p.Stage
}
if t := parsePipelineDateTime(p.FollowTime); t != nil {
row.FollowTime = t
}
@@ -237,17 +284,35 @@ func (c *BackendCrmAttachController) List() {
}
relatedType := strings.TrimSpace(c.GetString("related_type"))
relatedID := strings.TrimSpace(c.GetString("related_id"))
cond := orm.NewCondition().
And("tenant_id", pipelineTenantID(claims)).
And("delete_time__isnull", true)
if relatedType != "" {
cond = cond.And("related_type", relatedType)
}
if relatedID != "" {
cond = cond.And("related_id", relatedID)
}
includeSource := strings.TrimSpace(c.GetString("include_source")) == "1"
tenantID := pipelineTenantID(claims)
var list []models.TenantCrmAttach
_, _ = models.Orm.QueryTable(new(models.TenantCrmAttach)).SetCond(cond).OrderBy("-id").All(&list)
if includeSource && relatedType != "" && relatedID != "" && relatedID != "0" {
// 链路模式:线索 → 商机 → 项目 累积
rid, _ := strconv.ParseUint(relatedID, 10, 64)
rt, _ := strconv.Atoi(relatedType)
for _, r := range relatedChain(tenantID, int8(rt), rid) {
var rows []models.TenantCrmAttach
sub := orm.NewCondition().
And("tenant_id", tenantID).
And("delete_time__isnull", true).
And("related_type", r.Type).
And("related_id", r.ID)
_, _ = models.Orm.QueryTable(new(models.TenantCrmAttach)).SetCond(sub).All(&rows)
list = append(list, rows...)
}
sort.Slice(list, func(i, j int) bool { return list[i].ID > list[j].ID })
} else {
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
if relatedType != "" {
cond = cond.And("related_type", relatedType)
}
if relatedID != "" {
cond = cond.And("related_id", relatedID)
}
_, _ = models.Orm.QueryTable(new(models.TenantCrmAttach)).SetCond(cond).OrderBy("-id").All(&list)
}
pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)})
}
@@ -290,7 +355,7 @@ func (c *BackendCrmAttachController) Add() {
FileURL: strings.TrimSpace(p.FileURL),
FileSize: p.FileSize,
UploaderID: pipelineUID(claims),
UploaderName: claims.Username,
UploaderName: resolveUserName(claims),
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
@@ -350,18 +415,41 @@ func (c *BackendCrmEntityContactController) List() {
}
relatedType := strings.TrimSpace(c.GetString("related_type"))
relatedID := strings.TrimSpace(c.GetString("related_id"))
cond := orm.NewCondition().
And("tenant_id", pipelineTenantID(claims)).
And("delete_time__isnull", true)
if relatedType != "" {
cond = cond.And("related_type", relatedType)
}
if relatedID != "" {
cond = cond.And("related_id", relatedID)
}
includeSource := strings.TrimSpace(c.GetString("include_source")) == "1"
tenantID := pipelineTenantID(claims)
var list []models.TenantCrmEntityContact
_, _ = models.Orm.QueryTable(new(models.TenantCrmEntityContact)).SetCond(cond).
OrderBy("-is_primary", "-id").All(&list)
if includeSource && relatedType != "" && relatedID != "" && relatedID != "0" {
// 链路模式:线索 → 商机 → 项目 累积
rid, _ := strconv.ParseUint(relatedID, 10, 64)
rt, _ := strconv.Atoi(relatedType)
for _, r := range relatedChain(tenantID, int8(rt), rid) {
var rows []models.TenantCrmEntityContact
sub := orm.NewCondition().
And("tenant_id", tenantID).
And("delete_time__isnull", true).
And("related_type", r.Type).
And("related_id", r.ID)
_, _ = models.Orm.QueryTable(new(models.TenantCrmEntityContact)).SetCond(sub).All(&rows)
list = append(list, rows...)
}
sort.Slice(list, func(i, j int) bool {
if list[i].IsPrimary != list[j].IsPrimary {
return list[i].IsPrimary > list[j].IsPrimary
}
return list[i].ID > list[j].ID
})
} else {
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
if relatedType != "" {
cond = cond.And("related_type", relatedType)
}
if relatedID != "" {
cond = cond.And("related_id", relatedID)
}
_, _ = models.Orm.QueryTable(new(models.TenantCrmEntityContact)).SetCond(cond).
OrderBy("-is_primary", "-id").All(&list)
}
pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)})
}