Files
soul-yongping/soul-api/internal/handler/book.go

238 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"net/http"
"strconv"
"strings"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// BookAllChapters GET /api/book/all-chapters 返回所有章节(列表,来自 chapters 表)
func BookAllChapters(c *gin.Context) {
var list []model.Chapter
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
}
// 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
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
}
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
out := gin.H{
"success": true,
"data": ch,
"content": ch.Content,
"chapterTitle": ch.ChapterTitle,
"partTitle": ch.PartTitle,
"id": ch.ID,
"mid": ch.MID,
"sectionTitle": ch.SectionTitle,
}
if ch.IsFree != nil {
out["isFree"] = *ch.IsFree
}
if ch.Price != nil {
out["price"] = *ch.Price
}
c.JSON(http.StatusOK, out)
}
// BookChapters GET/POST/PUT/DELETE /api/book/chapters与 app/api/book/chapters 一致,用 GORM
func BookChapters(c *gin.Context) {
db := database.DB()
switch c.Request.Method {
case http.MethodGet:
partId := c.Query("partId")
status := c.Query("status")
if status == "" {
status = "published"
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "100"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 500 {
pageSize = 100
}
q := db.Model(&model.Chapter{})
if partId != "" {
q = q.Where("part_id = ?", partId)
}
if status != "" && status != "all" {
q = q.Where("status = ?", status)
}
var total int64
q.Count(&total)
var list []model.Chapter
q.Order("sort_order ASC, id ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list)
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"list": list, "total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
},
})
return
case http.MethodPost:
var body model.Chapter
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求体无效"})
return
}
if body.ID == "" || body.PartID == "" || body.ChapterID == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少必要字段 id/partId/chapterId"})
return
}
if err := db.Create(&body).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
return
case http.MethodPut:
var body model.Chapter
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
return
}
if err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(map[string]interface{}{
"part_title": body.PartTitle, "chapter_title": body.ChapterTitle, "section_title": body.SectionTitle,
"content": body.Content, "word_count": body.WordCount, "is_free": body.IsFree, "price": body.Price,
"sort_order": body.SortOrder, "status": body.Status,
}).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
return
case http.MethodDelete:
id := c.Query("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
return
}
if err := db.Where("id = ?", id).Delete(&model.Chapter{}).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
return
}
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
}
// BookHot GET /api/book/hot
func BookHot(c *gin.Context) {
var list []model.Chapter
database.DB().Order("sort_order ASC, id ASC").Limit(10).Find(&list)
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
}
// BookLatestChapters GET /api/book/latest-chapters
func BookLatestChapters(c *gin.Context) {
var list []model.Chapter
database.DB().Order("updated_at DESC, id ASC").Limit(20).Find(&list)
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
}
func escapeLikeBook(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "%", "\\%")
s = strings.ReplaceAll(s, "_", "\\_")
return s
}
// BookSearch GET /api/book/search?q= 章节搜索(与 /api/search 逻辑一致)
func BookSearch(c *gin.Context) {
q := strings.TrimSpace(c.Query("q"))
if q == "" {
c.JSON(http.StatusOK, gin.H{"success": true, "results": []interface{}{}, "total": 0, "keyword": ""})
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
}
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"
}
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,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "results": results, "total": len(results), "keyword": q})
}
// BookStats GET /api/book/stats
func BookStats(c *gin.Context) {
var total int64
database.DB().Model(&model.Chapter{}).Count(&total)
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalChapters": total}})
}
// BookSync GET/POST /api/book/sync
func BookSync(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步由 DB 维护"})
}