package controllers import ( "fmt" "strings" "time" "server/models" "server/pkg/jwtutil" "github.com/beego/beego/v2/client/orm" ) /* 客户 / 供应商互转(复制一套档案): 同一家单位可能既是客户又是供应商(先做客户后成为供应商,或反之),因此这里做「复制」而不是搬迁: - 原记录保留(客户仍是客户,供应商仍是供应商),两边各自独立维护、互不影响; - 企业资料整体复制:工商信息、注册 / 实缴资本、成立日期、行政区划、企业类型、纳税人资质、 经营范围、地址、合作期限(注册 / 到期时间)、开票资料、银行信息、备注; - 联系人(yz_backend_contact_company)一并复制,按「姓名 + 手机号」去重,避免重复建档; - 等级(核心 / 重要 / 普通 / 潜在)两边语义一致,直接沿用; - 类型(客户类型:企业 / 政府 / 国企 / 教育 / 个人 与 供应商类型:原材料 / 设备 / 服务 / 其他) 语义不同,按目标库默认值建档(客户→供应商 为「其他」、供应商→客户 为「企业」),转化后可再调整; - 目标库已存在同名企业时不再重复建档,只把联系人补同步过去,避免同库出现两条同名档案。 */ // findSameNameSupplier 查找租户内同名且未删除的供应商,未找到返回 0 func findSameNameSupplier(tenantID, name string) uint64 { name = strings.TrimSpace(name) if models.Orm == nil || name == "" { return 0 } var row models.TenantCrmSupplier if err := models.Orm.QueryTable(new(models.TenantCrmSupplier)). Filter("tenant_id", tenantID). Filter("supplier_name", name). Filter("delete_time__isnull", true). One(&row); err != nil { return 0 } return row.ID } // findSameNameCustomer 查找租户内同名且未删除的客户,未找到返回 0 func findSameNameCustomer(tenantID, name string) uint64 { name = strings.TrimSpace(name) if models.Orm == nil || name == "" { return 0 } var row models.TenantCrmCustomer if err := models.Orm.QueryTable(new(models.TenantCrmCustomer)). Filter("tenant_id", tenantID). Filter("customer_name", name). Filter("delete_time__isnull", true). One(&row); err != nil { return 0 } return row.ID } // collectCustomerNames / collectSupplierNames 提取名称集合(去重、去空),用于批量判断双重身份 func collectCustomerNames(list []models.TenantCrmCustomer) []string { names := make([]string, 0, len(list)) for _, r := range list { if n := strings.TrimSpace(r.CustomerName); n != "" { names = append(names, n) } } return names } func collectSupplierNames(list []models.TenantCrmSupplier) []string { names := make([]string, 0, len(list)) for _, r := range list { if n := strings.TrimSpace(r.SupplierName); n != "" { names = append(names, n) } } return names } // partyNameExists 批量判断名称是否已存在于指定库(kind:customer / supplier), // 供列表标记「同时也是客户 / 供应商」,避免用户重复转化。返回 map 的 key 已做 trim。 func partyNameExists(tenantID, kind string, names []string) map[string]bool { exists := map[string]bool{} if models.Orm == nil || len(names) == 0 { return exists } if kind == "supplier" { var rows []models.TenantCrmSupplier if _, err := models.Orm.QueryTable(new(models.TenantCrmSupplier)). Filter("tenant_id", tenantID). Filter("supplier_name__in", names). Filter("delete_time__isnull", true). All(&rows); err == nil { for _, r := range rows { exists[strings.TrimSpace(r.SupplierName)] = true } } return exists } var rows []models.TenantCrmCustomer if _, err := models.Orm.QueryTable(new(models.TenantCrmCustomer)). Filter("tenant_id", tenantID). Filter("customer_name__in", names). Filter("delete_time__isnull", true). All(&rows); err == nil { for _, r := range rows { exists[strings.TrimSpace(r.CustomerName)] = true } } return exists } // customerListRow 客户列表行:附带「同时也是供应商」标记,便于识别同一家单位的双重身份 type customerListRow struct { models.TenantCrmCustomer AlsoSupplier bool `json:"also_supplier"` } // supplierListRow 供应商列表行:附带「同时也是客户」标记 type supplierListRow struct { models.TenantCrmSupplier AlsoCustomer bool `json:"also_customer"` } // partyConvertRemark 转化后的备注:保留原备注并追加转化来源,便于溯源 func partyConvertRemark(origin, fromLabel, name string) string { tag := fmt.Sprintf("由%s「%s」转化而来", fromLabel, name) origin = strings.TrimSpace(origin) if origin == "" { return tag } return origin + "\n" + tag } // copyCompanyContacts 把来源企业(客户 / 供应商)的联系人复制到目标企业: // - 去重键「姓名 + 手机号」:命中时只补齐目标缺失字段,不覆盖已有资料; // - 未命中时新档;目标企业已有联系人时不再设为主联系人,避免出现多个主联系人。 // // 返回 (新增数, 合并数)。 func copyCompanyContacts(tenantID, fromType string, fromID uint64, toType string, toID uint64) (int, int) { if models.Orm == nil || fromID == 0 || toID == 0 { return 0, 0 } var src []models.ErpCompanyContact if _, err := models.Orm.QueryTable(new(models.ErpCompanyContact)). Filter("tenant_id", tenantID). Filter("company_type", fromType). Filter("company_id", fromID). Filter("delete_time__isnull", true). All(&src); err != nil && err != orm.ErrNoRows { return 0, 0 } if len(src) == 0 { return 0, 0 } var dst []models.ErpCompanyContact _, _ = models.Orm.QueryTable(new(models.ErpCompanyContact)). Filter("tenant_id", tenantID). Filter("company_type", toType). Filter("company_id", toID). Filter("delete_time__isnull", true). All(&dst) now := time.Now() inserted, merged := 0, 0 for _, s := range src { srcName := strings.TrimSpace(s.Name) srcMobile := firstMobile(s.Mobiles) hit := -1 for i := range dst { if !strings.EqualFold(strings.TrimSpace(dst[i].Name), srcName) { continue } // 有手机号时必须手机号也匹配,避免同名不同人被判为同一人 if srcMobile != "" && !contactPhoneMatched(dst[i], srcMobile) { continue } hit = i break } if hit >= 0 { d := dst[hit] up := map[string]interface{}{} if strings.TrimSpace(d.Mobiles) == "" && strings.TrimSpace(s.Mobiles) != "" { up["mobiles"] = s.Mobiles } if strings.TrimSpace(d.Phone) == "" && strings.TrimSpace(s.Phone) != "" { up["phone"] = s.Phone } if strings.TrimSpace(d.Email) == "" && strings.TrimSpace(s.Email) != "" { up["email"] = s.Email } if strings.TrimSpace(d.Wechat) == "" && strings.TrimSpace(s.Wechat) != "" { up["wechat"] = s.Wechat } if strings.TrimSpace(d.QQ) == "" && strings.TrimSpace(s.QQ) != "" { up["qq"] = s.QQ } if strings.TrimSpace(d.Dingtalk) == "" && strings.TrimSpace(s.Dingtalk) != "" { up["dingtalk"] = s.Dingtalk } if strings.TrimSpace(d.Department) == "" && strings.TrimSpace(s.Department) != "" { up["department"] = s.Department } if strings.TrimSpace(d.Position) == "" && strings.TrimSpace(s.Position) != "" { up["position"] = s.Position } if strings.TrimSpace(d.HomeAddress) == "" && strings.TrimSpace(s.HomeAddress) != "" { up["home_address"] = s.HomeAddress } if len(up) == 0 { continue } up["update_time"] = now if _, err := models.Orm.QueryTable(new(models.ErpCompanyContact)). Filter("id", d.ID).Update(up); err == nil { merged++ } continue } row := s row.ID = 0 row.CompanyType = toType row.CompanyID = toID row.CreateTime = now row.UpdateTime = now if len(dst) > 0 { row.IsPrimary = 0 } if _, err := models.Orm.Insert(&row); err == nil { inserted++ } } return inserted, merged } // convertCustomerToSupplier 客户 → 供应商:复制客户档案与联系人到供应商库。 // 返回 (供应商库是否已存在同名企业, 供应商ID, 新增联系人, 合并联系人)。 func convertCustomerToSupplier(tenantID string, src *models.TenantCrmCustomer, claims *jwtutil.Claims) (bool, uint64, int, int, error) { if models.Orm == nil { return false, 0, 0, 0, fmt.Errorf("数据库未初始化") } if src == nil || src.ID == 0 { return false, 0, 0, 0, fmt.Errorf("客户未找到") } name := strings.TrimSpace(src.CustomerName) if name == "" { return false, 0, 0, 0, fmt.Errorf("客户名称为空,无法转化") } // 供应商库已有同名企业:不重复建档,只把联系人补同步过去 if id := findSameNameSupplier(tenantID, name); id > 0 { inserted, merged := copyCompanyContacts(tenantID, "customer", src.ID, "supplier", id) return true, id, inserted, merged, nil } now := time.Now() supplier := models.TenantCrmSupplier{ TenantID: tenantID, SupplierName: name, SupplierType: "4", // 客户类型与供应商类型语义不同,默认「其他」,建档后可调整 ContactPerson: src.ContactPerson, ContactPhone: src.ContactPhone, ContactEmail: src.ContactEmail, SupplierLevel: firstNonEmpty(src.CustomerLevel, "3"), // 等级语义一致,直接沿用 Industry: src.Industry, RegisteredCapital: src.RegisteredCapital, PaidCapital: src.PaidCapital, EstablishDate: src.EstablishDate, AdministrativeDivision: src.AdministrativeDivision, EnterpriseType: src.EnterpriseType, TaxpayerQualification: src.TaxpayerQualification, BusinessScope: src.BusinessScope, Address: src.Address, RegisterTime: src.RegisterTime, ExpireTime: src.ExpireTime, Status: firstNonEmpty(src.Status, "1"), IsDraft: src.IsDraft, Remark: partyConvertRemark(src.Remark, "客户", name), InvoiceTitle: src.InvoiceTitle, TaxNumber: src.TaxNumber, BankName: src.BankName, BankAccount: src.BankAccount, RegisteredAddress: src.RegisteredAddress, RegisteredPhone: src.RegisteredPhone, CreateUserID: fmt.Sprintf("%d", claims.UserID), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&supplier) if err != nil { return false, 0, 0, 0, fmt.Errorf("生成供应商失败: " + err.Error()) } inserted, merged := copyCompanyContacts(tenantID, "customer", src.ID, "supplier", uint64(id)) return false, uint64(id), inserted, merged, nil } // convertSupplierToCustomer 供应商 → 客户:复制供应商档案与联系人到客户库。 // 返回 (客户库是否已存在同名企业, 客户ID, 新增联系人, 合并联系人)。 func convertSupplierToCustomer(tenantID string, src *models.TenantCrmSupplier, claims *jwtutil.Claims) (bool, uint64, int, int, error) { if models.Orm == nil { return false, 0, 0, 0, fmt.Errorf("数据库未初始化") } if src == nil || src.ID == 0 { return false, 0, 0, 0, fmt.Errorf("供应商未找到") } name := strings.TrimSpace(src.SupplierName) if name == "" { return false, 0, 0, 0, fmt.Errorf("供应商名称为空,无法转化") } // 客户库已有同名企业:不重复建档,只把联系人补同步过去 if id := findSameNameCustomer(tenantID, name); id > 0 { inserted, merged := copyCompanyContacts(tenantID, "supplier", src.ID, "customer", id) return true, id, inserted, merged, nil } now := time.Now() userID := fmt.Sprintf("%d", claims.UserID) customer := models.TenantCrmCustomer{ TenantID: tenantID, CustomerName: name, CustomerType: "1", // 供应商类型与客户类型语义不同,默认「企业」,建档后可调整 ContactPerson: src.ContactPerson, ContactPhone: src.ContactPhone, ContactEmail: src.ContactEmail, CustomerLevel: firstNonEmpty(src.SupplierLevel, "3"), // 等级语义一致,直接沿用 Industry: src.Industry, RegisteredCapital: src.RegisteredCapital, PaidCapital: src.PaidCapital, EstablishDate: src.EstablishDate, AdministrativeDivision: src.AdministrativeDivision, EnterpriseType: src.EnterpriseType, TaxpayerQualification: src.TaxpayerQualification, BusinessScope: src.BusinessScope, Address: src.Address, RegisterTime: src.RegisterTime, ExpireTime: src.ExpireTime, Status: firstNonEmpty(src.Status, "1"), IsDraft: src.IsDraft, Remark: partyConvertRemark(src.Remark, "供应商", name), InvoiceTitle: src.InvoiceTitle, TaxNumber: src.TaxNumber, BankName: src.BankName, BankAccount: src.BankAccount, RegisteredAddress: src.RegisteredAddress, RegisteredPhone: src.RegisteredPhone, InPool: 0, OwnerUserID: userID, OwnerUserName: resolveUserName(claims), CreateUserID: userID, CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&customer) if err != nil { return false, 0, 0, 0, fmt.Errorf("生成客户失败: " + err.Error()) } inserted, merged := copyCompanyContacts(tenantID, "supplier", src.ID, "customer", uint64(id)) return false, uint64(id), inserted, merged, nil }