1. Bug修复: - 修复Markdown星号/下划线在小程序端原样显示问题(markdownToHtml增加__和_支持,contentParser增加Markdown格式剥离) - 修复@提及无反应(MentionSuggestion使用ref保持persons最新值,解决闭包捕获空数组问题) - 修复#链接标签点击"未找到小程序配置"(增加appId直接跳转降级路径) 2. 分享功能优化: - "分享到朋友圈"改为"分享给好友"(open-type从shareTimeline改为share) - 90%收益提示移到分享按钮下方 - 阅读20%后向上滑动弹出分享浮层提示(4秒自动消失) 3. 代付功能: - 后端:新增UserBalance/BalanceTransaction/GiftUnlock三个模型 - 后端:新增8个余额相关API(查询/充值/充值确认/代付/领取/退款/交易记录/礼物信息) - 小程序:阅读页新增"代付分享"按钮,支持用余额为好友解锁章节 - 分享链接携带gift参数,好友打开自动领取解锁 Made-with: Cursor
110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
|
||
"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,参数化)
|
||
// 优化:先搜标题(快),再搜内容(慢),不加载完整 content 到内存
|
||
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) + "%"
|
||
db := database.DB()
|
||
|
||
// 第一步:标题匹配(快速,不加载 content)
|
||
type searchRow struct {
|
||
ID string `gorm:"column:id"`
|
||
MID uint `gorm:"column:mid"`
|
||
SectionTitle string `gorm:"column:section_title"`
|
||
PartTitle string `gorm:"column:part_title"`
|
||
ChapterTitle string `gorm:"column:chapter_title"`
|
||
Price *float64 `gorm:"column:price"`
|
||
IsFree *bool `gorm:"column:is_free"`
|
||
Snippet string `gorm:"column:snippet"`
|
||
}
|
||
|
||
var titleMatches []searchRow
|
||
db.Model(&model.Chapter{}).
|
||
Select("id, mid, section_title, part_title, chapter_title, price, is_free, '' as snippet").
|
||
Where("section_title LIKE ?", pattern).
|
||
Order("sort_order ASC, id ASC").
|
||
Limit(30).
|
||
Find(&titleMatches)
|
||
|
||
titleIDs := make(map[string]bool, len(titleMatches))
|
||
for _, m := range titleMatches {
|
||
titleIDs[m.ID] = true
|
||
}
|
||
|
||
// 第二步:内容匹配(排除已命中标题的,用 SQL 提取摘要避免加载完整 content)
|
||
remaining := 50 - len(titleMatches)
|
||
var contentMatches []searchRow
|
||
if remaining > 0 {
|
||
contentQ := db.Model(&model.Chapter{}).
|
||
Select("id, mid, section_title, part_title, chapter_title, price, is_free, "+
|
||
"CONCAT(CASE WHEN LOCATE(?, content) > 60 THEN '...' ELSE '' END, "+
|
||
"SUBSTRING(content, GREATEST(1, LOCATE(?, content) - 50), 200), "+
|
||
"CASE WHEN LENGTH(content) > LOCATE(?, content) + 150 THEN '...' ELSE '' END) as snippet",
|
||
q, q, q).
|
||
Where("content LIKE ?", pattern)
|
||
if len(titleIDs) > 0 {
|
||
ids := make([]string, 0, len(titleIDs))
|
||
for id := range titleIDs {
|
||
ids = append(ids, id)
|
||
}
|
||
contentQ = contentQ.Where("id NOT IN ?", ids)
|
||
}
|
||
contentQ.Order("sort_order ASC, id ASC").
|
||
Limit(remaining).
|
||
Find(&contentMatches)
|
||
}
|
||
|
||
results := make([]gin.H, 0, len(titleMatches)+len(contentMatches))
|
||
for _, ch := range titleMatches {
|
||
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": "title", "score": 10, "snippet": "",
|
||
})
|
||
}
|
||
for _, ch := range contentMatches {
|
||
price := 1.0
|
||
if ch.Price != nil {
|
||
price = *ch.Price
|
||
}
|
||
snippet := ch.Snippet
|
||
if len([]rune(snippet)) > 200 {
|
||
snippet = string([]rune(snippet)[:200]) + "..."
|
||
}
|
||
results = append(results, gin.H{
|
||
"id": ch.ID, "title": ch.SectionTitle, "partTitle": ch.PartTitle, "chapterTitle": ch.ChapterTitle,
|
||
"price": price, "isFree": ch.IsFree, "matchType": "content", "score": 5, "snippet": snippet,
|
||
})
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{"keyword": q, "total": len(results), "results": results},
|
||
})
|
||
}
|