feat(admin): 客服手动录单支持平台与操作人,并可后台配置可选项
- 录单列表新增「平台」「操作人」两列,录单表单与编辑弹窗新增同名字段下拉 - 新增 cs_manual_entry_options 表与 4 个接口(读取/新增/重命名/删除),页面右上角弹窗维护平台与操作人 - 迁移 000087:cs_manual_entries 增加 platform/operator 列与索引,注册 cs_manual_entry:option_manage 权限并授予 ops/super_admin - 记录保存名称快照,删除选项不影响历史记录;同类型重名返回 409,跨类型同名允许 - 顶部关键词查询支持命中平台/操作人 - 新增单测:选项 CRUD 边界与录单平台/操作人读写
This commit is contained in:
@@ -3,11 +3,15 @@ package csmanualentry
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"hfb_sys/backend/internal/timeutil"
|
||||
)
|
||||
|
||||
const maxEntryNoLength = 64
|
||||
const (
|
||||
maxEntryNoLength = 64
|
||||
maxOptionNameLength = 64
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
@@ -26,6 +30,14 @@ func (s *Service) Create(ctx context.Context, adminID uint64, req CreateRequest)
|
||||
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
|
||||
@@ -33,7 +45,7 @@ func (s *Service) Create(ctx context.Context, adminID uint64, req CreateRequest)
|
||||
if adminName == "" {
|
||||
adminName = adminUsername
|
||||
}
|
||||
entry, err := s.repo.Create(ctx, adminID, adminName, adminUsername, entryNo, strings.TrimSpace(req.Remark))
|
||||
entry, err := s.repo.Create(ctx, adminID, adminName, adminUsername, entryNo, platform, operator, strings.TrimSpace(req.Remark))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,7 +61,15 @@ func (s *Service) Update(ctx context.Context, id uint64, req UpdateRequest) (*En
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.Update(ctx, id, entryNo, strings.TrimSpace(req.Remark))
|
||||
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 管理员删除录单记录。
|
||||
@@ -76,6 +96,56 @@ func (s *Service) TodayAgentStats(ctx context.Context) ([]AgentDayStatsDTO, erro
|
||||
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)
|
||||
@@ -90,3 +160,16 @@ func normalizeEntryNo(raw string) (string, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user