feat: 添加设备管理模块,支持设备列表获取和设备解绑功能

This commit is contained in:
wood911010 committed 2026-01-04 00:09:31 +08:00
1 parent d7dc1e2dbe
commit 2228beed51
13 files changed
+1216 -7

No files matched your search

+2 -2
View File
@@ -47,7 +47,7 @@ license:
private_key_path: "../configs/rsa_private_key.pem" # RSA私钥文件路径(服务器端使用,必须保密)
public_key_path: "../configs/rsa_public_key.pem" # RSA公钥文件路径(客户端使用,可以公开)
key_size: 2048 # 密钥大小:2048或4096,默认2048
heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内为在线
heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内未上报心跳视为离线
offline_timeout: 1440 # 离线超时时间(分钟) - 24小时后为异常
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
+4
View File
@@ -75,6 +75,10 @@ errors:
"601001": "订单不存在"
"601005": "许可数量超出限制"
# 设备管理模块 (62xxxx)
cu_device:
"620001": "设备不存在或无权限访问"
# 系统通用 (90xxxx)
system:
"900001": "请求参数无效"
@@ -0,0 +1,165 @@
# 设备管理
## 概述
设备管理是为客户用户提供的功能,用户可以在客户端查看和管理自己激活授权码的设备信息。
**核心业务逻辑:**
- 用户激活授权码后,设备会在平台生成许可证记录
- 一个客户可以有多个设备(受每个授权码的`max_activations`限制)
- 设备支持解绑操作,解绑后可以用授权码在其他设备重新激活
## 相关表结构
### licenses 表(许可证表)
```sql
CREATE TABLE `licenses` (
`id` varchar(36) NOT NULL,
`license_key` varchar(200) NOT NULL,
`authorization_code_id` varchar(36) NOT NULL,
`customer_id` varchar(36) NOT NULL,
`hardware_fingerprint` varchar(200) NOT NULL,
`device_info` json DEFAULT NULL,
`activation_ip` varchar(45) DEFAULT NULL,
`status` varchar(20) NOT NULL DEFAULT 'inactive',
`activated_at` datetime(3) DEFAULT NULL,
`last_heartbeat` datetime(3) DEFAULT NULL,
`last_online_ip` varchar(45) DEFAULT NULL,
`config_updated_at` datetime(3) DEFAULT NULL,
`usage_data` json DEFAULT NULL,
`created_at` datetime(3) NOT NULL,
`updated_at` datetime(3) NOT NULL,
`deleted_at` datetime(3) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_licenses_license_key` (`license_key`),
KEY `idx_licenses_authorization_code_id` (`authorization_code_id`),
KEY `idx_licenses_customer_id` (`customer_id`),
KEY `idx_licenses_hardware_fingerprint` (`hardware_fingerprint`),
KEY `idx_licenses_status` (`status`),
KEY `idx_licenses_activated_at` (`activated_at`),
KEY `idx_licenses_last_heartbeat` (`last_heartbeat`),
KEY `idx_licenses_config_updated_at` (`config_updated_at`),
KEY `idx_licenses_created_at` (`created_at`),
KEY `idx_licenses_deleted_at` (`deleted_at`),
CONSTRAINT `fk_licenses_authorization_code` FOREIGN KEY (`authorization_code_id`) REFERENCES `authorization_codes` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `fk_licenses_customer` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
```
### authorization_codes 表(授权码表)
```sql
CREATE TABLE `authorization_codes` (
`id` varchar(36) NOT NULL,
`code` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`customer_id` varchar(36) NOT NULL,
`created_by` varchar(36) NOT NULL,
`software_id` varchar(50) DEFAULT NULL,
`description` text,
`start_date` datetime(3) NOT NULL,
`end_date` datetime(3) NOT NULL,
`deployment_type` varchar(20) NOT NULL DEFAULT 'standalone',
`encryption_type` varchar(20) DEFAULT 'standard',
`software_version` varchar(50) DEFAULT NULL,
`max_activations` bigint NOT NULL DEFAULT '1',
`is_locked` tinyint(1) NOT NULL DEFAULT '0',
`lock_reason` text,
`locked_at` datetime(3) DEFAULT NULL,
`locked_by` varchar(36) DEFAULT NULL,
`feature_config` json DEFAULT NULL,
`usage_limits` json DEFAULT NULL,
`custom_parameters` json DEFAULT NULL,
`created_at` datetime(3) NOT NULL,
`updated_at` datetime(3) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_authorization_codes_customer_id` (`customer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
```
## 配置要求
使用现有的心跳超时配置来判断设备在线状态:
```yaml
license:
heartbeat_timeout: 300 # 心跳超时时间(秒),超过此时间未上报心跳视为离线
```
## API 接口
### 1. 获取设备列表
**接口路径:** `GET /api/cu/devices`
**功能:** 根据用户token获取该客户的所有设备列表
**查询参数:**
- `page` (integer, optional): 页码,默认1
- `page_size` (integer, optional): 每页数量,默认20,最大100
- `device_name` (string, optional): 设备名称模糊搜索(匹配device_info中的name字段)
- `authorization_code_id` (string, optional): 按授权码ID筛选设备
**返回数据:**
```json
{
"code": "200",
"message": "success",
"data": {
"total": 25,
"page": 1,
"page_size": 20,
"list": [
{
"id": "license-uuid",
"device_info": {
"name": "我的电脑",
"os": "Windows 11",
"cpu": "Intel i7",
"memory": "16GB"
},
"is_online": true,
"last_online_ip": "192.168.1.100",
"last_heartbeat": "2026-01-03T10:30:00Z",
"activated_at": "2026-01-01T09:00:00Z",
"authorization_info": {
"authorization_code": "LIC-ABC123-XXXX...",
"authorization_code_id": "auth-code-uuid",
"end_date": "2026-12-31T23:59:59Z",
"description": "基础版许可"
}
}
]
}
}
```
**业务逻辑:**
1. 从用户token解析得到`customer_id`
2. 查询该客户的所有有效许可证(status='active'且未删除)
3. 支持按授权码ID筛选(可选)
4. 支持分页和设备名称模糊搜索
5. 在线状态计算:当前时间 - last_heartbeat < heartbeat_timeout(配置为300秒)
6. 授权信息通过关联`authorization_codes`表获取
### 2. 解绑设备
**接口路径:** `DELETE /api/cu/devices/{id}`
**功能:** 解绑指定设备,物理删除许可证记录
**路径参数:**
- `id` (string, required): 许可证ID
**返回数据:**
```json
{
"code": "200",
"message": "设备解绑成功"
}
```
**业务逻辑:**
1. 验证许可证ID存在且属于当前客户
2. 物理删除许可证记录(DELETE操作)
3. 删除后用户可以用该授权码在其他设备重新激活
4. `max_activations`限制检查在激活时处理,不在此接口处理
+234
View File
@@ -25,6 +25,157 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/cu/devices": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取当前登录用户的设备列表,支持分页和筛选",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"客户设备管理"
],
"summary": "获取设备列表",
"parameters": [
{
"type": "integer",
"description": "页码,默认1",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量,默认20,最大100",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "设备名称模糊搜索",
"name": "device_name",
"in": "query"
},
{
"type": "string",
"description": "按授权码ID筛选设备",
"name": "authorization_code_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "获取成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.DeviceListResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/cu/devices/{id}": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "解绑指定的设备,物理删除许可证记录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"客户设备管理"
],
"summary": "解绑设备",
"parameters": [
{
"type": "string",
"description": "许可证ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "解绑成功",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "无权限操作此设备",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "设备不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/cu/forgot-password": {
"post": {
"description": "提交手机号,发送密码重置验证码到手机",
@@ -3786,6 +3937,27 @@ const docTemplate = `{
}
}
},
"models.AuthorizationInfo": {
"type": "object",
"properties": {
"authorization_code": {
"description": "授权码",
"type": "string"
},
"authorization_code_id": {
"description": "授权码ID",
"type": "string"
},
"description": {
"description": "授权描述",
"type": "string"
},
"end_date": {
"description": "授权到期时间",
"type": "string"
}
}
},
"models.AuthorizationStats": {
"type": "object",
"properties": {
@@ -4524,6 +4696,68 @@ const docTemplate = `{
}
}
},
"models.DeviceListItem": {
"type": "object",
"properties": {
"activated_at": {
"description": "激活时间",
"type": "string"
},
"authorization_info": {
"description": "授权信息",
"allOf": [
{
"$ref": "#/definitions/models.AuthorizationInfo"
}
]
},
"device_info": {
"description": "设备信息",
"type": "object",
"additionalProperties": true
},
"id": {
"description": "许可证ID",
"type": "string"
},
"is_online": {
"description": "是否在线",
"type": "boolean"
},
"last_heartbeat": {
"description": "最后心跳时间",
"type": "string"
},
"last_online_ip": {
"description": "最后在线IP",
"type": "string"
}
}
},
"models.DeviceListResponse": {
"type": "object",
"properties": {
"list": {
"description": "设备列表",
"type": "array",
"items": {
"$ref": "#/definitions/models.DeviceListItem"
}
},
"page": {
"description": "当前页码",
"type": "integer"
},
"page_size": {
"description": "每页数量",
"type": "integer"
},
"total": {
"description": "总数量",
"type": "integer"
}
}
},
"models.EnumItem": {
"type": "object",
"properties": {
+234
View File
@@ -21,6 +21,157 @@
},
"basePath": "/",
"paths": {
"/api/cu/devices": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取当前登录用户的设备列表,支持分页和筛选",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"客户设备管理"
],
"summary": "获取设备列表",
"parameters": [
{
"type": "integer",
"description": "页码,默认1",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量,默认20,最大100",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "设备名称模糊搜索",
"name": "device_name",
"in": "query"
},
{
"type": "string",
"description": "按授权码ID筛选设备",
"name": "authorization_code_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "获取成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.DeviceListResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/cu/devices/{id}": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "解绑指定的设备,物理删除许可证记录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"客户设备管理"
],
"summary": "解绑设备",
"parameters": [
{
"type": "string",
"description": "许可证ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "解绑成功",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "无权限操作此设备",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "设备不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/cu/forgot-password": {
"post": {
"description": "提交手机号,发送密码重置验证码到手机",
@@ -3782,6 +3933,27 @@
}
}
},
"models.AuthorizationInfo": {
"type": "object",
"properties": {
"authorization_code": {
"description": "授权码",
"type": "string"
},
"authorization_code_id": {
"description": "授权码ID",
"type": "string"
},
"description": {
"description": "授权描述",
"type": "string"
},
"end_date": {
"description": "授权到期时间",
"type": "string"
}
}
},
"models.AuthorizationStats": {
"type": "object",
"properties": {
@@ -4520,6 +4692,68 @@
}
}
},
"models.DeviceListItem": {
"type": "object",
"properties": {
"activated_at": {
"description": "激活时间",
"type": "string"
},
"authorization_info": {
"description": "授权信息",
"allOf": [
{
"$ref": "#/definitions/models.AuthorizationInfo"
}
]
},
"device_info": {
"description": "设备信息",
"type": "object",
"additionalProperties": true
},
"id": {
"description": "许可证ID",
"type": "string"
},
"is_online": {
"description": "是否在线",
"type": "boolean"
},
"last_heartbeat": {
"description": "最后心跳时间",
"type": "string"
},
"last_online_ip": {
"description": "最后在线IP",
"type": "string"
}
}
},
"models.DeviceListResponse": {
"type": "object",
"properties": {
"list": {
"description": "设备列表",
"type": "array",
"items": {
"$ref": "#/definitions/models.DeviceListItem"
}
},
"page": {
"description": "当前页码",
"type": "integer"
},
"page_size": {
"description": "每页数量",
"type": "integer"
},
"total": {
"description": "总数量",
"type": "integer"
}
}
},
"models.EnumItem": {
"type": "object",
"properties": {
+152
View File
@@ -384,6 +384,21 @@ definitions:
required:
- change_type
type: object
models.AuthorizationInfo:
properties:
authorization_code:
description: 授权码
type: string
authorization_code_id:
description: 授权码ID
type: string
description:
description: 授权描述
type: string
end_date:
description: 授权到期时间
type: string
type: object
models.AuthorizationStats:
properties:
active_licenses:
@@ -901,6 +916,49 @@ definitions:
description: 符合条件的授权总数量
type: integer
type: object
models.DeviceListItem:
properties:
activated_at:
description: 激活时间
type: string
authorization_info:
allOf:
- $ref: '#/definitions/models.AuthorizationInfo'
description: 授权信息
device_info:
additionalProperties: true
description: 设备信息
type: object
id:
description: 许可证ID
type: string
is_online:
description: 是否在线
type: boolean
last_heartbeat:
description: 最后心跳时间
type: string
last_online_ip:
description: 最后在线IP
type: string
type: object
models.DeviceListResponse:
properties:
list:
description: 设备列表
items:
$ref: '#/definitions/models.DeviceListItem'
type: array
page:
description: 当前页码
type: integer
page_size:
description: 每页数量
type: integer
total:
description: 总数量
type: integer
type: object
models.EnumItem:
properties:
display:
@@ -1355,6 +1413,100 @@ info:
title: License Manager API
version: "1.0"
paths:
/api/cu/devices:
get:
consumes:
- application/json
description: 获取当前登录用户的设备列表,支持分页和筛选
parameters:
- description: 页码,默认1
in: query
name: page
type: integer
- description: 每页数量,默认20,最大100
in: query
name: page_size
type: integer
- description: 设备名称模糊搜索
in: query
name: device_name
type: string
- description: 按授权码ID筛选设备
in: query
name: authorization_code_id
type: string
produces:
- application/json
responses:
"200":
description: 获取成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.DeviceListResponse'
type: object
"400":
description: 请求参数无效
schema:
$ref: '#/definitions/models.ErrorResponse'
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 获取设备列表
tags:
- 客户设备管理
/api/cu/devices/{id}:
delete:
consumes:
- application/json
description: 解绑指定的设备,物理删除许可证记录
parameters:
- description: 许可证ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: 解绑成功
schema:
$ref: '#/definitions/models.APIResponse'
"400":
description: 请求参数无效
schema:
$ref: '#/definitions/models.ErrorResponse'
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"403":
description: 无权限操作此设备
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: 设备不存在
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 解绑设备
tags:
- 客户设备管理
/api/cu/forgot-password:
post:
consumes:
@@ -0,0 +1,154 @@
package handlers
import (
"net/http"
"license-manager/internal/api/middleware"
"license-manager/internal/models"
"license-manager/internal/service"
"license-manager/pkg/i18n"
"license-manager/pkg/utils"
"github.com/gin-gonic/gin"
)
type CuDeviceHandler struct {
cuDeviceService service.CuDeviceService
}
func NewCuDeviceHandler(cuDeviceService service.CuDeviceService) *CuDeviceHandler {
return &CuDeviceHandler{
cuDeviceService: cuDeviceService,
}
}
// GetDevices 获取设备列表
// @Summary 获取设备列表
// @Description 获取当前登录用户的设备列表,支持分页和筛选
// @Tags 客户设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码,默认1"
// @Param page_size query int false "每页数量,默认20,最大100"
// @Param device_name query string false "设备名称模糊搜索"
// @Param authorization_code_id query string false "按授权码ID筛选设备"
// @Success 200 {object} models.APIResponse{data=models.DeviceListResponse} "获取成功"
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/cu/devices [get]
func (h *CuDeviceHandler) GetDevices(c *gin.Context) {
// 从JWT Token中获取用户ID和客户ID
claims := c.MustGet("cu_user").(*utils.CuClaims)
// 解析查询参数
var req models.DeviceListRequest
if err := c.ShouldBindQuery(&req); err != nil {
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
// 调用服务层
result, err := h.cuDeviceService.GetDeviceList(c.Request.Context(), claims.CustomerID, &req)
if err != nil {
// err已经是i18n.I18nError,直接使用
i18nErr, ok := err.(*i18n.I18nError)
if ok {
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
Code: i18nErr.Code,
Message: i18nErr.Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 如果不是i18n错误,使用通用错误
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMessage := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMessage,
Data: result,
})
}
// UnbindDevice 解绑设备
// @Summary 解绑设备
// @Description 解绑指定的设备,物理删除许可证记录
// @Tags 客户设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "许可证ID"
// @Success 200 {object} models.APIResponse "解绑成功"
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 403 {object} models.ErrorResponse "无权限操作此设备"
// @Failure 404 {object} models.ErrorResponse "设备不存在"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/cu/devices/{id} [delete]
func (h *CuDeviceHandler) UnbindDevice(c *gin.Context) {
// 从JWT Token中获取用户ID和客户ID
claims := c.MustGet("cu_user").(*utils.CuClaims)
// 获取路径参数
licenseID := c.Param("id")
if licenseID == "" {
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": license id is required",
Timestamp: getCurrentTimestamp(),
})
return
}
// 调用服务层
err := h.cuDeviceService.UnbindDevice(c.Request.Context(), claims.CustomerID, licenseID)
if err != nil {
// err已经是i18n.I18nError,直接使用
i18nErr, ok := err.(*i18n.I18nError)
if ok {
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
Code: i18nErr.Code,
Message: i18nErr.Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 如果不是i18n错误,使用通用错误
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMessage := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMessage,
Data: nil,
})
}
+6
View File
@@ -54,6 +54,7 @@ func SetupRouter() *gin.Engine {
cuUserService := service.NewCuUserService(cuUserRepo, customerRepo, db)
authCodeService := service.NewAuthorizationCodeService(authCodeRepo, customerRepo, licenseRepo)
cuOrderService := service.NewCuOrderService(cuOrderRepo, cuUserRepo, authCodeRepo, db)
cuDeviceService := service.NewCuDeviceService(licenseRepo)
enumService := service.NewEnumService()
licenseService := service.NewLicenseService(licenseRepo, db, log)
dashboardService := service.NewDashboardService(dashboardRepo)
@@ -65,6 +66,7 @@ func SetupRouter() *gin.Engine {
cuAuthHandler := handlers.NewCuAuthHandler(cuUserService)
cuProfileHandler := handlers.NewCuProfileHandler(cuUserService)
cuOrderHandler := handlers.NewCuOrderHandler(cuOrderService)
cuDeviceHandler := handlers.NewCuDeviceHandler(cuDeviceService)
enumHandler := handlers.NewEnumHandler(enumService)
authCodeHandler := handlers.NewAuthorizationCodeHandler(authCodeService)
licenseHandler := handlers.NewLicenseHandler(licenseService)
@@ -170,6 +172,10 @@ func SetupRouter() *gin.Engine {
cuAuth.POST("/orders", cuOrderHandler.CreateOrder)
cuAuth.GET("/orders/:order_id", cuOrderHandler.GetOrder)
cuAuth.GET("/orders", cuOrderHandler.GetUserOrders)
// 设备管理
cuAuth.GET("/devices", cuDeviceHandler.GetDevices)
cuAuth.DELETE("/devices/:id", cuDeviceHandler.UnbindDevice)
}
}
+35
View File
@@ -183,3 +183,38 @@ type GrowthRate struct {
AuthCodes float64 `json:"auth_codes"` // 授权码增长率(%)(当前总授权码数相比一个月前总授权码数的增长率)
Licenses float64 `json:"licenses"` // 许可证增长率(%)(当前活跃许可证数相比一个月前活跃许可证数的增长率)
}
// DeviceListRequest 设备列表查询请求结构(客户用户接口)
type DeviceListRequest struct {
Page int `form:"page" binding:"omitempty,min=1"` // 页码,默认1
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"` // 每页数量,默认20,最大100
DeviceName string `form:"device_name" binding:"omitempty,max=100"` // 设备名称模糊搜索(匹配device_info.name)
AuthorizationCodeID string `form:"authorization_code_id" binding:"omitempty,len=36"` // 按授权码ID筛选设备
}
// DeviceListItem 设备列表项结构(客户用户接口)
type DeviceListItem struct {
ID string `json:"id"` // 许可证ID
DeviceInfo map[string]interface{} `json:"device_info"` // 设备信息
IsOnline bool `json:"is_online"` // 是否在线
LastOnlineIP *string `json:"last_online_ip"` // 最后在线IP
LastHeartbeat *string `json:"last_heartbeat"` // 最后心跳时间
ActivatedAt *string `json:"activated_at"` // 激活时间
AuthorizationInfo AuthorizationInfo `json:"authorization_info"` // 授权信息
}
// AuthorizationInfo 授权信息结构
type AuthorizationInfo struct {
AuthorizationCode string `json:"authorization_code"` // 授权码
AuthorizationCodeID string `json:"authorization_code_id"` // 授权码ID
EndDate string `json:"end_date"` // 授权到期时间
Description string `json:"description"` // 授权描述
}
// DeviceListResponse 设备列表响应结构(客户用户接口)
type DeviceListResponse struct {
List []DeviceListItem `json:"list"` // 设备列表
Total int64 `json:"total"` // 总数量
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页数量
}
@@ -112,6 +112,15 @@ type LicenseRepository interface {
// GetActiveLicenseCount 获取指定授权码的激活许可证数量
GetActiveLicenseCount(ctx context.Context, authCodeID string) (int64, error)
// GetCustomerDeviceList 查询客户设备列表(关联授权码信息)
GetCustomerDeviceList(ctx context.Context, customerID string, req *models.DeviceListRequest) (*models.DeviceListResponse, error)
// DeleteLicenseByID 根据ID删除许可证(物理删除,用于设备解绑)
DeleteLicenseByID(ctx context.Context, id string) error
// CheckLicenseBelongsToCustomer 检查许可证是否属于指定客户
CheckLicenseBelongsToCustomer(ctx context.Context, licenseID, customerID string) (bool, error)
}
// DashboardRepository 仪表盘数据访问接口
@@ -7,6 +7,7 @@ import (
"gorm.io/gorm"
"license-manager/internal/config"
"license-manager/internal/models"
)
@@ -155,10 +156,10 @@ func (r *licenseRepository) GetLicenseList(ctx context.Context, req *models.Lice
// GetLicenseByID 根据ID获取许可证信息
func (r *licenseRepository) GetLicenseByID(ctx context.Context, id string) (*models.License, error) {
var license models.License
err := r.db.Preload("AuthorizationCode").Preload("Customer").
Where("id = ?", id).First(&license).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrLicenseNotFound
@@ -224,10 +225,10 @@ func (r *licenseRepository) GetAuthorizationCodeByCode(ctx context.Context, code
// GetLicenseByKey 根据许可证密钥获取许可证信息
func (r *licenseRepository) GetLicenseByKey(ctx context.Context, licenseKey string) (*models.License, error) {
var license models.License
err := r.db.Preload("AuthorizationCode").Preload("Customer").
Where("license_key = ?", licenseKey).First(&license).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrLicenseNotFound
@@ -251,4 +252,146 @@ func (r *licenseRepository) GetActiveLicenseCount(ctx context.Context, authCodeI
Where("authorization_code_id = ? AND status = ?", authCodeID, "active").
Count(&count).Error
return count, err
}
}
// GetCustomerDeviceList 查询客户设备列表(关联授权码信息)
func (r *licenseRepository) GetCustomerDeviceList(ctx context.Context, customerID string, req *models.DeviceListRequest) (*models.DeviceListResponse, error) {
// 设置默认值
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
req.PageSize = 100
}
// 构建查询
query := r.db.Model(&models.License{}).
Select(`licenses.id, licenses.device_info, licenses.last_online_ip,
licenses.activated_at, licenses.last_heartbeat,
authorization_codes.code as authorization_code,
authorization_codes.id as authorization_code_id,
authorization_codes.end_date, authorization_codes.description`).
Joins("LEFT JOIN authorization_codes ON licenses.authorization_code_id = authorization_codes.id").
Where("licenses.customer_id = ? AND licenses.status = ? AND licenses.deleted_at IS NULL",
customerID, "active")
// 按授权码ID筛选
if req.AuthorizationCodeID != "" {
query = query.Where("licenses.authorization_code_id = ?", req.AuthorizationCodeID)
}
// 设备名称模糊搜索
if req.DeviceName != "" {
query = query.Where("JSON_EXTRACT(licenses.device_info, '$.name') LIKE ?", "%"+req.DeviceName+"%")
}
// 获取总数
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, err
}
// 定义扫描结构体
type deviceScanResult struct {
ID string `json:"id"`
DeviceInfo string `json:"device_info"`
LastOnlineIP *string `json:"last_online_ip"`
ActivatedAt *time.Time `json:"activated_at"`
LastHeartbeat *time.Time `json:"last_heartbeat"`
AuthorizationCode string `json:"authorization_code"`
AuthorizationCodeID string `json:"authorization_code_id"`
EndDate time.Time `json:"end_date"`
Description *string `json:"description"`
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
var scanResults []deviceScanResult
err := query.Order("licenses.activated_at DESC").
Offset(offset).Limit(req.PageSize).
Scan(&scanResults).Error
if err != nil {
return nil, err
}
// 获取心跳超时配置(秒)
cfg := config.GetConfig()
heartbeatTimeoutSeconds := cfg.License.HeartbeatTimeout
if heartbeatTimeoutSeconds <= 0 {
heartbeatTimeoutSeconds = 300 // 默认5分钟
}
now := time.Now()
devices := make([]models.DeviceListItem, len(scanResults))
for i, result := range scanResults {
// 解析device_info JSON
var deviceInfo map[string]interface{}
if result.DeviceInfo != "" {
// 这里需要JSON解析,暂时简化处理
deviceInfo = map[string]interface{}{"raw": result.DeviceInfo}
}
// 计算在线状态(使用心跳超时配置)
isOnline := false
if result.LastHeartbeat != nil && result.LastHeartbeat.After(now.Add(-time.Duration(heartbeatTimeoutSeconds)*time.Second)) {
isOnline = true
}
// 格式化时间字段
var activatedAtStr, lastHeartbeatStr *string
if result.ActivatedAt != nil {
str := result.ActivatedAt.Format("2006-01-02T15:04:05Z")
activatedAtStr = &str
}
if result.LastHeartbeat != nil {
str := result.LastHeartbeat.Format("2006-01-02T15:04:05Z")
lastHeartbeatStr = &str
}
// 构建授权信息
description := ""
if result.Description != nil {
description = *result.Description
}
devices[i] = models.DeviceListItem{
ID: result.ID,
DeviceInfo: deviceInfo,
IsOnline: isOnline,
LastOnlineIP: result.LastOnlineIP,
LastHeartbeat: lastHeartbeatStr,
ActivatedAt: activatedAtStr,
AuthorizationInfo: models.AuthorizationInfo{
AuthorizationCode: result.AuthorizationCode,
AuthorizationCodeID: result.AuthorizationCodeID,
EndDate: result.EndDate.Format("2006-01-02T15:04:05Z"),
Description: description,
},
}
}
return &models.DeviceListResponse{
List: devices,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// DeleteLicenseByID 根据ID删除许可证(物理删除,用于设备解绑)
func (r *licenseRepository) DeleteLicenseByID(ctx context.Context, id string) error {
return r.db.Unscoped().Delete(&models.License{}, "id = ?", id).Error
}
// CheckLicenseBelongsToCustomer 检查许可证是否属于指定客户
func (r *licenseRepository) CheckLicenseBelongsToCustomer(ctx context.Context, licenseID, customerID string) (bool, error) {
var count int64
err := r.db.Model(&models.License{}).
Where("id = ? AND customer_id = ? AND deleted_at IS NULL", licenseID, customerID).
Count(&count).Error
return count > 0, err
}
@@ -0,0 +1,64 @@
package service
import (
"context"
"license-manager/internal/models"
"license-manager/internal/repository"
pkgcontext "license-manager/pkg/context"
"license-manager/pkg/i18n"
)
type cuDeviceService struct {
licenseRepo repository.LicenseRepository
}
// NewCuDeviceService 创建客户设备服务实例
func NewCuDeviceService(licenseRepo repository.LicenseRepository) CuDeviceService {
return &cuDeviceService{
licenseRepo: licenseRepo,
}
}
// GetDeviceList 获取设备列表
func (s *cuDeviceService) GetDeviceList(ctx context.Context, customerID string, req *models.DeviceListRequest) (*models.DeviceListResponse, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
// 参数验证
if customerID == "" {
return nil, i18n.NewI18nError("900001", lang) // 业务错误,不覆盖多语言message
}
if req == nil {
req = &models.DeviceListRequest{}
}
// 委托给Repository层进行数据访问
return s.licenseRepo.GetCustomerDeviceList(ctx, customerID, req)
}
// UnbindDevice 解绑设备
func (s *cuDeviceService) UnbindDevice(ctx context.Context, customerID, licenseID string) error {
lang := pkgcontext.GetLanguageFromContext(ctx)
// 参数验证
if customerID == "" || licenseID == "" {
return i18n.NewI18nError("900001", lang) // 业务错误,不覆盖多语言message
}
// 检查许可证是否存在且属于当前客户
belongs, err := s.licenseRepo.CheckLicenseBelongsToCustomer(ctx, licenseID, customerID)
if err != nil {
return i18n.NewI18nError("900004", lang, err.Error())
}
if !belongs {
return i18n.NewI18nError("620001", lang) // 设备不存在或无权限访问
}
// 物理删除许可证记录
err = s.licenseRepo.DeleteLicenseByID(ctx, licenseID)
if err != nil {
return i18n.NewI18nError("900004", lang, err.Error())
}
return nil
}
+9
View File
@@ -70,3 +70,12 @@ type DashboardService interface {
// 获取最近授权列表
GetRecentAuthorizations(ctx context.Context, req *models.DashboardRecentAuthorizationsRequest) (*models.DashboardRecentAuthorizationsResponse, error)
}
// CuDeviceService 客户设备服务接口
type CuDeviceService interface {
// 获取设备列表
GetDeviceList(ctx context.Context, customerID string, req *models.DeviceListRequest) (*models.DeviceListResponse, error)
// 解绑设备
UnbindDevice(ctx context.Context, customerID, licenseID string) error
}