邹方成 45815bfb7d chore: 清理无用文件与优化代码结构
refactor(utils): 修复密码哈希比较逻辑错误
feat(user): 新增按状态筛选优惠券接口
docs: 添加虚拟发货与任务中心相关文档
fix(wechat): 修正Code2Session上下文传递问题
test: 补充订单折扣与积分转换测试用例
build: 更新配置文件与构建脚本
style: 清理多余的空行与注释
2025-12-18 17:35:55 +08:00

245 lines
6.1 KiB
Go

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
}