更新小程序首页精选推荐逻辑,调整展示的章节数据源为排名接口,优化展开功能以支持动态加载更多章节。修复图标组件的SVG映射,确保图标显示一致性。更新开发环境配置为本地地址,提升开发体验。

This commit is contained in:
Alex-larget
2026-03-20 17:02:09 +08:00
parent 1b87fa92f7
commit 905e8f1e8d
24 changed files with 337 additions and 161 deletions

View File

@@ -50,7 +50,7 @@ var allChaptersSelectCols = []string{
var chapterMetaCols = []string{
"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",
"is_new", "edition_standard", "edition_premium", "hot_score", "preview_percent", "created_at", "updated_at",
}
// allChaptersCache 内存缓存,减轻 DB 压力30 秒 TTL
@@ -621,12 +621,19 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
userID := c.Query("userId")
isPremium := ch.EditionPremium != nil && *ch.EditionPremium
var returnContent string
effectivePreviewPercent := 20 // 默认小程序付费墙「已阅读X%」展示用
if isFree {
returnContent = ch.Content
effectivePreviewPercent = 100
} else if checkUserChapterAccess(db, userID, ch.ID, isPremium) {
returnContent = ch.Content
effectivePreviewPercent = 100
} else {
percent := getUnpaidPreviewPercent(db)
if ch.PreviewPercent != nil && *ch.PreviewPercent > 0 {
percent = *ch.PreviewPercent
}
effectivePreviewPercent = percent
returnContent = previewContent(ch.Content, percent)
}
@@ -635,15 +642,16 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
chForResponse.Content = returnContent
out := gin.H{
"success": true,
"data": chForResponse,
"content": returnContent,
"chapterTitle": ch.ChapterTitle,
"partTitle": ch.PartTitle,
"id": ch.ID,
"mid": ch.MID,
"sectionTitle": ch.SectionTitle,
"isFree": isFree,
"success": true,
"data": chForResponse,
"content": returnContent,
"chapterTitle": ch.ChapterTitle,
"partTitle": ch.PartTitle,
"id": ch.ID,
"mid": ch.MID,
"sectionTitle": ch.SectionTitle,
"isFree": isFree,
"previewPercent": effectivePreviewPercent, // 小程序付费墙「已阅读X%」展示用
}
// 文章详情内直接输出上一篇/下一篇,省去单独请求
if list := getOrderedChapterList(); len(list) > 0 {
@@ -808,8 +816,8 @@ func bookHotChaptersSorted(db *gorm.DB, limit int) []model.Chapter {
}
// 按阅读量降序、同量按 updated_at 降序
type withSort struct {
ch model.Chapter
cnt int64
ch model.Chapter
cnt int64
}
withCnt := make([]withSort, 0, len(all))
for _, c := range all {
@@ -883,9 +891,53 @@ func BookRecommended(c *gin.Context) {
if i < len(tags) {
tag = tags[i]
}
// 与管理端内容排行榜 sectionListItem 字段一致,便于两端数据对齐
out = append(out, gin.H{
"id": s.ID,
"mid": s.MID,
"title": s.Title,
"sectionTitle": s.Title, // 兼容小程序旧字段
"partTitle": s.PartTitle,
"chapterTitle": s.ChapterTitle,
"tag": tag,
"isFree": s.IsFree,
"price": s.Price,
"isNew": s.IsNew,
"clickCount": s.ClickCount,
"payCount": s.PayCount,
"hotScore": s.HotScore,
"isPinned": s.IsPinned,
})
}
cache.Set(context.Background(), cache.KeyBookRecommended, out, cache.BookRelatedTTL)
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
}
// BookRanking GET /api/miniprogram/book/ranking 内容排行榜(与 recommended 同算法,支持 limit供精选推荐展开用
func BookRanking(c *gin.Context) {
limit := 50
if l := c.Query("limit"); l != "" {
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 50 {
limit = n
}
}
sections, err := computeArticleRankingSections(database.DB())
if err != nil || len(sections) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "data": []gin.H{}})
return
}
if len(sections) < limit {
limit = len(sections)
}
tags := []string{"热门", "推荐", "精选"}
out := make([]gin.H, 0, limit)
for i := 0; i < limit; i++ {
s := sections[i]
tag := tags[i%len(tags)]
out = append(out, gin.H{
"id": s.ID,
"mid": s.MID,
"title": s.Title,
"sectionTitle": s.Title,
"partTitle": s.PartTitle,
"chapterTitle": s.ChapterTitle,
@@ -893,9 +945,12 @@ func BookRecommended(c *gin.Context) {
"isFree": s.IsFree,
"price": s.Price,
"isNew": s.IsNew,
"clickCount": s.ClickCount,
"payCount": s.PayCount,
"hotScore": s.HotScore,
"isPinned": s.IsPinned,
})
}
cache.Set(context.Background(), cache.KeyBookRecommended, out, cache.BookRelatedTTL)
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
}

View File

@@ -700,6 +700,10 @@ func DBConfigPost(c *gin.Context) {
return
}
cache.InvalidateConfig()
// 排名权重或置顶配置变更时,使精选推荐缓存失效,与管理端内容排行榜保持一致
if body.Key == "article_ranking_weights" || body.Key == "pinned_section_ids" {
cache.InvalidateBookCache()
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "配置保存成功"})
}

View File

@@ -377,6 +377,7 @@ func DBBookAction(c *gin.Context) {
"chapterTitle": ch.ChapterTitle,
"editionStandard": ch.EditionStandard,
"editionPremium": ch.EditionPremium,
"previewPercent": ch.PreviewPercent,
},
})
return
@@ -517,6 +518,7 @@ func DBBookAction(c *gin.Context) {
TargetPartTitle string `json:"targetPartTitle"`
TargetChapterTitle string `json:"targetChapterTitle"`
ID string `json:"id"`
NewID string `json:"newId"` // 修改章节 ID 时传入,后端会更新 id 列
Title string `json:"title"`
Content string `json:"content"`
Price *float64 `json:"price"`
@@ -524,6 +526,7 @@ func DBBookAction(c *gin.Context) {
IsNew *bool `json:"isNew"` // stitch_soul标记最新新增
EditionStandard *bool `json:"editionStandard"` // 是否属于普通版
EditionPremium *bool `json:"editionPremium"` // 是否属于增值版
PreviewPercent *int `json:"previewPercent"` // 未解锁显示前 N%,空则用全局
PartID string `json:"partId"`
PartTitle string `json:"partTitle"`
ChapterID string `json:"chapterId"`
@@ -684,6 +687,14 @@ func DBBookAction(c *gin.Context) {
if body.HotScore != nil {
updates["hot_score"] = *body.HotScore
}
if body.PreviewPercent != nil {
p := *body.PreviewPercent
if p <= 0 || p > 100 {
updates["preview_percent"] = nil // 无效值则清空,用全局
} else {
updates["preview_percent"] = p
}
}
if body.PartID != "" {
updates["part_id"] = body.PartID
}
@@ -696,6 +707,9 @@ func DBBookAction(c *gin.Context) {
if body.ChapterTitle != "" {
updates["chapter_title"] = body.ChapterTitle
}
if body.NewID != "" && body.NewID != body.ID {
updates["id"] = body.NewID
}
var existing model.Chapter
err = db.Where("id = ?", body.ID).First(&existing).Error
if err == gorm.ErrRecordNotFound {
@@ -741,6 +755,9 @@ func DBBookAction(c *gin.Context) {
if body.IsNew != nil {
ch.IsNew = body.IsNew
}
if body.PreviewPercent != nil && *body.PreviewPercent > 0 && *body.PreviewPercent <= 100 {
ch.PreviewPercent = body.PreviewPercent
}
if err := db.Create(&ch).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
@@ -761,7 +778,12 @@ func DBBookAction(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
cache.InvalidateChapterContentByID(body.ID)
// id 变更后需按 mid 失效缓存(按 id 查会失败)
if body.NewID != "" && body.NewID != body.ID {
cache.InvalidateChapterContent(existing.MID)
} else {
cache.InvalidateChapterContentByID(body.ID)
}
cache.InvalidateBookParts()
InvalidateChaptersByPartCache()
cache.InvalidateBookCache()