bindbox-game/internal/api/admin/dashboard_user_spending.go

250 lines
7.6 KiB
Go
Executable File

package admin
import (
"fmt"
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type userSpendingRequest struct {
RangeType string `form:"rangeType"`
StartDate string `form:"start"`
EndDate string `form:"end"`
}
// userActivitySpending 用户在具体活动实例上的消费统计
type userActivitySpending struct {
ActivityID int64 `json:"activity_id"`
ActivityName string `json:"activity_name"`
CategoryID int64 `json:"category_id"`
CategoryName string `json:"category_name"` // 一番赏/盲盒/对对碰/直播间
Spending int64 `json:"spending"` // 消费金额(分)
PrizeValue int64 `json:"prize_value"` // 产出价值(分)
Profit int64 `json:"profit"` // 收益(分)
OrderCount int64 `json:"order_count"` // 订单数
}
type userSpendingResponse struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
TotalSpend int64 `json:"total_spend"`
TotalPrize int64 `json:"total_prize"`
TotalProfit int64 `json:"total_profit"`
TotalOrders int64 `json:"total_orders"`
Activities []userActivitySpending `json:"activities"`
}
var categoryNames = map[int64]string{
1: "一番赏",
2: "盲盒/无限",
3: "对对碰",
}
func (h *handler) GetUserSpendingDashboard() core.HandlerFunc {
return func(ctx core.Context) {
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
req := new(userSpendingRequest)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
var start, end time.Time
hasRange := req.RangeType != "" && req.RangeType != "all"
if hasRange {
start, end = parseRange(req.RangeType, req.StartDate, req.EndDate)
}
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
rsp := &userSpendingResponse{UserID: userID}
// 获取用户基本信息
user, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.Eq(userID)).First()
if user != nil {
rsp.Nickname = user.Nickname
rsp.Avatar = user.Avatar
}
// 1. 按活动实例统计消费
type activityStat struct {
ActivityID int64
ActivityName string
CategoryID int64
Spending int64
OrderCount int64
}
var actStats []activityStat
query := db.Table(model.TableNameOrders).
Joins("LEFT JOIN activity_draw_logs ON activity_draw_logs.order_id = orders.id").
Joins("LEFT JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
Joins("LEFT JOIN activities ON activities.id = activity_issues.activity_id").
Where("orders.user_id = ?", userID).
Where("orders.status = ?", 2)
if hasRange {
query = query.Where("orders.created_at >= ?", start).Where("orders.created_at <= ?", end)
}
if err := query.Select(`
COALESCE(activities.id, 0) as activity_id,
COALESCE(activities.name, '其他') as activity_name,
COALESCE(activities.activity_category_id, 0) as category_id,
SUM(orders.total_amount) as spending,
COUNT(DISTINCT orders.id) as order_count
`).
Group("COALESCE(activities.id, 0)").
Order("spending DESC").
Scan(&actStats).Error; err != nil {
h.logger.Error(fmt.Sprintf("UserSpending SQL error: %v", err))
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21030, err.Error()))
return
}
// 2. 按活动实例统计产出价值
type prizeStat struct {
ActivityID int64
PrizeValue int64
}
var prizeStats []prizeStat
prizeQuery := db.Table(model.TableNameUserInventory).
Where("user_inventory.user_id = ?", userID).
Where("user_inventory.status IN ?", []int{1, 3}).
Where("user_inventory.remark NOT LIKE ?", "%void%")
if hasRange {
prizeQuery = prizeQuery.Where("user_inventory.created_at >= ?", start).Where("user_inventory.created_at <= ?", end)
}
prizeQuery.Select(`
COALESCE(user_inventory.activity_id, 0) as activity_id,
SUM(user_inventory.value_cents) as prize_value
`).
Group("COALESCE(user_inventory.activity_id, 0)").
Scan(&prizeStats)
prizeMap := make(map[int64]int64)
for _, p := range prizeStats {
prizeMap[p.ActivityID] = p.PrizeValue
}
// 3. 直播间消费统计
type livestreamStat struct {
ActivityID int64
ActivityName string
Spending int64
OrderCount int64
}
var lsStats []livestreamStat
lsQuery := db.Table("douyin_orders").
Joins("LEFT JOIN livestream_activities ON livestream_activities.id = douyin_orders.livestream_activity_id").
Select(`
COALESCE(douyin_orders.livestream_activity_id, 0) as activity_id,
COALESCE(livestream_activities.name, '直播间') as activity_name,
SUM(actual_pay_amount) as spending,
COUNT(*) as order_count
`).
Where("CAST(local_user_id AS SIGNED) = ?", userID).
Where("local_user_id != '' AND local_user_id != '0'")
if hasRange {
lsQuery = lsQuery.Where("douyin_orders.created_at >= ?", start).Where("douyin_orders.created_at <= ?", end)
}
lsQuery.Group("COALESCE(douyin_orders.livestream_activity_id, 0)").Scan(&lsStats)
// 直播间产出
type lsPrizeStat struct {
ActivityID int64
PrizeValue int64
}
var lsPrizeStats []lsPrizeStat
lsPrizeQuery := db.Table("livestream_draw_logs").
Joins("JOIN products ON products.id = livestream_draw_logs.product_id").
Select(`
livestream_draw_logs.livestream_activity_id as activity_id,
SUM(products.price) as prize_value
`).
Where("livestream_draw_logs.local_user_id = ?", userID).
Where("livestream_draw_logs.is_refunded = 0").
Where("livestream_draw_logs.product_id > 0")
if hasRange {
lsPrizeQuery = lsPrizeQuery.Where("livestream_draw_logs.created_at >= ?", start).Where("livestream_draw_logs.created_at <= ?", end)
}
lsPrizeQuery.Group("livestream_draw_logs.livestream_activity_id").Scan(&lsPrizeStats)
lsPrizeMap := make(map[int64]int64)
for _, p := range lsPrizeStats {
lsPrizeMap[p.ActivityID] = p.PrizeValue
}
// 4. 组装结果
activities := make([]userActivitySpending, 0)
var totalSpend, totalPrize, totalOrders int64
for _, s := range actStats {
prize := prizeMap[s.ActivityID]
catName := categoryNames[s.CategoryID]
if catName == "" {
catName = "其他"
}
item := userActivitySpending{
ActivityID: s.ActivityID,
ActivityName: s.ActivityName,
CategoryID: s.CategoryID,
CategoryName: catName,
Spending: s.Spending,
PrizeValue: prize,
Profit: s.Spending - prize,
OrderCount: s.OrderCount,
}
activities = append(activities, item)
totalSpend += s.Spending
totalPrize += prize
totalOrders += s.OrderCount
}
// 追加直播间活动
for _, ls := range lsStats {
prize := lsPrizeMap[ls.ActivityID]
item := userActivitySpending{
ActivityID: ls.ActivityID + 100000, // 避免和普通活动 ID 冲突
ActivityName: ls.ActivityName,
CategoryID: 4,
CategoryName: "直播间",
Spending: ls.Spending,
PrizeValue: prize,
Profit: ls.Spending - prize,
OrderCount: ls.OrderCount,
}
activities = append(activities, item)
totalSpend += ls.Spending
totalPrize += prize
totalOrders += ls.OrderCount
}
rsp.TotalSpend = totalSpend
rsp.TotalPrize = totalPrize
rsp.TotalProfit = totalSpend - totalPrize
rsp.TotalOrders = totalOrders
rsp.Activities = activities
ctx.Payload(rsp)
}
}