feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -14,7 +14,7 @@ const (
|
||||
PeriodMonth = "month"
|
||||
PeriodAll = "all"
|
||||
|
||||
MetricAll = "all" // 综合:周/月=四项合计,总榜=累计积分
|
||||
MetricAll = "all" // 综合:总榜=累计积分,周/月=周期内获得积分
|
||||
MetricPost = "post" // 发帖最多
|
||||
MetricComment = "comment" // 回复最多
|
||||
MetricLike = "like" // 获赞最多
|
||||
@@ -112,6 +112,41 @@ func (s *LeaderboardService) contentStats(from, to *time.Time) (map[uint]statsRo
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// earnedPoints 周期内累计获得积分:point_ledgers 正入账合计(排除 bounty_refund,
|
||||
// 与 users.total_points 启动回填口径一致),仅统计未封禁用户。
|
||||
// 签到、发帖/回复奖励、被推荐、悬赏采纳等均按实际入账分值计入贡献值
|
||||
func (s *LeaderboardService) earnedPoints(from, to *time.Time) (map[uint]int64, error) {
|
||||
clause := ""
|
||||
var timeArgs []interface{}
|
||||
if from != nil {
|
||||
clause += " AND l.created_at >= ?"
|
||||
timeArgs = append(timeArgs, *from)
|
||||
}
|
||||
if to != nil {
|
||||
clause += " AND l.created_at < ?"
|
||||
timeArgs = append(timeArgs, *to)
|
||||
}
|
||||
sql := "SELECT l.user_id AS user_id, SUM(l.delta) AS pts FROM point_ledgers l" +
|
||||
" JOIN users AS u ON u.id = l.user_id" +
|
||||
" WHERE l.delta > 0 AND l.reason <> ? AND u.banned = ? AND u.deleted_at IS NULL" + clause +
|
||||
" GROUP BY l.user_id"
|
||||
args := []interface{}{model.PointReasonBountyRefund, false}
|
||||
args = append(args, timeArgs...)
|
||||
|
||||
var rows []struct {
|
||||
UserID uint
|
||||
Pts int64
|
||||
}
|
||||
if err := s.db.Raw(sql, args...).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(map[uint]int64, len(rows))
|
||||
for _, r := range rows {
|
||||
out[r.UserID] = r.Pts
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// answerStats 最佳答案统计:问答/悬赏帖中其评论被采纳(type_meta.accepted_comment_id)的条数,
|
||||
// 按帖子发布时间窗计;type_meta 为 JSON 字符串,空值/缺失统一落 0(JOIN 不到即不计)
|
||||
func (s *LeaderboardService) answerStats(from, to *time.Time) (map[uint]int64, error) {
|
||||
@@ -167,7 +202,7 @@ func (s *LeaderboardService) statsFor(from, to *time.Time) (map[uint]statsRow, e
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// scoreOf 指标 → 排名得分;综合 = 四项合计(总榜-全部走累计积分口径,见 boardBetween)
|
||||
// scoreOf 细分指标 → 排名得分;「全部」维度走积分口径(见 boardBetween / earnedPoints)
|
||||
func scoreOf(metric string, st statsRow) int64 {
|
||||
switch metric {
|
||||
case MetricPost:
|
||||
@@ -178,9 +213,8 @@ func scoreOf(metric string, st statsRow) int64 {
|
||||
return st.LikeCount
|
||||
case MetricAnswer:
|
||||
return st.AnswerCount
|
||||
default:
|
||||
return st.PostCount + st.CommentCount + st.LikeCount + st.AnswerCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// entryFromUser 由用户行构造榜单条目(不含统计)
|
||||
@@ -214,7 +248,8 @@ func periodBounds(period string, now time.Time) (from, to, prevFrom, prevTo *tim
|
||||
}
|
||||
|
||||
// boardBetween 指定时间窗的全量榜单(未截断,Rank 未编号)。
|
||||
// 「总榜+全部」走累计积分口径(total_points),其余走统计聚合口径。
|
||||
// 「全部」维度走积分口径(总榜=total_points,周/月=周期内获得积分,见 earnedPoints),
|
||||
// 细分维度走统计聚合口径。
|
||||
func (s *LeaderboardService) boardBetween(from, to *time.Time, period, metric string) ([]LeaderboardEntry, error) {
|
||||
if metric == MetricAll && from == nil {
|
||||
var users []model.User
|
||||
@@ -245,9 +280,29 @@ func (s *LeaderboardService) boardBetween(from, to *time.Time, period, metric st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := make([]LeaderboardEntry, 0, len(stats))
|
||||
// 周/月·全部:贡献值=周期内累计获得积分,候选集并入仅签到/获奖无内容的用户
|
||||
usePts := metric == MetricAll && from != nil
|
||||
pts := map[uint]int64{}
|
||||
if usePts {
|
||||
if pts, err = s.earnedPoints(from, to); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cand := make(map[uint]statsRow, len(stats)+len(pts))
|
||||
for id, r := range stats {
|
||||
cand[id] = r
|
||||
}
|
||||
for id := range pts {
|
||||
if _, ok := cand[id]; !ok {
|
||||
cand[id] = statsRow{}
|
||||
}
|
||||
}
|
||||
entries := make([]LeaderboardEntry, 0, len(cand))
|
||||
for id, r := range cand {
|
||||
score := scoreOf(metric, r)
|
||||
if usePts {
|
||||
score = pts[id]
|
||||
}
|
||||
if score <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user