Files
agent-desk-ng/internal/services/agent_profile_service.go
mono 2f7a491666 feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
2026-10-16 15:21:42 +00:00

229 lines
7.3 KiB
Go

// Modified in AgentDeskNext: authenticated support availability and calendars.
package services
import (
"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"
"strconv"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var AgentProfileService = newAgentProfileService()
func newAgentProfileService() *agentProfileService {
return &agentProfileService{}
}
type agentProfileService struct {
}
func (s *agentProfileService) Get(id int64) *models.AgentProfile {
return repositories.AgentProfileRepository.Get(sqls.DB(), id)
}
func (s *agentProfileService) Take(where ...interface{}) *models.AgentProfile {
return repositories.AgentProfileRepository.Take(sqls.DB(), where...)
}
func (s *agentProfileService) Find(cnd *sqls.Cnd) []models.AgentProfile {
return repositories.AgentProfileRepository.Find(sqls.DB(), cnd)
}
func (s *agentProfileService) FindOne(cnd *sqls.Cnd) *models.AgentProfile {
return repositories.AgentProfileRepository.FindOne(sqls.DB(), cnd)
}
func (s *agentProfileService) FindPageByParams(params *params.QueryParams) (list []models.AgentProfile, paging *sqls.Paging) {
return repositories.AgentProfileRepository.FindPageByParams(sqls.DB(), params)
}
func (s *agentProfileService) FindPageByCnd(cnd *sqls.Cnd) (list []models.AgentProfile, paging *sqls.Paging) {
return repositories.AgentProfileRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *agentProfileService) Count(cnd *sqls.Cnd) int64 {
return repositories.AgentProfileRepository.Count(sqls.DB(), cnd)
}
func (s *agentProfileService) GetByUserID(userID int64) *models.AgentProfile {
if userID <= 0 {
return nil
}
return repositories.AgentProfileRepository.FindOne(sqls.DB(), sqls.NewCnd().Eq("user_id", userID))
}
func (s *agentProfileService) GetUserIDsByTeamID(teamID int64) []int64 {
if teamID <= 0 {
return nil
}
list := s.Find(sqls.NewCnd().Eq("team_id", teamID))
if len(list) == 0 {
return nil
}
result := make([]int64, 0, len(list))
for _, item := range list {
if item.UserID > 0 {
result = append(result, item.UserID)
}
}
return result
}
// GetDispatchAgents 获取可用于分配会话的客服
func (s *agentProfileService) GetDispatchAgents(teamIds []int64) []models.AgentProfile {
profiles, err := repositories.SupportAvailabilityRepository.Profiles(sqls.DB(), teamIds)
if err != nil {
return nil
}
result := make([]models.AgentProfile, 0, len(profiles))
for _, profile := range profiles {
eligibility, err := SupportAvailabilityService.Evaluate(sqls.DB(), profile, true, time.Now(), 0)
if err == nil && eligibility.Eligible {
result = append(result, profile)
}
}
return result
}
func (s *agentProfileService) CreateAgentProfile(req request.CreateAgentProfileRequest, operator *dto.AuthPrincipal) (*models.AgentProfile, error) {
if operator == nil {
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
}
item, err := s.buildProfileModel(0, req)
if err != nil {
return nil, err
}
item.AuditFields = utils.BuildAuditFields(operator)
if err := repositories.AgentProfileRepository.Create(sqls.DB(), item); err != nil {
return nil, err
}
s.dispatchPendingConversationsIfEligible(item)
return item, nil
}
func (s *agentProfileService) UpdateAgentProfile(req request.UpdateAgentProfileRequest, operator *dto.AuthPrincipal) error {
if operator == nil {
return errorsx.UnauthorizedI18n("error.auth.expired")
}
current := s.Get(req.ID)
if current == nil {
return errorsx.InvalidParamI18n("error.e0164")
}
item, err := s.buildProfileModel(req.ID, req.CreateAgentProfileRequest)
if err != nil {
return err
}
if err := repositories.AgentProfileRepository.Updates(sqls.DB(), req.ID, map[string]any{
"user_id": item.UserID,
"team_id": item.TeamID,
"agent_code": item.AgentCode,
"display_name": item.DisplayName,
"avatar": item.Avatar,
// Availability is managed only through the dedicated guarded endpoint.
"max_concurrent_count": item.MaxConcurrentCount,
"priority_level": item.PriorityLevel,
"auto_assign_enabled": item.AutoAssignEnabled,
"receive_offline_message": item.ReceiveOfflineMessage,
"remark": item.Remark,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
}); err != nil {
return err
}
s.dispatchPendingConversationsIfEligible(item)
return nil
}
func (s *agentProfileService) DeleteAgentProfile(id int64) error {
current := s.Get(id)
if current == nil {
return errorsx.InvalidParamI18n("error.e0164")
}
repositories.AgentProfileRepository.Delete(sqls.DB(), id)
return nil
}
func (s *agentProfileService) buildProfileModel(id int64, req request.CreateAgentProfileRequest) (*models.AgentProfile, error) {
if req.UserID <= 0 {
return nil, errorsx.InvalidParamI18n("error.e0325")
}
if UserService.Get(req.UserID) == nil {
return nil, errorsx.InvalidParamI18n("error.e0127")
}
if req.TeamID <= 0 {
return nil, errorsx.InvalidParamI18n("error.e0328")
}
if AgentTeamService.Get(req.TeamID) == nil {
return nil, errorsx.InvalidParamI18n("error.e0205")
}
req.AgentCode = strings.TrimSpace(req.AgentCode)
req.DisplayName = strings.TrimSpace(req.DisplayName)
if req.AgentCode == "" || req.DisplayName == "" {
return nil, errorsx.InvalidParamI18n("error.e0162")
}
if exists := s.Take("user_id = ? AND id <> ?", req.UserID, id); exists != nil {
return nil, errorsx.InvalidParamI18n("error.e0314")
}
if exists := s.Take("agent_code = ? AND id <> ?", req.AgentCode, id); exists != nil {
return nil, errorsx.InvalidParamI18n("error.e0163")
}
if !enums.IsValidServiceStatus(req.ServiceStatus) || (id == 0 && req.ServiceStatus == enums.ServiceStatusVacation) {
return nil, errorsx.InvalidParamI18n("error.e0165")
}
if req.MaxConcurrentCount < 0 {
return nil, errorsx.InvalidParamI18n("error.e0229")
}
return &models.AgentProfile{
UserID: req.UserID,
TeamID: req.TeamID,
AgentCode: req.AgentCode,
DisplayName: req.DisplayName,
Avatar: strings.TrimSpace(req.Avatar),
ServiceStatus: req.ServiceStatus,
MaxConcurrentCount: req.MaxConcurrentCount,
PriorityLevel: req.PriorityLevel,
AutoAssignEnabled: req.AutoAssignEnabled,
ReceiveOfflineMessage: req.ReceiveOfflineMessage,
Remark: strings.TrimSpace(req.Remark),
}, nil
}
func (s *agentProfileService) dispatchPendingConversationsIfEligible(item *models.AgentProfile) {
if item == nil {
return
}
if item.Status != enums.StatusOk {
return
}
if !item.AutoAssignEnabled {
return
}
if effectiveSupportStatus(*item, time.Now()) != enums.ServiceStatusIdle {
return
}
_, _ = ConversationDispatchService.DispatchPendingConversations(0)
}
func (s *agentProfileService) FilterEffectiveServiceStatus(cnd *sqls.Cnd, value string) error {
if strings.TrimSpace(value) == "" {
return nil
}
number, err := strconv.Atoi(value)
if err != nil || !enums.IsValidServiceStatus(enums.ServiceStatus(number)) {
return errorsx.InvalidParamI18n("error.supportAvailability.invalidStatus")
}
repositories.SupportAvailabilityRepository.EffectiveStatusCondition(cnd, enums.ServiceStatus(number), time.Now())
return nil
}