feat: speed up dashboard page loading

This commit is contained in:
engigu
2025-09-20 14:09:07 +08:00
parent f51295e945
commit 0ecad7f23d
5 changed files with 235 additions and 34 deletions
+96
View File
@@ -129,6 +129,25 @@ type StatisticData struct {
WayCateData []WayCateData `json:"way_cate_data" gorm:"many2many:way_cate_data;"` WayCateData []WayCateData `json:"way_cate_data" gorm:"many2many:way_cate_data;"`
} }
// BasicStatisticData 基础统计数据
type BasicStatisticData struct {
TodaySuccNum int `json:"today_succ_num"`
TodayFailedNum int `json:"today_failed_num"`
TodayTotalNum int `json:"today_total_num"`
MessageTotalNum int `json:"message_total_num"`
HostedMessageTotalNum int `json:"hosted_message_total_num"`
}
// TrendStatisticData 趋势统计数据
type TrendStatisticData struct {
LatestSendData []LatestSendData `json:"latest_send_data"`
}
// ChannelStatisticData 渠道统计数据
type ChannelStatisticData struct {
WayCateData []WayCateData `json:"way_cate_data"`
}
type LatestSendData struct { type LatestSendData struct {
Day string `json:"day"` Day string `json:"day"`
Num int `json:"num"` Num int `json:"num"`
@@ -204,3 +223,80 @@ func GetStatisticData() (StatisticData, error) {
statistic.WayCateData = wayCateData statistic.WayCateData = wayCateData
return statistic, nil return statistic, nil
} }
// GetBasicStatisticData 获取基础统计数据
func GetBasicStatisticData() (BasicStatisticData, error) {
var statistic BasicStatisticData
logt := GetSchema(SendTasksLogs{})
hostedt := GetSchema(HostedMessage{})
currDay := util.GetNowTimeStr()[:10]
// 今日统计数据
query := db.
Table(logt).
Select(`
COUNT(*) AS today_total_num,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS today_succ_num,
SUM(CASE WHEN status != 1 or status is null THEN 1 ELSE 0 END) AS today_failed_num`).
Where("DATE(created_on) = ?", currDay)
query.Take(&statistic)
// 全部消息统计数据
totalQuery := db.Table(logt).Select(`COUNT(*) AS message_total_num`)
totalQuery.Take(&statistic)
// 托管消息统计数据
hostedMessageTotalQuery := db.Table(hostedt).Select(`COUNT(*) AS hosted_message_total_num`)
hostedMessageTotalQuery.Take(&statistic)
return statistic, nil
}
// GetTrendStatisticData 获取趋势统计数据
func GetTrendStatisticData() (TrendStatisticData, error) {
var statistic TrendStatisticData
var latestData []LatestSendData
logt := GetSchema(SendTasksLogs{})
// 最近30天数据
days := 30
now := util.GetNowTime()
past := now.AddDate(0, 0, -days)
pastDate := past.Format("2006-01-02")
next := now.AddDate(0, 0, 1)
nextDate := next.Format("2006-01-02")
queryData := db.
Table(logt).
Select(`
CAST(DATE(created_on) AS CHAR) AS day,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS day_succ_num,
SUM(CASE WHEN status != 1 or status is null THEN 1 ELSE 0 END) AS day_failed_num,
COUNT(*) AS num`).
Where(fmt.Sprintf(" created_on >= '%s' and created_on <= '%s' ", pastDate, nextDate)).
Group("day").
Order("day")
queryData.Scan(&latestData)
statistic.LatestSendData = latestData
return statistic, nil
}
// GetChannelStatisticData 获取渠道统计数据
func GetChannelStatisticData() (ChannelStatisticData, error) {
var statistic ChannelStatisticData
var wayCateData []WayCateData
inst := GetSchema(SendTasksIns{})
wayst := GetSchema(SendWays{})
// 消息实例分类数目
db.
Table(inst).
Select(fmt.Sprintf("%s.name as way_name, count(%s.id) as count_num", wayst, wayst)).
Joins(fmt.Sprintf("JOIN %s ON %s.way_id = %s.id", wayst, inst, wayst)).
Group(fmt.Sprintf("%s.id", wayst)).
Scan(&wayCateData)
statistic.WayCateData = wayCateData
return statistic, nil
}
+3 -1
View File
@@ -2,9 +2,10 @@ package util
import ( import (
"fmt" "fmt"
"github.com/golang-jwt/jwt/v5"
"message-nest/pkg/setting" "message-nest/pkg/setting"
"time" "time"
"github.com/golang-jwt/jwt/v5"
) )
type UserClaims struct { type UserClaims struct {
@@ -16,6 +17,7 @@ type UserClaims struct {
func GenerateToken(username, password string) (string, error) { func GenerateToken(username, password string) (string, error) {
expHours := 1 * 24 * time.Hour expHours := 1 * 24 * time.Hour
//expHours := 1 * time.Minute
SetClaims := UserClaims{ SetClaims := UserClaims{
Username: username, Username: username,
Password: EncodeMD5(password), Password: EncodeMD5(password),
+36 -7
View File
@@ -14,12 +14,41 @@ func GetStatisticData(c *gin.Context) {
appG = app.Gin{C: c} appG = app.Gin{C: c}
) )
msgService := statistic_service.StatisticService{} // 获取类型参数
data, err := msgService.GetStatisticData() statisticType := c.Query("type")
if err != nil {
appG.CResponse(http.StatusInternalServerError, fmt.Sprintf("获取失败!原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "获取成功", data) msgService := statistic_service.StatisticService{}
switch statisticType {
case "basic":
data, err := msgService.GetBasicStatisticData()
if err != nil {
appG.CResponse(http.StatusInternalServerError, fmt.Sprintf("获取基础统计失败!原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "获取基础统计成功", data)
case "trend":
data, err := msgService.GetTrendStatisticData()
if err != nil {
appG.CResponse(http.StatusInternalServerError, fmt.Sprintf("获取趋势统计失败!原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "获取趋势统计成功", data)
case "channels":
data, err := msgService.GetChannelStatisticData()
if err != nil {
appG.CResponse(http.StatusInternalServerError, fmt.Sprintf("获取渠道统计失败!原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "获取渠道统计成功", data)
default:
// 默认返回完整统计数据(保持向后兼容)
data, err := msgService.GetStatisticData()
if err != nil {
appG.CResponse(http.StatusInternalServerError, fmt.Sprintf("获取失败!原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "获取成功", data)
}
} }
+15
View File
@@ -10,3 +10,18 @@ type StatisticService struct {
func (sw *StatisticService) GetStatisticData() (models.StatisticData, error) { func (sw *StatisticService) GetStatisticData() (models.StatisticData, error) {
return models.GetStatisticData() return models.GetStatisticData()
} }
// GetBasicStatisticData 获取基础统计数据
func (sw *StatisticService) GetBasicStatisticData() (models.BasicStatisticData, error) {
return models.GetBasicStatisticData()
}
// GetTrendStatisticData 获取趋势统计数据
func (sw *StatisticService) GetTrendStatisticData() (models.TrendStatisticData, error) {
return models.GetTrendStatisticData()
}
// GetChannelStatisticData 获取渠道统计数据
func (sw *StatisticService) GetChannelStatisticData() (models.ChannelStatisticData, error) {
return models.GetChannelStatisticData()
}
@@ -4,6 +4,7 @@ import { DatabaseIcon, BarChartIcon, SendIcon, CheckCircleIcon, XCircleIcon } fr
// import { LineChart } from "@/components/ui/chart-line" // import { LineChart } from "@/components/ui/chart-line"
import { onMounted, reactive } from 'vue'; import { onMounted, reactive } from 'vue';
import { request } from '@/api/api'; import { request } from '@/api/api';
import { toast } from 'vue-sonner';
// import VueApexCharts from 'vue3-apexcharts' // import VueApexCharts from 'vue3-apexcharts'
import ApexCharts from 'apexcharts' import ApexCharts from 'apexcharts'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@@ -21,54 +22,113 @@ interface CateData {
} }
let state = reactive({ let state = reactive({
data: { basicData: {
message_total_num: 0, message_total_num: 0,
hosted_message_total_num: 0, hosted_message_total_num: 0,
today_total_num: 0, today_total_num: 0,
today_succ_num: 0, today_succ_num: 0,
today_failed_num: 0, today_failed_num: 0,
way_cate_data: [] as CateData[], },
trendData: {
latest_send_data: [] as SendData[], latest_send_data: [] as SendData[],
},
channelData: {
way_cate_data: [] as CateData[],
},
loading: {
basic: false,
trend: false,
channels: false,
} }
}); });
const getStatisticData = async () => { // 获取基础统计数据
const getBasicStatisticData = async () => {
state.loading.basic = true;
try { try {
const rsp = await request.get('/statistic'); const rsp = await request.get('/statistic?type=basic');
if (rsp && rsp.data && rsp.data.code == 200) { if (rsp && rsp.data && rsp.data.code == 200) {
state.data = rsp.data.data; state.basicData = rsp.data.data;
// 数据加载完成后重新渲染图表 } else {
toast.error(rsp?.data?.msg || '获取基础统计数据失败');
}
} catch (error) {
toast.error('获取基础统计数据时发生错误');
} finally {
state.loading.basic = false;
}
}
// 获取趋势统计数据
const getTrendStatisticData = async () => {
state.loading.trend = true;
try {
const rsp = await request.get('/statistic?type=trend');
if (rsp && rsp.data && rsp.data.code == 200) {
state.trendData = rsp.data.data;
// 数据加载完成后重新渲染折线图
setTimeout(() => { setTimeout(() => {
renderLineChart(); renderLineChart();
}, 100);
} else {
toast.error(rsp?.data?.msg || '获取趋势统计数据失败');
}
} catch (error) {
toast.error('获取趋势统计数据时发生错误');
} finally {
state.loading.trend = false;
}
}
// 获取渠道统计数据
const getChannelStatisticData = async () => {
state.loading.channels = true;
try {
const rsp = await request.get('/statistic?type=channels');
if (rsp && rsp.data && rsp.data.code == 200) {
state.channelData = rsp.data.data;
// 数据加载完成后重新渲染饼图
setTimeout(() => {
renderPieChart(); renderPieChart();
}, 100); }, 100);
} else { } else {
console.error('Failed to load statistics:', rsp?.data?.msg || 'Unknown error'); toast.error(rsp?.data?.msg || '获取渠道统计数据失败');
} }
} catch (error) { } catch (error) {
console.error('Error loading statistics:', error); toast.error('获取渠道统计数据时发生错误');
} finally {
state.loading.channels = false;
} }
} }
// 并行加载所有统计数据
const loadAllStatisticData = async () => {
await Promise.all([
getBasicStatisticData(),
getTrendStatisticData(),
getChannelStatisticData()
]);
}
const renderLineChart = () => { const renderLineChart = () => {
const options = { const options = {
series: [ series: [
{ {
name: '发送总数', name: '发送总数',
data: state.data.latest_send_data.length > 0 data: state.trendData.latest_send_data.length > 0
? state.data.latest_send_data.map(item => item.num || 0) ? state.trendData.latest_send_data.map(item => item.num || 0)
: [] : []
}, },
{ {
name: '发送成功数', name: '发送成功数',
data: state.data.latest_send_data.length > 0 data: state.trendData.latest_send_data.length > 0
? state.data.latest_send_data.map(item => item.day_succ_num || 0) ? state.trendData.latest_send_data.map(item => item.day_succ_num || 0)
: [] : []
}, },
{ {
name: '发送失败数', name: '发送失败数',
data: state.data.latest_send_data.length > 0 data: state.trendData.latest_send_data.length > 0
? state.data.latest_send_data.map(item => item.day_failed_num || 0) ? state.trendData.latest_send_data.map(item => item.day_failed_num || 0)
: [] : []
}, },
], ],
@@ -115,8 +175,8 @@ const renderLineChart = () => {
} }
}, },
xaxis: { xaxis: {
categories: state.data.latest_send_data.length > 0 categories: state.trendData.latest_send_data.length > 0
? state.data.latest_send_data.map(item => item.day) ? state.trendData.latest_send_data.map(item => item.day)
: [], : [],
axisBorder: { axisBorder: {
show: false show: false
@@ -268,8 +328,8 @@ const renderLineChart = () => {
const renderPieChart = () => { const renderPieChart = () => {
const options = { const options = {
series: state.data.way_cate_data.length > 0 series: state.channelData.way_cate_data.length > 0
? state.data.way_cate_data.map(item => item.count_num) ? state.channelData.way_cate_data.map(item => item.count_num)
: [], : [],
chart: { chart: {
type: 'pie', type: 'pie',
@@ -290,8 +350,8 @@ const renderPieChart = () => {
} }
} }
}, },
labels: state.data.way_cate_data.length > 0 labels: state.channelData.way_cate_data.length > 0
? state.data.way_cate_data.map(item => item.way_name) ? state.channelData.way_cate_data.map(item => item.way_name)
: [], : [],
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'], colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'],
legend: { legend: {
@@ -353,8 +413,7 @@ const renderPieChart = () => {
} }
onMounted(() => { onMounted(() => {
getStatisticData(); loadAllStatisticData();
// renderLineChart();
}) })
@@ -363,11 +422,11 @@ onMounted(() => {
<template> <template>
<div class="w-[90%] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"> <div class="w-[90%] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
<StatCard title="推送记录留存数" :value="state.data.message_total_num" description="" :icon="DatabaseIcon" /> <StatCard title="推送记录留存数" :value="state.basicData.message_total_num" description="" :icon="DatabaseIcon" />
<StatCard title="托管消息数" :value="state.data.hosted_message_total_num" description="" :icon="BarChartIcon" /> <StatCard title="托管消息数" :value="state.basicData.hosted_message_total_num" description="" :icon="BarChartIcon" />
<StatCard title="今日发送总数" :value="state.data.today_total_num" description="" :icon="SendIcon" /> <StatCard title="今日发送总数" :value="state.basicData.today_total_num" description="" :icon="SendIcon" />
<StatCard title="今日成功数" :value="state.data.today_succ_num" description="" :icon="CheckCircleIcon" /> <StatCard title="今日成功数" :value="state.basicData.today_succ_num" description="" :icon="CheckCircleIcon" />
<StatCard title="今日失败数" :value="state.data.today_failed_num" description="" :icon="XCircleIcon" /> <StatCard title="今日失败数" :value="state.basicData.today_failed_num" description="" :icon="XCircleIcon" />
</div> </div>
<!-- 折线图 --> <!-- 折线图 -->