mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 09:46:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package leaderboard
|
||||
|
||||
const (
|
||||
cacheKeyPrefix = "user:leaderboard:"
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package leaderboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
)
|
||||
|
||||
func getList(ctx context.Context, req *ListRequest) (*ListResponse, error) {
|
||||
// 检查 Redis 缓存
|
||||
cacheKey := fmt.Sprintf("%slist:p:%d:s:%d", cacheKeyPrefix, req.Page, req.PageSize)
|
||||
if data, err := db.Redis.Get(ctx, db.PrefixedKey(cacheKey)).Bytes(); err == nil {
|
||||
var cached ListResponse
|
||||
if err := json.Unmarshal(data, &cached); err == nil {
|
||||
return &cached, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 查询数据库
|
||||
items, total, err := queryLeaderboard(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &ListResponse{
|
||||
SortBy: "available_balance",
|
||||
Order: "desc",
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
Total: total,
|
||||
Items: items,
|
||||
}
|
||||
|
||||
if data, err := json.Marshal(response); err == nil {
|
||||
_ = db.Redis.Set(ctx, db.PrefixedKey(cacheKey), data, getCacheTTL(ctx)).Err()
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func getUserRank(ctx context.Context, userID uint64) (*UserRankResponse, error) {
|
||||
// 检查缓存
|
||||
cacheKey := fmt.Sprintf("%suser:%d", cacheKeyPrefix, userID)
|
||||
if data, err := db.Redis.Get(ctx, db.PrefixedKey(cacheKey)).Bytes(); err == nil {
|
||||
var cached UserRankResponse
|
||||
if err := json.Unmarshal(data, &cached); err == nil {
|
||||
return &cached, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 查询用户余额
|
||||
var user model.User
|
||||
if err := db.DB(ctx).Select("id, available_balance").Where("id = ? AND is_active = ?", userID, true).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算排名:统计比当前用户排位更高的人数
|
||||
// 排序规则:available_balance DESC, id ASC
|
||||
// 即:余额更高,或余额相同但 id 更小
|
||||
var rank int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).
|
||||
Where("is_active = ? AND ((available_balance > ?) OR (available_balance = ? AND id < ?))",
|
||||
true, user.AvailableBalance, user.AvailableBalance, userID).
|
||||
Count(&rank).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &UserRankResponse{
|
||||
User: UserRankInfo{
|
||||
UserID: userID,
|
||||
Rank: int(rank + 1),
|
||||
AvailableBalance: user.AvailableBalance,
|
||||
},
|
||||
}
|
||||
|
||||
if data, err := json.Marshal(response); err == nil {
|
||||
_ = db.Redis.Set(ctx, db.PrefixedKey(cacheKey), data, getCacheTTL(ctx)).Err()
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package leaderboard
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/apps/oauth"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// ListRequest 排行榜列表请求
|
||||
type ListRequest struct {
|
||||
Page int `form:"page" binding:"required,min=1"`
|
||||
PageSize int `form:"page_size" binding:"required,min=1,max=50"`
|
||||
}
|
||||
|
||||
// LeaderboardEntry 排行榜条目(rank 由前端根据 offset + index + 1 计算)
|
||||
type LeaderboardEntry struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
AvailableBalance decimal.Decimal `json:"available_balance"`
|
||||
}
|
||||
|
||||
// ListResponse 排行榜列表响应
|
||||
type ListResponse struct {
|
||||
SortBy string `json:"sort_by"`
|
||||
Order string `json:"order"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
Items []LeaderboardEntry `json:"items"`
|
||||
}
|
||||
|
||||
// UserRankResponse 用户排名响应
|
||||
type UserRankResponse struct {
|
||||
User UserRankInfo `json:"user"`
|
||||
}
|
||||
|
||||
// UserRankInfo 用户排名信息
|
||||
type UserRankInfo struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
Rank int `json:"rank"`
|
||||
AvailableBalance decimal.Decimal `json:"available_balance"`
|
||||
}
|
||||
|
||||
// List 获取排行榜列表
|
||||
// @Summary 获取排行榜列表
|
||||
// @Tags leaderboard
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int true "页码"
|
||||
// @Param page_size query int true "每页数量"
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Router /api/v1/leaderboard [get]
|
||||
func List(c *gin.Context) {
|
||||
var req ListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
response, err := getList(ctx, &req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(response))
|
||||
}
|
||||
|
||||
// GetMyRank 获取当前用户排名
|
||||
// @Summary 获取当前用户排名
|
||||
// @Tags leaderboard
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Router /api/v1/leaderboard/me [get]
|
||||
func GetMyRank(c *gin.Context) {
|
||||
user, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
response, err := getUserRank(ctx, user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(response))
|
||||
}
|
||||
|
||||
// GetUserRankByID 获取指定用户排名
|
||||
// @Summary 获取指定用户排名
|
||||
// @Tags leaderboard
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "用户ID"
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Router /api/v1/leaderboard/users/{id} [get]
|
||||
func GetUserRankByID(c *gin.Context) {
|
||||
userIDStr := c.Param("id")
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid user id"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
response, err := getUserRank(ctx, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(response))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package leaderboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
)
|
||||
|
||||
func getCacheTTL(ctx context.Context) time.Duration {
|
||||
ttl, err := model.GetIntByKey(ctx, model.ConfigKeyLeaderboardCacheTTLSeconds)
|
||||
if err != nil || ttl <= 0 {
|
||||
ttl = 30
|
||||
}
|
||||
return time.Duration(ttl) * time.Second
|
||||
}
|
||||
|
||||
func queryLeaderboard(ctx context.Context, req *ListRequest) ([]LeaderboardEntry, int64, error) {
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
|
||||
baseQuery := db.DB(ctx).Model(&model.User{}).Where("is_active = ?", true)
|
||||
|
||||
var total int64
|
||||
if err := baseQuery.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var items []LeaderboardEntry
|
||||
if err := baseQuery.
|
||||
Select("id as user_id, username, avatar_url, available_balance").
|
||||
Order("available_balance DESC, id ASC").
|
||||
Offset(offset).
|
||||
Limit(req.PageSize).
|
||||
Scan(&items).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
Reference in New Issue
Block a user