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 }