license.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. package handlers
  2. import (
  3. "encoding/csv"
  4. "encoding/json"
  5. "license-admin/models"
  6. "math/rand"
  7. "net/http"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/gin-gonic/gin"
  12. "gorm.io/gorm"
  13. )
  14. // CreateLicenseRequest 创建 License 请求结构
  15. type CreateLicenseRequest struct {
  16. Key string `json:"key" binding:"required"` // 激活码(必填)
  17. MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2)
  18. BoundDevices string `json:"bound_devices,omitempty"` // 初始绑定设备列表(可选,默认为空数组)
  19. }
  20. // UpdateLicenseRequest 更新 License 请求结构
  21. type UpdateLicenseRequest struct {
  22. Key string `json:"key,omitempty"` // 激活码(可选)
  23. MaxDevices *int `json:"max_devices,omitempty"` // 最大设备数(可选,使用指针以区分零值)
  24. BoundDevices string `json:"bound_devices,omitempty"` // 绑定设备列表(可选)
  25. }
  26. // LicenseResponse License 响应结构
  27. type LicenseResponse struct {
  28. Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
  29. Msg string `json:"msg"` // 响应消息
  30. Data *models.License `json:"data,omitempty"` // License 数据
  31. }
  32. // LicenseListResponse License 列表响应结构
  33. type LicenseListResponse struct {
  34. Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
  35. Msg string `json:"msg"` // 响应消息
  36. Data []models.License `json:"data"` // License 列表
  37. Total int64 `json:"total"` // 总数
  38. }
  39. // CreateLicense 创建新的 License
  40. // POST /api/licenses
  41. func CreateLicense(db *gorm.DB) gin.HandlerFunc {
  42. return func(c *gin.Context) {
  43. var req CreateLicenseRequest
  44. if err := c.ShouldBindJSON(&req); err != nil {
  45. c.JSON(http.StatusBadRequest, LicenseResponse{
  46. Code: 400,
  47. Msg: "请求参数错误: " + err.Error(),
  48. })
  49. return
  50. }
  51. // 检查 Key 是否已存在(使用 Count 更高效,不会产生 record not found 日志)
  52. var count int64
  53. if err := db.Model(&models.License{}).Where("license_key = ?", req.Key).Count(&count).Error; err != nil {
  54. // 数据库查询错误
  55. c.JSON(http.StatusInternalServerError, LicenseResponse{
  56. Code: 500,
  57. Msg: "数据库查询错误: " + err.Error(),
  58. })
  59. return
  60. }
  61. if count > 0 {
  62. // Key 已存在
  63. c.JSON(http.StatusBadRequest, LicenseResponse{
  64. Code: 400,
  65. Msg: "激活码已存在",
  66. })
  67. return
  68. }
  69. // 设置默认值
  70. if req.MaxDevices <= 0 {
  71. req.MaxDevices = 2 // 默认最大设备数为 2
  72. }
  73. if req.BoundDevices == "" {
  74. req.BoundDevices = "[]" // 默认为空数组
  75. }
  76. // 创建新的 License
  77. license := models.License{
  78. LicenseKey: req.Key,
  79. MaxDevices: req.MaxDevices,
  80. BoundDevices: req.BoundDevices,
  81. }
  82. if err := db.Create(&license).Error; err != nil {
  83. c.JSON(http.StatusInternalServerError, LicenseResponse{
  84. Code: 500,
  85. Msg: "创建 License 失败: " + err.Error(),
  86. })
  87. return
  88. }
  89. c.JSON(http.StatusOK, LicenseResponse{
  90. Code: 0,
  91. Msg: "创建成功",
  92. Data: &license,
  93. })
  94. }
  95. }
  96. // GetLicenseList 获取 License 列表
  97. // GET /api/licenses?page=1&page_size=10&search=keyword
  98. func GetLicenseList(db *gorm.DB) gin.HandlerFunc {
  99. return func(c *gin.Context) {
  100. // 解析分页参数
  101. page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
  102. pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
  103. search := c.Query("search") // 搜索关键词
  104. status := c.Query("status") // 状态筛选:activated(已激活)、unactivated(未激活)
  105. if page < 1 {
  106. page = 1
  107. }
  108. if pageSize < 1 || pageSize > 100 {
  109. pageSize = 10
  110. }
  111. var licenses []models.License
  112. var total int64
  113. // 构建查询
  114. query := db.Model(&models.License{})
  115. // 如果有搜索关键词,添加搜索条件(按 license_key 模糊匹配)
  116. if search != "" {
  117. query = query.Where("license_key LIKE ?", "%"+search+"%")
  118. }
  119. // 状态筛选
  120. if status == "activated" {
  121. // 已激活:有绑定设备
  122. query = query.Where("JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0")
  123. } else if status == "unactivated" {
  124. // 未激活:没有绑定设备
  125. query = query.Where("JSON_LENGTH(COALESCE(bound_devices, '[]')) = 0 OR bound_devices IS NULL OR bound_devices = ''")
  126. }
  127. // 获取总数
  128. if err := query.Count(&total).Error; err != nil {
  129. c.JSON(http.StatusInternalServerError, LicenseListResponse{
  130. Code: 500,
  131. Msg: "查询总数失败: " + err.Error(),
  132. Data: []models.License{},
  133. Total: 0,
  134. })
  135. return
  136. }
  137. // 使用 SQL 排序:
  138. // 1. 有绑定设备的优先(bound_devices 不为空且不是 '[]')
  139. // 2. 然后按心跳时间最新的排序(从 device_heartbeats JSON 中提取最新时间)
  140. //
  141. // 使用 MySQL JSON 函数来提取最新的心跳时间:
  142. // - 对于 MySQL 5.7+,我们可以使用 JSON_EXTRACT 和 JSON_KEYS 来遍历
  143. // - 但由于提取所有值并找最大值在 SQL 中比较复杂,我们使用一个更实用的方法:
  144. // 使用 updated_at 字段,因为每次心跳更新(UpdateDeviceHeartbeat)都会更新 updated_at
  145. // 这样 updated_at 可以准确反映最后心跳时间
  146. //
  147. // 排序 SQL:
  148. // 1. 有设备的排在前面:CASE WHEN JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0 THEN 0 ELSE 1 END
  149. // 2. 然后按 updated_at 倒序(最新的心跳会更新 updated_at)
  150. orderBy := `CASE
  151. WHEN JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0 THEN 0
  152. ELSE 1
  153. END ASC,
  154. updated_at DESC`
  155. // 分页查询
  156. offset := (page - 1) * pageSize
  157. if err := query.Offset(offset).Limit(pageSize).Order(orderBy).Find(&licenses).Error; err != nil {
  158. c.JSON(http.StatusInternalServerError, LicenseListResponse{
  159. Code: 500,
  160. Msg: "查询 License 列表失败: " + err.Error(),
  161. Data: []models.License{},
  162. Total: 0,
  163. })
  164. return
  165. }
  166. c.JSON(http.StatusOK, LicenseListResponse{
  167. Code: 0,
  168. Msg: "success",
  169. Data: licenses,
  170. Total: total,
  171. })
  172. }
  173. }
  174. // GetLicense 获取单个 License
  175. // GET /api/licenses/:id
  176. func GetLicense(db *gorm.DB) gin.HandlerFunc {
  177. return func(c *gin.Context) {
  178. id, err := strconv.ParseUint(c.Param("id"), 10, 32)
  179. if err != nil {
  180. c.JSON(http.StatusBadRequest, LicenseResponse{
  181. Code: 400,
  182. Msg: "无效的 ID 参数",
  183. })
  184. return
  185. }
  186. var license models.License
  187. result := db.First(&license, id)
  188. if result.Error != nil {
  189. if result.Error == gorm.ErrRecordNotFound {
  190. c.JSON(http.StatusNotFound, LicenseResponse{
  191. Code: 404,
  192. Msg: "License 不存在",
  193. })
  194. return
  195. }
  196. c.JSON(http.StatusInternalServerError, LicenseResponse{
  197. Code: 500,
  198. Msg: "查询失败: " + result.Error.Error(),
  199. })
  200. return
  201. }
  202. c.JSON(http.StatusOK, LicenseResponse{
  203. Code: 0,
  204. Msg: "success",
  205. Data: &license,
  206. })
  207. }
  208. }
  209. // UpdateLicense 更新 License
  210. // PUT /api/licenses/:id
  211. func UpdateLicense(db *gorm.DB) gin.HandlerFunc {
  212. return func(c *gin.Context) {
  213. id, err := strconv.ParseUint(c.Param("id"), 10, 32)
  214. if err != nil {
  215. c.JSON(http.StatusBadRequest, LicenseResponse{
  216. Code: 400,
  217. Msg: "无效的 ID 参数",
  218. })
  219. return
  220. }
  221. // 查找 License
  222. var license models.License
  223. result := db.First(&license, id)
  224. if result.Error != nil {
  225. if result.Error == gorm.ErrRecordNotFound {
  226. c.JSON(http.StatusNotFound, LicenseResponse{
  227. Code: 404,
  228. Msg: "License 不存在",
  229. })
  230. return
  231. }
  232. c.JSON(http.StatusInternalServerError, LicenseResponse{
  233. Code: 500,
  234. Msg: "查询失败: " + result.Error.Error(),
  235. })
  236. return
  237. }
  238. // 解析更新请求
  239. var req UpdateLicenseRequest
  240. if err := c.ShouldBindJSON(&req); err != nil {
  241. c.JSON(http.StatusBadRequest, LicenseResponse{
  242. Code: 400,
  243. Msg: "请求参数错误: " + err.Error(),
  244. })
  245. return
  246. }
  247. // 更新字段
  248. if req.Key != "" {
  249. // 检查新 Key 是否与其他 License 冲突(使用 Count 更高效)
  250. var count int64
  251. if err := db.Model(&models.License{}).Where("license_key = ? AND id != ?", req.Key, id).Count(&count).Error; err != nil {
  252. c.JSON(http.StatusInternalServerError, LicenseResponse{
  253. Code: 500,
  254. Msg: "检查激活码失败: " + err.Error(),
  255. })
  256. return
  257. }
  258. if count > 0 {
  259. c.JSON(http.StatusBadRequest, LicenseResponse{
  260. Code: 400,
  261. Msg: "激活码已存在",
  262. })
  263. return
  264. }
  265. license.LicenseKey = req.Key
  266. }
  267. if req.MaxDevices != nil {
  268. if *req.MaxDevices <= 0 {
  269. c.JSON(http.StatusBadRequest, LicenseResponse{
  270. Code: 400,
  271. Msg: "最大设备数必须大于 0",
  272. })
  273. return
  274. }
  275. license.MaxDevices = *req.MaxDevices
  276. }
  277. if req.BoundDevices != "" {
  278. // 验证 BoundDevices 是否为有效的 JSON 数组
  279. var testDevices []string
  280. if err := json.Unmarshal([]byte(req.BoundDevices), &testDevices); err != nil {
  281. c.JSON(http.StatusBadRequest, LicenseResponse{
  282. Code: 400,
  283. Msg: "bound_devices 必须是有效的 JSON 数组: " + err.Error(),
  284. })
  285. return
  286. }
  287. license.BoundDevices = req.BoundDevices
  288. }
  289. // 保存更新
  290. if err := db.Save(&license).Error; err != nil {
  291. c.JSON(http.StatusInternalServerError, LicenseResponse{
  292. Code: 500,
  293. Msg: "更新失败: " + err.Error(),
  294. })
  295. return
  296. }
  297. c.JSON(http.StatusOK, LicenseResponse{
  298. Code: 0,
  299. Msg: "更新成功",
  300. Data: &license,
  301. })
  302. }
  303. }
  304. // DeleteLicense 删除 License
  305. // DELETE /api/licenses/:id
  306. func DeleteLicense(db *gorm.DB) gin.HandlerFunc {
  307. return func(c *gin.Context) {
  308. id, err := strconv.ParseUint(c.Param("id"), 10, 32)
  309. if err != nil {
  310. c.JSON(http.StatusBadRequest, gin.H{
  311. "code": 400,
  312. "msg": "无效的 ID 参数",
  313. })
  314. return
  315. }
  316. // 查找 License
  317. var license models.License
  318. result := db.First(&license, id)
  319. if result.Error != nil {
  320. if result.Error == gorm.ErrRecordNotFound {
  321. c.JSON(http.StatusNotFound, gin.H{
  322. "code": 404,
  323. "msg": "License 不存在",
  324. })
  325. return
  326. }
  327. c.JSON(http.StatusInternalServerError, gin.H{
  328. "code": 500,
  329. "msg": "查询失败: " + result.Error.Error(),
  330. })
  331. return
  332. }
  333. // 删除 License
  334. if err := db.Delete(&license).Error; err != nil {
  335. c.JSON(http.StatusInternalServerError, gin.H{
  336. "code": 500,
  337. "msg": "删除失败: " + err.Error(),
  338. })
  339. return
  340. }
  341. c.JSON(http.StatusOK, gin.H{
  342. "code": 0,
  343. "msg": "删除成功",
  344. })
  345. }
  346. }
  347. // BatchDeleteLicenseRequest 批量删除 License 请求结构
  348. type BatchDeleteLicenseRequest struct {
  349. IDs []uint `json:"ids" binding:"required"` // License ID 列表
  350. }
  351. // BatchDeleteLicenseResponse 批量删除响应结构
  352. type BatchDeleteLicenseResponse struct {
  353. Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
  354. Msg string `json:"msg"` // 响应消息
  355. Total int `json:"total"` // 成功删除的数量
  356. }
  357. // BatchDeleteLicense 批量删除 License
  358. // DELETE /api/licenses/batch
  359. func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
  360. return func(c *gin.Context) {
  361. var req BatchDeleteLicenseRequest
  362. if err := c.ShouldBindJSON(&req); err != nil {
  363. c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
  364. Code: 400,
  365. Msg: "请求参数错误: " + err.Error(),
  366. Total: 0,
  367. })
  368. return
  369. }
  370. // 验证参数
  371. if len(req.IDs) == 0 {
  372. c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
  373. Code: 400,
  374. Msg: "请至少选择一个 License",
  375. Total: 0,
  376. })
  377. return
  378. }
  379. if len(req.IDs) > 100 {
  380. c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
  381. Code: 400,
  382. Msg: "一次最多只能删除 100 个 License",
  383. Total: 0,
  384. })
  385. return
  386. }
  387. // 批量删除
  388. result := db.Where("id IN ?", req.IDs).Delete(&models.License{})
  389. if result.Error != nil {
  390. c.JSON(http.StatusInternalServerError, BatchDeleteLicenseResponse{
  391. Code: 500,
  392. Msg: "批量删除失败: " + result.Error.Error(),
  393. Total: 0,
  394. })
  395. return
  396. }
  397. deletedCount := int(result.RowsAffected)
  398. c.JSON(http.StatusOK, BatchDeleteLicenseResponse{
  399. Code: 0,
  400. Msg: "成功删除 " + strconv.Itoa(deletedCount) + " 个 License",
  401. Total: deletedCount,
  402. })
  403. }
  404. }
  405. // BatchCreateLicenseRequest 批量创建 License 请求结构
  406. type BatchCreateLicenseRequest struct {
  407. Prefix string `json:"prefix" binding:"required"` // 激活码前缀(必填)
  408. Count int `json:"count" binding:"required"` // 生成数量(必填)
  409. MaxDevices int `json:"max_devices"` // 最大设备数(可选,默认2)
  410. }
  411. // BatchCreateLicenseResponse 批量创建响应结构
  412. type BatchCreateLicenseResponse struct {
  413. Code int `json:"code"` // 状态码:0 表示成功,非0 表示失败
  414. Msg string `json:"msg"` // 响应消息
  415. Data []models.License `json:"data"` // 创建的 License 列表
  416. Total int `json:"total"` // 成功创建的数量
  417. }
  418. // BatchCreateLicense 批量创建 License
  419. // POST /api/licenses/batch
  420. func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
  421. return func(c *gin.Context) {
  422. var req BatchCreateLicenseRequest
  423. if err := c.ShouldBindJSON(&req); err != nil {
  424. c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
  425. Code: 400,
  426. Msg: "请求参数错误: " + err.Error(),
  427. Data: []models.License{},
  428. Total: 0,
  429. })
  430. return
  431. }
  432. // 验证参数
  433. if req.Count <= 0 || req.Count > 1000 {
  434. c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
  435. Code: 400,
  436. Msg: "生成数量必须在 1-1000 之间",
  437. Data: []models.License{},
  438. Total: 0,
  439. })
  440. return
  441. }
  442. // 设置默认值
  443. if req.MaxDevices <= 0 {
  444. req.MaxDevices = 2
  445. }
  446. // 批量创建 License
  447. var licenses []models.License
  448. var successCount int
  449. var failedKeys []string
  450. for i := 1; i <= req.Count; i++ {
  451. // 生成激活码:前缀 + 32位随机字符串
  452. key := req.Prefix + "-" + generateRandomKey(32)
  453. // 检查 Key 是否已存在(使用 Count 更高效,不会产生 record not found 日志)
  454. var count int64
  455. if err := db.Model(&models.License{}).Where("license_key = ?", key).Count(&count).Error; err != nil {
  456. // 数据库查询错误
  457. failedKeys = append(failedKeys, key)
  458. continue
  459. }
  460. if count > 0 {
  461. // Key 已存在,跳过并记录
  462. failedKeys = append(failedKeys, key)
  463. continue
  464. }
  465. // 创建新的 License
  466. license := models.License{
  467. LicenseKey: key,
  468. MaxDevices: req.MaxDevices,
  469. BoundDevices: "[]",
  470. }
  471. if err := db.Create(&license).Error; err != nil {
  472. failedKeys = append(failedKeys, key)
  473. continue
  474. }
  475. licenses = append(licenses, license)
  476. successCount++
  477. }
  478. // 构建响应消息
  479. msg := "成功创建 " + strconv.Itoa(successCount) + " 个 License"
  480. if len(failedKeys) > 0 {
  481. msg += ",跳过 " + strconv.Itoa(len(failedKeys)) + " 个重复或失败的激活码"
  482. }
  483. c.JSON(http.StatusOK, BatchCreateLicenseResponse{
  484. Code: 0,
  485. Msg: msg,
  486. Data: licenses,
  487. Total: successCount,
  488. })
  489. }
  490. }
  491. // generateRandomKey 生成随机激活码后缀
  492. func generateRandomKey(length int) string {
  493. const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  494. b := make([]byte, length)
  495. for i := range b {
  496. b[i] = charset[getRandomInt(len(charset))]
  497. }
  498. return string(b)
  499. }
  500. // getRandomInt 获取随机整数
  501. func getRandomInt(max int) int {
  502. return rand.Intn(max)
  503. }
  504. // ExportLicenseRequest 导出请求结构
  505. type ExportLicenseRequest struct {
  506. Format string `json:"format"` // csv 或 json
  507. }
  508. // ExportLicenses 导出 License 列表
  509. // GET /api/licenses/export?format=csv
  510. func ExportLicenses(db *gorm.DB) gin.HandlerFunc {
  511. return func(c *gin.Context) {
  512. format := c.DefaultQuery("format", "csv")
  513. var licenses []models.License
  514. // 使用与列表相同的排序逻辑
  515. orderBy := `CASE
  516. WHEN JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0 THEN 0
  517. ELSE 1
  518. END ASC,
  519. updated_at DESC`
  520. if err := db.Order(orderBy).Find(&licenses).Error; err != nil {
  521. c.JSON(http.StatusInternalServerError, LicenseListResponse{
  522. Code: 500,
  523. Msg: "查询失败: " + err.Error(),
  524. Data: []models.License{},
  525. Total: 0,
  526. })
  527. return
  528. }
  529. if format == "csv" {
  530. // 导出 CSV
  531. c.Header("Content-Type", "text/csv; charset=utf-8")
  532. c.Header("Content-Disposition", "attachment; filename=licenses_"+time.Now().Format("20060102_150405")+".csv")
  533. c.Header("Content-Transfer-Encoding", "binary")
  534. writer := csv.NewWriter(c.Writer)
  535. defer writer.Flush()
  536. // 写入表头
  537. headers := []string{"ID", "激活码", "最大设备数", "已绑定设备数", "绑定设备列表", "创建时间", "更新时间"}
  538. writer.Write(headers)
  539. // 写入数据
  540. for _, license := range licenses {
  541. devices, _ := license.GetBoundDeviceList()
  542. deviceList := strings.Join(devices, "; ")
  543. row := []string{
  544. strconv.FormatUint(uint64(license.ID), 10),
  545. license.LicenseKey,
  546. strconv.Itoa(license.MaxDevices),
  547. strconv.Itoa(len(devices)),
  548. deviceList,
  549. license.CreatedAt.Format("2006-01-02 15:04:05"),
  550. license.UpdatedAt.Format("2006-01-02 15:04:05"),
  551. }
  552. writer.Write(row)
  553. }
  554. } else {
  555. // 导出 JSON
  556. c.Header("Content-Type", "application/json; charset=utf-8")
  557. c.Header("Content-Disposition", "attachment; filename=licenses_"+time.Now().Format("20060102_150405")+".json")
  558. c.JSON(http.StatusOK, LicenseListResponse{
  559. Code: 0,
  560. Msg: "success",
  561. Data: licenses,
  562. Total: int64(len(licenses)),
  563. })
  564. }
  565. }
  566. }
  567. // BatchUpdateMaxDevicesRequest 批量修改最大设备数请求结构
  568. type BatchUpdateMaxDevicesRequest struct {
  569. IDs []uint `json:"ids" binding:"required"`
  570. MaxDevices int `json:"max_devices" binding:"required,min=1"`
  571. }
  572. // BatchUpdateMaxDevices 批量修改最大设备数
  573. // PUT /api/licenses/batch/max-devices
  574. func BatchUpdateMaxDevices(db *gorm.DB) gin.HandlerFunc {
  575. return func(c *gin.Context) {
  576. var req BatchUpdateMaxDevicesRequest
  577. if err := c.ShouldBindJSON(&req); err != nil {
  578. c.JSON(http.StatusBadRequest, LicenseResponse{
  579. Code: 400,
  580. Msg: "请求参数错误: " + err.Error(),
  581. })
  582. return
  583. }
  584. if len(req.IDs) == 0 {
  585. c.JSON(http.StatusBadRequest, LicenseResponse{
  586. Code: 400,
  587. Msg: "请至少选择一个 License",
  588. })
  589. return
  590. }
  591. // 批量更新
  592. result := db.Model(&models.License{}).
  593. Where("id IN ?", req.IDs).
  594. Update("max_devices", req.MaxDevices)
  595. if result.Error != nil {
  596. c.JSON(http.StatusInternalServerError, LicenseResponse{
  597. Code: 500,
  598. Msg: "批量更新失败: " + result.Error.Error(),
  599. })
  600. return
  601. }
  602. c.JSON(http.StatusOK, LicenseResponse{
  603. Code: 0,
  604. Msg: "成功更新 " + strconv.FormatInt(result.RowsAffected, 10) + " 个 License 的最大设备数",
  605. })
  606. }
  607. }
  608. // UnbindDeviceRequest 解绑设备请求结构
  609. type UnbindDeviceRequest struct {
  610. DeviceID string `json:"device_id" binding:"required"`
  611. }
  612. // UnbindDevice 解绑设备
  613. // DELETE /api/licenses/:id/devices/:device_id
  614. func UnbindDevice(db *gorm.DB) gin.HandlerFunc {
  615. return func(c *gin.Context) {
  616. id, err := strconv.ParseUint(c.Param("id"), 10, 32)
  617. if err != nil {
  618. c.JSON(http.StatusBadRequest, LicenseResponse{
  619. Code: 400,
  620. Msg: "无效的 ID 参数",
  621. })
  622. return
  623. }
  624. deviceID := c.Param("device_id")
  625. if deviceID == "" {
  626. c.JSON(http.StatusBadRequest, LicenseResponse{
  627. Code: 400,
  628. Msg: "设备ID不能为空",
  629. })
  630. return
  631. }
  632. var license models.License
  633. if err := db.First(&license, id).Error; err != nil {
  634. if err == gorm.ErrRecordNotFound {
  635. c.JSON(http.StatusNotFound, LicenseResponse{
  636. Code: 404,
  637. Msg: "License 不存在",
  638. })
  639. return
  640. }
  641. c.JSON(http.StatusInternalServerError, LicenseResponse{
  642. Code: 500,
  643. Msg: "查询失败: " + err.Error(),
  644. })
  645. return
  646. }
  647. // 获取设备列表
  648. devices, err := license.GetBoundDeviceList()
  649. if err != nil {
  650. c.JSON(http.StatusInternalServerError, LicenseResponse{
  651. Code: 500,
  652. Msg: "解析设备列表失败: " + err.Error(),
  653. })
  654. return
  655. }
  656. // 移除设备
  657. newDevices := []string{}
  658. found := false
  659. for _, d := range devices {
  660. if d != deviceID {
  661. newDevices = append(newDevices, d)
  662. } else {
  663. found = true
  664. }
  665. }
  666. if !found {
  667. c.JSON(http.StatusBadRequest, LicenseResponse{
  668. Code: 400,
  669. Msg: "设备未绑定",
  670. })
  671. return
  672. }
  673. // 更新设备列表
  674. if err := license.SetBoundDeviceList(newDevices); err != nil {
  675. c.JSON(http.StatusInternalServerError, LicenseResponse{
  676. Code: 500,
  677. Msg: "更新设备列表失败: " + err.Error(),
  678. })
  679. return
  680. }
  681. // 同时清理激活时间和心跳时间
  682. var activations map[string]string
  683. if license.DeviceActivations != "" {
  684. json.Unmarshal([]byte(license.DeviceActivations), &activations)
  685. } else {
  686. activations = make(map[string]string)
  687. }
  688. delete(activations, deviceID)
  689. if activationsData, err := json.Marshal(activations); err == nil {
  690. license.DeviceActivations = string(activationsData)
  691. }
  692. var heartbeats map[string]string
  693. if license.DeviceHeartbeats != "" {
  694. json.Unmarshal([]byte(license.DeviceHeartbeats), &heartbeats)
  695. } else {
  696. heartbeats = make(map[string]string)
  697. }
  698. delete(heartbeats, deviceID)
  699. if heartbeatsData, err := json.Marshal(heartbeats); err == nil {
  700. license.DeviceHeartbeats = string(heartbeatsData)
  701. }
  702. // 保存到数据库
  703. if err := db.Save(&license).Error; err != nil {
  704. c.JSON(http.StatusInternalServerError, LicenseResponse{
  705. Code: 500,
  706. Msg: "保存失败: " + err.Error(),
  707. })
  708. return
  709. }
  710. c.JSON(http.StatusOK, LicenseResponse{
  711. Code: 0,
  712. Msg: "成功解绑设备",
  713. Data: &license,
  714. })
  715. }
  716. }