238 lines
7.3 KiB
Go
238 lines
7.3 KiB
Go
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 维护"})
|
||
}
|