476 lines
16 KiB
Go
476 lines
16 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"server/models"
|
|
|
|
"github.com/beego/beego/v2/client/orm"
|
|
beego "github.com/beego/beego/v2/server/web"
|
|
)
|
|
|
|
// BackendCrmBusinessController CRM 商机管理
|
|
type BackendCrmBusinessController struct {
|
|
beego.Controller
|
|
}
|
|
|
|
type businessPayload struct {
|
|
BusinessName string `json:"business_name"`
|
|
CustomerID uint64 `json:"customer_id"`
|
|
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"`
|
|
Stage string `json:"stage"`
|
|
Amount float64 `json:"amount"`
|
|
ExpectDealDate string `json:"expect_deal_date"`
|
|
NextContactTime string `json:"next_contact_time"`
|
|
Level string `json:"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/business/list
|
|
func (c *BackendCrmBusinessController) 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"))
|
|
stage := strings.TrimSpace(c.GetString("stage"))
|
|
level := strings.TrimSpace(c.GetString("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)
|
|
// 数据范围:平台/租户管理员可见全部,其他用户仅本人(负责人或创建人)
|
|
cond = crmApplyOwnerScope(cond, claims)
|
|
if keyword != "" {
|
|
kw := orm.NewCondition().
|
|
Or("business_name__contains", keyword).
|
|
Or("customer_name__contains", keyword).
|
|
Or("contact_person__contains", keyword)
|
|
cond = cond.AndCond(kw)
|
|
}
|
|
if stage != "" {
|
|
cond = cond.And("stage", stage)
|
|
}
|
|
if level != "" {
|
|
cond = cond.And("level", level)
|
|
}
|
|
if status != "" {
|
|
cond = cond.And("status", status)
|
|
}
|
|
if ownerID != "" {
|
|
cond = cond.And("owner_user_id", ownerID)
|
|
}
|
|
qs := models.Orm.QueryTable(new(models.TenantCrmBusiness)).SetCond(cond)
|
|
|
|
total, _ := qs.Count()
|
|
var list []models.TenantCrmBusiness
|
|
if total > 0 {
|
|
// 未转化的排前面,已转化的(locked=1)沉到列表下方
|
|
_, _ = qs.OrderBy("locked", "-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
|
}
|
|
attachClueName(tenantID, list)
|
|
pipelineOk(&c.Controller, map[string]interface{}{
|
|
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
|
})
|
|
}
|
|
|
|
// Detail GET /backend/crm/business/:id
|
|
func (c *BackendCrmBusinessController) 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 biz models.TenantCrmBusiness
|
|
err = models.Orm.QueryTable(new(models.TenantCrmBusiness)).
|
|
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
|
Filter("delete_time__isnull", true).One(&biz)
|
|
if err != nil {
|
|
pipelineErr(&c.Controller, 404, 404, "商机未找到")
|
|
return
|
|
}
|
|
tmp := []models.TenantCrmBusiness{biz}
|
|
attachClueName(pipelineTenantID(claims), tmp)
|
|
biz.ClueName = tmp[0].ClueName
|
|
pipelineOk(&c.Controller, biz)
|
|
}
|
|
|
|
// Create POST /backend/crm/business
|
|
func (c *BackendCrmBusinessController) 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 businessPayload
|
|
if err := json.Unmarshal(raw, &p); err != nil {
|
|
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
|
return
|
|
}
|
|
if strings.TrimSpace(p.BusinessName) == "" {
|
|
pipelineErr(&c.Controller, 400, 400, "商机名称不能为空")
|
|
return
|
|
}
|
|
if strings.TrimSpace(p.CustomerName) == "" && p.CustomerID == 0 {
|
|
pipelineErr(&c.Controller, 400, 400, "请选择或填写客户")
|
|
return
|
|
}
|
|
|
|
tenantID := pipelineTenantID(claims)
|
|
customerName := strings.TrimSpace(p.CustomerName)
|
|
var custIDVal *uint64
|
|
if 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 {
|
|
cid := cust.ID
|
|
custIDVal = &cid
|
|
customerName = cust.CustomerName
|
|
}
|
|
}
|
|
if customerName == "" {
|
|
pipelineErr(&c.Controller, 400, 400, "客户名称不能为空")
|
|
return
|
|
}
|
|
|
|
ownerID := firstNonEmpty(p.OwnerUserID, pipelineUID(claims))
|
|
ownerName := firstNonEmpty(p.OwnerUserName, resolveUserName(claims))
|
|
stage := firstNonEmpty(p.Stage, "1")
|
|
level := firstNonEmpty(p.Level, "2")
|
|
now := time.Now()
|
|
|
|
biz := models.TenantCrmBusiness{
|
|
TenantID: tenantID,
|
|
BusinessName: strings.TrimSpace(p.BusinessName),
|
|
CustomerID: custIDVal,
|
|
CustomerName: customerName,
|
|
Source: strings.TrimSpace(p.Source),
|
|
OwnerUserID: ownerID,
|
|
OwnerUserName: ownerName,
|
|
Industry: strings.TrimSpace(p.Industry),
|
|
Stage: stage,
|
|
Amount: p.Amount,
|
|
ExpectDealDate: parsePipelineDate(p.ExpectDealDate),
|
|
NextContactTime: parsePipelineDateTime(p.NextContactTime),
|
|
Level: 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,
|
|
Remark: p.Remark,
|
|
CreateUserID: pipelineUID(claims),
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
// 唯一性校验:同租户内不允许重复创建同名商机(重复数据请联系管理员转移跟进)
|
|
if crmNameDuplicated(new(models.TenantCrmBusiness), tenantID, "business_name", biz.BusinessName) {
|
|
pipelineErr(&c.Controller, 409, 409, "该商机名称已存在(可能由其他同事创建),如需继续跟进请联系管理员转移")
|
|
return
|
|
}
|
|
id, err := models.Orm.Insert(&biz)
|
|
if err != nil {
|
|
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
|
return
|
|
}
|
|
crmWriteLog(tenantID, 2, uint64(id), "create", "创建商机:"+biz.BusinessName, claims)
|
|
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
|
}
|
|
|
|
// Update PUT /backend/crm/business/:id
|
|
func (c *BackendCrmBusinessController) 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 businessPayload
|
|
if err := json.Unmarshal(raw, &p); err != nil {
|
|
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
|
return
|
|
}
|
|
tenantID := pipelineTenantID(claims)
|
|
var biz models.TenantCrmBusiness
|
|
if err := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
|
|
Filter("id", id).Filter("tenant_id", tenantID).
|
|
Filter("delete_time__isnull", true).One(&biz); err != nil {
|
|
pipelineErr(&c.Controller, 404, 404, "商机未找到")
|
|
return
|
|
}
|
|
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
|
|
if !canAccessCrmRecord(claims, biz.OwnerUserID, biz.CreateUserID) {
|
|
pipelineErr(&c.Controller, 403, 403, "无权操作该商机数据")
|
|
return
|
|
}
|
|
if biz.Locked == 1 {
|
|
pipelineErr(&c.Controller, 400, 400, "商机已转为项目并锁定,不能编辑")
|
|
return
|
|
}
|
|
if strings.TrimSpace(p.BusinessName) == "" {
|
|
pipelineErr(&c.Controller, 400, 400, "商机名称不能为空")
|
|
return
|
|
}
|
|
|
|
biz.BusinessName = strings.TrimSpace(p.BusinessName)
|
|
if 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 {
|
|
cid := cust.ID
|
|
biz.CustomerID = &cid
|
|
biz.CustomerName = cust.CustomerName
|
|
}
|
|
} else if strings.TrimSpace(p.CustomerName) != "" {
|
|
biz.CustomerName = strings.TrimSpace(p.CustomerName)
|
|
}
|
|
biz.Source = strings.TrimSpace(p.Source)
|
|
if strings.TrimSpace(p.OwnerUserID) != "" {
|
|
biz.OwnerUserID = strings.TrimSpace(p.OwnerUserID)
|
|
}
|
|
if strings.TrimSpace(p.OwnerUserName) != "" {
|
|
biz.OwnerUserName = strings.TrimSpace(p.OwnerUserName)
|
|
}
|
|
biz.Industry = strings.TrimSpace(p.Industry)
|
|
if strings.TrimSpace(p.Stage) != "" {
|
|
biz.Stage = strings.TrimSpace(p.Stage)
|
|
}
|
|
biz.Amount = p.Amount
|
|
biz.ExpectDealDate = parsePipelineDate(p.ExpectDealDate)
|
|
biz.NextContactTime = parsePipelineDateTime(p.NextContactTime)
|
|
if strings.TrimSpace(p.Level) != "" {
|
|
biz.Level = strings.TrimSpace(p.Level)
|
|
}
|
|
biz.ContactPerson = strings.TrimSpace(p.ContactPerson)
|
|
biz.ContactPosition = strings.TrimSpace(p.ContactPosition)
|
|
biz.ContactPhone = strings.TrimSpace(p.ContactPhone)
|
|
biz.ContactWechat = strings.TrimSpace(p.ContactWechat)
|
|
biz.ContactQQ = strings.TrimSpace(p.ContactQQ)
|
|
biz.Address = strings.TrimSpace(p.Address)
|
|
biz.Remark = p.Remark
|
|
biz.UpdateTime = time.Now()
|
|
|
|
if _, err := models.Orm.Update(&biz); err != nil {
|
|
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
|
return
|
|
}
|
|
crmWriteLog(tenantID, 2, biz.ID, "update", "更新商机:"+biz.BusinessName, claims)
|
|
pipelineOk(&c.Controller, map[string]interface{}{"id": biz.ID})
|
|
}
|
|
|
|
// Delete DELETE /backend/crm/business/:id
|
|
func (c *BackendCrmBusinessController) 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
|
|
}
|
|
tenantID := pipelineTenantID(claims)
|
|
var biz models.TenantCrmBusiness
|
|
if err := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
|
|
Filter("id", id).Filter("tenant_id", tenantID).
|
|
Filter("delete_time__isnull", true).One(&biz); err != nil {
|
|
pipelineErr(&c.Controller, 404, 404, "商机未找到")
|
|
return
|
|
}
|
|
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
|
|
if !canAccessCrmRecord(claims, biz.OwnerUserID, biz.CreateUserID) {
|
|
pipelineErr(&c.Controller, 403, 403, "无权操作该商机数据")
|
|
return
|
|
}
|
|
if biz.Locked == 1 || biz.Status == 2 {
|
|
pipelineErr(&c.Controller, 400, 400, "商机已转化并锁定,不能删除")
|
|
return
|
|
}
|
|
if !canDeleteCrmRecord(claims, biz.CreateUserID) {
|
|
pipelineErr(&c.Controller, 403, 403, "只有创建人、租户管理员或平台管理员可以删除该商机")
|
|
return
|
|
}
|
|
now := time.Now()
|
|
_, err = models.Orm.QueryTable(new(models.TenantCrmBusiness)).
|
|
Filter("id", id).Filter("tenant_id", tenantID).
|
|
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/business/:id/convert 商机转项目
|
|
func (c *BackendCrmBusinessController) 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 {
|
|
ProjectName string `json:"project_name"`
|
|
ProjectNo string `json:"project_no"`
|
|
Amount float64 `json:"amount"`
|
|
StartDate string `json:"start_date"`
|
|
EndDate string `json:"end_date"`
|
|
Remark string `json:"remark"`
|
|
}
|
|
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 biz models.TenantCrmBusiness
|
|
if err := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
|
|
Filter("id", id).Filter("tenant_id", tenantID).
|
|
Filter("delete_time__isnull", true).One(&biz); err != nil {
|
|
pipelineErr(&c.Controller, 404, 404, "商机未找到")
|
|
return
|
|
}
|
|
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
|
|
if !canAccessCrmRecord(claims, biz.OwnerUserID, biz.CreateUserID) {
|
|
pipelineErr(&c.Controller, 403, 403, "无权操作该商机数据")
|
|
return
|
|
}
|
|
if biz.Locked == 1 || biz.Status == 2 {
|
|
pipelineErr(&c.Controller, 400, 400, "商机已转化,不能重复转化")
|
|
return
|
|
}
|
|
|
|
projectName := strings.TrimSpace(p.ProjectName)
|
|
if projectName == "" {
|
|
projectName = biz.BusinessName
|
|
}
|
|
amount := p.Amount
|
|
if amount == 0 {
|
|
amount = biz.Amount
|
|
}
|
|
now := time.Now()
|
|
bizIDVal := biz.ID
|
|
proj := models.TenantCrmProject{
|
|
TenantID: tenantID,
|
|
ProjectName: projectName,
|
|
ProjectNo: strings.TrimSpace(p.ProjectNo),
|
|
// 商机转化默认按「单一项目」创建:金额与对接人先落到上游侧,
|
|
// 后续可在项目中选择「上下游项目」并补充下游供应商 / 金额 / 对接人
|
|
ProjectType: 1,
|
|
BusinessID: &bizIDVal,
|
|
CustomerID: biz.CustomerID,
|
|
CustomerName: biz.CustomerName,
|
|
OwnerUserID: biz.OwnerUserID,
|
|
OwnerUserName: biz.OwnerUserName,
|
|
Industry: biz.Industry,
|
|
UpstreamAmount: amount,
|
|
Amount: amount, // 兼容旧字段
|
|
Status: 1,
|
|
StartDate: parsePipelineDate(p.StartDate),
|
|
EndDate: parsePipelineDate(p.EndDate),
|
|
UpstreamContact: biz.ContactPerson,
|
|
UpstreamPhone: biz.ContactPhone,
|
|
ContactPerson: biz.ContactPerson,
|
|
ContactPosition: biz.ContactPosition,
|
|
ContactPhone: biz.ContactPhone,
|
|
Address: biz.Address,
|
|
Remark: p.Remark,
|
|
CreateUserID: pipelineUID(claims),
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
projID, err := models.Orm.Insert(&proj)
|
|
if err != nil {
|
|
pipelineErr(&c.Controller, 500, 500, "创建项目失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// 锁定商机
|
|
biz.Status = 2
|
|
biz.Locked = 1
|
|
pid := uint64(projID)
|
|
biz.ProjectID = &pid
|
|
biz.ConvertTime = &now
|
|
biz.UpdateTime = now
|
|
_, _ = models.Orm.Update(&biz, "status", "locked", "project_id", "convert_time", "update_time")
|
|
|
|
crmWriteLog(tenantID, 2, biz.ID, "convert", "商机转化为项目:"+projectName, claims)
|
|
crmWriteLog(tenantID, 3, uint64(projID), "create", "由商机「"+biz.BusinessName+"」转化生成", claims)
|
|
|
|
// 生成项目时,把「商机 + 来源线索」过程库中的联系人去重同步到正式公司联系人库
|
|
if biz.CustomerID != nil && *biz.CustomerID > 0 {
|
|
var contacts []models.TenantCrmEntityContact
|
|
var bizContacts []models.TenantCrmEntityContact
|
|
_, _ = models.Orm.QueryTable(new(models.TenantCrmEntityContact)).
|
|
Filter("tenant_id", tenantID).
|
|
Filter("related_type", int8(2)).
|
|
Filter("related_id", biz.ID).
|
|
Filter("delete_time__isnull", true).
|
|
All(&bizContacts)
|
|
contacts = append(contacts, bizContacts...)
|
|
if biz.ClueID != nil && *biz.ClueID > 0 {
|
|
var clueContacts []models.TenantCrmEntityContact
|
|
_, _ = models.Orm.QueryTable(new(models.TenantCrmEntityContact)).
|
|
Filter("tenant_id", tenantID).
|
|
Filter("related_type", int8(1)).
|
|
Filter("related_id", *biz.ClueID).
|
|
Filter("delete_time__isnull", true).
|
|
All(&clueContacts)
|
|
contacts = append(contacts, clueContacts...)
|
|
}
|
|
insertedN, mergedN := syncContactsToCompany(tenantID, *biz.CustomerID, contacts)
|
|
if insertedN+mergedN > 0 {
|
|
crmWriteLog(tenantID, 3, uint64(projID), "sync-contacts",
|
|
fmt.Sprintf("同步联系人到正式库:新增 %d,合并 %d", insertedN, mergedN), claims)
|
|
}
|
|
}
|
|
|
|
pipelineOk(&c.Controller, map[string]interface{}{"project_id": projID})
|
|
}
|