批量更新功能

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
+216 -6
View File
@@ -69,6 +69,99 @@ func buildMobiles(mobile string) string {
return string(b)
}
// buildMobilesFromList 将多个手机号去空、去重后转为 mobiles 的 JSON 数组字符串(最多 5 个,与 ERP 通讯录一致)
func buildMobilesFromList(mobiles []string) string {
cleaned := make([]string, 0, len(mobiles))
seen := map[string]bool{}
for _, m := range mobiles {
m = strings.TrimSpace(m)
if m == "" || seen[m] {
continue
}
seen[m] = true
cleaned = append(cleaned, m)
if len(cleaned) >= 5 {
break
}
}
if len(cleaned) == 0 {
return ""
}
b, err := json.Marshal(cleaned)
if err != nil {
return ""
}
return string(b)
}
// normalizeMobiles 归一请求中的手机号:
// 优先取 mobiles(数组,CRM 联系人支持登记多个手机号,如一人多号),
// 缺省时回退单个 mobile(兼容线索转化 / 合同签约人自动建档等旧调用方)。
func normalizeMobiles(mobiles []string, mobile string) string {
if len(mobiles) == 0 {
return buildMobiles(mobile)
}
return buildMobilesFromList(mobiles)
}
// splitMobiles 把手机号文本(JSON 数组 / 逗号等分隔符拼接)拆成号码列表(去空)
func splitMobiles(mobiles string) []string {
mobiles = strings.TrimSpace(mobiles)
if mobiles == "" {
return nil
}
var arr []string
if err := json.Unmarshal([]byte(mobiles), &arr); err == nil {
out := make([]string, 0, len(arr))
for _, m := range arr {
if m = strings.TrimSpace(m); m != "" {
out = append(out, m)
}
}
return out
}
parts := strings.FieldsFunc(mobiles, func(r rune) bool {
switch r {
case ',', ',', '、', ';', ';', ' ', '/', '|':
return true
}
return false
})
out := make([]string, 0, len(parts))
for _, p := range parts {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
// mobilesFromPayload 解析请求体中的手机号字段,兼容多种入参形式:
// - JSON 数组:["138…","139…"](新前端)
// - JSON 字符串:'["138…"]' 或 '138…,139…'(旧调用方)
// - 缺省:回退单个 mobile / phone 字段
//
// 统一返回 mobiles 的 JSON 数组字符串(去空去重,最多 5 个)。
func mobilesFromPayload(raw json.RawMessage, fallback string) string {
if len(raw) > 0 {
text := strings.TrimSpace(string(raw))
if text != "" && text != "null" {
var arr []string
if err := json.Unmarshal(raw, &arr); err == nil {
return buildMobilesFromList(arr)
}
var s string
if err := json.Unmarshal(raw, &s); err == nil {
if strings.TrimSpace(s) == "" {
return ""
}
return buildMobilesFromList(splitMobiles(s))
}
}
}
return buildMobiles(fallback)
}
// firstMobile 从 mobiles(JSON 数组或普通字符串)中取出第一个手机号
func firstMobile(mobiles string) string {
mobiles = strings.TrimSpace(mobiles)
@@ -221,6 +314,118 @@ func clearOtherPrimary(tenantID, companyType string, companyID, excludeID uint64
_, _ = qs.Update(map[string]interface{}{"is_primary": 0})
}
// isMobileNumber 粗略识别手机号(11 位、1 开头,忽略空格 / 横线等分隔符);
// 非手机号按座机写入 phone 字段,保证联系人资料里手机号 / 座机分列正确。
func isMobileNumber(phone string) bool {
digits := strings.Map(func(r rune) rune {
if r >= '0' && r <= '9' {
return r
}
return -1
}, phone)
return len(digits) == 11 && digits[0] == '1'
}
// crmContactCompanyExists 关联企业(客户 / 供应商)在当前租户下是否存在且未删除
func crmContactCompanyExists(tenantID, companyType string, companyID uint64) bool {
if models.Orm == nil || companyID == 0 {
return false
}
if companyType == "supplier" {
cnt, err := models.Orm.QueryTable(new(models.TenantCrmSupplier)).
Filter("id", companyID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).Count()
return err == nil && cnt > 0
}
cnt, err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
Filter("id", companyID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).Count()
return err == nil && cnt > 0
}
// contactPhoneMatched 联系人是否已登记该电话(手机号数组或座机任一命中)
func contactPhoneMatched(row models.ErpCompanyContact, phone string) bool {
if strings.TrimSpace(row.Phone) == phone {
return true
}
var mobiles []string
if err := json.Unmarshal([]byte(strings.TrimSpace(row.Mobiles)), &mobiles); err == nil {
for _, m := range mobiles {
if strings.TrimSpace(m) == phone {
return true
}
}
}
return false
}
// crmEnsureCompanyContact 确保企业通讯录(联系人)中已有该对接人:
// - 同名或同电话已存在时不再重复建档,仅在原联系人缺电话时补全;
// - 不存在时按「姓名 + 电话」建档,其余资料留空,后续可在联系人管理中补全;
// - 该企业还没有任何联系人时,默认把首个联系人设为主联系人。
func crmEnsureCompanyContact(tenantID, companyType string, companyID uint64, name, phone string) {
if models.Orm == nil || companyID == 0 {
return
}
name = strings.TrimSpace(name)
phone = strings.TrimSpace(phone)
if name == "" {
return
}
var rows []models.ErpCompanyContact
if _, err := models.Orm.QueryTable(new(models.ErpCompanyContact)).
Filter("tenant_id", tenantID).
Filter("company_type", companyType).
Filter("company_id", companyID).
Filter("delete_time__isnull", true).
All(&rows); err != nil && err != orm.ErrNoRows {
return
}
for i := range rows {
row := rows[i]
sameName := strings.EqualFold(strings.TrimSpace(row.Name), name)
samePhone := phone != "" && contactPhoneMatched(row, phone)
if !sameName && !samePhone {
continue
}
// 已存在:仅在原记录没有电话时补全,不覆盖其他已填资料
if phone != "" && strings.TrimSpace(row.Mobiles) == "" && strings.TrimSpace(row.Phone) == "" {
up := map[string]interface{}{"update_time": time.Now()}
if isMobileNumber(phone) {
up["mobiles"] = buildMobiles(phone)
} else {
up["phone"] = phone
}
_, _ = models.Orm.QueryTable(new(models.ErpCompanyContact)).
Filter("id", row.ID).Update(up)
}
return
}
now := time.Now()
contact := models.ErpCompanyContact{
TenantID: tenantID,
CompanyType: companyType,
CompanyID: companyID,
Name: name,
Status: 1,
Remark: "由合同签约信息自动写入,可在联系人中补全其他资料",
CreateTime: now,
UpdateTime: now,
}
if len(rows) == 0 {
contact.IsPrimary = 1
}
if phone != "" {
if isMobileNumber(phone) {
contact.Mobiles = buildMobiles(phone)
} else {
contact.Phone = phone
}
}
_, _ = models.Orm.Insert(&contact)
}
// List GET /backend/crm/contact/list?related_type=1&related_id=7&keyword=&is_primary=
func (c *BackendCrmContactController) List() {
claims, err := c.contactClaims()
@@ -259,6 +464,7 @@ func (c *BackendCrmContactController) List() {
if keyword != "" {
kw := orm.NewCondition().
Or("name__contains", keyword).
Or("mobiles__contains", keyword).
Or("phone__contains", keyword).
Or("email__contains", keyword)
cond = cond.AndCond(kw)
@@ -301,7 +507,9 @@ func (c *BackendCrmContactController) Add() {
ContactName string `json:"contact_name"`
Gender int8 `json:"gender"`
Mobile string `json:"mobile"`
Phone string `json:"phone"`
// 手机号可登记多个(有的人有多个号码):优先使用,缺省时回退单个 mobile
Mobiles []string `json:"mobiles"`
Phone string `json:"phone"`
Email string `json:"email"`
Wechat string `json:"wechat"`
QQ string `json:"qq"`
@@ -335,8 +543,8 @@ func (c *BackendCrmContactController) Add() {
CompanyID: p.RelatedID,
Name: strings.TrimSpace(p.ContactName),
Gender: p.Gender,
Phone: strings.TrimSpace(p.Phone), // 座机
Mobiles: buildMobiles(p.Mobile), // 手机号(JSON数组)
Phone: strings.TrimSpace(p.Phone), // 座机
Mobiles: normalizeMobiles(p.Mobiles, p.Mobile), // 手机号(支持多个,JSON数组)
Email: strings.TrimSpace(p.Email),
Wechat: strings.TrimSpace(p.Wechat),
QQ: strings.TrimSpace(p.QQ),
@@ -381,7 +589,9 @@ func (c *BackendCrmContactController) Edit() {
ContactName string `json:"contact_name"`
Gender int8 `json:"gender"`
Mobile string `json:"mobile"`
Phone string `json:"phone"`
// 手机号可登记多个(有的人有多个号码):优先使用,缺省时回退单个 mobile
Mobiles []string `json:"mobiles"`
Phone string `json:"phone"`
Email string `json:"email"`
Wechat string `json:"wechat"`
QQ string `json:"qq"`
@@ -430,8 +640,8 @@ func (c *BackendCrmContactController) Edit() {
contact.CompanyID = companyID
contact.Name = strings.TrimSpace(p.ContactName)
contact.Gender = p.Gender
contact.Phone = strings.TrimSpace(p.Phone) // 座机
contact.Mobiles = buildMobiles(p.Mobile) // 手机号(JSON数组)
contact.Phone = strings.TrimSpace(p.Phone) // 座机
contact.Mobiles = normalizeMobiles(p.Mobiles, p.Mobile) // 手机号(支持多个,JSON数组)
contact.Email = strings.TrimSpace(p.Email)
contact.Wechat = strings.TrimSpace(p.Wechat)
contact.QQ = strings.TrimSpace(p.QQ)