Files
yunzerwebsiteallinone/go/controllers/backend_crm_support.go
T

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)})
}