package csmanualentry import ( "context" "strings" "unicode/utf8" "hfb_sys/backend/internal/timeutil" ) const ( maxEntryNoLength = 64 maxOptionNameLength = 64 ) type Service struct { repo *Repository } func NewService(repo *Repository) *Service { return &Service{repo: repo} } // Create 校验编号并落库。客服身份由 handler 从登录上下文注入。 func (s *Service) Create(ctx context.Context, adminID uint64, req CreateRequest) (*CreateResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } entryNo, err := normalizeEntryNo(req.EntryNo) if err != nil { return nil, err } platform, err := normalizeOptionName(req.Platform) if err != nil { return nil, err } operator, err := normalizeOptionName(req.Operator) if err != nil { return nil, err } adminName, adminUsername, err := s.repo.FindAdminSnapshot(ctx, adminID) if err != nil { return nil, err } if adminName == "" { adminName = adminUsername } entry, err := s.repo.Create(ctx, adminID, adminName, adminUsername, entryNo, platform, operator, strings.TrimSpace(req.Remark)) if err != nil { return nil, err } return &CreateResult{Entry: *entry}, nil } // Update 管理员修正录单记录。 func (s *Service) Update(ctx context.Context, id uint64, req UpdateRequest) (*EntryDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } entryNo, err := normalizeEntryNo(req.EntryNo) if err != nil { return nil, err } platform, err := normalizeOptionName(req.Platform) if err != nil { return nil, err } operator, err := normalizeOptionName(req.Operator) if err != nil { return nil, err } return s.repo.Update(ctx, id, entryNo, platform, operator, strings.TrimSpace(req.Remark)) } // Delete 管理员删除录单记录。 func (s *Service) Delete(ctx context.Context, id uint64) error { if s.repo == nil { return ErrDependencyUnavailable } return s.repo.Delete(ctx, id) } // List 分页查询录单记录。 func (s *Service) List(ctx context.Context, query ListQuery) (*PaginatedResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.List(ctx, query) } // TodayAgentStats 返回今日(北京时间)各客服录单统计。 func (s *Service) TodayAgentStats(ctx context.Context) ([]AgentDayStatsDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.TodayAgentStats(ctx, timeutil.ShanghaiNow()) } // ListOptions 返回平台与操作人两组可选项。 func (s *Service) ListOptions(ctx context.Context) (*OptionListDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } return s.repo.ListAllOptions(ctx) } // CreateOption 新增平台 / 操作人选项。 func (s *Service) CreateOption(ctx context.Context, req CreateOptionRequest) (*OptionDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } optionType := strings.TrimSpace(req.Type) if !IsValidOptionType(optionType) { return nil, ErrOptionTypeInvalid } name, err := normalizeOptionName(req.Name) if err != nil { return nil, err } if name == "" { return nil, ErrOptionNameInvalid } return s.repo.CreateOption(ctx, optionType, name) } // UpdateOption 重命名平台 / 操作人选项。 func (s *Service) UpdateOption(ctx context.Context, id uint64, req UpdateOptionRequest) (*OptionDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } name, err := normalizeOptionName(req.Name) if err != nil { return nil, err } if name == "" { return nil, ErrOptionNameInvalid } return s.repo.UpdateOption(ctx, id, name) } // DeleteOption 删除平台 / 操作人选项。 func (s *Service) DeleteOption(ctx context.Context, id uint64) error { if s.repo == nil { return ErrDependencyUnavailable } return s.repo.DeleteOption(ctx, id) } // normalizeEntryNo 去除首尾空白并做基础校验。 func normalizeEntryNo(raw string) (string, error) { value := strings.TrimSpace(raw) if value == "" { return "", ErrEntryNoInvalid } if len(value) > maxEntryNoLength { return "", ErrEntryNoInvalid } if strings.ContainsAny(value, " \t\r\n") { return "", ErrEntryNoInvalid } return value, nil } // normalizeOptionName 规整平台 / 操作人名称:去首尾空白、按字符数限长。 // 允许为空(录单时平台与操作人选填),但内部不允许换行等控制字符。 func normalizeOptionName(raw string) (string, error) { value := strings.TrimSpace(raw) if utf8.RuneCountInString(value) > maxOptionNameLength { return "", ErrOptionNameInvalid } if strings.ContainsAny(value, "\t\r\n") { return "", ErrOptionNameInvalid } return value, nil }