基于 opsx-web-lite 框架(Go+Gin+Vue3+Element Plus)构建许可证管理服务器,
业务域参考 Licence 服务器 Web 版。
【后端 - 4层架构】
- 认证:单 Admin 表 + JWT(HS256)
- 核心授权域:Product / Customer / License / Activation
· License 生成/验证/激活/机器绑定/心跳/续期/吊销
· 客户端契约接口:/api/v1/license/{verify,activate,deactivate,heartbeat}
- 定价订单:Plan / Order / 支付网关配置
- 防重复购买:CustomerEntitlement + 幂等支付 webhook + 手动发放
- 分析审计:Dashboard 统计 / Analytics / AuditLog / Setting 管理
【前端】
- Dashboard(ECharts 图表)、产品、客户、License、计划、订单、权益、日志、设置
- 9 个完整 CRUD 业务页面 + 登录/布局
【部署】
- SQLite + 单二进制(//go:embed 嵌入前端)
- 开发:./run.sh start(前端5181/后端9001)
- 生产:./run.sh build && ./run.sh binary start
- 默认账号:admin/admin
Author: Logan.Li
168 lines
5.4 KiB
Go
168 lines
5.4 KiB
Go
package analytics
|
|
|
|
import (
|
|
"backend/internal/dto"
|
|
"backend/internal/model"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// AnalyticsService 数据分析业务逻辑层 // Logan.Li
|
|
type AnalyticsService struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewAnalyticsService 创建 service 实例 // Logan.Li
|
|
func NewAnalyticsService(db *gorm.DB) *AnalyticsService {
|
|
return &AnalyticsService{db: db}
|
|
}
|
|
|
|
// GetDashboardStats 仪表板统计 // Logan.Li
|
|
func (s *AnalyticsService) GetDashboardStats() (*dto.DashboardStats, error) {
|
|
var stats dto.DashboardStats
|
|
|
|
s.db.Model(&model.License{}).Count(&stats.TotalLicenses)
|
|
s.db.Model(&model.License{}).Where("status = ?", model.LicenseStatusActive).Count(&stats.ActiveLicenses)
|
|
|
|
// 本月新增 // Logan.Li
|
|
now := time.Now()
|
|
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
s.db.Model(&model.License{}).Where("created_at >= ?", monthStart).Count(&stats.NewThisMonth)
|
|
|
|
// 即将到期(30 天内) // Logan.Li
|
|
thirtyDaysLater := now.AddDate(0, 0, 30)
|
|
s.db.Model(&model.License{}).
|
|
Where("status = ? AND end_date > ? AND end_date < ?", model.LicenseStatusActive, now, thirtyDaysLater).
|
|
Count(&stats.ExpiringSoon)
|
|
|
|
s.db.Model(&model.Customer{}).Count(&stats.TotalCustomers)
|
|
s.db.Model(&model.Product{}).Count(&stats.TotalProducts)
|
|
|
|
// 在线设备(5 分钟内有心跳) // Logan.Li
|
|
fiveMinAgo := now.Add(-5 * time.Minute)
|
|
s.db.Model(&model.Activation{}).Where("is_online = ? AND last_seen_at >= ?", true, fiveMinAgo).Count(&stats.OnlineDevices)
|
|
|
|
// 本月收入 // Logan.Li
|
|
s.db.Model(&model.Order{}).
|
|
Where("status = ? AND paid_at >= ?", "paid", monthStart).
|
|
Select("COALESCE(SUM(amount), 0)").
|
|
Scan(&stats.MonthlyRevenue)
|
|
|
|
return &stats, nil
|
|
}
|
|
|
|
// GetExpiringLicenses 即将到期 License 列表 // Logan.Li
|
|
func (s *AnalyticsService) GetExpiringLicenses(days int) ([]dto.ExpiringLicenseItem, error) {
|
|
if days <= 0 {
|
|
days = 30
|
|
}
|
|
now := time.Now()
|
|
deadline := now.AddDate(0, 0, days)
|
|
|
|
var licenses []model.License
|
|
s.db.Preload("Customer").Preload("Product").
|
|
Where("status = ? AND end_date > ? AND end_date < ?", model.LicenseStatusActive, now, deadline).
|
|
Order("end_date ASC").Limit(20).Find(&licenses)
|
|
|
|
results := make([]dto.ExpiringLicenseItem, 0, len(licenses))
|
|
for _, l := range licenses {
|
|
results = append(results, dto.ExpiringLicenseItem{
|
|
ID: l.ID,
|
|
LicenseKey: l.LicenseKey,
|
|
CustomerName: l.Customer.Name,
|
|
ProductName: l.Product.Name,
|
|
EndDate: l.EndDate.Format("2006-01-02"),
|
|
DaysLeft: int(l.EndDate.Sub(now).Hours() / 24),
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// GetRecentActivities 最近操作活动 // Logan.Li
|
|
func (s *AnalyticsService) GetRecentActivities(limit int) ([]dto.RecentActivityItem, error) {
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
var logs []model.AuditLog
|
|
s.db.Order("created_at DESC").Limit(limit).Find(&logs)
|
|
|
|
results := make([]dto.RecentActivityItem, 0, len(logs))
|
|
for _, l := range logs {
|
|
results = append(results, dto.RecentActivityItem{
|
|
ID: l.ID,
|
|
Username: l.Username,
|
|
Action: l.Action,
|
|
TargetType: l.TargetType,
|
|
TargetName: l.TargetName,
|
|
Detail: l.Detail,
|
|
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// GetOnlineStats 在线统计 // Logan.Li
|
|
func (s *AnalyticsService) GetOnlineStats() (map[string]interface{}, error) {
|
|
now := time.Now()
|
|
fiveMinAgo := now.Add(-5 * time.Minute)
|
|
|
|
var onlineDevices int64
|
|
var totalActivations int64
|
|
s.db.Model(&model.Activation{}).Where("is_online = ? AND last_seen_at >= ?", true, fiveMinAgo).Count(&onlineDevices)
|
|
s.db.Model(&model.Activation{}).Where("status = ?", "active").Count(&totalActivations)
|
|
|
|
return map[string]interface{}{
|
|
"online_devices": onlineDevices,
|
|
"total_activations": totalActivations,
|
|
"offline_devices": totalActivations - onlineDevices,
|
|
}, nil
|
|
}
|
|
|
|
// GetRevenueStats 收入统计(近 6 个月) // Logan.Li
|
|
func (s *AnalyticsService) GetRevenueStats() ([]dto.RevenuePoint, error) {
|
|
now := time.Now()
|
|
results := make([]dto.RevenuePoint, 0, 6)
|
|
for i := 5; i >= 0; i-- {
|
|
monthStart := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, now.Location())
|
|
nextMonth := monthStart.AddDate(0, 1, 0)
|
|
var amount float64
|
|
s.db.Model(&model.Order{}).
|
|
Where("status = ? AND created_at >= ? AND created_at < ?", "paid", monthStart, nextMonth).
|
|
Select("COALESCE(SUM(amount), 0)").
|
|
Scan(&amount)
|
|
results = append(results, dto.RevenuePoint{
|
|
Period: monthStart.Format("2006-01"),
|
|
Amount: amount,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// GetLicenseGrowth License 增长趋势(近 6 个月) // Logan.Li
|
|
func (s *AnalyticsService) GetLicenseGrowth() ([]dto.LicenseGrowthPoint, error) {
|
|
now := time.Now()
|
|
results := make([]dto.LicenseGrowthPoint, 0, 6)
|
|
for i := 5; i >= 0; i-- {
|
|
monthEnd := time.Date(now.Year(), now.Month()-time.Month(i)+1, 0, 23, 59, 59, 0, now.Location())
|
|
var count int64
|
|
s.db.Model(&model.License{}).Where("created_at <= ?", monthEnd).Count(&count)
|
|
results = append(results, dto.LicenseGrowthPoint{
|
|
Date: monthEnd.Format("2006-01"),
|
|
Count: count,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// GetProductDistribution 产品分布 // Logan.Li
|
|
func (s *AnalyticsService) GetProductDistribution() ([]dto.ProductDistribution, error) {
|
|
var results []dto.ProductDistribution
|
|
s.db.Model(&model.License{}).
|
|
Select("products.name as product_name, count(licenses.id) as count").
|
|
Joins("LEFT JOIN products ON products.id = licenses.product_id").
|
|
Group("licenses.product_id").
|
|
Scan(&results)
|
|
return results, nil
|
|
}
|