批量更新功能

This commit is contained in:
2026-09-14 16:15:44 +08:00
parent 6f106e43af
commit a26c98b92b
79 changed files with 4529 additions and 724 deletions
+363
View File
@@ -0,0 +1,363 @@
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
}