Files
agent-desk-ng/internal/services/customer_contact_service.go

525 lines
16 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 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)
})
}