diff --git a/internal/service/channel/channel.go b/internal/service/channel/channel.go index d14ad6b..96d44f9 100755 --- a/internal/service/channel/channel.go +++ b/internal/service/channel/channel.go @@ -396,6 +396,46 @@ func (s *service) List(ctx context.Context, in ListInput) (items []*ChannelWithS return } +func (s *service) resolveStatsRange(ctx context.Context, channelID int64, days int, startDateStr, endDateStr string, now time.Time) (*time.Time, *time.Time, error) { + if startDateStr != "" && endDateStr != "" { + startDate, err := time.ParseInLocation("2006-01-02", startDateStr, time.Local) + if err != nil { + return nil, nil, err + } + endDate, err := time.ParseInLocation("2006-01-02", endDateStr, time.Local) + if err != nil { + return nil, nil, err + } + endDate = endDate.Add(24*time.Hour - time.Second) + return &startDate, &endDate, nil + } + + if days > 0 { + startDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -days+1) + endDate := now + return &startDate, &endDate, nil + } + + type minTimeRow struct { + MinCreatedAt *time.Time `gorm:"column:min_created_at"` + } + var row minTimeRow + err := s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders"). + Joins("JOIN users ON users.id = orders.user_id"). + Select("MIN(orders.created_at) as min_created_at"). + Where("users.channel_id = ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type IN (2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)", channelID). + Scan(&row).Error + if err != nil { + return nil, nil, err + } + if row.MinCreatedAt == nil { + return nil, nil, nil + } + startDate := time.Date(row.MinCreatedAt.Year(), row.MinCreatedAt.Month(), row.MinCreatedAt.Day(), 0, 0, 0, 0, now.Location()) + endDate := now + return &startDate, &endDate, nil +} + func (s *service) GetStats(ctx context.Context, channelID int64, days int, startDateStr, endDateStr string) (*StatsOutput, error) { now := time.Now() @@ -411,49 +451,53 @@ func (s *service) GetStats(ctx context.Context, channelID int64, days int, start // source_type: 2=小程序抽奖 3=对对碰 4=一番赏/次卡 5=直播间抽奖抖店(不计入);排除商城直购(1) // actual_amount>0 排除次卡免费使用的订单(避免与购买次卡的订单重复计入GMV) orderFilter := "users.channel_id = ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type IN (2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)" + statsStart, statsEnd, err := s.resolveStatsRange(ctx, channelID, days, startDateStr, endDateStr, now) + if err != nil { + return nil, err + } - // ========== 1. Overview(全量,不限时间)========== + // ========== 1. Overview(与当前筛选窗口保持一致)========== - userCount, _ := s.readDB.Users.WithContext(ctx).Where(s.readDB.Users.ChannelID.Eq(channelID)).Count() + userCountQuery := s.readDB.Users.WithContext(ctx) + if statsStart != nil && statsEnd != nil { + userCountQuery = userCountQuery.Where(s.readDB.Users.CreatedAt.Gte(*statsStart)).Where(s.readDB.Users.CreatedAt.Lte(*statsEnd)) + } + userCount, _ := userCountQuery.Where(s.readDB.Users.ChannelID.Eq(channelID)).Count() out.Overview.TotalUsers = userCount type countResult struct{ Count int64 } var cr countResult - s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders"). + overviewOrders := s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders"). Joins("JOIN users ON users.id = orders.user_id"). Select("count(*) as count"). - Where(orderFilter, channelID). - Scan(&cr) + Where(orderFilter, channelID) + if statsStart != nil && statsEnd != nil { + overviewOrders = overviewOrders.Where("orders.created_at >= ? AND orders.created_at <= ?", *statsStart, *statsEnd) + } + overviewOrders.Scan(&cr) out.Overview.TotalOrders = cr.Count - totalGMV, _ := s.calcGMVByTotalAmount(ctx, channelID, "2006-01-02", orderFilter, nil, nil) + totalGMV, _ := s.calcGMVByTotalAmount(ctx, channelID, "2006-01-02", orderFilter, statsStart, statsEnd) out.Overview.TotalPaidCents = totalGMV.Total out.Overview.TotalGMV = totalGMV.Total / 100 out.Overview.CashCents = totalGMV.Cash out.Overview.CouponCents = totalGMV.Coupon out.Overview.PointsCents = totalGMV.Points - // 1d. 累计成本(全量,按原始订单/抽奖来源归因) - totalCost, _ := s.calcCostByDrawSource(ctx, channelID, "2006-01-02", nil, nil) + // 1d. 累计成本(当前筛选窗口,按原始订单/抽奖来源归因) + totalCost, _ := s.calcCostByDrawSource(ctx, channelID, "2006-01-02", statsStart, statsEnd) out.Overview.TotalCostCents = totalCost out.Overview.TotalCost = totalCost / 100 out.Overview.TotalProfitCents = totalGMV.Total - totalCost out.Overview.TotalProfit = out.Overview.TotalProfitCents / 100 - // ========== 2. 趋势图(按天分组,受 days 限制)========== + // ========== 2. 趋势图(按天分组,与 overview 保持相同时间窗口)========== - var startDate, endDate time.Time - if startDateStr != "" && endDateStr != "" { - startDate, _ = time.Parse("2006-01-02", startDateStr) - endDate, _ = time.Parse("2006-01-02", endDateStr) - endDate = endDate.Add(24*time.Hour - time.Second) - } else { - if days <= 0 { - days = 12 - } - startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -days+1) - endDate = now + if statsStart == nil || statsEnd == nil { + return out, nil } + startDate := *statsStart + endDate := *statsEnd dateMap := make(map[string]*StatsDailyItem) var dateList []string