feat(admin): 新增客服手动录单功能与仪表盘今日录单统计
- 做了什么: - 新增客服手动录单数据库迁移 000084 及对应模型,支持编号唯一校验、客服快照绑定与 CRUD 操作; - 后端提供 csmanualentry 业务模块及相关 API,并在仪表盘摘要中聚合今日各客服录单数量; - 前端增加客服手动录单视图页面、路由与侧边栏导航,支持录单二次确认与细粒度权限控制; - 管理后台仪表盘增加今日客服手动录单卡片展示。 - 为什么: - 满足客服在后台手动登记外部业务编号需求,并便于运营按客服统计今日录单业绩和追溯对账。 - 验证方式: - 执行后端全量测试 go test ./... 与静态检查 go vet ./... 全部通过; - 执行前端代码门禁 npm run check 与 vite build 全部通过; - 执行 goose down/up 迁移回滚与应用测试通过,角色权限分配正确; - 本地 dev 联调 POST /api/admin/manual-entries 录单成功返回 201。
This commit is contained in:
@@ -3,12 +3,13 @@ package admindashboard
|
||||
import "time"
|
||||
|
||||
type DashboardDTO struct {
|
||||
Metrics MetricsDTO `json:"metrics"`
|
||||
Pending PendingDTO `json:"pending"`
|
||||
ListingDaily ListingDailyOverviewDTO `json:"listing_daily"`
|
||||
RecentOrders []RecentOrderDTO `json:"recent_orders"`
|
||||
RecentDisputes []RecentDisputeDTO `json:"recent_disputes"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Metrics MetricsDTO `json:"metrics"`
|
||||
Pending PendingDTO `json:"pending"`
|
||||
ListingDaily ListingDailyOverviewDTO `json:"listing_daily"`
|
||||
ManualEntryToday []ManualEntryAgentStatsDTO `json:"manual_entry_today"`
|
||||
RecentOrders []RecentOrderDTO `json:"recent_orders"`
|
||||
RecentDisputes []RecentDisputeDTO `json:"recent_disputes"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
type MetricsDTO struct {
|
||||
@@ -56,6 +57,13 @@ type ListingUploaderDayStatsDTO struct {
|
||||
UploadCount int64 `json:"upload_count"`
|
||||
}
|
||||
|
||||
// ManualEntryAgentStatsDTO 今日客服手动录单统计(按客服聚合)。
|
||||
type ManualEntryAgentStatsDTO struct {
|
||||
AdminID uint64 `json:"admin_id"`
|
||||
AdminName string `json:"admin_name"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type PendingDTO struct {
|
||||
ListingReviews int64 `json:"listing_reviews"`
|
||||
Disputes int64 `json:"disputes"`
|
||||
|
||||
@@ -78,6 +78,10 @@ func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manualEntryToday, err := r.manualEntryToday(ctx, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recentOrders, err := r.recentOrders(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -87,12 +91,13 @@ func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &DashboardDTO{
|
||||
Metrics: metrics,
|
||||
Pending: pending,
|
||||
ListingDaily: listingDaily,
|
||||
RecentOrders: recentOrders,
|
||||
RecentDisputes: recentDisputes,
|
||||
GeneratedAt: now,
|
||||
Metrics: metrics,
|
||||
Pending: pending,
|
||||
ListingDaily: listingDaily,
|
||||
ManualEntryToday: manualEntryToday,
|
||||
RecentOrders: recentOrders,
|
||||
RecentDisputes: recentDisputes,
|
||||
GeneratedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -348,6 +353,47 @@ func isTradeLeaveEvent(eventType, source string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// manualEntryToday 统计今日(北京时间自然日)各客服的手动录单数量。
|
||||
// 录单表是后来新增的功能表,若迁移尚未执行(表不存在)则降级为空统计,
|
||||
// 避免连带整个仪表盘不可用。
|
||||
func (r *Repository) manualEntryToday(ctx context.Context, now time.Time) ([]ManualEntryAgentStatsDTO, error) {
|
||||
if r.db == nil || !r.db.Migrator().HasTable("cs_manual_entries") {
|
||||
return []ManualEntryAgentStatsDTO{}, nil
|
||||
}
|
||||
loc := timeutil.ShanghaiLocation()
|
||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||
end := start.AddDate(0, 0, 1)
|
||||
|
||||
type row struct {
|
||||
AdminID uint64
|
||||
AdminName string
|
||||
Count int64
|
||||
}
|
||||
rows := make([]row, 0)
|
||||
if err := r.db.WithContext(ctx).Table("cs_manual_entries").
|
||||
Select("admin_id, MAX(admin_name) AS admin_name, COUNT(*) AS count").
|
||||
Where("created_at >= ? AND created_at < ?", start, end).
|
||||
Group("admin_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].Count != rows[j].Count {
|
||||
return rows[i].Count > rows[j].Count
|
||||
}
|
||||
return rows[i].AdminName < rows[j].AdminName
|
||||
})
|
||||
items := make([]ManualEntryAgentStatsDTO, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
items = append(items, ManualEntryAgentStatsDTO{
|
||||
AdminID: item.AdminID,
|
||||
AdminName: item.AdminName,
|
||||
Count: item.Count,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) recentOrders(ctx context.Context) ([]RecentOrderDTO, error) {
|
||||
rows := make([]RecentOrderDTO, 0)
|
||||
err := r.db.WithContext(ctx).Table("rental_orders AS o").
|
||||
|
||||
Reference in New Issue
Block a user