feat: 支持章节通过 mid 进行访问,优化阅读跳转逻辑。新增章节数据结构,包含章节的 mid 信息,提升用户体验。更新 API 以支持通过 mid 查询章节内容,确保兼容性与灵活性。

This commit is contained in:
乘风
2026-02-12 15:52:35 +08:00
parent 046e686cda
commit a571583be4
18 changed files with 353 additions and 391 deletions

View File

@@ -22,16 +22,40 @@ func BookAllChapters(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
}
// BookChapterByID GET /api/book/chapter/:id
// 同时兼容小程序:将 content/chapterTitle/partTitle 等放到顶层,便于 miniprogram 直接 res.content
// BookChapterByID GET /api/book/chapter/:id 按业务 id 查询(兼容旧链接)
func BookChapterByID(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
return
}
findChapterAndRespond(c, func(db *gorm.DB) *gorm.DB {
return db.Where("id = ?", id)
})
}
// BookChapterByMID GET /api/book/chapter/by-mid/:mid 按自增主键 mid 查询(新链接推荐)
func BookChapterByMID(c *gin.Context) {
midStr := c.Param("mid")
if midStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 mid"})
return
}
mid, err := strconv.Atoi(midStr)
if err != nil || mid < 1 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "mid 必须为正整数"})
return
}
findChapterAndRespond(c, func(db *gorm.DB) *gorm.DB {
return db.Where("mid = ?", mid)
})
}
// findChapterAndRespond 按条件查章节并返回统一格式
func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
var ch model.Chapter
if err := database.DB().Where("id = ?", id).First(&ch).Error; err != nil {
db := database.DB()
if err := whereFn(db).First(&ch).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "章节不存在"})
return
@@ -39,14 +63,14 @@ func BookChapterByID(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
// 返回格式:顶层携带 content/chapterTitle/partTitle 等,供小程序 read 页直接使用
out := gin.H{
"success": true,
"data": ch,
"content": ch.Content,
"success": true,
"data": ch,
"content": ch.Content,
"chapterTitle": ch.ChapterTitle,
"partTitle": ch.PartTitle,
"id": ch.ID,
"partTitle": ch.PartTitle,
"id": ch.ID,
"mid": ch.MID,
"sectionTitle": ch.SectionTitle,
}
if ch.IsFree != nil {
@@ -193,7 +217,7 @@ func BookSearch(c *gin.Context) {
matchType = "title"
}
results = append(results, gin.H{
"id": ch.ID, "title": ch.SectionTitle, "part": ch.PartTitle, "chapter": ch.ChapterTitle,
"id": ch.ID, "mid": ch.MID, "title": ch.SectionTitle, "part": ch.PartTitle, "chapter": ch.ChapterTitle,
"isFree": ch.IsFree, "matchType": matchType,
})
}

View File

@@ -268,12 +268,21 @@ func UserPurchaseStatus(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "用户不存在"})
return
}
var orderRows []struct{ ProductID string }
db.Raw("SELECT DISTINCT product_id FROM orders WHERE user_id = ? AND status = ? AND product_type = ?", userId, "paid", "section").Scan(&orderRows)
var orderRows []struct {
ProductID string
MID int
}
db.Raw(`SELECT DISTINCT o.product_id, c.mid FROM orders o
LEFT JOIN chapters c ON c.id = o.product_id
WHERE o.user_id = ? AND o.status = ? AND o.product_type = ?`, userId, "paid", "section").Scan(&orderRows)
purchasedSections := make([]string, 0, len(orderRows))
sectionMidMap := make(map[string]int)
for _, r := range orderRows {
if r.ProductID != "" {
purchasedSections = append(purchasedSections, r.ProductID)
if r.MID > 0 {
sectionMidMap[r.ProductID] = r.MID
}
}
}
// 匹配次数配额:纯计算(订单 + match_records
@@ -290,6 +299,7 @@ func UserPurchaseStatus(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
"hasFullBook": user.HasFullBook != nil && *user.HasFullBook,
"purchasedSections": purchasedSections,
"sectionMidMap": sectionMidMap,
"purchasedCount": len(purchasedSections),
"matchCount": matchQuota.PurchasedTotal,
"matchQuota": gin.H{

View File

@@ -2,9 +2,10 @@ package model
import "time"
// Chapter 对应表 chapters与 Prisma 一致JSON 小写驼峰
// Chapter 对应表 chaptersmid 为自增主键id 保留业务标识如 1.1、preface
type Chapter struct {
ID string `gorm:"column:id;primaryKey;size:20" json:"id"`
MID int `gorm:"column:mid;primaryKey;autoIncrement" json:"mid"`
ID string `gorm:"column:id;size:20;uniqueIndex" json:"id"`
PartID string `gorm:"column:part_id;size:20" json:"partId"`
PartTitle string `gorm:"column:part_title;size:100" json:"partTitle"`
ChapterID string `gorm:"column:chapter_id;size:20" json:"chapterId"`

View File

@@ -75,6 +75,7 @@ func Setup(cfg *config.Config) *gin.Engine {
// ----- 书籍/章节 -----
api.GET("/book/all-chapters", handler.BookAllChapters)
api.GET("/book/chapter/:id", handler.BookChapterByID)
api.GET("/book/chapter/by-mid/:mid", handler.BookChapterByMID)
api.GET("/book/chapters", handler.BookChapters)
api.POST("/book/chapters", handler.BookChapters)
api.PUT("/book/chapters", handler.BookChapters)
@@ -218,6 +219,7 @@ func Setup(cfg *config.Config) *gin.Engine {
miniprogram.GET("/qrcode/image", handler.MiniprogramQrcodeImage)
miniprogram.GET("/book/all-chapters", handler.BookAllChapters)
miniprogram.GET("/book/chapter/:id", handler.BookChapterByID)
miniprogram.GET("/book/chapter/by-mid/:mid", handler.BookChapterByMID)
miniprogram.GET("/book/hot", handler.BookHot)
miniprogram.GET("/book/search", handler.BookSearch)
miniprogram.GET("/book/stats", handler.BookStats)