package handlers import ( "encoding/csv" "encoding/json" "license-admin/models" "math/rand" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // CreateLicenseRequest 创建 License 请求结构 type CreateLicenseRequest struct { Key string `json:"key" binding:"required"` // 激活码(必填) MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2) BoundDevices string `json:"bound_devices,omitempty"` // 初始绑定设备列表(可选,默认为空数组) } // UpdateLicenseRequest 更新 License 请求结构 type UpdateLicenseRequest struct { Key string `json:"key,omitempty"` // 激活码(可选) MaxDevices *int `json:"max_devices,omitempty"` // 最大设备数(可选,使用指针以区分零值) BoundDevices string `json:"bound_devices,omitempty"` // 绑定设备列表(可选) } // LicenseResponse License 响应结构 type LicenseResponse struct { Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败 Msg string `json:"msg"` // 响应消息 Data *models.License `json:"data,omitempty"` // License 数据 } // LicenseListResponse License 列表响应结构 type LicenseListResponse struct { Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败 Msg string `json:"msg"` // 响应消息 Data []models.License `json:"data"` // License 列表 Total int64 `json:"total"` // 总数 } // CreateLicense 创建新的 License // POST /api/licenses func CreateLicense(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var req CreateLicenseRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, LicenseResponse{ Code: 400, Msg: "请求参数错误: " + err.Error(), }) return } // 检查 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 已存在 c.JSON(http.StatusBadRequest, LicenseResponse{ Code: 400, Msg: "激活码已存在", }) return } // 设置默认值 if req.MaxDevices <= 0 { req.MaxDevices = 2 // 默认最大设备数为 2 } if req.BoundDevices == "" { req.BoundDevices = "[]" // 默认为空数组 } // 创建新的 License license := models.License{ LicenseKey: req.Key, MaxDevices: req.MaxDevices, BoundDevices: req.BoundDevices, } if err := db.Create(&license).Error; err != nil { c.JSON(http.StatusInternalServerError, LicenseResponse{ Code: 500, Msg: "创建 License 失败: " + err.Error(), }) return } c.JSON(http.StatusOK, LicenseResponse{ Code: 0, Msg: "创建成功", Data: &license, }) } } // GetLicenseList 获取 License 列表 // GET /api/licenses?page=1&page_size=10&search=keyword func GetLicenseList(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // 解析分页参数 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) search := c.Query("search") // 搜索关键词 status := c.Query("status") // 状态筛选:activated(已激活)、unactivated(未激活) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 10 } var licenses []models.License 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 := query.Count(&total).Error; err != nil { c.JSON(http.StatusInternalServerError, LicenseListResponse{ Code: 500, Msg: "查询总数失败: " + err.Error(), Data: []models.License{}, Total: 0, }) 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 if err := query.Offset(offset).Limit(pageSize).Order(orderBy).Find(&licenses).Error; err != nil { c.JSON(http.StatusInternalServerError, LicenseListResponse{ Code: 500, Msg: "查询 License 列表失败: " + err.Error(), Data: []models.License{}, Total: 0, }) return } c.JSON(http.StatusOK, LicenseListResponse{ Code: 0, Msg: "success", Data: licenses, Total: total, }) } } // GetLicense 获取单个 License // GET /api/licenses/:id func GetLicense(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 } var license models.License result := db.First(&license, id) if result.Error != nil { if result.Error == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, LicenseResponse{ Code: 404, Msg: "License 不存在", }) return } c.JSON(http.StatusInternalServerError, LicenseResponse{ Code: 500, Msg: "查询失败: " + result.Error.Error(), }) return } c.JSON(http.StatusOK, LicenseResponse{ Code: 0, Msg: "success", Data: &license, }) } } // UpdateLicense 更新 License // PUT /api/licenses/:id func UpdateLicense(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 } // 查找 License var license models.License result := db.First(&license, id) if result.Error != nil { if result.Error == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, LicenseResponse{ Code: 404, Msg: "License 不存在", }) return } c.JSON(http.StatusInternalServerError, LicenseResponse{ Code: 500, Msg: "查询失败: " + result.Error.Error(), }) return } // 解析更新请求 var req UpdateLicenseRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, LicenseResponse{ Code: 400, Msg: "请求参数错误: " + err.Error(), }) return } // 更新字段 if req.Key != "" { // 检查新 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 } if count > 0 { c.JSON(http.StatusBadRequest, LicenseResponse{ Code: 400, Msg: "激活码已存在", }) return } license.LicenseKey = req.Key } if req.MaxDevices != nil { if *req.MaxDevices <= 0 { c.JSON(http.StatusBadRequest, LicenseResponse{ Code: 400, Msg: "最大设备数必须大于 0", }) return } license.MaxDevices = *req.MaxDevices } if req.BoundDevices != "" { // 验证 BoundDevices 是否为有效的 JSON 数组 var testDevices []string if err := json.Unmarshal([]byte(req.BoundDevices), &testDevices); err != nil { c.JSON(http.StatusBadRequest, LicenseResponse{ Code: 400, Msg: "bound_devices 必须是有效的 JSON 数组: " + err.Error(), }) return } license.BoundDevices = req.BoundDevices } // 保存更新 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, }) } } // DeleteLicense 删除 License // DELETE /api/licenses/:id func DeleteLicense(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, gin.H{ "code": 400, "msg": "无效的 ID 参数", }) return } // 查找 License var license models.License result := db.First(&license, id) if result.Error != nil { if result.Error == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{ "code": 404, "msg": "License 不存在", }) return } c.JSON(http.StatusInternalServerError, gin.H{ "code": 500, "msg": "查询失败: " + result.Error.Error(), }) return } // 删除 License if err := db.Delete(&license).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "code": 500, "msg": "删除失败: " + err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "删除成功", }) } } // BatchDeleteLicenseRequest 批量删除 License 请求结构 type BatchDeleteLicenseRequest struct { IDs []uint `json:"ids" binding:"required"` // License ID 列表 } // BatchDeleteLicenseResponse 批量删除响应结构 type BatchDeleteLicenseResponse struct { Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败 Msg string `json:"msg"` // 响应消息 Total int `json:"total"` // 成功删除的数量 } // BatchDeleteLicense 批量删除 License // DELETE /api/licenses/batch func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var req BatchDeleteLicenseRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{ Code: 400, Msg: "请求参数错误: " + err.Error(), Total: 0, }) return } // 验证参数 if len(req.IDs) == 0 { c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{ Code: 400, Msg: "请至少选择一个 License", Total: 0, }) return } if len(req.IDs) > 100 { c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{ Code: 400, Msg: "一次最多只能删除 100 个 License", Total: 0, }) return } // 批量删除 result := db.Where("id IN ?", req.IDs).Delete(&models.License{}) if result.Error != nil { c.JSON(http.StatusInternalServerError, BatchDeleteLicenseResponse{ Code: 500, Msg: "批量删除失败: " + result.Error.Error(), Total: 0, }) return } deletedCount := int(result.RowsAffected) c.JSON(http.StatusOK, BatchDeleteLicenseResponse{ Code: 0, Msg: "成功删除 " + strconv.Itoa(deletedCount) + " 个 License", Total: deletedCount, }) } } // BatchCreateLicenseRequest 批量创建 License 请求结构 type BatchCreateLicenseRequest struct { Prefix string `json:"prefix" binding:"required"` // 激活码前缀(必填) Count int `json:"count" binding:"required"` // 生成数量(必填) MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2) } // BatchCreateLicenseResponse 批量创建响应结构 type BatchCreateLicenseResponse struct { Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败 Msg string `json:"msg"` // 响应消息 Data []models.License `json:"data"` // 创建的 License 列表 Total int `json:"total"` // 成功创建的数量 } // BatchCreateLicense 批量创建 License // POST /api/licenses/batch func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var req BatchCreateLicenseRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{ Code: 400, Msg: "请求参数错误: " + err.Error(), Data: []models.License{}, Total: 0, }) return } // 验证参数 if req.Count <= 0 || req.Count > 1000 { c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{ Code: 400, Msg: "生成数量必须在 1-1000 之间", Data: []models.License{}, Total: 0, }) return } // 设置默认值 if req.MaxDevices <= 0 { req.MaxDevices = 2 } // 批量创建 License var licenses []models.License var successCount int var failedKeys []string for i := 1; i <= req.Count; i++ { // 生成激活码:前缀 + 32位随机字符串 key := req.Prefix + "-" + generateRandomKey(32) // 检查 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) continue } if count > 0 { // Key 已存在,跳过并记录 failedKeys = append(failedKeys, key) continue } // 创建新的 License license := models.License{ LicenseKey: key, MaxDevices: req.MaxDevices, BoundDevices: "[]", } if err := db.Create(&license).Error; err != nil { failedKeys = append(failedKeys, key) continue } licenses = append(licenses, license) successCount++ } // 构建响应消息 msg := "成功创建 " + strconv.Itoa(successCount) + " 个 License" if len(failedKeys) > 0 { msg += ",跳过 " + strconv.Itoa(len(failedKeys)) + " 个重复或失败的激活码" } c.JSON(http.StatusOK, BatchCreateLicenseResponse{ Code: 0, Msg: msg, Data: licenses, Total: successCount, }) } } // generateRandomKey 生成随机激活码后缀 func generateRandomKey(length int) string { const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, length) for i := range b { b[i] = charset[getRandomInt(len(charset))] } return string(b) } // getRandomInt 获取随机整数 func getRandomInt(max int) int { 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, }) } }