增加客服上传账号统计
This commit is contained in:
@@ -24,12 +24,14 @@ type MetricsDTO struct {
|
||||
|
||||
// ListingDailyOverviewDTO 商品上下架统计:今日三组 + 近 7 日趋势。
|
||||
type ListingDailyOverviewDTO struct {
|
||||
Today ListingDayStatsDTO `json:"today"`
|
||||
Trend []ListingDayStatsDTO `json:"trend"`
|
||||
TodayChannels []ListingChannelDayStatsDTO `json:"today_channels"`
|
||||
ChannelTrend []ListingChannelDayStatsDTO `json:"channel_trend"`
|
||||
Days int `json:"days"`
|
||||
Timezone string `json:"timezone"`
|
||||
Today ListingDayStatsDTO `json:"today"`
|
||||
Trend []ListingDayStatsDTO `json:"trend"`
|
||||
TodayChannels []ListingChannelDayStatsDTO `json:"today_channels"`
|
||||
ChannelTrend []ListingChannelDayStatsDTO `json:"channel_trend"`
|
||||
TodayUploaders []ListingUploaderDayStatsDTO `json:"today_uploaders"`
|
||||
UploaderTrend []ListingUploaderDayStatsDTO `json:"uploader_trend"`
|
||||
Days int `json:"days"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type ListingDayStatsDTO struct {
|
||||
@@ -47,6 +49,13 @@ type ListingChannelDayStatsDTO struct {
|
||||
TradeLeaveCount int64 `json:"trade_leave_count"`
|
||||
}
|
||||
|
||||
type ListingUploaderDayStatsDTO struct {
|
||||
Date string `json:"date"` // YYYY-MM-DD(上海时区)
|
||||
UploaderID uint64 `json:"uploader_id"`
|
||||
UploaderName string `json:"uploader_name"`
|
||||
UploadCount int64 `json:"upload_count"`
|
||||
}
|
||||
|
||||
type PendingDTO struct {
|
||||
ListingReviews int64 `json:"listing_reviews"`
|
||||
Disputes int64 `json:"disputes"`
|
||||
|
||||
@@ -130,9 +130,18 @@ func (r *Repository) listingDailyOverview(ctx context.Context, now time.Time, da
|
||||
Scan(&events).Error; err != nil {
|
||||
return ListingDailyOverviewDTO{}, err
|
||||
}
|
||||
uploads := make([]listingUploaderRow, 0)
|
||||
if err := db.Table("listing_uploads").
|
||||
Select("COALESCE(matched_admin_id, 0) AS uploader_id, uploader_name, created_at").
|
||||
Where("listing_id IS NOT NULL").
|
||||
Where("created_at >= ? AND created_at < ?", start, end).
|
||||
Scan(&uploads).Error; err != nil {
|
||||
return ListingDailyOverviewDTO{}, err
|
||||
}
|
||||
|
||||
byDay := make(map[string]*listingDailyBucket, days)
|
||||
byChannel := make(map[listingChannelKey]*listingDailyBucket)
|
||||
byUploader := make(map[listingUploaderKey]int64)
|
||||
for _, ev := range events {
|
||||
key := ev.CreatedAt.In(loc).Format("2006-01-02")
|
||||
b := byDay[key]
|
||||
@@ -152,6 +161,11 @@ func (r *Repository) listingDailyOverview(ctx context.Context, now time.Time, da
|
||||
}
|
||||
addListingDailyEvent(cb, ev.EventType, ev.Source)
|
||||
}
|
||||
for _, upload := range uploads {
|
||||
key := upload.CreatedAt.In(loc).Format("2006-01-02")
|
||||
uploaderKey := listingUploaderKey{date: key, uploaderID: upload.UploaderID, uploaderName: upload.UploaderName}
|
||||
byUploader[uploaderKey]++
|
||||
}
|
||||
|
||||
trend := make([]ListingDayStatsDTO, 0, days)
|
||||
for i := days - 1; i >= 0; i-- {
|
||||
@@ -171,27 +185,48 @@ func (r *Repository) listingDailyOverview(ctx context.Context, now time.Time, da
|
||||
todayStats = trend[len(trend)-1]
|
||||
}
|
||||
channelTrend := listingChannelTrend(byChannel)
|
||||
uploaderTrend := listingUploaderTrend(byUploader)
|
||||
todayChannels := make([]ListingChannelDayStatsDTO, 0)
|
||||
for _, item := range channelTrend {
|
||||
if item.Date == todayStats.Date {
|
||||
todayChannels = append(todayChannels, item)
|
||||
}
|
||||
}
|
||||
todayUploaders := make([]ListingUploaderDayStatsDTO, 0)
|
||||
for _, item := range uploaderTrend {
|
||||
if item.Date == todayStats.Date {
|
||||
todayUploaders = append(todayUploaders, item)
|
||||
}
|
||||
}
|
||||
return ListingDailyOverviewDTO{
|
||||
Today: todayStats,
|
||||
Trend: trend,
|
||||
TodayChannels: todayChannels,
|
||||
ChannelTrend: channelTrend,
|
||||
Days: days,
|
||||
Timezone: "Asia/Shanghai",
|
||||
Today: todayStats,
|
||||
Trend: trend,
|
||||
TodayChannels: todayChannels,
|
||||
ChannelTrend: channelTrend,
|
||||
TodayUploaders: todayUploaders,
|
||||
UploaderTrend: uploaderTrend,
|
||||
Days: days,
|
||||
Timezone: "Asia/Shanghai",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listingUploaderRow struct {
|
||||
UploaderID uint64
|
||||
UploaderName string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type listingChannelKey struct {
|
||||
date string
|
||||
sourceChannel string
|
||||
}
|
||||
|
||||
type listingUploaderKey struct {
|
||||
date string
|
||||
uploaderID uint64
|
||||
uploaderName string
|
||||
}
|
||||
|
||||
type listingDailyBucket struct {
|
||||
published int64
|
||||
activeOffline int64
|
||||
@@ -244,6 +279,35 @@ func listingChannelTrend(rows map[listingChannelKey]*listingDailyBucket) []Listi
|
||||
return items
|
||||
}
|
||||
|
||||
func listingUploaderTrend(rows map[listingUploaderKey]int64) []ListingUploaderDayStatsDTO {
|
||||
keys := make([]listingUploaderKey, 0, len(rows))
|
||||
for key := range rows {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if keys[i].date != keys[j].date {
|
||||
return keys[i].date < keys[j].date
|
||||
}
|
||||
if rows[keys[i]] != rows[keys[j]] {
|
||||
return rows[keys[i]] > rows[keys[j]]
|
||||
}
|
||||
if keys[i].uploaderName != keys[j].uploaderName {
|
||||
return keys[i].uploaderName < keys[j].uploaderName
|
||||
}
|
||||
return keys[i].uploaderID < keys[j].uploaderID
|
||||
})
|
||||
items := make([]ListingUploaderDayStatsDTO, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
items = append(items, ListingUploaderDayStatsDTO{
|
||||
Date: key.date,
|
||||
UploaderID: key.uploaderID,
|
||||
UploaderName: key.uploaderName,
|
||||
UploadCount: rows[key],
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func listingChannelRank(channel string) int {
|
||||
switch channel {
|
||||
case "咸鱼":
|
||||
|
||||
@@ -65,3 +65,44 @@ func assertChannelStats(t *testing.T, item ListingChannelDayStatsDTO, published,
|
||||
t.Fatalf("channel %q stats = %#v, want published=%d offline=%d tradeLeave=%d", item.SourceChannel, item, published, offline, tradeLeave)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListingDailyOverviewGroupsExternalUploadsByUploader(t *testing.T) {
|
||||
db := database.NewTestDB()
|
||||
if err := db.AutoMigrate(&model.ListingStatusEvent{}, &model.ListingUpload{}); err != nil {
|
||||
t.Fatalf("AutoMigrate() error = %v", err)
|
||||
}
|
||||
loc := timeutil.ShanghaiLocation()
|
||||
now := time.Date(2026, 7, 25, 12, 0, 0, 0, loc)
|
||||
uploaderX := uint64(11)
|
||||
uploaderY := uint64(12)
|
||||
listingOne := uint64(101)
|
||||
listingTwo := uint64(102)
|
||||
listingThree := uint64(103)
|
||||
uploads := []model.ListingUpload{
|
||||
{UploaderName: "小晴", MatchedAdminID: &uploaderX, ListingID: &listingOne, CreatedAt: now},
|
||||
{UploaderName: "小晴", MatchedAdminID: &uploaderX, ListingID: &listingTwo, CreatedAt: now.Add(time.Hour)},
|
||||
{UploaderName: "小美", MatchedAdminID: &uploaderY, ListingID: &listingThree, CreatedAt: now.Add(2 * time.Hour)},
|
||||
{UploaderName: "小晴", MatchedAdminID: &uploaderX, ListingID: &listingOne, CreatedAt: now.AddDate(0, 0, -1)},
|
||||
}
|
||||
if err := db.Create(&uploads).Error; err != nil {
|
||||
t.Fatalf("create uploads error = %v", err)
|
||||
}
|
||||
|
||||
overview, err := NewRepository(db).listingDailyOverview(t.Context(), now, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("listingDailyOverview() error = %v", err)
|
||||
}
|
||||
if len(overview.TodayUploaders) != 2 {
|
||||
t.Fatalf("today uploaders = %#v, want 2 rows", overview.TodayUploaders)
|
||||
}
|
||||
counts := make(map[string]int64)
|
||||
for _, item := range overview.TodayUploaders {
|
||||
counts[item.UploaderName] = item.UploadCount
|
||||
}
|
||||
if counts["小晴"] != 2 || counts["小美"] != 1 {
|
||||
t.Fatalf("today uploader counts = %#v, want 小晴=2, 小美=1", counts)
|
||||
}
|
||||
if len(overview.UploaderTrend) != 3 {
|
||||
t.Fatalf("uploader trend = %#v, want 3 rows", overview.UploaderTrend)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE listing_uploads
|
||||
ADD KEY idx_listing_uploads_created_admin (created_at, matched_admin_id);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE listing_uploads
|
||||
DROP KEY idx_listing_uploads_created_admin;
|
||||
Reference in New Issue
Block a user