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