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