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) } }