package service import ( "context" "fmt" "strings" "testing" "time" "affiliate_dash/internal/model" ) func createSupportTestOrder(t *testing.T, svc *RockService, merchantID uint, orderNo, clientOrderNo string) { t.Helper() now := time.Now() order := model.FulfillmentOrder{ MerchantID: merchantID, OrderNo: orderNo, ClientOrderNo: clientOrderNo, ProductSKU: model.RockSKUBlackTshirt, ProductName: "黑色ROCK长袖T恤", OrderStatus: model.OrderStatusPaid, OrderSource: "manual", Currency: "POINT", CreatedAt: now, UpdatedAt: now, } if err := svc.db.Create(&order).Error; err != nil { t.Fatalf("创建订单失败: %v", err) } } func createSupportTestToken(t *testing.T, svc *RockService, upstreamID, redeemCode, orderNo, status, roleName, roleID string) model.RockToken { t.Helper() now := time.Now() token := model.RockToken{ UpstreamTokenID: upstreamID, Token: "jwt-" + upstreamID, RedeemCode: redeemCode, Color: model.RockColorBlack, Status: status, AssignedOrderNo: orderNo, ExpectedUID: roleID, ActualRoleName: roleName, ActualRoleID: roleID, ActualChannel: "安卓-微信", BoundAt: &now, } if err := svc.db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } return token } func TestSupportBoundTokens_RequiresSearchAndFiltersBound(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() var selfMerchant model.Merchant if err := db.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfMerchant).Error; err != nil { t.Fatalf("读取自营商户失败: %v", err) } otherMerchant := model.Merchant{Code: "support-test-other", Name: "测试商户", Status: model.MerchantStatusActive} if err := db.Create(&otherMerchant).Error; err != nil { t.Fatalf("创建测试商户失败: %v", err) } createSupportTestOrder(t, svc, selfMerchant.ID, "SUP-ORDER-1", "SUP-CLIENT-1") createSupportTestOrder(t, svc, otherMerchant.ID, "SUP-ORDER-2", "SUP-CLIENT-2") selfToken1 := createSupportTestToken(t, svc, "SUP-TOKEN-1", "SUP-CODE-1", "SUP-ORDER-1", model.RockStatusBound, "客服测试角色", "10001") selfToken2 := createSupportTestToken(t, svc, "SUP-TOKEN-2", "SUP-CODE-2", "SUP-ORDER-1", model.RockStatusBound, "客服第二个角色", "10002") otherToken := createSupportTestToken(t, svc, "SUP-TOKEN-3", "SUP-CODE-3", "SUP-ORDER-2", model.RockStatusBound, "其他商户角色", "20001") usingToken := createSupportTestToken(t, svc, "SUP-TOKEN-4", "SUP-CODE-4", "SUP-ORDER-1", model.RockStatusUsing, "使用中角色", "10003") // 不传搜索条件时必须拒绝,避免一次拉取全量数据。 if _, _, err := svc.ListSupportBoundTokens(ctx, 1, 20, " "); err == nil || !strings.Contains(err.Error(), "请输入") { t.Fatalf("空搜索条件必须拒绝,实际: %v", err) } // 输入搜索条件后返回全部命中,且不混入 using。 allTokens, allTotal, err := svc.ListSupportBoundTokens(ctx, 1, 20, "SUP-") if err != nil { t.Fatalf("查询失败: %v", err) } if allTotal != 3 || len(allTokens) != 3 { t.Fatalf("应返回 3 条绑定中 Token,实际 total=%d len=%d", allTotal, len(allTokens)) } for _, token := range allTokens { if token.Status != model.RockStatusBound { t.Fatalf("查询混入非 bound 状态: %+v", token) } if token.TokenCode == "" { t.Fatalf("TokenCode 未填充: %+v", token) } if token.ID == usingToken.ID { t.Fatalf("using Token 不应出现在查询结果中: %+v", token) } } // 按商户单号查询:同一订单下的两条 Token 都应命中。 byClientOrder, clientTotal, err := svc.ListSupportBoundTokens(ctx, 1, 20, "SUP-CLIENT-1") if err != nil { t.Fatalf("按商户单号查询失败: %v", err) } if clientTotal != 2 || len(byClientOrder) != 2 { t.Fatalf("按商户单号应查到 2 条,实际 total=%d len=%d", clientTotal, len(byClientOrder)) } for _, token := range byClientOrder { if token.ID != selfToken1.ID && token.ID != selfToken2.ID { t.Fatalf("按商户单号查询结果不符: %+v", token) } } // 按上游编号查询:只命中其他商户那条。 byUpstream, upstreamTotal, err := svc.ListSupportBoundTokens(ctx, 1, 20, "SUP-TOKEN-3") if err != nil { t.Fatalf("按上游编号查询失败: %v", err) } if upstreamTotal != 1 || len(byUpstream) != 1 || byUpstream[0].ID != otherToken.ID { t.Fatalf("按上游编号查询结果不符: total=%d tokens=%+v", upstreamTotal, byUpstream) } // 按 Token ID 查询。 byID, idTotal, err := svc.ListSupportBoundTokens(ctx, 1, 20, fmt.Sprint(otherToken.ID)) if err != nil { t.Fatalf("按 Token ID 查询失败: %v", err) } if idTotal != 1 || len(byID) != 1 || byID[0].ID != otherToken.ID { t.Fatalf("按 Token ID 查询结果不符: total=%d tokens=%+v", idTotal, byID) } // 使用中的 Token 不能被客服查询到。 usingResult, usingTotal, err := svc.ListSupportBoundTokens(ctx, 1, 20, "SUP-TOKEN-4") if err != nil { t.Fatalf("查询 using Token 失败: %v", err) } if usingTotal != 0 || len(usingResult) != 0 { t.Fatalf("using Token 不应被查询到,实际 total=%d", usingTotal) } } func TestSupportTokenStatusLogs_RequiresBound(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() var selfMerchant model.Merchant if err := db.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfMerchant).Error; err != nil { t.Fatalf("读取自营商户失败: %v", err) } createSupportTestOrder(t, svc, selfMerchant.ID, "SUP-LOG-ORDER-1", "SUP-LOG-CLIENT-1") boundToken := createSupportTestToken(t, svc, "SUP-LOG-TOKEN-1", "SUP-LOG-CODE-1", "SUP-LOG-ORDER-1", model.RockStatusBound, "自营客服角色", "30001") usingToken := createSupportTestToken(t, svc, "SUP-LOG-TOKEN-2", "SUP-LOG-CODE-2", "SUP-LOG-ORDER-1", model.RockStatusUsing, "使用中角色", "30002") for _, token := range []model.RockToken{boundToken, usingToken} { entry := model.RockTokenStatusLog{ TokenID: token.ID, UpstreamTokenID: token.UpstreamTokenID, EventType: rockTokenEventStatusChanged, FromStatus: model.RockStatusUsing, ToStatus: token.Status, Source: rockTokenSourceBindCheck, Reason: "测试状态变更", } if err := db.Create(&entry).Error; err != nil { t.Fatalf("创建状态日志失败: %v", err) } } logs, err := svc.ListSupportTokenStatusLogs(ctx, boundToken.ID) if err != nil { t.Fatalf("读取绑定中 Token 历史失败: %v", err) } if len(logs) != 1 { t.Fatalf("绑定中 Token 应读到 1 条历史,实际 %d", len(logs)) } if _, err := svc.ListSupportTokenStatusLogs(ctx, usingToken.ID); err == nil || !strings.Contains(err.Error(), "不存在") { t.Fatalf("非绑定中 Token 必须拒绝,实际: %v", err) } if _, err := svc.ListSupportTokenStatusLogs(ctx, 999999); err == nil || !strings.Contains(err.Error(), "不存在") { t.Fatalf("不存在的 Token 必须拒绝,实际: %v", err) } }