266 lines
9.8 KiB
Go
Executable File
266 lines
9.8 KiB
Go
Executable File
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"`
|
||
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号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.DouyinUserID = user.DouyinUserID
|
||
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)).
|
||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||
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.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||
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.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||
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.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
||
Scan(&os.ThirtyDayPaid)
|
||
|
||
if os.TotalPaid != nil {
|
||
rsp.LifetimeStats.TotalPaid = *os.TotalPaid
|
||
}
|
||
rsp.LifetimeStats.OrderCount = os.OrderCount
|
||
if os.TodayPaid != nil {
|
||
rsp.LifetimeStats.TodayPaid = *os.TodayPaid
|
||
}
|
||
if os.SevenDayPaid != nil {
|
||
rsp.LifetimeStats.SevenDayPaid = *os.SevenDayPaid
|
||
}
|
||
if os.ThirtyDayPaid != nil {
|
||
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.repo.GetDbR().Raw(`
|
||
SELECT
|
||
COUNT(ui.id) as count,
|
||
COALESCE(SUM(COALESCE(NULLIF(ui.value_cents, 0), p.price, 0)), 0) as value
|
||
FROM user_inventory ui
|
||
LEFT JOIN products p ON p.id = ui.product_id
|
||
WHERE ui.user_id = ? AND ui.status = 1
|
||
`, userID).Scan(&is).Error
|
||
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元/次)
|
||
// 游戏资格不计入估值(购买其他商品赠送,无实际价值)
|
||
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值:2元/次
|
||
gameTicketValue := int64(0) // 游戏资格不计入估值
|
||
|
||
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)
|
||
}
|
||
}
|