Files
gochat/internal/canned/service.go
T
2026-06-04 15:44:48 +08:00

92 lines
3.3 KiB
Go

package canned
import (
"context"
"fmt"
"strings"
"gorm.io/gorm"
)
// DBProvider provides database access — implemented by app.App to break the import cycle.
type DBProvider interface {
DB() *gorm.DB
}
// CannedResponseService provides CRUD and search for canned responses.
// Reference: Chatwoot CannedResponse — CRUD + order_by_search scope
// (ranks by short_code match then content match).
type CannedResponseService struct {
db DBProvider
}
// NewCannedResponseService creates a new CannedResponseService.
func NewCannedResponseService(db DBProvider) *CannedResponseService {
return &CannedResponseService{db: db}
}
// Create creates a new canned response.
func (s *CannedResponseService) Create(ctx context.Context, response *CannedResponse) error {
return s.db.DB().WithContext(ctx).Create(response).Error
}
// GetByID retrieves a canned response by ID.
func (s *CannedResponseService) GetByID(ctx context.Context, id uint) (*CannedResponse, error) {
var resp CannedResponse
if err := s.db.DB().WithContext(ctx).First(&resp, id).Error; err != nil {
return nil, err
}
return &resp, nil
}
// Update updates a canned response.
func (s *CannedResponseService) Update(ctx context.Context, id uint, updates map[string]interface{}) error {
return s.db.DB().WithContext(ctx).Model(&CannedResponse{}).Where("id = ?", id).Updates(updates).Error
}
// Delete soft-deletes a canned response.
func (s *CannedResponseService) Delete(ctx context.Context, id uint) error {
return s.db.DB().WithContext(ctx).Delete(&CannedResponse{}, id).Error
}
// ListByAccount returns all canned responses for an account.
func (s *CannedResponseService) ListByAccount(ctx context.Context, accountID uint) ([]CannedResponse, error) {
var responses []CannedResponse
err := s.db.DB().WithContext(ctx).
Where("account_id = ?", accountID).
Order("short_code ASC").
Find(&responses).Error
return responses, err
}
// Search performs a ranked search over canned responses.
// Reference: Chatwoot order_by_search scope — ranks by short_code match then content match.
// Results are ordered so that short_code matches appear first, then content matches.
func (s *CannedResponseService) Search(ctx context.Context, accountID uint, query string) ([]CannedResponse, error) {
if query == "" {
return s.ListByAccount(ctx, accountID)
}
var responses []CannedResponse
// Use a CASE-based ordering: short_code matches rank higher than content matches.
// This mirrors Chatwoot's order_by_search scope.
// The search is case-insensitive.
likePattern := fmt.Sprintf("%%%s%%", strings.ToLower(query))
err := s.db.DB().WithContext(ctx).
Where("account_id = ? AND (LOWER(short_code) LIKE ? OR LOWER(content) LIKE ?)", accountID, likePattern, likePattern).
Order(fmt.Sprintf("CASE WHEN LOWER(short_code) LIKE '%s' THEN 0 ELSE 1 END, short_code ASC", likePattern)).
Find(&responses).Error
return responses, err
}
// GetByShortCode retrieves a canned response by its short code within an account.
func (s *CannedResponseService) GetByShortCode(ctx context.Context, accountID uint, shortCode string) (*CannedResponse, error) {
var resp CannedResponse
if err := s.db.DB().WithContext(ctx).
Where("account_id = ? AND short_code = ?", accountID, shortCode).
First(&resp).Error; err != nil {
return nil, err
}
return &resp, nil
}