Files
yunzerwebsiteallinone/go/controllers/backend_crm_party_convert.go
T
2026-09-14 16:15:44 +08:00

364 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}