fix: unable to get owner in chat and search (#17244)

### Summary

As title
This commit is contained in:
Haruko386
2026-07-22 21:27:08 +08:00
committed by GitHub
parent 48b63ab6b0
commit 1d165c7315
11 changed files with 245 additions and 50 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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"`
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"`

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}