feat: 完成 opsx-licence 许可证管理平台全栈实现
基于 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
This commit is contained in:
commit
4ae318335c
88 files changed
+10562
No files matched your search
@@ -0,0 +1,167 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user