319 lines
11 KiB
Go
319 lines
11 KiB
Go
package admin
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"bindbox-game/internal/pkg/core"
|
|
"bindbox-game/internal/pkg/logger"
|
|
"bindbox-game/internal/proposal"
|
|
"bindbox-game/internal/repository/mysql"
|
|
)
|
|
|
|
func Test_BindChannelUsers_SuccessWithPartialFailure(t *testing.T) {
|
|
repo, mux := setupChannelsTestRouter(t)
|
|
|
|
sqls := []string{
|
|
`INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '渠道A', 'CH_A', 'other', ''), (2, '渠道B', 'CH_B', 'other', '')`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (1, 'u1', '13800000001', 'I1', 1, 1)`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (2, 'u2', '13800000002', 'I2', 1, 0)`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (3, 'u3', '13800000003', 'I3', 1, 2)`,
|
|
}
|
|
for _, sql := range sqls {
|
|
if err := repo.GetDbW().Exec(sql).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
body := []byte(`{"user_ids":[1,2,3,999,2]}`)
|
|
rr := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/admin/channels/2/users/bind", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
mux.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("code=%d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var rsp struct {
|
|
SuccessCount int `json:"success_count"`
|
|
FailedCount int `json:"failed_count"`
|
|
SkippedCount int `json:"skipped_count"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rsp.SuccessCount != 2 || rsp.FailedCount != 1 || rsp.SkippedCount != 1 {
|
|
t.Fatalf("unexpected result: %+v", rsp)
|
|
}
|
|
|
|
type row struct {
|
|
ID int64
|
|
ChannelID int64
|
|
}
|
|
var rows []row
|
|
if err := repo.GetDbR().Raw(`SELECT id, channel_id FROM users WHERE id IN (1,2,3) ORDER BY id`).Scan(&rows).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 3 {
|
|
t.Fatalf("expect 3 rows, got %d", len(rows))
|
|
}
|
|
if rows[0].ChannelID != 2 || rows[1].ChannelID != 2 || rows[2].ChannelID != 2 {
|
|
t.Fatalf("bind verify failed, rows=%+v", rows)
|
|
}
|
|
}
|
|
|
|
func Test_BindChannelUsers_TooManyUsers(t *testing.T) {
|
|
repo, mux := setupChannelsTestRouter(t)
|
|
|
|
if err := repo.GetDbW().Exec(`INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '渠道A', 'CH_A', 'other', '')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
userIDs := make([]int, 0, 201)
|
|
for i := 1; i <= 201; i++ {
|
|
userIDs = append(userIDs, i)
|
|
}
|
|
reqBody, _ := json.Marshal(map[string]any{"user_ids": userIDs})
|
|
|
|
rr := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/admin/channels/1/users/bind", bytes.NewBuffer(reqBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
mux.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expect 400, got code=%d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func Test_ListChannels_FilterByCodeAndPaidStats(t *testing.T) {
|
|
repo, mux := setupChannelsTestRouter(t)
|
|
|
|
sqls := []string{
|
|
`INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '渠道A', 'CH_A', 'other', ''), (2, '渠道B', 'CH_B', 'other', '')`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (1, 'u1', '13800000001', 'I1', 1, 1)`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (2, 'u2', '13800000002', 'I2', 1, 1)`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (3, 'u3', '13800000003', 'I3', 1, 2)`,
|
|
`INSERT INTO orders (user_id, status, actual_amount, source_type, ext_order_id, created_at) VALUES (1, 2, 100, 1, '', '2026-03-01 10:00:00')`,
|
|
`INSERT INTO orders (user_id, status, actual_amount, source_type, ext_order_id, created_at) VALUES (2, 2, 300, 3, '', '2026-03-01 11:00:00')`,
|
|
`INSERT INTO orders (user_id, status, actual_amount, source_type, ext_order_id, created_at) VALUES (3, 2, 500, 2, '', '2026-03-01 12:00:00')`,
|
|
`INSERT INTO orders (user_id, status, actual_amount, source_type, ext_order_id, created_at) VALUES (1, 1, 700, 1, '', '2026-03-01 13:00:00')`,
|
|
`INSERT INTO orders (user_id, status, actual_amount, source_type, ext_order_id, created_at) VALUES (1, 2, 0, 1, '', '2026-03-01 14:00:00')`,
|
|
`INSERT INTO orders (user_id, status, actual_amount, source_type, ext_order_id, created_at) VALUES (1, 2, 900, 1, 'EXTERNAL-1', '2026-03-01 15:00:00')`,
|
|
}
|
|
for _, sql := range sqls {
|
|
if err := repo.GetDbW().Exec(sql).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
rr := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/admin/channels?name=CH_A&page=1&page_size=20", bytes.NewBufferString(""))
|
|
mux.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("code=%d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var rsp struct {
|
|
Total int64 `json:"total"`
|
|
List []struct {
|
|
ID int64 `json:"id"`
|
|
UserCount int64 `json:"user_count"`
|
|
PaidAmountCents int64 `json:"paid_amount_cents"`
|
|
PaidAmount int64 `json:"paid_amount"`
|
|
} `json:"list"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rsp.Total != 1 || len(rsp.List) != 1 {
|
|
t.Fatalf("unexpected list result: %+v", rsp)
|
|
}
|
|
if rsp.List[0].ID != 1 || rsp.List[0].UserCount != 2 {
|
|
t.Fatalf("unexpected channel row: %+v", rsp.List[0])
|
|
}
|
|
if rsp.List[0].PaidAmountCents != 400 || rsp.List[0].PaidAmount != 4 {
|
|
t.Fatalf("unexpected paid stats: %+v", rsp.List[0])
|
|
}
|
|
}
|
|
|
|
func Test_SearchChannelUsers_ByIDAndFuzzy(t *testing.T) {
|
|
repo, mux := setupChannelsTestRouter(t)
|
|
|
|
sqls := []string{
|
|
`INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '渠道A', 'CH_A', 'other', '')`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (11, 'Alice', '13800138000', 'I11', 1, 1)`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (12, 'Bob', '13900139000', 'I12', 1, 0)`,
|
|
}
|
|
for _, sql := range sqls {
|
|
if err := repo.GetDbW().Exec(sql).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
assertSearch := func(url string, expectTotal int64, expectIDs ...int64) {
|
|
rr := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, url, bytes.NewBufferString(""))
|
|
mux.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("url=%s code=%d body=%s", url, rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var rsp struct {
|
|
Total int64 `json:"total"`
|
|
List []struct {
|
|
ID int64 `json:"id"`
|
|
} `json:"list"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rsp.Total != expectTotal {
|
|
t.Fatalf("url=%s expect total=%d got %d", url, expectTotal, rsp.Total)
|
|
}
|
|
if len(rsp.List) != len(expectIDs) {
|
|
t.Fatalf("url=%s expect %d rows got %d", url, len(expectIDs), len(rsp.List))
|
|
}
|
|
for i, id := range expectIDs {
|
|
if rsp.List[i].ID != id {
|
|
t.Fatalf("url=%s expect id=%d got %d", url, id, rsp.List[i].ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
assertSearch("/api/admin/channels/users/search?keyword=11&page=1&page_size=10", 1, 11)
|
|
assertSearch("/api/admin/channels/users/search?keyword=1380013&page=1&page_size=10", 1, 11)
|
|
assertSearch("/api/admin/channels/users/search?keyword=Bob&page=1&page_size=10", 1, 12)
|
|
}
|
|
|
|
func Test_SearchChannelUsers_ByChannelOnly(t *testing.T) {
|
|
repo, mux := setupChannelsTestRouter(t)
|
|
|
|
sqls := []string{
|
|
`INSERT INTO channels (id, name, code, type, remarks) VALUES (1, '渠道A', 'CH_A', 'other', ''), (2, '渠道B', 'CH_B', 'other', '')`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (11, 'Alice', '13800138000', 'I11', 1, 1)`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (12, 'Bob', '13900139000', 'I12', 1, 2)`,
|
|
`INSERT INTO users (id, nickname, mobile, invite_code, status, channel_id) VALUES (13, 'Cathy', '13700137000', 'I13', 1, 1)`,
|
|
}
|
|
for _, sql := range sqls {
|
|
if err := repo.GetDbW().Exec(sql).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
rr := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/admin/channels/users/search?channel_id=1&page=1&page_size=10", bytes.NewBufferString(""))
|
|
mux.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("code=%d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var rsp struct {
|
|
Total int64 `json:"total"`
|
|
List []struct {
|
|
ID int64 `json:"id"`
|
|
ChannelID int64 `json:"channel_id"`
|
|
} `json:"list"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if rsp.Total != 2 || len(rsp.List) != 2 {
|
|
t.Fatalf("unexpected result: %+v", rsp)
|
|
}
|
|
if rsp.List[0].ID != 13 || rsp.List[0].ChannelID != 1 {
|
|
t.Fatalf("unexpected first row: %+v", rsp.List[0])
|
|
}
|
|
if rsp.List[1].ID != 11 || rsp.List[1].ChannelID != 1 {
|
|
t.Fatalf("unexpected second row: %+v", rsp.List[1])
|
|
}
|
|
}
|
|
|
|
func Test_SearchChannelUsers_RequireKeywordOrChannelID(t *testing.T) {
|
|
_, mux := setupChannelsTestRouter(t)
|
|
|
|
rr := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/admin/channels/users/search?page=1&page_size=10", bytes.NewBufferString(""))
|
|
mux.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expect 400, got code=%d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func setupChannelsTestRouter(t *testing.T) (mysql.Repo, http.Handler) {
|
|
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,
|
|
source_type INTEGER NOT NULL DEFAULT 1,
|
|
ext_order_id TEXT NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
}
|
|
for _, ddl := range ddls {
|
|
if err := repo.GetDbW().Exec(ddl).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
lg, err := logger.NewCustomLogger(logger.WithOutputInConsole())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux, err := core.New(lg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dummyAuth := func(ctx core.Context) (proposal.SessionUserInfo, core.BusinessError) {
|
|
return proposal.SessionUserInfo{Id: 1, IsSuper: 1}, nil
|
|
}
|
|
adminGroup := mux.Group("/api/admin", core.WrapAuthHandler(dummyAuth))
|
|
h := New(lg, repo, nil)
|
|
adminGroup.GET("/channels", h.ListChannels())
|
|
adminGroup.GET("/channels/users/search", h.SearchChannelUsers())
|
|
adminGroup.POST("/channels/:channel_id/users/bind", h.BindChannelUsers())
|
|
|
|
return repo, mux
|
|
}
|