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

249 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
)
// UserProfileResponse 用户综合画像
type UserProfileResponse struct {
// 基本信息
ID int64 `json:"id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Mobile string `json:"mobile"`
InviteCode string `json:"invite_code"`
InviterID int64 `json:"inviter_id"`
ChannelID int64 `json:"channel_id"`
CreatedAt string `json:"created_at"`
DouyinID string `json:"douyin_id"`
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
// 邀请统计
InviteCount int64 `json:"invite_count"`
// 生命周期财务指标
LifetimeStats struct {
TotalPaid int64 `json:"total_paid"` // 累计支付
TotalRefunded int64 `json:"total_refunded"` // 累计退款
NetCashCost int64 `json:"net_cash_cost"` // 净现金支出
OrderCount int64 `json:"order_count"` // 订单数
TodayPaid int64 `json:"today_paid"` // 当日支付
SevenDayPaid int64 `json:"seven_day_paid"` // 近7天支付
ThirtyDayPaid int64 `json:"thirty_day_paid"` // 近30天支付
} `json:"lifetime_stats"`
// 当前资产快照
CurrentAssets struct {
PointsBalance int64 `json:"points_balance"` // 积分余额
InventoryCount int64 `json:"inventory_count"` // 持有商品数
InventoryValue int64 `json:"inventory_value"` // 持有商品价值
CouponCount int64 `json:"coupon_count"` // 持有优惠券数
CouponValue int64 `json:"coupon_value"` // 持有优惠券价值
ItemCardCount int64 `json:"item_card_count"` // 持有道具卡数
ItemCardValue int64 `json:"item_card_value"` // 持有道具卡价值
GamePassCount int64 `json:"game_pass_count"` // 持有次数卡数
GameTicketCount int64 `json:"game_ticket_count"` // 持有游戏资格数
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
ProfitLossRatio float64 `json:"profit_loss_ratio"` // 累计盈亏比
} `json:"current_assets"`
}
// GetUserProfile 获取用户综合画像
// @Summary 获取用户综合画像
// @Description 聚合用户基本信息、生命周期财务指标、当前资产快照
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Success 200 {object} UserProfileResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/profile [get]
// @Security LoginVerifyToken
func (h *handler) GetUserProfile() 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
}
rsp := new(UserProfileResponse)
// 1. 基本信息
user, err := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(userID)).First()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, 20201, "用户不存在"))
return
}
rsp.ID = user.ID
rsp.Nickname = user.Nickname
rsp.Avatar = user.Avatar
rsp.Mobile = user.Mobile
rsp.InviteCode = user.InviteCode
rsp.InviterID = user.InviterID
rsp.ChannelID = user.ChannelID
rsp.DouyinID = user.DouyinID
rsp.CreatedAt = user.CreatedAt.Format(time.RFC3339)
// 1.1 查询邀请人昵称
if user.InviterID > 0 {
inviter, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(user.InviterID)).First()
if inviter != nil {
rsp.InviterNickname = inviter.Nickname
}
}
// 2. 邀请统计
rsp.InviteCount, _ = h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.InviterID.Eq(userID)).Count()
// 3. 生命周期财务指标
// 3.1 消费统计
type orderStats struct {
TotalPaid int64
OrderCount int64
TodayPaid int64
SevenDayPaid int64
ThirtyDayPaid int64
}
var os orderStats
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
sevenDayStart := todayStart.AddDate(0, 0, -6)
thirtyDayStart := todayStart.AddDate(0, 0, -29)
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
Select(
h.readDB.Orders.ActualAmount.Sum().As("total_paid"),
h.readDB.Orders.ID.Count().As("order_count"),
).
Where(h.readDB.Orders.UserID.Eq(userID)).
Where(h.readDB.Orders.Status.Eq(2)).
Scan(&os)
// 分阶段统计
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
Select(h.readDB.Orders.ActualAmount.Sum().As("today_paid")).
Where(h.readDB.Orders.UserID.Eq(userID)).
Where(h.readDB.Orders.Status.Eq(2)).
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
Scan(&os.TodayPaid)
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
Select(h.readDB.Orders.ActualAmount.Sum().As("seven_day_paid")).
Where(h.readDB.Orders.UserID.Eq(userID)).
Where(h.readDB.Orders.Status.Eq(2)).
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
Scan(&os.SevenDayPaid)
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
Select(h.readDB.Orders.ActualAmount.Sum().As("thirty_day_paid")).
Where(h.readDB.Orders.UserID.Eq(userID)).
Where(h.readDB.Orders.Status.Eq(2)).
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
Scan(&os.ThirtyDayPaid)
rsp.LifetimeStats.TotalPaid = os.TotalPaid
rsp.LifetimeStats.OrderCount = os.OrderCount
rsp.LifetimeStats.TodayPaid = os.TodayPaid
rsp.LifetimeStats.SevenDayPaid = os.SevenDayPaid
rsp.LifetimeStats.ThirtyDayPaid = os.ThirtyDayPaid
// 3.2 累计退款
var totalRefunded int64
_ = h.repo.GetDbR().Raw(`
SELECT COALESCE(SUM(pr.amount_refund), 0)
FROM payment_refunds pr
JOIN orders o ON o.order_no = pr.order_no COLLATE utf8mb4_unicode_ci
WHERE o.user_id = ? AND pr.status = 'SUCCESS'
`, userID).Scan(&totalRefunded).Error
rsp.LifetimeStats.TotalRefunded = totalRefunded
// 净现金投入 = 累计实付 - 累计退款
rsp.LifetimeStats.NetCashCost = rsp.LifetimeStats.TotalPaid - totalRefunded
if rsp.LifetimeStats.NetCashCost < 0 {
rsp.LifetimeStats.NetCashCost = 0
}
// 4. 当前资产快照
// 4.1 积分余额
var pointsBalance int64
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(points), 0) FROM user_points WHERE user_id = ?", userID).Scan(&pointsBalance).Error
rsp.CurrentAssets.PointsBalance = pointsBalance
// 4.2 持有商品
type invStats struct {
Count int64
Value int64
}
var is invStats
h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
LeftJoin(h.readDB.Products, h.readDB.Products.ID.EqCol(h.readDB.UserInventory.ProductID)).
Select(h.readDB.UserInventory.ID.Count().As("count"), h.readDB.Products.Price.Sum().As("value")).
Where(h.readDB.UserInventory.UserID.Eq(userID)).
Where(h.readDB.UserInventory.Status.Eq(1)).
Scan(&is)
rsp.CurrentAssets.InventoryCount = is.Count
rsp.CurrentAssets.InventoryValue = is.Value
// 4.3 持有优惠券
type cpStats struct {
Count int64
Value int64
}
var cs cpStats
_ = h.repo.GetDbR().Raw(`
SELECT COUNT(*) AS count, COALESCE(SUM(balance_amount), 0) AS value
FROM user_coupons
WHERE user_id = ? AND status = 1
`, userID).Scan(&cs).Error
rsp.CurrentAssets.CouponCount = cs.Count
rsp.CurrentAssets.CouponValue = cs.Value
// 4.4 持有道具卡
type cardStats struct {
Count int64
Value int64
}
var cds cardStats
h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB().
LeftJoin(h.readDB.SystemItemCards, h.readDB.SystemItemCards.ID.EqCol(h.readDB.UserItemCards.CardID)).
Select(h.readDB.UserItemCards.ID.Count().As("count"), h.readDB.SystemItemCards.Price.Sum().As("value")).
Where(h.readDB.UserItemCards.UserID.Eq(userID)).
Where(h.readDB.UserItemCards.Status.Eq(1)).
Scan(&cds)
rsp.CurrentAssets.ItemCardCount = cds.Count
rsp.CurrentAssets.ItemCardValue = cds.Value
// 4.5 持有次数卡
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(remaining), 0) FROM user_game_passes WHERE user_id = ? AND remaining > 0 AND (expired_at IS NULL OR expired_at > NOW())", userID).Scan(&rsp.CurrentAssets.GamePassCount).Error
// 4.6 持有游戏资格
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(available), 0) FROM user_game_tickets WHERE user_id = ?", userID).Scan(&rsp.CurrentAssets.GameTicketCount).Error
// 4.5 总资产估值
// 估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次) + 游戏资格(1元/场)
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值2元/次
gameTicketValue := rsp.CurrentAssets.GameTicketCount * 100 // 估值1元/场
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
rsp.CurrentAssets.InventoryValue +
rsp.CurrentAssets.CouponValue +
rsp.CurrentAssets.ItemCardValue +
gamePassValue +
gameTicketValue
// 4.6 累计盈亏比
if rsp.LifetimeStats.NetCashCost > 0 {
rsp.CurrentAssets.ProfitLossRatio = float64(rsp.CurrentAssets.TotalAssetValue) / float64(rsp.LifetimeStats.NetCashCost)
} else if rsp.CurrentAssets.TotalAssetValue > 0 {
rsp.CurrentAssets.ProfitLossRatio = 99.9 // 无成本但有资产
}
ctx.Payload(rsp)
}
}