feat: 内容管理第5批优化 - Bug修复 + 分享功能 + 代付功能

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
This commit is contained in:
卡若
2026-03-15 09:20:27 +08:00
parent 8778a42429
commit 991e17698c
260 changed files with 26780 additions and 1026 deletions

View File

@@ -24,7 +24,8 @@ var excludeParts = []string{"序言", "尾声", "附录"}
// 支持 excludeFixed=1排除序言、尾声、附录目录页固定模块不参与中间篇章
func BookAllChapters(c *gin.Context) {
db := database.DB()
q := db.Model(&model.Chapter{})
q := db.Model(&model.Chapter{}).
Select("mid, id, part_id, part_title, chapter_id, chapter_title, section_title, word_count, is_free, price, sort_order, status, is_new, edition_standard, edition_premium, hot_score, created_at, updated_at")
if c.Query("excludeFixed") == "1" {
for _, p := range excludeParts {
q = q.Where("part_title NOT LIKE ?", "%"+p+"%")
@@ -430,7 +431,8 @@ func escapeLikeBook(s string) string {
return s
}
// BookSearch GET /api/book/search?q= 章节搜索(与 /api/search 逻辑一致)
// BookSearch GET /api/book/search?q= 章节搜索
// 优化:先搜标题(快),再搜内容(慢),不加载完整 content
func BookSearch(c *gin.Context) {
q := strings.TrimSpace(c.Query("q"))
if q == "" {
@@ -438,26 +440,57 @@ func BookSearch(c *gin.Context) {
return
}
pattern := "%" + escapeLikeBook(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(20).
Find(&list).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "results": []interface{}{}, "total": 0, "keyword": q})
return
db := database.DB()
type row 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"`
IsFree *bool `gorm:"column:is_free"`
}
lowerQ := strings.ToLower(q)
results := make([]gin.H, 0, len(list))
for _, ch := range list {
matchType := "content"
if strings.Contains(strings.ToLower(ch.SectionTitle), lowerQ) {
matchType = "title"
var titleHits []row
db.Model(&model.Chapter{}).
Select("id, mid, section_title, part_title, chapter_title, is_free").
Where("section_title LIKE ?", pattern).
Order("sort_order ASC, id ASC").
Limit(15).
Find(&titleHits)
titleIDs := make(map[string]bool, len(titleHits))
for _, h := range titleHits {
titleIDs[h.ID] = true
}
remaining := 20 - len(titleHits)
var contentHits []row
if remaining > 0 {
cq := db.Model(&model.Chapter{}).
Select("id, mid, section_title, part_title, chapter_title, is_free").
Where("content LIKE ?", pattern)
if len(titleIDs) > 0 {
ids := make([]string, 0, len(titleIDs))
for id := range titleIDs {
ids = append(ids, id)
}
cq = cq.Where("id NOT IN ?", ids)
}
cq.Order("sort_order ASC, id ASC").Limit(remaining).Find(&contentHits)
}
results := make([]gin.H, 0, len(titleHits)+len(contentHits))
for _, ch := range titleHits {
results = append(results, gin.H{
"id": ch.ID, "mid": ch.MID, "title": ch.SectionTitle, "part": ch.PartTitle, "chapter": ch.ChapterTitle,
"isFree": ch.IsFree, "matchType": matchType,
"isFree": ch.IsFree, "matchType": "title",
})
}
for _, ch := range contentHits {
results = append(results, gin.H{
"id": ch.ID, "mid": ch.MID, "title": ch.SectionTitle, "part": ch.PartTitle, "chapter": ch.ChapterTitle,
"isFree": ch.IsFree, "matchType": "content",
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "results": results, "total": len(results), "keyword": q})