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 TestFindOrderConversationDistributionPrefersListingGroup(t *testing.T) { db := setupConversationTestDB(t) repo := NewRepository(db, nil, nil) now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC) site := model.DistributionSite{ SiteCode: "site_listing_group", SiteName: "账号群分销", Domain: "listing-group.hfb.com", Status: "active", } if err := db.Create(&site).Error; err != nil { t.Fatalf("创建分销站点失败: %v", err) } owner := model.User{Phone: "13800001005", Nickname: "号主"} renter := model.User{Phone: "13800001006", 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-DIST-LISTING-GROUP", ListingID: 103, AccountID: 203, OwnerID: owner.ID, RenterID: renter.ID, Status: "renting", DistributionSiteID: &site.ID, } if err := db.Create(&order).Error; err != nil { t.Fatalf("创建订单失败: %v", err) } orderConv := model.ChatConversation{ OrderID: &order.ID, DistributionSiteID: &site.ID, Type: ConversationTypeOrderGroup, Title: "订单群聊", Status: "active", } listingConv := model.ChatConversation{ ListingID: &order.ListingID, DistributionSiteID: &site.ID, 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 TestListingGroupVisibleToCrossSiteRenter(t *testing.T) { db := setupConversationTestDB(t) repo := NewRepository(db, nil, nil) now := time.Now() renter := model.User{Phone: "13900003001", Nickname: "跨站租客"} if err := db.Create(&renter).Error; err != nil { t.Fatalf("创建租客失败: %v", err) } buySite := model.DistributionSite{ SiteCode: "site_buyer", SiteName: "买家分站", Domain: "buyer.hfb.com", Status: "active", } listingSite := model.DistributionSite{ SiteCode: "site_listing", SiteName: "挂货分站", Domain: "listing.hfb.com", Status: "active", } if err := db.Create(&buySite).Error; err != nil { t.Fatalf("创建买家分站失败: %v", err) } if err := db.Create(&listingSite).Error; err != nil { t.Fatalf("创建挂货分站失败: %v", err) } mainListingID := uint64(301) distListingID := uint64(302) mainGroup := model.ChatConversation{ ListingID: &mainListingID, Type: ConversationTypeListingGroup, Title: "主站账号群", Status: "active", } distGroup := model.ChatConversation{ ListingID: &distListingID, DistributionSiteID: &listingSite.ID, Type: ConversationTypeListingGroup, Title: "分站账号群", Status: "active", } if err := db.Create(&mainGroup).Error; err != nil { t.Fatalf("创建主站账号群失败: %v", err) } if err := db.Create(&distGroup).Error; err != nil { t.Fatalf("创建分站账号群失败: %v", err) } if err := db.Create(&[]model.ChatParticipant{ {ConversationID: mainGroup.ID, ParticipantType: "user", ParticipantID: renter.ID, Role: "renter", JoinedAt: now}, {ConversationID: distGroup.ID, ParticipantType: "user", ParticipantID: renter.ID, Role: "renter", JoinedAt: now}, }).Error; err != nil { t.Fatalf("创建成员失败: %v", err) } orders := []model.RentalOrder{ { OrderNo: "ORDER-PLATFORM-RECEIVE", ListingID: mainListingID, RenterID: renter.ID, Status: "renting", DistributionSiteID: &buySite.ID, DistributionMode: "two_party_platform_receive", }, { OrderNo: "ORDER-DISTRIBUTOR-RECEIVE", ListingID: distListingID, RenterID: renter.ID, Status: "renting", DistributionSiteID: &listingSite.ID, DistributionMode: "two_party_distributor_receive", }, } if err := db.Create(&orders).Error; err != nil { t.Fatalf("创建订单失败: %v", err) } principal := Principal{Type: "user", ID: renter.ID} buyList, err := repo.ListConversations(t.Context(), principal, 1, 20, buySite.SiteCode) if err != nil { t.Fatalf("查询买家分站会话失败: %v", err) } if !conversationListHas(buyList, mainGroup.ID) { t.Fatalf("买家分站应看到主站账号群 %d", mainGroup.ID) } if conversationListHas(buyList, distGroup.ID) { t.Fatalf("买家分站不应看到其他分站账号群 %d", distGroup.ID) } mainList, err := repo.ListConversations(t.Context(), principal, 1, 20) if err != nil { t.Fatalf("查询主站会话失败: %v", err) } if !conversationListHas(mainList, mainGroup.ID) { t.Fatalf("主站应看到主站账号群 %d", mainGroup.ID) } if !conversationListHas(mainList, distGroup.ID) { t.Fatalf("主站买家应看到分站收号的账号群 %d", distGroup.ID) } } func conversationListHas(result *PaginatedResult, id uint64) bool { items, ok := result.Items.([]ConversationDTO) if !ok { return false } for _, item := range items { if item.ID == id { return true } } return false } 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) } }