up
This commit is contained in:
@@ -1,45 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/service"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DashboardHandler struct {
|
||||
db *gorm.DB
|
||||
dashboardService *service.DashboardService
|
||||
}
|
||||
|
||||
func NewDashboardHandler(db *gorm.DB) *DashboardHandler {
|
||||
return &DashboardHandler{db: db}
|
||||
func NewDashboardHandler(dashboardService *service.DashboardService) *DashboardHandler {
|
||||
return &DashboardHandler{dashboardService: dashboardService}
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
var stats struct {
|
||||
TotalDevices int64 `json:"total_devices"`
|
||||
TotalLicenses int64 `json:"total_licenses"`
|
||||
TodayNew int64 `json:"today_new"`
|
||||
OnlineDevices int64 `json:"online_devices"`
|
||||
// GetDashboardStats 获取仪表盘统计数据
|
||||
func (h *DashboardHandler) GetDashboardStats(c *gin.Context) {
|
||||
stats, err := h.dashboardService.GetDashboardStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取设备总数
|
||||
h.db.Model(&model.Device{}).Count(&stats.TotalDevices)
|
||||
|
||||
// 获取授权码总数
|
||||
h.db.Model(&model.LicenseCode{}).Count(&stats.TotalLicenses)
|
||||
|
||||
// 获取今日新增设备数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
h.db.Model(&model.Device{}).Where("DATE(created_at) = ?", today).Count(&stats.TodayNew)
|
||||
|
||||
// 获取在线设备数(最近30分钟内有活动的设备)
|
||||
thirtyMinutesAgo := time.Now().Add(-30 * time.Minute)
|
||||
h.db.Model(&model.Device{}).
|
||||
Where("last_active_at > ?", thirtyMinutesAgo).
|
||||
Count(&stats.OnlineDevices)
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": stats,
|
||||
})
|
||||
}
|
||||
|
@@ -3,22 +3,25 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"strconv"
|
||||
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/service"
|
||||
"licserver/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DeviceHandler struct {
|
||||
deviceService *service.DeviceService
|
||||
config *utils.Config
|
||||
}
|
||||
|
||||
func NewDeviceHandler(deviceService *service.DeviceService) *DeviceHandler {
|
||||
func NewDeviceHandler(deviceService *service.DeviceService, config *utils.Config) *DeviceHandler {
|
||||
|
||||
return &DeviceHandler{deviceService: deviceService}
|
||||
return &DeviceHandler{deviceService: deviceService, config: config}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +53,7 @@ func (h *DeviceHandler) GetDevices(c *gin.Context) {
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
params := &service.DeviceQueryParams{
|
||||
|
||||
@@ -89,19 +92,37 @@ func (h *DeviceHandler) GetDevices(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) UpdateStartCount(c *gin.Context) {
|
||||
|
||||
uid := c.Param("uid")
|
||||
|
||||
if err := h.deviceService.UpdateStartCount(uid); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
// 更新启动次数
|
||||
err := h.deviceService.UpdateStartCount(uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "启动次数更新成功"})
|
||||
// 获取更新后的设备信息
|
||||
device, err := h.deviceService.GetDevice(uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "启动次数更新成功",
|
||||
"data": gin.H{
|
||||
"start_count": device.StartCount,
|
||||
"status": device.Status,
|
||||
"last_active_at": device.LastActiveAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) UpdateDevice(c *gin.Context) {
|
||||
@@ -207,19 +228,50 @@ func (h *DeviceHandler) RegisterDevice(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) ValidateDevice(c *gin.Context) {
|
||||
|
||||
uid := c.Param("uid")
|
||||
|
||||
if err := h.deviceService.ValidateDevice(uid); err != nil {
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
|
||||
device, err := h.deviceService.GetDevice(uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "设备验证通过"})
|
||||
// 验证设备状态
|
||||
if err := h.deviceService.ValidateDevice(uid); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 准备加密响应数据
|
||||
response := utils.DeviceValidateResponse{
|
||||
Status: device.Status,
|
||||
LicenseType: device.LicenseType,
|
||||
ExpireTime: device.ExpireTime.Format(time.RFC3339),
|
||||
StartCount: device.StartCount,
|
||||
MaxUses: device.MaxUses,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// 加密响应
|
||||
encrypted, err := utils.EncryptResponse(response, []byte(h.config.Security.EncryptKey))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": -1,
|
||||
"error": "加密响应失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": encrypted,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) BindLicense(c *gin.Context) {
|
||||
@@ -360,14 +412,17 @@ func (h *DeviceHandler) UpdateDeviceModel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var model model.DeviceModel
|
||||
if err := c.ShouldBindJSON(&model); err != nil {
|
||||
var input model.DeviceModel
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deviceService.UpdateDeviceModel(uint(id), &model); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
if err := h.deviceService.UpdateDeviceModel(uint(id), &input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -455,17 +510,3 @@ func (h *DeviceHandler) GetDeviceLogs(c *gin.Context) {
|
||||
"data": logs,
|
||||
})
|
||||
}
|
||||
|
||||
// GetDashboardStats 获取仪表盘统计数据
|
||||
func (h *DeviceHandler) GetDashboardStats(c *gin.Context) {
|
||||
stats, err := h.deviceService.GetDashboardStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": stats,
|
||||
})
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ func SetupRouter(
|
||||
siteHandler *SiteHandler,
|
||||
tokenHandler *TokenHandler,
|
||||
licenseHandler *LicenseHandler,
|
||||
dashboardHandler *DashboardHandler,
|
||||
) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
@@ -63,6 +64,8 @@ func SetupRouter(
|
||||
api.POST("/captcha/reset-password", userHandler.SendResetPasswordCaptcha)
|
||||
api.POST("/validate-token", tokenHandler.ValidateToken)
|
||||
api.POST("/devices/register", deviceHandler.RegisterDevice)
|
||||
api.POST("/devices/:uid/start", deviceHandler.UpdateStartCount)
|
||||
api.GET("/devices/:uid/validate", deviceHandler.ValidateDevice)
|
||||
|
||||
// 需要认证的API
|
||||
authorized := api.Group("")
|
||||
@@ -76,7 +79,6 @@ func SetupRouter(
|
||||
authorized.POST("/devices/models/batch", middleware.AdminRequired(), deviceHandler.BatchDeleteDeviceModels)
|
||||
|
||||
// 设备管理
|
||||
authorized.POST("/devices/register", deviceHandler.RegisterDevice)
|
||||
authorized.GET("/devices/registered", deviceHandler.GetRegisteredDevices)
|
||||
authorized.POST("/devices/:uid/license", middleware.AdminRequired(), deviceHandler.BindLicense)
|
||||
authorized.DELETE("/devices/:uid/license", middleware.AdminRequired(), deviceHandler.UnbindLicense)
|
||||
@@ -113,7 +115,7 @@ func SetupRouter(
|
||||
authorized.POST("/licenses/use", licenseHandler.UseLicense)
|
||||
|
||||
// 仪表盘统计
|
||||
authorized.GET("/dashboard/stats", deviceHandler.GetDashboardStats)
|
||||
authorized.GET("/dashboard/stats", dashboardHandler.GetDashboardStats)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -21,44 +21,45 @@ func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
|
||||
var input struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
|
||||
Password string `json:"password" binding:"required"`
|
||||
|
||||
Captcha string `json:"captcha" binding:"required"`
|
||||
|
||||
CaptchaId string `json:"captchaId" binding:"required"`
|
||||
}
|
||||
|
||||
var input service.LoginInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if !h.userService.GetCaptchaService().VerifyImageCaptcha(input.CaptchaId, input.Captcha) {
|
||||
if !h.userService.VerifyCaptcha(input.CaptchaId, input.Captcha) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.userService.Login(input.Username, input.Password)
|
||||
|
||||
// 验证用户名密码
|
||||
user, err := h.userService.ValidateUser(input.Username, input.Password)
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// 设置 cookie
|
||||
// c.SetCookie("token", token, 86400, "/", "", false, true) // 24小时过期,httpOnly=true
|
||||
// 生成 JWT token
|
||||
token, err := h.userService.GenerateToken(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||
// 返回token和用户信息
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": gin.H{
|
||||
"token": token,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -146,7 +147,7 @@ func (h *UserHandler) ResetPassword(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "重置密码邮件已发送"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "重置密邮件已发送"})
|
||||
|
||||
}
|
||||
|
||||
|
@@ -12,11 +12,11 @@ func JWTAuth(config *utils.JWTConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var token string
|
||||
|
||||
// 1. 首先从 cookie 中获取 token
|
||||
tokenCookie, err := c.Cookie("token")
|
||||
if err == nil {
|
||||
token = tokenCookie
|
||||
}
|
||||
// // 1. 首先从 cookie 中获取 token
|
||||
// tokenCookie, err := c.Cookie("token")
|
||||
// if err == nil {
|
||||
// token = tokenCookie
|
||||
// }
|
||||
|
||||
// 2. 如果 cookie 中没有,则从 header 中获取
|
||||
if token == "" {
|
||||
@@ -44,8 +44,6 @@ func JWTAuth(config *utils.JWTConfig) gin.HandlerFunc {
|
||||
// 验证 token
|
||||
claims, err := utils.ParseToken(token, config)
|
||||
if err != nil {
|
||||
// 如果 token 无效,清除 cookie
|
||||
// c.SetCookie("token", "", -1, "/", "", true, true)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -56,12 +54,6 @@ func JWTAuth(config *utils.JWTConfig) gin.HandlerFunc {
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
// 如果是从 header 或 query 参数获取的 token,设置到 cookie 中
|
||||
if tokenCookie == "" {
|
||||
// 设置 cookie,过期时间与 token 一致
|
||||
// c.SetCookie("token", token, int(claims.ExpiresAt.Unix()-claims.IssuedAt.Unix()), "/", "", false, true)
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
1
internal/middleware/jwt.go
Normal file
1
internal/middleware/jwt.go
Normal file
@@ -0,0 +1 @@
|
||||
|
1
internal/model/device.go
Normal file
1
internal/model/device.go
Normal file
@@ -0,0 +1 @@
|
||||
|
@@ -7,7 +7,17 @@ import (
|
||||
type DeviceLog struct {
|
||||
gorm.Model
|
||||
DeviceUID string `gorm:"index" json:"device_uid"` // 设备UID
|
||||
Action string `json:"action"` // 操作类型
|
||||
Action string `json:"action"` // 操作类型:register/start/verify/bind/unbind
|
||||
Message string `json:"message"` // 详细信息
|
||||
Status string `json:"status"` // 状态:success/failed
|
||||
IP string `json:"ip"` // 操作IP
|
||||
}
|
||||
|
||||
// 定义日志操作类型常量
|
||||
const (
|
||||
LogActionRegister = "register"
|
||||
LogActionStart = "start"
|
||||
LogActionVerify = "verify"
|
||||
LogActionBind = "bind"
|
||||
LogActionUnbind = "unbind"
|
||||
)
|
||||
|
94
internal/service/dashboard.go
Normal file
94
internal/service/dashboard.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DashboardService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDashboardService(db *gorm.DB) *DashboardService {
|
||||
return &DashboardService{db: db}
|
||||
}
|
||||
|
||||
type DashboardStats struct {
|
||||
TotalDevices int64 `json:"total_devices"` // 设备总数
|
||||
TotalLicenses int64 `json:"total_licenses"` // 授权码总数
|
||||
TodayNew int64 `json:"today_new"` // 今日新增
|
||||
OnlineDevices int64 `json:"online_devices"` // 在线设备
|
||||
ActiveDevices int64 `json:"active_devices"` // 激活设备
|
||||
ExpiredDevices int64 `json:"expired_devices"` // 过期设备
|
||||
DeviceTypes []DeviceTypeStats `json:"device_types"` // 设备类型分布
|
||||
TrendData []DailyRegistration `json:"trend_data"` // 注册趋势
|
||||
LicenseStats LicenseStatistics `json:"license_stats"` // 授权码统计
|
||||
}
|
||||
|
||||
type DeviceTypeStats struct {
|
||||
Type string `json:"type"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type DailyRegistration struct {
|
||||
Date string `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type LicenseStatistics struct {
|
||||
Unused int64 `json:"unused"` // 未使用
|
||||
Used int64 `json:"used"` // 已使用
|
||||
Expired int64 `json:"expired"` // 已过期
|
||||
Revoked int64 `json:"revoked"` // 已撤销
|
||||
}
|
||||
|
||||
func (s *DashboardService) GetDashboardStats() (*DashboardStats, error) {
|
||||
stats := &DashboardStats{}
|
||||
|
||||
// 获取设备总数
|
||||
s.db.Model(&model.Device{}).Count(&stats.TotalDevices)
|
||||
|
||||
// 获取授权码总数
|
||||
s.db.Model(&model.LicenseCode{}).Count(&stats.TotalLicenses)
|
||||
|
||||
// 获取今日新增设备数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
s.db.Model(&model.Device{}).Where("DATE(register_time) = ?", today).Count(&stats.TodayNew)
|
||||
|
||||
// 获取在线设备数(最近30分钟内有活动的设备)
|
||||
thirtyMinutesAgo := time.Now().Add(-30 * time.Minute)
|
||||
s.db.Model(&model.Device{}).Where("last_active_at > ?", thirtyMinutesAgo).Count(&stats.OnlineDevices)
|
||||
|
||||
// 获取激活和过期设备数
|
||||
s.db.Model(&model.Device{}).Where("status = ?", "active").Count(&stats.ActiveDevices)
|
||||
s.db.Model(&model.Device{}).Where("status = ?", "expired").Count(&stats.ExpiredDevices)
|
||||
|
||||
// 获取设备类型分布
|
||||
var deviceTypes []DeviceTypeStats
|
||||
s.db.Model(&model.Device{}).
|
||||
Select("device_type as type, count(*) as count").
|
||||
Group("device_type").
|
||||
Scan(&deviceTypes)
|
||||
stats.DeviceTypes = deviceTypes
|
||||
|
||||
// 获取最近7天的注册趋势
|
||||
var trendData []DailyRegistration
|
||||
sevenDaysAgo := time.Now().AddDate(0, 0, -7)
|
||||
s.db.Model(&model.Device{}).
|
||||
Select("DATE(register_time) as date, count(*) as count").
|
||||
Where("register_time >= ?", sevenDaysAgo).
|
||||
Group("DATE(register_time)").
|
||||
Order("date ASC").
|
||||
Scan(&trendData)
|
||||
stats.TrendData = trendData
|
||||
|
||||
// 获取授权码统计
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "unused").Count(&stats.LicenseStats.Unused)
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "used").Count(&stats.LicenseStats.Used)
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "expired").Count(&stats.LicenseStats.Expired)
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "revoked").Count(&stats.LicenseStats.Revoked)
|
||||
|
||||
return stats, nil
|
||||
}
|
@@ -31,7 +31,7 @@ func NewDeviceService(db *gorm.DB, licenseService *LicenseService) *DeviceServic
|
||||
type DeviceRegisterInput struct {
|
||||
UID string `json:"uid" binding:"required"`
|
||||
DeviceModel string `json:"device_model" binding:"required"`
|
||||
LicenseCode string `json:"license_code"`
|
||||
LicenseCode string `json:"license_code,omitempty"`
|
||||
}
|
||||
|
||||
func (s *DeviceService) RegisterDevice(input *DeviceRegisterInput, ip string) error {
|
||||
@@ -104,15 +104,12 @@ func (s *DeviceService) RegisterDevice(input *DeviceRegisterInput, ip string) er
|
||||
}
|
||||
|
||||
// 记录设备日志
|
||||
logMsg := "设备注册成功"
|
||||
if device.LicenseCode != "" {
|
||||
logMsg += fmt.Sprintf(",使用授权码: %s", device.LicenseCode)
|
||||
}
|
||||
log := model.DeviceLog{
|
||||
DeviceUID: input.UID,
|
||||
Action: "register",
|
||||
Message: logMsg,
|
||||
Message: fmt.Sprintf("设备注册成功,状态:%s", device.Status),
|
||||
Status: "success",
|
||||
IP: ip,
|
||||
}
|
||||
if err := tx.Create(&log).Error; err != nil {
|
||||
return err
|
||||
@@ -337,7 +334,7 @@ type DeviceCreateInput struct {
|
||||
}
|
||||
|
||||
func (s *DeviceService) CreateDevice(input *DeviceCreateInput) error {
|
||||
// 检查设备UID是否已存在
|
||||
// 检查设备UID<EFBFBD><EFBFBD><EFBFBD>否已存在
|
||||
var count int64
|
||||
if err := s.db.Model(&model.Device{}).Where("uid = ?", input.UID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
@@ -408,15 +405,49 @@ func (s *DeviceService) CreateDeviceModel(model_ *model.DeviceModel) error {
|
||||
}
|
||||
|
||||
// UpdateDeviceModel 更新设备型号
|
||||
func (s *DeviceService) UpdateDeviceModel(id uint, model_ *model.DeviceModel) error {
|
||||
// 检查型号名称是否被其他型号使用
|
||||
var count int64
|
||||
s.db.Model(&model.DeviceModel{}).Where("model_name = ? AND id != ?", model_.ModelName, id).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("设备型号已存在")
|
||||
func (s *DeviceService) UpdateDeviceModel(id uint, input *model.DeviceModel) error {
|
||||
// 检查设备型号是否存在
|
||||
var existingModel model.DeviceModel
|
||||
if err := s.db.First(&existingModel, id).Error; err != nil {
|
||||
return errors.New("设备型号不存在")
|
||||
}
|
||||
|
||||
return s.db.Model(&model.DeviceModel{}).Where("id = ?", id).Updates(model_).Error
|
||||
// 如果型号名称发生变更,需要检查是否存在冲突
|
||||
if input.ModelName != existingModel.ModelName {
|
||||
// 检查新的型号名称是否与其他型号冲突(排除自身)
|
||||
var count int64
|
||||
s.db.Model(&model.DeviceModel{}).
|
||||
Where("model_name = ? AND id != ?", input.ModelName, id).
|
||||
Count(&count)
|
||||
if count > 0 {
|
||||
|
||||
return fmt.Errorf("设备型号[%d]:%s已存在", id, input.ModelName)
|
||||
}
|
||||
|
||||
// 检查是否有设备正在使用此型号
|
||||
var deviceCount int64
|
||||
s.db.Model(&model.Device{}).
|
||||
Where("device_model = ?", existingModel.ModelName).
|
||||
Count(&deviceCount)
|
||||
if deviceCount > 0 {
|
||||
return errors.New("该型号下存在设备,无法修改型号名称")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新设备型号信息
|
||||
updates := map[string]interface{}{
|
||||
"device_type": input.DeviceType,
|
||||
"company": input.Company,
|
||||
"status": input.Status,
|
||||
"remark": input.Remark,
|
||||
}
|
||||
|
||||
// 只有当型号名称变更时才更新
|
||||
if input.ModelName != existingModel.ModelName {
|
||||
updates["model_name"] = input.ModelName
|
||||
}
|
||||
|
||||
return s.db.Model(&existingModel).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteDeviceModel 删除设备型号
|
||||
@@ -448,7 +479,7 @@ func (s *DeviceService) GetDeviceModels(modelName, deviceType, company string, p
|
||||
query = query.Where("company LIKE ?", "%"+company+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
// 取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
@@ -683,39 +714,10 @@ func (s *DeviceService) GetDeviceLogs(uid string, page, pageSize int) ([]model.D
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
// DashboardStats 仪表盘统计数据
|
||||
type DashboardStats struct {
|
||||
TotalDevices int64 `json:"total_devices"` // 设备总数
|
||||
TotalLicenses int64 `json:"total_licenses"` // 授权码总数
|
||||
TodayNew int64 `json:"today_new"` // 今日新增
|
||||
OnlineDevices int64 `json:"online_devices"` // 在线设备
|
||||
ActiveDevices int64 `json:"active_devices"` // 激活设备
|
||||
ExpiredDevices int64 `json:"expired_devices"` // 过期设备
|
||||
}
|
||||
|
||||
// GetDashboardStats 获取仪表盘统计数据
|
||||
func (s *DeviceService) GetDashboardStats() (*DashboardStats, error) {
|
||||
var stats DashboardStats
|
||||
|
||||
// 获取设备总数
|
||||
s.db.Model(&model.Device{}).Count(&stats.TotalDevices)
|
||||
|
||||
// 获取授权码总数
|
||||
s.db.Model(&model.LicenseCode{}).Count(&stats.TotalLicenses)
|
||||
|
||||
// 获取今日新增设备数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
s.db.Model(&model.Device{}).Where("DATE(register_time) = ?", today).Count(&stats.TodayNew)
|
||||
|
||||
// 获取在线设备数(最近30分钟内有活动的设备)
|
||||
thirtyMinutesAgo := time.Now().Add(-30 * time.Minute)
|
||||
s.db.Model(&model.Device{}).Where("last_active_at > ?", thirtyMinutesAgo).Count(&stats.OnlineDevices)
|
||||
|
||||
// 获取激活设备数
|
||||
s.db.Model(&model.Device{}).Where("status = ?", "active").Count(&stats.ActiveDevices)
|
||||
|
||||
// 获取过期设备数
|
||||
s.db.Model(&model.Device{}).Where("status = ?", "expired").Count(&stats.ExpiredDevices)
|
||||
|
||||
return &stats, nil
|
||||
func (s *DeviceService) GetDevice(uid string) (*model.Device, error) {
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", uid).First(&device).Error; err != nil {
|
||||
return nil, errors.New("设备不存在")
|
||||
}
|
||||
return &device, nil
|
||||
}
|
||||
|
@@ -14,6 +14,10 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
@@ -29,6 +33,14 @@ type UserProfile struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// LoginInput 登录输入
|
||||
type LoginInput struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
CaptchaId string `json:"captchaId" binding:"required"`
|
||||
Captcha string `json:"captcha" binding:"required"`
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB, config *utils.Config) *UserService {
|
||||
return &UserService{
|
||||
db: db,
|
||||
@@ -359,3 +371,36 @@ func (s *UserService) DeleteUser(id uint) error {
|
||||
|
||||
return s.db.Delete(&model.User{}, id).Error
|
||||
}
|
||||
|
||||
// ValidateUser 验证用户名密码
|
||||
func (s *UserService) ValidateUser(username, password string) (*model.User, error) {
|
||||
var user model.User
|
||||
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
|
||||
// 使用 bcrypt 比较密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return nil, errors.New("密码错误")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func (s *UserService) GenerateToken(user *model.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(), // 24小时过期
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
// VerifyCaptcha 验证验证码
|
||||
func (s *UserService) VerifyCaptcha(captchaId, captcha string) bool {
|
||||
return base64Captcha.DefaultMemStore.Verify(captchaId, captcha, true)
|
||||
}
|
||||
|
@@ -7,21 +7,19 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
JWT JWTConfig
|
||||
Email EmailConfig
|
||||
Upload UploadConfig
|
||||
Site SiteConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Mode string
|
||||
Server struct {
|
||||
Port string `yaml:"port"`
|
||||
Mode string `yaml:"mode"`
|
||||
} `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
JWT JWTConfig `yaml:"jwt"`
|
||||
Email EmailConfig `yaml:"email"`
|
||||
Upload UploadConfig `yaml:"upload"`
|
||||
Site SiteConfig `yaml:"site"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string
|
||||
Path string
|
||||
}
|
||||
|
||||
@@ -42,13 +40,17 @@ type UploadConfig struct {
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Title string `mapstructure:"title"`
|
||||
Description string `mapstructure:"description"`
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
ICP string `mapstructure:"icp"`
|
||||
Copyright string `mapstructure:"copyright"`
|
||||
Logo string `mapstructure:"logo"`
|
||||
Favicon string `mapstructure:"favicon"`
|
||||
Title string
|
||||
Description string
|
||||
BaseURL string
|
||||
Logo string
|
||||
Favicon string
|
||||
Copyright string
|
||||
ICP string
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
EncryptKey string
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
|
79
internal/utils/crypto.go
Normal file
79
internal/utils/crypto.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type DeviceValidateResponse struct {
|
||||
Status string `json:"status"` // 设备状态
|
||||
LicenseType string `json:"license_type"` // 授权类型
|
||||
ExpireTime string `json:"expire_time"` // 过期时间
|
||||
StartCount int `json:"start_count"` // 启动次数
|
||||
MaxUses int `json:"max_uses"` // 最大使用次数
|
||||
Timestamp int64 `json:"timestamp"` // 时间戳
|
||||
Signature string `json:"signature"` // 签名
|
||||
}
|
||||
|
||||
// EncryptResponse 加密设备验证响应
|
||||
func EncryptResponse(data DeviceValidateResponse, key []byte) (string, error) {
|
||||
// 序列化数据
|
||||
plaintext, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 创建随机IV
|
||||
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 加密
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
||||
|
||||
// 返回base64编码的密文
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// DecryptResponse 解密设备验证响应
|
||||
func DecryptResponse(encrypted string, key []byte) (*DeviceValidateResponse, error) {
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, errors.New("密文太短")
|
||||
}
|
||||
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
|
||||
var response DeviceValidateResponse
|
||||
if err := json.Unmarshal(ciphertext, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
@@ -28,6 +28,8 @@ func InitDB(config *DatabaseConfig) (*gorm.DB, error) {
|
||||
|
||||
&model.DeviceModel{},
|
||||
|
||||
&model.DeviceLog{},
|
||||
|
||||
&model.PasswordResetToken{},
|
||||
|
||||
&model.Captcha{},
|
||||
|
@@ -36,33 +36,22 @@ func TestDB(t *testing.T) *gorm.DB {
|
||||
// TestConfig 创建测试配置
|
||||
func TestConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Server: struct {
|
||||
Port string `yaml:"port"`
|
||||
Mode string `yaml:"mode"`
|
||||
}{
|
||||
Port: "8080",
|
||||
Mode: "test",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: "sqlite3",
|
||||
Path: ":memory:",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "test-secret",
|
||||
Expire: "24h",
|
||||
},
|
||||
Email: EmailConfig{
|
||||
Host: "smtp.example.com",
|
||||
Port: 587,
|
||||
Username: "test@example.com",
|
||||
Password: "test-password",
|
||||
},
|
||||
Upload: UploadConfig{
|
||||
Path: "./test-uploads",
|
||||
},
|
||||
Site: SiteConfig{
|
||||
Title: "Test Site",
|
||||
Description: "Test Description",
|
||||
BaseURL: "http://localhost:8080",
|
||||
ICP: "Test ICP",
|
||||
Copyright: "Test Copyright",
|
||||
Security: SecurityConfig{
|
||||
EncryptKey: "test-32-byte-encrypt-key-here1234567",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user