使用了AI重构项目,并完善了一部分后台问题
This commit is contained in:
+344
-154
@@ -3,191 +3,381 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"home-vue-go/internal/analytics"
|
||||
"home-vue-go/internal/config"
|
||||
"home-vue-go/internal/database"
|
||||
"home-vue-go/internal/ent/visit"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetStats 获取统计数据
|
||||
func GetStats(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 获取站点数量
|
||||
sites, _ := db.Client.Site.Query().All(ctx)
|
||||
totalSites := len(sites)
|
||||
|
||||
// 获取总访问量(从内存记录)
|
||||
records := getVisitRecords()
|
||||
totalViews := len(records)
|
||||
|
||||
// 获取独立访客数(去重IP)
|
||||
uniqueIPs := make(map[string]bool)
|
||||
for _, r := range records {
|
||||
uniqueIPs[r.IP] = true
|
||||
}
|
||||
uniqueVisitors := len(uniqueIPs)
|
||||
|
||||
// 获取今日访问量
|
||||
today := time.Now()
|
||||
todayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location())
|
||||
todayViews := 0
|
||||
for _, r := range records {
|
||||
if r.VisitTime.After(todayStart) {
|
||||
todayViews++
|
||||
}
|
||||
}
|
||||
|
||||
stats := gin.H{
|
||||
"totalViews": totalViews,
|
||||
"uniqueVisitors": uniqueVisitors,
|
||||
"todayViews": todayViews,
|
||||
"totalSites": totalSites,
|
||||
}
|
||||
type analyticsCache struct {
|
||||
mu sync.Mutex
|
||||
statsExpires time.Time
|
||||
chartsExpires time.Time
|
||||
statsKey string
|
||||
chartsKey string
|
||||
stats gin.H
|
||||
trend []gin.H
|
||||
sources []gin.H
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
var dashboardAnalyticsCache analyticsCache
|
||||
var umamiClient = analytics.NewClient()
|
||||
|
||||
func GetStats(db *database.Database, cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
settings, err := db.LoadSiteSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载统计配置失败"})
|
||||
return
|
||||
}
|
||||
siteCount, _ := db.Client.Site.Query().Count(c.Request.Context())
|
||||
if settings.AnalyticsProvider == "umami" {
|
||||
stats, status, err := loadUmamiStats(c, settings, siteCount, cfg)
|
||||
if err != nil {
|
||||
c.JSON(status, gin.H{"error": "Umami统计暂时不可用", "analyticsSource": "umami", "analyticsConfigured": false, "analyticsError": "unavailable"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, stats)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, localStats(c, db, siteCount))
|
||||
}
|
||||
}
|
||||
|
||||
// GetChartData 获取图表数据
|
||||
func GetChartData(db *database.Database) gin.HandlerFunc {
|
||||
func GetChartData(db *database.Database, cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
period := c.DefaultQuery("period", "7")
|
||||
|
||||
// 根据period确定天数
|
||||
periodDays := 7
|
||||
if period == "30" {
|
||||
periodDays = 30
|
||||
} else if period == "90" {
|
||||
periodDays = 90
|
||||
settings, err := db.LoadSiteSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载统计配置失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取真实趋势数据(从内存记录)
|
||||
records := getVisitRecords()
|
||||
trend := []gin.H{}
|
||||
now := time.Now()
|
||||
|
||||
for i := 0; i < periodDays; i++ {
|
||||
date := now.AddDate(0, 0, -(periodDays-1-i))
|
||||
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
dayEnd := dayStart.Add(24 * time.Hour)
|
||||
|
||||
// 统计当天的访问量
|
||||
count := 0
|
||||
for _, r := range records {
|
||||
if r.VisitTime.After(dayStart) && r.VisitTime.Before(dayEnd) {
|
||||
count++
|
||||
}
|
||||
periodDays := chartPeriodDays(c.DefaultQuery("period", "7"))
|
||||
if settings.AnalyticsProvider == "umami" {
|
||||
trend, sources, status, err := loadUmamiCharts(c, settings, periodDays, cfg)
|
||||
if err != nil {
|
||||
c.JSON(status, gin.H{"error": "Umami统计暂时不可用", "analyticsSource": "umami", "analyticsConfigured": false, "analyticsError": "unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
trend = append(trend, gin.H{
|
||||
"label": date.Format("1/2"),
|
||||
"value": count,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"trend": trend, "sources": sources, "analyticsSource": "umami", "analyticsConfigured": true, "analyticsError": ""})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取访问来源数据(基于referer)
|
||||
sourceMap := make(map[string]int)
|
||||
total := len(records)
|
||||
|
||||
for _, r := range records {
|
||||
ref := strings.ToLower(r.Referer)
|
||||
if ref == "" {
|
||||
sourceMap["直接访问"]++
|
||||
} else if strings.Contains(ref, "google") || strings.Contains(ref, "baidu") || strings.Contains(ref, "bing") || strings.Contains(ref, "yahoo") || strings.Contains(ref, "sogou") {
|
||||
sourceMap["搜索引擎"]++
|
||||
} else if strings.Contains(ref, "twitter") || strings.Contains(ref, "facebook") || strings.Contains(ref, "weibo") || strings.Contains(ref, "wechat") || strings.Contains(ref, "qq") {
|
||||
sourceMap["社交媒体"]++
|
||||
} else {
|
||||
sourceMap["其他"]++
|
||||
}
|
||||
}
|
||||
|
||||
sources := []gin.H{}
|
||||
if total > 0 {
|
||||
for label, count := range sourceMap {
|
||||
sources = append(sources, gin.H{
|
||||
"label": label,
|
||||
"value": (count * 100) / total, // 转换为百分比
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 如果没有数据,返回默认值
|
||||
sources = []gin.H{
|
||||
{"label": "直接访问", "value": 100},
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"trend": trend,
|
||||
"sources": sources,
|
||||
})
|
||||
trend, sources := localCharts(c, db, periodDays)
|
||||
c.JSON(http.StatusOK, gin.H{"trend": trend, "sources": sources, "analyticsSource": "local", "analyticsConfigured": true, "analyticsError": ""})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// GetRecentVisits 获取最近访问记录
|
||||
func GetRecentVisits(db *database.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 支持通过 query 参数自定义条数,默认 5,最大 50
|
||||
settings, err := db.LoadSiteSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载统计配置失败"})
|
||||
return
|
||||
}
|
||||
if settings.AnalyticsProvider == "umami" {
|
||||
c.JSON(http.StatusOK, []gin.H{})
|
||||
return
|
||||
}
|
||||
limit := 5
|
||||
if q := c.DefaultQuery("limit", "5"); q != "" {
|
||||
if n, err := parseInt(q); err == nil && n > 0 {
|
||||
limit = n
|
||||
if limit > 50 {
|
||||
limit = 50
|
||||
}
|
||||
if value, err := strconv.Atoi(c.DefaultQuery("limit", "5")); err == nil && value > 0 {
|
||||
limit = value
|
||||
if limit > 50 {
|
||||
limit = 50
|
||||
}
|
||||
}
|
||||
|
||||
// 从内存记录获取最近访问
|
||||
records := getVisitRecords()
|
||||
result := make([]gin.H, 0, limit)
|
||||
records, err := queryVisits(c, db, 0)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载访问记录失败"})
|
||||
return
|
||||
}
|
||||
result := make([]gin.H, 0, min(limit, len(records)))
|
||||
now := time.Now()
|
||||
|
||||
// 取最近 limit 条
|
||||
start := len(records) - limit
|
||||
if start < 0 {
|
||||
start = 0
|
||||
for index := len(records) - 1; index >= 0 && len(result) < limit; index-- {
|
||||
record := records[index]
|
||||
result = append(result, gin.H{"path": record.Path, "ip": record.IP, "time": relativeVisitTime(now, record.VisitTime)})
|
||||
}
|
||||
|
||||
for i := len(records) - 1; i >= start && i >= 0; i-- {
|
||||
r := records[i]
|
||||
|
||||
// 计算相对时间
|
||||
diff := now.Sub(r.VisitTime)
|
||||
var timeStr string
|
||||
if diff < time.Minute {
|
||||
timeStr = "刚刚"
|
||||
} else if diff < time.Hour {
|
||||
timeStr = fmt.Sprintf("%.0f分钟前", diff.Minutes())
|
||||
} else if diff < 24*time.Hour {
|
||||
timeStr = fmt.Sprintf("%.0f小时前", diff.Hours())
|
||||
} else {
|
||||
timeStr = fmt.Sprintf("%.0f天前", diff.Hours()/24)
|
||||
}
|
||||
|
||||
result = append(result, gin.H{
|
||||
"path": r.Path,
|
||||
"ip": r.IP,
|
||||
"time": timeStr,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyConfigUpdate 通知配置更新(用于热重载)
|
||||
func NotifyConfigUpdate() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 这里可以添加通知逻辑,比如通过WebSocket或SSE通知前端
|
||||
// 目前简单返回成功
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "配置更新通知已发送",
|
||||
})
|
||||
resetAnalyticsCache()
|
||||
c.JSON(http.StatusOK, gin.H{"message": "配置更新通知已发送"})
|
||||
}
|
||||
}
|
||||
|
||||
func loadUmamiStats(c *gin.Context, settings *config.SiteSettings, siteCount int, appConfig *config.Config) (gin.H, int, error) {
|
||||
credential, err := decryptUmamiCredential(settings, appConfig)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadGateway, err
|
||||
}
|
||||
cacheKey := "stats:" + settings.UmamiWebsiteID + ":" + settings.UmamiAPIURL
|
||||
dashboardAnalyticsCache.mu.Lock()
|
||||
if dashboardAnalyticsCache.statsKey == cacheKey && time.Now().Before(dashboardAnalyticsCache.statsExpires) && dashboardAnalyticsCache.stats != nil {
|
||||
value := dashboardAnalyticsCache.stats
|
||||
dashboardAnalyticsCache.mu.Unlock()
|
||||
return value, http.StatusOK, nil
|
||||
}
|
||||
dashboardAnalyticsCache.mu.Unlock()
|
||||
|
||||
clientConfig := analytics.Config{Mode: settings.UmamiAPIMode, APIURL: settings.UmamiAPIURL, Credential: credential, WebsiteID: settings.UmamiWebsiteID}
|
||||
now := time.Now()
|
||||
allTime, err := umamiClient.GetStats(c.Request.Context(), clientConfig, time.Unix(0, 0), now)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadGateway, err
|
||||
}
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
todayStats, err := umamiClient.GetStats(c.Request.Context(), clientConfig, today, now)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadGateway, err
|
||||
}
|
||||
active, err := umamiClient.GetActive(c.Request.Context(), clientConfig)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadGateway, err
|
||||
}
|
||||
result := gin.H{"totalViews": allTime.Pageviews, "uniqueVisitors": allTime.Visitors, "todayViews": todayStats.Pageviews, "activeVisitors": active, "totalSites": siteCount, "analyticsSource": "umami", "analyticsConfigured": true}
|
||||
dashboardAnalyticsCache.mu.Lock()
|
||||
dashboardAnalyticsCache.statsKey, dashboardAnalyticsCache.statsExpires, dashboardAnalyticsCache.stats = cacheKey, time.Now().Add(30*time.Second), result
|
||||
dashboardAnalyticsCache.mu.Unlock()
|
||||
return result, http.StatusOK, nil
|
||||
}
|
||||
|
||||
func loadUmamiCharts(c *gin.Context, settings *config.SiteSettings, periodDays int, appConfig *config.Config) ([]gin.H, []gin.H, int, error) {
|
||||
credential, err := decryptUmamiCredential(settings, appConfig)
|
||||
if err != nil {
|
||||
return nil, nil, http.StatusBadGateway, err
|
||||
}
|
||||
cacheKey := fmt.Sprintf("charts:%s:%d:%s", settings.UmamiWebsiteID, periodDays, settings.UmamiAPIURL)
|
||||
dashboardAnalyticsCache.mu.Lock()
|
||||
if dashboardAnalyticsCache.chartsKey == cacheKey && time.Now().Before(dashboardAnalyticsCache.chartsExpires) && dashboardAnalyticsCache.trend != nil {
|
||||
trend, sources := dashboardAnalyticsCache.trend, dashboardAnalyticsCache.sources
|
||||
dashboardAnalyticsCache.mu.Unlock()
|
||||
return trend, sources, http.StatusOK, nil
|
||||
}
|
||||
dashboardAnalyticsCache.mu.Unlock()
|
||||
|
||||
clientConfig := analytics.Config{Mode: settings.UmamiAPIMode, APIURL: settings.UmamiAPIURL, Credential: credential, WebsiteID: settings.UmamiWebsiteID}
|
||||
now := time.Now()
|
||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -periodDays+1)
|
||||
pageviews, err := umamiClient.GetPageviews(c.Request.Context(), clientConfig, start, now)
|
||||
if err != nil {
|
||||
return nil, nil, http.StatusBadGateway, err
|
||||
}
|
||||
referrers, err := umamiClient.GetMetrics(c.Request.Context(), clientConfig, start, now, "referrer")
|
||||
if err != nil {
|
||||
return nil, nil, http.StatusBadGateway, err
|
||||
}
|
||||
trend := make([]gin.H, 0, len(pageviews))
|
||||
for _, point := range pageviews {
|
||||
trend = append(trend, gin.H{"label": point.X, "value": point.Y})
|
||||
}
|
||||
sources := percentageSources(referrers)
|
||||
dashboardAnalyticsCache.mu.Lock()
|
||||
dashboardAnalyticsCache.chartsKey, dashboardAnalyticsCache.chartsExpires, dashboardAnalyticsCache.trend, dashboardAnalyticsCache.sources = cacheKey, time.Now().Add(30*time.Second), trend, sources
|
||||
dashboardAnalyticsCache.mu.Unlock()
|
||||
return trend, sources, http.StatusOK, nil
|
||||
}
|
||||
|
||||
func localStats(c *gin.Context, db *database.Database, siteCount int) gin.H {
|
||||
records, err := queryVisits(c, db, 0)
|
||||
if err != nil {
|
||||
return gin.H{"totalViews": 0, "uniqueVisitors": 0, "todayViews": 0, "activeVisitors": 0, "totalSites": siteCount, "analyticsSource": "local", "analyticsConfigured": true, "analyticsError": "query_failed"}
|
||||
}
|
||||
now := time.Now()
|
||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
unique := map[string]struct{}{}
|
||||
active := map[string]struct{}{}
|
||||
todayViews := 0
|
||||
for _, record := range records {
|
||||
key := record.IP + "|" + record.UserAgent
|
||||
unique[key] = struct{}{}
|
||||
if !record.VisitTime.Before(start) {
|
||||
todayViews++
|
||||
}
|
||||
if !record.VisitTime.Before(now.Add(-5 * time.Minute)) {
|
||||
active[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
return gin.H{"totalViews": len(records), "uniqueVisitors": len(unique), "todayViews": todayViews, "activeVisitors": len(active), "totalSites": siteCount, "analyticsSource": "local", "analyticsConfigured": true, "analyticsError": ""}
|
||||
}
|
||||
|
||||
func localCharts(c *gin.Context, db *database.Database, periodDays int) ([]gin.H, []gin.H) {
|
||||
records, err := queryVisits(c, db, 0)
|
||||
if err != nil {
|
||||
return emptyTrend(periodDays), []gin.H{{"label": "直接访问", "value": 100}}
|
||||
}
|
||||
now := time.Now()
|
||||
trend := make([]gin.H, 0, periodDays)
|
||||
for index := 0; index < periodDays; index++ {
|
||||
date := now.AddDate(0, 0, -(periodDays - 1 - index))
|
||||
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
dayEnd := dayStart.AddDate(0, 0, 1)
|
||||
count := 0
|
||||
for _, record := range records {
|
||||
if !record.VisitTime.Before(dayStart) && record.VisitTime.Before(dayEnd) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
trend = append(trend, gin.H{"label": date.Format("1/2"), "value": count})
|
||||
}
|
||||
counts := make(map[string]int)
|
||||
for _, record := range records {
|
||||
counts[classifyReferer(record.Referer)]++
|
||||
}
|
||||
sources := percentageCounts(counts, len(records))
|
||||
return trend, sources
|
||||
}
|
||||
|
||||
func queryVisits(c *gin.Context, db *database.Database, days int) ([]*structVisit, error) {
|
||||
query := db.Client.Visit.Query().Order(visit.ByVisitTime())
|
||||
if days > 0 {
|
||||
query = query.Where(visit.VisitTimeGTE(time.Now().AddDate(0, 0, -days)))
|
||||
}
|
||||
rows, err := query.All(c.Request.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*structVisit, len(rows))
|
||||
for index, row := range rows {
|
||||
result[index] = &structVisit{Path: row.Path, IP: row.IP, UserAgent: row.UserAgent, Referer: row.Referer, VisitTime: row.VisitTime}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type structVisit struct {
|
||||
Path, IP, UserAgent, Referer string
|
||||
VisitTime time.Time
|
||||
}
|
||||
|
||||
func percentageSources(points []analytics.Point) []gin.H {
|
||||
counts := make(map[string]int)
|
||||
for _, point := range points {
|
||||
counts[classifyReferer(point.X)] += point.Y
|
||||
}
|
||||
total := 0
|
||||
for _, count := range counts {
|
||||
total += count
|
||||
}
|
||||
return percentageCounts(counts, total)
|
||||
}
|
||||
|
||||
func percentageCounts(counts map[string]int, total int) []gin.H {
|
||||
if total <= 0 {
|
||||
return []gin.H{{"label": "直接访问", "value": 100}}
|
||||
}
|
||||
type item struct {
|
||||
label string
|
||||
count, value, remainder int
|
||||
}
|
||||
items := make([]item, 0, len(counts))
|
||||
used := 0
|
||||
for label, count := range counts {
|
||||
value := count * 100 / total
|
||||
items = append(items, item{label: label, count: count, value: value, remainder: count * 100 % total})
|
||||
used += value
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].remainder != items[j].remainder {
|
||||
return items[i].remainder > items[j].remainder
|
||||
}
|
||||
return items[i].label < items[j].label
|
||||
})
|
||||
for index := 0; index < 100-used && len(items) > 0; index++ {
|
||||
items[index%len(items)].value++
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].count != items[j].count {
|
||||
return items[i].count > items[j].count
|
||||
}
|
||||
return items[i].label < items[j].label
|
||||
})
|
||||
result := make([]gin.H, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, gin.H{"label": item.label, "value": item.value})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func classifyReferer(referrer string) string {
|
||||
referrer = strings.ToLower(strings.TrimSpace(referrer))
|
||||
if referrer == "" {
|
||||
return "直接访问"
|
||||
}
|
||||
for _, source := range []string{"google", "baidu", "bing", "yahoo", "sogou"} {
|
||||
if strings.Contains(referrer, source) {
|
||||
return "搜索引擎"
|
||||
}
|
||||
}
|
||||
for _, source := range []string{"twitter", "facebook", "weibo", "wechat", "qq"} {
|
||||
if strings.Contains(referrer, source) {
|
||||
return "社交媒体"
|
||||
}
|
||||
}
|
||||
return "其他"
|
||||
}
|
||||
|
||||
func decryptUmamiCredential(settings *config.SiteSettings, appConfig *config.Config) (string, error) {
|
||||
if settings.UmamiWebsiteID == "" || settings.UmamiCredential == "" {
|
||||
return "", fmt.Errorf("Umami配置不完整")
|
||||
}
|
||||
if appConfig == nil {
|
||||
return "", fmt.Errorf("统计加密配置不可用")
|
||||
}
|
||||
return appConfig.DecryptSecret(settings.UmamiCredential)
|
||||
}
|
||||
|
||||
func resetAnalyticsCache() {
|
||||
dashboardAnalyticsCache.mu.Lock()
|
||||
dashboardAnalyticsCache.statsKey = ""
|
||||
dashboardAnalyticsCache.chartsKey = ""
|
||||
dashboardAnalyticsCache.statsExpires = time.Time{}
|
||||
dashboardAnalyticsCache.chartsExpires = time.Time{}
|
||||
dashboardAnalyticsCache.stats = nil
|
||||
dashboardAnalyticsCache.trend = nil
|
||||
dashboardAnalyticsCache.sources = nil
|
||||
dashboardAnalyticsCache.mu.Unlock()
|
||||
}
|
||||
func chartPeriodDays(value string) int {
|
||||
switch value {
|
||||
case "30":
|
||||
return 30
|
||||
case "90":
|
||||
return 90
|
||||
default:
|
||||
return 7
|
||||
}
|
||||
}
|
||||
func emptyTrend(days int) []gin.H {
|
||||
result := make([]gin.H, days)
|
||||
for index := range result {
|
||||
result[index] = gin.H{"label": "", "value": 0}
|
||||
}
|
||||
return result
|
||||
}
|
||||
func relativeVisitTime(now, visited time.Time) string {
|
||||
diff := now.Sub(visited)
|
||||
switch {
|
||||
case diff < time.Minute:
|
||||
return "刚刚"
|
||||
case diff < time.Hour:
|
||||
return fmt.Sprintf("%.0f分钟前", diff.Minutes())
|
||||
case diff < 24*time.Hour:
|
||||
return fmt.Sprintf("%.0f小时前", diff.Hours())
|
||||
default:
|
||||
return fmt.Sprintf("%.0f天前", diff.Hours()/24)
|
||||
}
|
||||
}
|
||||
func min(left, right int) int {
|
||||
if left < right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user