package channel import ( "context" "time" "bindbox-game/internal/pkg/logger" "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" ) type Service interface { Create(ctx context.Context, in CreateInput) (*model.Channels, error) Modify(ctx context.Context, id int64, in ModifyInput) error Delete(ctx context.Context, id int64) error List(ctx context.Context, in ListInput) (items []*ChannelWithStat, total int64, err error) GetStats(ctx context.Context, channelID int64, days int) (*StatsOutput, error) } type service struct { logger logger.CustomLogger readDB *dao.Query writeDB *dao.Query } func New(l logger.CustomLogger, db mysql.Repo) Service { return &service{logger: l, readDB: dao.Use(db.GetDbR()), writeDB: dao.Use(db.GetDbW())} } type CreateInput struct { Name string Code string Type string Remarks string } type ModifyInput struct { Name *string Type *string Remarks *string } type ListInput struct { Name string Page int PageSize int } type ChannelWithStat struct { *model.Channels UserCount int64 `json:"user_count"` } type StatsOutput struct { Overview StatsOverview `json:"overview"` Daily []StatsDailyItem `json:"daily"` } type StatsOverview struct { TotalUsers int64 `json:"total_users"` TotalOrders int64 `json:"total_orders"` TotalGMV int64 `json:"total_gmv"` } type StatsDailyItem struct { Date string `json:"date"` UserCount int64 `json:"user_count"` OrderCount int64 `json:"order_count"` GMV int64 `json:"gmv"` } func (s *service) Create(ctx context.Context, in CreateInput) (*model.Channels, error) { m := &model.Channels{Name: in.Name, Code: in.Code, Type: in.Type, Remarks: in.Remarks} if err := s.writeDB.Channels.WithContext(ctx).Create(m); err != nil { return nil, err } return m, nil } func (s *service) Modify(ctx context.Context, id int64, in ModifyInput) error { updater := s.writeDB.Channels.WithContext(ctx).Where(s.writeDB.Channels.ID.Eq(id)) set := map[string]any{} if in.Name != nil { set["name"] = *in.Name } if in.Type != nil { set["type"] = *in.Type } if in.Remarks != nil { set["remarks"] = *in.Remarks } if len(set) == 0 { return nil } _, err := updater.Updates(set) return err } func (s *service) Delete(ctx context.Context, id int64) error { _, err := s.writeDB.Channels.WithContext(ctx).Where(s.writeDB.Channels.ID.Eq(id)).Delete() return err } func (s *service) List(ctx context.Context, in ListInput) (items []*ChannelWithStat, total int64, err error) { if in.Page <= 0 { in.Page = 1 } if in.PageSize <= 0 { in.PageSize = 20 } q := s.readDB.Channels.WithContext(ctx) if in.Name != "" { q = q.Where(s.readDB.Channels.Name.Like("%" + in.Name + "%")) } total, err = q.Count() if err != nil { return } // List channels channels, err := q.Order(s.readDB.Channels.ID.Desc()).Limit(in.PageSize).Offset((in.Page - 1) * in.PageSize).Find() if err != nil { return } // Get user counts var channelIDs []int64 for _, c := range channels { channelIDs = append(channelIDs, c.ID) } stats := make(map[int64]int64) if len(channelIDs) > 0 { type Result struct { ChannelID int64 Count int64 } var results []Result // Using raw query for grouping err = s.readDB.Users.WithContext(ctx).UnderlyingDB().Table("users"). Select("channel_id, count(*) as count"). Where("channel_id IN ?", channelIDs). Group("channel_id"). Scan(&results).Error if err == nil { for _, r := range results { stats[r.ChannelID] = r.Count } } } for _, c := range channels { items = append(items, &ChannelWithStat{ Channels: c, UserCount: stats[c.ID], }) } return } func (s *service) GetStats(ctx context.Context, channelID int64, months int) (*StatsOutput, error) { if months <= 0 { months = 12 } now := time.Now() // Calculate start date (first day of the month N months ago) startMonth := now.AddDate(0, -months+1, 0) startDate := time.Date(startMonth.Year(), startMonth.Month(), 1, 0, 0, 0, 0, startMonth.Location()) out := &StatsOutput{} // 1. Overview // Users userCount, _ := s.readDB.Users.WithContext(ctx).Where(s.readDB.Users.ChannelID.Eq(channelID)).Count() out.Overview.TotalUsers = userCount // Orders & GMV type OrderStat struct { Count int64 GMV int64 } var os OrderStat s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders"). Joins("JOIN users ON users.id = orders.user_id"). Select("count(*) as count, coalesce(sum(actual_amount),0) as gmv"). Where("users.channel_id = ? AND orders.status = 2", channelID). Scan(&os) out.Overview.TotalOrders = os.Count out.Overview.TotalGMV = os.GMV // 2. Monthly Stats dateMap := make(map[string]*StatsDailyItem) var dateList []string for i := 0; i < months; i++ { d := startDate.AddDate(0, i, 0).Format("2006-01") dateList = append(dateList, d) dateMap[d] = &StatsDailyItem{Date: d} } // Monthly Users type MonthlyUser struct { Date string Count int64 } var monthlyUsers []MonthlyUser s.readDB.Users.WithContext(ctx).UnderlyingDB().Table("users"). Select("DATE_FORMAT(created_at, '%Y-%m') as date, count(*) as count"). Where("channel_id = ? AND created_at >= ?", channelID, startDate). Group("date").Scan(&monthlyUsers) for _, u := range monthlyUsers { if item, ok := dateMap[u.Date]; ok { item.UserCount = u.Count } } // Monthly Orders type MonthlyOrder struct { Date string Count int64 GMV int64 } var monthlyOrders []MonthlyOrder s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders"). Joins("JOIN users ON users.id = orders.user_id"). Select("DATE_FORMAT(orders.created_at, '%Y-%m') as date, count(*) as count, coalesce(sum(actual_amount),0) as gmv"). Where("users.channel_id = ? AND orders.status = 2 AND orders.created_at >= ?", channelID, startDate). Group("date").Scan(&monthlyOrders) for _, o := range monthlyOrders { if item, ok := dateMap[o.Date]; ok { item.OrderCount = o.Count item.GMV = o.GMV } } for _, d := range dateList { out.Daily = append(out.Daily, *dateMap[d]) } return out, nil }