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

172 lines
4.6 KiB
Go

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)
}
})
}
}