增加商机线索管理修复若干bug
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendCrmClueController CRM 线索管理
|
||||
type BackendCrmClueController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// cluePayload 线索新增/编辑请求体
|
||||
type cluePayload struct {
|
||||
ClueName string `json:"clue_name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
Source string `json:"source"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
Industry string `json:"industry"`
|
||||
NextContactTime string `json:"next_contact_time"`
|
||||
ClueLevel string `json:"clue_level"`
|
||||
ContactPerson string `json:"contact_person"`
|
||||
ContactPosition string `json:"contact_position"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactWechat string `json:"contact_wechat"`
|
||||
ContactQQ string `json:"contact_qq"`
|
||||
Address string `json:"address"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List GET /backend/crm/clue/list
|
||||
func (c *BackendCrmClueController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
source := strings.TrimSpace(c.GetString("source"))
|
||||
level := strings.TrimSpace(c.GetString("clue_level"))
|
||||
status := strings.TrimSpace(c.GetString("status"))
|
||||
ownerID := strings.TrimSpace(c.GetString("owner_user_id"))
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
kw := orm.NewCondition().
|
||||
Or("clue_name__contains", keyword).
|
||||
Or("customer_name__contains", keyword).
|
||||
Or("contact_person__contains", keyword).
|
||||
Or("contact_phone__contains", keyword)
|
||||
cond = cond.AndCond(kw)
|
||||
}
|
||||
if source != "" {
|
||||
cond = cond.And("source", source)
|
||||
}
|
||||
if level != "" {
|
||||
cond = cond.And("clue_level", level)
|
||||
}
|
||||
if status != "" {
|
||||
cond = cond.And("status", status)
|
||||
}
|
||||
if ownerID != "" {
|
||||
cond = cond.And("owner_user_id", ownerID)
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.TenantCrmClue)).SetCond(cond)
|
||||
|
||||
total, _ := qs.Count()
|
||||
var list []models.TenantCrmClue
|
||||
if total > 0 {
|
||||
_, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
||||
}
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// Detail GET /backend/crm/clue/:id
|
||||
func (c *BackendCrmClueController) Detail() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var clue models.TenantCrmClue
|
||||
err = models.Orm.QueryTable(new(models.TenantCrmClue)).
|
||||
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&clue)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "线索未找到")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, clue)
|
||||
}
|
||||
|
||||
// Create POST /backend/crm/clue
|
||||
func (c *BackendCrmClueController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p cluePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.ClueName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "线索名称不能为空")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.CustomerName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "客户名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
ownerID := strings.TrimSpace(p.OwnerUserID)
|
||||
ownerName := strings.TrimSpace(p.OwnerUserName)
|
||||
if ownerID == "" {
|
||||
ownerID = pipelineUID(claims)
|
||||
}
|
||||
if ownerName == "" {
|
||||
ownerName = claims.Username
|
||||
}
|
||||
level := strings.TrimSpace(p.ClueLevel)
|
||||
if level == "" {
|
||||
level = "2"
|
||||
}
|
||||
|
||||
clue := models.TenantCrmClue{
|
||||
TenantID: pipelineTenantID(claims),
|
||||
ClueName: strings.TrimSpace(p.ClueName),
|
||||
CustomerName: strings.TrimSpace(p.CustomerName),
|
||||
Source: strings.TrimSpace(p.Source),
|
||||
OwnerUserID: ownerID,
|
||||
OwnerUserName: ownerName,
|
||||
Industry: strings.TrimSpace(p.Industry),
|
||||
NextContactTime: parsePipelineDateTime(p.NextContactTime),
|
||||
ClueLevel: level,
|
||||
ContactPerson: strings.TrimSpace(p.ContactPerson),
|
||||
ContactPosition: strings.TrimSpace(p.ContactPosition),
|
||||
ContactPhone: strings.TrimSpace(p.ContactPhone),
|
||||
ContactWechat: strings.TrimSpace(p.ContactWechat),
|
||||
ContactQQ: strings.TrimSpace(p.ContactQQ),
|
||||
Address: strings.TrimSpace(p.Address),
|
||||
Status: 1,
|
||||
Locked: 0,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&clue)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(clue.TenantID, 1, uint64(id), "create", "创建线索:"+clue.ClueName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Update PUT /backend/crm/clue/:id
|
||||
func (c *BackendCrmClueController) Update() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p cluePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var clue models.TenantCrmClue
|
||||
err = models.Orm.QueryTable(new(models.TenantCrmClue)).
|
||||
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&clue)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "线索未找到")
|
||||
return
|
||||
}
|
||||
if clue.Locked == 1 {
|
||||
pipelineErr(&c.Controller, 400, 400, "线索已转化并锁定,不能编辑")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.ClueName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "线索名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
clue.ClueName = strings.TrimSpace(p.ClueName)
|
||||
clue.CustomerName = strings.TrimSpace(p.CustomerName)
|
||||
clue.Source = strings.TrimSpace(p.Source)
|
||||
if strings.TrimSpace(p.OwnerUserID) != "" {
|
||||
clue.OwnerUserID = strings.TrimSpace(p.OwnerUserID)
|
||||
}
|
||||
if strings.TrimSpace(p.OwnerUserName) != "" {
|
||||
clue.OwnerUserName = strings.TrimSpace(p.OwnerUserName)
|
||||
}
|
||||
clue.Industry = strings.TrimSpace(p.Industry)
|
||||
clue.NextContactTime = parsePipelineDateTime(p.NextContactTime)
|
||||
if strings.TrimSpace(p.ClueLevel) != "" {
|
||||
clue.ClueLevel = strings.TrimSpace(p.ClueLevel)
|
||||
}
|
||||
clue.ContactPerson = strings.TrimSpace(p.ContactPerson)
|
||||
clue.ContactPosition = strings.TrimSpace(p.ContactPosition)
|
||||
clue.ContactPhone = strings.TrimSpace(p.ContactPhone)
|
||||
clue.ContactWechat = strings.TrimSpace(p.ContactWechat)
|
||||
clue.ContactQQ = strings.TrimSpace(p.ContactQQ)
|
||||
clue.Address = strings.TrimSpace(p.Address)
|
||||
clue.Remark = p.Remark
|
||||
clue.UpdateTime = time.Now()
|
||||
|
||||
if _, err := models.Orm.Update(&clue); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(clue.TenantID, 1, clue.ID, "update", "更新线索:"+clue.ClueName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": clue.ID})
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/crm/clue/:id
|
||||
func (c *BackendCrmClueController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var clue models.TenantCrmClue
|
||||
err = models.Orm.QueryTable(new(models.TenantCrmClue)).
|
||||
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&clue)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "线索未找到")
|
||||
return
|
||||
}
|
||||
if !canDeleteCrmRecord(claims, clue.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 403, 403, "只有创建人、租户管理员或平台管理员可以删除该线索")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmClue)).
|
||||
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// Convert POST /backend/crm/clue/:id/convert
|
||||
// 线索转商机:把线索的客户名称落地为正式客户(或关联已有客户),再创建商机并锁定线索。
|
||||
func (c *BackendCrmClueController) Convert() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
var p struct {
|
||||
BusinessName string `json:"business_name"`
|
||||
Stage string `json:"stage"`
|
||||
Amount float64 `json:"amount"`
|
||||
ExpectDealDate string `json:"expect_deal_date"`
|
||||
NextContactTime string `json:"next_contact_time"`
|
||||
Level string `json:"level"`
|
||||
Remark string `json:"remark"`
|
||||
CustomerMode string `json:"customer_mode"` // create=新建客户(默认) / existing=关联已有客户
|
||||
CustomerID uint64 `json:"customer_id"`
|
||||
Customer struct {
|
||||
CustomerName string `json:"customer_name"`
|
||||
CustomerType string `json:"customer_type"`
|
||||
Industry string `json:"industry"`
|
||||
ContactPerson string `json:"contact_person"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
Address string `json:"address"`
|
||||
Remark string `json:"remark"`
|
||||
} `json:"customer"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var clue models.TenantCrmClue
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmClue)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&clue); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "线索未找到")
|
||||
return
|
||||
}
|
||||
if clue.Locked == 1 || clue.Status == 2 {
|
||||
pipelineErr(&c.Controller, 400, 400, "线索已转化,不能重复转化")
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 处理客户:新建正式客户 或 关联已有客户
|
||||
customerName := strings.TrimSpace(p.Customer.CustomerName)
|
||||
if customerName == "" {
|
||||
customerName = clue.CustomerName
|
||||
}
|
||||
var customerID uint64
|
||||
if strings.TrimSpace(p.CustomerMode) == "existing" && p.CustomerID > 0 {
|
||||
var cust models.TenantCrmCustomer
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||
Filter("id", p.CustomerID).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&cust); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "关联的客户未找到")
|
||||
return
|
||||
}
|
||||
customerID = cust.ID
|
||||
customerName = cust.CustomerName
|
||||
} else {
|
||||
if customerName == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "客户名称不能为空")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
custType := strings.TrimSpace(p.Customer.CustomerType)
|
||||
if custType == "" {
|
||||
custType = "1"
|
||||
}
|
||||
cust := models.TenantCrmCustomer{
|
||||
TenantID: tenantID,
|
||||
CustomerName: customerName,
|
||||
CustomerType: custType,
|
||||
ContactPerson: firstNonEmpty(p.Customer.ContactPerson, clue.ContactPerson),
|
||||
ContactPhone: firstNonEmpty(p.Customer.ContactPhone, clue.ContactPhone),
|
||||
ContactEmail: strings.TrimSpace(p.Customer.ContactEmail),
|
||||
CustomerLevel: "3",
|
||||
Industry: firstNonEmpty(p.Customer.Industry, clue.Industry),
|
||||
Address: firstNonEmpty(p.Customer.Address, clue.Address),
|
||||
Status: "1",
|
||||
IsDraft: 0,
|
||||
Remark: strings.TrimSpace(p.Customer.Remark),
|
||||
OwnerUserID: clue.OwnerUserID,
|
||||
OwnerUserName: clue.OwnerUserName,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
newID, err := models.Orm.Insert(&cust)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "生成正式客户失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
customerID = uint64(newID)
|
||||
crmWriteLog(tenantID, 1, clue.ID, "convert-customer", "线索转化生成正式客户:"+customerName, claims)
|
||||
}
|
||||
|
||||
// 2. 创建商机
|
||||
businessName := strings.TrimSpace(p.BusinessName)
|
||||
if businessName == "" {
|
||||
businessName = clue.ClueName
|
||||
}
|
||||
level := strings.TrimSpace(p.Level)
|
||||
if level == "" {
|
||||
level = clue.ClueLevel
|
||||
}
|
||||
stage := strings.TrimSpace(p.Stage)
|
||||
if stage == "" {
|
||||
stage = "1"
|
||||
}
|
||||
nextContact := parsePipelineDateTime(p.NextContactTime)
|
||||
if nextContact == nil {
|
||||
nextContact = clue.NextContactTime
|
||||
}
|
||||
now := time.Now()
|
||||
clueIDVal := clue.ID
|
||||
custIDVal := customerID
|
||||
biz := models.TenantCrmBusiness{
|
||||
TenantID: tenantID,
|
||||
BusinessName: businessName,
|
||||
ClueID: &clueIDVal,
|
||||
CustomerID: &custIDVal,
|
||||
CustomerName: customerName,
|
||||
Source: clue.Source,
|
||||
OwnerUserID: clue.OwnerUserID,
|
||||
OwnerUserName: clue.OwnerUserName,
|
||||
Industry: clue.Industry,
|
||||
Stage: stage,
|
||||
Amount: p.Amount,
|
||||
ExpectDealDate: parsePipelineDate(p.ExpectDealDate),
|
||||
NextContactTime: nextContact,
|
||||
Level: level,
|
||||
ContactPerson: clue.ContactPerson,
|
||||
ContactPosition: clue.ContactPosition,
|
||||
ContactPhone: clue.ContactPhone,
|
||||
ContactWechat: clue.ContactWechat,
|
||||
ContactQQ: clue.ContactQQ,
|
||||
Address: clue.Address,
|
||||
Status: 1,
|
||||
Locked: 0,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
bizID, err := models.Orm.Insert(&biz)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建商机失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 锁定线索
|
||||
clue.Status = 2
|
||||
clue.Locked = 1
|
||||
bid := uint64(bizID)
|
||||
clue.BusinessID = &bid
|
||||
clue.ConvertTime = &now
|
||||
clue.UpdateTime = now
|
||||
_, _ = models.Orm.Update(&clue, "status", "locked", "business_id", "convert_time", "update_time")
|
||||
|
||||
crmWriteLog(tenantID, 1, clue.ID, "convert", "线索转化为商机:"+businessName, claims)
|
||||
crmWriteLog(tenantID, 2, uint64(bizID), "create", "由线索「"+clue.ClueName+"」转化生成", claims)
|
||||
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"business_id": bizID, "customer_id": customerID,
|
||||
})
|
||||
}
|
||||
|
||||
// firstNonEmpty 返回第一个非空字符串。
|
||||
func firstNonEmpty(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user