license.go 24 KB

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