package services import ( "sort" "strings" "time" "agent-desk/internal/models" "agent-desk/internal/pkg/dto" "agent-desk/internal/pkg/dto/request" "agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/errorsx" "agent-desk/internal/pkg/utils" "agent-desk/internal/repositories" "agent-desk/internal/pkg/httpx/params" "github.com/mlogclub/simple/sqls" "gorm.io/gorm" ) var CustomerContactService = newCustomerContactService() func newCustomerContactService() *customerContactService { return &customerContactService{} } type customerContactService struct { } func (s *customerContactService) Get(id int64) *models.CustomerContact { return repositories.CustomerContactRepository.Get(sqls.DB(), id) } func (s *customerContactService) Take(where ...interface{}) *models.CustomerContact { return repositories.CustomerContactRepository.Take(sqls.DB(), where...) } func (s *customerContactService) Find(cnd *sqls.Cnd) []models.CustomerContact { return repositories.CustomerContactRepository.Find(sqls.DB(), cnd) } func (s *customerContactService) FindOne(cnd *sqls.Cnd) *models.CustomerContact { return repositories.CustomerContactRepository.FindOne(sqls.DB(), cnd) } func (s *customerContactService) FindPageByParams(params *params.QueryParams) (list []models.CustomerContact, paging *sqls.Paging) { return repositories.CustomerContactRepository.FindPageByParams(sqls.DB(), params) } func (s *customerContactService) FindPageByCnd(cnd *sqls.Cnd) (list []models.CustomerContact, paging *sqls.Paging) { return repositories.CustomerContactRepository.FindPageByCnd(sqls.DB(), cnd) } func (s *customerContactService) Count(cnd *sqls.Cnd) int64 { return repositories.CustomerContactRepository.Count(sqls.DB(), cnd) } func (s *customerContactService) Create(t *models.CustomerContact) error { return repositories.CustomerContactRepository.Create(sqls.DB(), t) } func (s *customerContactService) Update(t *models.CustomerContact) error { return repositories.CustomerContactRepository.Update(sqls.DB(), t) } func (s *customerContactService) Updates(id int64, columns map[string]interface{}) error { return repositories.CustomerContactRepository.Updates(sqls.DB(), id, columns) } func (s *customerContactService) UpdateColumn(id int64, name string, value interface{}) error { return repositories.CustomerContactRepository.UpdateColumn(sqls.DB(), id, name, value) } func (s *customerContactService) Delete(id int64) { repositories.CustomerContactRepository.Delete(sqls.DB(), id) } // FindActiveByCustomerID 返回某客户下未删除的联系方式列表。 func (s *customerContactService) FindActiveByCustomerID(customerID int64) []models.CustomerContact { if customerID <= 0 { return nil } cnd := sqls.NewCnd(). Where("customer_id = ?", customerID). Where("status <> ?", enums.StatusDeleted). Asc("id") return repositories.CustomerContactRepository.Find(sqls.DB(), cnd) } func normalizeContactSource(v string) string { v = strings.TrimSpace(v) if v == "" { return "manual" } return v } func (s *customerContactService) hasDuplicateContact( db *gorm.DB, customerID int64, contactType enums.ContactType, contactValue string, excludeID int64, ) bool { cnd := sqls.NewCnd(). Where("customer_id = ?", customerID). Where("contact_type = ?", contactType). Where("contact_value = ?", contactValue). Where("status <> ?", enums.StatusDeleted) if excludeID > 0 { cnd = cnd.Where("id <> ?", excludeID) } return repositories.CustomerContactRepository.FindOne(db, cnd) != nil } // findSoftDeletedContactByNaturalKey 按 uk_customer_contact 业务键查找已软删行;复活时用 UPDATE 代替 INSERT,避免唯一索引冲突。 func (s *customerContactService) findSoftDeletedContactByNaturalKey( db *gorm.DB, customerID int64, contactType enums.ContactType, contactValue string, ) *models.CustomerContact { cnd := sqls.NewCnd(). Where("customer_id = ?", customerID). Where("contact_type = ?", contactType). Where("contact_value = ?", contactValue). Where("status = ?", enums.StatusDeleted) return repositories.CustomerContactRepository.FindOne(db, cnd) } // syncCustomerPrimaryFromContacts 根据当前主联系方式更新客户表冗余字段(列表检索用)。 func (s *customerContactService) syncCustomerPrimaryFromContacts(db *gorm.DB, customerID int64) error { if customerID <= 0 { return nil } if repositories.CustomerRepository.Get(db, customerID) == nil { return nil } cnd := sqls.NewCnd(). Where("customer_id = ?", customerID). Where("is_primary = ?", true). Where("status <> ?", enums.StatusDeleted) primary := repositories.CustomerContactRepository.FindOne(db, cnd) pm, pe := "", "" if primary != nil { val := strings.TrimSpace(primary.ContactValue) switch primary.ContactType { case enums.ContactTypeEmail: pe = val default: pm = val } } return repositories.CustomerRepository.Updates(db, customerID, map[string]any{ "primary_mobile": pm, "primary_email": pe, "updated_at": time.Now(), }) } // ReplaceAllForCustomerInTx 在事务内全量替换客户联系方式(软删未出现在 payload 中的记录),并同步客户主联系方式冗余字段。 func (s *customerContactService) ReplaceAllForCustomerInTx( ctx *sqls.TxContext, customerID int64, raw []request.CustomerProfileContactItem, operator *dto.AuthPrincipal, ) error { if operator == nil { return errorsx.UnauthorizedI18n("error.auth.expired") } type line struct { id *int64 ct enums.ContactType val string remark string primary bool } var items []line for _, r := range raw { ct := strings.TrimSpace(r.ContactType) val := strings.TrimSpace(r.ContactValue) if val == "" { continue } if !enums.IsValidContactType(ct) { return errorsx.InvalidParamI18n("error.e0301") } items = append(items, line{ id: r.ID, ct: enums.ContactType(ct), val: val, remark: strings.TrimSpace(r.Remark), primary: r.IsPrimary, }) } if len(items) > 0 { primaryCount := 0 for i := range items { if items[i].primary { primaryCount++ } } if primaryCount == 0 { items[0].primary = true } else if primaryCount > 1 { return errorsx.InvalidParamI18n("error.e0092") } } existing := repositories.CustomerContactRepository.Find(ctx.Tx, sqls.NewCnd(). Where("customer_id = ?", customerID). Where("status <> ?", enums.StatusDeleted). Asc("id")) wantIDs := map[int64]struct{}{} for i := range items { if items[i].id != nil && *items[i].id > 0 { wantIDs[*items[i].id] = struct{}{} } } now := time.Now() for _, ex := range existing { if _, ok := wantIDs[ex.ID]; !ok { if err := repositories.CustomerContactRepository.Updates(ctx.Tx, ex.ID, map[string]any{ "status": enums.StatusDeleted, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": now, }); err != nil { return err } } } sort.SliceStable(items, func(i, j int) bool { return !items[i].primary && items[j].primary }) for _, it := range items { if it.id != nil && *it.id > 0 { row := repositories.CustomerContactRepository.Get(ctx.Tx, *it.id) if row == nil || row.CustomerID != customerID || row.Status == enums.StatusDeleted { return errorsx.InvalidParamI18n("error.e0299") } if s.hasDuplicateContact(ctx.Tx, customerID, it.ct, it.val, *it.id) { return errorsx.InvalidParamI18n("error.e0318") } if it.primary { if err := s.clearPrimaryExcept(ctx.Tx, customerID, *it.id); err != nil { return err } } if err := repositories.CustomerContactRepository.Updates(ctx.Tx, *it.id, map[string]any{ "contact_type": it.ct, "contact_value": it.val, "is_primary": it.primary, "remark": it.remark, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": now, }); err != nil { return err } continue } if s.hasDuplicateContact(ctx.Tx, customerID, it.ct, it.val, 0) { return errorsx.InvalidParamI18n("error.e0318") } if deleted := s.findSoftDeletedContactByNaturalKey(ctx.Tx, customerID, it.ct, it.val); deleted != nil { if it.primary { if err := s.clearPrimaryExcept(ctx.Tx, customerID, deleted.ID); err != nil { return err } } if err := repositories.CustomerContactRepository.Updates(ctx.Tx, deleted.ID, map[string]any{ "status": enums.StatusOk, "contact_type": it.ct, "contact_value": it.val, "is_primary": it.primary, "is_verified": false, "verified_at": nil, "remark": it.remark, "source": normalizeContactSource("manual"), "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": now, }); err != nil { return err } continue } if it.primary { if err := s.clearPrimaryExcept(ctx.Tx, customerID, 0); err != nil { return err } } item := &models.CustomerContact{ CustomerID: customerID, ContactType: it.ct, ContactValue: it.val, IsPrimary: it.primary, IsVerified: false, Source: normalizeContactSource("manual"), Status: enums.StatusOk, Remark: it.remark, AuditFields: utils.BuildAuditFields(operator), } if err := repositories.CustomerContactRepository.Create(ctx.Tx, item); err != nil { return err } } return s.syncCustomerPrimaryFromContacts(ctx.Tx, customerID) } func (s *customerContactService) clearPrimaryExcept(db *gorm.DB, customerID int64, exceptID int64) error { cnd := sqls.NewCnd(). Where("customer_id = ?", customerID). Where("is_primary = ?", true) if exceptID > 0 { cnd = cnd.Where("id <> ?", exceptID) } list := repositories.CustomerContactRepository.Find(db, cnd) for i := range list { if err := repositories.CustomerContactRepository.UpdateColumn(db, list[i].ID, "is_primary", false); err != nil { return err } } return nil } func (s *customerContactService) validateContactStatus(status int) error { if !enums.IsValidStatus(status) { return errorsx.InvalidParamI18n("error.e0254") } if status == int(enums.StatusDeleted) { return errorsx.InvalidParamI18n("error.e0254") } return nil } // CreateCustomerContact 创建联系方式;主联系方式在同一客户下唯一。 func (s *customerContactService) CreateCustomerContact(req request.CreateCustomerContactRequest, operator *dto.AuthPrincipal) (*models.CustomerContact, error) { if operator == nil { return nil, errorsx.UnauthorizedI18n("error.auth.expired") } if req.CustomerID <= 0 { return nil, errorsx.InvalidParamI18n("error.e0155") } if CustomerService.Get(req.CustomerID) == nil { return nil, errorsx.InvalidParamI18n("error.e0155") } ct := strings.TrimSpace(req.ContactType) if !enums.IsValidContactType(ct) { return nil, errorsx.InvalidParamI18n("error.e0301") } val := strings.TrimSpace(req.ContactValue) if val == "" { return nil, errorsx.InvalidParamI18n("error.e0300") } if err := s.validateContactStatus(req.Status); err != nil { return nil, err } status := enums.Status(req.Status) if status == 0 { status = enums.StatusOk } var created *models.CustomerContact err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { if s.hasDuplicateContact(ctx.Tx, req.CustomerID, enums.ContactType(ct), val, 0) { return errorsx.InvalidParamI18n("error.e0318") } now := time.Now() if deleted := s.findSoftDeletedContactByNaturalKey(ctx.Tx, req.CustomerID, enums.ContactType(ct), val); deleted != nil { if req.IsPrimary { if err := s.clearPrimaryExcept(ctx.Tx, req.CustomerID, deleted.ID); err != nil { return err } } var verifiedAt *time.Time if req.IsVerified { verifiedAt = &now } if err := repositories.CustomerContactRepository.Updates(ctx.Tx, deleted.ID, map[string]any{ "status": status, "contact_type": enums.ContactType(ct), "contact_value": val, "is_primary": req.IsPrimary, "is_verified": req.IsVerified, "verified_at": verifiedAt, "source": normalizeContactSource(req.Source), "remark": strings.TrimSpace(req.Remark), "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": now, }); err != nil { return err } created = repositories.CustomerContactRepository.Get(ctx.Tx, deleted.ID) return s.syncCustomerPrimaryFromContacts(ctx.Tx, req.CustomerID) } if req.IsPrimary { if err := s.clearPrimaryExcept(ctx.Tx, req.CustomerID, 0); err != nil { return err } } var verifiedAt *time.Time if req.IsVerified { verifiedAt = &now } item := &models.CustomerContact{ CustomerID: req.CustomerID, ContactType: enums.ContactType(ct), ContactValue: val, IsPrimary: req.IsPrimary, IsVerified: req.IsVerified, VerifiedAt: verifiedAt, Source: normalizeContactSource(req.Source), Status: status, Remark: strings.TrimSpace(req.Remark), AuditFields: utils.BuildAuditFields(operator), } if err := repositories.CustomerContactRepository.Create(ctx.Tx, item); err != nil { return err } created = item if err := s.syncCustomerPrimaryFromContacts(ctx.Tx, req.CustomerID); err != nil { return err } return nil }) if err != nil { return nil, err } return created, nil } // UpdateCustomerContact 更新联系方式。 func (s *customerContactService) UpdateCustomerContact(req request.UpdateCustomerContactRequest, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.UnauthorizedI18n("error.auth.expired") } if req.ID <= 0 { return errorsx.InvalidParamI18n("error.e0299") } current := s.Get(req.ID) if current == nil { return errorsx.InvalidParamI18n("error.e0299") } ct := strings.TrimSpace(req.ContactType) if !enums.IsValidContactType(ct) { return errorsx.InvalidParamI18n("error.e0301") } val := strings.TrimSpace(req.ContactValue) if val == "" { return errorsx.InvalidParamI18n("error.e0300") } if err := s.validateContactStatus(req.Status); err != nil { return err } return sqls.WithTransaction(func(ctx *sqls.TxContext) error { if s.hasDuplicateContact(ctx.Tx, current.CustomerID, enums.ContactType(ct), val, req.ID) { return errorsx.InvalidParamI18n("error.e0318") } if req.IsPrimary { if err := s.clearPrimaryExcept(ctx.Tx, current.CustomerID, req.ID); err != nil { return err } } now := time.Now() verifiedAt := current.VerifiedAt if req.IsVerified { if verifiedAt == nil { verifiedAt = &now } } else { verifiedAt = nil } if err := repositories.CustomerContactRepository.Updates(ctx.Tx, req.ID, map[string]any{ "contact_type": enums.ContactType(ct), "contact_value": val, "is_primary": req.IsPrimary, "is_verified": req.IsVerified, "verified_at": verifiedAt, "source": normalizeContactSource(req.Source), "status": req.Status, "remark": strings.TrimSpace(req.Remark), "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": now, }); err != nil { return err } return s.syncCustomerPrimaryFromContacts(ctx.Tx, current.CustomerID) }) } // DeleteCustomerContact 软删除联系方式并同步客户主联系方式冗余字段。 func (s *customerContactService) DeleteCustomerContact(id int64, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.UnauthorizedI18n("error.auth.expired") } if id <= 0 { return errorsx.InvalidParamI18n("error.e0299") } current := s.Get(id) if current == nil { return errorsx.InvalidParamI18n("error.e0299") } return sqls.WithTransaction(func(ctx *sqls.TxContext) error { now := time.Now() if err := repositories.CustomerContactRepository.Updates(ctx.Tx, id, map[string]any{ "status": enums.StatusDeleted, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": now, }); err != nil { return err } return s.syncCustomerPrimaryFromContacts(ctx.Tx, current.CustomerID) }) }