183 lines
6.4 KiB
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)
|
|
}
|
|
}
|