541 lines
17 KiB
Go
541 lines
17 KiB
Go
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)})
|
|
}
|