feat: 数据概览简化 + 用户管理增加余额/提现列
- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额 - 数据概览:移除余额统计区块(余额改在用户管理中展示) - 数据概览:恢复转化率卡片(唯一付费用户/总用户) - 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额 - 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段 - 后端:User model 添加 WalletBalance 非数据库字段 - 包含之前的小程序埋点和管理后台点击统计面板 Made-with: Cursor
This commit is contained in:
@@ -147,17 +147,58 @@ func checkUserChapterAccess(db *gorm.DB, userID, chapterID string, isPremium boo
|
||||
return cnt > 0
|
||||
}
|
||||
|
||||
// previewContent 取内容的前 50%(不少于 100 个字符),并追加省略提示
|
||||
func previewContent(content string) string {
|
||||
// getUnpaidPreviewPercent 从 system_config 读取 unpaid_preview_percent,默认 20
|
||||
func getUnpaidPreviewPercent(db *gorm.DB) int {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "unpaid_preview_percent").First(&row).Error; err != nil || len(row.ConfigValue) == 0 {
|
||||
return 20
|
||||
}
|
||||
var val interface{}
|
||||
if err := json.Unmarshal(row.ConfigValue, &val); err != nil {
|
||||
return 20
|
||||
}
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
p := int(v)
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
return p
|
||||
case int:
|
||||
if v < 1 {
|
||||
return 1
|
||||
}
|
||||
if v > 100 {
|
||||
return 100
|
||||
}
|
||||
return v
|
||||
}
|
||||
return 20
|
||||
}
|
||||
|
||||
// previewContent 取内容的前 percent%,上限 500 字(手动设置 percent 也受此限制),不少于 100 字
|
||||
func previewContent(content string, percent int) string {
|
||||
total := utf8.RuneCountInString(content)
|
||||
if total == 0 {
|
||||
return ""
|
||||
}
|
||||
// 截取前 50% 的内容,保证有足够的预览长度
|
||||
limit := total / 2
|
||||
if percent < 1 {
|
||||
percent = 1
|
||||
}
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
limit := total * percent / 100
|
||||
if limit < 100 {
|
||||
limit = 100
|
||||
}
|
||||
const maxPreview = 500
|
||||
if limit > maxPreview {
|
||||
limit = maxPreview
|
||||
}
|
||||
if limit > total {
|
||||
limit = total
|
||||
}
|
||||
@@ -198,7 +239,11 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
|
||||
} else if checkUserChapterAccess(db, userID, ch.ID, isPremium) {
|
||||
returnContent = ch.Content
|
||||
} else {
|
||||
returnContent = previewContent(ch.Content)
|
||||
percent := getUnpaidPreviewPercent(db)
|
||||
if ch.PreviewPercent != nil && *ch.PreviewPercent >= 1 && *ch.PreviewPercent <= 100 {
|
||||
percent = *ch.PreviewPercent
|
||||
}
|
||||
returnContent = previewContent(ch.Content, percent)
|
||||
}
|
||||
|
||||
// data 中的 content 必须与外层 content 一致,避免泄露完整内容给未授权用户
|
||||
@@ -289,7 +334,7 @@ func BookChapters(c *gin.Context) {
|
||||
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,
|
||||
"sort_order": body.SortOrder, "status": body.Status, "hot_score": body.HotScore,
|
||||
}
|
||||
if body.EditionStandard != nil {
|
||||
updates["edition_standard"] = body.EditionStandard
|
||||
@@ -368,18 +413,59 @@ func bookHotChaptersSorted(db *gorm.DB, limit int) []model.Chapter {
|
||||
return out
|
||||
}
|
||||
|
||||
// BookHot GET /api/book/hot 热门章节(按阅读量排序,排除序言/尾声/附录)
|
||||
// BookHot GET /api/book/hot 热门章节(按 hot_score 降序,使用与管理端相同的排名算法)
|
||||
// 支持 ?limit=N 参数,默认 20,最大 100
|
||||
func BookHot(c *gin.Context) {
|
||||
list := bookHotChaptersSorted(database.DB(), 10)
|
||||
if len(list) == 0 {
|
||||
// 兜底:按 sort_order 取前 10,同样排除序言/尾声/附录
|
||||
q := database.DB().Model(&model.Chapter{})
|
||||
db := database.DB()
|
||||
|
||||
sections, err := computeArticleRankingSections(db)
|
||||
if err != nil || len(sections) == 0 {
|
||||
var list []model.Chapter
|
||||
q := db.Model(&model.Chapter{}).
|
||||
Select("mid, id, part_id, part_title, chapter_id, chapter_title, section_title, is_free, price, sort_order, hot_score, updated_at")
|
||||
for _, p := range excludeParts {
|
||||
q = q.Where("part_title NOT LIKE ?", "%"+p+"%")
|
||||
}
|
||||
q.Order("sort_order ASC, id ASC").Limit(10).Find(&list)
|
||||
q.Order("hot_score DESC, sort_order ASC, id ASC").Limit(20).Find(&list)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
|
||||
limit := 20
|
||||
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
|
||||
limit = l
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
}
|
||||
if len(sections) < limit {
|
||||
limit = len(sections)
|
||||
}
|
||||
tags := []string{"热门", "推荐", "精选"}
|
||||
result := make([]gin.H, 0, limit)
|
||||
for i := 0; i < limit; i++ {
|
||||
s := sections[i]
|
||||
tag := ""
|
||||
if i < len(tags) {
|
||||
tag = tags[i]
|
||||
}
|
||||
result = append(result, gin.H{
|
||||
"id": s.ID,
|
||||
"mid": s.MID,
|
||||
"sectionTitle": s.Title,
|
||||
"partTitle": s.PartTitle,
|
||||
"chapterTitle": s.ChapterTitle,
|
||||
"price": s.Price,
|
||||
"isFree": s.IsFree,
|
||||
"clickCount": s.ClickCount,
|
||||
"payCount": s.PayCount,
|
||||
"hotScore": s.HotScore,
|
||||
"hotRank": i + 1,
|
||||
"isPinned": s.IsPinned,
|
||||
"tag": tag,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": result})
|
||||
}
|
||||
|
||||
// BookRecommended GET /api/book/recommended 精选推荐(首页「为你推荐」前 3 章)
|
||||
@@ -498,9 +584,21 @@ func BookSearch(c *gin.Context) {
|
||||
|
||||
// BookStats GET /api/book/stats
|
||||
func BookStats(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var total int64
|
||||
database.DB().Model(&model.Chapter{}).Count(&total)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalChapters": total}})
|
||||
db.Model(&model.Chapter{}).Count(&total)
|
||||
var freeCount int64
|
||||
db.Model(&model.Chapter{}).Where("is_free = ?", true).Count(&freeCount)
|
||||
var totalWords struct{ S int64 }
|
||||
db.Model(&model.Chapter{}).Select("COALESCE(SUM(word_count),0) as s").Scan(&totalWords)
|
||||
var userCount int64
|
||||
db.Model(&model.User{}).Count(&userCount)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||||
"totalChapters": total,
|
||||
"freeChapters": freeCount,
|
||||
"totalWordCount": totalWords.S,
|
||||
"totalUsers": userCount,
|
||||
}})
|
||||
}
|
||||
|
||||
// BookSync GET/POST /api/book/sync
|
||||
|
||||
Reference in New Issue
Block a user