删除过时的批处理脚本和部署Python文件
- 删除了macOS虚拟机的安装和迁移批处理脚本,因为它们已不再需要。 - 删除了macOS虚拟机的导出脚本,以简化项目流程。 - 删除了soul-admin项目的部署Python脚本,以简化代码库。 - 更新了小程序,以反映环境配置的变化并提升用户体验。
This commit is contained in:
@@ -54,6 +54,30 @@ var allChaptersCache struct {
|
||||
|
||||
const allChaptersCacheTTL = 30 * time.Second
|
||||
|
||||
// bookPartsCache 目录接口内存缓存,30 秒 TTL,减轻 DB 压力
|
||||
type cachedPartRow struct {
|
||||
PartID string `json:"id"`
|
||||
PartTitle string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
ChapterCount int `json:"chapterCount"`
|
||||
MinSortOrder int `json:"minSortOrder"`
|
||||
}
|
||||
type cachedFixedItem struct {
|
||||
ID string `json:"id"`
|
||||
MID int `json:"mid"`
|
||||
SectionTitle string `json:"title"`
|
||||
}
|
||||
|
||||
var bookPartsCache struct {
|
||||
mu sync.RWMutex
|
||||
parts []cachedPartRow
|
||||
total int64
|
||||
fixed []cachedFixedItem
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
const bookPartsCacheTTL = 30 * time.Second
|
||||
|
||||
// WarmAllChaptersCache 启动时预热缓存,避免首请求冷启动 502
|
||||
func WarmAllChaptersCache() {
|
||||
db := database.DB()
|
||||
@@ -79,6 +103,86 @@ func WarmAllChaptersCache() {
|
||||
allChaptersCache.mu.Unlock()
|
||||
}
|
||||
|
||||
// fetchAndCacheBookParts 执行 DB 查询并更新缓存,供 BookParts 与 WarmBookPartsCache 复用
|
||||
func fetchAndCacheBookParts() (parts []cachedPartRow, total int64, fixed []cachedFixedItem) {
|
||||
db := database.DB()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
conds := make([]string, len(excludeParts))
|
||||
args := make([]interface{}, len(excludeParts))
|
||||
for i, p := range excludeParts {
|
||||
conds[i] = "part_title LIKE ?"
|
||||
args[i] = "%" + p + "%"
|
||||
}
|
||||
where := "(" + strings.Join(conds, " OR ") + ")"
|
||||
var rows []model.Chapter
|
||||
if err := db.Model(&model.Chapter{}).Select("id", "mid", "section_title", "sort_order").
|
||||
Where(where, args...).
|
||||
Order("COALESCE(sort_order, 999999) ASC, id ASC").
|
||||
Find(&rows).Error; err == nil {
|
||||
sortChaptersByNaturalID(rows)
|
||||
for _, r := range rows {
|
||||
fixed = append(fixed, cachedFixedItem{r.ID, r.MID, r.SectionTitle})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
where := "1=1"
|
||||
args := []interface{}{}
|
||||
for _, p := range excludeParts {
|
||||
where += " AND part_title NOT LIKE ?"
|
||||
args = append(args, "%"+p+"%")
|
||||
}
|
||||
sql := `SELECT part_id, part_title, '' as subtitle,
|
||||
COUNT(DISTINCT chapter_id) as chapter_count,
|
||||
MIN(COALESCE(sort_order, 999999)) as min_sort
|
||||
FROM chapters WHERE ` + where + `
|
||||
GROUP BY part_id, part_title ORDER BY min_sort ASC, part_id ASC`
|
||||
var raw []struct {
|
||||
PartID string `gorm:"column:part_id"`
|
||||
PartTitle string `gorm:"column:part_title"`
|
||||
Subtitle string `gorm:"column:subtitle"`
|
||||
ChapterCount int `gorm:"column:chapter_count"`
|
||||
MinSortOrder int `gorm:"column:min_sort"`
|
||||
}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
db.Raw(sql, args...).Scan(&raw)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
db.Model(&model.Chapter{}).Count(&total)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
parts = make([]cachedPartRow, len(raw))
|
||||
for i, r := range raw {
|
||||
parts[i] = cachedPartRow{
|
||||
PartID: r.PartID, PartTitle: r.PartTitle, Subtitle: r.Subtitle,
|
||||
ChapterCount: r.ChapterCount, MinSortOrder: r.MinSortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
bookPartsCache.mu.Lock()
|
||||
bookPartsCache.parts = parts
|
||||
bookPartsCache.total = total
|
||||
bookPartsCache.fixed = fixed
|
||||
bookPartsCache.expires = time.Now().Add(bookPartsCacheTTL)
|
||||
bookPartsCache.mu.Unlock()
|
||||
|
||||
return parts, total, fixed
|
||||
}
|
||||
|
||||
// WarmBookPartsCache 启动时预热目录缓存,避免首请求慢
|
||||
func WarmBookPartsCache() {
|
||||
fetchAndCacheBookParts()
|
||||
}
|
||||
|
||||
// BookAllChapters GET /api/book/all-chapters 返回所有章节(列表,来自 chapters 表)
|
||||
// 排序须与管理端 PUT /api/db/book action=reorder 一致:按 sort_order 升序,同序按 id
|
||||
// 免费判断:system_config.free_chapters / chapter_config.freeChapters 优先于 chapters.is_free
|
||||
@@ -144,78 +248,30 @@ func BookChapterByID(c *gin.Context) {
|
||||
|
||||
// BookParts GET /api/miniprogram/book/parts 目录懒加载:仅返回篇章列表,不含章节详情
|
||||
// 返回 parts(排除序言/尾声/附录)、totalSections、fixedSections(id, mid, title 供序言/尾声/附录跳转用 mid)
|
||||
// 带 30 秒内存缓存,固定模块合并为 1 次查询,三路并行执行
|
||||
func BookParts(c *gin.Context) {
|
||||
db := database.DB()
|
||||
// 固定模块(序言、尾声、附录)的 id、mid、title,供 goToRead 传 data-mid
|
||||
var fixedList []struct {
|
||||
ID string `json:"id"`
|
||||
MID int `json:"mid"`
|
||||
SectionTitle string `json:"title"`
|
||||
}
|
||||
for _, p := range excludeParts {
|
||||
var rows []model.Chapter
|
||||
if err := db.Model(&model.Chapter{}).Select("id", "mid", "section_title", "sort_order").
|
||||
Where("part_title LIKE ?", "%"+p+"%").
|
||||
Order("COALESCE(sort_order, 999999) ASC, id ASC").
|
||||
Find(&rows).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
sortChaptersByNaturalID(rows)
|
||||
for _, r := range rows {
|
||||
fixedList = append(fixedList, struct {
|
||||
ID string `json:"id"`
|
||||
MID int `json:"mid"`
|
||||
SectionTitle string `json:"title"`
|
||||
}{r.ID, r.MID, r.SectionTitle})
|
||||
}
|
||||
}
|
||||
|
||||
// 中间篇章:轻量聚合,不拉取 content
|
||||
type partRow struct {
|
||||
PartID string `json:"id"`
|
||||
PartTitle string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
ChapterCount int `json:"chapterCount"`
|
||||
MinSortOrder int `json:"minSortOrder"`
|
||||
}
|
||||
where := "1=1"
|
||||
args := []interface{}{}
|
||||
for _, p := range excludeParts {
|
||||
where += " AND part_title NOT LIKE ?"
|
||||
args = append(args, "%"+p+"%")
|
||||
}
|
||||
var raw []struct {
|
||||
PartID string `gorm:"column:part_id"`
|
||||
PartTitle string `gorm:"column:part_title"`
|
||||
Subtitle string `gorm:"column:subtitle"`
|
||||
ChapterCount int `gorm:"column:chapter_count"`
|
||||
MinSortOrder int `gorm:"column:min_sort"`
|
||||
}
|
||||
sql := `SELECT part_id, part_title, '' as subtitle,
|
||||
COUNT(DISTINCT chapter_id) as chapter_count,
|
||||
MIN(COALESCE(sort_order, 999999)) as min_sort
|
||||
FROM chapters WHERE ` + where + `
|
||||
GROUP BY part_id, part_title ORDER BY min_sort ASC, part_id ASC`
|
||||
if err := db.Raw(sql, args...).Scan(&raw).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "parts": []interface{}{}, "totalSections": 0, "fixedSections": fixedList})
|
||||
bookPartsCache.mu.RLock()
|
||||
if time.Now().Before(bookPartsCache.expires) {
|
||||
parts := bookPartsCache.parts
|
||||
total := bookPartsCache.total
|
||||
fixed := bookPartsCache.fixed
|
||||
bookPartsCache.mu.RUnlock()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"parts": parts,
|
||||
"totalSections": total,
|
||||
"fixedSections": fixed,
|
||||
})
|
||||
return
|
||||
}
|
||||
parts := make([]partRow, len(raw))
|
||||
for i, r := range raw {
|
||||
parts[i] = partRow{
|
||||
PartID: r.PartID, PartTitle: r.PartTitle, Subtitle: r.Subtitle,
|
||||
ChapterCount: r.ChapterCount, MinSortOrder: r.MinSortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
var total int64
|
||||
db.Model(&model.Chapter{}).Count(&total)
|
||||
bookPartsCache.mu.RUnlock()
|
||||
|
||||
parts, total, fixed := fetchAndCacheBookParts()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"parts": parts,
|
||||
"totalSections": total,
|
||||
"fixedSections": fixedList,
|
||||
"fixedSections": fixed,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user