feat: 完成后台Issue#6 数据统计接口模块

This commit is contained in:
root
2026-03-09 19:26:07 +08:00
parent 54cf7ea37f
commit bd1e644ef5
3 changed files with 230 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
package admin
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"wx_service/internal/model"
)
func (h *Handler) StatsOverview(c *gin.Context) {
data, err := h.svc.StatsOverview(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load stats overview failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) StatsMiniPrograms(c *gin.Context) {
data, err := h.svc.StatsMiniPrograms(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load mini-program stats failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) StatsUserGrowth(c *gin.Context) {
days, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("days", "7")))
data, err := h.svc.StatsUserGrowth(c.Request.Context(), days)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load user growth failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
+187
View File
@@ -0,0 +1,187 @@
package admin
import (
"context"
"time"
expirymodel "wx_service/internal/expiry"
membershipmodel "wx_service/internal/membership/model"
"wx_service/internal/model"
rmmodel "wx_service/internal/remove_watermark/model"
)
type OverviewStats struct {
TotalMiniPrograms int64 `json:"total_mini_programs"`
TotalUsers int64 `json:"total_users"`
TotalMembers int64 `json:"total_members"`
TodayNewUsers int64 `json:"today_new_users"`
}
func (s *Service) StatsOverview(ctx context.Context) (*OverviewStats, error) {
var result OverviewStats
if err := s.db.WithContext(ctx).Model(&model.MiniProgram{}).Count(&result.TotalMiniPrograms).Error; err != nil {
return nil, err
}
if err := s.db.WithContext(ctx).Model(&model.User{}).Count(&result.TotalUsers).Error; err != nil {
return nil, err
}
now := time.Now()
if err := s.db.WithContext(ctx).Model(&model.UserMembership{}).
Where("status = ? AND ends_at > ?", "active", now).
Count(&result.TotalMembers).Error; err != nil {
return nil, err
}
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
if err := s.db.WithContext(ctx).Model(&model.User{}).
Where("created_at >= ?", todayStart).
Count(&result.TodayNewUsers).Error; err != nil {
return nil, err
}
return &result, nil
}
type MiniProgramStatsItem struct {
MiniProgramID uint `json:"mini_program_id"`
Name string `json:"name"`
UserCount int64 `json:"user_count"`
MemberCount int64 `json:"member_count"`
DataCount int64 `json:"data_count"`
TodayActive int64 `json:"today_active"`
}
func (s *Service) StatsMiniPrograms(ctx context.Context) ([]MiniProgramStatsItem, error) {
var miniPrograms []model.MiniProgram
if err := s.db.WithContext(ctx).
Select("id", "name", "app_id", "description", "created_at", "updated_at").
Order("id ASC").
Find(&miniPrograms).Error; err != nil {
return nil, err
}
if len(miniPrograms) == 0 {
return []MiniProgramStatsItem{}, nil
}
ids := make([]uint, 0, len(miniPrograms))
for _, item := range miniPrograms {
ids = append(ids, item.ID)
}
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
userCounts, err := s.groupCountByMiniProgramID(ctx, &model.User{}, ids, "")
if err != nil {
return nil, err
}
memberCounts, err := s.groupCountByMiniProgramID(ctx, &model.UserMembership{}, ids, "status = ? AND ends_at > ?", "active", now)
if err != nil {
return nil, err
}
todayUserCounts, err := s.groupCountByMiniProgramID(ctx, &model.User{}, ids, "created_at >= ?", todayStart)
if err != nil {
return nil, err
}
expiryCounts, err := s.groupCountByMiniProgramID(ctx, &expirymodel.ExpiryItem{}, ids, "")
if err != nil {
return nil, err
}
videoCounts, err := s.groupCountByMiniProgramID(ctx, &rmmodel.VideoParseLog{}, ids, "")
if err != nil {
return nil, err
}
redeemCounts, err := s.groupCountByMiniProgramID(ctx, &membershipmodel.MembershipRedemption{}, ids, "")
if err != nil {
return nil, err
}
result := make([]MiniProgramStatsItem, 0, len(miniPrograms))
for _, item := range miniPrograms {
dataCount := expiryCounts[item.ID] + videoCounts[item.ID] + redeemCounts[item.ID]
result = append(result, MiniProgramStatsItem{
MiniProgramID: item.ID,
Name: item.Name,
UserCount: userCounts[item.ID],
MemberCount: memberCounts[item.ID],
DataCount: dataCount,
TodayActive: todayUserCounts[item.ID],
})
}
return result, nil
}
type UserGrowthPoint struct {
Date string `json:"date"`
Count int64 `json:"count"`
}
func (s *Service) StatsUserGrowth(ctx context.Context, days int) ([]UserGrowthPoint, error) {
if days <= 0 {
days = 7
}
if days > 90 {
days = 90
}
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
startDate := todayStart.AddDate(0, 0, -(days - 1))
endDate := todayStart.AddDate(0, 0, 1)
type row struct {
Date string `gorm:"column:date"`
Count int64 `gorm:"column:count"`
}
var rows []row
if err := s.db.WithContext(ctx).
Model(&model.User{}).
Select("DATE(created_at) AS date, COUNT(*) AS count").
Where("created_at >= ? AND created_at < ?", startDate, endDate).
Group("DATE(created_at)").
Order("DATE(created_at) ASC").
Scan(&rows).Error; err != nil {
return nil, err
}
countByDate := make(map[string]int64, len(rows))
for _, item := range rows {
countByDate[item.Date] = item.Count
}
result := make([]UserGrowthPoint, 0, days)
for i := 0; i < days; i++ {
date := startDate.AddDate(0, 0, i).Format("2006-01-02")
result = append(result, UserGrowthPoint{
Date: date,
Count: countByDate[date],
})
}
return result, nil
}
func (s *Service) groupCountByMiniProgramID(ctx context.Context, modelObj interface{}, ids []uint, where string, args ...interface{}) (map[uint]int64, error) {
result := make(map[uint]int64)
if len(ids) == 0 {
return result, nil
}
query := s.db.WithContext(ctx).
Model(modelObj).
Select("mini_program_id, COUNT(*) AS count").
Where("mini_program_id IN ?", ids)
if where != "" {
query = query.Where(where, args...)
}
var rows []groupedCountRow
if err := query.Group("mini_program_id").Scan(&rows).Error; err != nil {
return nil, err
}
for _, item := range rows {
result[item.MiniProgramID] = item.Count
}
return result, nil
}
+4
View File
@@ -20,6 +20,10 @@ func registerAdminRoutes(router *gin.Engine, handler *adminhandler.Handler) {
{
protected.GET("/profile", handler.Profile)
protected.POST("/logout", handler.Logout)
protected.GET("/stats/overview", handler.StatsOverview)
protected.GET("/stats/mini-programs", handler.StatsMiniPrograms)
protected.GET("/stats/user-growth", handler.StatsUserGrowth)
}
}
}