2026-03-06 17:52:52 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
2026-03-12 11:36:50 +08:00
|
|
|
|
"encoding/json"
|
2026-03-06 17:52:52 +08:00
|
|
|
|
"net/http"
|
2026-03-12 11:36:50 +08:00
|
|
|
|
"sort"
|
2026-03-14 18:04:05 +08:00
|
|
|
|
"strconv"
|
|
|
|
|
|
"strings"
|
2026-03-12 11:36:50 +08:00
|
|
|
|
"time"
|
2026-03-06 17:52:52 +08:00
|
|
|
|
|
2026-03-17 14:02:09 +08:00
|
|
|
|
"soul-api/internal/cache"
|
2026-03-06 17:52:52 +08:00
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// naturalLessSectionID 对章节 id(如 9.1、9.2、9.10)做自然排序,避免 9.1 < 9.10 < 9.2 的字典序问题
|
|
|
|
|
|
func naturalLessSectionID(a, b string) bool {
|
|
|
|
|
|
partsA := strings.Split(a, ".")
|
|
|
|
|
|
partsB := strings.Split(b, ".")
|
|
|
|
|
|
for i := 0; i < len(partsA) && i < len(partsB); i++ {
|
|
|
|
|
|
na, errA := strconv.Atoi(partsA[i])
|
|
|
|
|
|
nb, errB := strconv.Atoi(partsB[i])
|
|
|
|
|
|
if errA != nil || errB != nil {
|
|
|
|
|
|
if partsA[i] != partsB[i] {
|
|
|
|
|
|
return partsA[i] < partsB[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if na != nb {
|
|
|
|
|
|
return na < nb
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return len(partsA) < len(partsB)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 17:52:52 +08:00
|
|
|
|
// listSelectCols 列表/导出不加载 content,大幅加速
|
|
|
|
|
|
var listSelectCols = []string{
|
2026-03-12 11:36:50 +08:00
|
|
|
|
"id", "mid", "section_title", "price", "is_free", "is_new",
|
2026-03-06 17:52:52 +08:00
|
|
|
|
"part_id", "part_title", "chapter_id", "chapter_title", "sort_order",
|
2026-03-12 11:36:50 +08:00
|
|
|
|
"hot_score", "updated_at",
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sectionListItem 与前端 SectionListItem 一致(小写驼峰)
|
|
|
|
|
|
type sectionListItem struct {
|
|
|
|
|
|
ID string `json:"id"`
|
2026-03-12 11:36:50 +08:00
|
|
|
|
MID int `json:"mid,omitempty"` // 自增主键,小程序跳转用
|
2026-03-06 17:52:52 +08:00
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Price float64 `json:"price"`
|
|
|
|
|
|
IsFree *bool `json:"isFree,omitempty"`
|
|
|
|
|
|
IsNew *bool `json:"isNew,omitempty"` // stitch_soul:标记最新新增
|
|
|
|
|
|
PartID string `json:"partId"`
|
|
|
|
|
|
PartTitle string `json:"partTitle"`
|
|
|
|
|
|
ChapterID string `json:"chapterId"`
|
|
|
|
|
|
ChapterTitle string `json:"chapterTitle"`
|
|
|
|
|
|
FilePath *string `json:"filePath,omitempty"`
|
2026-03-12 11:36:50 +08:00
|
|
|
|
ClickCount int64 `json:"clickCount"` // 阅读次数(reading_progress)
|
|
|
|
|
|
PayCount int64 `json:"payCount"` // 付款笔数(orders.product_type=section)
|
|
|
|
|
|
HotScore float64 `json:"hotScore"` // 热度积分(加权计算)
|
|
|
|
|
|
IsPinned bool `json:"isPinned,omitempty"` // 是否置顶(仅 ranking 返回)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// computeSectionListWithHotScore 计算章节列表(含 hotScore),保持 sort_order 顺序,供 章节管理 树使用
|
|
|
|
|
|
func computeSectionListWithHotScore(db *gorm.DB) ([]sectionListItem, error) {
|
|
|
|
|
|
sections, err := computeSectionsWithHotScore(db, false)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return sections, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// computeArticleRankingSections 统一计算内容排行榜:置顶优先 + 按 hotScore 降序
|
|
|
|
|
|
// 供管理端内容排行榜页与小程序首页精选推荐共用,排序与置顶均在后端计算
|
|
|
|
|
|
func computeArticleRankingSections(db *gorm.DB) ([]sectionListItem, error) {
|
|
|
|
|
|
sections, err := computeSectionsWithHotScore(db, true)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
// 读取置顶配置 pinned_section_ids
|
|
|
|
|
|
pinnedIDs := []string{}
|
|
|
|
|
|
var cfg model.SystemConfig
|
|
|
|
|
|
if err := db.Where("config_key = ?", "pinned_section_ids").First(&cfg).Error; err == nil && len(cfg.ConfigValue) > 0 {
|
|
|
|
|
|
_ = json.Unmarshal(cfg.ConfigValue, &pinnedIDs)
|
|
|
|
|
|
}
|
|
|
|
|
|
pinnedSet := make(map[string]int) // id -> 置顶顺序
|
|
|
|
|
|
for i, id := range pinnedIDs {
|
|
|
|
|
|
if id != "" {
|
|
|
|
|
|
pinnedSet[id] = i
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 排序:置顶优先(按置顶顺序),其次按 hotScore 降序
|
|
|
|
|
|
sort.Slice(sections, func(i, j int) bool {
|
|
|
|
|
|
pi, pj := pinnedSet[sections[i].ID], pinnedSet[sections[j].ID]
|
|
|
|
|
|
piOk, pjOk := sections[i].IsPinned, sections[j].IsPinned
|
|
|
|
|
|
if piOk && !pjOk {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if !piOk && pjOk {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if piOk && pjOk {
|
|
|
|
|
|
return pi < pj
|
|
|
|
|
|
}
|
|
|
|
|
|
return sections[i].HotScore > sections[j].HotScore
|
|
|
|
|
|
})
|
|
|
|
|
|
return sections, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 16:23:01 +08:00
|
|
|
|
// computeSectionsWithHotScore 内部:按排名分算法计算 hotScore
|
|
|
|
|
|
// 热度积分 = 阅读权重×阅读排名分 + 新度权重×新度排名分 + 付款权重×付款排名分
|
|
|
|
|
|
// 阅读量前20名: 第1名=20分...第20名=1分;最近更新前30篇: 第1名=30分...第30名=1分;付款数前20名: 第1名=20分...第20名=1分
|
2026-03-12 11:36:50 +08:00
|
|
|
|
func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem, error) {
|
|
|
|
|
|
var rows []model.Chapter
|
2026-03-14 18:04:05 +08:00
|
|
|
|
if err := db.Select(listSelectCols).Order("COALESCE(sort_order, 999999) ASC, id ASC").Find(&rows).Error; err != nil {
|
2026-03-12 11:36:50 +08:00
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-03-14 18:04:05 +08:00
|
|
|
|
// 同 sort_order 时按 id 自然排序(9.1 < 9.2 < 9.10),避免字典序 9.1 < 9.10 < 9.2
|
|
|
|
|
|
sort.Slice(rows, func(i, j int) bool {
|
|
|
|
|
|
soI, soJ := 999999, 999999
|
|
|
|
|
|
if rows[i].SortOrder != nil {
|
|
|
|
|
|
soI = *rows[i].SortOrder
|
|
|
|
|
|
}
|
|
|
|
|
|
if rows[j].SortOrder != nil {
|
|
|
|
|
|
soJ = *rows[j].SortOrder
|
|
|
|
|
|
}
|
|
|
|
|
|
if soI != soJ {
|
|
|
|
|
|
return soI < soJ
|
|
|
|
|
|
}
|
|
|
|
|
|
return naturalLessSectionID(rows[i].ID, rows[j].ID)
|
|
|
|
|
|
})
|
2026-03-12 11:36:50 +08:00
|
|
|
|
ids := make([]string, 0, len(rows))
|
|
|
|
|
|
for _, r := range rows {
|
|
|
|
|
|
ids = append(ids, r.ID)
|
|
|
|
|
|
}
|
|
|
|
|
|
readCountMap := make(map[string]int64)
|
|
|
|
|
|
if len(ids) > 0 {
|
|
|
|
|
|
var rp []struct {
|
|
|
|
|
|
SectionID string `gorm:"column:section_id"`
|
|
|
|
|
|
Cnt int64 `gorm:"column:cnt"`
|
|
|
|
|
|
}
|
|
|
|
|
|
db.Table("reading_progress").Select("section_id, COUNT(*) as cnt").
|
|
|
|
|
|
Where("section_id IN ?", ids).Group("section_id").Scan(&rp)
|
|
|
|
|
|
for _, r := range rp {
|
|
|
|
|
|
readCountMap[r.SectionID] = r.Cnt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
payCountMap := make(map[string]int64)
|
|
|
|
|
|
if len(ids) > 0 {
|
|
|
|
|
|
var op []struct {
|
|
|
|
|
|
ProductID string `gorm:"column:product_id"`
|
|
|
|
|
|
Cnt int64 `gorm:"column:cnt"`
|
|
|
|
|
|
}
|
|
|
|
|
|
db.Model(&model.Order{}).
|
|
|
|
|
|
Select("product_id, COUNT(*) as cnt").
|
|
|
|
|
|
Where("product_type = ? AND product_id IN ? AND status IN ?", "section", ids, []string{"paid", "completed", "success"}).
|
|
|
|
|
|
Group("product_id").Scan(&op)
|
|
|
|
|
|
for _, r := range op {
|
|
|
|
|
|
payCountMap[r.ProductID] = r.Cnt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-14 16:23:01 +08:00
|
|
|
|
readWeight, payWeight, recencyWeight := 0.1, 0.4, 0.5 // 默认与截图一致
|
2026-03-12 11:36:50 +08:00
|
|
|
|
var cfg model.SystemConfig
|
|
|
|
|
|
if err := db.Where("config_key = ?", "article_ranking_weights").First(&cfg).Error; err == nil && len(cfg.ConfigValue) > 0 {
|
|
|
|
|
|
var v struct {
|
|
|
|
|
|
ReadWeight float64 `json:"readWeight"`
|
|
|
|
|
|
RecencyWeight float64 `json:"recencyWeight"`
|
|
|
|
|
|
PayWeight float64 `json:"payWeight"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := json.Unmarshal(cfg.ConfigValue, &v); err == nil {
|
2026-03-14 16:23:01 +08:00
|
|
|
|
if v.ReadWeight >= 0 {
|
2026-03-12 11:36:50 +08:00
|
|
|
|
readWeight = v.ReadWeight
|
|
|
|
|
|
}
|
2026-03-14 16:23:01 +08:00
|
|
|
|
if v.PayWeight >= 0 {
|
2026-03-12 11:36:50 +08:00
|
|
|
|
payWeight = v.PayWeight
|
|
|
|
|
|
}
|
2026-03-14 16:23:01 +08:00
|
|
|
|
if v.RecencyWeight >= 0 {
|
2026-03-12 11:36:50 +08:00
|
|
|
|
recencyWeight = v.RecencyWeight
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pinnedIDs := []string{}
|
|
|
|
|
|
if setPinned {
|
|
|
|
|
|
var cfg2 model.SystemConfig
|
|
|
|
|
|
if err := db.Where("config_key = ?", "pinned_section_ids").First(&cfg2).Error; err == nil && len(cfg2.ConfigValue) > 0 {
|
|
|
|
|
|
_ = json.Unmarshal(cfg2.ConfigValue, &pinnedIDs)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pinnedSet := make(map[string]bool)
|
|
|
|
|
|
for _, id := range pinnedIDs {
|
|
|
|
|
|
if id != "" {
|
|
|
|
|
|
pinnedSet[id] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-14 16:23:01 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 阅读量排名:按 readCount 降序,前20名得 20~1 分
|
|
|
|
|
|
type idCnt struct {
|
|
|
|
|
|
id string
|
|
|
|
|
|
cnt int64
|
|
|
|
|
|
}
|
|
|
|
|
|
readRank := make([]idCnt, 0, len(rows))
|
|
|
|
|
|
for _, r := range rows {
|
|
|
|
|
|
readRank = append(readRank, idCnt{r.ID, readCountMap[r.ID]})
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Slice(readRank, func(i, j int) bool { return readRank[i].cnt > readRank[j].cnt })
|
|
|
|
|
|
readRankScoreMap := make(map[string]float64)
|
|
|
|
|
|
for i := 0; i < len(readRank) && i < 20; i++ {
|
|
|
|
|
|
readRankScoreMap[readRank[i].id] = float64(20 - i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 新度排名:按 updated_at 降序(最近更新在前),前30篇得 30~1 分
|
|
|
|
|
|
recencyRank := make([]struct {
|
|
|
|
|
|
id string
|
|
|
|
|
|
updatedAt time.Time
|
|
|
|
|
|
}, 0, len(rows))
|
|
|
|
|
|
for _, r := range rows {
|
|
|
|
|
|
recencyRank = append(recencyRank, struct {
|
|
|
|
|
|
id string
|
|
|
|
|
|
updatedAt time.Time
|
|
|
|
|
|
}{r.ID, r.UpdatedAt})
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Slice(recencyRank, func(i, j int) bool {
|
|
|
|
|
|
return recencyRank[i].updatedAt.After(recencyRank[j].updatedAt)
|
|
|
|
|
|
})
|
|
|
|
|
|
recencyRankScoreMap := make(map[string]float64)
|
|
|
|
|
|
for i := 0; i < len(recencyRank) && i < 30; i++ {
|
|
|
|
|
|
recencyRankScoreMap[recencyRank[i].id] = float64(30 - i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 付款数排名:按 payCount 降序,前20名得 20~1 分
|
|
|
|
|
|
payRank := make([]idCnt, 0, len(rows))
|
|
|
|
|
|
for _, r := range rows {
|
|
|
|
|
|
payRank = append(payRank, idCnt{r.ID, payCountMap[r.ID]})
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Slice(payRank, func(i, j int) bool { return payRank[i].cnt > payRank[j].cnt })
|
|
|
|
|
|
payRankScoreMap := make(map[string]float64)
|
|
|
|
|
|
for i := 0; i < len(payRank) && i < 20; i++ {
|
|
|
|
|
|
payRankScoreMap[payRank[i].id] = float64(20 - i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:36:50 +08:00
|
|
|
|
sections := make([]sectionListItem, 0, len(rows))
|
|
|
|
|
|
for _, r := range rows {
|
|
|
|
|
|
price := 1.0
|
|
|
|
|
|
if r.Price != nil {
|
|
|
|
|
|
price = *r.Price
|
|
|
|
|
|
}
|
|
|
|
|
|
readCnt := readCountMap[r.ID]
|
|
|
|
|
|
payCnt := payCountMap[r.ID]
|
2026-03-14 16:23:01 +08:00
|
|
|
|
readRankScore := readRankScoreMap[r.ID]
|
|
|
|
|
|
recencyRankScore := recencyRankScoreMap[r.ID]
|
|
|
|
|
|
payRankScore := payRankScoreMap[r.ID]
|
|
|
|
|
|
// 热度积分 = 阅读权重×阅读排名分 + 新度权重×新度排名分 + 付款权重×付款排名分
|
|
|
|
|
|
hot := readWeight*readRankScore + recencyWeight*recencyRankScore + payWeight*payRankScore
|
|
|
|
|
|
// 若章节有手动覆盖的 hot_score(>0),则优先使用
|
|
|
|
|
|
if r.HotScore > 0 {
|
|
|
|
|
|
hot = float64(r.HotScore)
|
2026-03-12 11:36:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
if setPinned {
|
|
|
|
|
|
item.IsPinned = pinnedSet[r.ID]
|
|
|
|
|
|
}
|
|
|
|
|
|
sections = append(sections, item)
|
|
|
|
|
|
}
|
|
|
|
|
|
return sections, nil
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DBBookAction GET/POST/PUT /api/db/book
|
|
|
|
|
|
func DBBookAction(c *gin.Context) {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
switch c.Request.Method {
|
|
|
|
|
|
case http.MethodGet:
|
|
|
|
|
|
action := c.Query("action")
|
|
|
|
|
|
id := c.Query("id")
|
|
|
|
|
|
switch action {
|
|
|
|
|
|
case "list":
|
2026-03-12 11:36:50 +08:00
|
|
|
|
// 章节管理树:按 sort_order 顺序,含 hotScore
|
|
|
|
|
|
sections, err := computeSectionListWithHotScore(db)
|
|
|
|
|
|
if err != nil {
|
2026-03-06 17:52:52 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-12 11:36:50 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections, "total": len(sections)})
|
|
|
|
|
|
return
|
|
|
|
|
|
case "ranking":
|
|
|
|
|
|
// 内容排行榜:置顶优先 + hotScore 降序,排序由后端统一计算,前端只展示
|
|
|
|
|
|
sections, err := computeArticleRankingSections(db)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}})
|
|
|
|
|
|
return
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections, "total": len(sections)})
|
|
|
|
|
|
return
|
|
|
|
|
|
case "read":
|
2026-03-14 23:27:22 +08:00
|
|
|
|
midStr := c.Query("mid")
|
|
|
|
|
|
if midStr != "" {
|
|
|
|
|
|
// 优先用 mid 获取(管理端编辑、小程序跳转推荐)
|
|
|
|
|
|
mid, err := strconv.Atoi(midStr)
|
|
|
|
|
|
if err != nil || mid < 1 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "mid 必须为正整数"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var ch model.Chapter
|
|
|
|
|
|
if err := db.Where("mid = ?", mid).First(&ch).Error; err != nil {
|
|
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "章节不存在"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
price := 1.0
|
|
|
|
|
|
if ch.Price != nil {
|
|
|
|
|
|
price = *ch.Price
|
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-06 17:52:52 +08:00
|
|
|
|
if id == "" {
|
2026-03-14 23:27:22 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或 mid"})
|
2026-03-06 17:52:52 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var ch model.Chapter
|
|
|
|
|
|
if err := db.Where("id = ?", id).First(&ch).Error; err != nil {
|
|
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "章节不存在"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
price := 1.0
|
|
|
|
|
|
if ch.Price != nil {
|
|
|
|
|
|
price = *ch.Price
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"section": gin.H{
|
2026-03-10 11:04:34 +08:00
|
|
|
|
"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,
|
2026-03-06 17:52:52 +08:00
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
2026-03-10 11:04:34 +08:00
|
|
|
|
case "section-orders":
|
|
|
|
|
|
// 某章节的付款记录(管理端展示)
|
|
|
|
|
|
if id == "" {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var orders []model.Order
|
|
|
|
|
|
if err := db.Where("product_type = ? AND product_id = ? AND status IN ?", "section", id, []string{"paid", "completed", "success"}).
|
|
|
|
|
|
Order("pay_time DESC, created_at DESC").Limit(200).Find(&orders).Error; err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "orders": []model.Order{}})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "orders": orders})
|
|
|
|
|
|
return
|
2026-03-06 17:52:52 +08:00
|
|
|
|
case "export":
|
|
|
|
|
|
var rows []model.Chapter
|
2026-03-14 18:04:05 +08:00
|
|
|
|
if err := db.Select(listSelectCols).Order("COALESCE(sort_order, 999999) ASC, id ASC").Find(&rows).Error; err != nil {
|
2026-03-06 17:52:52 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-14 18:04:05 +08:00
|
|
|
|
sort.Slice(rows, func(i, j int) bool {
|
|
|
|
|
|
soI, soJ := 999999, 999999
|
|
|
|
|
|
if rows[i].SortOrder != nil {
|
|
|
|
|
|
soI = *rows[i].SortOrder
|
|
|
|
|
|
}
|
|
|
|
|
|
if rows[j].SortOrder != nil {
|
|
|
|
|
|
soJ = *rows[j].SortOrder
|
|
|
|
|
|
}
|
|
|
|
|
|
if soI != soJ {
|
|
|
|
|
|
return soI < soJ
|
|
|
|
|
|
}
|
|
|
|
|
|
return naturalLessSectionID(rows[i].ID, rows[j].ID)
|
|
|
|
|
|
})
|
2026-03-06 17:52:52 +08:00
|
|
|
|
sections := make([]sectionListItem, 0, len(rows))
|
|
|
|
|
|
for _, r := range rows {
|
|
|
|
|
|
price := 1.0
|
|
|
|
|
|
if r.Price != nil {
|
|
|
|
|
|
price = *r.Price
|
|
|
|
|
|
}
|
|
|
|
|
|
sections = append(sections, sectionListItem{
|
|
|
|
|
|
ID: r.ID, Title: r.SectionTitle, Price: price, IsFree: r.IsFree, IsNew: r.IsNew,
|
|
|
|
|
|
PartID: r.PartID, PartTitle: r.PartTitle, ChapterID: r.ChapterID, ChapterTitle: r.ChapterTitle,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections})
|
|
|
|
|
|
return
|
|
|
|
|
|
default:
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无效的 action"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
case http.MethodPost:
|
|
|
|
|
|
var body struct {
|
|
|
|
|
|
Action string `json:"action"`
|
|
|
|
|
|
Data []importItem `json:"data"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
switch body.Action {
|
|
|
|
|
|
case "sync":
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-06 17:52:52 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步完成(Gin 无文件源时可从 DB 已存在数据视为已同步)"})
|
|
|
|
|
|
return
|
|
|
|
|
|
case "import":
|
|
|
|
|
|
imported, failed := 0, 0
|
|
|
|
|
|
for _, item := range body.Data {
|
|
|
|
|
|
price := 1.0
|
|
|
|
|
|
if item.Price != nil {
|
|
|
|
|
|
price = *item.Price
|
|
|
|
|
|
}
|
|
|
|
|
|
isFree := false
|
|
|
|
|
|
if item.IsFree != nil {
|
|
|
|
|
|
isFree = *item.IsFree
|
|
|
|
|
|
}
|
2026-03-16 17:18:49 +08:00
|
|
|
|
processed, _ := ParseAutoLinkContent(item.Content)
|
|
|
|
|
|
wordCount := len(processed)
|
2026-03-06 17:52:52 +08:00
|
|
|
|
status := "published"
|
2026-03-10 18:06:10 +08:00
|
|
|
|
editionStandard, editionPremium := true, false
|
2026-03-06 17:52:52 +08:00
|
|
|
|
ch := model.Chapter{
|
2026-03-10 18:06:10 +08:00
|
|
|
|
ID: item.ID,
|
|
|
|
|
|
PartID: strPtr(item.PartID, "part-1"),
|
|
|
|
|
|
PartTitle: strPtr(item.PartTitle, "未分类"),
|
|
|
|
|
|
ChapterID: strPtr(item.ChapterID, "chapter-1"),
|
|
|
|
|
|
ChapterTitle: strPtr(item.ChapterTitle, "未分类"),
|
|
|
|
|
|
SectionTitle: item.Title,
|
2026-03-16 17:18:49 +08:00
|
|
|
|
Content: processed,
|
2026-03-10 18:06:10 +08:00
|
|
|
|
WordCount: &wordCount,
|
|
|
|
|
|
IsFree: &isFree,
|
|
|
|
|
|
Price: &price,
|
|
|
|
|
|
Status: &status,
|
|
|
|
|
|
EditionStandard: &editionStandard,
|
|
|
|
|
|
EditionPremium: &editionPremium,
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
err := db.Where("id = ?", item.ID).First(&model.Chapter{}).Error
|
|
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
|
|
|
|
err = db.Create(&ch).Error
|
|
|
|
|
|
} else if err == nil {
|
|
|
|
|
|
err = db.Model(&model.Chapter{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
|
|
|
|
|
"section_title": ch.SectionTitle,
|
|
|
|
|
|
"content": ch.Content,
|
|
|
|
|
|
"word_count": ch.WordCount,
|
|
|
|
|
|
"is_free": ch.IsFree,
|
|
|
|
|
|
"price": ch.Price,
|
|
|
|
|
|
}).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
failed++
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateChapterContentByID(item.ID)
|
2026-03-06 17:52:52 +08:00
|
|
|
|
imported++
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-06 17:52:52 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "导入完成", "imported": imported, "failed": failed})
|
|
|
|
|
|
return
|
|
|
|
|
|
default:
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无效的 action"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
case http.MethodPut:
|
|
|
|
|
|
var body struct {
|
|
|
|
|
|
Action string `json:"action"`
|
|
|
|
|
|
// reorder:新顺序,支持跨篇跨章时附带 partId/chapterId
|
|
|
|
|
|
IDs []string `json:"ids"`
|
|
|
|
|
|
Items []reorderItem `json:"items"`
|
2026-03-10 11:04:34 +08:00
|
|
|
|
// move-sections:批量移动节到目标篇/章
|
|
|
|
|
|
SectionIds []string `json:"sectionIds"`
|
|
|
|
|
|
TargetPartID string `json:"targetPartId"`
|
|
|
|
|
|
TargetChapterID string `json:"targetChapterId"`
|
|
|
|
|
|
TargetPartTitle string `json:"targetPartTitle"`
|
|
|
|
|
|
TargetChapterTitle string `json:"targetChapterTitle"`
|
2026-03-10 18:06:10 +08:00
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
|
Price *float64 `json:"price"`
|
|
|
|
|
|
IsFree *bool `json:"isFree"`
|
|
|
|
|
|
IsNew *bool `json:"isNew"` // stitch_soul:标记最新新增
|
|
|
|
|
|
EditionStandard *bool `json:"editionStandard"` // 是否属于普通版
|
|
|
|
|
|
EditionPremium *bool `json:"editionPremium"` // 是否属于增值版
|
|
|
|
|
|
PartID string `json:"partId"`
|
|
|
|
|
|
PartTitle string `json:"partTitle"`
|
|
|
|
|
|
ChapterID string `json:"chapterId"`
|
|
|
|
|
|
ChapterTitle string `json:"chapterTitle"`
|
|
|
|
|
|
HotScore *float64 `json:"hotScore"`
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.Action == "reorder" {
|
|
|
|
|
|
// 立即返回成功,后台异步执行排序更新
|
|
|
|
|
|
if len(body.Items) > 0 {
|
|
|
|
|
|
items := make([]reorderItem, len(body.Items))
|
|
|
|
|
|
copy(items, body.Items)
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
for i, it := range items {
|
|
|
|
|
|
if it.ID == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
up := map[string]interface{}{"sort_order": i}
|
|
|
|
|
|
if it.PartID != "" {
|
|
|
|
|
|
up["part_id"] = it.PartID
|
|
|
|
|
|
}
|
|
|
|
|
|
if it.PartTitle != "" {
|
|
|
|
|
|
up["part_title"] = it.PartTitle
|
|
|
|
|
|
}
|
|
|
|
|
|
if it.ChapterID != "" {
|
|
|
|
|
|
up["chapter_id"] = it.ChapterID
|
|
|
|
|
|
}
|
|
|
|
|
|
if it.ChapterTitle != "" {
|
|
|
|
|
|
up["chapter_title"] = it.ChapterTitle
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = db.WithContext(context.Background()).Model(&model.Chapter{}).Where("id = ?", it.ID).Updates(up).Error
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(body.IDs) > 0 {
|
|
|
|
|
|
ids := make([]string, len(body.IDs))
|
|
|
|
|
|
copy(ids, body.IDs)
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
for i, id := range ids {
|
|
|
|
|
|
if id != "" {
|
|
|
|
|
|
_ = db.WithContext(context.Background()).Model(&model.Chapter{}).Where("id = ?", id).Update("sort_order", i).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 11:04:34 +08:00
|
|
|
|
if body.Action == "move-sections" {
|
|
|
|
|
|
if len(body.SectionIds) == 0 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "批量移动缺少 sectionIds(请先勾选要移动的节)"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.TargetPartID == "" || body.TargetChapterID == "" {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "批量移动缺少目标篇或目标章(targetPartId、targetChapterId)"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
up := map[string]interface{}{
|
|
|
|
|
|
"part_id": body.TargetPartID,
|
|
|
|
|
|
"chapter_id": body.TargetChapterID,
|
|
|
|
|
|
"part_title": body.TargetPartTitle,
|
|
|
|
|
|
"chapter_title": body.TargetChapterTitle,
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := db.Model(&model.Chapter{}).Where("id IN ?", body.SectionIds).Updates(up).Error; err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-10 11:04:34 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已移动", "count": len(body.SectionIds)})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-06 17:52:52 +08:00
|
|
|
|
if body.ID == "" {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
price := 1.0
|
|
|
|
|
|
if body.Price != nil {
|
|
|
|
|
|
price = *body.Price
|
|
|
|
|
|
}
|
|
|
|
|
|
isFree := false
|
|
|
|
|
|
if body.IsFree != nil {
|
|
|
|
|
|
isFree = *body.IsFree
|
|
|
|
|
|
}
|
2026-03-16 17:18:49 +08:00
|
|
|
|
// 后端统一解析 @/# 并转为带 data-id 的 span
|
|
|
|
|
|
processedContent, err := ParseAutoLinkContent(body.Content)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "解析 @/# 失败: " + err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
wordCount := len(processedContent)
|
2026-03-06 17:52:52 +08:00
|
|
|
|
updates := map[string]interface{}{
|
|
|
|
|
|
"section_title": body.Title,
|
2026-03-16 17:18:49 +08:00
|
|
|
|
"content": processedContent,
|
2026-03-06 17:52:52 +08:00
|
|
|
|
"word_count": wordCount,
|
|
|
|
|
|
"price": price,
|
|
|
|
|
|
"is_free": isFree,
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.IsNew != nil {
|
|
|
|
|
|
updates["is_new"] = *body.IsNew
|
|
|
|
|
|
}
|
2026-03-10 18:06:10 +08:00
|
|
|
|
// 默认普通版:未传时按普通版处理
|
2026-03-06 17:52:52 +08:00
|
|
|
|
if body.EditionStandard != nil {
|
|
|
|
|
|
updates["edition_standard"] = *body.EditionStandard
|
2026-03-10 18:06:10 +08:00
|
|
|
|
} else if body.EditionPremium == nil {
|
|
|
|
|
|
updates["edition_standard"] = true
|
|
|
|
|
|
updates["edition_premium"] = false
|
2026-03-06 17:52:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
if body.EditionPremium != nil {
|
|
|
|
|
|
updates["edition_premium"] = *body.EditionPremium
|
|
|
|
|
|
}
|
2026-03-10 18:06:10 +08:00
|
|
|
|
if body.HotScore != nil {
|
|
|
|
|
|
updates["hot_score"] = *body.HotScore
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.PartID != "" {
|
|
|
|
|
|
updates["part_id"] = body.PartID
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.PartTitle != "" {
|
|
|
|
|
|
updates["part_title"] = body.PartTitle
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.ChapterID != "" {
|
|
|
|
|
|
updates["chapter_id"] = body.ChapterID
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.ChapterTitle != "" {
|
|
|
|
|
|
updates["chapter_title"] = body.ChapterTitle
|
|
|
|
|
|
}
|
|
|
|
|
|
var existing model.Chapter
|
2026-03-16 17:18:49 +08:00
|
|
|
|
err = db.Where("id = ?", body.ID).First(&existing).Error
|
2026-03-10 18:06:10 +08:00
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
|
|
|
|
// 新建:Create
|
|
|
|
|
|
partID := body.PartID
|
|
|
|
|
|
if partID == "" {
|
|
|
|
|
|
partID = "part-1"
|
|
|
|
|
|
}
|
|
|
|
|
|
partTitle := body.PartTitle
|
|
|
|
|
|
if partTitle == "" {
|
|
|
|
|
|
partTitle = "未分类"
|
|
|
|
|
|
}
|
|
|
|
|
|
chapterID := body.ChapterID
|
|
|
|
|
|
if chapterID == "" {
|
|
|
|
|
|
chapterID = "chapter-1"
|
|
|
|
|
|
}
|
|
|
|
|
|
chapterTitle := body.ChapterTitle
|
|
|
|
|
|
if chapterTitle == "" {
|
|
|
|
|
|
chapterTitle = "未分类"
|
|
|
|
|
|
}
|
|
|
|
|
|
editionStandard, editionPremium := true, false
|
|
|
|
|
|
if body.EditionPremium != nil && *body.EditionPremium {
|
|
|
|
|
|
editionStandard, editionPremium = false, true
|
|
|
|
|
|
} else if body.EditionStandard != nil {
|
|
|
|
|
|
editionStandard = *body.EditionStandard
|
|
|
|
|
|
}
|
|
|
|
|
|
status := "published"
|
|
|
|
|
|
ch := model.Chapter{
|
|
|
|
|
|
ID: body.ID,
|
|
|
|
|
|
PartID: partID,
|
|
|
|
|
|
PartTitle: partTitle,
|
|
|
|
|
|
ChapterID: chapterID,
|
|
|
|
|
|
ChapterTitle: chapterTitle,
|
|
|
|
|
|
SectionTitle: body.Title,
|
2026-03-16 17:18:49 +08:00
|
|
|
|
Content: processedContent,
|
2026-03-10 18:06:10 +08:00
|
|
|
|
WordCount: &wordCount,
|
|
|
|
|
|
IsFree: &isFree,
|
|
|
|
|
|
Price: &price,
|
|
|
|
|
|
Status: &status,
|
|
|
|
|
|
EditionStandard: &editionStandard,
|
|
|
|
|
|
EditionPremium: &editionPremium,
|
|
|
|
|
|
}
|
|
|
|
|
|
if body.IsNew != nil {
|
|
|
|
|
|
ch.IsNew = body.IsNew
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := db.Create(&ch).Error; err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateChapterContent(ch.MID)
|
|
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-10 18:06:10 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
err = db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error
|
2026-03-06 17:52:52 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateChapterContentByID(body.ID)
|
|
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-06 17:52:52 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type reorderItem struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
PartID string `json:"partId"`
|
|
|
|
|
|
PartTitle string `json:"partTitle"`
|
|
|
|
|
|
ChapterID string `json:"chapterId"`
|
|
|
|
|
|
ChapterTitle string `json:"chapterTitle"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type importItem struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
|
Price *float64 `json:"price"`
|
|
|
|
|
|
IsFree *bool `json:"isFree"`
|
|
|
|
|
|
PartID *string `json:"partId"`
|
|
|
|
|
|
PartTitle *string `json:"partTitle"`
|
|
|
|
|
|
ChapterID *string `json:"chapterId"`
|
|
|
|
|
|
ChapterTitle *string `json:"chapterTitle"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func strPtr(s *string, def string) string {
|
|
|
|
|
|
if s != nil && *s != "" {
|
|
|
|
|
|
return *s
|
|
|
|
|
|
}
|
|
|
|
|
|
return def
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DBBookDelete DELETE /api/db/book
|
|
|
|
|
|
func DBBookDelete(c *gin.Context) {
|
|
|
|
|
|
id := c.Query("id")
|
|
|
|
|
|
if id == "" {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateChapterContentByID(id)
|
2026-03-06 17:52:52 +08:00
|
|
|
|
if err := database.DB().Where("id = ?", id).Delete(&model.Chapter{}).Error; err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
cache.InvalidateBookParts()
|
|
|
|
|
|
cache.InvalidateBookCache()
|
2026-03-06 17:52:52 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
|
|
|
|
}
|