fix(channel): align channel cost with draw-source attribution
Replace channel cost aggregation with draw-source based cost calculation that follows activity profit-loss logic and keeps cost attribution on the original ordering user's channel. Update channel stats tests to cover the new cost path and related schema fields.
This commit is contained in:
parent
c7a6e1e017
commit
03214dddf2
@ -234,6 +234,56 @@ func (s *service) calcCostByInventory(ctx context.Context, channelID int64, date
|
||||
return total, byDate
|
||||
}
|
||||
|
||||
// calcCostByDrawSource 按订单/抽奖来源统计渠道奖品成本。
|
||||
// 成本口径与活动盈亏保持一致:
|
||||
// - 来源:activity_draw_logs + activity_reward_settings + products.cost_price
|
||||
// - 数量:drop_quantity,默认 1
|
||||
// - 倍数:命中特定道具卡翻倍规则时 +1 份
|
||||
//
|
||||
// 注意:这里按 orders.user_id -> users.channel_id 归因,而不是按当前 user_inventory.user_id。
|
||||
// 这样 inventory 转赠后,成本仍归到原下单用户所属渠道。
|
||||
func (s *service) calcCostByDrawSource(ctx context.Context, channelID int64, dateFmt string, startDate, endDate *time.Time) (int64, map[string]int64) {
|
||||
type costRow struct {
|
||||
CostCents int64
|
||||
PaidAt time.Time
|
||||
}
|
||||
|
||||
q := s.readDB.ActivityDrawLogs.WithContext(ctx).UnderlyingDB().
|
||||
Table("activity_draw_logs").
|
||||
Select(`
|
||||
SUM(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
|
||||
)) AS cost_cents,
|
||||
orders.paid_at
|
||||
`).
|
||||
Joins("JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||
Joins("JOIN users ON users.id = orders.user_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 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("orders.status = 2").
|
||||
Where("orders.source_type IN ?", []int{2, 3, 4}).
|
||||
Where("orders.ext_order_id = '' OR orders.ext_order_id IS NULL")
|
||||
|
||||
if startDate != nil && endDate != nil {
|
||||
q = q.Where("orders.paid_at >= ? AND orders.paid_at <= ?", *startDate, *endDate)
|
||||
}
|
||||
|
||||
var rows []costRow
|
||||
q.Group("orders.id, orders.paid_at").Scan(&rows)
|
||||
|
||||
var total int64
|
||||
byDate := make(map[string]int64)
|
||||
for _, r := range rows {
|
||||
total += r.CostCents
|
||||
byDate[r.PaidAt.Format(dateFmt)] += r.CostCents
|
||||
}
|
||||
return total, byDate
|
||||
}
|
||||
|
||||
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}
|
||||
if err := s.writeDB.Channels.WithContext(ctx).Create(m); err != nil {
|
||||
@ -383,8 +433,8 @@ func (s *service) GetStats(ctx context.Context, channelID int64, days int, start
|
||||
out.Overview.CouponCents = totalGMV.Coupon
|
||||
out.Overview.PointsCents = totalGMV.Points
|
||||
|
||||
// 1d. 累计成本(全量,含道具卡倍数)
|
||||
totalCost, _ := s.calcCostByInventory(ctx, channelID, "2006-01-02", nil, nil)
|
||||
// 1d. 累计成本(全量,按原始订单/抽奖来源归因)
|
||||
totalCost, _ := s.calcCostByDrawSource(ctx, channelID, "2006-01-02", nil, nil)
|
||||
out.Overview.TotalCostCents = totalCost
|
||||
out.Overview.TotalCost = totalCost / 100
|
||||
out.Overview.TotalProfitCents = totalGMV.Total - totalCost
|
||||
@ -452,8 +502,8 @@ func (s *service) GetStats(ctx context.Context, channelID int64, days int, start
|
||||
}
|
||||
}
|
||||
|
||||
// 2f. 每日成本(含道具卡倍数)
|
||||
_, dailyCost := s.calcCostByInventory(ctx, channelID, "2006-01-02", &startDate, &endDate)
|
||||
// 2f. 每日成本(按原始订单/抽奖来源归因)
|
||||
_, dailyCost := s.calcCostByDrawSource(ctx, channelID, "2006-01-02", &startDate, &endDate)
|
||||
for dateKey, cost := range dailyCost {
|
||||
if item, ok := dateMap[dateKey]; ok {
|
||||
item.CostCents = cost
|
||||
|
||||
@ -58,6 +58,7 @@ func setupTestService(t *testing.T) (*service, mysql.Repo) {
|
||||
ext_order_id TEXT NOT NULL DEFAULT '',
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
item_card_id INTEGER DEFAULT 0,
|
||||
paid_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE user_inventory (
|
||||
@ -73,18 +74,29 @@ func setupTestService(t *testing.T) (*service, mysql.Repo) {
|
||||
)`,
|
||||
`CREATE TABLE activity_reward_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
price_snapshot_cents INTEGER DEFAULT 0
|
||||
product_id INTEGER DEFAULT 0,
|
||||
price_snapshot_cents INTEGER DEFAULT 0,
|
||||
drop_quantity INTEGER DEFAULT 1
|
||||
)`,
|
||||
`CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
price INTEGER DEFAULT 0
|
||||
price INTEGER DEFAULT 0,
|
||||
cost_price INTEGER DEFAULT 0
|
||||
)`,
|
||||
`CREATE TABLE activity_draw_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER DEFAULT 0,
|
||||
reward_id INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE user_item_cards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
card_id INTEGER DEFAULT 0
|
||||
card_id INTEGER DEFAULT 0,
|
||||
used_draw_log_id INTEGER DEFAULT 0
|
||||
)`,
|
||||
`CREATE TABLE system_item_cards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
effect_type INTEGER DEFAULT 0,
|
||||
reward_multiplier_x1000 INTEGER DEFAULT 1000
|
||||
)`,
|
||||
}
|
||||
@ -120,32 +132,22 @@ func TestCalcGMVByTotalAmount_ThreeGameTypes(t *testing.T) {
|
||||
|
||||
orderFilter := "users.channel_id = ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type IN (2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)"
|
||||
|
||||
// 抽奖订单 source=2,含优惠券200
|
||||
mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 2, 800, 1000, 200, 0, 2, '', 'lottery:activity:10|count:1')`)
|
||||
// 对对碰付费 source=3
|
||||
mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 2, 500, 500, 0, 0, 3, '', 'matching_game:issue:50')`)
|
||||
// 一番赏 source=4,含优惠券300
|
||||
mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 2, 500, 800, 300, 0, 4, '', 'game_pass_package:幸运|pkg_id:7|count:2')`)
|
||||
// 次卡免费使用:actual_amount=0 但 total_amount=600,不应计入GMV(避免与购买次卡重复计数)
|
||||
mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 2, 0, 600, 0, 0, 2, '', 'lottery:activity:10|count:1|use_game_pass')`)
|
||||
// 过滤条件:status!=2,不应计入
|
||||
mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 1, 9999, 9999, 0, 0, 2, '', 'lottery:activity:10|count:1')`)
|
||||
// 过滤条件:total_amount=0,不应计入
|
||||
mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 2, 0, 0, 0, 0, 2, '', 'lottery:activity:10|count:1')`)
|
||||
// 过滤条件:有 ext_order_id,不应计入
|
||||
mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 2, 9999, 9999, 0, 0, 2, 'EXT-1', 'lottery:activity:10|count:1')`)
|
||||
|
||||
total, byDate := svc.calcGMVByTotalAmount(context.Background(), 1, "2006-01-02", orderFilter, nil, nil)
|
||||
|
||||
// GMV = 1000 + 500 + 800 = 2300(次卡免费使用actual=0的600不计入)
|
||||
if total.Total != 2300 {
|
||||
t.Errorf("total.Total = %d, want 2300 (抽奖1000 + 对对碰500 + 一番赏800)", total.Total)
|
||||
}
|
||||
// 现金 = 800 + 500 + 500 = 1800
|
||||
if total.Cash != 1800 {
|
||||
t.Errorf("total.Cash = %d, want 1800", total.Cash)
|
||||
}
|
||||
// 优惠券 = 200 + 300 = 500
|
||||
if total.Coupon != 500 {
|
||||
t.Errorf("total.Coupon = %d, want 500", total.Coupon)
|
||||
}
|
||||
@ -157,7 +159,6 @@ func TestCalcGMVByTotalAmount_ThreeGameTypes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcGMVByTotalAmount_DateFilter 验证时间范围过滤正确。
|
||||
func TestCalcGMVByTotalAmount_DateFilter(t *testing.T) {
|
||||
svc, repo := setupTestService(t)
|
||||
|
||||
@ -175,20 +176,14 @@ func TestCalcGMVByTotalAmount_DateFilter(t *testing.T) {
|
||||
end = end.Add(24*time.Hour - time.Second)
|
||||
|
||||
total, byDate := svc.calcGMVByTotalAmount(context.Background(), 1, "2006-01-02", orderFilter, &start, &end)
|
||||
|
||||
// 只有 03-05 的 300 在范围内
|
||||
if total.Total != 300 {
|
||||
t.Errorf("total.Total = %d, want 300 (only 2026-03-05 order)", total.Total)
|
||||
}
|
||||
if byDate["2026-03-05"].Total != 300 {
|
||||
t.Errorf("byDate[2026-03-05].Total = %d, want 300", byDate["2026-03-05"].Total)
|
||||
}
|
||||
if byDate["2026-03-01"].Total != 0 && byDate["2026-03-10"].Total != 0 {
|
||||
t.Error("dates outside range should not appear")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcGMVByTotalAmount_MultiChannel 验证不同渠道数据互不干扰。
|
||||
func TestCalcGMVByTotalAmount_MultiChannel(t *testing.T) {
|
||||
svc, repo := setupTestService(t)
|
||||
|
||||
@ -212,93 +207,72 @@ func TestCalcGMVByTotalAmount_MultiChannel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcCostByInventory_Basic 验证成本从 value_cents 读取。
|
||||
func TestCalcCostByInventory_Basic(t *testing.T) {
|
||||
func TestCalcCostByDrawSource_TransferSafe(t *testing.T) {
|
||||
svc, repo := setupTestService(t)
|
||||
|
||||
mustExec(t, repo, `INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '渠道A', 'CA', 'other', ''), (2, '渠道B', 'CB', 'other', '')`)
|
||||
mustExec(t, repo, `INSERT INTO users (id, nickname, invite_code, status, channel_id) VALUES (1, 'u1', 'I1', 1, 1)`)
|
||||
mustExec(t, repo, `INSERT INTO users (id, nickname, invite_code, status, channel_id) VALUES (2, 'u2', 'I2', 1, 2)`)
|
||||
mustExec(t, repo, `INSERT INTO orders (id, user_id, status, actual_amount, total_amount, source_type, ext_order_id, paid_at, created_at) VALUES (10, 1, 2, 1000, 1000, 2, '', '2026-03-05 10:00:00', '2026-03-05 10:00:00')`)
|
||||
mustExec(t, repo, `INSERT INTO products (id, price, cost_price) VALUES (100, 1500, 900)`)
|
||||
mustExec(t, repo, `INSERT INTO activity_reward_settings (id, product_id, price_snapshot_cents, drop_quantity) VALUES (200, 100, 0, 1)`)
|
||||
mustExec(t, repo, `INSERT INTO activity_draw_logs (id, order_id, reward_id, created_at) VALUES (300, 10, 200, '2026-03-05 10:00:00')`)
|
||||
|
||||
totalA, byDateA := svc.calcCostByDrawSource(context.Background(), 1, "2006-01-02", nil, nil)
|
||||
totalB, _ := svc.calcCostByDrawSource(context.Background(), 2, "2006-01-02", nil, nil)
|
||||
|
||||
if totalA != 900 {
|
||||
t.Errorf("channel A cost = %d, want 900", totalA)
|
||||
}
|
||||
if totalB != 0 {
|
||||
t.Errorf("channel B cost = %d, want 0", totalB)
|
||||
}
|
||||
if byDateA["2026-03-05"] != 900 {
|
||||
t.Errorf("channel A cost on 2026-03-05 = %d, want 900", byDateA["2026-03-05"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcCostByDrawSource_DoubleRewardAndDropQuantity(t *testing.T) {
|
||||
svc, repo := setupTestService(t)
|
||||
|
||||
mustExec(t, repo, `INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '测试渠道', 'TEST', 'other', '')`)
|
||||
mustExec(t, repo, `INSERT INTO users (id, nickname, invite_code, status, channel_id) VALUES (1, 'u1', 'I1', 1, 1)`)
|
||||
mustExec(t, repo, `INSERT INTO system_item_cards (id, effect_type, reward_multiplier_x1000) VALUES (1, 1, 2000)`)
|
||||
mustExec(t, repo, `INSERT INTO user_item_cards (id, card_id, used_draw_log_id) VALUES (1, 1, 300)`)
|
||||
mustExec(t, repo, `INSERT INTO orders (id, user_id, status, actual_amount, total_amount, source_type, ext_order_id, item_card_id, paid_at, created_at) VALUES (10, 1, 2, 1000, 1000, 2, '', 1, '2026-03-05 10:00:00', '2026-03-05 10:00:00')`)
|
||||
mustExec(t, repo, `INSERT INTO products (id, price, cost_price) VALUES (100, 1500, 500)`)
|
||||
mustExec(t, repo, `INSERT INTO activity_reward_settings (id, product_id, price_snapshot_cents, drop_quantity) VALUES (200, 100, 0, 2)`)
|
||||
mustExec(t, repo, `INSERT INTO activity_draw_logs (id, order_id, reward_id, created_at) VALUES (300, 10, 200, '2026-03-05 10:00:00')`)
|
||||
|
||||
// status=1(待发货) 和 status=3(已发货) 都计入成本
|
||||
mustExec(t, repo, `INSERT INTO user_inventory (user_id, order_id, status, value_cents, remark) VALUES (1, 0, 1, 500, '')`)
|
||||
mustExec(t, repo, `INSERT INTO user_inventory (user_id, order_id, status, value_cents, remark) VALUES (1, 0, 3, 300, '')`)
|
||||
// status=2 不计入
|
||||
mustExec(t, repo, `INSERT INTO user_inventory (user_id, order_id, status, value_cents, remark) VALUES (1, 0, 2, 999, '')`)
|
||||
// remark含void 不计入
|
||||
mustExec(t, repo, `INSERT INTO user_inventory (user_id, order_id, status, value_cents, remark) VALUES (1, 0, 1, 888, 'void-item')`)
|
||||
|
||||
total, byDate := svc.calcCostByInventory(context.Background(), 1, "2006-01-02", nil, nil)
|
||||
|
||||
// 500 + 300 = 800
|
||||
if total != 800 {
|
||||
t.Errorf("cost total = %d, want 800", total)
|
||||
total, byDate := svc.calcCostByDrawSource(context.Background(), 1, "2006-01-02", nil, nil)
|
||||
if total != 1500 {
|
||||
t.Errorf("cost total = %d, want 1500", total)
|
||||
}
|
||||
if len(byDate) == 0 {
|
||||
t.Error("byDate should not be empty")
|
||||
if byDate["2026-03-05"] != 1500 {
|
||||
t.Errorf("cost on 2026-03-05 = %d, want 1500", byDate["2026-03-05"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfitLoss_AllGameTypes 端到端验证盈亏 = GMV(原价) - 成本,覆盖三种游戏类型及道具卡免单。
|
||||
// 核心场景:道具卡免单订单 actual_amount=0 但 total_amount=活动原价,成本真实存在,
|
||||
// 使用 total_amount 口径确保盈亏计算准确。
|
||||
func TestProfitLoss_AllGameTypes(t *testing.T) {
|
||||
func TestCalcCostByDrawSource_DateFilterUsesOrderDate(t *testing.T) {
|
||||
svc, repo := setupTestService(t)
|
||||
|
||||
mustExec(t, repo, `INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '测试渠道', 'TEST', 'other', '')`)
|
||||
mustExec(t, repo, `INSERT INTO users (id, nickname, invite_code, status, channel_id) VALUES (1, 'u1', 'I1', 1, 1)`)
|
||||
mustExec(t, repo, `INSERT INTO orders (id, user_id, status, actual_amount, total_amount, source_type, ext_order_id, paid_at, created_at) VALUES (10, 1, 2, 1000, 1000, 2, '', '2026-03-05 10:00:00', '2026-03-05 10:00:00')`)
|
||||
mustExec(t, repo, `INSERT INTO products (id, price, cost_price) VALUES (100, 1500, 700)`)
|
||||
mustExec(t, repo, `INSERT INTO activity_reward_settings (id, product_id, price_snapshot_cents, drop_quantity) VALUES (200, 100, 0, 1)`)
|
||||
mustExec(t, repo, `INSERT INTO activity_draw_logs (id, order_id, reward_id, created_at) VALUES (300, 10, 200, '2026-03-09 10:00:00')`)
|
||||
mustExec(t, repo, `INSERT INTO user_inventory (user_id, order_id, reward_id, product_id, status, value_cents, remark, created_at) VALUES (1, 10, 200, 100, 1, 1500, '', '2026-03-09 10:00:00')`)
|
||||
|
||||
orderFilter := "users.channel_id = ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type IN (2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)"
|
||||
start, _ := time.Parse("2006-01-02", "2026-03-05")
|
||||
end, _ := time.Parse("2006-01-02", "2026-03-05")
|
||||
end = end.Add(24*time.Hour - time.Second)
|
||||
|
||||
// 收入:3种游戏(total_amount = 活动原价),含优惠券拆分
|
||||
mustExec(t, repo, `INSERT INTO orders (id, user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (1, 1, 2, 3600, 4600, 1000, 0, 2, '', 'lottery:activity:10|count:1')`) // 抽奖 46元(现金36+券10)
|
||||
mustExec(t, repo, `INSERT INTO orders (id, user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (2, 1, 2, 1086, 1086, 0, 0, 3, '', 'matching_game:issue:50')`) // 对对碰 10.86元(全现金)
|
||||
mustExec(t, repo, `INSERT INTO orders (id, user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (3, 1, 2, 2820, 3320, 500, 0, 4, '', 'game_pass_package:x|pkg_id:7|count:2')`) // 一番赏 33.20元(现金28.20+券5)
|
||||
// 次卡免费使用:actual_amount=0,total_amount=2000,不计入GMV(避免重复计数),但成本仍计入
|
||||
mustExec(t, repo, `INSERT INTO orders (id, user_id, status, actual_amount, total_amount, discount_amount, points_amount, source_type, ext_order_id, remark) VALUES (4, 1, 2, 0, 2000, 0, 0, 2, '', 'lottery:activity:10|count:1|use_game_pass')`)
|
||||
|
||||
// 成本:库存资产
|
||||
mustExec(t, repo, `INSERT INTO user_inventory (user_id, order_id, status, value_cents, remark) VALUES (1, 0, 1, 8000, '')`) // 成本 80元
|
||||
|
||||
totalGMV, _ := svc.calcGMVByTotalAmount(context.Background(), 1, "2006-01-02", orderFilter, nil, nil)
|
||||
totalCost, _ := svc.calcCostByInventory(context.Background(), 1, "2006-01-02", nil, nil)
|
||||
profit := totalGMV.Total - totalCost
|
||||
|
||||
// GMV = 4600 + 1086 + 3320 = 9006(次卡免费使用的2000不计入)
|
||||
if totalGMV.Total != 9006 {
|
||||
t.Errorf("totalGMV.Total = %d, want 9006 (抽奖4600 + 对对碰1086 + 一番赏3320)", totalGMV.Total)
|
||||
total, byDate := svc.calcCostByDrawSource(context.Background(), 1, "2006-01-02", &start, &end)
|
||||
if total != 700 {
|
||||
t.Errorf("cost total = %d, want 700", total)
|
||||
}
|
||||
// 现金 = 3600 + 1086 + 2820 = 7506
|
||||
if totalGMV.Cash != 7506 {
|
||||
t.Errorf("totalGMV.Cash = %d, want 7506", totalGMV.Cash)
|
||||
}
|
||||
// 优惠券 = 1000 + 500 = 1500
|
||||
if totalGMV.Coupon != 1500 {
|
||||
t.Errorf("totalGMV.Coupon = %d, want 1500", totalGMV.Coupon)
|
||||
}
|
||||
// 成本 = 8000
|
||||
if totalCost != 8000 {
|
||||
t.Errorf("totalCost = %d, want 8000", totalCost)
|
||||
}
|
||||
// 盈亏 = 9006 - 8000 = 1006
|
||||
if profit != 1006 {
|
||||
t.Errorf("profit = %d, want 1006", profit)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcGMVByTotalAmount_Empty 验证无订单时返回零值。
|
||||
func TestCalcGMVByTotalAmount_Empty(t *testing.T) {
|
||||
svc, repo := setupTestService(t)
|
||||
|
||||
mustExec(t, repo, `INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '空渠道', 'EMPTY', 'other', '')`)
|
||||
|
||||
orderFilter := "users.channel_id = ? AND users.deleted_at IS NULL AND orders.status = 2 AND orders.total_amount > 0 AND orders.actual_amount > 0 AND orders.source_type IN (2,3,4) AND (orders.ext_order_id = '' OR orders.ext_order_id IS NULL)"
|
||||
|
||||
total, byDate := svc.calcGMVByTotalAmount(context.Background(), 1, "2006-01-02", orderFilter, nil, nil)
|
||||
|
||||
if total.Total != 0 {
|
||||
t.Errorf("empty channel total = %d, want 0", total.Total)
|
||||
}
|
||||
if len(byDate) != 0 {
|
||||
t.Errorf("byDate should be empty, got %v", byDate)
|
||||
if byDate["2026-03-05"] != 700 {
|
||||
t.Errorf("cost on 2026-03-05 = %d, want 700", byDate["2026-03-05"])
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user