feat(conversations): align search finder payload

This commit is contained in:
2026-06-07 03:32:51 +08:00
parent f001560c81
commit 9f1ff28521
4 changed files with 141 additions and 20 deletions
@@ -9,7 +9,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/search"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
@@ -465,22 +464,21 @@ func (h *ConversationHandler) Search(c *gin.Context) {
return
}
q := c.Query("q")
if q == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "search query 'q' is required")
p := pagination.Parse(c)
var params service.FilterParams
if err := c.ShouldBindQuery(&params); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
p := pagination.Parse(c)
searchMode := search.ParseSearchMode(c.DefaultQuery("search_mode", ""))
conversations, total, svcErr := h.conversationSvc.Search(c.Request.Context(), accountID, q, p.Offset, p.PerPage, searchMode)
result, svcErr := h.conversationSvc.ListWithFinder(c.Request.Context(), accountID, getUserID(c), params, p.Offset, p.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeConversationList(c.Request.Context(), h.conversationSvc.DB(), conversations, total))
c.JSON(http.StatusOK, serializeConversationSearchList(c.Request.Context(), h.conversationSvc.DB(), result.Conversations, result.Count))
}
// Filter retrieves conversations matching advanced filter criteria.
@@ -897,23 +897,44 @@ func (s *ConversationCrudTestSuite) TestUpdateLabels_NotFound() {
// ========== Search Handler Tests ==========
func (s *ConversationCrudTestSuite) TestSearch_Success() {
skipIfSQLiteForConv(s.T()) // Search uses ILIKE which SQLite doesn't support
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, Content: "needle open", MessageType: string(model.MessageTypeIncoming)}).Error)
resolved := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"}
s.Require().NoError(s.db.Create(resolved).Error)
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: resolved.ID, Content: "needle resolved", MessageType: string(model.MessageTypeOutgoing)}).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search?q=open", nil)
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search?q=needle&status=open", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp struct {
Meta struct {
AllCount int64 `json:"all_count"`
MineCount int64 `json:"mine_count"`
UnassignedCount int64 `json:"unassigned_count"`
AllCount int64 `json:"all_count"`
} `json:"meta"`
Payload []map[string]interface{} `json:"payload"`
Payload []struct {
ID uint `json:"id"`
Contact map[string]any `json:"contact"`
Inbox map[string]any `json:"inbox"`
Messages []map[string]any `json:"messages"`
} `json:"payload"`
}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
assert.Len(s.T(), resp.Payload, 1)
assert.NotContains(s.T(), w.Body.String(), "data")
var raw map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &raw))
rawMeta := raw["meta"].(map[string]any)
assert.NotContains(s.T(), rawMeta, "assigned_count")
assert.Equal(s.T(), int64(2), resp.Meta.AllCount)
assert.Equal(s.T(), int64(2), resp.Meta.UnassignedCount)
assert.Len(s.T(), resp.Payload, 2)
assert.NotZero(s.T(), resp.Payload[0].ID)
assert.Equal(s.T(), s.testContact.Name, resp.Payload[0].Contact["name"])
assert.Equal(s.T(), s.testInbox.Name, resp.Payload[0].Inbox["name"])
assert.NotEmpty(s.T(), resp.Payload[0].Messages)
}
func (s *ConversationCrudTestSuite) TestSearch_InvalidAccountID() {
@@ -929,7 +950,16 @@ func (s *ConversationCrudTestSuite) TestSearch_MissingQuery() {
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp struct {
Meta struct {
AllCount int64 `json:"all_count"`
} `json:"meta"`
Payload []map[string]any `json:"payload"`
}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
assert.Len(s.T(), resp.Payload, 1)
}
// ========== Filter Handler Tests ==========
@@ -8,6 +8,7 @@ import (
"time"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -67,6 +68,36 @@ type chatwootConversationMeta struct {
HMACVerified *bool `json:"hmac_verified,omitempty"`
}
type chatwootConversationSearchResponse struct {
Meta chatwootConversationSearchMeta `json:"meta"`
Payload []chatwootConversationSearchPayload `json:"payload"`
}
type chatwootConversationSearchMeta struct {
MineCount int64 `json:"mine_count"`
UnassignedCount int64 `json:"unassigned_count"`
AllCount int64 `json:"all_count"`
}
type chatwootConversationSearchPayload struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
CreatedAt int64 `json:"created_at"`
Contact map[string]any `json:"contact"`
Inbox map[string]any `json:"inbox"`
Messages []chatwootConversationSearchMessage `json:"messages"`
AccountID uint `json:"account_id"`
Meta chatwootConversationMeta `json:"meta"`
}
type chatwootConversationSearchMessage struct {
Content string `json:"content"`
ID uint `json:"id"`
SenderName string `json:"sender_name,omitempty"`
MessageType string `json:"message_type"`
CreatedAt int64 `json:"created_at"`
}
type chatwootMessageIndexResponse struct {
Meta chatwootMessageIndexMeta `json:"meta"`
Payload []chatwootMessagePayload `json:"payload"`
@@ -109,6 +140,64 @@ func serializeConversationList(ctx context.Context, db *gorm.DB, conversations [
}}
}
func serializeConversationSearchList(ctx context.Context, db *gorm.DB, conversations []model.Conversation, counts service.FilterCountMeta) chatwootConversationSearchResponse {
payload := make([]chatwootConversationSearchPayload, 0, len(conversations))
for i := range conversations {
payload = append(payload, serializeConversationSearch(ctx, db, &conversations[i]))
}
return chatwootConversationSearchResponse{
Meta: chatwootConversationSearchMeta{
MineCount: counts.MineCount,
UnassignedCount: counts.UnassignedCount,
AllCount: counts.AllCount,
},
Payload: payload,
}
}
func serializeConversationSearch(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationSearchPayload {
payload := chatwootConversationSearchPayload{
ID: conversationDisplayID(conversation),
UUID: conversation.UUID,
CreatedAt: conversation.CreatedAt.Unix(),
Contact: map[string]any{"id": conversation.ContactID},
Inbox: map[string]any{"id": conversation.InboxID, "channel_type": conversation.ChannelType},
Messages: []chatwootConversationSearchMessage{},
AccountID: conversation.AccountID,
Meta: serializeConversationMeta(ctx, db, conversation),
}
if db == nil {
return payload
}
var contact model.Contact
if err := db.WithContext(ctx).Where("id = ?", conversation.ContactID).First(&contact).Error; err == nil {
payload.Contact = map[string]any{"id": contact.ID, "name": contact.Name}
}
var inbox model.Inbox
if err := db.WithContext(ctx).Where("id = ?", conversation.InboxID).First(&inbox).Error; err == nil {
payload.Inbox = map[string]any{"id": inbox.ID, "name": inbox.Name, "channel_type": inbox.ChannelType}
}
var messages []model.Message
if err := db.WithContext(ctx).
Where("account_id = ? AND conversation_id = ?", conversation.AccountID, conversation.ID).
Order("id ASC").
Find(&messages).Error; err == nil {
payload.Messages = make([]chatwootConversationSearchMessage, 0, len(messages))
for i := range messages {
payload.Messages = append(payload.Messages, chatwootConversationSearchMessage{
Content: messages[i].Content,
ID: messages[i].ID,
MessageType: messages[i].MessageType,
CreatedAt: messages[i].CreatedAt.Unix(),
})
}
}
return payload
}
func serializeConversationPayloads(ctx context.Context, db *gorm.DB, conversations []model.Conversation) []chatwootConversationPayload {
payload := make([]chatwootConversationPayload, 0, len(conversations))
for i := range conversations {