增加商机线索管理修复若干bug
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
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)
|
||||
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 {
|
||||
_, _ = 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/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
|
||||
}
|
||||
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, claims.Username)
|
||||
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,
|
||||
}
|
||||
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 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 !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 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),
|
||||
BusinessID: &bizIDVal,
|
||||
CustomerID: biz.CustomerID,
|
||||
CustomerName: biz.CustomerName,
|
||||
OwnerUserID: biz.OwnerUserID,
|
||||
OwnerUserName: biz.OwnerUserName,
|
||||
Industry: biz.Industry,
|
||||
Amount: amount,
|
||||
Status: 1,
|
||||
StartDate: parsePipelineDate(p.StartDate),
|
||||
EndDate: parsePipelineDate(p.EndDate),
|
||||
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})
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// CRM 业务管线(线索/商机/项目/回访)公共辅助方法。
|
||||
// related_type 约定:1=线索 2=商机 3=项目
|
||||
|
||||
// pipelineClaims 解析租户端后台 JWT。
|
||||
func pipelineClaims(c *beego.Controller) (*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" && claims.UserType != "platform" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// pipelineOk 统一成功响应。
|
||||
func pipelineOk(c *beego.Controller, data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// pipelineErr 统一错误响应。
|
||||
func pipelineErr(c *beego.Controller, httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// pipelineTenantID 返回当前租户ID字符串。
|
||||
func pipelineTenantID(claims *jwtutil.Claims) string {
|
||||
return fmt.Sprintf("%d", claims.TenantId)
|
||||
}
|
||||
|
||||
// pipelineUID 返回当前用户ID字符串。
|
||||
func pipelineUID(claims *jwtutil.Claims) string {
|
||||
return fmt.Sprintf("%d", claims.UserID)
|
||||
}
|
||||
|
||||
// parsePipelineDateTime 解析时间:支持 "2006-01-02 15:04:05"、"2006-01-02T15:04:05"、"2006-01-02"。
|
||||
func parsePipelineDateTime(s string) *time.Time {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
layouts := []string{
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02T15:04",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePipelineDate 解析日期(仅取年月日,返回当天零点)。
|
||||
func parsePipelineDate(s string) *time.Time {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
if t, err := time.ParseInLocation("2006-01-02", s, time.Local); err == nil {
|
||||
return &t
|
||||
}
|
||||
return parsePipelineDateTime(s)
|
||||
}
|
||||
|
||||
// crmWriteLog 写入 CRM 操作日志(失败静默忽略,不影响主流程)。
|
||||
func crmWriteLog(tenantID string, relatedType int8, relatedID uint64, action, content string, claims *jwtutil.Claims) {
|
||||
if models.Orm == nil || relatedID == 0 {
|
||||
return
|
||||
}
|
||||
operatorID, operatorName := "", ""
|
||||
if claims != nil {
|
||||
operatorID = pipelineUID(claims)
|
||||
operatorName = claims.Username
|
||||
}
|
||||
log := models.TenantCrmOperateLog{
|
||||
TenantID: tenantID,
|
||||
RelatedType: relatedType,
|
||||
RelatedID: relatedID,
|
||||
Action: action,
|
||||
Content: content,
|
||||
OperatorID: operatorID,
|
||||
OperatorName: operatorName,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
_, _ = models.Orm.Insert(&log)
|
||||
}
|
||||
|
||||
// relatedTypeText 关联类型文案。
|
||||
func relatedTypeText(t int8) string {
|
||||
switch t {
|
||||
case 1:
|
||||
return "线索"
|
||||
case 2:
|
||||
return "商机"
|
||||
case 3:
|
||||
return "项目"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// mobileInMobiles 判断手机号是否已存在于 mobiles(JSON 数组或普通字符串)中。
|
||||
func mobileInMobiles(mobile, mobiles string) bool {
|
||||
mobile = strings.TrimSpace(mobile)
|
||||
if mobile == "" || strings.TrimSpace(mobiles) == "" {
|
||||
return false
|
||||
}
|
||||
if firstMobile(mobiles) == mobile {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(mobiles, mobile)
|
||||
}
|
||||
|
||||
// mergeCompanyContact 合并过程库联系人到正式库联系人:仅补齐正式库空缺字段,不覆盖已有数据。
|
||||
func mergeCompanyContact(dst *models.ErpCompanyContact, src models.TenantCrmEntityContact) {
|
||||
changed := false
|
||||
if dst.Wechat == "" && strings.TrimSpace(src.Wechat) != "" {
|
||||
dst.Wechat = strings.TrimSpace(src.Wechat)
|
||||
changed = true
|
||||
}
|
||||
if dst.QQ == "" && strings.TrimSpace(src.QQ) != "" {
|
||||
dst.QQ = strings.TrimSpace(src.QQ)
|
||||
changed = true
|
||||
}
|
||||
if dst.Email == "" && strings.TrimSpace(src.Email) != "" {
|
||||
dst.Email = strings.TrimSpace(src.Email)
|
||||
changed = true
|
||||
}
|
||||
if dst.Position == "" && strings.TrimSpace(src.Position) != "" {
|
||||
dst.Position = strings.TrimSpace(src.Position)
|
||||
changed = true
|
||||
}
|
||||
if strings.TrimSpace(dst.Mobiles) == "" && strings.TrimSpace(src.Mobile) != "" {
|
||||
dst.Mobiles = buildMobiles(src.Mobile)
|
||||
changed = true
|
||||
}
|
||||
if dst.Remark == "" && strings.TrimSpace(src.Remark) != "" {
|
||||
dst.Remark = strings.TrimSpace(src.Remark)
|
||||
changed = true
|
||||
}
|
||||
if src.IsPrimary == 1 && dst.IsPrimary != 1 {
|
||||
dst.IsPrimary = 1
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
dst.UpdateTime = time.Now()
|
||||
_, _ = models.Orm.Update(dst)
|
||||
}
|
||||
}
|
||||
|
||||
// syncContactsToCompany 把线索/商机过程库(yz_tenant_crm_entity_contact)的联系人
|
||||
// 去重同步到正式公司联系人库(yz_backend_contact_company,company_type=customer)。
|
||||
// 去重键:company_type + company_id + name + mobile;命中则补齐字段,否则新增。
|
||||
// 返回 (新增数, 合并数)。
|
||||
func syncContactsToCompany(tenantID string, customerID uint64, contacts []models.TenantCrmEntityContact) (int, int) {
|
||||
if models.Orm == nil || customerID == 0 || len(contacts) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
inserted, merged := 0, 0
|
||||
seen := map[string]bool{}
|
||||
for _, src := range contacts {
|
||||
name := strings.TrimSpace(src.ContactName)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
mobile := strings.TrimSpace(src.Mobile)
|
||||
// 本次待同步集合内去重(同名同手机只处理一次)
|
||||
key := name + "|" + mobile
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
var existingList []models.ErpCompanyContact
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpCompanyContact)).
|
||||
Filter("tenant_id", tenantID).
|
||||
Filter("company_type", "customer").
|
||||
Filter("company_id", customerID).
|
||||
Filter("name", name).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&existingList)
|
||||
|
||||
matched := false
|
||||
for i := range existingList {
|
||||
// 有手机号时必须手机号匹配;无手机号时按同名视为同一人
|
||||
if mobile == "" || mobileInMobiles(mobile, existingList[i].Mobiles) {
|
||||
mergeCompanyContact(&existingList[i], src)
|
||||
merged++
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
continue
|
||||
}
|
||||
|
||||
row := models.ErpCompanyContact{
|
||||
TenantID: tenantID,
|
||||
CompanyType: "customer",
|
||||
CompanyID: customerID,
|
||||
Name: name,
|
||||
Phone: "",
|
||||
Mobiles: buildMobiles(mobile),
|
||||
Wechat: strings.TrimSpace(src.Wechat),
|
||||
QQ: strings.TrimSpace(src.QQ),
|
||||
Email: strings.TrimSpace(src.Email),
|
||||
Position: strings.TrimSpace(src.Position),
|
||||
IsPrimary: src.IsPrimary,
|
||||
Status: 1,
|
||||
Remark: strings.TrimSpace(src.Remark),
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if _, err := models.Orm.Insert(&row); err == nil {
|
||||
inserted++
|
||||
}
|
||||
}
|
||||
return inserted, merged
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// BackendCrmProjectController CRM 项目管理
|
||||
type BackendCrmProjectController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type projectPayload struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
ProjectNo string `json:"project_no"`
|
||||
CustomerID uint64 `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
Industry string `json:"industry"`
|
||||
Amount float64 `json:"amount"`
|
||||
Status int8 `json:"status"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
ContactPerson string `json:"contact_person"`
|
||||
ContactPosition string `json:"contact_position"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
Address string `json:"address"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List GET /backend/crm/project/list
|
||||
func (c *BackendCrmProjectController) 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"))
|
||||
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("project_name__contains", keyword).
|
||||
Or("project_no__contains", keyword).
|
||||
Or("customer_name__contains", keyword)
|
||||
cond = cond.AndCond(kw)
|
||||
}
|
||||
if status != "" {
|
||||
cond = cond.And("status", status)
|
||||
}
|
||||
if ownerID != "" {
|
||||
cond = cond.And("owner_user_id", ownerID)
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.TenantCrmProject)).SetCond(cond)
|
||||
|
||||
total, _ := qs.Count()
|
||||
var list []models.TenantCrmProject
|
||||
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/project/:id
|
||||
func (c *BackendCrmProjectController) 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 proj models.TenantCrmProject
|
||||
err = models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&proj)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "项目未找到")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, proj)
|
||||
}
|
||||
|
||||
// Create POST /backend/crm/project
|
||||
func (c *BackendCrmProjectController) 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 projectPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.ProjectName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "项目名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var custIDVal *uint64
|
||||
customerName := strings.TrimSpace(p.CustomerName)
|
||||
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
|
||||
}
|
||||
}
|
||||
status := p.Status
|
||||
if status == 0 {
|
||||
status = 1
|
||||
}
|
||||
now := time.Now()
|
||||
proj := models.TenantCrmProject{
|
||||
TenantID: tenantID,
|
||||
ProjectName: strings.TrimSpace(p.ProjectName),
|
||||
ProjectNo: strings.TrimSpace(p.ProjectNo),
|
||||
CustomerID: custIDVal,
|
||||
CustomerName: customerName,
|
||||
OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)),
|
||||
OwnerUserName: firstNonEmpty(p.OwnerUserName, claims.Username),
|
||||
Industry: strings.TrimSpace(p.Industry),
|
||||
Amount: p.Amount,
|
||||
Status: status,
|
||||
StartDate: parsePipelineDate(p.StartDate),
|
||||
EndDate: parsePipelineDate(p.EndDate),
|
||||
ContactPerson: strings.TrimSpace(p.ContactPerson),
|
||||
ContactPosition: strings.TrimSpace(p.ContactPosition),
|
||||
ContactPhone: strings.TrimSpace(p.ContactPhone),
|
||||
Address: strings.TrimSpace(p.Address),
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&proj)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 3, uint64(id), "create", "创建项目:"+proj.ProjectName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Update PUT /backend/crm/project/:id
|
||||
func (c *BackendCrmProjectController) 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 projectPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var proj models.TenantCrmProject
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&proj); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "项目未找到")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.ProjectName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "项目名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
proj.ProjectName = strings.TrimSpace(p.ProjectName)
|
||||
proj.ProjectNo = strings.TrimSpace(p.ProjectNo)
|
||||
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
|
||||
proj.CustomerID = &cid
|
||||
proj.CustomerName = cust.CustomerName
|
||||
}
|
||||
} else if strings.TrimSpace(p.CustomerName) != "" {
|
||||
proj.CustomerName = strings.TrimSpace(p.CustomerName)
|
||||
}
|
||||
if strings.TrimSpace(p.OwnerUserID) != "" {
|
||||
proj.OwnerUserID = strings.TrimSpace(p.OwnerUserID)
|
||||
}
|
||||
if strings.TrimSpace(p.OwnerUserName) != "" {
|
||||
proj.OwnerUserName = strings.TrimSpace(p.OwnerUserName)
|
||||
}
|
||||
proj.Industry = strings.TrimSpace(p.Industry)
|
||||
proj.Amount = p.Amount
|
||||
if p.Status != 0 {
|
||||
proj.Status = p.Status
|
||||
}
|
||||
proj.StartDate = parsePipelineDate(p.StartDate)
|
||||
proj.EndDate = parsePipelineDate(p.EndDate)
|
||||
proj.ContactPerson = strings.TrimSpace(p.ContactPerson)
|
||||
proj.ContactPosition = strings.TrimSpace(p.ContactPosition)
|
||||
proj.ContactPhone = strings.TrimSpace(p.ContactPhone)
|
||||
proj.Address = strings.TrimSpace(p.Address)
|
||||
proj.Remark = p.Remark
|
||||
proj.UpdateTime = time.Now()
|
||||
|
||||
if _, err := models.Orm.Update(&proj); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 3, proj.ID, "update", "更新项目:"+proj.ProjectName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": proj.ID})
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/crm/project/:id
|
||||
func (c *BackendCrmProjectController) 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 proj models.TenantCrmProject
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&proj); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "项目未找到")
|
||||
return
|
||||
}
|
||||
if !canDeleteCrmRecord(claims, proj.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 403, 403, "只有创建人、租户管理员或平台管理员可以删除该项目")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// ============================== 回访 / 跟进记录 ==============================
|
||||
|
||||
// BackendCrmFollowController 回访记录(贯穿线索/商机/项目)
|
||||
type BackendCrmFollowController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /backend/crm/follow/list
|
||||
func (c *BackendCrmFollowController) 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
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
relatedType := strings.TrimSpace(c.GetString("related_type"))
|
||||
relatedID := strings.TrimSpace(c.GetString("related_id"))
|
||||
followType := strings.TrimSpace(c.GetString("follow_type"))
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
|
||||
if relatedType != "" {
|
||||
cond = cond.And("related_type", relatedType)
|
||||
}
|
||||
if relatedID != "" && relatedID != "0" {
|
||||
cond = cond.And("related_id", relatedID)
|
||||
}
|
||||
if followType != "" {
|
||||
cond = cond.And("follow_type", followType)
|
||||
}
|
||||
if keyword != "" {
|
||||
kw := orm.NewCondition().
|
||||
Or("related_name__contains", keyword).
|
||||
Or("content__contains", keyword)
|
||||
cond = cond.AndCond(kw)
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.TenantCrmFollow)).SetCond(cond)
|
||||
total, _ := qs.Count()
|
||||
var list []models.TenantCrmFollow
|
||||
if total > 0 {
|
||||
_, _ = qs.OrderBy("-follow_time", "-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
||||
}
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
type followPayload struct {
|
||||
ID uint64 `json:"id"`
|
||||
RelatedType int8 `json:"related_type"`
|
||||
RelatedID uint64 `json:"related_id"`
|
||||
RelatedName string `json:"related_name"`
|
||||
FollowType string `json:"follow_type"`
|
||||
FollowTime string `json:"follow_time"`
|
||||
Content string `json:"content"`
|
||||
NextContactTime string `json:"next_contact_time"`
|
||||
}
|
||||
|
||||
// Add POST /backend/crm/follow/add
|
||||
func (c *BackendCrmFollowController) Add() {
|
||||
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 followPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.RelatedID == 0 || (p.RelatedType != 1 && p.RelatedType != 2 && p.RelatedType != 3) {
|
||||
pipelineErr(&c.Controller, 400, 400, "请指定关联对象")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.Content) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "回访内容不能为空")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
followTime := parsePipelineDateTime(p.FollowTime)
|
||||
if followTime == nil {
|
||||
t := time.Now()
|
||||
followTime = &t
|
||||
}
|
||||
nextContact := parsePipelineDateTime(p.NextContactTime)
|
||||
row := models.TenantCrmFollow{
|
||||
TenantID: tenantID,
|
||||
RelatedType: p.RelatedType,
|
||||
RelatedID: p.RelatedID,
|
||||
RelatedName: strings.TrimSpace(p.RelatedName),
|
||||
FollowType: firstNonEmpty(p.FollowType, "1"),
|
||||
FollowTime: followTime,
|
||||
Content: p.Content,
|
||||
NextContactTime: nextContact,
|
||||
OwnerUserID: pipelineUID(claims),
|
||||
OwnerUserName: claims.Username,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "新增失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 同步更新关联对象的下次联系时间
|
||||
if nextContact != nil {
|
||||
syncNextContact(tenantID, p.RelatedType, p.RelatedID, nextContact)
|
||||
}
|
||||
crmWriteLog(tenantID, p.RelatedType, p.RelatedID, "follow", "新增回访记录:"+strings.TrimSpace(p.RelatedName), claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Edit POST /backend/crm/follow/edit
|
||||
func (c *BackendCrmFollowController) Edit() {
|
||||
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 followPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.ID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "缺少ID")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmFollow
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmFollow)).
|
||||
Filter("id", p.ID).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "回访记录未找到")
|
||||
return
|
||||
}
|
||||
row.FollowType = firstNonEmpty(p.FollowType, row.FollowType)
|
||||
if t := parsePipelineDateTime(p.FollowTime); t != nil {
|
||||
row.FollowTime = t
|
||||
}
|
||||
row.Content = p.Content
|
||||
row.NextContactTime = parsePipelineDateTime(p.NextContactTime)
|
||||
row.UpdateTime = time.Now()
|
||||
if _, err := models.Orm.Update(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
|
||||
}
|
||||
|
||||
// Delete POST /backend/crm/follow/delete
|
||||
func (c *BackendCrmFollowController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 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 || p.ID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
num, err := models.Orm.QueryTable(new(models.TenantCrmFollow)).
|
||||
Filter("id", p.ID).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
pipelineErr(&c.Controller, 404, 404, "回访记录未找到")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// syncNextContact 回访后同步更新线索/商机的下次联系时间。
|
||||
func syncNextContact(tenantID string, relatedType int8, relatedID uint64, next *time.Time) {
|
||||
if models.Orm == nil || next == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
switch relatedType {
|
||||
case 1:
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmClue)).
|
||||
Filter("id", relatedID).Filter("tenant_id", tenantID).
|
||||
Update(map[string]interface{}{"next_contact_time": next, "update_time": now})
|
||||
case 2:
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBusiness)).
|
||||
Filter("id", relatedID).Filter("tenant_id", tenantID).
|
||||
Update(map[string]interface{}{"next_contact_time": next, "update_time": now})
|
||||
}
|
||||
}
|
||||
|
||||
// ================================= 附件 =================================
|
||||
|
||||
// BackendCrmAttachController 附件(线索/商机/项目)
|
||||
type BackendCrmAttachController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /backend/crm/attach/list
|
||||
func (c *BackendCrmAttachController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
var list []models.TenantCrmAttach
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmAttach)).SetCond(cond).OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)})
|
||||
}
|
||||
|
||||
// Add POST /backend/crm/attach/add
|
||||
func (c *BackendCrmAttachController) Add() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
RelatedType int8 `json:"related_type"`
|
||||
RelatedID uint64 `json:"related_id"`
|
||||
FileID uint64 `json:"file_id"`
|
||||
FileName string `json:"file_name"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.RelatedID == 0 || strings.TrimSpace(p.FileURL) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "附件信息不完整")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var fileIDPtr *uint64
|
||||
if p.FileID > 0 {
|
||||
fid := p.FileID
|
||||
fileIDPtr = &fid
|
||||
}
|
||||
row := models.TenantCrmAttach{
|
||||
TenantID: tenantID,
|
||||
RelatedType: p.RelatedType,
|
||||
RelatedID: p.RelatedID,
|
||||
FileID: fileIDPtr,
|
||||
FileName: strings.TrimSpace(p.FileName),
|
||||
FileURL: strings.TrimSpace(p.FileURL),
|
||||
FileSize: p.FileSize,
|
||||
UploaderID: pipelineUID(claims),
|
||||
UploaderName: claims.Username,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "新增失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, p.RelatedType, p.RelatedID, "attach", "新增附件:"+row.FileName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Delete POST /backend/crm/attach/delete
|
||||
func (c *BackendCrmAttachController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
raw := c.Ctx.Input.RequestBody
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
num, err := models.Orm.QueryTable(new(models.TenantCrmAttach)).
|
||||
Filter("id", p.ID).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
pipelineErr(&c.Controller, 404, 404, "附件未找到")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ========================== 联系人(线索/商机/项目) ==========================
|
||||
|
||||
// BackendCrmEntityContactController 实体联系人
|
||||
type BackendCrmEntityContactController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /backend/crm/entity/contact/list
|
||||
func (c *BackendCrmEntityContactController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
var list []models.TenantCrmEntityContact
|
||||
_, _ = 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)})
|
||||
}
|
||||
|
||||
type entityContactPayload struct {
|
||||
ID uint64 `json:"id"`
|
||||
RelatedType int8 `json:"related_type"`
|
||||
RelatedID uint64 `json:"related_id"`
|
||||
ContactName string `json:"contact_name"`
|
||||
Position string `json:"position"`
|
||||
Mobile string `json:"mobile"`
|
||||
Wechat string `json:"wechat"`
|
||||
QQ string `json:"qq"`
|
||||
Email string `json:"email"`
|
||||
IsPrimary int8 `json:"is_primary"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// Add POST /backend/crm/entity/contact/add
|
||||
func (c *BackendCrmEntityContactController) Add() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw := c.Ctx.Input.RequestBody
|
||||
var p entityContactPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.ContactName) == "" || p.RelatedID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "联系人姓名和关联对象不能为空")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
row := models.TenantCrmEntityContact{
|
||||
TenantID: tenantID,
|
||||
RelatedType: p.RelatedType,
|
||||
RelatedID: p.RelatedID,
|
||||
ContactName: strings.TrimSpace(p.ContactName),
|
||||
Position: strings.TrimSpace(p.Position),
|
||||
Mobile: strings.TrimSpace(p.Mobile),
|
||||
Wechat: strings.TrimSpace(p.Wechat),
|
||||
QQ: strings.TrimSpace(p.QQ),
|
||||
Email: strings.TrimSpace(p.Email),
|
||||
IsPrimary: p.IsPrimary,
|
||||
Remark: strings.TrimSpace(p.Remark),
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "新增失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if p.IsPrimary == 1 {
|
||||
clearOtherEntityPrimary(tenantID, p.RelatedType, p.RelatedID, uint64(id))
|
||||
}
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Edit POST /backend/crm/entity/contact/edit
|
||||
func (c *BackendCrmEntityContactController) Edit() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw := c.Ctx.Input.RequestBody
|
||||
var p entityContactPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmEntityContact
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmEntityContact)).
|
||||
Filter("id", p.ID).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "联系人未找到")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.ContactName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "联系人姓名不能为空")
|
||||
return
|
||||
}
|
||||
row.ContactName = strings.TrimSpace(p.ContactName)
|
||||
row.Position = strings.TrimSpace(p.Position)
|
||||
row.Mobile = strings.TrimSpace(p.Mobile)
|
||||
row.Wechat = strings.TrimSpace(p.Wechat)
|
||||
row.QQ = strings.TrimSpace(p.QQ)
|
||||
row.Email = strings.TrimSpace(p.Email)
|
||||
row.IsPrimary = p.IsPrimary
|
||||
row.Remark = strings.TrimSpace(p.Remark)
|
||||
row.UpdateTime = time.Now()
|
||||
if _, err := models.Orm.Update(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if p.IsPrimary == 1 {
|
||||
clearOtherEntityPrimary(tenantID, row.RelatedType, row.RelatedID, row.ID)
|
||||
}
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
|
||||
}
|
||||
|
||||
// Delete POST /backend/crm/entity/contact/delete
|
||||
func (c *BackendCrmEntityContactController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
raw := c.Ctx.Input.RequestBody
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
num, err := models.Orm.QueryTable(new(models.TenantCrmEntityContact)).
|
||||
Filter("id", p.ID).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
pipelineErr(&c.Controller, 404, 404, "联系人未找到")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
func clearOtherEntityPrimary(tenantID string, relatedType int8, relatedID, excludeID uint64) {
|
||||
if models.Orm == nil {
|
||||
return
|
||||
}
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmEntityContact)).
|
||||
Filter("tenant_id", tenantID).
|
||||
Filter("related_type", relatedType).
|
||||
Filter("related_id", relatedID).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("id", excludeID).
|
||||
Update(map[string]interface{}{"is_primary": 0})
|
||||
}
|
||||
|
||||
// ============================== 操作日志 ==============================
|
||||
|
||||
// BackendCrmOperateLogController 操作日志
|
||||
type BackendCrmOperateLogController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /backend/crm/oplog/list
|
||||
func (c *BackendCrmOperateLogController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
relatedType := strings.TrimSpace(c.GetString("related_type"))
|
||||
relatedID := strings.TrimSpace(c.GetString("related_id"))
|
||||
cond := orm.NewCondition().And("tenant_id", pipelineTenantID(claims))
|
||||
if relatedType != "" {
|
||||
cond = cond.And("related_type", relatedType)
|
||||
}
|
||||
if relatedID != "" {
|
||||
cond = cond.And("related_id", relatedID)
|
||||
}
|
||||
var list []models.TenantCrmOperateLog
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmOperateLog)).SetCond(cond).OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)})
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// ErpCompanyContact 客户/供应商联系人表
|
||||
// ErpCompanyContact 正式客户/供应商联系人通用表: yz_backend_contact_company
|
||||
// 全模块共用的「公司联系人」正式库(客户/供应商对接人)。
|
||||
type ErpCompanyContact struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
@@ -28,5 +29,5 @@ type ErpCompanyContact struct {
|
||||
}
|
||||
|
||||
func (m *ErpCompanyContact) TableName() string {
|
||||
return "yz_backend_erp_company_contact"
|
||||
return "yz_backend_contact_company"
|
||||
}
|
||||
|
||||
@@ -74,6 +74,13 @@ func Init(_ string) {
|
||||
new(BackendUserNotifyConfig),
|
||||
new(TenantCrmCustomer),
|
||||
new(TenantCrmSupplier),
|
||||
new(TenantCrmClue),
|
||||
new(TenantCrmBusiness),
|
||||
new(TenantCrmProject),
|
||||
new(TenantCrmFollow),
|
||||
new(TenantCrmAttach),
|
||||
new(TenantCrmEntityContact),
|
||||
new(TenantCrmOperateLog),
|
||||
new(ErpAccountSet),
|
||||
new(ErpNormalSetting),
|
||||
new(ErpCompanyContact),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmAttach 附件表: yz_tenant_crm_attach
|
||||
// related_type: 1线索 / 2商机 / 3项目
|
||||
type TenantCrmAttach struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
RelatedType int8 `orm:"column(related_type)" json:"related_type"`
|
||||
RelatedID uint64 `orm:"column(related_id)" json:"related_id"`
|
||||
FileID *uint64 `orm:"column(file_id);null" json:"file_id"`
|
||||
FileName string `orm:"column(file_name);size(255)" json:"file_name"`
|
||||
FileURL string `orm:"column(file_url);size(500)" json:"file_url"`
|
||||
FileSize int64 `orm:"column(file_size);default(0)" json:"file_size"`
|
||||
UploaderID string `orm:"column(uploader_id);size(64)" json:"uploader_id"`
|
||||
UploaderName string `orm:"column(uploader_name);size(128)" json:"uploader_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmAttach) TableName() string {
|
||||
return "yz_tenant_crm_attach"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmBusiness 商机表: yz_tenant_crm_business
|
||||
type TenantCrmBusiness struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BusinessName string `orm:"column(business_name);size(100)" json:"business_name"` // 商机名称
|
||||
ClueID *uint64 `orm:"column(clue_id);null" json:"clue_id"` // 来源线索ID
|
||||
CustomerID *uint64 `orm:"column(customer_id);null" json:"customer_id"` // 正式客户ID
|
||||
CustomerName string `orm:"column(customer_name);size(100)" json:"customer_name"`
|
||||
Source string `orm:"column(source);size(50)" json:"source"`
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
Industry string `orm:"column(industry);size(50)" json:"industry"`
|
||||
Stage string `orm:"column(stage);size(20);default(1)" json:"stage"` // 商机阶段
|
||||
Amount float64 `orm:"column(amount);digits(14);decimals(2);default(0)" json:"amount"`
|
||||
ExpectDealDate *time.Time `orm:"column(expect_deal_date);type(date);null" json:"expect_deal_date"`
|
||||
NextContactTime *time.Time `orm:"column(next_contact_time);type(datetime);null" json:"next_contact_time"`
|
||||
Level string `orm:"column(level);size(20);default(2)" json:"level"`
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"`
|
||||
ContactPosition string `orm:"column(contact_position);size(50)" json:"contact_position"` // 对接人职位
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"`
|
||||
ContactWechat string `orm:"column(contact_wechat);size(64)" json:"contact_wechat"`
|
||||
ContactQQ string `orm:"column(contact_qq);size(20)" json:"contact_qq"`
|
||||
Address string `orm:"column(address);size(255)" json:"address"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1跟进中/2已转化项目/3已关闭
|
||||
Locked int8 `orm:"column(locked);default(0)" json:"locked"` // 转化项目后锁定
|
||||
ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"` // 转化后的项目ID
|
||||
ConvertTime *time.Time `orm:"column(convert_time);type(datetime);null" json:"convert_time"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBusiness) TableName() string {
|
||||
return "yz_tenant_crm_business"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmClue 线索表: yz_tenant_crm_clue
|
||||
// 线索阶段的「客户名称」不关联客户管理,转化商机时才落地为正式客户。
|
||||
type TenantCrmClue struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
ClueName string `orm:"column(clue_name);size(100)" json:"clue_name"` // 线索名称
|
||||
CustomerName string `orm:"column(customer_name);size(100)" json:"customer_name"` // 客户名称(暂不关联客户管理)
|
||||
Source string `orm:"column(source);size(50)" json:"source"` // 客户来源
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"` // 负责人
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
Industry string `orm:"column(industry);size(50)" json:"industry"` // 客户行业
|
||||
NextContactTime *time.Time `orm:"column(next_contact_time);type(datetime);null" json:"next_contact_time"` // 下次联系时间
|
||||
ClueLevel string `orm:"column(clue_level);size(20);default(2)" json:"clue_level"` // 客户级别:1重点/2普通/3非优先
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"` // 客户对接人
|
||||
ContactPosition string `orm:"column(contact_position);size(50)" json:"contact_position"` // 对接人职位
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"` // 对接人手机
|
||||
ContactWechat string `orm:"column(contact_wechat);size(64)" json:"contact_wechat"` // 对接人微信
|
||||
ContactQQ string `orm:"column(contact_qq);size(20)" json:"contact_qq"` // 对接人QQ
|
||||
Address string `orm:"column(address);size(255)" json:"address"` // 公司地址
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1跟进中/2已转化/3已关闭
|
||||
Locked int8 `orm:"column(locked);default(0)" json:"locked"` // 0否/1是(转化后锁定不可编辑)
|
||||
BusinessID *uint64 `orm:"column(business_id);null" json:"business_id"` // 转化后的商机ID
|
||||
ConvertTime *time.Time `orm:"column(convert_time);type(datetime);null" json:"convert_time"` // 转化时间
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmClue) TableName() string {
|
||||
return "yz_tenant_crm_clue"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmEntityContact 联系人表: yz_tenant_crm_entity_contact
|
||||
// related_type: 1线索 / 2商机 / 3项目
|
||||
type TenantCrmEntityContact struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
RelatedType int8 `orm:"column(related_type)" json:"related_type"`
|
||||
RelatedID uint64 `orm:"column(related_id)" json:"related_id"`
|
||||
ContactName string `orm:"column(contact_name);size(50)" json:"contact_name"`
|
||||
Position string `orm:"column(position);size(50)" json:"position"`
|
||||
Mobile string `orm:"column(mobile);size(20)" json:"mobile"`
|
||||
Wechat string `orm:"column(wechat);size(64)" json:"wechat"`
|
||||
QQ string `orm:"column(qq);size(20)" json:"qq"`
|
||||
Email string `orm:"column(email);size(100)" json:"email"`
|
||||
IsPrimary int8 `orm:"column(is_primary);default(0)" json:"is_primary"`
|
||||
Remark string `orm:"column(remark);size(500)" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmEntityContact) TableName() string {
|
||||
return "yz_tenant_crm_entity_contact"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmFollow 回访/跟进记录表: yz_tenant_crm_follow
|
||||
// related_type: 1线索 / 2商机 / 3项目
|
||||
type TenantCrmFollow struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
RelatedType int8 `orm:"column(related_type)" json:"related_type"`
|
||||
RelatedID uint64 `orm:"column(related_id)" json:"related_id"`
|
||||
RelatedName string `orm:"column(related_name);size(100)" json:"related_name"`
|
||||
FollowType string `orm:"column(follow_type);size(20);default(1)" json:"follow_type"` // 回访方式
|
||||
FollowTime *time.Time `orm:"column(follow_time);type(datetime);null" json:"follow_time"`
|
||||
Content string `orm:"column(content);type(text);null" json:"content"`
|
||||
NextContactTime *time.Time `orm:"column(next_contact_time);type(datetime);null" json:"next_contact_time"`
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmFollow) TableName() string {
|
||||
return "yz_tenant_crm_follow"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmOperateLog 操作日志表: yz_tenant_crm_operate_log
|
||||
// related_type: 1线索 / 2商机 / 3项目
|
||||
type TenantCrmOperateLog struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
RelatedType int8 `orm:"column(related_type)" json:"related_type"`
|
||||
RelatedID uint64 `orm:"column(related_id)" json:"related_id"`
|
||||
Action string `orm:"column(action);size(50)" json:"action"`
|
||||
Content string `orm:"column(content);size(500)" json:"content"`
|
||||
OperatorID string `orm:"column(operator_id);size(64)" json:"operator_id"`
|
||||
OperatorName string `orm:"column(operator_name);size(128)" json:"operator_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmOperateLog) TableName() string {
|
||||
return "yz_tenant_crm_operate_log"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmProject 项目表: yz_tenant_crm_project
|
||||
type TenantCrmProject struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
ProjectName string `orm:"column(project_name);size(100)" json:"project_name"` // 项目名称
|
||||
ProjectNo string `orm:"column(project_no);size(50)" json:"project_no"` // 项目编号
|
||||
BusinessID *uint64 `orm:"column(business_id);null" json:"business_id"` // 来源商机ID
|
||||
CustomerID *uint64 `orm:"column(customer_id);null" json:"customer_id"`
|
||||
CustomerName string `orm:"column(customer_name);size(100)" json:"customer_name"`
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
Industry string `orm:"column(industry);size(50)" json:"industry"`
|
||||
Amount float64 `orm:"column(amount);digits(14);decimals(2);default(0)" json:"amount"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1未开始/2进行中/3已完成/4已暂停
|
||||
StartDate *time.Time `orm:"column(start_date);type(date);null" json:"start_date"`
|
||||
EndDate *time.Time `orm:"column(end_date);type(date);null" json:"end_date"`
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"`
|
||||
ContactPosition string `orm:"column(contact_position);size(50)" json:"contact_position"` // 对接人职位
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"`
|
||||
Address string `orm:"column(address);size(255)" json:"address"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmProject) TableName() string {
|
||||
return "yz_tenant_crm_project"
|
||||
}
|
||||
@@ -384,6 +384,43 @@ func registerOrganizationRoutes(module string) {
|
||||
beego.Router("/backend/crm/pool/claim", &controllers.BackendCrmPoolController{}, "post:Claim")
|
||||
beego.Router("/backend/crm/pool/assign", &controllers.BackendCrmPoolController{}, "post:Assign")
|
||||
|
||||
// CRM线索管理(线索 -> 商机转化)
|
||||
beego.Router("/backend/crm/clue/list", &controllers.BackendCrmClueController{}, "get:List")
|
||||
beego.Router("/backend/crm/clue", &controllers.BackendCrmClueController{}, "post:Create")
|
||||
beego.Router("/backend/crm/clue/:id", &controllers.BackendCrmClueController{}, "get:Detail;put:Update;delete:Delete")
|
||||
beego.Router("/backend/crm/clue/:id/convert", &controllers.BackendCrmClueController{}, "post:Convert")
|
||||
|
||||
// CRM商机管理(商机 -> 项目转化)
|
||||
beego.Router("/backend/crm/business/list", &controllers.BackendCrmBusinessController{}, "get:List")
|
||||
beego.Router("/backend/crm/business", &controllers.BackendCrmBusinessController{}, "post:Create")
|
||||
beego.Router("/backend/crm/business/:id", &controllers.BackendCrmBusinessController{}, "get:Detail;put:Update;delete:Delete")
|
||||
beego.Router("/backend/crm/business/:id/convert", &controllers.BackendCrmBusinessController{}, "post:Convert")
|
||||
|
||||
// CRM项目管理
|
||||
beego.Router("/backend/crm/project/list", &controllers.BackendCrmProjectController{}, "get:List")
|
||||
beego.Router("/backend/crm/project", &controllers.BackendCrmProjectController{}, "post:Create")
|
||||
beego.Router("/backend/crm/project/:id", &controllers.BackendCrmProjectController{}, "get:Detail;put:Update;delete:Delete")
|
||||
|
||||
// CRM回访记录(贯穿线索/商机/项目)
|
||||
beego.Router("/backend/crm/follow/list", &controllers.BackendCrmFollowController{}, "get:List")
|
||||
beego.Router("/backend/crm/follow/add", &controllers.BackendCrmFollowController{}, "post:Add")
|
||||
beego.Router("/backend/crm/follow/edit", &controllers.BackendCrmFollowController{}, "post:Edit")
|
||||
beego.Router("/backend/crm/follow/delete", &controllers.BackendCrmFollowController{}, "post:Delete")
|
||||
|
||||
// CRM附件(线索/商机/项目资料)
|
||||
beego.Router("/backend/crm/attach/list", &controllers.BackendCrmAttachController{}, "get:List")
|
||||
beego.Router("/backend/crm/attach/add", &controllers.BackendCrmAttachController{}, "post:Add")
|
||||
beego.Router("/backend/crm/attach/delete", &controllers.BackendCrmAttachController{}, "post:Delete")
|
||||
|
||||
// CRM联系人(线索/商机/项目的对接人列表)
|
||||
beego.Router("/backend/crm/entity/contact/list", &controllers.BackendCrmEntityContactController{}, "get:List")
|
||||
beego.Router("/backend/crm/entity/contact/add", &controllers.BackendCrmEntityContactController{}, "post:Add")
|
||||
beego.Router("/backend/crm/entity/contact/edit", &controllers.BackendCrmEntityContactController{}, "post:Edit")
|
||||
beego.Router("/backend/crm/entity/contact/delete", &controllers.BackendCrmEntityContactController{}, "post:Delete")
|
||||
|
||||
// CRM操作日志
|
||||
beego.Router("/backend/crm/oplog/list", &controllers.BackendCrmOperateLogController{}, "get:List")
|
||||
|
||||
// ERP供应商管理
|
||||
beego.Router("/backend/erp/supplier/list", &controllers.BackendErpSupplierController{}, "get:List")
|
||||
beego.Router("/backend/erp/supplier", &controllers.BackendErpSupplierController{}, "post:Create")
|
||||
|
||||
Reference in New Issue
Block a user