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>
This commit is contained in:
parent
d45096d13f
commit
91dd42ca1c
145
cmd/channel_stats_compare/profit_loss.go
Normal file
145
cmd/channel_stats_compare/profit_loss.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
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("【道具卡使用详情】无道具卡使用记录")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ type OrderRemark struct {
|
|||||||
IssueID int64
|
IssueID int64
|
||||||
Count int64
|
Count int64
|
||||||
ItemCardID int64
|
ItemCardID int64
|
||||||
|
PkgID int64 // 一番赏游戏包 ID (game_pass_packages.id)
|
||||||
Slots []SlotInfo
|
Slots []SlotInfo
|
||||||
Coupons []CouponInfo
|
Coupons []CouponInfo
|
||||||
}
|
}
|
||||||
@ -60,6 +61,8 @@ func Parse(remark string) *OrderRemark {
|
|||||||
} else if strings.HasPrefix(p, "slots:") {
|
} else if strings.HasPrefix(p, "slots:") {
|
||||||
// 处理多个 slots:X:Y,X:Y
|
// 处理多个 slots:X:Y,X:Y
|
||||||
r.Slots = append(r.Slots, parseSlots(p[6:])...)
|
r.Slots = append(r.Slots, parseSlots(p[6:])...)
|
||||||
|
} else if strings.HasPrefix(p, "pkg_id:") {
|
||||||
|
r.PkgID = parseInt64(p[7:])
|
||||||
} else if strings.HasPrefix(p, "c:") {
|
} else if strings.HasPrefix(p, "c:") {
|
||||||
// 处理优惠券 c:ID:AMT
|
// 处理优惠券 c:ID:AMT
|
||||||
r.Coupons = append(r.Coupons, parseCoupon(p[2:]))
|
r.Coupons = append(r.Coupons, parseCoupon(p[2:]))
|
||||||
|
|||||||
171
internal/pkg/util/remark/remark_test.go
Normal file
171
internal/pkg/util/remark/remark_test.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package remark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse_Lottery(t *testing.T) {
|
||||||
|
// 抽奖订单: lottery:activity:82|issue:89|count:1|slots:15:1
|
||||||
|
rm := Parse("lottery:activity:82|issue:89|count:1|slots:15:1")
|
||||||
|
if rm.ActivityID != 82 {
|
||||||
|
t.Errorf("ActivityID = %d, want 82", rm.ActivityID)
|
||||||
|
}
|
||||||
|
if rm.IssueID != 89 {
|
||||||
|
t.Errorf("IssueID = %d, want 89", rm.IssueID)
|
||||||
|
}
|
||||||
|
if rm.Count != 1 {
|
||||||
|
t.Errorf("Count = %d, want 1", rm.Count)
|
||||||
|
}
|
||||||
|
if rm.PkgID != 0 {
|
||||||
|
t.Errorf("PkgID = %d, want 0", rm.PkgID)
|
||||||
|
}
|
||||||
|
if len(rm.Slots) != 1 || rm.Slots[0].SlotIndex != 15 {
|
||||||
|
t.Errorf("Slots = %+v, want [{SlotIndex:15 Count:1}]", rm.Slots)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_MatchingGamePaid(t *testing.T) {
|
||||||
|
// 对对碰付费: matching_game:issue:104|coupon:267|c:267:500
|
||||||
|
rm := Parse("matching_game:issue:104|coupon:267|c:267:500")
|
||||||
|
if rm.ActivityID != 0 {
|
||||||
|
t.Errorf("ActivityID = %d, want 0 (matching game paid has no activity prefix)", rm.ActivityID)
|
||||||
|
}
|
||||||
|
if rm.IssueID != 104 {
|
||||||
|
t.Errorf("IssueID = %d, want 104", rm.IssueID)
|
||||||
|
}
|
||||||
|
if rm.PkgID != 0 {
|
||||||
|
t.Errorf("PkgID = %d, want 0", rm.PkgID)
|
||||||
|
}
|
||||||
|
if rm.Count != 1 {
|
||||||
|
t.Errorf("Count = %d, want 1 (default)", rm.Count)
|
||||||
|
}
|
||||||
|
if len(rm.Coupons) != 1 || rm.Coupons[0].UserCouponID != 267 || rm.Coupons[0].AppliedAmount != 500 {
|
||||||
|
t.Errorf("Coupons = %+v, want [{UserCouponID:267 AppliedAmount:500}]", rm.Coupons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_MatchingGameFree(t *testing.T) {
|
||||||
|
// 对对碰免费(game_pass): activity:50|game_pass:12|matching_game:issue:96
|
||||||
|
rm := Parse("activity:50|game_pass:12|matching_game:issue:96")
|
||||||
|
if rm.ActivityID != 50 {
|
||||||
|
t.Errorf("ActivityID = %d, want 50", rm.ActivityID)
|
||||||
|
}
|
||||||
|
if rm.IssueID != 96 {
|
||||||
|
t.Errorf("IssueID = %d, want 96", rm.IssueID)
|
||||||
|
}
|
||||||
|
if rm.PkgID != 0 {
|
||||||
|
t.Errorf("PkgID = %d, want 0", rm.PkgID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_Ichiban(t *testing.T) {
|
||||||
|
// 一番赏: game_pass_package:梦的起点!|pkg_id:11|count:4
|
||||||
|
rm := Parse("game_pass_package:梦的起点!|pkg_id:11|count:4")
|
||||||
|
if rm.PkgID != 11 {
|
||||||
|
t.Errorf("PkgID = %d, want 11", rm.PkgID)
|
||||||
|
}
|
||||||
|
if rm.Count != 4 {
|
||||||
|
t.Errorf("Count = %d, want 4", rm.Count)
|
||||||
|
}
|
||||||
|
if rm.ActivityID != 0 {
|
||||||
|
t.Errorf("ActivityID = %d, want 0", rm.ActivityID)
|
||||||
|
}
|
||||||
|
if rm.IssueID != 0 {
|
||||||
|
t.Errorf("IssueID = %d, want 0", rm.IssueID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_IchibanSingle(t *testing.T) {
|
||||||
|
// 一番赏单包: game_pass_package:幸运一番|pkg_id:5|count:1
|
||||||
|
rm := Parse("game_pass_package:幸运一番|pkg_id:5|count:1")
|
||||||
|
if rm.PkgID != 5 {
|
||||||
|
t.Errorf("PkgID = %d, want 5", rm.PkgID)
|
||||||
|
}
|
||||||
|
if rm.Count != 1 {
|
||||||
|
t.Errorf("Count = %d, want 1", rm.Count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_Empty(t *testing.T) {
|
||||||
|
rm := Parse("")
|
||||||
|
if rm.ActivityID != 0 || rm.IssueID != 0 || rm.PkgID != 0 || rm.Count != 1 {
|
||||||
|
t.Errorf("empty remark should have zero IDs and count=1, got %+v", rm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_ActivityOnly(t *testing.T) {
|
||||||
|
// activity: 前缀(直购)
|
||||||
|
rm := Parse("activity:30|issue:45|count:2")
|
||||||
|
if rm.ActivityID != 30 {
|
||||||
|
t.Errorf("ActivityID = %d, want 30", rm.ActivityID)
|
||||||
|
}
|
||||||
|
if rm.IssueID != 45 {
|
||||||
|
t.Errorf("IssueID = %d, want 45", rm.IssueID)
|
||||||
|
}
|
||||||
|
if rm.Count != 2 {
|
||||||
|
t.Errorf("Count = %d, want 2", rm.Count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_ClassificationPriority(t *testing.T) {
|
||||||
|
// 验证三路分类逻辑:
|
||||||
|
// Case 1: ActivityID > 0 → 抽奖/直购
|
||||||
|
// Case 2: ActivityID==0 && IssueID > 0 → 对对碰
|
||||||
|
// Case 3: ActivityID==0 && IssueID==0 && PkgID > 0 → 一番赏
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
remark string
|
||||||
|
wantCase string
|
||||||
|
activityID int64
|
||||||
|
issueID int64
|
||||||
|
pkgID int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "lottery → case1",
|
||||||
|
remark: "lottery:activity:10|issue:20|count:1",
|
||||||
|
wantCase: "case1",
|
||||||
|
activityID: 10, issueID: 20, pkgID: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matching_game paid → case2",
|
||||||
|
remark: "matching_game:issue:50",
|
||||||
|
wantCase: "case2",
|
||||||
|
activityID: 0, issueID: 50, pkgID: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ichiban → case3",
|
||||||
|
remark: "game_pass_package:test|pkg_id:7|count:2",
|
||||||
|
wantCase: "case3",
|
||||||
|
activityID: 0, issueID: 0, pkgID: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
rm := Parse(tt.remark)
|
||||||
|
if rm.ActivityID != tt.activityID {
|
||||||
|
t.Errorf("ActivityID = %d, want %d", rm.ActivityID, tt.activityID)
|
||||||
|
}
|
||||||
|
if rm.IssueID != tt.issueID {
|
||||||
|
t.Errorf("IssueID = %d, want %d", rm.IssueID, tt.issueID)
|
||||||
|
}
|
||||||
|
if rm.PkgID != tt.pkgID {
|
||||||
|
t.Errorf("PkgID = %d, want %d", rm.PkgID, tt.pkgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证分类
|
||||||
|
var gotCase string
|
||||||
|
if rm.ActivityID > 0 {
|
||||||
|
gotCase = "case1"
|
||||||
|
} else if rm.IssueID > 0 {
|
||||||
|
gotCase = "case2"
|
||||||
|
} else if rm.PkgID > 0 {
|
||||||
|
gotCase = "case3"
|
||||||
|
}
|
||||||
|
if gotCase != tt.wantCase {
|
||||||
|
t.Errorf("classification = %s, want %s", gotCase, tt.wantCase)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bindbox-game/internal/pkg/logger"
|
"bindbox-game/internal/pkg/logger"
|
||||||
|
"bindbox-game/internal/pkg/util/remark"
|
||||||
"bindbox-game/internal/repository/mysql"
|
"bindbox-game/internal/repository/mysql"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
@ -69,18 +70,24 @@ type StatsOutput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StatsOverview struct {
|
type StatsOverview struct {
|
||||||
TotalUsers int64 `json:"total_users"`
|
TotalUsers int64 `json:"total_users"`
|
||||||
TotalOrders int64 `json:"total_orders"`
|
TotalOrders int64 `json:"total_orders"`
|
||||||
TotalGMV int64 `json:"total_gmv"`
|
TotalGMV int64 `json:"total_gmv"`
|
||||||
TotalPaidCents int64 `json:"total_paid_cents"`
|
TotalPaidCents int64 `json:"total_paid_cents"`
|
||||||
|
TotalCostCents int64 `json:"total_cost_cents"` // 总成本(分)
|
||||||
|
TotalProfitCents int64 `json:"total_profit_cents"` // 盈亏(分) = paid - cost
|
||||||
|
TotalCost int64 `json:"total_cost"` // 总成本(元)
|
||||||
|
TotalProfit int64 `json:"total_profit"` // 盈亏(元)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatsDailyItem struct {
|
type StatsDailyItem struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
UserCount int64 `json:"user_count"`
|
UserCount int64 `json:"user_count"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"order_count"`
|
||||||
GMV int64 `json:"gmv"`
|
GMV int64 `json:"gmv"`
|
||||||
PaidCents int64 `json:"paid_cents"`
|
PaidCents int64 `json:"paid_cents"`
|
||||||
|
CostCents int64 `json:"cost_cents"` // 当日成本(分)
|
||||||
|
ProfitCents int64 `json:"profit_cents"` // 当日盈亏(分)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchUsersInput struct {
|
type SearchUsersInput struct {
|
||||||
@ -122,6 +129,213 @@ var (
|
|||||||
ErrSearchKeywordEmpty = errors.New("search_keyword_empty")
|
ErrSearchKeywordEmpty = errors.New("search_keyword_empty")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type orderRemarkRow struct {
|
||||||
|
Remark string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcPaidByPriceDraw 解析订单 remark,按游戏类型分三路计算实付金额:
|
||||||
|
// - Case 1 (抽奖/直购): ActivityID > 0 → activities.price_draw × count
|
||||||
|
// - Case 2 (对对碰): IssueID > 0 → activity_issues → activities.price_draw × count
|
||||||
|
// - Case 3 (一番赏): PkgID > 0 → game_pass_packages.price × count
|
||||||
|
//
|
||||||
|
// 返回:总金额(分)、按 dateFmt 格式分组的金额。
|
||||||
|
func (s *service) calcPaidByPriceDraw(ctx context.Context, rows []orderRemarkRow, dateFmt string) (int64, map[string]int64) {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type parsedActivity struct {
|
||||||
|
activityID int64
|
||||||
|
count int64
|
||||||
|
dateKey string
|
||||||
|
}
|
||||||
|
type parsedIssue struct {
|
||||||
|
issueID int64
|
||||||
|
count int64
|
||||||
|
dateKey string
|
||||||
|
}
|
||||||
|
type parsedPkg struct {
|
||||||
|
pkgID int64
|
||||||
|
count int64
|
||||||
|
dateKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
var actItems []parsedActivity
|
||||||
|
var issueItems []parsedIssue
|
||||||
|
var pkgItems []parsedPkg
|
||||||
|
|
||||||
|
actIDSet := make(map[int64]struct{})
|
||||||
|
issueIDSet := make(map[int64]struct{})
|
||||||
|
pkgIDSet := make(map[int64]struct{})
|
||||||
|
|
||||||
|
for _, r := range rows {
|
||||||
|
rmk := remark.Parse(r.Remark)
|
||||||
|
dateKey := r.CreatedAt.Format(dateFmt)
|
||||||
|
|
||||||
|
if rmk.ActivityID > 0 {
|
||||||
|
// Case 1: 抽奖/直购 — 直接有 activityID
|
||||||
|
actItems = append(actItems, parsedActivity{rmk.ActivityID, rmk.Count, dateKey})
|
||||||
|
actIDSet[rmk.ActivityID] = struct{}{}
|
||||||
|
} else if rmk.IssueID > 0 {
|
||||||
|
// Case 2: 对对碰付费路径 — 只有 issueID,需查 activity_issues
|
||||||
|
issueItems = append(issueItems, parsedIssue{rmk.IssueID, rmk.Count, dateKey})
|
||||||
|
issueIDSet[rmk.IssueID] = struct{}{}
|
||||||
|
} else if rmk.PkgID > 0 {
|
||||||
|
// Case 3: 一番赏 — 有 pkgID,需查 game_pass_packages
|
||||||
|
pkgItems = append(pkgItems, parsedPkg{rmk.PkgID, rmk.Count, dateKey})
|
||||||
|
pkgIDSet[rmk.PkgID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Case 2: 批量查 activity_issues → 拿到 activityID ──
|
||||||
|
issueActivityMap := make(map[int64]int64) // issueID → activityID
|
||||||
|
if len(issueIDSet) > 0 {
|
||||||
|
issueIDs := make([]int64, 0, len(issueIDSet))
|
||||||
|
for id := range issueIDSet {
|
||||||
|
issueIDs = append(issueIDs, id)
|
||||||
|
}
|
||||||
|
type issueRow struct {
|
||||||
|
ID int64
|
||||||
|
ActivityID int64
|
||||||
|
}
|
||||||
|
var issueRows []issueRow
|
||||||
|
s.readDB.ActivityIssues.WithContext(ctx).UnderlyingDB().
|
||||||
|
Table("activity_issues").
|
||||||
|
Select("id, activity_id").
|
||||||
|
Where("id IN ?", issueIDs).
|
||||||
|
Scan(&issueRows)
|
||||||
|
for _, ir := range issueRows {
|
||||||
|
issueActivityMap[ir.ID] = ir.ActivityID
|
||||||
|
actIDSet[ir.ActivityID] = struct{}{} // 合并到 actIDSet 一起查 price_draw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Case 1+2: 批量查 activities.price_draw(含软删除)──
|
||||||
|
priceMap := make(map[int64]int64) // activityID → price_draw
|
||||||
|
if len(actIDSet) > 0 {
|
||||||
|
actIDs := make([]int64, 0, len(actIDSet))
|
||||||
|
for id := range actIDSet {
|
||||||
|
actIDs = append(actIDs, id)
|
||||||
|
}
|
||||||
|
var acts []model.Activities
|
||||||
|
s.readDB.Activities.WithContext(ctx).UnderlyingDB().
|
||||||
|
Unscoped().
|
||||||
|
Table("activities").
|
||||||
|
Select("id, price_draw").
|
||||||
|
Where("id IN ?", actIDs).
|
||||||
|
Find(&acts)
|
||||||
|
for _, a := range acts {
|
||||||
|
priceMap[a.ID] = a.PriceDraw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Case 3: 批量查 game_pass_packages.price ──
|
||||||
|
pkgPriceMap := make(map[int64]int64) // pkgID → price
|
||||||
|
if len(pkgIDSet) > 0 {
|
||||||
|
pkgIDs := make([]int64, 0, len(pkgIDSet))
|
||||||
|
for id := range pkgIDSet {
|
||||||
|
pkgIDs = append(pkgIDs, id)
|
||||||
|
}
|
||||||
|
type pkgRow struct {
|
||||||
|
ID int64
|
||||||
|
Price int64
|
||||||
|
}
|
||||||
|
var pkgRows []pkgRow
|
||||||
|
s.readDB.Activities.WithContext(ctx).UnderlyingDB().
|
||||||
|
Unscoped().
|
||||||
|
Table("game_pass_packages").
|
||||||
|
Select("id, price").
|
||||||
|
Where("id IN ?", pkgIDs).
|
||||||
|
Scan(&pkgRows)
|
||||||
|
for _, pr := range pkgRows {
|
||||||
|
pkgPriceMap[pr.ID] = pr.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 累加金额 ──
|
||||||
|
var total int64
|
||||||
|
byDate := make(map[string]int64)
|
||||||
|
|
||||||
|
// Case 1: 抽奖/直购
|
||||||
|
for _, item := range actItems {
|
||||||
|
if price, ok := priceMap[item.activityID]; ok {
|
||||||
|
amt := price * item.count
|
||||||
|
total += amt
|
||||||
|
byDate[item.dateKey] += amt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: 对对碰
|
||||||
|
for _, item := range issueItems {
|
||||||
|
if actID, ok := issueActivityMap[item.issueID]; ok {
|
||||||
|
if price, ok := priceMap[actID]; ok {
|
||||||
|
amt := price * item.count
|
||||||
|
total += amt
|
||||||
|
byDate[item.dateKey] += amt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: 一番赏
|
||||||
|
for _, item := range pkgItems {
|
||||||
|
if price, ok := pkgPriceMap[item.pkgID]; ok {
|
||||||
|
amt := price * item.count
|
||||||
|
total += amt
|
||||||
|
byDate[item.dateKey] += amt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, byDate
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcCostByInventory 计算渠道用户获得奖品的成本(含道具卡倍数)。
|
||||||
|
// 成本 = SUM(奖品价值 × 道具卡倍数)
|
||||||
|
// 奖品价值优先级: user_inventory.value_cents → activity_reward_settings.price_snapshot_cents → products.price
|
||||||
|
// 道具卡倍数: system_item_cards.reward_multiplier_x1000 / 1000,无卡时 ×1.0
|
||||||
|
func (s *service) calcCostByInventory(ctx context.Context, channelID int64, dateFmt string, startDate, endDate *time.Time) (int64, map[string]int64) {
|
||||||
|
type costRow struct {
|
||||||
|
UnitCost int64
|
||||||
|
Multiplier int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
q := s.readDB.UserInventory.WithContext(ctx).UnderlyingDB().
|
||||||
|
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,
|
||||||
|
user_inventory.created_at
|
||||||
|
`).
|
||||||
|
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)")
|
||||||
|
|
||||||
|
if startDate != nil && endDate != nil {
|
||||||
|
q = q.Where("user_inventory.created_at >= ? AND user_inventory.created_at <= ?", *startDate, *endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []costRow
|
||||||
|
q.Scan(&rows)
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
byDate := make(map[string]int64)
|
||||||
|
for _, r := range rows {
|
||||||
|
cost := r.UnitCost * r.Multiplier / 1000
|
||||||
|
total += cost
|
||||||
|
byDate[r.CreatedAt.Format(dateFmt)] += cost
|
||||||
|
}
|
||||||
|
return total, byDate
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) Create(ctx context.Context, in CreateInput) (*model.Channels, error) {
|
func (s *service) Create(ctx context.Context, in CreateInput) (*model.Channels, error) {
|
||||||
m := &model.Channels{Name: in.Name, Code: in.Code, Type: in.Type, Remarks: in.Remarks}
|
m := &model.Channels{Name: in.Name, Code: in.Code, Type: in.Type, Remarks: in.Remarks}
|
||||||
if err := s.writeDB.Channels.WithContext(ctx).Create(m); err != nil {
|
if err := s.writeDB.Channels.WithContext(ctx).Create(m); err != nil {
|
||||||
@ -206,19 +420,26 @@ func (s *service) List(ctx context.Context, in ListInput) (items []*ChannelWithS
|
|||||||
|
|
||||||
type PaidResult struct {
|
type PaidResult struct {
|
||||||
ChannelID int64
|
ChannelID int64
|
||||||
Paid int64
|
Remark string
|
||||||
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
var paidResults []PaidResult
|
var paidResults []PaidResult
|
||||||
err = s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
err = s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||||
Joins("JOIN users ON users.id = orders.user_id").
|
Joins("JOIN users ON users.id = orders.user_id").
|
||||||
Select("users.channel_id as channel_id, coalesce(sum(orders.actual_amount),0) as paid").
|
Select("users.channel_id, orders.remark, orders.created_at").
|
||||||
Where("users.channel_id IN ?", channelIDs).
|
Where("users.channel_id IN ?", channelIDs).
|
||||||
Where("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)").
|
Where("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)").
|
||||||
Group("users.channel_id").
|
|
||||||
Scan(&paidResults).Error
|
Scan(&paidResults).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
grouped := make(map[int64][]orderRemarkRow)
|
||||||
for _, r := range paidResults {
|
for _, r := range paidResults {
|
||||||
paidStats[r.ChannelID] = r.Paid
|
grouped[r.ChannelID] = append(grouped[r.ChannelID], orderRemarkRow{
|
||||||
|
Remark: r.Remark, CreatedAt: r.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for chID, rows := range grouped {
|
||||||
|
total, _ := s.calcPaidByPriceDraw(ctx, rows, "2006-01-02")
|
||||||
|
paidStats[chID] = total
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,10 +456,8 @@ func (s *service) List(ctx context.Context, in ListInput) (items []*ChannelWithS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetStats(ctx context.Context, channelID int64, months int, startDateStr, endDateStr string) (*StatsOutput, error) {
|
func (s *service) GetStats(ctx context.Context, channelID int64, days int, startDateStr, endDateStr string) (*StatsOutput, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
var startDate, endDate time.Time
|
|
||||||
var startBucket, endBucket time.Time
|
|
||||||
|
|
||||||
_, err := s.readDB.Channels.WithContext(ctx).Where(s.readDB.Channels.ID.Eq(channelID)).First()
|
_, err := s.readDB.Channels.WithContext(ctx).Where(s.readDB.Channels.ID.Eq(channelID)).First()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -248,102 +467,113 @@ func (s *service) GetStats(ctx context.Context, channelID int64, months int, sta
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果指定了日期范围,使用自定义日期
|
|
||||||
if startDateStr != "" && endDateStr != "" {
|
|
||||||
var err error
|
|
||||||
startDate, err = time.Parse("2006-01-02", startDateStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
endDate, err = time.Parse("2006-01-02", endDateStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 确保 endDate 是当天结束
|
|
||||||
endDate = endDate.Add(24*time.Hour - time.Second)
|
|
||||||
startBucket = time.Date(startDate.Year(), startDate.Month(), 1, 0, 0, 0, 0, startDate.Location())
|
|
||||||
endBucket = time.Date(endDate.Year(), endDate.Month(), 1, 0, 0, 0, 0, endDate.Location())
|
|
||||||
months = (endBucket.Year()-startBucket.Year())*12 + int(endBucket.Month()-startBucket.Month()) + 1
|
|
||||||
if months <= 0 {
|
|
||||||
months = 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 默认按月份计算
|
|
||||||
if months <= 0 {
|
|
||||||
months = 12
|
|
||||||
}
|
|
||||||
startMonth := now.AddDate(0, -months+1, 0)
|
|
||||||
startDate = time.Date(startMonth.Year(), startMonth.Month(), 1, 0, 0, 0, 0, startMonth.Location())
|
|
||||||
endDate = now
|
|
||||||
startBucket = startDate
|
|
||||||
}
|
|
||||||
|
|
||||||
out := &StatsOutput{}
|
out := &StatsOutput{}
|
||||||
|
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)"
|
||||||
|
|
||||||
|
// ========== 1. Overview(全量,不限时间)==========
|
||||||
|
|
||||||
// 1. Overview
|
|
||||||
// Users
|
|
||||||
userCount, _ := s.readDB.Users.WithContext(ctx).Where(s.readDB.Users.ChannelID.Eq(channelID)).Count()
|
userCount, _ := s.readDB.Users.WithContext(ctx).Where(s.readDB.Users.ChannelID.Eq(channelID)).Count()
|
||||||
out.Overview.TotalUsers = userCount
|
out.Overview.TotalUsers = userCount
|
||||||
|
|
||||||
// Orders & GMV
|
type countResult struct{ Count int64 }
|
||||||
type OrderStat struct {
|
var cr countResult
|
||||||
Count int64
|
|
||||||
GMV int64
|
|
||||||
}
|
|
||||||
var os OrderStat
|
|
||||||
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||||
Joins("JOIN users ON users.id = orders.user_id").
|
Joins("JOIN users ON users.id = orders.user_id").
|
||||||
Select("count(*) as count, coalesce(sum(actual_amount),0) as gmv").
|
Select("count(*) as count").
|
||||||
Where("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)", channelID).
|
Where(orderFilter, channelID).
|
||||||
Scan(&os)
|
Scan(&cr)
|
||||||
out.Overview.TotalOrders = os.Count
|
out.Overview.TotalOrders = cr.Count
|
||||||
out.Overview.TotalGMV = os.GMV / 100
|
|
||||||
out.Overview.TotalPaidCents = os.GMV
|
|
||||||
|
|
||||||
// 2. Monthly Stats
|
var allRemarks []orderRemarkRow
|
||||||
dateMap := make(map[string]*StatsDailyItem)
|
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||||
var dateList []string
|
Joins("JOIN users ON users.id = orders.user_id").
|
||||||
for i := 0; i < months; i++ {
|
Select("orders.remark, orders.created_at").
|
||||||
d := startBucket.AddDate(0, i, 0).Format("2006-01")
|
Where(orderFilter, channelID).
|
||||||
dateList = append(dateList, d)
|
Scan(&allRemarks)
|
||||||
dateMap[d] = &StatsDailyItem{Date: d}
|
|
||||||
|
totalPaid, _ := s.calcPaidByPriceDraw(ctx, allRemarks, "2006-01-02")
|
||||||
|
out.Overview.TotalPaidCents = totalPaid
|
||||||
|
out.Overview.TotalGMV = totalPaid / 100
|
||||||
|
|
||||||
|
// 1d. 累计成本(全量,含道具卡倍数)
|
||||||
|
totalCost, _ := s.calcCostByInventory(ctx, channelID, "2006-01-02", nil, nil)
|
||||||
|
out.Overview.TotalCostCents = totalCost
|
||||||
|
out.Overview.TotalCost = totalCost / 100
|
||||||
|
out.Overview.TotalProfitCents = totalPaid - totalCost
|
||||||
|
out.Overview.TotalProfit = out.Overview.TotalProfitCents / 100
|
||||||
|
|
||||||
|
// ========== 2. 趋势图(按天分组,受 days 限制)==========
|
||||||
|
|
||||||
|
var startDate, endDate time.Time
|
||||||
|
if startDateStr != "" && endDateStr != "" {
|
||||||
|
startDate, _ = time.Parse("2006-01-02", startDateStr)
|
||||||
|
endDate, _ = time.Parse("2006-01-02", endDateStr)
|
||||||
|
endDate = endDate.Add(24*time.Hour - time.Second)
|
||||||
|
} else {
|
||||||
|
if days <= 0 {
|
||||||
|
days = 12
|
||||||
|
}
|
||||||
|
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -days+1)
|
||||||
|
endDate = now
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monthly Users
|
dateMap := make(map[string]*StatsDailyItem)
|
||||||
type MonthlyUser struct {
|
var dateList []string
|
||||||
|
for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) {
|
||||||
|
key := d.Format("2006-01-02")
|
||||||
|
dateList = append(dateList, key)
|
||||||
|
dateMap[key] = &StatsDailyItem{Date: key}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dailyCount struct {
|
||||||
Date string
|
Date string
|
||||||
Count int64
|
Count int64
|
||||||
}
|
}
|
||||||
var monthlyUsers []MonthlyUser
|
|
||||||
s.readDB.Users.WithContext(ctx).UnderlyingDB().Table("users").
|
|
||||||
Select("DATE_FORMAT(created_at, '%Y-%m') as date, count(*) as count").
|
|
||||||
Where("channel_id = ? AND deleted_at IS NULL AND created_at >= ? AND created_at <= ?", channelID, startDate, endDate).
|
|
||||||
Group("date").Scan(&monthlyUsers)
|
|
||||||
|
|
||||||
for _, u := range monthlyUsers {
|
var dailyUsers []dailyCount
|
||||||
|
s.readDB.Users.WithContext(ctx).UnderlyingDB().Table("users").
|
||||||
|
Select("DATE_FORMAT(created_at, '%Y-%m-%d') as date, count(*) as count").
|
||||||
|
Where("channel_id = ? AND deleted_at IS NULL AND created_at >= ? AND created_at <= ?", channelID, startDate, endDate).
|
||||||
|
Group("date").Scan(&dailyUsers)
|
||||||
|
for _, u := range dailyUsers {
|
||||||
if item, ok := dateMap[u.Date]; ok {
|
if item, ok := dateMap[u.Date]; ok {
|
||||||
item.UserCount = u.Count
|
item.UserCount = u.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monthly Orders
|
var dailyOrders []dailyCount
|
||||||
type MonthlyOrder struct {
|
|
||||||
Date string
|
|
||||||
Count int64
|
|
||||||
GMV int64
|
|
||||||
}
|
|
||||||
var monthlyOrders []MonthlyOrder
|
|
||||||
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||||
Joins("JOIN users ON users.id = orders.user_id").
|
Joins("JOIN users ON users.id = orders.user_id").
|
||||||
Select("DATE_FORMAT(orders.created_at, '%Y-%m') as date, count(*) as count, coalesce(sum(actual_amount),0) as gmv").
|
Select("DATE_FORMAT(orders.created_at, '%Y-%m-%d') as date, count(*) as count").
|
||||||
Where("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) AND orders.created_at >= ? AND orders.created_at <= ?", channelID, startDate, endDate).
|
Where(orderFilter+" AND orders.created_at >= ? AND orders.created_at <= ?", channelID, startDate, endDate).
|
||||||
Group("date").Scan(&monthlyOrders)
|
Group("date").Scan(&dailyOrders)
|
||||||
|
for _, o := range dailyOrders {
|
||||||
for _, o := range monthlyOrders {
|
|
||||||
if item, ok := dateMap[o.Date]; ok {
|
if item, ok := dateMap[o.Date]; ok {
|
||||||
item.OrderCount = o.Count
|
item.OrderCount = o.Count
|
||||||
item.GMV = o.GMV / 100
|
}
|
||||||
item.PaidCents = o.GMV
|
}
|
||||||
|
|
||||||
|
var rangeRemarks []orderRemarkRow
|
||||||
|
s.readDB.Orders.WithContext(ctx).UnderlyingDB().Table("orders").
|
||||||
|
Joins("JOIN users ON users.id = orders.user_id").
|
||||||
|
Select("orders.remark, orders.created_at").
|
||||||
|
Where(orderFilter+" AND orders.created_at >= ? AND orders.created_at <= ?", channelID, startDate, endDate).
|
||||||
|
Scan(&rangeRemarks)
|
||||||
|
|
||||||
|
_, dailyPaid := s.calcPaidByPriceDraw(ctx, rangeRemarks, "2006-01-02")
|
||||||
|
for dateKey, paid := range dailyPaid {
|
||||||
|
if item, ok := dateMap[dateKey]; ok {
|
||||||
|
item.PaidCents = paid
|
||||||
|
item.GMV = paid / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2f. 每日成本(含道具卡倍数)
|
||||||
|
_, dailyCost := s.calcCostByInventory(ctx, channelID, "2006-01-02", &startDate, &endDate)
|
||||||
|
for dateKey, cost := range dailyCost {
|
||||||
|
if item, ok := dateMap[dateKey]; ok {
|
||||||
|
item.CostCents = cost
|
||||||
|
item.ProfitCents = item.PaidCents - cost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 7b8a8766c6b607976b3856cd3775a795873f085d
|
Subproject commit 212aacb5f8dac05b214a4c19eed4395ed7922943
|
||||||
Loading…
x
Reference in New Issue
Block a user