From 1d165c73153aa599ace601047cdc408b953420bf Mon Sep 17 00:00:00 2001 From: Haruko386 Date: Wed, 22 Jul 2026 21:27:08 +0800 Subject: [PATCH] fix: unable to get owner in chat and search (#17244) ### Summary As title --- internal/dao/chat.go | 14 ++-- internal/dao/search.go | 14 ++-- internal/entity/chat.go | 7 ++ internal/entity/search.go | 7 ++ internal/handler/chat.go | 4 +- internal/handler/mcp_server.go | 2 +- internal/handler/search.go | 4 +- internal/service/chat.go | 113 ++++++++++++++++++++++++----- internal/service/chat_list_test.go | 58 +++++++++++++-- internal/service/search.go | 15 ++-- internal/service/search_test.go | 57 +++++++++++++++ 11 files changed, 245 insertions(+), 50 deletions(-) diff --git a/internal/dao/chat.go b/internal/dao/chat.go index d26ee52cc8..9303accc40 100644 --- a/internal/dao/chat.go +++ b/internal/dao/chat.go @@ -54,8 +54,8 @@ func (dao *ChatDAO) ListByTenantID(tenantID string, status string) ([]*entity.Ch } // ListByTenantIDs list chats by tenant IDs with pagination and filtering -func (dao *ChatDAO) ListByTenantIDs(tenantIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords string) ([]*entity.Chat, int64, error) { - var chats []*entity.Chat +func (dao *ChatDAO) ListByTenantIDs(tenantIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords string) ([]*entity.ChatListItem, int64, error) { + var chats []*entity.ChatListItem var total int64 // Build query with join to user table for nickname and avatar @@ -93,11 +93,11 @@ func (dao *ChatDAO) ListByTenantIDs(tenantIDs []string, userID string, page, pag // Apply pagination if page > 0 && pageSize > 0 { offset := (page - 1) * pageSize - if err := query.Offset(offset).Limit(pageSize).Find(&chats).Error; err != nil { + if err := query.Offset(offset).Limit(pageSize).Scan(&chats).Error; err != nil { return nil, 0, err } } else { - if err := query.Find(&chats).Error; err != nil { + if err := query.Scan(&chats).Error; err != nil { return nil, 0, err } } @@ -106,8 +106,8 @@ func (dao *ChatDAO) ListByTenantIDs(tenantIDs []string, userID string, page, pag } // ListByOwnerIDs list chats by owner IDs with filtering (manual pagination) -func (dao *ChatDAO) ListByOwnerIDs(ownerIDs []string, userID string, orderby string, desc bool, keywords string) ([]*entity.Chat, int64, error) { - var chats []*entity.Chat +func (dao *ChatDAO) ListByOwnerIDs(ownerIDs []string, userID string, orderby string, desc bool, keywords string) ([]*entity.ChatListItem, int64, error) { + var chats []*entity.ChatListItem // Build query with join to user table query := DB.Model(&entity.Chat{}). @@ -135,7 +135,7 @@ func (dao *ChatDAO) ListByOwnerIDs(ownerIDs []string, userID string, orderby str query = query.Order(orderby + " " + orderDirection) // Get all matching records - if err := query.Find(&chats).Error; err != nil { + if err := query.Scan(&chats).Error; err != nil { return nil, 0, err } diff --git a/internal/dao/search.go b/internal/dao/search.go index 0d8ce57cf6..46f1e241f2 100644 --- a/internal/dao/search.go +++ b/internal/dao/search.go @@ -45,8 +45,8 @@ type SearchDetailRow struct { } // ListByTenantIDs list searches by tenant IDs with pagination and filtering -func (dao *SearchDAO) ListByTenantIDs(tenantIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords string) ([]*entity.Search, int64, error) { - var searches []*entity.Search +func (dao *SearchDAO) ListByTenantIDs(tenantIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords string) ([]*entity.SearchListItem, int64, error) { + var searches []*entity.SearchListItem var total int64 // Build query with join to user table for nickname and avatar @@ -84,11 +84,11 @@ func (dao *SearchDAO) ListByTenantIDs(tenantIDs []string, userID string, page, p // Apply pagination if page > 0 && pageSize > 0 { offset := (page - 1) * pageSize - if err := query.Offset(offset).Limit(pageSize).Find(&searches).Error; err != nil { + if err := query.Offset(offset).Limit(pageSize).Scan(&searches).Error; err != nil { return nil, 0, err } } else { - if err := query.Find(&searches).Error; err != nil { + if err := query.Scan(&searches).Error; err != nil { return nil, 0, err } } @@ -97,8 +97,8 @@ func (dao *SearchDAO) ListByTenantIDs(tenantIDs []string, userID string, page, p } // ListByOwnerIDs list searches by owner IDs with filtering (manual pagination) -func (dao *SearchDAO) ListByOwnerIDs(ownerIDs []string, userID string, orderby string, desc bool, keywords string) ([]*entity.Search, int64, error) { - var searches []*entity.Search +func (dao *SearchDAO) ListByOwnerIDs(ownerIDs []string, userID string, orderby string, desc bool, keywords string) ([]*entity.SearchListItem, int64, error) { + var searches []*entity.SearchListItem // Build query with join to user table query := DB.Model(&entity.Search{}). @@ -126,7 +126,7 @@ func (dao *SearchDAO) ListByOwnerIDs(ownerIDs []string, userID string, orderby s query = query.Order(orderby + " " + orderDirection) // Get all matching records - if err := query.Find(&searches).Error; err != nil { + if err := query.Scan(&searches).Error; err != nil { return nil, 0, err } diff --git a/internal/entity/chat.go b/internal/entity/chat.go index a2ecde254e..d169e3ecd8 100644 --- a/internal/entity/chat.go +++ b/internal/entity/chat.go @@ -55,6 +55,13 @@ func (Chat) TableName() string { return "dialog" } +// ChatListItem represents a chat list row with owner profile fields. +type ChatListItem struct { + Chat + Nickname *string `gorm:"column:nickname" json:"nickname,omitempty"` + TenantAvatar *string `gorm:"column:tenant_avatar" json:"tenant_avatar,omitempty"` +} + // Conversation conversation model type ChatSession struct { ID string `gorm:"column:id;primaryKey;size:32" json:"id"` diff --git a/internal/entity/search.go b/internal/entity/search.go index b58e02ea7e..e882cc9961 100644 --- a/internal/entity/search.go +++ b/internal/entity/search.go @@ -33,3 +33,10 @@ type Search struct { func (Search) TableName() string { return "search" } + +// SearchListItem represents a search list row with owner profile fields. +type SearchListItem struct { + Search + Nickname *string `gorm:"column:nickname" json:"nickname,omitempty"` + TenantAvatar *string `gorm:"column:tenant_avatar" json:"tenant_avatar,omitempty"` +} diff --git a/internal/handler/chat.go b/internal/handler/chat.go index 07ba8ccc5a..c0a6680505 100644 --- a/internal/handler/chat.go +++ b/internal/handler/chat.go @@ -102,8 +102,10 @@ func (h *ChatHandler) ListChats(c *gin.Context) { desc = descStr != "false" } + ownerIDs := getOwnerIDs(c) + // List chats - default to valid status "1" (same as Python StatusEnum.VALID.value) - result, err := h.chatService.ListChats(userID, "1", keywords, page, pageSize, orderby, desc) + result, err := h.chatService.ListChats(userID, "1", keywords, page, pageSize, orderby, desc, ownerIDs) if err != nil { common.ResponseWithHttpCodeData(c, http.StatusInternalServerError, 500, nil, err.Error()) return diff --git a/internal/handler/mcp_server.go b/internal/handler/mcp_server.go index f475c2e939..b490f70caf 100644 --- a/internal/handler/mcp_server.go +++ b/internal/handler/mcp_server.go @@ -124,7 +124,7 @@ func MCPListDatasets(ds *dataset.DatasetService, userID string, page, pageSize i // MCPListChats wraps ChatService.ListChats for the MCP tool handler, // converting the typed response into a generic []map[string]interface{}. func MCPListChats(cs *service.ChatService, userID string, page, pageSize int, orderby string, desc bool) ([]map[string]interface{}, int64, error) { - resp, err := cs.ListChats(userID, "1", "", page, pageSize, orderby, desc) + resp, err := cs.ListChats(userID, "1", "", page, pageSize, orderby, desc, nil) if err != nil { return nil, 0, err } diff --git a/internal/handler/search.go b/internal/handler/search.go index 2d800bb41d..14abd3f2a2 100644 --- a/internal/handler/search.go +++ b/internal/handler/search.go @@ -51,7 +51,7 @@ func (h *SearchHandler) SetCompletionDependencies(streamLLM *service.ModelProvid h.askService = askService } -func getSearchOwnerIDs(c *gin.Context) []string { +func getOwnerIDs(c *gin.Context) []string { values := c.QueryArray("owner_ids") if len(values) == 0 { values = c.QueryArray("owner_id") @@ -114,7 +114,7 @@ func (h *SearchHandler) ListSearches(c *gin.Context) { desc = descStr != "false" } - ownerIDs := getSearchOwnerIDs(c) + ownerIDs := getOwnerIDs(c) // Keep body parsing as a compatibility fallback for existing callers that // send owner_ids in a GET body. Python reads owner_ids from the query. diff --git a/internal/service/chat.go b/internal/service/chat.go index d28f0210dd..a4d2613054 100644 --- a/internal/service/chat.go +++ b/internal/service/chat.go @@ -65,8 +65,10 @@ func NewChatService() *ChatService { // ChatWithKBNames chat with knowledge base names type ChatWithKBNames struct { *entity.Chat - KBNames []string `json:"kb_names"` - DatasetIDs []string `json:"dataset_ids"` + KBNames []string `json:"kb_names"` + DatasetIDs []string `json:"dataset_ids"` + Nickname string `json:"nickname"` + TenantAvatar *string `json:"tenant_avatar,omitempty"` } // ListChatsResponse list chats response @@ -76,18 +78,53 @@ type ListChatsResponse struct { } // ListChats list chats for a user -func (s *ChatService) ListChats(userID, status, keywords string, page, pageSize int, orderBy string, desc bool) (*ListChatsResponse, error) { - chats, total, err := s.chatDAO.ListByTenantIDs( - nil, - userID, - page, - pageSize, - orderBy, - desc, - keywords, - ) - if err != nil { - return nil, err +func (s *ChatService) ListChats(userID, status, keywords string, page, pageSize int, orderBy string, desc bool, ownerIDs []string) (*ListChatsResponse, error) { + var chats []*entity.ChatListItem + var total int64 + var err error + + if len(ownerIDs) == 0 { + chats, total, err = s.chatDAO.ListByTenantIDs( + nil, + userID, + page, + pageSize, + orderBy, + desc, + keywords, + ) + if err != nil { + return nil, err + } + } else { + filterOwnerIDs, err := s.filterAccessibleChatOwnerIDs(userID, ownerIDs) + if err != nil { + return nil, err + } + if len(filterOwnerIDs) == 0 { + return &ListChatsResponse{ + Total: 0, + Chats: []*ChatWithKBNames{}, + }, nil + } + + chats, total, err = s.chatDAO.ListByOwnerIDs(filterOwnerIDs, userID, orderBy, desc, keywords) + if err != nil { + return nil, err + } + + if page > 0 && pageSize > 0 { + start := (page - 1) * pageSize + end := start + pageSize + if start < int(total) { + if end > int(total) { + end = int(total) + } + chats = chats[start:end] + } else { + chats = []*entity.ChatListItem{} + } + } } // Enrich with knowledge base names @@ -95,9 +132,11 @@ func (s *ChatService) ListChats(userID, status, keywords string, page, pageSize for _, chat := range chats { kbNames, datasetIDs := s.getDatasetNamesAndIDs(chat.KBIDs) chatsWithKBNames = append(chatsWithKBNames, &ChatWithKBNames{ - Chat: chat, - KBNames: kbNames, - DatasetIDs: datasetIDs, + Chat: &chat.Chat, + KBNames: kbNames, + DatasetIDs: datasetIDs, + Nickname: ownerNickname(chat.Nickname, chat.TenantID), + TenantAvatar: chat.TenantAvatar, }) } @@ -107,6 +146,46 @@ func (s *ChatService) ListChats(userID, status, keywords string, page, pageSize }, nil } +func (s *ChatService) filterAccessibleChatOwnerIDs(userID string, ownerIDs []string) ([]string, error) { + tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(userID) + if err != nil { + return nil, err + } + + allowed := map[string]struct{}{userID: {}} + for _, tenantID := range tenantIDs { + tenantID = strings.TrimSpace(tenantID) + if tenantID != "" { + allowed[tenantID] = struct{}{} + } + } + + filtered := make([]string, 0, len(ownerIDs)) + seen := make(map[string]struct{}, len(ownerIDs)) + for _, ownerID := range ownerIDs { + ownerID = strings.TrimSpace(ownerID) + if ownerID == "" { + continue + } + if _, ok := allowed[ownerID]; !ok { + continue + } + if _, ok := seen[ownerID]; ok { + continue + } + seen[ownerID] = struct{}{} + filtered = append(filtered, ownerID) + } + return filtered, nil +} + +func ownerNickname(nickname *string, tenantID string) string { + if nickname != nil && strings.TrimSpace(*nickname) != "" { + return *nickname + } + return tenantID +} + type CreateChatRequest struct { Name string DatasetIDs []string `json:"dataset_ids"` diff --git a/internal/service/chat_list_test.go b/internal/service/chat_list_test.go index 307177b673..62022d159c 100644 --- a/internal/service/chat_list_test.go +++ b/internal/service/chat_list_test.go @@ -82,7 +82,7 @@ func TestChatServiceListChatsDefaultReturnsAllWithCorrectTotal(t *testing.T) { createChatListTestChat(t, db, "chat-3", "user-1", "list_test_2") svc := NewChatService() - result, err := svc.ListChats("user-1", "1", "", 0, 0, "create_time", true) + result, err := svc.ListChats("user-1", "1", "", 0, 0, "create_time", true, nil) if err != nil { t.Fatalf("ListChats failed: %v", err) } @@ -92,6 +92,50 @@ func TestChatServiceListChatsDefaultReturnsAllWithCorrectTotal(t *testing.T) { if len(result.Chats) != 3 { t.Fatalf("expected 3 chats, got %d", len(result.Chats)) } + if result.Chats[0].Nickname != "tester" { + t.Fatalf("expected nickname tester, got %q", result.Chats[0].Nickname) + } +} + +func TestChatServiceListChatsFiltersByOwnerIDs(t *testing.T) { + db := setupChatListTestDB(t) + + if err := db.Create(&entity.User{ + ID: "tenant-2", + Nickname: "team owner", + Email: "tenant-2@test.com", + Status: sptr("1"), + }).Error; err != nil { + t.Fatalf("failed to create tenant user: %v", err) + } + if err := db.Create(&entity.UserTenant{ + ID: "rel-1", + UserID: "user-1", + TenantID: "tenant-2", + Role: "normal", + InvitedBy: "tenant-2", + Status: sptr("1"), + }).Error; err != nil { + t.Fatalf("failed to create user tenant relation: %v", err) + } + + createChatListTestChat(t, db, "chat-own", "user-1", "own_chat") + createChatListTestChat(t, db, "chat-team", "tenant-2", "team_chat") + + svc := NewChatService() + result, err := svc.ListChats("user-1", "1", "", 0, 0, "create_time", true, []string{"tenant-2"}) + if err != nil { + t.Fatalf("ListChats failed: %v", err) + } + if result.Total != 1 || len(result.Chats) != 1 { + t.Fatalf("expected one filtered chat, got total=%d len=%d", result.Total, len(result.Chats)) + } + if result.Chats[0].TenantID != "tenant-2" { + t.Fatalf("expected tenant-2 chat, got tenant %q", result.Chats[0].TenantID) + } + if result.Chats[0].Nickname != "team owner" { + t.Fatalf("expected nickname team owner, got %q", result.Chats[0].Nickname) + } } func TestChatServiceListChatsKeywordFiltersCorrectly(t *testing.T) { @@ -102,7 +146,7 @@ func TestChatServiceListChatsKeywordFiltersCorrectly(t *testing.T) { svc := NewChatService() - exactResult, err := svc.ListChats("user-1", "1", "list_keyword_1", 0, 0, "create_time", true) + exactResult, err := svc.ListChats("user-1", "1", "list_keyword_1", 0, 0, "create_time", true, nil) if err != nil { t.Fatalf("ListChats keyword exact failed: %v", err) } @@ -113,7 +157,7 @@ func TestChatServiceListChatsKeywordFiltersCorrectly(t *testing.T) { t.Fatalf("expected chat name 'list_keyword_1', got %+v", exactResult.Chats[0].Name) } - unknownResult, err := svc.ListChats("user-1", "1", "unknown_keyword", 0, 0, "create_time", true) + unknownResult, err := svc.ListChats("user-1", "1", "unknown_keyword", 0, 0, "create_time", true, nil) if err != nil { t.Fatalf("ListChats unknown keyword failed: %v", err) } @@ -121,7 +165,7 @@ func TestChatServiceListChatsKeywordFiltersCorrectly(t *testing.T) { t.Fatalf("expected 0 chats for unknown keyword, got %d", len(unknownResult.Chats)) } - partialResult, err := svc.ListChats("user-1", "1", "list_keyword", 0, 0, "create_time", true) + partialResult, err := svc.ListChats("user-1", "1", "list_keyword", 0, 0, "create_time", true, nil) if err != nil { t.Fatalf("ListChats partial keyword failed: %v", err) } @@ -138,7 +182,7 @@ func TestChatServiceListChatsPagination(t *testing.T) { svc := NewChatService() - page1, err := svc.ListChats("user-1", "1", "", 1, 2, "create_time", true) + page1, err := svc.ListChats("user-1", "1", "", 1, 2, "create_time", true, nil) if err != nil { t.Fatalf("ListChats page 1 failed: %v", err) } @@ -149,7 +193,7 @@ func TestChatServiceListChatsPagination(t *testing.T) { t.Fatalf("expected total=5, got %d", page1.Total) } - page3, err := svc.ListChats("user-1", "1", "", 3, 2, "create_time", true) + page3, err := svc.ListChats("user-1", "1", "", 3, 2, "create_time", true, nil) if err != nil { t.Fatalf("ListChats page 3 failed: %v", err) } @@ -167,7 +211,7 @@ func TestChatServiceListChatsExcludesDeletedChats(t *testing.T) { db.Model(&entity.Chat{}).Where("id = ?", "chat-2").Update("status", invalidStatus) svc := NewChatService() - result, err := svc.ListChats("user-1", "1", "", 0, 0, "create_time", true) + result, err := svc.ListChats("user-1", "1", "", 0, 0, "create_time", true, nil) if err != nil { t.Fatalf("ListChats failed: %v", err) } diff --git a/internal/service/search.go b/internal/service/search.go index 991a6b273f..dd926707de 100644 --- a/internal/service/search.go +++ b/internal/service/search.go @@ -80,7 +80,7 @@ type SearchShareDetail struct { // ListSearches list search apps with advanced filtering (equivalent to list_search_app) func (s *SearchService) ListSearches(userID string, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string) (*ListSearchAppsResponse, error) { - var searches []*entity.Search + var searches []*entity.SearchListItem var total int64 var err error @@ -115,7 +115,7 @@ func (s *SearchService) ListSearches(userID string, keywords string, page, pageS } searches = searches[start:end] } else { - searches = []*entity.Search{} + searches = []*entity.SearchListItem{} } } } @@ -166,7 +166,7 @@ func (s *SearchService) filterAccessibleSearchOwnerIDs(userID string, ownerIDs [ } // toSearchAppResponse converts search model to response format -func (s *SearchService) toSearchAppResponse(search *entity.Search) map[string]interface{} { +func (s *SearchService) toSearchAppResponse(search *entity.SearchListItem) map[string]interface{} { result := map[string]interface{}{ "id": search.ID, "tenant_id": search.TenantID, @@ -177,16 +177,15 @@ func (s *SearchService) toSearchAppResponse(search *entity.Search) map[string]in "create_time": search.CreateTime, "update_time": search.UpdateTime, "search_config": map[string]interface{}(search.SearchConfig), + "nickname": ownerNickname(search.Nickname, search.TenantID), } if search.Avatar != nil { result["avatar"] = *search.Avatar } - - // Add joined fields from user table - // Note: These fields are populated by the DAO query with Select clause - // but GORM will map them to the model's embedded fields if available - // We need to handle the extra fields manually + if search.TenantAvatar != nil { + result["tenant_avatar"] = *search.TenantAvatar + } return result } diff --git a/internal/service/search_test.go b/internal/service/search_test.go index 62d24c7618..a117ca8267 100644 --- a/internal/service/search_test.go +++ b/internal/service/search_test.go @@ -33,6 +33,22 @@ func setupSearchServiceTestDB(t *testing.T) { pushServiceDB(t, db) } +func createSearchServiceTestSearch(t *testing.T, id, tenantID, name string) { + t.Helper() + + status := string(entity.StatusValid) + if err := dao.DB.Create(&entity.Search{ + ID: id, + TenantID: tenantID, + Name: name, + CreatedBy: tenantID, + SearchConfig: entity.JSONMap{}, + Status: &status, + }).Error; err != nil { + t.Fatalf("failed to create search: %v", err) + } +} + func TestSearchServiceCreateRejectsEmptyName(t *testing.T) { setupSearchServiceTestDB(t) @@ -106,3 +122,44 @@ func TestSearchServiceCreateAndUpdateRoundTrip(t *testing.T) { t.Fatalf("expected persisted name, got %q", persisted.Name) } } + +func TestSearchServiceListSearchesReturnsOwnerDisplayFields(t *testing.T) { + setupSearchServiceTestDB(t) + + if err := dao.DB.Create(&entity.User{ + ID: "user-1", + Nickname: "search owner", + Status: sptr("1"), + }).Error; err != nil { + t.Fatalf("failed to create user: %v", err) + } + createSearchServiceTestSearch(t, "search-1", "user-1", "Search One") + + result, err := NewSearchService().ListSearches("user-1", "", 0, 0, "create_time", true, nil) + if err != nil { + t.Fatalf("ListSearches failed: %v", err) + } + if result.Total != 1 || len(result.SearchApps) != 1 { + t.Fatalf("expected one search app, got total=%d len=%d", result.Total, len(result.SearchApps)) + } + if got, want := result.SearchApps[0]["nickname"], "search owner"; got != want { + t.Fatalf("nickname = %v, want %v", got, want) + } +} + +func TestSearchServiceListSearchesNicknameFallsBackToTenantID(t *testing.T) { + setupSearchServiceTestDB(t) + + createSearchServiceTestSearch(t, "search-1", "user-1", "Search One") + + result, err := NewSearchService().ListSearches("user-1", "", 0, 0, "create_time", true, nil) + if err != nil { + t.Fatalf("ListSearches failed: %v", err) + } + if result.Total != 1 || len(result.SearchApps) != 1 { + t.Fatalf("expected one search app, got total=%d len=%d", result.Total, len(result.SearchApps)) + } + if got, want := result.SearchApps[0]["nickname"], "user-1"; got != want { + t.Fatalf("nickname = %v, want %v", got, want) + } +}