82 lines
2.1 KiB
Go
82 lines
2.1 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
"unicode/utf8"
|
||
|
||
"soul-api/internal/database"
|
||
"soul-api/internal/model"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// escapeLike 转义 LIKE 中的 % _ \,防止注入与通配符滥用
|
||
func escapeLike(s string) string {
|
||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||
s = strings.ReplaceAll(s, "%", "\\%")
|
||
s = strings.ReplaceAll(s, "_", "\\_")
|
||
return s
|
||
}
|
||
|
||
// SearchGet GET /api/search?q= 从 chapters 表搜索(GORM,参数化)
|
||
func SearchGet(c *gin.Context) {
|
||
q := strings.TrimSpace(c.Query("q"))
|
||
if q == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请输入搜索关键词"})
|
||
return
|
||
}
|
||
pattern := "%" + escapeLike(q) + "%"
|
||
var list []model.Chapter
|
||
err := database.DB().Model(&model.Chapter{}).
|
||
Where("section_title LIKE ? OR content LIKE ?", pattern, pattern).
|
||
Order("sort_order ASC, id ASC").
|
||
Limit(50).
|
||
Find(&list).Error
|
||
if err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"keyword": q, "total": 0, "results": []interface{}{}}})
|
||
return
|
||
}
|
||
lowerQ := strings.ToLower(q)
|
||
results := make([]gin.H, 0, len(list))
|
||
for _, ch := range list {
|
||
matchType := "content"
|
||
score := 5
|
||
if strings.Contains(strings.ToLower(ch.SectionTitle), lowerQ) {
|
||
matchType = "title"
|
||
score = 10
|
||
}
|
||
snippet := ""
|
||
pos := strings.Index(strings.ToLower(ch.Content), lowerQ)
|
||
if pos >= 0 && len(ch.Content) > 0 {
|
||
start := pos - 50
|
||
if start < 0 {
|
||
start = 0
|
||
}
|
||
end := pos + utf8.RuneCountInString(q) + 50
|
||
if end > len(ch.Content) {
|
||
end = len(ch.Content)
|
||
}
|
||
snippet = ch.Content[start:end]
|
||
if start > 0 {
|
||
snippet = "..." + snippet
|
||
}
|
||
if end < len(ch.Content) {
|
||
snippet = snippet + "..."
|
||
}
|
||
}
|
||
price := 1.0
|
||
if ch.Price != nil {
|
||
price = *ch.Price
|
||
}
|
||
results = append(results, gin.H{
|
||
"id": ch.ID, "title": ch.SectionTitle, "partTitle": ch.PartTitle, "chapterTitle": ch.ChapterTitle,
|
||
"price": price, "isFree": ch.IsFree, "matchType": matchType, "score": score, "snippet": snippet,
|
||
})
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{"keyword": q, "total": len(results), "results": results},
|
||
})
|
||
}
|