package chat import ( "testing" "time" "hfb_sys/backend/internal/database" "hfb_sys/backend/internal/model" "gorm.io/gorm" ) func setupConversationTestDB(t *testing.T) *gorm.DB { t.Helper() db := database.NewTestDB() if err := database.AutoMigrateIfMissing(db, &model.User{}, &model.RentalOrder{}, &model.ChatConversation{}, &model.ChatParticipant{}, &model.ChatAdminConversationState{}, &model.ChatMessage{}, &model.DistributionSite{}, ); err != nil { t.Fatalf("数据库迁移失败: %v", err) } return db } func TestFindOrderConversationPrefersListingGroup(t *testing.T) { db := setupConversationTestDB(t) repo := NewRepository(db, nil, nil) now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC) owner := model.User{Phone: "13800001001", Nickname: "号主"} renter := model.User{Phone: "13800001002", Nickname: "租客"} if err := db.Create(&owner).Error; err != nil { t.Fatalf("创建号主失败: %v", err) } if err := db.Create(&renter).Error; err != nil { t.Fatalf("创建租客失败: %v", err) } order := model.RentalOrder{ OrderNo: "ORDER-LISTING-GROUP", ListingID: 101, AccountID: 201, OwnerID: owner.ID, RenterID: renter.ID, Status: "renting", } if err := db.Create(&order).Error; err != nil { t.Fatalf("创建订单失败: %v", err) } orderConv := model.ChatConversation{ OrderID: &order.ID, Type: ConversationTypeOrderGroup, Title: "旧订单群", Status: "active", } listingConv := model.ChatConversation{ ListingID: &order.ListingID, Type: ConversationTypeListingGroup, Title: "发布群", Status: "active", } if err := db.Create(&orderConv).Error; err != nil { t.Fatalf("创建订单群失败: %v", err) } if err := db.Create(&listingConv).Error; err != nil { t.Fatalf("创建发布群失败: %v", err) } participants := []model.ChatParticipant{ {ConversationID: orderConv.ID, ParticipantType: "user", ParticipantID: order.RenterID, Role: "renter", JoinedAt: now}, {ConversationID: listingConv.ID, ParticipantType: "user", ParticipantID: order.RenterID, Role: "renter", JoinedAt: now}, } if err := db.Create(&participants).Error; err != nil { t.Fatalf("创建成员失败: %v", err) } dto, err := repo.FindOrderConversation(t.Context(), order.RenterID, order.ID) if err != nil { t.Fatalf("查询订单会话失败: %v", err) } if dto.ID != listingConv.ID { t.Fatalf("会话 ID = %d, want 发布群 %d", dto.ID, listingConv.ID) } if dto.Type != ConversationTypeListingGroup { t.Fatalf("会话类型 = %q, want %q", dto.Type, ConversationTypeListingGroup) } } func TestFindOrderConversationFallsBackToOrderGroup(t *testing.T) { db := setupConversationTestDB(t) repo := NewRepository(db, nil, nil) now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC) owner := model.User{Phone: "13800001003", Nickname: "号主"} renter := model.User{Phone: "13800001004", Nickname: "租客"} if err := db.Create(&owner).Error; err != nil { t.Fatalf("创建号主失败: %v", err) } if err := db.Create(&renter).Error; err != nil { t.Fatalf("创建租客失败: %v", err) } order := model.RentalOrder{ OrderNo: "ORDER-LEGACY-GROUP", ListingID: 102, AccountID: 202, OwnerID: owner.ID, RenterID: renter.ID, Status: "renting", } if err := db.Create(&order).Error; err != nil { t.Fatalf("创建订单失败: %v", err) } orderConv := model.ChatConversation{ OrderID: &order.ID, Type: ConversationTypeOrderGroup, Title: "旧订单群", Status: "active", } if err := db.Create(&orderConv).Error; err != nil { t.Fatalf("创建订单群失败: %v", err) } participant := model.ChatParticipant{ ConversationID: orderConv.ID, ParticipantType: "user", ParticipantID: order.RenterID, Role: "renter", JoinedAt: now, } if err := db.Create(&participant).Error; err != nil { t.Fatalf("创建成员失败: %v", err) } dto, err := repo.FindOrderConversation(t.Context(), order.RenterID, order.ID) if err != nil { t.Fatalf("查询订单会话失败: %v", err) } if dto.ID != orderConv.ID { t.Fatalf("会话 ID = %d, want 订单群 %d", dto.ID, orderConv.ID) } if dto.Type != ConversationTypeOrderGroup { t.Fatalf("会话类型 = %q, want %q", dto.Type, ConversationTypeOrderGroup) } } func TestChatTenantIsolation(t *testing.T) { db := setupConversationTestDB(t) repo := NewRepository(db, nil, nil) now := time.Now() user := model.User{Phone: "13900002001", Nickname: "分销用户"} if err := db.Create(&user).Error; err != nil { t.Fatalf("创建用户失败: %v", err) } site := model.DistributionSite{ ID: 20, SiteCode: "site_beta", SiteName: "贝塔分销", Domain: "beta.hfb.com", Status: "active", } if err := db.Create(&site).Error; err != nil { t.Fatalf("创建分销站点失败: %v", err) } // 1. 创建主站客服会话 mainConv := model.ChatConversation{ Type: ConversationTypeGeneralSupport, SupportScene: SupportSceneGeneral, Title: "主站客服", Status: "active", } if err := db.Create(&mainConv).Error; err != nil { t.Fatalf("创建主站客服会话失败: %v", err) } if err := db.Create(&model.ChatParticipant{ ConversationID: mainConv.ID, ParticipantType: "user", ParticipantID: user.ID, Role: "customer", JoinedAt: now, }).Error; err != nil { t.Fatalf("添加主站会话成员失败: %v", err) } // 2. 创建分销站客服会话 siteConv := model.ChatConversation{ DistributionSiteID: &site.ID, Type: ConversationTypeGeneralSupport, SupportScene: SupportSceneGeneral, Title: "【贝塔分销】平台客服", Status: "active", } if err := db.Create(&siteConv).Error; err != nil { t.Fatalf("创建分站客服会话失败: %v", err) } if err := db.Create(&model.ChatParticipant{ ConversationID: siteConv.ID, ParticipantType: "user", ParticipantID: user.ID, Role: "customer", JoinedAt: now, }).Error; err != nil { t.Fatalf("添加分站会话成员失败: %v", err) } principal := Principal{Type: "user", ID: user.ID} // 3. 校验主站查询会话列表:严格只看到主站会话 mainList, err := repo.ListConversations(t.Context(), principal, 1, 20) if err != nil { t.Fatalf("查询主站会话列表失败: %v", err) } if mainList.Total != 1 { t.Fatalf("主站会话数量 = %d, want 1", mainList.Total) } mainItems := mainList.Items.([]ConversationDTO) if mainItems[0].ID != mainConv.ID { t.Fatalf("主站会话 ID = %d, want %d", mainItems[0].ID, mainConv.ID) } if mainItems[0].DistributionSiteID != nil { t.Fatalf("主站会话 DistributionSiteID = %v, want nil", mainItems[0].DistributionSiteID) } // 4. 校验分站查询会话列表:严格只看到分站会话 siteList, err := repo.ListConversations(t.Context(), principal, 1, 20, site.SiteCode) if err != nil { t.Fatalf("查询分站会话列表失败: %v", err) } if siteList.Total != 1 { t.Fatalf("分站会话数量 = %d, want 1", siteList.Total) } siteItems := siteList.Items.([]ConversationDTO) if siteItems[0].ID != siteConv.ID { t.Fatalf("分站会话 ID = %d, want %d", siteItems[0].ID, siteConv.ID) } if siteItems[0].DistributionSiteID == nil || *siteItems[0].DistributionSiteID != site.ID { t.Fatalf("分站会话 DistributionSiteID = %v, want %d", siteItems[0].DistributionSiteID, site.ID) } // 5. 测试 EnsureSupportConversation 分销站点场景 ensuredSiteConv, err := repo.EnsureSupportConversation(t.Context(), user.ID, "crash", site.SiteCode) if err != nil { t.Fatalf("分站创建撞车客服失败: %v", err) } if ensuredSiteConv.DistributionSiteID == nil || *ensuredSiteConv.DistributionSiteID != site.ID { t.Fatalf("新建分站会话 DistributionSiteID = %v, want %d", ensuredSiteConv.DistributionSiteID, site.ID) } // 主站再次查询,仍然只有 1 个主站会话 mainListAfter, err := repo.ListConversations(t.Context(), principal, 1, 20) if err != nil { t.Fatalf("主站列表查询失败: %v", err) } if mainListAfter.Total != 1 { t.Fatalf("主站会话数量 = %d, want 1 (分站新增会话不应出现在主站)", mainListAfter.Total) } // 分站再次查询,有 2 个分站会话 siteListAfter, err := repo.ListConversations(t.Context(), principal, 1, 20, site.SiteCode) if err != nil { t.Fatalf("分站列表查询失败: %v", err) } if siteListAfter.Total != 2 { t.Fatalf("分站会话数量 = %d, want 2", siteListAfter.Total) } }