将直播间统计从基于 user_inventory 当前持有状态和 remark 反推成本,改为基于 livestream_draw_logs 中奖事实直接关联 products.cost_price 计算成本。统一 /livestream/activities/:id/stats 与 /livestream/activities/:id/draw_logs 两个接口的营收、退款、成本和净利润口径,避免因转赠、remark 覆盖或订单行缺失导致统计失真,并补充针对转赠、退款、零订单和 product 回退场景的回归测试。
88 lines
3.1 KiB
Go
Executable File
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,
|
|
})
|
|
}
|
|
}
|