fix(dashboard): 按有效抽奖口径修正产品动销排行
本次仅调整产品动销排行接口的后端统计逻辑,不改动活动盈亏分析页面与接口。 详细说明: - 将销量从 activity_draw_logs 总条数改为有效抽奖次数,仅统计 orders.status = 2 的抽奖记录,避免把退款、取消等无效抽奖一并计入。 - 将营收统一改为“有效抽奖次数 × 活动单抽价格(price_draw)”,不再使用总日志数乘单价,确保产品动销排行内部次数与营收口径一致。 - 将成本从 price_snapshot_cents / products.price 的近似标价口径改为 products.cost_price 成本价口径,并继续保留 drop_quantity 与倍数卡 multiplier 的影响,避免利润长期被标价成本压低。 - 将盈亏与利润率统一为 Revenue - Cost 与 Profit / Revenue,使产品动销排行内部的销量、营收、成本、盈亏逻辑自洽。 - 将贡献率从按次数占比调整为按营收占比,更符合“营收贡献率”的业务含义。 本次未处理内容: - 不修改活动盈亏分析的现有统计口径。 - 不修改前端卡片结构与活动盈亏分析页面。 已核对的业务结论: - 活动 103 的产品动销排行 4696 次是最近 30 天有效抽奖次数;活动盈亏分析 5960 次是全量历史有效抽奖次数,两边差异来自时间范围而非当前产品动销排行次数公式。
This commit is contained in:
parent
400cd68d00
commit
e6e4214df4
@ -1855,18 +1855,44 @@ type productPerformanceItem struct {
|
||||
func (h *handler) OperationsProductPerformance() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
s, e := parseRange(ctx.Request().URL.Query().Get("rangeType"), "", "")
|
||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
||||
|
||||
// 按活动聚合抽奖数据
|
||||
type drawRow struct {
|
||||
ActivityID int64 `gorm:"column:activity_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
TotalCost int64 `gorm:"column:total_cost"`
|
||||
type performanceRow struct {
|
||||
ActivityID int64 `gorm:"column:activity_id"`
|
||||
SalesCount int64 `gorm:"column:sales_count"`
|
||||
PaymentCount int64 `gorm:"column:payment_count"`
|
||||
GamePassCount int64 `gorm:"column:game_pass_count"`
|
||||
TotalCost int64 `gorm:"column:total_cost"`
|
||||
PriceDraw int64 `gorm:"column:price_draw"`
|
||||
RevenueCents int64 `gorm:"column:revenue_cents"`
|
||||
ContributionBase int64 `gorm:"column:contribution_base"`
|
||||
}
|
||||
var rows []drawRow
|
||||
var rows []performanceRow
|
||||
|
||||
// 统计抽奖日志,按活动分组,并计算奖品成本
|
||||
if err := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).UnderlyingDB().
|
||||
if err := db.Table(model.TableNameActivityDrawLogs).
|
||||
Select(`
|
||||
activity_issues.activity_id,
|
||||
SUM(CASE WHEN orders.status = 2 AND (
|
||||
orders.source_type = 4
|
||||
OR orders.order_no LIKE 'GP%'
|
||||
OR (orders.actual_amount = 0 AND COALESCE(orders.remark, '') LIKE '%use_game_pass%')
|
||||
) THEN 1 ELSE 0 END) as game_pass_count,
|
||||
SUM(CASE WHEN orders.status = 2 AND NOT (
|
||||
orders.source_type = 4
|
||||
OR orders.order_no LIKE 'GP%'
|
||||
OR (orders.actual_amount = 0 AND COALESCE(orders.remark, '') LIKE '%use_game_pass%')
|
||||
) THEN 1 ELSE 0 END) as payment_count,
|
||||
SUM(CASE WHEN orders.status = 2 THEN 1 ELSE 0 END) as sales_count,
|
||||
COALESCE(MAX(activities.price_draw), 0) as price_draw,
|
||||
SUM(CASE WHEN orders.status = 2 THEN COALESCE(activities.price_draw, 0) ELSE 0 END) as revenue_cents,
|
||||
SUM(CASE WHEN orders.status = 2 THEN COALESCE(activities.price_draw, 0) ELSE 0 END) as contribution_base,
|
||||
CAST(SUM(CASE WHEN orders.status = 2 THEN COALESCE(products.cost_price, 0) * (
|
||||
COALESCE(NULLIF(activity_reward_settings.drop_quantity, 0), 1) +
|
||||
CASE WHEN user_item_cards.used_draw_log_id = activity_draw_logs.id AND system_item_cards.effect_type = 1 AND system_item_cards.reward_multiplier_x1000 >= 2000 THEN 1 ELSE 0 END
|
||||
) ELSE 0 END) AS SIGNED) as total_cost
|
||||
`).
|
||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||
Joins("JOIN activities ON activities.id = activity_issues.activity_id").
|
||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
||||
Joins("LEFT JOIN products ON products.id = activity_reward_settings.product_id").
|
||||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||
@ -1874,41 +1900,33 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc {
|
||||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
|
||||
Where("activity_draw_logs.created_at >= ?", s).
|
||||
Where("activity_draw_logs.created_at <= ?", e).
|
||||
Select(
|
||||
"activity_issues.activity_id",
|
||||
"COUNT(activity_draw_logs.id) as count",
|
||||
"CAST(SUM(IF(activity_draw_logs.is_winner = 1, COALESCE(activity_reward_settings.price_snapshot_cents, products.price, 0) * GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000, 0)) AS SIGNED) as total_cost",
|
||||
).
|
||||
Group("activity_issues.activity_id").
|
||||
Order("count DESC").
|
||||
Order("sales_count DESC").
|
||||
Limit(10).
|
||||
Scan(&rows).Error; err != nil {
|
||||
h.logger.Error(fmt.Sprintf("OperationsProductPerformance draw cost stats error: %v", err))
|
||||
h.logger.Error(fmt.Sprintf("OperationsProductPerformance stats error: %v", err))
|
||||
}
|
||||
|
||||
// 获取活动详情(名称和单价)
|
||||
activityIDs := make([]int64, len(rows))
|
||||
for i, r := range rows {
|
||||
activityIDs[i] = r.ActivityID
|
||||
}
|
||||
|
||||
type actInfo struct {
|
||||
Name string
|
||||
PriceDraw int64
|
||||
Name string
|
||||
}
|
||||
actMap := make(map[int64]actInfo)
|
||||
if len(activityIDs) > 0 {
|
||||
acts, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Where(h.readDB.Activities.ID.In(activityIDs...)).Find()
|
||||
for _, a := range acts {
|
||||
actMap[a.ID] = actInfo{Name: a.Name, PriceDraw: a.PriceDraw}
|
||||
actMap[a.ID] = actInfo{Name: a.Name}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总数用于贡献率
|
||||
var totalCount int64
|
||||
var totalRevenueCents int64
|
||||
for _, r := range rows {
|
||||
totalCount += r.Count
|
||||
totalRevenueCents += r.ContributionBase
|
||||
}
|
||||
|
||||
out := make([]productPerformanceItem, len(rows))
|
||||
@ -1916,30 +1934,29 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc {
|
||||
info := actMap[r.ActivityID]
|
||||
|
||||
var contribution float64
|
||||
if totalCount > 0 {
|
||||
contribution = float64(r.Count) / float64(totalCount) * 100
|
||||
if totalRevenueCents > 0 {
|
||||
contribution = float64(r.ContributionBase) / float64(totalRevenueCents) * 100
|
||||
}
|
||||
|
||||
// 周转率简化计算
|
||||
days := e.Sub(s).Hours() / 24
|
||||
if days < 1 {
|
||||
days = 1
|
||||
}
|
||||
turnover := float64(r.Count) / days * 7
|
||||
turnover := float64(r.SalesCount) / days * 7
|
||||
|
||||
profitCents := r.RevenueCents - r.TotalCost
|
||||
out[i] = productPerformanceItem{
|
||||
ID: r.ActivityID,
|
||||
SeriesName: info.Name,
|
||||
SalesCount: r.Count,
|
||||
Amount: (r.Count * info.PriceDraw) / 100, // 转换为元
|
||||
Profit: (r.Count*info.PriceDraw - r.TotalCost) / 100,
|
||||
SalesCount: r.SalesCount,
|
||||
Amount: r.RevenueCents / 100,
|
||||
Profit: profitCents / 100,
|
||||
ProfitRate: 0,
|
||||
ContributionRate: float64(int(contribution*10)) / 10.0,
|
||||
InventoryTurnover: float64(int(turnover*10)) / 10.0,
|
||||
}
|
||||
if r.Count > 0 && info.PriceDraw > 0 {
|
||||
revenue := r.Count * info.PriceDraw
|
||||
pr := float64(revenue-r.TotalCost) / float64(revenue) * 100
|
||||
if r.RevenueCents > 0 {
|
||||
pr := float64(profitCents) / float64(r.RevenueCents) * 100
|
||||
out[i].ProfitRate = float64(int(pr*10)) / 10.0
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user