win 91dd42ca1c feat(channel): 渠道统计新增盈亏计算并修复成本口径
后端:
- StatsOverview/StatsDailyItem 新增 cost/profit 字段
- 新增 calcPaidByPriceDraw 三路收入分类(抽奖/对对碰/一番赏)
- 新增 calcCostByInventory 成本计算(含道具卡倍数)
- 修复成本统计未过滤 source_type 导致直播间免费发奖资产被错误计入
- remark.go 新增 PkgID 解析支持一番赏订单

前端:
- 渠道统计弹窗新增"总成本"和"盈亏"卡片
- 趋势图新增"盈亏分析"Tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-11 02:29:19 +08:00

146 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"fmt"
"strings"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println("连接失败:", err)
return
}
channelID := 3
// 1. 成本统计(复用 calcCostByInventory 的 SQL 逻辑)
type costRow struct {
UnitCost int64
Multiplier int64
}
var rows []costRow
db.Table("user_inventory").
Select(`
COALESCE(NULLIF(user_inventory.value_cents, 0), activity_reward_settings.price_snapshot_cents, products.price, 0) AS unit_cost,
GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) AS multiplier
`).
Joins("JOIN users ON users.id = user_inventory.user_id").
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = user_inventory.reward_id").
Joins("LEFT JOIN products ON products.id = user_inventory.product_id").
Joins("LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id").
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
Where("users.channel_id = ? AND users.deleted_at IS NULL", channelID).
Where("user_inventory.status IN ?", []int{1, 3}).
Where("COALESCE(user_inventory.remark, '') NOT LIKE ?", "%void%").
Where("(orders.status = 2 OR user_inventory.order_id = 0 OR user_inventory.order_id IS NULL)").
Where("(orders.source_type IN (1,2,3,4) OR user_inventory.order_id = 0 OR user_inventory.order_id IS NULL)").
Scan(&rows)
var totalCostBase, totalCostFinal int64
var withCard, withoutCard int
for _, r := range rows {
cost := r.UnitCost * r.Multiplier / 1000
totalCostFinal += cost
totalCostBase += r.UnitCost
if r.Multiplier > 1000 {
withCard++
} else {
withoutCard++
}
}
fmt.Println("========================================")
fmt.Printf("渠道 %d 盈亏分析\n", channelID)
fmt.Println("========================================")
fmt.Println()
fmt.Println("【成本统计】")
fmt.Printf(" 资产记录数: %d 条\n", len(rows))
fmt.Printf(" 无道具卡: %d 条\n", withoutCard)
fmt.Printf(" 有道具卡: %d 条(成本×倍数)\n", withCard)
fmt.Printf(" 基础成本: %d 分 = %.2f 元\n", totalCostBase, float64(totalCostBase)/100)
fmt.Printf(" 含卡成本: %d 分 = %.2f 元\n", totalCostFinal, float64(totalCostFinal)/100)
if totalCostBase > 0 {
fmt.Printf(" 道具卡加成: +%.2f 元 (%.1f%%)\n",
float64(totalCostFinal-totalCostBase)/100,
float64(totalCostFinal-totalCostBase)/float64(totalCostBase)*100)
}
fmt.Println()
// 2. 收入统计(已有的 price_draw × count
// 简化:直接用 SQL 统计 actual_amount 作为对比参考
type amountResult struct {
TotalCents int64
}
var ar amountResult
orderFilter := "users.channel_id = ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.actual_amount > 0 AND orders.source_type IN (1,2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)"
db.Table("orders").
Joins("JOIN users ON users.id = orders.user_id").
Select("COALESCE(SUM(orders.actual_amount), 0) as total_cents").
Where(orderFilter, channelID).
Scan(&ar)
fmt.Println("【收入参考 (actual_amount)】")
fmt.Printf(" 实付金额: %d 分 = %.2f 元\n", ar.TotalCents, float64(ar.TotalCents)/100)
fmt.Println()
// 3. 盈亏
profit := ar.TotalCents - totalCostFinal
fmt.Println("【盈亏】")
fmt.Printf(" 收入(实付): %.2f 元\n", float64(ar.TotalCents)/100)
fmt.Printf(" 成本(含卡): %.2f 元\n", float64(totalCostFinal)/100)
fmt.Println(strings.Repeat("-", 40))
fmt.Printf(" 盈亏: %.2f 元\n", float64(profit)/100)
if profit > 0 {
fmt.Printf(" 状态: 盈利 ✅\n")
} else if profit < 0 {
fmt.Printf(" 状态: 亏损 ❌\n")
} else {
fmt.Printf(" 状态: 持平\n")
}
fmt.Println()
// 4. 道具卡详情
type cardDetail struct {
CardName string
Multiplier int64
Count int64
}
var cards []cardDetail
db.Table("user_inventory").
Select(`
system_item_cards.name as card_name,
system_item_cards.reward_multiplier_x1000 as multiplier,
COUNT(*) as count
`).
Joins("JOIN users ON users.id = user_inventory.user_id").
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
Joins("LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id").
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
Where("users.channel_id = ? AND users.deleted_at IS NULL", channelID).
Where("user_inventory.status IN ?", []int{1, 3}).
Where("COALESCE(user_inventory.remark, '') NOT LIKE ?", "%void%").
Where("(orders.status = 2 OR user_inventory.order_id = 0 OR user_inventory.order_id IS NULL)").
Where("(orders.source_type IN (1,2,3,4) OR user_inventory.order_id = 0 OR user_inventory.order_id IS NULL)").
Where("system_item_cards.id IS NOT NULL").
Group("system_item_cards.id").
Scan(&cards)
if len(cards) > 0 {
fmt.Println("【道具卡使用详情】")
fmt.Printf("%-20s %-10s %-10s\n", "卡名", "倍数", "次数")
fmt.Println(strings.Repeat("-", 40))
for _, c := range cards {
fmt.Printf("%-20s ×%.1f %-10d\n", c.CardName, float64(c.Multiplier)/1000, c.Count)
}
} else {
fmt.Println("【道具卡使用详情】无道具卡使用记录")
}
}