455 lines
13 KiB
Go
455 lines
13 KiB
Go
package admin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
nethttp "net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/goravel/framework/contracts/http"
|
|
"github.com/goravel/framework/facades"
|
|
|
|
"goravel/app/models"
|
|
"goravel/app/services"
|
|
"goravel/app/utils"
|
|
)
|
|
|
|
type DashboardController struct{}
|
|
|
|
func NewDashboardController() *DashboardController {
|
|
return &DashboardController{}
|
|
}
|
|
|
|
// GetCount 获取统计数据
|
|
func (r *DashboardController) GetCount(ctx http.Context) http.Response {
|
|
countData := r.getCountData()
|
|
|
|
// 获取今日访问量(今日登录日志数)
|
|
now := time.Now()
|
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
todayEnd := todayStart.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
|
todayVisits, _ := facades.Orm().Query().Model(&models.LoginLog{}).
|
|
Where("created_at >= ?", todayStart).
|
|
Where("created_at <= ?", todayEnd).
|
|
Where("status", 1).
|
|
Count()
|
|
|
|
// 获取在线管理员数
|
|
onlineAdminCount := r.getOnlineAdminCount()
|
|
|
|
// 获取最近一年的订单总数
|
|
orderService := services.NewOrderService()
|
|
orderCountInYear, _ := orderService.GetOrdersCountInYear()
|
|
|
|
return ctx.Response().Success().Json(http.Json{
|
|
"code": 200,
|
|
"message": "get_success",
|
|
"data": map[string]any{
|
|
"admin_count": countData["admins"],
|
|
"role_count": countData["roles"],
|
|
"menu_count": countData["menus"],
|
|
"today_visits": todayVisits,
|
|
"online_admins": onlineAdminCount,
|
|
"order_count_in_year": orderCountInYear,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetUserAccessSource 获取用户访问来源数据(根据 UserAgent 判断设备类型)
|
|
func (r *DashboardController) GetUserAccessSource(ctx http.Context) http.Response {
|
|
// 从登录日志中统计不同设备类型的访问量
|
|
// 统计最近30天的数据
|
|
thirtyDaysAgo := time.Now().AddDate(0, 0, -30)
|
|
|
|
var loginLogs []models.LoginLog
|
|
facades.Orm().Query().Model(&models.LoginLog{}).
|
|
Where("created_at >= ?", thirtyDaysAgo).
|
|
Where("status", 1).
|
|
Get(&loginLogs)
|
|
|
|
// 统计设备类型
|
|
deviceStats := make(map[string]int64)
|
|
for _, log := range loginLogs {
|
|
deviceType := r.parseDeviceType(log.UserAgent)
|
|
deviceStats[deviceType]++
|
|
}
|
|
|
|
// 转换为数组格式
|
|
result := []map[string]any{
|
|
{"name": "桌面端", "value": deviceStats["desktop"]},
|
|
{"name": "移动端", "value": deviceStats["mobile"]},
|
|
{"name": "平板端", "value": deviceStats["tablet"]},
|
|
{"name": "其他", "value": deviceStats["other"]},
|
|
}
|
|
|
|
return ctx.Response().Success().Json(http.Json{
|
|
"code": 200,
|
|
"message": "get_success",
|
|
"data": result,
|
|
})
|
|
}
|
|
|
|
// parseDeviceType 根据 UserAgent 解析设备类型
|
|
func (r *DashboardController) parseDeviceType(userAgent string) string {
|
|
if userAgent == "" {
|
|
return "other"
|
|
}
|
|
|
|
ua := strings.ToLower(userAgent)
|
|
|
|
// 平板设备检测(需要在移动设备之前检测)
|
|
if strings.Contains(ua, "ipad") || (strings.Contains(ua, "tablet") && !strings.Contains(ua, "mobile")) {
|
|
return "tablet"
|
|
}
|
|
|
|
// 移动设备检测
|
|
if strings.Contains(ua, "mobile") || strings.Contains(ua, "android") || strings.Contains(ua, "iphone") {
|
|
return "mobile"
|
|
}
|
|
|
|
// 桌面设备
|
|
return "desktop"
|
|
}
|
|
|
|
// GetWeeklyUserActivity 获取每周用户活跃量(从操作日志统计)
|
|
func (r *DashboardController) GetWeeklyUserActivity(ctx http.Context) http.Response {
|
|
weeklyData := r.getWeeklyUserActivityData()
|
|
return ctx.Response().Success().Json(http.Json{
|
|
"code": 200,
|
|
"message": "get_success",
|
|
"data": weeklyData,
|
|
})
|
|
}
|
|
|
|
// GetMonthlySales 获取每月操作统计(替换销售额数据)
|
|
func (r *DashboardController) GetMonthlySales(ctx http.Context) http.Response {
|
|
// 替换成操作日志月度统计
|
|
monthlyData := r.getMonthlyOperationData()
|
|
return ctx.Response().Success().Json(http.Json{
|
|
"code": 200,
|
|
"message": "get_success",
|
|
"data": monthlyData,
|
|
})
|
|
}
|
|
|
|
// GetRecentActivities 获取最近活动
|
|
func (r *DashboardController) GetRecentActivities(ctx http.Context) http.Response {
|
|
// 获取最近10条操作日志
|
|
var logs []models.OperationLog
|
|
facades.Orm().Query().Model(&models.OperationLog{}).
|
|
With("Admin").
|
|
Order("id desc").
|
|
Limit(10).
|
|
Get(&logs)
|
|
|
|
activities := make([]map[string]any, 0, len(logs))
|
|
for _, log := range logs {
|
|
adminName := "未知用户"
|
|
if log.Admin.ID > 0 {
|
|
adminName = log.Admin.Nickname
|
|
if adminName == "" {
|
|
adminName = log.Admin.Username
|
|
}
|
|
}
|
|
|
|
statusText := "成功"
|
|
statusType := "success"
|
|
if log.Status == 0 {
|
|
statusText = "失败"
|
|
statusType = "danger"
|
|
}
|
|
|
|
// 计算时间差
|
|
var timeAgo string
|
|
if log.CreatedAt != nil {
|
|
// carbon.DateTime 转换为 time.Time
|
|
timeStr := log.CreatedAt.ToDateTimeString()
|
|
if t, err := utils.ParseDateTime(timeStr); err == nil {
|
|
timeAgo = r.formatTimeAgo(t)
|
|
} else {
|
|
timeAgo = "未知"
|
|
}
|
|
} else {
|
|
timeAgo = "未知"
|
|
}
|
|
|
|
activities = append(activities, map[string]any{
|
|
"user": adminName,
|
|
"action": log.Title,
|
|
"time": timeAgo,
|
|
"status": statusText,
|
|
"type": statusType,
|
|
"avatarColor": r.getAvatarColor(adminName),
|
|
})
|
|
}
|
|
|
|
return ctx.Response().Success().Json(http.Json{
|
|
"code": 200,
|
|
"message": "get_success",
|
|
"data": activities,
|
|
})
|
|
}
|
|
|
|
// formatTimeAgo 格式化时间差
|
|
func (r *DashboardController) formatTimeAgo(t time.Time) string {
|
|
now := time.Now()
|
|
duration := now.Sub(t)
|
|
|
|
if duration < time.Minute {
|
|
return "刚刚"
|
|
} else if duration < time.Hour {
|
|
minutes := int(duration.Minutes())
|
|
return fmt.Sprintf("%d分钟前", minutes)
|
|
} else if duration < 24*time.Hour {
|
|
hours := int(duration.Hours())
|
|
return fmt.Sprintf("%d小时前", hours)
|
|
} else {
|
|
days := int(duration.Hours() / 24)
|
|
return fmt.Sprintf("%d天前", days)
|
|
}
|
|
}
|
|
|
|
// getAvatarColor 根据用户名生成头像颜色
|
|
func (r *DashboardController) getAvatarColor(name string) string {
|
|
colors := []string{"#409EFF", "#67C23A", "#E6A23C", "#F56C6C", "#909399", "#606266"}
|
|
if name == "" {
|
|
return colors[0]
|
|
}
|
|
hash := 0
|
|
for _, char := range name {
|
|
hash = hash*31 + int(char)
|
|
}
|
|
return colors[hash%len(colors)]
|
|
}
|
|
|
|
// StreamDashboardData SSE 实时推送 Dashboard 数据
|
|
// 定期推送所有 Dashboard 统计数据,包括计数、用户来源、用户活跃度、销售额等
|
|
func (r *DashboardController) StreamDashboardData(ctx http.Context) http.Response {
|
|
// 获取推送间隔(秒),默认 5 秒
|
|
interval := 5
|
|
if intervalStr := ctx.Request().Query("interval", ""); intervalStr != "" {
|
|
if parsed, err := time.ParseDuration(intervalStr + "s"); err == nil {
|
|
interval = int(parsed.Seconds())
|
|
if interval < 2 {
|
|
interval = 2
|
|
}
|
|
if interval > 60 {
|
|
interval = 60
|
|
}
|
|
}
|
|
}
|
|
|
|
// 设置 SSE 响应头
|
|
writer := ctx.Response().Writer()
|
|
writer.Header().Set("Content-Type", "text/event-stream")
|
|
writer.Header().Set("Cache-Control", "no-cache")
|
|
writer.Header().Set("Connection", "keep-alive")
|
|
writer.Header().Set("X-Accel-Buffering", "no") // 禁用 Nginx 缓冲
|
|
|
|
// 发送初始连接消息
|
|
initMsg := map[string]any{
|
|
"type": "connected",
|
|
"message": "SSE连接已建立,开始推送 Dashboard 数据",
|
|
"interval": interval,
|
|
}
|
|
initData, _ := json.Marshal(initMsg)
|
|
fmt.Fprintf(writer, "data: %s\n\n", string(initData))
|
|
if flusher, ok := writer.(nethttp.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
|
|
// 创建 ticker,定期推送数据
|
|
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
// 检测客户端断开连接
|
|
clientGone := ctx.Request().Origin().Context().Done()
|
|
|
|
for {
|
|
select {
|
|
case <-clientGone:
|
|
// 客户端断开连接
|
|
return nil
|
|
case <-ticker.C:
|
|
// 收集所有 Dashboard 数据
|
|
dashboardData := r.collectDashboardData(ctx)
|
|
|
|
// 构造 SSE 消息
|
|
message := map[string]any{
|
|
"type": "dashboard_data",
|
|
"data": dashboardData,
|
|
"timestamp": time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
messageData, err := json.Marshal(message)
|
|
if err != nil {
|
|
// 记录错误但继续推送
|
|
facades.Log().Errorf("Dashboard SSE: failed to marshal data: %v", err)
|
|
continue
|
|
}
|
|
|
|
// 发送 SSE 消息
|
|
fmt.Fprintf(writer, "data: %s\n\n", string(messageData))
|
|
|
|
// 刷新缓冲区
|
|
if flusher, ok := writer.(nethttp.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// collectDashboardData 收集 Dashboard 数据
|
|
func (r *DashboardController) collectDashboardData(ctx http.Context) map[string]any {
|
|
data := make(map[string]any)
|
|
|
|
// 1. 获取统计数据(管理员、角色、权限等)
|
|
countData := r.getCountData()
|
|
data["count"] = countData
|
|
|
|
// 2. 获取用户访问来源数据
|
|
accessSourceData := r.getUserAccessSourceData()
|
|
data["user_access_source"] = accessSourceData
|
|
|
|
// 3. 获取每周用户活跃量
|
|
weeklyActivityData := r.getWeeklyUserActivityData()
|
|
data["weekly_user_activity"] = weeklyActivityData
|
|
|
|
// 4. 获取每月销售额
|
|
monthlySalesData := r.getMonthlySalesData()
|
|
data["monthly_sales"] = monthlySalesData
|
|
|
|
// 5. 获取在线管理员数
|
|
onlineAdminCount := r.getOnlineAdminCount()
|
|
data["online_admin_count"] = onlineAdminCount
|
|
|
|
return data
|
|
}
|
|
|
|
// getCountData 获取统计数据
|
|
func (r *DashboardController) getCountData() map[string]any {
|
|
// 统计各种数据
|
|
adminCount, _ := facades.Orm().Query().Model(&models.Admin{}).Count()
|
|
roleCount, _ := facades.Orm().Query().Model(&models.Role{}).Count()
|
|
permissionCount, _ := facades.Orm().Query().Model(&models.Permission{}).Count()
|
|
menuCount, _ := facades.Orm().Query().Model(&models.Menu{}).Count()
|
|
departmentCount, _ := facades.Orm().Query().Model(&models.Department{}).Count()
|
|
dictionaryCount, _ := facades.Orm().Query().Model(&models.Dictionary{}).Count()
|
|
configCount, _ := facades.Orm().Query().Model(&models.Config{}).Count()
|
|
|
|
return map[string]any{
|
|
"admins": adminCount,
|
|
"roles": roleCount,
|
|
"permissions": permissionCount,
|
|
"menus": menuCount,
|
|
"departments": departmentCount,
|
|
"dictionaries": dictionaryCount,
|
|
"configs": configCount,
|
|
}
|
|
}
|
|
|
|
// getUserAccessSourceData 获取用户访问来源数据
|
|
func (r *DashboardController) getUserAccessSourceData() []map[string]any {
|
|
// 这里可以根据实际业务逻辑查询用户访问来源
|
|
// 例如:根据登录日志统计不同来源的用户数
|
|
// 暂时返回示例数据
|
|
return []map[string]any{
|
|
{"source": "web", "count": 0},
|
|
{"source": "mobile", "count": 0},
|
|
{"source": "api", "count": 0},
|
|
}
|
|
}
|
|
|
|
// getWeeklyUserActivityData 获取每周用户活跃量(从操作日志统计)
|
|
func (r *DashboardController) getWeeklyUserActivityData() []map[string]any {
|
|
now := time.Now()
|
|
weeklyData := make([]map[string]any, 7)
|
|
|
|
for i := 6; i >= 0; i-- {
|
|
date := now.AddDate(0, 0, -i)
|
|
dateStr := utils.FormatDate(date)
|
|
|
|
// 计算当天的开始和结束时间
|
|
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
|
endOfDay := startOfDay.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
|
|
|
// 统计当天的操作日志数(访问量)
|
|
visitCount, _ := facades.Orm().Query().Model(&models.OperationLog{}).
|
|
Where("created_at >= ?", startOfDay).
|
|
Where("created_at <= ?", endOfDay).
|
|
Where("status", 1).
|
|
Count()
|
|
|
|
// 统计当天活跃的管理员数(去重)
|
|
var uniqueAdmins []uint
|
|
facades.Orm().Query().Model(&models.OperationLog{}).
|
|
Where("created_at >= ?", startOfDay).
|
|
Where("created_at <= ?", endOfDay).
|
|
Where("status", 1).
|
|
Select("DISTINCT admin_id").
|
|
Pluck("admin_id", &uniqueAdmins)
|
|
|
|
userCount := int64(len(uniqueAdmins))
|
|
|
|
weeklyData[6-i] = map[string]any{
|
|
"date": dateStr,
|
|
"visits": visitCount,
|
|
"users": userCount,
|
|
}
|
|
}
|
|
return weeklyData
|
|
}
|
|
|
|
// getMonthlySalesData 获取每月销售额(保留方法名以兼容 SSE)
|
|
func (r *DashboardController) getMonthlySalesData() []map[string]any {
|
|
return r.getMonthlyOperationData()
|
|
}
|
|
|
|
// getMonthlyOperationData 获取每月操作统计(替换销售额)
|
|
func (r *DashboardController) getMonthlyOperationData() []map[string]any {
|
|
now := time.Now()
|
|
monthlyData := make([]map[string]any, 12)
|
|
|
|
for i := 11; i >= 0; i-- {
|
|
date := now.AddDate(0, -i, 0)
|
|
monthStr := date.Format("2006-01")
|
|
|
|
// 计算当月的开始和结束时间
|
|
startOfMonth := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
|
|
var endOfMonth time.Time
|
|
if i == 0 {
|
|
// 当前月,使用当前时间
|
|
endOfMonth = now
|
|
} else {
|
|
// 历史月份,使用月末
|
|
endOfMonth = startOfMonth.AddDate(0, 1, -1).Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
|
}
|
|
|
|
// 统计当月的操作日志数
|
|
operationCount, _ := facades.Orm().Query().Model(&models.OperationLog{}).
|
|
Where("created_at >= ?", startOfMonth).
|
|
Where("created_at <= ?", endOfMonth).
|
|
Where("status", 1).
|
|
Count()
|
|
|
|
monthlyData[11-i] = map[string]any{
|
|
"month": monthStr,
|
|
"count": operationCount,
|
|
}
|
|
}
|
|
return monthlyData
|
|
}
|
|
|
|
// getOnlineAdminCount 获取在线管理员数
|
|
func (r *DashboardController) getOnlineAdminCount() int64 {
|
|
// 统计最近15分钟内有活动的管理员(在线管理员)
|
|
onlineThreshold := time.Now().Add(-15 * time.Minute)
|
|
count, _ := facades.Orm().Query().Model(&models.PersonalAccessToken{}).
|
|
Where("tokenable_type", "admin").
|
|
Where("last_used_at IS NOT NULL").
|
|
Where("last_used_at >= ?", onlineThreshold).
|
|
Count()
|
|
return count
|
|
}
|