174 lines
5.6 KiB
Go
174 lines
5.6 KiB
Go
package admin
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"bindbox-game/internal/code"
|
||
"bindbox-game/internal/pkg/core"
|
||
"bindbox-game/internal/pkg/validation"
|
||
)
|
||
|
||
type userProfitLossRequest struct {
|
||
RangeType string `form:"rangeType"`
|
||
Granularity string `form:"granularity"`
|
||
}
|
||
|
||
type userProfitLossPoint struct {
|
||
Date string `json:"date"`
|
||
Cost int64 `json:"cost"` // 净支出(仅已支付未退款订单)
|
||
Value int64 `json:"value"` // 当前资产快照(实时)
|
||
Profit int64 `json:"profit"` // 净盈亏
|
||
Ratio float64 `json:"ratio"` // 盈亏比
|
||
Breakdown struct {
|
||
Products int64 `json:"products"`
|
||
Points int64 `json:"points"`
|
||
Cards int64 `json:"cards"`
|
||
Coupons int64 `json:"coupons"`
|
||
} `json:"breakdown"`
|
||
}
|
||
|
||
type userProfitLossResponse struct {
|
||
Granularity string `json:"granularity"`
|
||
List []userProfitLossPoint `json:"list"`
|
||
CurrentAssets struct {
|
||
Points int64 `json:"points"`
|
||
Products int64 `json:"products"`
|
||
Cards int64 `json:"cards"`
|
||
Coupons int64 `json:"coupons"`
|
||
Total int64 `json:"total"`
|
||
} `json:"currentAssets"`
|
||
}
|
||
|
||
func (h *handler) GetUserProfitLossTrend() 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
|
||
}
|
||
|
||
req := new(userProfitLossRequest)
|
||
if err := ctx.ShouldBindForm(req); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||
return
|
||
}
|
||
|
||
start, end := parseRange(req.RangeType, "", "")
|
||
if req.RangeType == "all" {
|
||
u, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(userID)).First()
|
||
if u != nil {
|
||
start = u.CreatedAt
|
||
} else {
|
||
start = time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local)
|
||
}
|
||
}
|
||
gran := normalizeGranularity(req.Granularity)
|
||
buckets := buildBuckets(start, end, gran)
|
||
|
||
if len(buckets) > 1500 {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "范围过大"))
|
||
return
|
||
}
|
||
|
||
// --- 1. 获取当前资产快照(实时余额)---
|
||
var curAssets struct {
|
||
Points int64
|
||
Products int64
|
||
Cards int64
|
||
Coupons int64
|
||
}
|
||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(points), 0) FROM user_points WHERE user_id = ? AND (valid_end IS NULL OR valid_end > NOW())", userID).Scan(&curAssets.Points).Error
|
||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(p.price), 0) FROM user_inventory ui LEFT JOIN products p ON p.id = ui.product_id WHERE ui.user_id = ? AND ui.status = 1", userID).Scan(&curAssets.Products).Error
|
||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(sc.price), 0) FROM user_item_cards uic LEFT JOIN system_item_cards sc ON sc.id = uic.card_id WHERE uic.user_id = ? AND uic.status = 1", userID).Scan(&curAssets.Cards).Error
|
||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(balance_amount), 0) FROM user_coupons WHERE user_id = ? AND status = 1", userID).Scan(&curAssets.Coupons).Error
|
||
totalAssetValue := curAssets.Points + curAssets.Products + curAssets.Cards + curAssets.Coupons
|
||
|
||
// --- 2. 获取订单数据(仅 status=2 已支付未退款)---
|
||
orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||
Where(h.readDB.Orders.Status.Eq(2)). // 仅已支付,不含已退款
|
||
Where(h.readDB.Orders.CreatedAt.Gte(start)).
|
||
Where(h.readDB.Orders.CreatedAt.Lte(end)).
|
||
Find()
|
||
|
||
// --- 3. 按时间分桶计算 ---
|
||
list := make([]userProfitLossPoint, len(buckets))
|
||
|
||
inBucket := func(t time.Time, b bucket) bool {
|
||
return (t.After(b.Start) || t.Equal(b.Start)) && t.Before(b.End)
|
||
}
|
||
|
||
var cumulativeCost int64 = 0
|
||
|
||
for i, b := range buckets {
|
||
p := &list[i]
|
||
p.Date = b.Label
|
||
|
||
// 计算该时间段内的支出
|
||
var periodCost int64 = 0
|
||
for _, o := range orderRows {
|
||
if inBucket(o.CreatedAt, b) {
|
||
periodCost += o.ActualAmount
|
||
}
|
||
}
|
||
cumulativeCost += periodCost
|
||
p.Cost = periodCost
|
||
|
||
// 使用当前资产快照作为产出值(最后一个桶显示完整值,其他桶按比例或显示0)
|
||
// 简化:所有桶都显示当前快照值,让用户一眼看到当前状态
|
||
p.Value = totalAssetValue
|
||
p.Breakdown.Points = curAssets.Points
|
||
p.Breakdown.Products = curAssets.Products
|
||
p.Breakdown.Cards = curAssets.Cards
|
||
p.Breakdown.Coupons = curAssets.Coupons
|
||
|
||
p.Profit = p.Value - p.Cost
|
||
if p.Cost > 0 {
|
||
p.Ratio = float64(p.Value) / float64(p.Cost)
|
||
} else if p.Value > 0 {
|
||
p.Ratio = 99.9
|
||
}
|
||
}
|
||
|
||
// 计算累计值用于汇总显示
|
||
var totalCost int64 = 0
|
||
for _, o := range orderRows {
|
||
totalCost += o.ActualAmount
|
||
}
|
||
|
||
// 最后一个桶使用累计成本
|
||
if len(list) > 0 {
|
||
lastIdx := len(list) - 1
|
||
// 汇总数据:使用累计成本和当前资产值
|
||
list[lastIdx].Cost = totalCost
|
||
list[lastIdx].Value = totalAssetValue
|
||
list[lastIdx].Profit = totalAssetValue - totalCost
|
||
if totalCost > 0 {
|
||
list[lastIdx].Ratio = float64(totalAssetValue) / float64(totalCost)
|
||
} else if totalAssetValue > 0 {
|
||
list[lastIdx].Ratio = 99.9
|
||
}
|
||
}
|
||
|
||
ctx.Payload(userProfitLossResponse{
|
||
Granularity: gran,
|
||
List: list,
|
||
CurrentAssets: struct {
|
||
Points int64 `json:"points"`
|
||
Products int64 `json:"products"`
|
||
Cards int64 `json:"cards"`
|
||
Coupons int64 `json:"coupons"`
|
||
Total int64 `json:"total"`
|
||
}{
|
||
Points: curAssets.Points,
|
||
Products: curAssets.Products,
|
||
Cards: curAssets.Cards,
|
||
Coupons: curAssets.Coupons,
|
||
Total: totalAssetValue,
|
||
},
|
||
})
|
||
}
|
||
}
|