Refactor user profile handling and navigation logic in the mini program. Introduce functions to ensure user profile completeness after login, update avatar selection process, and enhance navigation between chapters based on backend data. Update API endpoints for user data synchronization and improve user experience with new UI elements for profile editing.
This commit is contained in:
@@ -2,7 +2,11 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
@@ -13,13 +17,15 @@ import (
|
||||
|
||||
// listSelectCols 列表/导出不加载 content,大幅加速
|
||||
var listSelectCols = []string{
|
||||
"id", "section_title", "price", "is_free", "is_new",
|
||||
"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"`
|
||||
@@ -29,6 +35,166 @@ type sectionListItem struct {
|
||||
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,可选设置 isPinned
|
||||
func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem, error) {
|
||||
var rows []model.Chapter
|
||||
if err := db.Select(listSelectCols).Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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.5, 0.3, 0.2
|
||||
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
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
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]
|
||||
recencyScore := 0.0
|
||||
if !r.UpdatedAt.IsZero() {
|
||||
days := now.Sub(r.UpdatedAt).Hours() / 24
|
||||
recencyScore = math.Max(0, (30-days)/30)
|
||||
if recencyScore > 1 {
|
||||
recencyScore = 1
|
||||
}
|
||||
}
|
||||
hot := float64(readCnt)*readWeight + float64(payCnt)*payWeight + recencyScore*recencyWeight
|
||||
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
|
||||
@@ -40,28 +206,20 @@ func DBBookAction(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
switch action {
|
||||
case "list":
|
||||
var rows []model.Chapter
|
||||
if err := db.Select(listSelectCols).Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
// 章节管理树:按 sort_order 顺序,含 hotScore
|
||||
sections, err := computeSectionListWithHotScore(db)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}})
|
||||
return
|
||||
}
|
||||
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, "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
|
||||
|
||||
Reference in New Issue
Block a user