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

183 lines
6.4 KiB
Go

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"`
// 邀请统计
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"` // 订单数
} `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"` // 持有道具卡价值
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)
// 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
}
var os orderStats
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)
rsp.LifetimeStats.TotalPaid = os.TotalPaid
rsp.LifetimeStats.OrderCount = os.OrderCount
// 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
// 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 总资产估值
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
rsp.CurrentAssets.InventoryValue +
rsp.CurrentAssets.CouponValue +
rsp.CurrentAssets.ItemCardValue
// 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)
}
}