Files
ai-agent/internal/services/customer_contact_service.go
T
mlogclub 0b4a1b4594 Refactor error handling in services to use internationalized error messages
- Updated OSSStorage validation errors to use internationalized messages.
- Changed error messages in provider.go for unsupported file storage types.
- Refactored tag_service.go to replace hardcoded error messages with internationalized versions.
- Updated ticket_service.go to use internationalized error messages for various validation checks.
- Refactored ticket_tag_service.go to use internationalized error messages for tag validation.
- Changed ticket_view_service.go to use internationalized error messages for view validation.
- Updated tool_catalog_service.go to use internationalized error messages for tool code validation.
- Refactored user_service.go to replace error messages with internationalized versions.
- Updated ws_service.go to use internationalized error messages for WebSocket handling.
- Refactored wxwork_kf_inbound_service.go to use internationalized error messages for message handling.
- Updated wxwork_kf_outbound_service.go to use internationalized error messages for outbound message handling.
- Refactored wxwork_login_service.go to use internationalized error messages for login handling.
- Updated login.go in wxwork package to use internationalized error messages for login state and ticket validation.
2026-06-02 20:51:13 +08:00

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)
})
}