bindbox-game/internal/api/admin/livestream_stats.go
Zuncle c927f46cdd fix(livestream): 统一直播间盈亏成本口径并消除转赠影响
将直播间统计从基于 user_inventory 当前持有状态和 remark 反推成本,改为基于 livestream_draw_logs 中奖事实直接关联 products.cost_price 计算成本。统一 /livestream/activities/:id/stats 与 /livestream/activities/:id/draw_logs 两个接口的营收、退款、成本和净利润口径,避免因转赠、remark 覆盖或订单行缺失导致统计失真,并补充针对转赠、退款、零订单和 product 回退场景的回归测试。
2026-04-12 21:23:36 +08:00

88 lines
3.1 KiB
Go
Executable File

package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/repository/mysql/model"
)
type dailyLivestreamStats struct {
Date string `json:"date"` // 日期
TotalRevenue int64 `json:"total_revenue"` // 营收
TotalRefund int64 `json:"total_refund"` // 退款
TotalCost int64 `json:"total_cost"` // 成本
NetProfit int64 `json:"net_profit"` // 净利润
ProfitMargin float64 `json:"profit_margin"` // 利润率
OrderCount int64 `json:"order_count"` // 订单数
RefundCount int64 `json:"refund_count"` // 退款单数
}
type livestreamStatsResponse struct {
TotalRevenue int64 `json:"total_revenue"` // 总营收(分)
TotalRefund int64 `json:"total_refund"` // 总退款(分)
TotalCost int64 `json:"total_cost"` // 总成本(分)
NetProfit int64 `json:"net_profit"` // 净利润(分)
OrderCount int64 `json:"order_count"` // 订单数
RefundCount int64 `json:"refund_count"` // 退款数
ProfitMargin float64 `json:"profit_margin"` // 利润率 %
Daily []dailyLivestreamStats `json:"daily"` // 每日明细
}
// GetLivestreamStats 获取直播间盈亏统计
// @Summary 获取直播间盈亏统计
// @Description 计算逻辑:净利润 = (营收 - 退款) - 奖品成本。营收 = 抽奖次数 * 门票价格。成本 = 中奖奖品成本总和。
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} livestreamStatsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id}/stats [get]
// @Security LoginVerifyToken
func (h *handler) GetLivestreamStats() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
req := new(struct {
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
})
_ = ctx.ShouldBindQuery(req)
startTime, endTime := parseLivestreamDateRange(req.StartTime, req.EndTime, false)
var activity model.LivestreamActivities
if err := h.repo.GetDbR().Where("id = ?", id).First(&activity).Error; err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
return
}
metrics, err := h.buildLivestreamMetrics(livestreamMetricsFilter{
ActivityID: id,
StartTime: startTime,
EndTime: endTime,
}, int64(activity.TicketPrice))
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&livestreamStatsResponse{
TotalRevenue: metrics.TotalRevenue,
TotalRefund: metrics.TotalRefund,
TotalCost: metrics.TotalCost,
NetProfit: metrics.NetProfit,
OrderCount: metrics.OrderCount,
RefundCount: metrics.RefundCount,
ProfitMargin: metrics.ProfitMargin,
Daily: metrics.Daily,
})
}
}