增加商机线索管理修复若干bug

This commit is contained in:
2026-09-11 11:11:43 +08:00
parent b9cdabe045
commit 9c07af2b59
39 changed files with 6219 additions and 42 deletions
@@ -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
}