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