Files
OpenFlare/internal/apps/upload/stats.go
T

229 lines
6.5 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package upload
import (
"net/http"
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/gin-gonic/gin"
)
type trendItem struct {
Date string `json:"date"`
Count int64 `json:"count"`
Size int64 `json:"size"`
}
type distributionItem struct {
Name string `json:"name"`
Count int64 `json:"count"`
Size int64 `json:"size"`
}
type fileStatsResponse struct {
TotalCount int64 `json:"total_count"`
TotalSize int64 `json:"total_size"`
Trend []trendItem `json:"trend"`
Categories []distributionItem `json:"categories"`
Types []distributionItem `json:"types"`
}
// GetFileStats 获取当前登录用户的文件统计数据
// @Summary 获取文件统计数据
// @Description 返回当前用户的总文件数、占用大小、最近 7 天新增趋势、文件类型/格式分布等数据
// @Tags upload
// @Produce json
// @Security SessionCookie
// @Success 200 {object} util.ResponseAny{data=fileStatsResponse} "获取成功"
// @Failure 401 {object} util.ResponseAny "未登录"
// @Failure 500 {object} util.ResponseAny "内部错误"
// @Router /api/v1/upload/stats [get]
func GetFileStats(c *gin.Context) {
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
ctx := c.Request.Context()
userID := currUser.ID
// 1. 获取总文件数与总文件大小
var summary struct {
TotalCount int64 `json:"total_count"`
TotalSize int64 `json:"total_size"`
}
err := db.DB(ctx).Model(&model.Upload{}).
Select("COUNT(*) as total_count, COALESCE(SUM(file_size), 0) as total_size").
Where("user_id = ? AND status != ?", userID, model.UploadStatusDeleted).
Scan(&summary).Error
if err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
// 2. 获取业务类型分布 (Group By type)
type rawDist struct {
Key string `gorm:"column:key"`
Count int64 `gorm:"column:count"`
Size int64 `gorm:"column:size"`
}
var typeRaw []rawDist
err = db.DB(ctx).Model(&model.Upload{}).
Select("type as key, COUNT(*) as count, COALESCE(SUM(file_size), 0) as size").
Where("user_id = ? AND status != ?", userID, model.UploadStatusDeleted).
Group("type").
Scan(&typeRaw).Error
if err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
types := make([]distributionItem, 0, len(typeRaw))
for _, tr := range typeRaw {
name := tr.Key
if name == "" {
name = "generic"
}
types = append(types, distributionItem{
Name: name,
Count: tr.Count,
Size: tr.Size,
})
}
// 3. 获取所有文件的大小、后缀与MIME,用于在 Go 中内存分类统计 (避免数据库中写复杂的 JSON/String 匹配逻辑)
type fileCategoryRaw struct {
Extension string `gorm:"column:extension"`
MimeType string `gorm:"column:mime_type"`
FileSize int64 `gorm:"column:file_size"`
}
var fileRaws []fileCategoryRaw
err = db.DB(ctx).Model(&model.Upload{}).
Select("extension, mime_type, file_size").
Where("user_id = ? AND status != ?", userID, model.UploadStatusDeleted).
Scan(&fileRaws).Error
if err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
catCount := make(map[string]int64)
catSize := make(map[string]int64)
categoriesList := []string{"图片", "视频", "音频", "文档", "压缩包", "其他"}
for _, cat := range categoriesList {
catCount[cat] = 0
catSize[cat] = 0
}
for _, fr := range fileRaws {
cat := getFileCategory(fr.MimeType, fr.Extension)
catCount[cat]++
catSize[cat] += fr.FileSize
}
categories := make([]distributionItem, 0, len(categoriesList))
for _, cat := range categoriesList {
categories = append(categories, distributionItem{
Name: cat,
Count: catCount[cat],
Size: catSize[cat],
})
}
// 4. 获取近 7 天的新增文件趋势 (在 Go 中补全没有新增记录的日期为 0)
type fileTrendRaw struct {
CreatedAt time.Time `gorm:"column:created_at"`
FileSize int64 `gorm:"column:file_size"`
}
var trendRaws []fileTrendRaw
// 7天前 00:00:00 (即 6 天前 00:00:00 至今天)
now := time.Now()
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -6)
err = db.DB(ctx).Model(&model.Upload{}).
Select("created_at, file_size").
Where("user_id = ? AND status != ? AND created_at >= ?", userID, model.UploadStatusDeleted, startTime).
Scan(&trendRaws).Error
if err != nil {
c.JSON(http.StatusOK, util.Err(err.Error()))
return
}
trendCountMap := make(map[string]int64)
trendSizeMap := make(map[string]int64)
for i := 0; i < 7; i++ {
dStr := now.AddDate(0, 0, -i).Format("2006-01-02")
trendCountMap[dStr] = 0
trendSizeMap[dStr] = 0
}
for _, tr := range trendRaws {
dStr := tr.CreatedAt.Format("2006-01-02")
if _, exists := trendCountMap[dStr]; exists {
trendCountMap[dStr]++
trendSizeMap[dStr] += tr.FileSize
}
}
const trendDays = 7
trend := make([]trendItem, 0, trendDays)
for i := trendDays - 1; i >= 0; i-- {
dStr := now.AddDate(0, 0, -i).Format("2006-01-02")
trend = append(trend, trendItem{
Date: dStr,
Count: trendCountMap[dStr],
Size: trendSizeMap[dStr],
})
}
c.JSON(http.StatusOK, util.OK(fileStatsResponse{
TotalCount: summary.TotalCount,
TotalSize: summary.TotalSize,
Trend: trend,
Categories: categories,
Types: types,
}))
}
func getFileCategory(mimeType, ext string) string {
mimeType = strings.ToLower(mimeType)
ext = strings.ToLower(ext)
if strings.HasPrefix(mimeType, "image/") || isImageExtension(ext) {
return "图片"
}
if strings.HasPrefix(mimeType, "video/") {
return "视频"
}
if strings.HasPrefix(mimeType, "audio/") {
return "音频"
}
if isArchiveExtension(ext) || strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") || strings.Contains(mimeType, "gzip") {
return "压缩包"
}
if isDocumentExtension(ext) || strings.HasPrefix(mimeType, "text/") || mimeType == "application/pdf" {
return "文档"
}
return "其他"
}
func isArchiveExtension(ext string) bool {
for _, e := range []string{"zip", "rar", "7z", "tar", "gz", "tgz", "bz2", "xz"} {
if ext == e {
return true
}
}
return false
}
func isDocumentExtension(ext string) bool {
for _, e := range []string{"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "md", "csv", "json", "yaml", "yml", "xml"} {
if ext == e {
return true
}
}
return false
}