Files
hfb_sys/backend/internal/modules/admindashboard/repository.go
T
2026-07-11 23:37:24 +08:00

206 lines
6.4 KiB
Go

package admindashboard
import (
"context"
"time"
"hfb_sys/backend/internal/listingstatus"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/timeutil"
"gorm.io/gorm"
)
const listingDailyTrendDays = 7
type Repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
db := r.db.WithContext(ctx)
loc := timeutil.ShanghaiLocation()
now := timeutil.ShanghaiNow()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
metrics := MetricsDTO{}
pending := PendingDTO{}
if err := db.Model(&model.User{}).Count(&metrics.TotalUsers).Error; err != nil {
return nil, err
}
if err := db.Model(&model.User{}).Where("realname_status = ?", "verified").Count(&metrics.VerifiedUsers).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalListing{}).Count(&metrics.TotalListings).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalListing{}).Where("status = ? AND review_status = ?", "published", "approved").Count(&metrics.PublishedListings).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalOrder{}).Count(&metrics.TotalOrders).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalOrder{}).Where("status = ?", "renting").Count(&metrics.RentingOrders).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalOrder{}).Where("created_at >= ?", today).Count(&metrics.TodayOrders).Error; err != nil {
return nil, err
}
if err := db.Model(&model.WalletLedger{}).
Select("COALESCE(SUM(amount_cent), 0)").
Where("created_at >= ?", today).
Scan(&metrics.TodayLedgerAmountCent).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalListing{}).Where("review_status = ?", "pending").Count(&pending.ListingReviews).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Dispute{}).Where("status IN ?", []string{"open", "processing"}).Count(&pending.Disputes).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalOrder{}).Where("status = ? AND handoff_status IN ?", "pending_handoff", []string{"pending_owner", "pending_renter_confirm"}).Count(&pending.PendingHandoffs).Error; err != nil {
return nil, err
}
if err := db.Model(&model.RentalOrder{}).Where("status IN ?", []string{"pending_checkout_confirm", "pending_checkout_accept"}).Count(&pending.PendingReturnConfirms).Error; err != nil {
return nil, err
}
listingDaily, err := r.listingDailyOverview(ctx, now, listingDailyTrendDays)
if err != nil {
return nil, err
}
recentOrders, err := r.recentOrders(ctx)
if err != nil {
return nil, err
}
recentDisputes, err := r.recentDisputes(ctx)
if err != nil {
return nil, err
}
return &DashboardDTO{
Metrics: metrics,
Pending: pending,
ListingDaily: listingDaily,
RecentOrders: recentOrders,
RecentDisputes: recentDisputes,
GeneratedAt: now,
}, nil
}
func (r *Repository) listingDailyOverview(ctx context.Context, now time.Time, days int) (ListingDailyOverviewDTO, error) {
if days <= 0 {
days = listingDailyTrendDays
}
loc := timeutil.ShanghaiLocation()
now = now.In(loc)
// 右开区间 [start, end)
end := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, 1)
start := end.AddDate(0, 0, -days)
type eventRow struct {
EventType string
Source string
CreatedAt time.Time
}
events := make([]eventRow, 0)
if err := r.db.WithContext(ctx).Model(&model.ListingStatusEvent{}).
Select("event_type, source, created_at").
Where("created_at >= ? AND created_at < ?", start, end).
Scan(&events).Error; err != nil {
return ListingDailyOverviewDTO{}, err
}
type bucket struct {
published int64
activeOffline int64
tradeLeave int64
}
byDay := make(map[string]*bucket, days)
for _, ev := range events {
key := ev.CreatedAt.In(loc).Format("2006-01-02")
b := byDay[key]
if b == nil {
b = &bucket{}
byDay[key] = b
}
switch {
case ev.EventType == listingstatus.EventPublished:
b.published++
case isActiveOfflineEvent(ev.EventType, ev.Source):
b.activeOffline++
case isTradeLeaveEvent(ev.EventType, ev.Source):
b.tradeLeave++
}
}
trend := make([]ListingDayStatsDTO, 0, days)
for i := days - 1; i >= 0; i-- {
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -i)
key := day.Format("2006-01-02")
item := ListingDayStatsDTO{Date: key}
if b := byDay[key]; b != nil {
item.PublishedCount = b.published
item.ActiveOfflineCount = b.activeOffline
item.TradeLeaveCount = b.tradeLeave
}
trend = append(trend, item)
}
todayStats := ListingDayStatsDTO{Date: now.Format("2006-01-02")}
if len(trend) > 0 {
todayStats = trend[len(trend)-1]
}
return ListingDailyOverviewDTO{
Today: todayStats,
Trend: trend,
Days: days,
Timezone: "Asia/Shanghai",
}, nil
}
func isActiveOfflineEvent(eventType, source string) bool {
return eventType == listingstatus.EventOffline &&
(source == listingstatus.SourceSeller || source == listingstatus.SourceAdmin)
}
func isTradeLeaveEvent(eventType, source string) bool {
switch eventType {
case listingstatus.EventRented, listingstatus.EventCompleted, listingstatus.EventSealed:
return true
case listingstatus.EventOffline:
return source == listingstatus.SourceOrder ||
source == listingstatus.SourceDispute ||
source == listingstatus.SourcePickup ||
source == listingstatus.SourceSystem
default:
return false
}
}
func (r *Repository) recentOrders(ctx context.Context) ([]RecentOrderDTO, error) {
rows := make([]RecentOrderDTO, 0)
err := r.db.WithContext(ctx).Table("rental_orders AS o").
Select("o.id, o.order_no, a.title, o.renter_id, o.owner_id, o.status, o.rent_amount_cent, o.deposit_amount_cent, o.created_at").
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
Order("o.id DESC").
Limit(8).
Scan(&rows).Error
return rows, err
}
func (r *Repository) recentDisputes(ctx context.Context) ([]RecentDisputeDTO, error) {
rows := make([]RecentDisputeDTO, 0)
err := r.db.WithContext(ctx).Table("disputes AS d").
Select("d.id, d.order_id, o.order_no, a.title, d.type, d.status, d.initiator_id, d.created_at").
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
Order("d.id DESC").
Limit(8).
Scan(&rows).Error
return rows, err
}