package channel import ( "context" "testing" "time" "bindbox-game/internal/pkg/logger" "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" ) // setupTestService 创建使用 SQLite 内存库的 service 实例及基础表结构。 func setupTestService(t *testing.T) (*service, mysql.Repo) { t.Helper() repo, err := mysql.NewSQLiteRepoForTest() if err != nil { t.Fatal(err) } ddls := []string{ `CREATE TABLE channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, code TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'other', remarks TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted_at DATETIME )`, `CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted_at DATETIME, nickname TEXT NOT NULL, avatar TEXT, mobile TEXT, openid TEXT, unionid TEXT, invite_code TEXT NOT NULL, inviter_id INTEGER DEFAULT 0, status INTEGER NOT NULL DEFAULT 1, douyin_id TEXT, channel_id INTEGER DEFAULT 0, douyin_user_id TEXT, remark TEXT NOT NULL DEFAULT '' )`, `CREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, status INTEGER NOT NULL, actual_amount INTEGER NOT NULL DEFAULT 0, total_amount INTEGER NOT NULL DEFAULT 0, discount_amount INTEGER NOT NULL DEFAULT 0, points_amount INTEGER NOT NULL DEFAULT 0, source_type INTEGER NOT NULL DEFAULT 1, 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 ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, order_id INTEGER DEFAULT 0, reward_id INTEGER DEFAULT 0, product_id INTEGER DEFAULT 0, status INTEGER NOT NULL DEFAULT 1, value_cents INTEGER DEFAULT 0, remark TEXT NOT NULL DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE activity_reward_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, 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, 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, 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 )`, } for _, ddl := range ddls { if err := repo.GetDbW().Exec(ddl).Error; err != nil { t.Fatalf("DDL failed: %v\nSQL: %s", err, ddl) } } lg, err := logger.NewCustomLogger(logger.WithOutputInConsole()) if err != nil { t.Fatal(err) } q := dao.Use(repo.GetDbR()) svc := &service{logger: lg, readDB: q, writeDB: dao.Use(repo.GetDbW())} return svc, repo } // mustExec 执行 SQL,失败则 Fatal。 func mustExec(t *testing.T, repo mysql.Repo, sql string, args ...interface{}) { t.Helper() if err := repo.GetDbW().Exec(sql, args...).Error; err != nil { t.Fatalf("exec failed: %v\nSQL: %s", err, sql) } } // TestCalcGMVByTotalAmount_ThreeGameTypes 验证三种游戏类型的原价都被正确统计。 // 使用 total_amount(活动原价)确保优惠券、道具卡免单的订单也完整计入。 func TestCalcGMVByTotalAmount_ThreeGameTypes(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)`) 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)" 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')`) 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')`) 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')`) 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')`) 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')`) 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')`) 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) if total.Total != 2300 { t.Errorf("total.Total = %d, want 2300 (抽奖1000 + 对对碰500 + 一番赏800)", total.Total) } if total.Cash != 1800 { t.Errorf("total.Cash = %d, want 1800", total.Cash) } if total.Coupon != 500 { t.Errorf("total.Coupon = %d, want 500", total.Coupon) } if total.Points != 0 { t.Errorf("total.Points = %d, want 0", total.Points) } if len(byDate) == 0 { t.Error("byDate should not be empty") } } func TestCalcGMVByTotalAmount_DateFilter(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)`) 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)" mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, source_type, ext_order_id, created_at) VALUES (1, 2, 500, 500, 2, '', '2026-03-01 10:00:00')`) mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, source_type, ext_order_id, created_at) VALUES (1, 2, 300, 300, 3, '', '2026-03-05 10:00:00')`) mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, source_type, ext_order_id, created_at) VALUES (1, 2, 700, 700, 4, '', '2026-03-10 10:00:00')`) start, _ := time.Parse("2006-01-02", "2026-03-02") end, _ := time.Parse("2006-01-02", "2026-03-09") end = end.Add(24*time.Hour - time.Second) total, byDate := svc.calcGMVByTotalAmount(context.Background(), 1, "2006-01-02", orderFilter, &start, &end) 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) } } func TestCalcGMVByTotalAmount_MultiChannel(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)`) 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)" mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, source_type, ext_order_id) VALUES (1, 2, 1000, 1000, 2, '')`) mustExec(t, repo, `INSERT INTO orders (user_id, status, actual_amount, total_amount, source_type, ext_order_id) VALUES (2, 2, 2000, 2000, 3, '')`) total1, _ := svc.calcGMVByTotalAmount(context.Background(), 1, "2006-01-02", orderFilter, nil, nil) total2, _ := svc.calcGMVByTotalAmount(context.Background(), 2, "2006-01-02", orderFilter, nil, nil) if total1.Total != 1000 { t.Errorf("channel1 total = %d, want 1000", total1.Total) } if total2.Total != 2000 { t.Errorf("channel2 total = %d, want 2000", total2.Total) } } 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')`) total, byDate := svc.calcCostByDrawSource(context.Background(), 1, "2006-01-02", nil, nil) if total != 1500 { t.Errorf("cost total = %d, want 1500", total) } if byDate["2026-03-05"] != 1500 { t.Errorf("cost on 2026-03-05 = %d, want 1500", byDate["2026-03-05"]) } } 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')`) 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) total, byDate := svc.calcCostByDrawSource(context.Background(), 1, "2006-01-02", &start, &end) if total != 700 { t.Errorf("cost total = %d, want 700", total) } if byDate["2026-03-05"] != 700 { t.Errorf("cost on 2026-03-05 = %d, want 700", byDate["2026-03-05"]) } }