package handler import ( "context" "encoding/json" "net/http" "sort" "strconv" "strings" "time" "soul-api/internal/cache" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // 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) } // listSelectCols 列表/导出不加载 content,大幅加速 var listSelectCols = []string{ "id", "mid", "section_title", "price", "is_free", "is_new", "part_id", "part_title", "chapter_id", "chapter_title", "sort_order", "hot_score", "updated_at", } // sectionListItem 与前端 SectionListItem 一致(小写驼峰) type sectionListItem struct { ID string `json:"id"` MID int `json:"mid,omitempty"` // 自增主键,小程序跳转用 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"` 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 } // computeSectionsWithHotScore 内部:按排名分算法计算 hotScore // 热度积分 = 阅读权重×阅读排名分 + 新度权重×新度排名分 + 付款权重×付款排名分 // 阅读量前20名: 第1名=20分...第20名=1分;最近更新前30篇: 第1名=30分...第30名=1分;付款数前20名: 第1名=20分...第20名=1分 func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem, error) { var rows []model.Chapter if err := db.Select(listSelectCols).Order("COALESCE(sort_order, 999999) ASC, id ASC").Find(&rows).Error; err != nil { return nil, err } // 同 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) }) 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 } } readWeight, payWeight, recencyWeight := 0.1, 0.4, 0.5 // 默认与截图一致 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 { if v.ReadWeight >= 0 { readWeight = v.ReadWeight } if v.PayWeight >= 0 { payWeight = v.PayWeight } if v.RecencyWeight >= 0 { 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 } } // 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) } 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] 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) } 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 } // 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": // 章节管理树:按 sort_order 顺序,含 hotScore sections, err := computeSectionListWithHotScore(db) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}}) return } 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 } c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections, "total": len(sections)}) return case "read": 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 } if id == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或 mid"}) 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{ "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 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 case "export": var rows []model.Chapter if err := db.Select(listSelectCols).Order("COALESCE(sort_order, 999999) ASC, id ASC").Find(&rows).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } 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) }) 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": cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() 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 } processed, _ := ParseAutoLinkContent(item.Content) wordCount := len(processed) status := "published" editionStandard, editionPremium := true, false ch := model.Chapter{ 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, Content: processed, WordCount: &wordCount, IsFree: &isFree, Price: &price, Status: &status, EditionStandard: &editionStandard, EditionPremium: &editionPremium, } 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 } cache.InvalidateChapterContentByID(item.ID) imported++ } cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() 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"` // move-sections:批量移动节到目标篇/章 SectionIds []string `json:"sectionIds"` TargetPartID string `json:"targetPartId"` TargetChapterID string `json:"targetChapterId"` TargetPartTitle string `json:"targetPartTitle"` TargetChapterTitle string `json:"targetChapterTitle"` 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"` } 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 } cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() }() 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 } } cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() }() return } } 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 } cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() c.JSON(http.StatusOK, gin.H{"success": true, "message": "已移动", "count": len(body.SectionIds)}) return } // update-chapter-pricing:按篇+章批量更新该章下所有「节」行的 price / is_free(管理端章节统一定价) if body.Action == "update-chapter-pricing" { if body.PartID == "" || body.ChapterID == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 partId 或 chapterId"}) return } p := 1.0 if body.Price != nil { p = *body.Price } free := false if body.IsFree != nil { free = *body.IsFree } if free { p = 0 } up := map[string]interface{}{ "price": p, "is_free": free, } res := db.Model(&model.Chapter{}).Where("part_id = ? AND chapter_id = ?", body.PartID, body.ChapterID).Updates(up) if res.Error != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": res.Error.Error()}) return } cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() c.JSON(http.StatusOK, gin.H{"success": true, "message": "已更新本章全部节的定价", "affected": res.RowsAffected}) return } 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 } // 后端统一解析 @/# 并转为带 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) updates := map[string]interface{}{ "section_title": body.Title, "content": processedContent, "word_count": wordCount, "price": price, "is_free": isFree, } if body.IsNew != nil { updates["is_new"] = *body.IsNew } // 默认普通版:未传时按普通版处理 if body.EditionStandard != nil { updates["edition_standard"] = *body.EditionStandard } else if body.EditionPremium == nil { updates["edition_standard"] = true updates["edition_premium"] = false } if body.EditionPremium != nil { updates["edition_premium"] = *body.EditionPremium } 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 err = db.Where("id = ?", body.ID).First(&existing).Error 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, Content: processedContent, 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 } cache.InvalidateChapterContent(ch.MID) cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() 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 if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } cache.InvalidateChapterContentByID(body.ID) cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() 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 } cache.InvalidateChapterContentByID(id) 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 } cache.InvalidateBookParts() InvalidateChaptersByPartCache() cache.InvalidateBookCache() c.JSON(http.StatusOK, gin.H{"success": true}) }