364 lines
13 KiB
Go
364 lines
13 KiB
Go
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
|
||
}
|