|
@@ -1,11 +1,14 @@
|
|
|
package handlers
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
|
|
+ "encoding/csv"
|
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
"license-admin/models"
|
|
"license-admin/models"
|
|
|
"math/rand"
|
|
"math/rand"
|
|
|
"net/http"
|
|
"net/http"
|
|
|
"strconv"
|
|
"strconv"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "time"
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gin-gonic/gin"
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm"
|
|
@@ -13,30 +16,30 @@ import (
|
|
|
|
|
|
|
|
// CreateLicenseRequest 创建 License 请求结构
|
|
// CreateLicenseRequest 创建 License 请求结构
|
|
|
type CreateLicenseRequest struct {
|
|
type CreateLicenseRequest struct {
|
|
|
- Key string `json:"key" binding:"required"` // 激活码(必填)
|
|
|
|
|
- MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2)
|
|
|
|
|
- BoundDevices string `json:"bound_devices,omitempty"` // 初始绑定设备列表(可选,默认为空数组)
|
|
|
|
|
|
|
+ Key string `json:"key" binding:"required"` // 激活码(必填)
|
|
|
|
|
+ MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2)
|
|
|
|
|
+ BoundDevices string `json:"bound_devices,omitempty"` // 初始绑定设备列表(可选,默认为空数组)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// UpdateLicenseRequest 更新 License 请求结构
|
|
// UpdateLicenseRequest 更新 License 请求结构
|
|
|
type UpdateLicenseRequest struct {
|
|
type UpdateLicenseRequest struct {
|
|
|
- Key string `json:"key,omitempty"` // 激活码(可选)
|
|
|
|
|
- MaxDevices *int `json:"max_devices,omitempty"` // 最大设备数(可选,使用指针以区分零值)
|
|
|
|
|
|
|
+ Key string `json:"key,omitempty"` // 激活码(可选)
|
|
|
|
|
+ MaxDevices *int `json:"max_devices,omitempty"` // 最大设备数(可选,使用指针以区分零值)
|
|
|
BoundDevices string `json:"bound_devices,omitempty"` // 绑定设备列表(可选)
|
|
BoundDevices string `json:"bound_devices,omitempty"` // 绑定设备列表(可选)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// LicenseResponse License 响应结构
|
|
// LicenseResponse License 响应结构
|
|
|
type LicenseResponse struct {
|
|
type LicenseResponse struct {
|
|
|
- Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
- Msg string `json:"msg"` // 响应消息
|
|
|
|
|
|
|
+ Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
+ Msg string `json:"msg"` // 响应消息
|
|
|
Data *models.License `json:"data,omitempty"` // License 数据
|
|
Data *models.License `json:"data,omitempty"` // License 数据
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// LicenseListResponse License 列表响应结构
|
|
// LicenseListResponse License 列表响应结构
|
|
|
type LicenseListResponse struct {
|
|
type LicenseListResponse struct {
|
|
|
- Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
- Msg string `json:"msg"` // 响应消息
|
|
|
|
|
- Data []models.License `json:"data"` // License 列表
|
|
|
|
|
|
|
+ Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
+ Msg string `json:"msg"` // 响应消息
|
|
|
|
|
+ Data []models.License `json:"data"` // License 列表
|
|
|
Total int64 `json:"total"` // 总数
|
|
Total int64 `json:"total"` // 总数
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -53,10 +56,17 @@ func CreateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 检查 Key 是否已存在
|
|
|
|
|
- var existingLicense models.License
|
|
|
|
|
- result := db.Where("license_key = ?", req.Key).First(&existingLicense)
|
|
|
|
|
- if result.Error == nil {
|
|
|
|
|
|
|
+ // 检查 Key 是否已存在(使用 Count 更高效,不会产生 record not found 日志)
|
|
|
|
|
+ var count int64
|
|
|
|
|
+ if err := db.Model(&models.License{}).Where("license_key = ?", req.Key).Count(&count).Error; err != nil {
|
|
|
|
|
+ // 数据库查询错误
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "数据库查询错误: " + err.Error(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if count > 0 {
|
|
|
// Key 已存在
|
|
// Key 已存在
|
|
|
c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
Code: 400,
|
|
Code: 400,
|
|
@@ -64,14 +74,6 @@ func CreateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if result.Error != gorm.ErrRecordNotFound {
|
|
|
|
|
- // 数据库查询错误
|
|
|
|
|
- c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
- Code: 500,
|
|
|
|
|
- Msg: "数据库查询错误: " + result.Error.Error(),
|
|
|
|
|
- })
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
// 设置默认值
|
|
// 设置默认值
|
|
|
if req.MaxDevices <= 0 {
|
|
if req.MaxDevices <= 0 {
|
|
@@ -105,12 +107,14 @@ func CreateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// GetLicenseList 获取 License 列表
|
|
// GetLicenseList 获取 License 列表
|
|
|
-// GET /api/licenses?page=1&page_size=10
|
|
|
|
|
|
|
+// GET /api/licenses?page=1&page_size=10&search=keyword
|
|
|
func GetLicenseList(db *gorm.DB) gin.HandlerFunc {
|
|
func GetLicenseList(db *gorm.DB) gin.HandlerFunc {
|
|
|
return func(c *gin.Context) {
|
|
return func(c *gin.Context) {
|
|
|
// 解析分页参数
|
|
// 解析分页参数
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
|
|
|
|
+ search := c.Query("search") // 搜索关键词
|
|
|
|
|
+ status := c.Query("status") // 状态筛选:activated(已激活)、unactivated(未激活)
|
|
|
|
|
|
|
|
if page < 1 {
|
|
if page < 1 {
|
|
|
page = 1
|
|
page = 1
|
|
@@ -122,33 +126,69 @@ func GetLicenseList(db *gorm.DB) gin.HandlerFunc {
|
|
|
var licenses []models.License
|
|
var licenses []models.License
|
|
|
var total int64
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
+ // 构建查询
|
|
|
|
|
+ query := db.Model(&models.License{})
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有搜索关键词,添加搜索条件(按 license_key 模糊匹配)
|
|
|
|
|
+ if search != "" {
|
|
|
|
|
+ query = query.Where("license_key LIKE ?", "%"+search+"%")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 状态筛选
|
|
|
|
|
+ if status == "activated" {
|
|
|
|
|
+ // 已激活:有绑定设备
|
|
|
|
|
+ query = query.Where("JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0")
|
|
|
|
|
+ } else if status == "unactivated" {
|
|
|
|
|
+ // 未激活:没有绑定设备
|
|
|
|
|
+ query = query.Where("JSON_LENGTH(COALESCE(bound_devices, '[]')) = 0 OR bound_devices IS NULL OR bound_devices = ''")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 获取总数
|
|
// 获取总数
|
|
|
- if err := db.Model(&models.License{}).Count(&total).Error; err != nil {
|
|
|
|
|
|
|
+ if err := query.Count(&total).Error; err != nil {
|
|
|
c.JSON(http.StatusInternalServerError, LicenseListResponse{
|
|
c.JSON(http.StatusInternalServerError, LicenseListResponse{
|
|
|
- Code: 500,
|
|
|
|
|
- Msg: "查询总数失败: " + err.Error(),
|
|
|
|
|
- Data: []models.License{},
|
|
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "查询总数失败: " + err.Error(),
|
|
|
|
|
+ Data: []models.License{},
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 使用 SQL 排序:
|
|
|
|
|
+ // 1. 有绑定设备的优先(bound_devices 不为空且不是 '[]')
|
|
|
|
|
+ // 2. 然后按心跳时间最新的排序(从 device_heartbeats JSON 中提取最新时间)
|
|
|
|
|
+ //
|
|
|
|
|
+ // 使用 MySQL JSON 函数来提取最新的心跳时间:
|
|
|
|
|
+ // - 对于 MySQL 5.7+,我们可以使用 JSON_EXTRACT 和 JSON_KEYS 来遍历
|
|
|
|
|
+ // - 但由于提取所有值并找最大值在 SQL 中比较复杂,我们使用一个更实用的方法:
|
|
|
|
|
+ // 使用 updated_at 字段,因为每次心跳更新(UpdateDeviceHeartbeat)都会更新 updated_at
|
|
|
|
|
+ // 这样 updated_at 可以准确反映最后心跳时间
|
|
|
|
|
+ //
|
|
|
|
|
+ // 排序 SQL:
|
|
|
|
|
+ // 1. 有设备的排在前面:CASE WHEN JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0 THEN 0 ELSE 1 END
|
|
|
|
|
+ // 2. 然后按 updated_at 倒序(最新的心跳会更新 updated_at)
|
|
|
|
|
+ orderBy := `CASE
|
|
|
|
|
+ WHEN JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0 THEN 0
|
|
|
|
|
+ ELSE 1
|
|
|
|
|
+ END ASC,
|
|
|
|
|
+ updated_at DESC`
|
|
|
|
|
+
|
|
|
// 分页查询
|
|
// 分页查询
|
|
|
offset := (page - 1) * pageSize
|
|
offset := (page - 1) * pageSize
|
|
|
- if err := db.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&licenses).Error; err != nil {
|
|
|
|
|
|
|
+ if err := query.Offset(offset).Limit(pageSize).Order(orderBy).Find(&licenses).Error; err != nil {
|
|
|
c.JSON(http.StatusInternalServerError, LicenseListResponse{
|
|
c.JSON(http.StatusInternalServerError, LicenseListResponse{
|
|
|
- Code: 500,
|
|
|
|
|
- Msg: "查询 License 列表失败: " + err.Error(),
|
|
|
|
|
- Data: []models.License{},
|
|
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "查询 License 列表失败: " + err.Error(),
|
|
|
|
|
+ Data: []models.License{},
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, LicenseListResponse{
|
|
c.JSON(http.StatusOK, LicenseListResponse{
|
|
|
- Code: 0,
|
|
|
|
|
- Msg: "success",
|
|
|
|
|
- Data: licenses,
|
|
|
|
|
|
|
+ Code: 0,
|
|
|
|
|
+ Msg: "success",
|
|
|
|
|
+ Data: licenses,
|
|
|
Total: total,
|
|
Total: total,
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -235,20 +275,19 @@ func UpdateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
|
|
|
|
|
// 更新字段
|
|
// 更新字段
|
|
|
if req.Key != "" {
|
|
if req.Key != "" {
|
|
|
- // 检查新 Key 是否与其他 License 冲突
|
|
|
|
|
- var existingLicense models.License
|
|
|
|
|
- checkResult := db.Where("license_key = ? AND id != ?", req.Key, id).First(&existingLicense)
|
|
|
|
|
- if checkResult.Error == nil {
|
|
|
|
|
- c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
|
|
- Code: 400,
|
|
|
|
|
- Msg: "激活码已存在",
|
|
|
|
|
|
|
+ // 检查新 Key 是否与其他 License 冲突(使用 Count 更高效)
|
|
|
|
|
+ var count int64
|
|
|
|
|
+ if err := db.Model(&models.License{}).Where("license_key = ? AND id != ?", req.Key, id).Count(&count).Error; err != nil {
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "检查激活码失败: " + err.Error(),
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if checkResult.Error != gorm.ErrRecordNotFound {
|
|
|
|
|
- c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
- Code: 500,
|
|
|
|
|
- Msg: "检查激活码失败: " + checkResult.Error.Error(),
|
|
|
|
|
|
|
+ if count > 0 {
|
|
|
|
|
+ c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "激活码已存在",
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -350,9 +389,9 @@ type BatchDeleteLicenseRequest struct {
|
|
|
|
|
|
|
|
// BatchDeleteLicenseResponse 批量删除响应结构
|
|
// BatchDeleteLicenseResponse 批量删除响应结构
|
|
|
type BatchDeleteLicenseResponse struct {
|
|
type BatchDeleteLicenseResponse struct {
|
|
|
- Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
- Msg string `json:"msg"` // 响应消息
|
|
|
|
|
- Total int `json:"total"` // 成功删除的数量
|
|
|
|
|
|
|
+ Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
+ Msg string `json:"msg"` // 响应消息
|
|
|
|
|
+ Total int `json:"total"` // 成功删除的数量
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// BatchDeleteLicense 批量删除 License
|
|
// BatchDeleteLicense 批量删除 License
|
|
@@ -362,8 +401,8 @@ func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
var req BatchDeleteLicenseRequest
|
|
var req BatchDeleteLicenseRequest
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
|
|
c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
|
|
|
- Code: 400,
|
|
|
|
|
- Msg: "请求参数错误: " + err.Error(),
|
|
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "请求参数错误: " + err.Error(),
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
@@ -372,8 +411,8 @@ func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
// 验证参数
|
|
// 验证参数
|
|
|
if len(req.IDs) == 0 {
|
|
if len(req.IDs) == 0 {
|
|
|
c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
|
|
c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
|
|
|
- Code: 400,
|
|
|
|
|
- Msg: "请至少选择一个 License",
|
|
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "请至少选择一个 License",
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
@@ -381,8 +420,8 @@ func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
|
|
|
|
|
if len(req.IDs) > 100 {
|
|
if len(req.IDs) > 100 {
|
|
|
c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
|
|
c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
|
|
|
- Code: 400,
|
|
|
|
|
- Msg: "一次最多只能删除 100 个 License",
|
|
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "一次最多只能删除 100 个 License",
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
@@ -392,8 +431,8 @@ func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
result := db.Where("id IN ?", req.IDs).Delete(&models.License{})
|
|
result := db.Where("id IN ?", req.IDs).Delete(&models.License{})
|
|
|
if result.Error != nil {
|
|
if result.Error != nil {
|
|
|
c.JSON(http.StatusInternalServerError, BatchDeleteLicenseResponse{
|
|
c.JSON(http.StatusInternalServerError, BatchDeleteLicenseResponse{
|
|
|
- Code: 500,
|
|
|
|
|
- Msg: "批量删除失败: " + result.Error.Error(),
|
|
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "批量删除失败: " + result.Error.Error(),
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
@@ -401,8 +440,8 @@ func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
|
|
|
|
|
deletedCount := int(result.RowsAffected)
|
|
deletedCount := int(result.RowsAffected)
|
|
|
c.JSON(http.StatusOK, BatchDeleteLicenseResponse{
|
|
c.JSON(http.StatusOK, BatchDeleteLicenseResponse{
|
|
|
- Code: 0,
|
|
|
|
|
- Msg: "成功删除 " + strconv.Itoa(deletedCount) + " 个 License",
|
|
|
|
|
|
|
+ Code: 0,
|
|
|
|
|
+ Msg: "成功删除 " + strconv.Itoa(deletedCount) + " 个 License",
|
|
|
Total: deletedCount,
|
|
Total: deletedCount,
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -410,16 +449,16 @@ func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
|
|
|
|
|
// BatchCreateLicenseRequest 批量创建 License 请求结构
|
|
// BatchCreateLicenseRequest 批量创建 License 请求结构
|
|
|
type BatchCreateLicenseRequest struct {
|
|
type BatchCreateLicenseRequest struct {
|
|
|
- Prefix string `json:"prefix" binding:"required"` // 激活码前缀(必填)
|
|
|
|
|
- Count int `json:"count" binding:"required"` // 生成数量(必填)
|
|
|
|
|
- MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2)
|
|
|
|
|
|
|
+ Prefix string `json:"prefix" binding:"required"` // 激活码前缀(必填)
|
|
|
|
|
+ Count int `json:"count" binding:"required"` // 生成数量(必填)
|
|
|
|
|
+ MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// BatchCreateLicenseResponse 批量创建响应结构
|
|
// BatchCreateLicenseResponse 批量创建响应结构
|
|
|
type BatchCreateLicenseResponse struct {
|
|
type BatchCreateLicenseResponse struct {
|
|
|
- Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
- Msg string `json:"msg"` // 响应消息
|
|
|
|
|
- Data []models.License `json:"data"` // 创建的 License 列表
|
|
|
|
|
|
|
+ Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
|
|
|
|
|
+ Msg string `json:"msg"` // 响应消息
|
|
|
|
|
+ Data []models.License `json:"data"` // 创建的 License 列表
|
|
|
Total int `json:"total"` // 成功创建的数量
|
|
Total int `json:"total"` // 成功创建的数量
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -430,9 +469,9 @@ func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
var req BatchCreateLicenseRequest
|
|
var req BatchCreateLicenseRequest
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
|
|
c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
|
|
|
- Code: 400,
|
|
|
|
|
- Msg: "请求参数错误: " + err.Error(),
|
|
|
|
|
- Data: []models.License{},
|
|
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "请求参数错误: " + err.Error(),
|
|
|
|
|
+ Data: []models.License{},
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
@@ -441,9 +480,9 @@ func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
// 验证参数
|
|
// 验证参数
|
|
|
if req.Count <= 0 || req.Count > 1000 {
|
|
if req.Count <= 0 || req.Count > 1000 {
|
|
|
c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
|
|
c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
|
|
|
- Code: 400,
|
|
|
|
|
- Msg: "生成数量必须在 1-1000 之间",
|
|
|
|
|
- Data: []models.License{},
|
|
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "生成数量必须在 1-1000 之间",
|
|
|
|
|
+ Data: []models.License{},
|
|
|
Total: 0,
|
|
Total: 0,
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
@@ -463,16 +502,15 @@ func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
// 生成激活码:前缀 + 32位随机字符串
|
|
// 生成激活码:前缀 + 32位随机字符串
|
|
|
key := req.Prefix + "-" + generateRandomKey(32)
|
|
key := req.Prefix + "-" + generateRandomKey(32)
|
|
|
|
|
|
|
|
- // 检查 Key 是否已存在
|
|
|
|
|
- var existingLicense models.License
|
|
|
|
|
- result := db.Where("license_key = ?", key).First(&existingLicense)
|
|
|
|
|
- if result.Error == nil {
|
|
|
|
|
- // Key 已存在,跳过并记录
|
|
|
|
|
|
|
+ // 检查 Key 是否已存在(使用 Count 更高效,不会产生 record not found 日志)
|
|
|
|
|
+ var count int64
|
|
|
|
|
+ if err := db.Model(&models.License{}).Where("license_key = ?", key).Count(&count).Error; err != nil {
|
|
|
|
|
+ // 数据库查询错误
|
|
|
failedKeys = append(failedKeys, key)
|
|
failedKeys = append(failedKeys, key)
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
- if result.Error != gorm.ErrRecordNotFound {
|
|
|
|
|
- // 数据库查询错误
|
|
|
|
|
|
|
+ if count > 0 {
|
|
|
|
|
+ // Key 已存在,跳过并记录
|
|
|
failedKeys = append(failedKeys, key)
|
|
failedKeys = append(failedKeys, key)
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
@@ -500,9 +538,9 @@ func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, BatchCreateLicenseResponse{
|
|
c.JSON(http.StatusOK, BatchCreateLicenseResponse{
|
|
|
- Code: 0,
|
|
|
|
|
- Msg: msg,
|
|
|
|
|
- Data: licenses,
|
|
|
|
|
|
|
+ Code: 0,
|
|
|
|
|
+ Msg: msg,
|
|
|
|
|
+ Data: licenses,
|
|
|
Total: successCount,
|
|
Total: successCount,
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -523,3 +561,246 @@ func getRandomInt(max int) int {
|
|
|
return rand.Intn(max)
|
|
return rand.Intn(max)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+// ExportLicenseRequest 导出请求结构
|
|
|
|
|
+type ExportLicenseRequest struct {
|
|
|
|
|
+ Format string `json:"format"` // csv 或 json
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ExportLicenses 导出 License 列表
|
|
|
|
|
+// GET /api/licenses/export?format=csv
|
|
|
|
|
+func ExportLicenses(db *gorm.DB) gin.HandlerFunc {
|
|
|
|
|
+ return func(c *gin.Context) {
|
|
|
|
|
+ format := c.DefaultQuery("format", "csv")
|
|
|
|
|
+
|
|
|
|
|
+ var licenses []models.License
|
|
|
|
|
+
|
|
|
|
|
+ // 使用与列表相同的排序逻辑
|
|
|
|
|
+ orderBy := `CASE
|
|
|
|
|
+ WHEN JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0 THEN 0
|
|
|
|
|
+ ELSE 1
|
|
|
|
|
+ END ASC,
|
|
|
|
|
+ updated_at DESC`
|
|
|
|
|
+
|
|
|
|
|
+ if err := db.Order(orderBy).Find(&licenses).Error; err != nil {
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseListResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "查询失败: " + err.Error(),
|
|
|
|
|
+ Data: []models.License{},
|
|
|
|
|
+ Total: 0,
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if format == "csv" {
|
|
|
|
|
+ // 导出 CSV
|
|
|
|
|
+ c.Header("Content-Type", "text/csv; charset=utf-8")
|
|
|
|
|
+ c.Header("Content-Disposition", "attachment; filename=licenses_"+time.Now().Format("20060102_150405")+".csv")
|
|
|
|
|
+ c.Header("Content-Transfer-Encoding", "binary")
|
|
|
|
|
+
|
|
|
|
|
+ writer := csv.NewWriter(c.Writer)
|
|
|
|
|
+ defer writer.Flush()
|
|
|
|
|
+
|
|
|
|
|
+ // 写入表头
|
|
|
|
|
+ headers := []string{"ID", "激活码", "最大设备数", "已绑定设备数", "绑定设备列表", "创建时间", "更新时间"}
|
|
|
|
|
+ writer.Write(headers)
|
|
|
|
|
+
|
|
|
|
|
+ // 写入数据
|
|
|
|
|
+ for _, license := range licenses {
|
|
|
|
|
+ devices, _ := license.GetBoundDeviceList()
|
|
|
|
|
+ deviceList := strings.Join(devices, "; ")
|
|
|
|
|
+ row := []string{
|
|
|
|
|
+ strconv.FormatUint(uint64(license.ID), 10),
|
|
|
|
|
+ license.LicenseKey,
|
|
|
|
|
+ strconv.Itoa(license.MaxDevices),
|
|
|
|
|
+ strconv.Itoa(len(devices)),
|
|
|
|
|
+ deviceList,
|
|
|
|
|
+ license.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
|
|
|
+ license.UpdatedAt.Format("2006-01-02 15:04:05"),
|
|
|
|
|
+ }
|
|
|
|
|
+ writer.Write(row)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 导出 JSON
|
|
|
|
|
+ c.Header("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
+ c.Header("Content-Disposition", "attachment; filename=licenses_"+time.Now().Format("20060102_150405")+".json")
|
|
|
|
|
+ c.JSON(http.StatusOK, LicenseListResponse{
|
|
|
|
|
+ Code: 0,
|
|
|
|
|
+ Msg: "success",
|
|
|
|
|
+ Data: licenses,
|
|
|
|
|
+ Total: int64(len(licenses)),
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// BatchUpdateMaxDevicesRequest 批量修改最大设备数请求结构
|
|
|
|
|
+type BatchUpdateMaxDevicesRequest struct {
|
|
|
|
|
+ IDs []uint `json:"ids" binding:"required"`
|
|
|
|
|
+ MaxDevices int `json:"max_devices" binding:"required,min=1"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// BatchUpdateMaxDevices 批量修改最大设备数
|
|
|
|
|
+// PUT /api/licenses/batch/max-devices
|
|
|
|
|
+func BatchUpdateMaxDevices(db *gorm.DB) gin.HandlerFunc {
|
|
|
|
|
+ return func(c *gin.Context) {
|
|
|
|
|
+ var req BatchUpdateMaxDevicesRequest
|
|
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
+ c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "请求参数错误: " + err.Error(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if len(req.IDs) == 0 {
|
|
|
|
|
+ c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "请至少选择一个 License",
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 批量更新
|
|
|
|
|
+ result := db.Model(&models.License{}).
|
|
|
|
|
+ Where("id IN ?", req.IDs).
|
|
|
|
|
+ Update("max_devices", req.MaxDevices)
|
|
|
|
|
+
|
|
|
|
|
+ if result.Error != nil {
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "批量更新失败: " + result.Error.Error(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ c.JSON(http.StatusOK, LicenseResponse{
|
|
|
|
|
+ Code: 0,
|
|
|
|
|
+ Msg: "成功更新 " + strconv.FormatInt(result.RowsAffected, 10) + " 个 License 的最大设备数",
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// UnbindDeviceRequest 解绑设备请求结构
|
|
|
|
|
+type UnbindDeviceRequest struct {
|
|
|
|
|
+ DeviceID string `json:"device_id" binding:"required"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// UnbindDevice 解绑设备
|
|
|
|
|
+// DELETE /api/licenses/:id/devices/:device_id
|
|
|
|
|
+func UnbindDevice(db *gorm.DB) gin.HandlerFunc {
|
|
|
|
|
+ return func(c *gin.Context) {
|
|
|
|
|
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "无效的 ID 参数",
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ deviceID := c.Param("device_id")
|
|
|
|
|
+ if deviceID == "" {
|
|
|
|
|
+ c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "设备ID不能为空",
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var license models.License
|
|
|
|
|
+ if err := db.First(&license, id).Error; err != nil {
|
|
|
|
|
+ if err == gorm.ErrRecordNotFound {
|
|
|
|
|
+ c.JSON(http.StatusNotFound, LicenseResponse{
|
|
|
|
|
+ Code: 404,
|
|
|
|
|
+ Msg: "License 不存在",
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "查询失败: " + err.Error(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取设备列表
|
|
|
|
|
+ devices, err := license.GetBoundDeviceList()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "解析设备列表失败: " + err.Error(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 移除设备
|
|
|
|
|
+ newDevices := []string{}
|
|
|
|
|
+ found := false
|
|
|
|
|
+ for _, d := range devices {
|
|
|
|
|
+ if d != deviceID {
|
|
|
|
|
+ newDevices = append(newDevices, d)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ found = true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if !found {
|
|
|
|
|
+ c.JSON(http.StatusBadRequest, LicenseResponse{
|
|
|
|
|
+ Code: 400,
|
|
|
|
|
+ Msg: "设备未绑定",
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新设备列表
|
|
|
|
|
+ if err := license.SetBoundDeviceList(newDevices); err != nil {
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "更新设备列表失败: " + err.Error(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 同时清理激活时间和心跳时间
|
|
|
|
|
+ var activations map[string]string
|
|
|
|
|
+ if license.DeviceActivations != "" {
|
|
|
|
|
+ json.Unmarshal([]byte(license.DeviceActivations), &activations)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ activations = make(map[string]string)
|
|
|
|
|
+ }
|
|
|
|
|
+ delete(activations, deviceID)
|
|
|
|
|
+ if activationsData, err := json.Marshal(activations); err == nil {
|
|
|
|
|
+ license.DeviceActivations = string(activationsData)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var heartbeats map[string]string
|
|
|
|
|
+ if license.DeviceHeartbeats != "" {
|
|
|
|
|
+ json.Unmarshal([]byte(license.DeviceHeartbeats), &heartbeats)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ heartbeats = make(map[string]string)
|
|
|
|
|
+ }
|
|
|
|
|
+ delete(heartbeats, deviceID)
|
|
|
|
|
+ if heartbeatsData, err := json.Marshal(heartbeats); err == nil {
|
|
|
|
|
+ license.DeviceHeartbeats = string(heartbeatsData)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存到数据库
|
|
|
|
|
+ if err := db.Save(&license).Error; err != nil {
|
|
|
|
|
+ c.JSON(http.StatusInternalServerError, LicenseResponse{
|
|
|
|
|
+ Code: 500,
|
|
|
|
|
+ Msg: "保存失败: " + err.Error(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ c.JSON(http.StatusOK, LicenseResponse{
|
|
|
|
|
+ Code: 0,
|
|
|
|
|
+ Msg: "成功解绑设备",
|
|
|
|
|
+ Data: &license,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|