2026-03-07 22:58:43 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"net/http"
|
2026-03-16 09:21:39 +08:00
|
|
|
|
"strconv"
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
2026-03-17 14:02:09 +08:00
|
|
|
|
"soul-api/internal/cache"
|
2026-03-07 22:58:43 +08:00
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// AdminChaptersList GET /api/admin/chapters 从 chapters 表组树:part -> chapters -> sections
|
|
|
|
|
|
func AdminChaptersList(c *gin.Context) {
|
2026-03-16 09:21:39 +08:00
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
|
page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
|
|
|
|
|
if pageSize < 1 {
|
|
|
|
|
|
pageSize = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
if pageSize > 200 {
|
|
|
|
|
|
pageSize = 200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
var list []model.Chapter
|
|
|
|
|
|
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
2026-03-16 09:21:39 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"structure": []interface{}{},
|
|
|
|
|
|
"records": []interface{}{},
|
|
|
|
|
|
"stats": nil,
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"pageSize": pageSize,
|
|
|
|
|
|
"totalPages": 0,
|
|
|
|
|
|
"total": 0,
|
2026-03-16 09:21:39 +08:00
|
|
|
|
}})
|
2026-03-07 22:58:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
type section struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Price float64 `json:"price"`
|
|
|
|
|
|
IsFree bool `json:"isFree"`
|
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
|
EditionStandard *bool `json:"editionStandard,omitempty"`
|
|
|
|
|
|
EditionPremium *bool `json:"editionPremium,omitempty"`
|
|
|
|
|
|
}
|
2026-03-16 09:21:39 +08:00
|
|
|
|
type sectionRecord struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
PartID string `json:"partId"`
|
|
|
|
|
|
PartTitle string `json:"partTitle"`
|
|
|
|
|
|
ChapterID string `json:"chapterId"`
|
|
|
|
|
|
ChapterTitle string `json:"chapterTitle"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Price float64 `json:"price"`
|
|
|
|
|
|
IsFree bool `json:"isFree"`
|
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
|
EditionStandard *bool `json:"editionStandard,omitempty"`
|
|
|
|
|
|
EditionPremium *bool `json:"editionPremium,omitempty"`
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
type chapter struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Sections []section `json:"sections"`
|
|
|
|
|
|
}
|
|
|
|
|
|
type part struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
|
Chapters []chapter `json:"chapters"`
|
|
|
|
|
|
}
|
|
|
|
|
|
partMap := make(map[string]*part)
|
|
|
|
|
|
chapterMap := make(map[string]map[string]*chapter)
|
2026-03-16 09:21:39 +08:00
|
|
|
|
records := make([]sectionRecord, 0, len(list))
|
2026-03-07 22:58:43 +08:00
|
|
|
|
for _, row := range list {
|
|
|
|
|
|
if partMap[row.PartID] == nil {
|
|
|
|
|
|
partMap[row.PartID] = &part{ID: row.PartID, Title: row.PartTitle, Type: "part", Chapters: []chapter{}}
|
|
|
|
|
|
chapterMap[row.PartID] = make(map[string]*chapter)
|
|
|
|
|
|
}
|
|
|
|
|
|
p := partMap[row.PartID]
|
|
|
|
|
|
if chapterMap[row.PartID][row.ChapterID] == nil {
|
|
|
|
|
|
ch := chapter{ID: row.ChapterID, Title: row.ChapterTitle, Sections: []section{}}
|
|
|
|
|
|
p.Chapters = append(p.Chapters, ch)
|
|
|
|
|
|
chapterMap[row.PartID][row.ChapterID] = &p.Chapters[len(p.Chapters)-1]
|
|
|
|
|
|
}
|
|
|
|
|
|
ch := chapterMap[row.PartID][row.ChapterID]
|
|
|
|
|
|
price := 1.0
|
|
|
|
|
|
if row.Price != nil {
|
|
|
|
|
|
price = *row.Price
|
|
|
|
|
|
}
|
|
|
|
|
|
isFree := false
|
|
|
|
|
|
if row.IsFree != nil {
|
|
|
|
|
|
isFree = *row.IsFree
|
|
|
|
|
|
}
|
|
|
|
|
|
st := "published"
|
|
|
|
|
|
if row.Status != nil {
|
|
|
|
|
|
st = *row.Status
|
|
|
|
|
|
}
|
|
|
|
|
|
ch.Sections = append(ch.Sections, section{
|
|
|
|
|
|
ID: row.ID, Title: row.SectionTitle, Price: price, IsFree: isFree, Status: st,
|
|
|
|
|
|
EditionStandard: row.EditionStandard, EditionPremium: row.EditionPremium,
|
|
|
|
|
|
})
|
2026-03-16 09:21:39 +08:00
|
|
|
|
records = append(records, sectionRecord{
|
|
|
|
|
|
ID: row.ID,
|
|
|
|
|
|
PartID: row.PartID,
|
|
|
|
|
|
PartTitle: row.PartTitle,
|
|
|
|
|
|
ChapterID: row.ChapterID,
|
|
|
|
|
|
ChapterTitle: row.ChapterTitle,
|
|
|
|
|
|
Title: row.SectionTitle,
|
|
|
|
|
|
Price: price,
|
|
|
|
|
|
IsFree: isFree,
|
|
|
|
|
|
Status: st,
|
|
|
|
|
|
EditionStandard: row.EditionStandard,
|
|
|
|
|
|
EditionPremium: row.EditionPremium,
|
|
|
|
|
|
})
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
structure := make([]part, 0, len(partMap))
|
|
|
|
|
|
for _, p := range partMap {
|
|
|
|
|
|
structure = append(structure, *p)
|
|
|
|
|
|
}
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
database.DB().Model(&model.Chapter{}).Count(&total)
|
2026-03-16 09:21:39 +08:00
|
|
|
|
totalPages := 0
|
|
|
|
|
|
if pageSize > 0 {
|
|
|
|
|
|
totalPages = (int(total) + pageSize - 1) / pageSize
|
|
|
|
|
|
}
|
|
|
|
|
|
start := (page - 1) * pageSize
|
|
|
|
|
|
if start > len(records) {
|
|
|
|
|
|
start = len(records)
|
|
|
|
|
|
}
|
|
|
|
|
|
end := start + pageSize
|
|
|
|
|
|
if end > len(records) {
|
|
|
|
|
|
end = len(records)
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
2026-03-16 09:21:39 +08:00
|
|
|
|
"data": gin.H{
|
|
|
|
|
|
"structure": structure,
|
|
|
|
|
|
"records": records[start:end],
|
|
|
|
|
|
"stats": gin.H{"totalSections": total},
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"pageSize": pageSize,
|
|
|
|
|
|
"totalPages": totalPages,
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
},
|
2026-03-07 22:58:43 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminChaptersAction POST/PUT/DELETE /api/admin/chapters
|
|
|
|
|
|
func AdminChaptersAction(c *gin.Context) {
|
|
|
|
|
|
var body struct {
|
|
|
|
|
|
Action string `json:"action"`
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
ChapterID string `json:"chapterId"` // 前端兼容:section id
|
|
|
|
|
|
SectionTitle string `json:"sectionTitle"`
|
|
|
|
|
|
Ids []string `json:"ids"` // reorder:新顺序的 section id 列表
|
|
|
|
|
|
Price *float64 `json:"price"`
|
|
|
|
|
|
IsFree *bool `json:"isFree"`
|
|
|
|
|
|
Status *string `json:"status"`
|
|
|
|
|
|
EditionStandard *bool `json:"editionStandard"`
|
|
|
|
|
|
EditionPremium *bool `json:"editionPremium"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
resolveID := func() string {
|
|
|
|
|
|
if body.ID != "" {
|
|
|
|
|
|
return body.ID
|
|
|
|
|
|
}
|
|
|
|
|
|
return body.ChapterID
|
|
|
|
|
|
}
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
if body.Action == "updatePrice" {
|
|
|
|
|
|
id := resolveID()
|
|
|
|
|
|
if id != "" && body.Price != nil {
|
|
|
|
|
|
db.Model(&model.Chapter{}).Where("id = ?", id).Update("price", *body.Price)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Action == "toggleFree" {
|
|
|
|
|
|
id := resolveID()
|
|
|
|
|
|
if id != "" && body.IsFree != nil {
|
|
|
|
|
|
db.Model(&model.Chapter{}).Where("id = ?", id).Update("is_free", *body.IsFree)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Action == "updateStatus" {
|
|
|
|
|
|
id := resolveID()
|
|
|
|
|
|
if id != "" && body.Status != nil {
|
|
|
|
|
|
db.Model(&model.Chapter{}).Where("id = ?", id).Update("status", *body.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Action == "rename" {
|
|
|
|
|
|
id := resolveID()
|
|
|
|
|
|
if id != "" && body.SectionTitle != "" {
|
|
|
|
|
|
db.Model(&model.Chapter{}).Where("id = ?", id).Update("section_title", body.SectionTitle)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Action == "delete" {
|
|
|
|
|
|
id := resolveID()
|
|
|
|
|
|
if id != "" {
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateChapterContentByID(id)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
db.Where("id = ?", id).Delete(&model.Chapter{})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Action == "reorder" && len(body.Ids) > 0 {
|
|
|
|
|
|
for i, id := range body.Ids {
|
|
|
|
|
|
if id != "" {
|
|
|
|
|
|
db.Model(&model.Chapter{}).Where("id = ?", id).Update("sort_order", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Action == "updateEdition" {
|
|
|
|
|
|
id := resolveID()
|
|
|
|
|
|
if id != "" {
|
|
|
|
|
|
updates := make(map[string]interface{})
|
|
|
|
|
|
if body.EditionStandard != nil {
|
|
|
|
|
|
updates["edition_standard"] = *body.EditionStandard
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.EditionPremium != nil {
|
|
|
|
|
|
updates["edition_premium"] = *body.EditionPremium
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(updates) > 0 {
|
|
|
|
|
|
db.Model(&model.Chapter{}).Where("id = ?", id).Updates(updates)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookParts()
|
2026-03-18 12:56:34 +08:00
|
|
|
|
InvalidateChaptersByPartCache()
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-07 22:58:43 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
|
|
|
|
}
|