feat: 添加设备管理模块,支持设备列表获取和设备解绑功能
This commit is contained in:
1 parent
d7dc1e2dbe
commit
2228beed51
13 files changed
+1216
-7
No files matched your search
@@ -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天内到期算即将过期
|
||||
@@ -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`限制检查在激活时处理,不在此接口处理
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user