加强了多个组件的预览百分比处理。
在 API 中新增了章节专属的预览百分比,并更新了相关模型和处理器。 改进了阅读场景下确定有效预览百分比的逻辑。 更新了小程序配置,加入了新的阅读页面入口。
This commit is contained in:
@@ -58,14 +58,14 @@ func sortChaptersByNaturalID(list []model.Chapter) {
|
||||
var allChaptersSelectCols = []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",
|
||||
}
|
||||
|
||||
// chapterMetaCols 章节详情元数据(不含 content),用于 content 缓存命中时的轻量查询
|
||||
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
|
||||
@@ -643,6 +643,17 @@ func getUnpaidPreviewPercent(db *gorm.DB) int {
|
||||
return 20
|
||||
}
|
||||
|
||||
// effectivePreviewPercent 章节 preview_percent 优先(1~100),否则用全局 unpaid_preview_percent
|
||||
func effectivePreviewPercent(db *gorm.DB, ch *model.Chapter) int {
|
||||
if ch != nil && ch.PreviewPercent != nil {
|
||||
p := *ch.PreviewPercent
|
||||
if p >= 1 && p <= 100 {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return getUnpaidPreviewPercent(db)
|
||||
}
|
||||
|
||||
// previewContent 取内容的前 percent%(不少于 100 字,上限 500 字),并追加省略提示
|
||||
func previewContent(content string, percent int) string {
|
||||
total := utf8.RuneCountInString(content)
|
||||
@@ -712,12 +723,13 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
|
||||
isPremium := ch.EditionPremium != nil && *ch.EditionPremium
|
||||
hasFullAccess := isFree || checkUserChapterAccess(db, userID, ch.ID, isPremium)
|
||||
var returnContent string
|
||||
var unpaidPreviewPercent int // 未解锁时试读比例(system_config.unpaid_preview_percent);已解锁时不写入响应
|
||||
// 未解锁:正文截取用「章节覆盖 ∪ 全局」;响应里顶层 previewPercent 仅表示全局默认,data.previewPercent 表示章节私有(model omitempty)
|
||||
var effectiveUnpaidPreviewPercent int
|
||||
if hasFullAccess {
|
||||
returnContent = ch.Content
|
||||
} else {
|
||||
unpaidPreviewPercent = getUnpaidPreviewPercent(db)
|
||||
returnContent = previewContent(ch.Content, unpaidPreviewPercent)
|
||||
effectiveUnpaidPreviewPercent = effectivePreviewPercent(db, &ch)
|
||||
returnContent = previewContent(ch.Content, effectiveUnpaidPreviewPercent)
|
||||
}
|
||||
|
||||
// data 中的 content 必须与外层 content 一致,避免泄露完整内容给未授权用户
|
||||
@@ -740,7 +752,7 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
|
||||
"hasFullAccess": hasFullAccess,
|
||||
}
|
||||
if !hasFullAccess {
|
||||
out["previewPercent"] = unpaidPreviewPercent
|
||||
out["previewPercent"] = getUnpaidPreviewPercent(db)
|
||||
}
|
||||
// 文章详情内直接输出上一篇/下一篇,省去单独请求
|
||||
if list := getOrderedChapterList(); len(list) > 0 {
|
||||
|
||||
@@ -41,7 +41,7 @@ func naturalLessSectionID(a, b string) bool {
|
||||
var listSelectCols = []string{
|
||||
"id", "mid", "section_title", "price", "is_free", "is_new",
|
||||
"part_id", "part_title", "chapter_id", "chapter_title", "sort_order",
|
||||
"hot_score", "updated_at",
|
||||
"hot_score", "preview_percent", "updated_at",
|
||||
}
|
||||
|
||||
// sectionListItem 与前端 SectionListItem 一致(小写驼峰)
|
||||
@@ -60,7 +60,8 @@ type sectionListItem struct {
|
||||
ClickCount int64 `json:"clickCount"` // 阅读次数(reading_progress)
|
||||
PayCount int64 `json:"payCount"` // 付款笔数(orders.product_type=section)
|
||||
HotScore float64 `json:"hotScore"` // 热度积分(加权计算)
|
||||
IsPinned bool `json:"isPinned,omitempty"` // 是否置顶(仅 ranking 返回)
|
||||
IsPinned bool `json:"isPinned,omitempty"` // 是否置顶(仅 ranking 返回)
|
||||
PreviewPercent *int `json:"previewPercent,omitempty"`
|
||||
}
|
||||
|
||||
// computeSectionListWithHotScore 计算章节列表(含 hotScore),保持 sort_order 顺序,供 章节管理 树使用
|
||||
@@ -264,19 +265,20 @@ func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem
|
||||
hot = float64(r.HotScore)
|
||||
}
|
||||
item := sectionListItem{
|
||||
ID: r.ID,
|
||||
MID: r.MID,
|
||||
Title: r.SectionTitle,
|
||||
Price: price,
|
||||
IsFree: r.IsFree,
|
||||
IsNew: r.IsNew,
|
||||
PartID: r.PartID,
|
||||
PartTitle: r.PartTitle,
|
||||
ChapterID: r.ChapterID,
|
||||
ChapterTitle: r.ChapterTitle,
|
||||
ClickCount: readCnt,
|
||||
PayCount: payCnt,
|
||||
HotScore: hot,
|
||||
ID: r.ID,
|
||||
MID: r.MID,
|
||||
Title: r.SectionTitle,
|
||||
Price: price,
|
||||
IsFree: r.IsFree,
|
||||
IsNew: r.IsNew,
|
||||
PartID: r.PartID,
|
||||
PartTitle: r.PartTitle,
|
||||
ChapterID: r.ChapterID,
|
||||
ChapterTitle: r.ChapterTitle,
|
||||
ClickCount: readCnt,
|
||||
PayCount: payCnt,
|
||||
HotScore: hot,
|
||||
PreviewPercent: r.PreviewPercent,
|
||||
}
|
||||
if setPinned {
|
||||
item.IsPinned = pinnedSet[r.ID]
|
||||
@@ -286,6 +288,42 @@ func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
// dbBookReadSectionOut 管理端 read 详情:previewPercent 必须始终出现在 JSON(null=走全局),避免 gin.H+nil 被序列化省略
|
||||
type dbBookReadSectionOut struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Price float64 `json:"price"`
|
||||
Content string `json:"content"`
|
||||
IsNew *bool `json:"isNew,omitempty"`
|
||||
PartID string `json:"partId"`
|
||||
PartTitle string `json:"partTitle"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
ChapterTitle string `json:"chapterTitle"`
|
||||
EditionStandard *bool `json:"editionStandard,omitempty"`
|
||||
EditionPremium *bool `json:"editionPremium,omitempty"`
|
||||
PreviewPercent *int `json:"previewPercent"` // 禁止 omitempty,与 chapters 表 preview_percent 对齐
|
||||
}
|
||||
|
||||
func chapterToReadSectionOut(ch *model.Chapter, price float64) dbBookReadSectionOut {
|
||||
if ch == nil {
|
||||
return dbBookReadSectionOut{Price: price}
|
||||
}
|
||||
return dbBookReadSectionOut{
|
||||
ID: ch.ID,
|
||||
Title: ch.SectionTitle,
|
||||
Price: price,
|
||||
Content: ch.Content,
|
||||
IsNew: ch.IsNew,
|
||||
PartID: ch.PartID,
|
||||
PartTitle: ch.PartTitle,
|
||||
ChapterID: ch.ChapterID,
|
||||
ChapterTitle: ch.ChapterTitle,
|
||||
EditionStandard: ch.EditionStandard,
|
||||
EditionPremium: ch.EditionPremium,
|
||||
PreviewPercent: ch.PreviewPercent,
|
||||
}
|
||||
}
|
||||
|
||||
// DBBookAction GET/POST/PUT /api/db/book
|
||||
func DBBookAction(c *gin.Context) {
|
||||
db := database.DB()
|
||||
@@ -336,19 +374,7 @@ func DBBookAction(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"section": gin.H{
|
||||
"id": ch.ID,
|
||||
"title": ch.SectionTitle,
|
||||
"price": price,
|
||||
"content": ch.Content,
|
||||
"isNew": ch.IsNew,
|
||||
"partId": ch.PartID,
|
||||
"partTitle": ch.PartTitle,
|
||||
"chapterId": ch.ChapterID,
|
||||
"chapterTitle": ch.ChapterTitle,
|
||||
"editionStandard": ch.EditionStandard,
|
||||
"editionPremium": ch.EditionPremium,
|
||||
},
|
||||
"section": chapterToReadSectionOut(&ch, price),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -371,19 +397,7 @@ func DBBookAction(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"section": gin.H{
|
||||
"id": ch.ID,
|
||||
"title": ch.SectionTitle,
|
||||
"price": price,
|
||||
"content": ch.Content,
|
||||
"isNew": ch.IsNew,
|
||||
"partId": ch.PartID,
|
||||
"partTitle": ch.PartTitle,
|
||||
"chapterId": ch.ChapterID,
|
||||
"chapterTitle": ch.ChapterTitle,
|
||||
"editionStandard": ch.EditionStandard,
|
||||
"editionPremium": ch.EditionPremium,
|
||||
},
|
||||
"section": chapterToReadSectionOut(&ch, price),
|
||||
})
|
||||
return
|
||||
case "section-orders":
|
||||
@@ -536,6 +550,7 @@ func DBBookAction(c *gin.Context) {
|
||||
ChapterID string `json:"chapterId"`
|
||||
ChapterTitle string `json:"chapterTitle"`
|
||||
HotScore *float64 `json:"hotScore"`
|
||||
PreviewPercent nullablePreviewPercentJSON `json:"previewPercent"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
@@ -703,6 +718,20 @@ func DBBookAction(c *gin.Context) {
|
||||
if body.ChapterTitle != "" {
|
||||
updates["chapter_title"] = body.ChapterTitle
|
||||
}
|
||||
if body.PreviewPercent.Set {
|
||||
if body.PreviewPercent.Val == nil {
|
||||
updates["preview_percent"] = nil
|
||||
} else {
|
||||
p := *body.PreviewPercent.Val
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
updates["preview_percent"] = p
|
||||
}
|
||||
}
|
||||
var existing model.Chapter
|
||||
err = db.Where("id = ?", body.ID).First(&existing).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -748,6 +777,16 @@ func DBBookAction(c *gin.Context) {
|
||||
if body.IsNew != nil {
|
||||
ch.IsNew = body.IsNew
|
||||
}
|
||||
if body.PreviewPercent.Set && body.PreviewPercent.Val != nil {
|
||||
p := *body.PreviewPercent.Val
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
ch.PreviewPercent = &p
|
||||
}
|
||||
if err := db.Create(&ch).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
@@ -812,6 +851,26 @@ func DBBookAction(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||
}
|
||||
|
||||
// nullablePreviewPercentJSON 区分:JSON 未传 key(不改 preview_percent)、null(清空用全局)、数字(章节覆盖)
|
||||
type nullablePreviewPercentJSON struct {
|
||||
Set bool
|
||||
Val *int
|
||||
}
|
||||
|
||||
func (n *nullablePreviewPercentJSON) UnmarshalJSON(data []byte) error {
|
||||
n.Set = true
|
||||
if string(data) == "null" {
|
||||
n.Val = nil
|
||||
return nil
|
||||
}
|
||||
var v int
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
n.Val = &v
|
||||
return nil
|
||||
}
|
||||
|
||||
type reorderItem struct {
|
||||
ID string `json:"id"`
|
||||
PartID string `json:"partId"`
|
||||
|
||||
@@ -37,7 +37,7 @@ func H5ReadPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
percent := getUnpaidPreviewPercent(db)
|
||||
percent := effectivePreviewPercent(db, &ch)
|
||||
preview := h5PreviewContent(ch.Content, percent)
|
||||
|
||||
title := ch.SectionTitle
|
||||
|
||||
Reference in New Issue
Block a user