- 做了什么: - 新增客服手动录单数据库迁移 000084 及对应模型,支持编号唯一校验、客服快照绑定与 CRUD 操作; - 后端提供 csmanualentry 业务模块及相关 API,并在仪表盘摘要中聚合今日各客服录单数量; - 前端增加客服手动录单视图页面、路由与侧边栏导航,支持录单二次确认与细粒度权限控制; - 管理后台仪表盘增加今日客服手动录单卡片展示。 - 为什么: - 满足客服在后台手动登记外部业务编号需求,并便于运营按客服统计今日录单业绩和追溯对账。 - 验证方式: - 执行后端全量测试 go test ./... 与静态检查 go vet ./... 全部通过; - 执行前端代码门禁 npm run check 与 vite build 全部通过; - 执行 goose down/up 迁移回滚与应用测试通过,角色权限分配正确; - 本地 dev 联调 POST /api/admin/manual-entries 录单成功返回 201。
93 lines
2.3 KiB
Go
93 lines
2.3 KiB
Go
package csmanualentry
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"hfb_sys/backend/internal/timeutil"
|
|
)
|
|
|
|
const maxEntryNoLength = 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
|
|
}
|
|
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, 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
|
|
}
|
|
return s.repo.Update(ctx, id, entryNo, 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())
|
|
}
|
|
|
|
// 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
|
|
}
|