package services import ( "code.tczkiot.com/wlw/ai-agent/internal/models" "code.tczkiot.com/wlw/ai-agent/internal/pkg/dto" "code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request" "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums" "code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx" "code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx" "code.tczkiot.com/wlw/ai-agent/internal/pkg/utils" "code.tczkiot.com/wlw/ai-agent/internal/repositories" "slices" "strings" "sync" "time" "code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params" "github.com/mlogclub/simple/sqls" "gorm.io/gorm" ) var AgentTeamScheduleService = newAgentTeamScheduleService() func newAgentTeamScheduleService() *agentTeamScheduleService { return &agentTeamScheduleService{} } type agentTeamScheduleService struct { writeMu sync.Mutex } const maxAgentTeamScheduleBatchItems = 500 type AgentTeamScheduleBatchPreviewResult struct { Total int Conflict bool Items []AgentTeamScheduleBatchPreviewItem } type AgentTeamScheduleBatchPreviewItem struct { TeamID int64 TeamName string Date time.Time Weekday int StartAt time.Time EndAt time.Time Remark string Conflict bool ConflictReason string } type AgentTeamScheduleBatchGenerateResult struct { Created int } type batchScheduleCandidate struct { TeamID int64 TeamName string Date time.Time StartAt time.Time EndAt time.Time Remark string } func (s *agentTeamScheduleService) Get(id int64) *models.AgentTeamSchedule { return repositories.AgentTeamScheduleRepository.Get(sqls.DB(), id) } func (s *agentTeamScheduleService) Take(where ...interface{}) *models.AgentTeamSchedule { return repositories.AgentTeamScheduleRepository.Take(sqls.DB(), where...) } func (s *agentTeamScheduleService) Find(cnd *sqls.Cnd) []models.AgentTeamSchedule { return repositories.AgentTeamScheduleRepository.Find(sqls.DB(), cnd) } func (s *agentTeamScheduleService) FindOne(cnd *sqls.Cnd) *models.AgentTeamSchedule { return repositories.AgentTeamScheduleRepository.FindOne(sqls.DB(), cnd) } func (s *agentTeamScheduleService) FindPageByParams(params *params.QueryParams) (list []models.AgentTeamSchedule, paging *sqls.Paging) { return repositories.AgentTeamScheduleRepository.FindPageByParams(sqls.DB(), params) } func (s *agentTeamScheduleService) FindPageByCnd(cnd *sqls.Cnd) (list []models.AgentTeamSchedule, paging *sqls.Paging) { return repositories.AgentTeamScheduleRepository.FindPageByCnd(sqls.DB(), cnd) } func (s *agentTeamScheduleService) Count(cnd *sqls.Cnd) int64 { return repositories.AgentTeamScheduleRepository.Count(sqls.DB(), cnd) } func (s *agentTeamScheduleService) FindCalendarSchedules(req request.AgentTeamScheduleCalendarRequest) ([]models.AgentTeamSchedule, error) { startAtValue, err := parseRequiredDateTime(req.StartAt, "error.agentTeamSchedule.startAtInvalid", "error.agentTeamSchedule.startAtInvalidWithFormat") if err != nil { return nil, err } endAtValue, err := parseRequiredDateTime(req.EndAt, "error.agentTeamSchedule.endAtInvalid", "error.agentTeamSchedule.endAtInvalidWithFormat") if err != nil { return nil, err } if !endAtValue.After(startAtValue) { return nil, errorsx.InvalidParamI18n("error.e0296") } return repositories.AgentTeamScheduleRepository.FindByTimeRange(sqls.DB(), startAtValue, endAtValue, req.TeamID), nil } func (s *agentTeamScheduleService) Create(t *models.AgentTeamSchedule) error { return repositories.AgentTeamScheduleRepository.Create(sqls.DB(), t) } func (s *agentTeamScheduleService) Update(t *models.AgentTeamSchedule) error { return repositories.AgentTeamScheduleRepository.Update(sqls.DB(), t) } func (s *agentTeamScheduleService) Updates(id int64, columns map[string]interface{}) error { return repositories.AgentTeamScheduleRepository.Updates(sqls.DB(), id, columns) } func (s *agentTeamScheduleService) UpdateColumn(id int64, name string, value interface{}) error { return repositories.AgentTeamScheduleRepository.UpdateColumn(sqls.DB(), id, name, value) } func (s *agentTeamScheduleService) Delete(id int64) { repositories.AgentTeamScheduleRepository.Delete(sqls.DB(), id) } func (s *agentTeamScheduleService) CreateAgentTeamSchedule(req request.CreateAgentTeamScheduleRequest, operator *dto.AuthPrincipal) (*models.AgentTeamSchedule, error) { if operator == nil { return nil, errorsx.UnauthorizedI18n("error.auth.expired") } s.writeMu.Lock() item, err := s.buildScheduleModel(0, req.TeamID, req.StartAt, req.EndAt, req.Remark) if err != nil { s.writeMu.Unlock() return nil, err } item.AuditFields = utils.BuildAuditFields(operator) if err := repositories.AgentTeamScheduleRepository.Create(sqls.DB(), item); err != nil { s.writeMu.Unlock() return nil, err } s.writeMu.Unlock() s.dispatchPendingConversationsIfActive(item) return item, nil } func (s *agentTeamScheduleService) UpdateAgentTeamSchedule(req request.UpdateAgentTeamScheduleRequest, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.UnauthorizedI18n("error.auth.expired") } s.writeMu.Lock() if s.Get(req.ID) == nil { s.writeMu.Unlock() return errorsx.InvalidParamI18n("error.e0172") } item, err := s.buildScheduleModel(req.ID, req.TeamID, req.StartAt, req.EndAt, req.Remark) if err != nil { s.writeMu.Unlock() return err } if err := repositories.AgentTeamScheduleRepository.Updates(sqls.DB(), req.ID, map[string]any{ "team_id": item.TeamID, "start_at": item.StartAt, "end_at": item.EndAt, "remark": item.Remark, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": time.Now(), }); err != nil { s.writeMu.Unlock() return err } s.writeMu.Unlock() s.dispatchPendingConversationsIfActive(item) return nil } func (s *agentTeamScheduleService) DeleteAgentTeamSchedule(id int64) error { if s.Get(id) == nil { return errorsx.InvalidParamI18n("error.e0172") } repositories.AgentTeamScheduleRepository.Delete(sqls.DB(), id) return nil } func (s *agentTeamScheduleService) BatchPreview(req request.AgentTeamScheduleBatchRequest, operator *dto.AuthPrincipal) (*AgentTeamScheduleBatchPreviewResult, error) { if operator == nil { return nil, errorsx.UnauthorizedI18n("error.auth.expired") } candidates, err := s.buildBatchScheduleCandidates(req) if err != nil { return nil, err } conflicts := s.findBatchConflict(candidates, req.Locale) return buildBatchPreviewResult(candidates, conflicts), nil } func (s *agentTeamScheduleService) BatchGenerate(req request.AgentTeamScheduleBatchRequest, operator *dto.AuthPrincipal) (*AgentTeamScheduleBatchGenerateResult, error) { if operator == nil { return nil, errorsx.UnauthorizedI18n("error.auth.expired") } s.writeMu.Lock() candidates, err := s.buildBatchScheduleCandidates(req) if err != nil { s.writeMu.Unlock() return nil, err } conflicts := s.findBatchConflict(candidates, req.Locale) for _, conflict := range conflicts { if conflict != "" { s.writeMu.Unlock() return nil, errorsx.InvalidParamI18n("error.e0151") } } schedules := make([]models.AgentTeamSchedule, 0, len(candidates)) for _, candidate := range candidates { schedules = append(schedules, models.AgentTeamSchedule{ TeamID: candidate.TeamID, StartAt: candidate.StartAt, EndAt: candidate.EndAt, Remark: candidate.Remark, Status: enums.StatusOk, AuditFields: utils.BuildAuditFields(operator), }) } if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { conflicts := s.findBatchConflictByDB(ctx.Tx, candidates, req.Locale) for _, conflict := range conflicts { if conflict != "" { return errorsx.InvalidParamI18n("error.e0151") } } return repositories.AgentTeamScheduleRepository.CreateBatch(ctx.Tx, schedules) }); err != nil { s.writeMu.Unlock() return nil, err } s.writeMu.Unlock() for i := range schedules { s.dispatchPendingConversationsIfActive(&schedules[i]) } return &AgentTeamScheduleBatchGenerateResult{Created: len(schedules)}, nil } func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt, endAt, remark string) (*models.AgentTeamSchedule, error) { if teamID <= 0 { return nil, errorsx.InvalidParamI18n("error.e0326") } team := AgentTeamService.Get(teamID) if team == nil { return nil, errorsx.InvalidParamI18n("error.e0169") } if !slices.Contains(enums.StatusValues, team.Status) { return nil, errorsx.InvalidParamI18n("error.e0174") } startAtValue, err := parseRequiredDateTime(startAt, "error.agentTeamSchedule.startAtInvalid", "error.agentTeamSchedule.startAtInvalidWithFormat") if err != nil { return nil, err } endAtValue, err := parseRequiredDateTime(endAt, "error.agentTeamSchedule.endAtInvalid", "error.agentTeamSchedule.endAtInvalidWithFormat") if err != nil { return nil, err } if !endAtValue.After(startAtValue) { return nil, errorsx.InvalidParamI18n("error.e0296") } if !sameLocalDay(startAtValue, endAtValue) { return nil, errorsx.InvalidParamI18n("error.e0132") } if startAtValue.Before(startOfLocalDay(time.Now())) { return nil, errorsx.InvalidParamI18n("error.e0085") } overlapping := repositories.AgentTeamScheduleRepository.FindOverlappingByTeamIDsAndTimeRange(sqls.DB(), []int64{teamID}, startAtValue, endAtValue) for _, item := range overlapping { if item.ID != id { return nil, errorsx.InvalidParamI18n("error.e0309") } } return &models.AgentTeamSchedule{ TeamID: teamID, StartAt: startAtValue, EndAt: endAtValue, Remark: strings.TrimSpace(remark), }, nil } func (s *agentTeamScheduleService) buildBatchScheduleCandidates(req request.AgentTeamScheduleBatchRequest) ([]batchScheduleCandidate, error) { teamIDs := uniquePositiveInt64s(req.TeamIDs) if len(teamIDs) == 0 { return nil, errorsx.InvalidParamI18n("error.e0326") } weekdays, err := normalizeBatchWeekdays(req.Weekdays) if err != nil { return nil, err } startDate, err := parseRequiredDate(req.StartDate, "error.agentTeamSchedule.startDateInvalid", "error.agentTeamSchedule.startDateInvalidWithFormat") if err != nil { return nil, err } endDate, err := parseRequiredDate(req.EndDate, "error.agentTeamSchedule.endDateInvalid", "error.agentTeamSchedule.endDateInvalidWithFormat") if err != nil { return nil, err } if endDate.Before(startDate) { return nil, errorsx.InvalidParamI18n("error.e0295") } if startDate.Before(startOfLocalDay(time.Now())) { return nil, errorsx.InvalidParamI18n("error.e0085") } startClock, err := parseRequiredClock(req.StartTime, "error.agentTeamSchedule.startTimeInvalid", "error.agentTeamSchedule.startTimeInvalidWithFormat") if err != nil { return nil, err } endClock, err := parseRequiredClock(req.EndTime, "error.agentTeamSchedule.endTimeInvalid", "error.agentTeamSchedule.endTimeInvalidWithFormat") if err != nil { return nil, err } firstStartAt := combineDateAndClock(startDate, startClock) firstEndAt := combineDateAndClock(startDate, endClock) if !firstEndAt.After(firstStartAt) { return nil, errorsx.InvalidParamI18n("error.e0296") } teams := AgentTeamService.FindByIds(teamIDs) teamsByID := make(map[int64]models.AgentTeam, len(teams)) for _, team := range teams { teamsByID[team.ID] = team } for _, teamID := range teamIDs { team, ok := teamsByID[teamID] if !ok || team.Status == enums.StatusDeleted { return nil, errorsx.InvalidParamI18n("error.e0169") } if !slices.Contains(enums.StatusValues, team.Status) { return nil, errorsx.InvalidParamI18n("error.e0174") } } weekdaySet := make(map[int]struct{}, len(weekdays)) for _, weekday := range weekdays { weekdaySet[weekday] = struct{}{} } candidates := make([]batchScheduleCandidate, 0) remark := strings.TrimSpace(req.Remark) for _, teamID := range teamIDs { team := teamsByID[teamID] for date := startDate; !date.After(endDate); date = date.AddDate(0, 0, 1) { if _, ok := weekdaySet[weekdayForBatchRequest(date)]; !ok { continue } if len(candidates) >= maxAgentTeamScheduleBatchItems { return nil, errorsx.InvalidParamI18n("error.agentTeamSchedule.batchLimit", maxAgentTeamScheduleBatchItems) } candidates = append(candidates, batchScheduleCandidate{ TeamID: teamID, TeamName: team.Name, Date: date, StartAt: combineDateAndClock(date, startClock), EndAt: combineDateAndClock(date, endClock), Remark: remark, }) } } if len(candidates) == 0 { return nil, errorsx.InvalidParamI18n("error.e0232") } return candidates, nil } func parseRequiredDate(value, emptyKey, formatKey string) (time.Time, error) { value = strings.TrimSpace(value) if value == "" { return time.Time{}, errorsx.InvalidParamI18n(emptyKey) } ret, err := time.ParseInLocation(time.DateOnly, value, time.Local) if err != nil { return time.Time{}, errorsx.InvalidParamI18n(formatKey) } return startOfLocalDay(ret), nil } func parseRequiredClock(value, emptyKey, formatKey string) (time.Time, error) { value = strings.TrimSpace(value) if value == "" { return time.Time{}, errorsx.InvalidParamI18n(emptyKey) } layouts := []string{"15:04", "15:04:05"} for _, layout := range layouts { if ret, err := time.ParseInLocation(layout, value, time.Local); err == nil { return ret, nil } } return time.Time{}, errorsx.InvalidParamI18n(formatKey) } func combineDateAndClock(date, clock time.Time) time.Time { year, month, day := date.In(time.Local).Date() hour, minute, second := clock.In(time.Local).Clock() return time.Date(year, month, day, hour, minute, second, 0, time.Local) } func buildBatchPreviewResult(candidates []batchScheduleCandidate, conflicts map[int]string) *AgentTeamScheduleBatchPreviewResult { items := make([]AgentTeamScheduleBatchPreviewItem, 0, len(candidates)) hasConflict := false for i, candidate := range candidates { conflictReason := conflicts[i] conflict := conflictReason != "" if conflict { hasConflict = true } items = append(items, AgentTeamScheduleBatchPreviewItem{ TeamID: candidate.TeamID, TeamName: candidate.TeamName, Date: candidate.Date, Weekday: weekdayForBatchRequest(candidate.Date), StartAt: candidate.StartAt, EndAt: candidate.EndAt, Remark: candidate.Remark, Conflict: conflict, ConflictReason: conflictReason, }) } return &AgentTeamScheduleBatchPreviewResult{ Total: len(items), Conflict: hasConflict, Items: items, } } func (s *agentTeamScheduleService) findBatchConflict(candidates []batchScheduleCandidate, locale string) map[int]string { return s.findBatchConflictByDB(sqls.DB(), candidates, locale) } func (s *agentTeamScheduleService) findBatchConflictByDB(db *gorm.DB, candidates []batchScheduleCandidate, locale string) map[int]string { conflicts := make(map[int]string) if len(candidates) == 0 { return conflicts } teamIDs := make([]int64, 0, len(candidates)) startAt := candidates[0].StartAt endAt := candidates[0].EndAt for _, candidate := range candidates { teamIDs = append(teamIDs, candidate.TeamID) if candidate.StartAt.Before(startAt) { startAt = candidate.StartAt } if candidate.EndAt.After(endAt) { endAt = candidate.EndAt } } existing := repositories.AgentTeamScheduleRepository.FindOverlappingByTeamIDsAndTimeRange(db, uniquePositiveInt64s(teamIDs), startAt, endAt) for i, candidate := range candidates { for _, item := range existing { if item.TeamID != candidate.TeamID { continue } if item.StartAt.Before(candidate.EndAt) && item.EndAt.After(candidate.StartAt) { conflicts[i] = i18nx.Getf(locale, "error.agentTeamSchedule.conflictRange", item.StartAt.Format(time.DateTime), item.EndAt.Format(time.DateTime)) break } } } return conflicts } func normalizeBatchWeekdays(values []int) ([]int, error) { seen := make(map[int]struct{}, len(values)) ret := make([]int, 0, len(values)) for _, value := range values { if value < 1 || value > 7 { return nil, errorsx.InvalidParamI18n("error.e0228") } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} ret = append(ret, value) } if len(ret) == 0 { return nil, errorsx.InvalidParamI18n("error.e0329") } return ret, nil } func weekdayForBatchRequest(value time.Time) int { if value.Weekday() == time.Sunday { return 7 } return int(value.Weekday()) } func uniquePositiveInt64s(values []int64) []int64 { seen := make(map[int64]struct{}, len(values)) ret := make([]int64, 0, len(values)) for _, value := range values { if value <= 0 { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} ret = append(ret, value) } return ret } func parseRequiredDateTime(value, emptyKey, formatKey string) (time.Time, error) { value = strings.TrimSpace(value) if value == "" { return time.Time{}, errorsx.InvalidParamI18n(emptyKey) } ret, err := parseDateTimeValue(value) if err != nil { return time.Time{}, errorsx.InvalidParamI18n(formatKey) } return ret, nil } func parseDateTimeValue(value string) (time.Time, error) { layouts := []string{ time.DateTime, time.RFC3339, "2006-01-02T15:04", "2006-01-02T15:04:05", } for _, layout := range layouts { if ret, err := time.ParseInLocation(layout, value, time.Local); err == nil { return ret, nil } } return time.Time{}, errorsx.InvalidParamI18n("error.e0227") } func startOfLocalDay(value time.Time) time.Time { year, month, day := value.In(time.Local).Date() return time.Date(year, month, day, 0, 0, 0, 0, time.Local) } func sameLocalDay(a, b time.Time) bool { aYear, aMonth, aDay := a.In(time.Local).Date() bYear, bMonth, bDay := b.In(time.Local).Date() return aYear == bYear && aMonth == bMonth && aDay == bDay } func (s *agentTeamScheduleService) dispatchPendingConversationsIfActive(item *models.AgentTeamSchedule) { if item == nil { return } if item.Status != enums.StatusOk { return } now := time.Now() if item.StartAt.After(now) || !item.EndAt.After(now) { return } _, _ = ConversationDispatchService.DispatchPendingConversations(0) }