feat: 授权变更历史、许可证增删改查、详情、下载、授权概览统计、激活许可证、心跳检查
This commit is contained in:
1 parent
3e0dd9a4cf
commit
9352b113dd
28 files changed
+5505
-55
No files matched your search
@@ -8,6 +8,15 @@
|
||||
|
||||
软件授权管理平台是一个独立的软件授权管理系统,为软件系统提供授权码生成、分发、验证和管理服务。支持在线和离线两种授权模式,基于硬件绑定的授权机制确保安全性。
|
||||
|
||||
## Demo环境
|
||||
|
||||
访问地址:[lm.cedar-v.com](http://lm.cedar-v.com)
|
||||
**登录凭证:**
|
||||
- 用户名:`admin`
|
||||
- 密码:`admin@123`
|
||||
|
||||
> 💡 这是一个当前功能的演示环境,您可以体验已开发的核心特性。我们正在持续开发中,更多功能将陆续上线。
|
||||
|
||||
## 系统界面
|
||||
|
||||
### 登录界面
|
||||
@@ -16,6 +25,11 @@
|
||||
### 管理仪表盘
|
||||

|
||||
|
||||
### 客户管理
|
||||

|
||||
|
||||
客户管理模块提供完整的客户信息管理功能,包括客户信息的增删改查、状态管理、授权关联等核心功能。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 🔧 **客户管理**:完整的客户信息管理和状态控制
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
|
||||
License Manager is an independent software licensing platform that provides license code generation, distribution, validation, and management services for IoT platforms and other software systems. It offers both online and offline licensing modes with hardware-based binding for enhanced security.
|
||||
|
||||
## Demo Environment
|
||||
|
||||
Access URL: [lm.cedar-v.com](http://lm.cedar-v.com)
|
||||
**Login Credentials:**
|
||||
- Username: `admin`
|
||||
- Password: `admin@123`
|
||||
|
||||
> 💡 This is a demo environment showcasing our current features. You can experience the implemented core functionalities while we continue active development with more features coming soon.
|
||||
|
||||
## System Interface
|
||||
|
||||
### Login Page
|
||||
@@ -16,6 +25,11 @@ License Manager is an independent software licensing platform that provides lice
|
||||
### Management Dashboard
|
||||

|
||||
|
||||
### Customer Management
|
||||

|
||||
|
||||
The customer management module provides comprehensive customer information management features, including CRUD operations for customer data, status management, and authorization associations.
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🔧 **Customer Management**: Complete customer information management with status control
|
||||
|
||||
@@ -39,4 +39,10 @@ i18n:
|
||||
enable: true # 是否启用多语言支持
|
||||
default_lang: "zh-CN" # 默认语言
|
||||
config_path: "../configs/i18n/errors" # 语言包文件路径(相对于backend目录)
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"] # 支持的语言列表
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"] # 支持的语言列表
|
||||
|
||||
license:
|
||||
encryption_key: "your-32-byte-secret-key-here!!" # 32字节许可证文件加密密钥
|
||||
heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内为在线
|
||||
offline_timeout: 1440 # 离线超时时间(分钟) - 24小时后为异常
|
||||
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
|
||||
@@ -38,4 +38,10 @@ i18n:
|
||||
enable: true # 是否启用多语言支持
|
||||
default_lang: "zh-CN" # 默认语言
|
||||
config_path: "../configs/i18n/errors" # 语言包文件路径(相对于backend目录)
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"] # 支持的语言列表
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"] # 支持的语言列表
|
||||
|
||||
license:
|
||||
encryption_key: "license-mgr-aes256-secret-key123" # 32字节许可证文件加密密钥
|
||||
heartbeat_timeout: 300 # 心跳超时时间(秒) - 5分钟内为在线
|
||||
offline_timeout: 1440 # 离线超时时间(分钟) - 24小时后为异常
|
||||
expiring_days: 30 # 即将过期天数 - 30天内到期算即将过期
|
||||
@@ -79,6 +79,24 @@ enums:
|
||||
"normal": "Normal"
|
||||
"locked": "Locked"
|
||||
"expired": "Expired"
|
||||
|
||||
license_status:
|
||||
"active": "Active"
|
||||
"inactive": "Inactive"
|
||||
"revoked": "Revoked"
|
||||
|
||||
license_online_status:
|
||||
"online": "Online"
|
||||
"offline": "Offline"
|
||||
|
||||
authorization_change_type:
|
||||
"renewal": "Renewal"
|
||||
"upgrade": "Upgrade"
|
||||
"limit_change": "Limit Change"
|
||||
"feature_toggle": "Feature Toggle"
|
||||
"lock": "Lock"
|
||||
"unlock": "Unlock"
|
||||
"other": "Other"
|
||||
|
||||
# Default error message
|
||||
default_error: "Unknown error"
|
||||
@@ -79,6 +79,24 @@ enums:
|
||||
"normal": "正常"
|
||||
"locked": "ロック済み"
|
||||
"expired": "期限切れ"
|
||||
|
||||
license_status:
|
||||
"active": "アクティブ"
|
||||
"inactive": "非アクティブ"
|
||||
"revoked": "取り消し済み"
|
||||
|
||||
license_online_status:
|
||||
"online": "オンライン"
|
||||
"offline": "オフライン"
|
||||
|
||||
authorization_change_type:
|
||||
"renewal": "更新"
|
||||
"upgrade": "アップグレード"
|
||||
"limit_change": "制限変更"
|
||||
"feature_toggle": "機能切り替え"
|
||||
"lock": "ロック"
|
||||
"unlock": "アンロック"
|
||||
"other": "その他"
|
||||
|
||||
# デフォルトエラーメッセージ
|
||||
default_error: "不明なエラー"
|
||||
@@ -79,6 +79,24 @@ enums:
|
||||
"normal": "正常"
|
||||
"locked": "已锁定"
|
||||
"expired": "已过期"
|
||||
|
||||
license_status:
|
||||
"active": "激活"
|
||||
"inactive": "未激活"
|
||||
"revoked": "已撤销"
|
||||
|
||||
license_online_status:
|
||||
"online": "在线"
|
||||
"offline": "离线"
|
||||
|
||||
authorization_change_type:
|
||||
"renewal": "续期"
|
||||
"upgrade": "升级"
|
||||
"limit_change": "限制变更"
|
||||
"feature_toggle": "功能切换"
|
||||
"lock": "锁定"
|
||||
"unlock": "解锁"
|
||||
"other": "其他"
|
||||
|
||||
# 默认错误信息
|
||||
default_error: "未知错误"
|
||||
@@ -11,27 +11,29 @@ GET /api/v1/authorization-codes
|
||||
- `customer_id` - 客户ID
|
||||
- `status` - 状态 (normal/locked/expired)
|
||||
- `page` - 页码
|
||||
- `limit` - 每页数量
|
||||
- `page_size` - 每页数量(默认20,最大100)
|
||||
- `start_date` - 创建开始时间
|
||||
- `end_date` - 创建结束时间
|
||||
- `sort` - 排序字段 (created_at/updated_at/code)
|
||||
- `order` - 排序方向 (asc/desc)
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": "uuid-string",
|
||||
"code": "LIC-COMP001-A7B9X2-C8F4",
|
||||
"customer_id": 1,
|
||||
"customer_id": "customer-uuid",
|
||||
"customer_name": "张三公司",
|
||||
"customer_name_display": "张三公司",
|
||||
"status": "normal",
|
||||
"status_display": "正常",
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-12-31",
|
||||
"start_date": "2024-01-01T00:00:00Z",
|
||||
"end_date": "2024-12-31T23:59:59Z",
|
||||
"max_activations": 10,
|
||||
"current_activations": 7,
|
||||
"deployment_type": "standalone",
|
||||
@@ -43,7 +45,8 @@ GET /api/v1/authorization-codes
|
||||
],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"limit": 20
|
||||
"page_size": 20,
|
||||
"total_pages": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -93,10 +96,10 @@ POST /api/v1/authorization-codes
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"id": "uuid-string",
|
||||
"code": "LIC-COMP001-A7B9X2-C8F4"
|
||||
}
|
||||
}
|
||||
@@ -110,17 +113,17 @@ GET /api/v1/authorization-codes/{id}
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"id": "uuid-string",
|
||||
"code": "LIC-COMP001-A7B9X2-C8F4",
|
||||
"customer_id": 1,
|
||||
"customer_id": "customer-uuid",
|
||||
"customer_name": "张三公司",
|
||||
"status": "normal",
|
||||
"status_display": "正常",
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-12-31",
|
||||
"start_date": "2024-01-01T00:00:00Z",
|
||||
"end_date": "2024-12-31T23:59:59Z",
|
||||
"max_activations": 10,
|
||||
"current_activations": 7,
|
||||
"deployment_type": "standalone",
|
||||
@@ -197,18 +200,22 @@ GET /api/v1/licenses
|
||||
- `customer_id` - 客户ID
|
||||
- `status` - 状态 (active/inactive/revoked)
|
||||
- `is_online` - 在线状态
|
||||
- `page` - 页码(默认1)
|
||||
- `page_size` - 每页数量(默认20,最大100)
|
||||
- `sort` - 排序字段 (created_at/updated_at/activated_at/last_heartbeat)
|
||||
- `order` - 排序方向 (asc/desc)
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"license_key": "LIC-DEVICE-ABC123",
|
||||
"authorization_code_id": 1,
|
||||
"id": "license-uuid",
|
||||
"license_key": "LIC-DEVICE-ABC123456789",
|
||||
"authorization_code_id": "auth-code-uuid",
|
||||
"authorization_code": "LIC-COMP001-A7B9X2-C8F4",
|
||||
"customer_name": "张三公司",
|
||||
"hardware_fingerprint": "CPU:ABC123,MB:DEF456",
|
||||
@@ -224,7 +231,8 @@ GET /api/v1/licenses
|
||||
],
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"limit": 20
|
||||
"page_size": 20,
|
||||
"total_pages": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -234,6 +242,44 @@ GET /api/v1/licenses
|
||||
GET /api/v1/licenses/{id}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "license-uuid",
|
||||
"license_key": "LIC-DEVICE-ABC123456789",
|
||||
"authorization_code_id": "auth-code-uuid",
|
||||
"authorization_code": "LIC-COMP001-A7B9X2-C8F4",
|
||||
"customer_id": "customer-uuid",
|
||||
"customer_name": "张三公司",
|
||||
"hardware_fingerprint": "CPU:ABC123,MB:DEF456,MAC:00:11:22:33:44:55",
|
||||
"device_info": {
|
||||
"cpu": "Intel i7-8700",
|
||||
"memory": "16GB",
|
||||
"disk": "512GB SSD",
|
||||
"os": "Windows 10 Pro"
|
||||
},
|
||||
"activation_ip": "192.168.1.100",
|
||||
"status": "active",
|
||||
"status_display": "激活",
|
||||
"is_online": true,
|
||||
"is_online_display": "在线",
|
||||
"activated_at": "2024-01-01T10:00:00Z",
|
||||
"last_heartbeat": "2024-01-01T14:30:00Z",
|
||||
"last_online_ip": "192.168.1.100",
|
||||
"config_updated_at": "2024-01-01T10:00:00Z",
|
||||
"usage_data": {
|
||||
"active_users": 50,
|
||||
"api_calls_today": 5000
|
||||
},
|
||||
"created_at": "2024-01-01T10:00:00Z",
|
||||
"updated_at": "2024-01-01T14:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 手动添加许可证
|
||||
```http
|
||||
POST /api/v1/licenses
|
||||
@@ -242,7 +288,7 @@ POST /api/v1/licenses
|
||||
**请求体**
|
||||
```json
|
||||
{
|
||||
"authorization_code_id": 1,
|
||||
"authorization_code_id": "auth-code-uuid",
|
||||
"hardware_fingerprint": "CPU:ABC123,MB:DEF456,MAC:00:11:22:33:44:55",
|
||||
"device_info": {
|
||||
"cpu": "Intel i7-8700",
|
||||
@@ -254,6 +300,31 @@ POST /api/v1/licenses
|
||||
}
|
||||
```
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "license-uuid",
|
||||
"license_key": "LIC-DEVICE-ABC123456789",
|
||||
"authorization_code_id": "auth-code-uuid",
|
||||
"customer_id": "customer-uuid",
|
||||
"hardware_fingerprint": "CPU:ABC123,MB:DEF456,MAC:00:11:22:33:44:55",
|
||||
"device_info": {
|
||||
"cpu": "Intel i7-8700",
|
||||
"memory": "16GB",
|
||||
"disk": "512GB SSD",
|
||||
"os": "Windows 10 Pro"
|
||||
},
|
||||
"activation_ip": "192.168.1.100",
|
||||
"status": "inactive",
|
||||
"created_at": "2024-01-01T10:00:00Z",
|
||||
"updated_at": "2024-01-01T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 撤销许可证
|
||||
```http
|
||||
PUT /api/v1/licenses/{id}/revoke
|
||||
@@ -297,10 +368,10 @@ POST /api/v1/activate
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"license_key": "LIC-DEVICE-ABC123",
|
||||
"license_key": "LIC-DEVICE-ABC123456789",
|
||||
"license_file": "base64编码的加密许可证文件",
|
||||
"heartbeat_interval": 300
|
||||
}
|
||||
@@ -315,8 +386,8 @@ POST /api/v1/heartbeat
|
||||
**请求体**
|
||||
```json
|
||||
{
|
||||
"license_key": "LIC-DEVICE-ABC123",
|
||||
"hardware_fingerprint": "CPU:ABC123,MB:DEF456",
|
||||
"license_key": "LIC-DEVICE-ABC123456789",
|
||||
"hardware_fingerprint": "CPU:ABC123,MB:DEF456,MAC:00:11:22:33:44:55",
|
||||
"config_updated_at": "2024-01-01T10:00:00Z",
|
||||
"usage_data": {
|
||||
"active_users": 50,
|
||||
@@ -329,8 +400,8 @@ POST /api/v1/heartbeat
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"status": "active",
|
||||
"config_updated": true,
|
||||
@@ -350,8 +421,8 @@ GET /api/v1/stats/overview
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"total_auth_codes": 1234,
|
||||
"active_licenses": 856,
|
||||
@@ -370,22 +441,37 @@ GET /api/v1/stats/overview
|
||||
GET /api/v1/authorization-codes/{id}/changes
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
- `page` - 页码(默认1)
|
||||
- `page_size` - 每页数量(默认20,最大100)
|
||||
- `change_type` - 变更类型筛选 (renewal/upgrade/limit_change/feature_toggle/lock/unlock/other)
|
||||
- `operator_id` - 操作人筛选
|
||||
- `start_date` - 开始时间 (YYYY-MM-DD格式)
|
||||
- `end_date` - 结束时间 (YYYY-MM-DD格式)
|
||||
- `sort` - 排序字段 (created_at/change_type)
|
||||
- `order` - 排序方向 (asc/desc)
|
||||
|
||||
**响应**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"code": "000000",
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": "change-uuid",
|
||||
"change_type": "upgrade",
|
||||
"change_type_display": "升级",
|
||||
"reason": "客户升级套餐",
|
||||
"operator_id": "user-uuid",
|
||||
"operator_name": "张三",
|
||||
"reason": "客户升级套餐",
|
||||
"created_at": "2024-01-01T10:00:00Z"
|
||||
}
|
||||
]
|
||||
],
|
||||
"total": 25,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total_pages": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -10,6 +10,69 @@ definitions:
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
models.ActivateRequest:
|
||||
properties:
|
||||
authorization_code:
|
||||
description: 授权码,必填
|
||||
type: string
|
||||
device_info:
|
||||
additionalProperties: true
|
||||
description: 设备信息,可选
|
||||
type: object
|
||||
hardware_fingerprint:
|
||||
description: 硬件指纹,必填
|
||||
type: string
|
||||
software_version:
|
||||
description: 软件版本,可选
|
||||
type: string
|
||||
required:
|
||||
- authorization_code
|
||||
- hardware_fingerprint
|
||||
type: object
|
||||
models.ActivateResponse:
|
||||
properties:
|
||||
heartbeat_interval:
|
||||
description: 心跳间隔(秒)
|
||||
type: integer
|
||||
license_file:
|
||||
description: base64编码的加密许可证文件
|
||||
type: string
|
||||
license_key:
|
||||
description: 许可证密钥
|
||||
type: string
|
||||
type: object
|
||||
models.AuthorizationChangeListItem:
|
||||
properties:
|
||||
change_type:
|
||||
type: string
|
||||
change_type_display:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
operator_id:
|
||||
type: string
|
||||
operator_name:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
type: object
|
||||
models.AuthorizationChangeListResponse:
|
||||
properties:
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/models.AuthorizationChangeListItem'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
page_size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
type: object
|
||||
models.AuthorizationCode:
|
||||
properties:
|
||||
code:
|
||||
@@ -490,6 +553,15 @@ definitions:
|
||||
description: 错误发生时间
|
||||
type: string
|
||||
type: object
|
||||
models.GrowthRate:
|
||||
properties:
|
||||
auth_codes:
|
||||
description: 授权码增长率(%)
|
||||
type: number
|
||||
licenses:
|
||||
description: 许可证增长率(%)
|
||||
type: number
|
||||
type: object
|
||||
models.HealthResponse:
|
||||
properties:
|
||||
services:
|
||||
@@ -507,6 +579,205 @@ definitions:
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
models.HeartbeatRequest:
|
||||
properties:
|
||||
config_updated_at:
|
||||
description: 客户端配置更新时间,可选
|
||||
type: string
|
||||
hardware_fingerprint:
|
||||
description: 硬件指纹,必填
|
||||
type: string
|
||||
license_key:
|
||||
description: 许可证密钥,必填
|
||||
type: string
|
||||
software_version:
|
||||
description: 软件版本,可选
|
||||
type: string
|
||||
usage_data:
|
||||
additionalProperties: true
|
||||
description: 使用数据,可选
|
||||
type: object
|
||||
required:
|
||||
- hardware_fingerprint
|
||||
- license_key
|
||||
type: object
|
||||
models.HeartbeatResponse:
|
||||
properties:
|
||||
config_updated:
|
||||
description: 配置是否有更新
|
||||
type: boolean
|
||||
heartbeat_interval:
|
||||
description: 下次心跳间隔(秒)
|
||||
type: integer
|
||||
license_file:
|
||||
description: base64编码的新许可证文件(如有更新)
|
||||
type: string
|
||||
status:
|
||||
description: 许可证状态
|
||||
type: string
|
||||
type: object
|
||||
models.License:
|
||||
properties:
|
||||
activated_at:
|
||||
type: string
|
||||
activation_ip:
|
||||
type: string
|
||||
authorization_code:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.AuthorizationCode'
|
||||
description: 关联字段(用于查询时的JOIN)
|
||||
authorization_code_id:
|
||||
type: string
|
||||
config_updated_at:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
customer:
|
||||
$ref: '#/definitions/models.Customer'
|
||||
customer_id:
|
||||
type: string
|
||||
device_info:
|
||||
type: object
|
||||
hardware_fingerprint:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
is_online:
|
||||
type: boolean
|
||||
is_online_display:
|
||||
type: string
|
||||
last_heartbeat:
|
||||
type: string
|
||||
last_online_ip:
|
||||
type: string
|
||||
license_key:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
status_display:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
usage_data:
|
||||
type: object
|
||||
type: object
|
||||
models.LicenseCreateRequest:
|
||||
properties:
|
||||
activation_ip:
|
||||
description: 激活IP,可选
|
||||
type: string
|
||||
authorization_code_id:
|
||||
description: 授权码ID,必填
|
||||
type: string
|
||||
device_info:
|
||||
additionalProperties: true
|
||||
description: 设备信息,可选
|
||||
type: object
|
||||
hardware_fingerprint:
|
||||
description: 硬件指纹,必填
|
||||
type: string
|
||||
required:
|
||||
- authorization_code_id
|
||||
- hardware_fingerprint
|
||||
type: object
|
||||
models.LicenseDetailResponse:
|
||||
properties:
|
||||
activated_at:
|
||||
type: string
|
||||
activation_ip:
|
||||
type: string
|
||||
authorization_code:
|
||||
type: string
|
||||
authorization_code_id:
|
||||
type: string
|
||||
config_updated_at:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
customer_id:
|
||||
type: string
|
||||
customer_name:
|
||||
type: string
|
||||
device_info:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
hardware_fingerprint:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
is_online:
|
||||
type: boolean
|
||||
is_online_display:
|
||||
type: string
|
||||
last_heartbeat:
|
||||
type: string
|
||||
last_online_ip:
|
||||
type: string
|
||||
license_key:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
status_display:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
usage_data:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type: object
|
||||
models.LicenseListItem:
|
||||
properties:
|
||||
activated_at:
|
||||
type: string
|
||||
activation_ip:
|
||||
type: string
|
||||
authorization_code:
|
||||
type: string
|
||||
authorization_code_id:
|
||||
type: string
|
||||
customer_name:
|
||||
type: string
|
||||
hardware_fingerprint:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
is_online:
|
||||
type: boolean
|
||||
is_online_display:
|
||||
type: string
|
||||
last_heartbeat:
|
||||
type: string
|
||||
last_online_ip:
|
||||
type: string
|
||||
license_key:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
status_display:
|
||||
type: string
|
||||
type: object
|
||||
models.LicenseListResponse:
|
||||
properties:
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/models.LicenseListItem'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
page_size:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
type: object
|
||||
models.LicenseRevokeRequest:
|
||||
properties:
|
||||
reason:
|
||||
description: 撤销原因,可选
|
||||
maxLength: 500
|
||||
type: string
|
||||
type: object
|
||||
models.LoginRequest:
|
||||
properties:
|
||||
password:
|
||||
@@ -525,6 +796,25 @@ definitions:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
models.StatsOverviewResponse:
|
||||
properties:
|
||||
abnormal_alerts:
|
||||
description: 异常告警数量
|
||||
type: integer
|
||||
active_licenses:
|
||||
description: 活跃许可证数量
|
||||
type: integer
|
||||
expiring_soon:
|
||||
description: 即将过期数量
|
||||
type: integer
|
||||
growth_rate:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.GrowthRate'
|
||||
description: 增长率
|
||||
total_auth_codes:
|
||||
description: 总授权码数量
|
||||
type: integer
|
||||
type: object
|
||||
models.SystemInfo:
|
||||
properties:
|
||||
arch:
|
||||
@@ -942,6 +1232,53 @@ paths:
|
||||
summary: 获取指定类型的枚举值
|
||||
tags:
|
||||
- 枚举管理
|
||||
/api/v1/activate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 客户端使用授权码激活软件,获取许可证文件
|
||||
parameters:
|
||||
- description: 激活请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.ActivateRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 激活成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.ActivateResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"404":
|
||||
description: 授权码不存在
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"409":
|
||||
description: 授权码已锁定或已过期
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"429":
|
||||
description: 激活数量已达上限
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
summary: 激活许可证
|
||||
tags:
|
||||
- 许可证激活
|
||||
/api/v1/admin/system/info:
|
||||
get:
|
||||
consumes:
|
||||
@@ -1252,6 +1589,91 @@ paths:
|
||||
summary: 更新授权码
|
||||
tags:
|
||||
- 授权码管理
|
||||
/api/v1/authorization-codes/{id}/changes:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 分页查询指定授权码的变更历史,支持筛选
|
||||
parameters:
|
||||
- description: 授权码ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: 页码,默认1
|
||||
in: query
|
||||
minimum: 1
|
||||
name: page
|
||||
type: integer
|
||||
- description: 每页条数,默认20,最大100
|
||||
in: query
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 变更类型筛选
|
||||
in: query
|
||||
name: change_type
|
||||
type: string
|
||||
- description: 操作人筛选
|
||||
in: query
|
||||
name: operator_id
|
||||
type: string
|
||||
- description: 开始时间 (YYYY-MM-DD格式)
|
||||
in: query
|
||||
name: start_date
|
||||
type: string
|
||||
- description: 结束时间 (YYYY-MM-DD格式)
|
||||
in: query
|
||||
name: end_date
|
||||
type: string
|
||||
- description: 排序字段,默认created_at
|
||||
enum:
|
||||
- created_at
|
||||
- change_type
|
||||
in: query
|
||||
name: sort
|
||||
type: string
|
||||
- description: 排序方向,默认desc
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
in: query
|
||||
name: order
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 查询成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.AuthorizationChangeListResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
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/v1/authorization-codes/{id}/lock:
|
||||
put:
|
||||
consumes:
|
||||
@@ -1302,6 +1724,316 @@ paths:
|
||||
summary: 锁定/解锁授权码
|
||||
tags:
|
||||
- 授权码管理
|
||||
/api/v1/heartbeat:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 客户端定期发送心跳,更新在线状态和使用数据
|
||||
parameters:
|
||||
- description: 心跳请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.HeartbeatRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 心跳成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.HeartbeatResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"404":
|
||||
description: 许可证不存在
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"409":
|
||||
description: 许可证已被撤销
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
summary: 心跳检测
|
||||
tags:
|
||||
- 许可证激活
|
||||
/api/v1/licenses:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 分页查询许可证列表,支持筛选
|
||||
parameters:
|
||||
- description: 页码,默认1
|
||||
in: query
|
||||
minimum: 1
|
||||
name: page
|
||||
type: integer
|
||||
- description: 每页条数,默认20,最大100
|
||||
in: query
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 授权码ID筛选
|
||||
in: query
|
||||
name: authorization_code_id
|
||||
type: string
|
||||
- description: 客户ID筛选
|
||||
in: query
|
||||
name: customer_id
|
||||
type: string
|
||||
- description: 状态筛选
|
||||
enum:
|
||||
- active
|
||||
- inactive
|
||||
- revoked
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
- description: 在线状态筛选
|
||||
in: query
|
||||
name: is_online
|
||||
type: boolean
|
||||
- description: 排序字段,默认created_at
|
||||
enum:
|
||||
- created_at
|
||||
- updated_at
|
||||
- activated_at
|
||||
- last_heartbeat
|
||||
in: query
|
||||
name: sort
|
||||
type: string
|
||||
- description: 排序方向,默认desc
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
in: query
|
||||
name: order
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 查询成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.LicenseListResponse'
|
||||
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:
|
||||
- 许可证管理
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 为指定授权码手动创建许可证
|
||||
parameters:
|
||||
- description: 许可证创建信息
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.LicenseCreateRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: 创建成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.License'
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
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/v1/licenses/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据许可证ID获取许可证详细信息
|
||||
parameters:
|
||||
- description: 许可证ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 查询成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.LicenseDetailResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
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/v1/licenses/{id}/download:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 下载加密的许可证文件,用于客户端软件激活
|
||||
parameters:
|
||||
- description: 许可证ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/octet-stream
|
||||
responses:
|
||||
"200":
|
||||
description: 许可证文件
|
||||
schema:
|
||||
type: file
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"404":
|
||||
description: 许可证不存在
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"409":
|
||||
description: 许可证已被撤销
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 下载许可证文件
|
||||
tags:
|
||||
- 许可证管理
|
||||
/api/v1/licenses/{id}/revoke:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 撤销指定的许可证,撤销后无法再使用
|
||||
parameters:
|
||||
- description: 许可证ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: 撤销原因
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.LicenseRevokeRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 撤销成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.License'
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"404":
|
||||
description: 许可证不存在
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"409":
|
||||
description: 许可证已被撤销
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 撤销许可证
|
||||
tags:
|
||||
- 许可证管理
|
||||
/api/v1/login:
|
||||
post:
|
||||
consumes:
|
||||
@@ -1357,6 +2089,36 @@ paths:
|
||||
summary: 用户登出
|
||||
tags:
|
||||
- 认证
|
||||
/api/v1/stats/overview:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取授权码、许可证的总体统计信息
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 查询成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/models.StatsOverviewResponse'
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取授权概览统计
|
||||
tags:
|
||||
- 统计分析
|
||||
/health:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@@ -427,3 +427,85 @@ func (h *AuthorizationCodeHandler) DeleteAuthorizationCode(c *gin.Context) {
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAuthorizationChangeList 查询授权变更历史列表
|
||||
// @Summary 查询授权变更历史列表
|
||||
// @Description 分页查询指定授权码的变更历史,支持筛选
|
||||
// @Tags 授权码管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "授权码ID"
|
||||
// @Param page query int false "页码,默认1" minimum(1)
|
||||
// @Param page_size query int false "每页条数,默认20,最大100" minimum(1) maximum(100)
|
||||
// @Param change_type query string false "变更类型筛选"
|
||||
// @Param operator_id query string false "操作人筛选"
|
||||
// @Param start_date query string false "开始时间 (YYYY-MM-DD格式)"
|
||||
// @Param end_date query string false "结束时间 (YYYY-MM-DD格式)"
|
||||
// @Param sort query string false "排序字段,默认created_at" Enums(created_at, change_type)
|
||||
// @Param order query string false "排序方向,默认desc" Enums(asc, desc)
|
||||
// @Success 200 {object} models.APIResponse{data=models.AuthorizationChangeListResponse} "查询成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 404 {object} models.ErrorResponse "授权码不存在"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/authorization-codes/{id}/changes [get]
|
||||
func (h *AuthorizationCodeHandler) GetAuthorizationChangeList(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.AuthorizationChangeListRequest
|
||||
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: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
data, err := h.authCodeService.GetAuthorizationChangeList(ctx, id, &req)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"license-manager/internal/api/middleware"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/service"
|
||||
"license-manager/pkg/i18n"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LicenseHandler struct {
|
||||
licenseService service.LicenseService
|
||||
}
|
||||
|
||||
// NewLicenseHandler 创建许可证处理器
|
||||
func NewLicenseHandler(licenseService service.LicenseService) *LicenseHandler {
|
||||
return &LicenseHandler{
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLicenseList 查询许可证列表
|
||||
// @Summary 查询许可证列表
|
||||
// @Description 分页查询许可证列表,支持筛选
|
||||
// @Tags 许可证管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query int false "页码,默认1" minimum(1)
|
||||
// @Param page_size query int false "每页条数,默认20,最大100" minimum(1) maximum(100)
|
||||
// @Param authorization_code_id query string false "授权码ID筛选"
|
||||
// @Param customer_id query string false "客户ID筛选"
|
||||
// @Param status query string false "状态筛选" Enums(active, inactive, revoked)
|
||||
// @Param is_online query bool false "在线状态筛选"
|
||||
// @Param sort query string false "排序字段,默认created_at" Enums(created_at, updated_at, activated_at, last_heartbeat)
|
||||
// @Param order query string false "排序方向,默认desc" Enums(asc, desc)
|
||||
// @Success 200 {object} models.APIResponse{data=models.LicenseListResponse} "查询成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/licenses [get]
|
||||
func (h *LicenseHandler) GetLicenseList(c *gin.Context) {
|
||||
var req models.LicenseListRequest
|
||||
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: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
data, err := h.licenseService.GetLicenseList(ctx, &req)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// GetLicense 获取许可证详情
|
||||
// @Summary 获取许可证详情
|
||||
// @Description 根据许可证ID获取许可证详细信息
|
||||
// @Tags 许可证管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "许可证ID"
|
||||
// @Success 200 {object} models.APIResponse{data=models.LicenseDetailResponse} "查询成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 404 {object} models.ErrorResponse "许可证不存在"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/licenses/{id} [get]
|
||||
func (h *LicenseHandler) GetLicense(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
data, err := h.licenseService.GetLicense(ctx, id)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateLicense 手动添加许可证
|
||||
// @Summary 手动添加许可证
|
||||
// @Description 为指定授权码手动创建许可证
|
||||
// @Tags 许可证管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body models.LicenseCreateRequest true "许可证创建信息"
|
||||
// @Success 201 {object} models.APIResponse{data=models.License} "创建成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 404 {object} models.ErrorResponse "授权码不存在"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/licenses [post]
|
||||
func (h *LicenseHandler) CreateLicense(c *gin.Context) {
|
||||
var req models.LicenseCreateRequest
|
||||
if err := c.ShouldBindJSON(&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: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
data, err := h.licenseService.CreateLicense(ctx, &req)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusCreated, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// RevokeLicense 撤销许可证
|
||||
// @Summary 撤销许可证
|
||||
// @Description 撤销指定的许可证,撤销后无法再使用
|
||||
// @Tags 许可证管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "许可证ID"
|
||||
// @Param request body models.LicenseRevokeRequest true "撤销原因"
|
||||
// @Success 200 {object} models.APIResponse{data=models.License} "撤销成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 404 {object} models.ErrorResponse "许可证不存在"
|
||||
// @Failure 409 {object} models.ErrorResponse "许可证已被撤销"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/licenses/{id}/revoke [put]
|
||||
func (h *LicenseHandler) RevokeLicense(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.LicenseRevokeRequest
|
||||
if err := c.ShouldBindJSON(&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: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
data, err := h.licenseService.RevokeLicense(ctx, id, &req)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadLicenseFile 下载许可证文件
|
||||
// @Summary 下载许可证文件
|
||||
// @Description 下载加密的许可证文件,用于客户端软件激活
|
||||
// @Tags 许可证管理
|
||||
// @Accept json
|
||||
// @Produce application/octet-stream
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "许可证ID"
|
||||
// @Success 200 {file} binary "许可证文件"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 404 {object} models.ErrorResponse "许可证不存在"
|
||||
// @Failure 409 {object} models.ErrorResponse "许可证已被撤销"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/licenses/{id}/download [get]
|
||||
func (h *LicenseHandler) DownloadLicenseFile(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
fileData, fileName, err := h.licenseService.GenerateLicenseFile(ctx, id)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头,指示这是一个文件下载
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Cache-Control", "must-revalidate")
|
||||
c.Header("Pragma", "public")
|
||||
|
||||
// 返回文件内容
|
||||
c.Data(http.StatusOK, "application/octet-stream", fileData)
|
||||
}
|
||||
|
||||
// ActivateLicense 激活许可证
|
||||
// @Summary 激活许可证
|
||||
// @Description 客户端使用授权码激活软件,获取许可证文件
|
||||
// @Tags 许可证激活
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.ActivateRequest true "激活请求"
|
||||
// @Success 200 {object} models.APIResponse{data=models.ActivateResponse} "激活成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 404 {object} models.ErrorResponse "授权码不存在"
|
||||
// @Failure 409 {object} models.ErrorResponse "授权码已锁定或已过期"
|
||||
// @Failure 429 {object} models.ErrorResponse "激活数量已达上限"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/activate [post]
|
||||
func (h *LicenseHandler) ActivateLicense(c *gin.Context) {
|
||||
var req models.ActivateRequest
|
||||
if err := c.ShouldBindJSON(&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: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
// 获取客户端IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
data, err := h.licenseService.ActivateLicense(ctx, &req, clientIP)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Heartbeat 心跳检测
|
||||
// @Summary 心跳检测
|
||||
// @Description 客户端定期发送心跳,更新在线状态和使用数据
|
||||
// @Tags 许可证激活
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.HeartbeatRequest true "心跳请求"
|
||||
// @Success 200 {object} models.APIResponse{data=models.HeartbeatResponse} "心跳成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 404 {object} models.ErrorResponse "许可证不存在"
|
||||
// @Failure 409 {object} models.ErrorResponse "许可证已被撤销"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/heartbeat [post]
|
||||
func (h *LicenseHandler) Heartbeat(c *gin.Context) {
|
||||
var req models.HeartbeatRequest
|
||||
if err := c.ShouldBindJSON(&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: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
// 获取客户端IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
data, err := h.licenseService.Heartbeat(ctx, &req, clientIP)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStatsOverview 获取授权概览统计
|
||||
// @Summary 获取授权概览统计
|
||||
// @Description 获取授权码、许可证的总体统计信息
|
||||
// @Tags 统计分析
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} models.APIResponse{data=models.StatsOverviewResponse} "查询成功"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/stats/overview [get]
|
||||
func (h *LicenseHandler) GetStatsOverview(c *gin.Context) {
|
||||
// 设置语言到Context中
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
|
||||
data, err := h.licenseService.GetStatsOverview(ctx)
|
||||
if err != nil {
|
||||
// 错误已经在Service层完全包装好了,直接使用
|
||||
var i18nErr *i18n.I18nError
|
||||
if errors.As(err, &i18nErr) {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
// 兜底:理论上不应该到这里,因为Service层应该返回I18nError
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"license-manager/internal/database"
|
||||
"license-manager/internal/repository"
|
||||
"license-manager/internal/service"
|
||||
"license-manager/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
@@ -38,6 +39,10 @@ func SetupRouter() *gin.Engine {
|
||||
customerRepo := repository.NewCustomerRepository(db)
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
authCodeRepo := repository.NewAuthorizationCodeRepository(db)
|
||||
licenseRepo := repository.NewLicenseRepository(db)
|
||||
|
||||
// 获取logger实例
|
||||
log := logger.GetLogger()
|
||||
|
||||
// 初始化服务层
|
||||
authService := service.NewAuthService(userRepo)
|
||||
@@ -45,6 +50,7 @@ func SetupRouter() *gin.Engine {
|
||||
customerService := service.NewCustomerService(customerRepo)
|
||||
enumService := service.NewEnumService()
|
||||
authCodeService := service.NewAuthorizationCodeService(authCodeRepo)
|
||||
licenseService := service.NewLicenseService(licenseRepo, db, log)
|
||||
|
||||
// 初始化处理器层
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
@@ -52,6 +58,7 @@ func SetupRouter() *gin.Engine {
|
||||
customerHandler := handlers.NewCustomerHandler(customerService)
|
||||
enumHandler := handlers.NewEnumHandler(enumService)
|
||||
authCodeHandler := handlers.NewAuthorizationCodeHandler(authCodeService)
|
||||
licenseHandler := handlers.NewLicenseHandler(licenseService)
|
||||
|
||||
// 健康检测接口(无需认证)
|
||||
router.GET("/health", systemHandler.HealthCheck)
|
||||
@@ -66,6 +73,10 @@ func SetupRouter() *gin.Engine {
|
||||
public := api.Group("")
|
||||
{
|
||||
public.POST("/v1/login", authHandler.Login)
|
||||
|
||||
// 许可证激活接口(客户端软件使用)
|
||||
public.POST("/v1/activate", licenseHandler.ActivateLicense)
|
||||
public.POST("/v1/heartbeat", licenseHandler.Heartbeat)
|
||||
}
|
||||
|
||||
// 需要认证的接口
|
||||
@@ -95,6 +106,17 @@ func SetupRouter() *gin.Engine {
|
||||
auth.PUT("/v1/authorization-codes/:id", authCodeHandler.UpdateAuthorizationCode)
|
||||
auth.PUT("/v1/authorization-codes/:id/lock", authCodeHandler.LockUnlockAuthorizationCode)
|
||||
auth.DELETE("/v1/authorization-codes/:id", authCodeHandler.DeleteAuthorizationCode)
|
||||
auth.GET("/v1/authorization-codes/:id/changes", authCodeHandler.GetAuthorizationChangeList)
|
||||
|
||||
// 许可证管理
|
||||
auth.GET("/v1/licenses", licenseHandler.GetLicenseList)
|
||||
auth.GET("/v1/licenses/:id", licenseHandler.GetLicense)
|
||||
auth.POST("/v1/licenses", licenseHandler.CreateLicense)
|
||||
auth.PUT("/v1/licenses/:id/revoke", licenseHandler.RevokeLicense)
|
||||
auth.GET("/v1/licenses/:id/download", licenseHandler.DownloadLicenseFile)
|
||||
|
||||
// 统计分析
|
||||
auth.GET("/v1/stats/overview", licenseHandler.GetStatsOverview)
|
||||
}
|
||||
|
||||
// 管理员接口
|
||||
|
||||
@@ -13,6 +13,7 @@ type Config struct {
|
||||
Auth AuthConfig `mapstructure:"auth"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
I18n I18nConfig `mapstructure:"i18n"`
|
||||
License LicenseConfig `mapstructure:"license"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -71,6 +72,13 @@ type I18nConfig struct {
|
||||
SupportLangs []string `mapstructure:"support_langs"` // 支持的语言列表
|
||||
}
|
||||
|
||||
type LicenseConfig struct {
|
||||
EncryptionKey string `mapstructure:"encryption_key"` // 许可证文件加密密钥
|
||||
HeartbeatTimeout int `mapstructure:"heartbeat_timeout"` // 心跳超时时间(秒)
|
||||
OfflineTimeout int `mapstructure:"offline_timeout"` // 离线超时时间(分钟)
|
||||
ExpiringDays int `mapstructure:"expiring_days"` // 即将过期天数
|
||||
}
|
||||
|
||||
var AppConfig *Config
|
||||
|
||||
func Load(configPath string) error {
|
||||
@@ -144,6 +152,12 @@ func setDefaults() {
|
||||
viper.SetDefault("i18n.default_lang", "zh-CN")
|
||||
viper.SetDefault("i18n.config_path", "../configs/i18n/errors")
|
||||
viper.SetDefault("i18n.support_langs", []string{"zh-CN", "en-US", "ja-JP"})
|
||||
|
||||
// License defaults
|
||||
viper.SetDefault("license.encryption_key", "license-manager-secret-key-32b")
|
||||
viper.SetDefault("license.heartbeat_timeout", 300)
|
||||
viper.SetDefault("license.offline_timeout", 1440)
|
||||
viper.SetDefault("license.expiring_days", 30)
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
|
||||
@@ -24,6 +24,8 @@ func AutoMigrate() error {
|
||||
&models.CustomerCodeSequence{},
|
||||
&models.User{},
|
||||
&models.AuthorizationCode{},
|
||||
&models.License{},
|
||||
&models.AuthorizationChange{},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate database: %w", err)
|
||||
|
||||
@@ -175,4 +175,71 @@ type AuthorizationCodeLockRequest struct {
|
||||
IsLocked bool `json:"is_locked" binding:""` // true-锁定,false-解锁
|
||||
LockReason *string `json:"lock_reason" binding:"omitempty,max=500"` // 锁定原因
|
||||
Reason *string `json:"reason" binding:"omitempty,max=500"` // 变更原因(记录到变更历史)
|
||||
}
|
||||
|
||||
// AuthorizationChange 授权变更历史模型
|
||||
type AuthorizationChange struct {
|
||||
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
|
||||
AuthorizationCodeID string `gorm:"type:varchar(36);not null;index" json:"authorization_code_id"`
|
||||
ChangeType string `gorm:"type:varchar(30);not null;index" json:"change_type"`
|
||||
ChangeTypeDisplay string `gorm:"-" json:"change_type_display,omitempty"`
|
||||
OldConfig JSON `gorm:"type:json" json:"old_config,omitempty" swaggertype:"object"`
|
||||
NewConfig JSON `gorm:"type:json" json:"new_config,omitempty" swaggertype:"object"`
|
||||
OperatorID string `gorm:"type:varchar(36);not null;index" json:"operator_id"`
|
||||
OperatorName string `gorm:"-" json:"operator_name,omitempty"`
|
||||
Reason *string `gorm:"type:text" json:"reason"`
|
||||
CreatedAt time.Time `gorm:"type:datetime(3);not null;index" json:"created_at"`
|
||||
|
||||
// 关联字段
|
||||
AuthorizationCode *AuthorizationCode `gorm:"foreignKey:AuthorizationCodeID" json:"authorization_code,omitempty"`
|
||||
Operator *User `gorm:"foreignKey:OperatorID" json:"operator,omitempty"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (AuthorizationChange) TableName() string {
|
||||
return "authorization_changes"
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前自动设置时间戳和ID
|
||||
func (a *AuthorizationChange) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
if a.CreatedAt.IsZero() {
|
||||
a.CreatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizationChangeListRequest 授权变更历史列表查询请求结构
|
||||
type AuthorizationChangeListRequest struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1"` // 页码,默认1
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"` // 每页条数,默认20,最大100
|
||||
ChangeType string `form:"change_type" binding:"omitempty"` // 变更类型筛选
|
||||
OperatorID string `form:"operator_id" binding:"omitempty"` // 操作人筛选
|
||||
StartDate string `form:"start_date" binding:"omitempty"` // 开始时间
|
||||
EndDate string `form:"end_date" binding:"omitempty"` // 结束时间
|
||||
Sort string `form:"sort" binding:"omitempty,oneof=created_at change_type"` // 排序字段,默认created_at
|
||||
Order string `form:"order" binding:"omitempty,oneof=asc desc"` // 排序方向,默认desc
|
||||
}
|
||||
|
||||
// AuthorizationChangeListItem 授权变更历史列表项结构
|
||||
type AuthorizationChangeListItem struct {
|
||||
ID string `json:"id"`
|
||||
ChangeType string `json:"change_type"`
|
||||
ChangeTypeDisplay string `json:"change_type_display,omitempty"`
|
||||
OperatorID string `json:"operator_id"`
|
||||
OperatorName string `json:"operator_name,omitempty"`
|
||||
Reason *string `json:"reason"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// AuthorizationChangeListResponse 授权变更历史列表响应结构
|
||||
type AuthorizationChangeListResponse struct {
|
||||
List []AuthorizationChangeListItem `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// License 许可证模型
|
||||
type License struct {
|
||||
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
|
||||
LicenseKey string `gorm:"type:varchar(200);uniqueIndex;not null" json:"license_key"`
|
||||
AuthorizationCodeID string `gorm:"type:varchar(36);not null;index" json:"authorization_code_id"`
|
||||
CustomerID string `gorm:"type:varchar(36);not null;index" json:"customer_id"`
|
||||
HardwareFingerprint string `gorm:"type:varchar(200);not null;index" json:"hardware_fingerprint"`
|
||||
DeviceInfo JSON `gorm:"type:json" json:"device_info,omitempty" swaggertype:"object"`
|
||||
ActivationIP *string `gorm:"type:varchar(45)" json:"activation_ip"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'inactive';index" json:"status"`
|
||||
StatusDisplay string `gorm:"-" json:"status_display,omitempty"`
|
||||
ActivatedAt *time.Time `gorm:"index" json:"activated_at"`
|
||||
LastHeartbeat *time.Time `gorm:"index" json:"last_heartbeat"`
|
||||
LastOnlineIP *string `gorm:"type:varchar(45)" json:"last_online_ip"`
|
||||
ConfigUpdatedAt *time.Time `gorm:"index" json:"config_updated_at"`
|
||||
UsageData JSON `gorm:"type:json" json:"usage_data,omitempty" swaggertype:"object"`
|
||||
CreatedAt time.Time `gorm:"not null;index" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// 关联字段(用于查询时的JOIN)
|
||||
AuthorizationCode *AuthorizationCode `gorm:"foreignKey:AuthorizationCodeID" json:"authorization_code,omitempty"`
|
||||
Customer *Customer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"`
|
||||
IsOnline bool `gorm:"-" json:"is_online"`
|
||||
IsOnlineDisplay string `gorm:"-" json:"is_online_display,omitempty"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (License) TableName() string {
|
||||
return "licenses"
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前自动设置时间戳和ID
|
||||
func (l *License) BeforeCreate(tx *gorm.DB) error {
|
||||
if l.ID == "" {
|
||||
l.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if l.CreatedAt.IsZero() {
|
||||
l.CreatedAt = now
|
||||
}
|
||||
if l.UpdatedAt.IsZero() {
|
||||
l.UpdatedAt = now
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate 更新前自动刷新更新时间
|
||||
func (l *License) BeforeUpdate(tx *gorm.DB) error {
|
||||
l.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LicenseListRequest 许可证列表查询请求结构
|
||||
type LicenseListRequest struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1"` // 页码,默认1
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"` // 每页条数,默认20,最大100
|
||||
AuthorizationCodeID string `form:"authorization_code_id" binding:"omitempty"` // 授权码ID筛选
|
||||
CustomerID string `form:"customer_id" binding:"omitempty"` // 客户ID筛选
|
||||
Status string `form:"status" binding:"omitempty,oneof=active inactive revoked"` // 状态筛选
|
||||
IsOnline *bool `form:"is_online" binding:"omitempty"` // 在线状态筛选
|
||||
Sort string `form:"sort" binding:"omitempty,oneof=created_at updated_at activated_at last_heartbeat"` // 排序字段,默认created_at
|
||||
Order string `form:"order" binding:"omitempty,oneof=asc desc"` // 排序方向,默认desc
|
||||
}
|
||||
|
||||
// LicenseListItem 许可证列表项结构
|
||||
type LicenseListItem struct {
|
||||
ID string `json:"id"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
AuthorizationCodeID string `json:"authorization_code_id"`
|
||||
AuthorizationCode string `json:"authorization_code"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
HardwareFingerprint string `json:"hardware_fingerprint"`
|
||||
Status string `json:"status"`
|
||||
StatusDisplay string `json:"status_display,omitempty"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
IsOnlineDisplay string `json:"is_online_display,omitempty"`
|
||||
ActivationIP *string `json:"activation_ip"`
|
||||
LastOnlineIP *string `json:"last_online_ip"`
|
||||
ActivatedAt *string `json:"activated_at"`
|
||||
LastHeartbeat *string `json:"last_heartbeat"`
|
||||
}
|
||||
|
||||
// LicenseListResponse 许可证列表响应结构
|
||||
type LicenseListResponse struct {
|
||||
List []LicenseListItem `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// LicenseDetailResponse 许可证详情响应结构
|
||||
type LicenseDetailResponse struct {
|
||||
ID string `json:"id"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
AuthorizationCodeID string `json:"authorization_code_id"`
|
||||
AuthorizationCode string `json:"authorization_code"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
HardwareFingerprint string `json:"hardware_fingerprint"`
|
||||
DeviceInfo map[string]interface{} `json:"device_info,omitempty"`
|
||||
ActivationIP *string `json:"activation_ip"`
|
||||
Status string `json:"status"`
|
||||
StatusDisplay string `json:"status_display,omitempty"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
IsOnlineDisplay string `json:"is_online_display,omitempty"`
|
||||
ActivatedAt *string `json:"activated_at"`
|
||||
LastHeartbeat *string `json:"last_heartbeat"`
|
||||
LastOnlineIP *string `json:"last_online_ip"`
|
||||
ConfigUpdatedAt *string `json:"config_updated_at"`
|
||||
UsageData map[string]interface{} `json:"usage_data,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// LicenseCreateRequest 手动添加许可证请求结构
|
||||
type LicenseCreateRequest struct {
|
||||
AuthorizationCodeID string `json:"authorization_code_id" binding:"required"` // 授权码ID,必填
|
||||
HardwareFingerprint string `json:"hardware_fingerprint" binding:"required"` // 硬件指纹,必填
|
||||
DeviceInfo map[string]interface{} `json:"device_info,omitempty"` // 设备信息,可选
|
||||
ActivationIP *string `json:"activation_ip" binding:"omitempty,ip"` // 激活IP,可选
|
||||
}
|
||||
|
||||
// LicenseRevokeRequest 撤销许可证请求结构
|
||||
type LicenseRevokeRequest struct {
|
||||
Reason string `json:"reason" binding:"omitempty,max=500"` // 撤销原因,可选
|
||||
}
|
||||
|
||||
// ActivateRequest 软件激活请求结构
|
||||
type ActivateRequest struct {
|
||||
AuthorizationCode string `json:"authorization_code" binding:"required"` // 授权码,必填
|
||||
HardwareFingerprint string `json:"hardware_fingerprint" binding:"required"` // 硬件指纹,必填
|
||||
DeviceInfo map[string]interface{} `json:"device_info,omitempty"` // 设备信息,可选
|
||||
SoftwareVersion *string `json:"software_version" binding:"omitempty"` // 软件版本,可选
|
||||
}
|
||||
|
||||
// ActivateResponse 软件激活响应结构
|
||||
type ActivateResponse struct {
|
||||
LicenseKey string `json:"license_key"` // 许可证密钥
|
||||
LicenseFile string `json:"license_file"` // base64编码的加密许可证文件
|
||||
HeartbeatInterval int `json:"heartbeat_interval"` // 心跳间隔(秒)
|
||||
}
|
||||
|
||||
// HeartbeatRequest 心跳检测请求结构
|
||||
type HeartbeatRequest struct {
|
||||
LicenseKey string `json:"license_key" binding:"required"` // 许可证密钥,必填
|
||||
HardwareFingerprint string `json:"hardware_fingerprint" binding:"required"` // 硬件指纹,必填
|
||||
ConfigUpdatedAt *string `json:"config_updated_at,omitempty"` // 客户端配置更新时间,可选
|
||||
UsageData map[string]interface{} `json:"usage_data,omitempty"` // 使用数据,可选
|
||||
SoftwareVersion *string `json:"software_version,omitempty"` // 软件版本,可选
|
||||
}
|
||||
|
||||
// HeartbeatResponse 心跳检测响应结构
|
||||
type HeartbeatResponse struct {
|
||||
Status string `json:"status"` // 许可证状态
|
||||
ConfigUpdated bool `json:"config_updated"` // 配置是否有更新
|
||||
LicenseFile *string `json:"license_file"` // base64编码的新许可证文件(如有更新)
|
||||
HeartbeatInterval int `json:"heartbeat_interval"` // 下次心跳间隔(秒)
|
||||
}
|
||||
|
||||
// StatsOverviewResponse 授权概览统计响应结构
|
||||
type StatsOverviewResponse struct {
|
||||
TotalAuthCodes int64 `json:"total_auth_codes"` // 总授权码数量
|
||||
ActiveLicenses int64 `json:"active_licenses"` // 活跃许可证数量
|
||||
ExpiringSoon int64 `json:"expiring_soon"` // 即将过期数量
|
||||
AbnormalAlerts int64 `json:"abnormal_alerts"` // 异常告警数量
|
||||
GrowthRate GrowthRate `json:"growth_rate"` // 增长率
|
||||
}
|
||||
|
||||
// GrowthRate 增长率结构
|
||||
type GrowthRate struct {
|
||||
AuthCodes float64 `json:"auth_codes"` // 授权码增长率(%)
|
||||
Licenses float64 `json:"licenses"` // 许可证增长率(%)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
"license-manager/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -61,8 +63,7 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte
|
||||
Select(`ac.id, ac.code, ac.customer_id, c.customer_name,
|
||||
ac.start_date, ac.end_date, ac.max_activations,
|
||||
ac.deployment_type, ac.is_locked, ac.description, ac.created_at`).
|
||||
Joins("LEFT JOIN customers c ON ac.customer_id = c.id AND c.deleted_at IS NULL").
|
||||
Where("ac.deleted_at IS NULL")
|
||||
Joins("LEFT JOIN customers c ON ac.customer_id = c.id AND c.deleted_at IS NULL")
|
||||
|
||||
// 添加筛选条件
|
||||
if req.CustomerID != "" {
|
||||
@@ -157,4 +158,110 @@ func (r *authorizationCodeRepository) CheckCustomerExists(ctx context.Context, c
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetAuthorizationChangeList 查询授权变更历史列表
|
||||
func (r *authorizationCodeRepository) GetAuthorizationChangeList(ctx context.Context, authCodeID string, req *models.AuthorizationChangeListRequest) (*models.AuthorizationChangeListResponse, error) {
|
||||
// 设置默认值
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
if req.PageSize > 100 {
|
||||
req.PageSize = 100
|
||||
}
|
||||
if req.Sort == "" {
|
||||
req.Sort = "created_at"
|
||||
}
|
||||
if req.Order == "" {
|
||||
req.Order = "desc"
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := r.db.WithContext(ctx).Model(&models.AuthorizationChange{}).
|
||||
Select(`authorization_changes.id, authorization_changes.change_type,
|
||||
authorization_changes.operator_id, users.username as operator_name,
|
||||
authorization_changes.reason, authorization_changes.created_at`).
|
||||
Joins("LEFT JOIN users ON authorization_changes.operator_id = users.id").
|
||||
Where("authorization_changes.authorization_code_id = ?", authCodeID)
|
||||
|
||||
// 变更类型筛选
|
||||
if req.ChangeType != "" {
|
||||
query = query.Where("authorization_changes.change_type = ?", req.ChangeType)
|
||||
}
|
||||
|
||||
// 操作人筛选
|
||||
if req.OperatorID != "" {
|
||||
query = query.Where("authorization_changes.operator_id = ?", req.OperatorID)
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if req.StartDate != "" {
|
||||
startTime, err := time.Parse("2006-01-02", req.StartDate)
|
||||
if err == nil {
|
||||
query = query.Where("authorization_changes.created_at >= ?", startTime)
|
||||
}
|
||||
}
|
||||
if req.EndDate != "" {
|
||||
endTime, err := time.Parse("2006-01-02", req.EndDate)
|
||||
if err == nil {
|
||||
// 结束时间加一天,以包含当天的所有时间
|
||||
endTime = endTime.AddDate(0, 0, 1)
|
||||
query = query.Where("authorization_changes.created_at < ?", endTime)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 排序和分页
|
||||
orderBy := "authorization_changes." + req.Sort + " " + req.Order
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
|
||||
var changes []struct {
|
||||
ID string `json:"id"`
|
||||
ChangeType string `json:"change_type"`
|
||||
OperatorID string `json:"operator_id"`
|
||||
OperatorName *string `json:"operator_name"`
|
||||
Reason *string `json:"reason"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
if err := query.Order(orderBy).Limit(req.PageSize).Offset(offset).Scan(&changes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
list := make([]models.AuthorizationChangeListItem, len(changes))
|
||||
for i, change := range changes {
|
||||
var operatorName string
|
||||
if change.OperatorName != nil {
|
||||
operatorName = *change.OperatorName
|
||||
}
|
||||
|
||||
list[i] = models.AuthorizationChangeListItem{
|
||||
ID: change.ID,
|
||||
ChangeType: change.ChangeType,
|
||||
OperatorID: change.OperatorID,
|
||||
OperatorName: operatorName,
|
||||
Reason: change.Reason,
|
||||
CreatedAt: change.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
totalPages := int(math.Ceil(float64(total) / float64(req.PageSize)))
|
||||
|
||||
return &models.AuthorizationChangeListResponse{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
@@ -16,6 +16,13 @@ var (
|
||||
ErrAuthorizationCodeDuplicate = errors.New("authorization code already exists")
|
||||
)
|
||||
|
||||
// 许可证领域的业务错误
|
||||
var (
|
||||
ErrLicenseNotFound = errors.New("license not found")
|
||||
ErrLicenseAlreadyExists = errors.New("license already exists")
|
||||
ErrLicenseDuplicate = errors.New("license already exists")
|
||||
)
|
||||
|
||||
// 通用的数据库/系统错误(给开发者看的)
|
||||
var (
|
||||
ErrDatabaseConnection = errors.New("database connection failed")
|
||||
|
||||
@@ -66,4 +66,37 @@ type AuthorizationCodeRepository interface {
|
||||
|
||||
// CheckCustomerExists 检查客户是否存在
|
||||
CheckCustomerExists(ctx context.Context, customerID string) (bool, error)
|
||||
|
||||
// GetAuthorizationChangeList 查询授权变更历史列表
|
||||
GetAuthorizationChangeList(ctx context.Context, authCodeID string, req *models.AuthorizationChangeListRequest) (*models.AuthorizationChangeListResponse, error)
|
||||
}
|
||||
|
||||
// LicenseRepository 许可证数据访问接口
|
||||
type LicenseRepository interface {
|
||||
// GetLicenseList 查询许可证列表
|
||||
GetLicenseList(ctx context.Context, req *models.LicenseListRequest) (*models.LicenseListResponse, error)
|
||||
|
||||
// GetLicenseByID 根据ID获取许可证信息
|
||||
GetLicenseByID(ctx context.Context, id string) (*models.License, error)
|
||||
|
||||
// CreateLicense 创建许可证
|
||||
CreateLicense(ctx context.Context, license *models.License) error
|
||||
|
||||
// UpdateLicense 更新许可证信息
|
||||
UpdateLicense(ctx context.Context, license *models.License) error
|
||||
|
||||
// CheckAuthorizationCodeExists 检查授权码是否存在
|
||||
CheckAuthorizationCodeExists(ctx context.Context, authCodeID string) (bool, error)
|
||||
|
||||
// GetAuthorizationCodeByID 根据ID获取授权码信息
|
||||
GetAuthorizationCodeByID(ctx context.Context, authCodeID string) (*models.AuthorizationCode, error)
|
||||
|
||||
// GetAuthorizationCodeByCode 根据授权码获取授权码信息
|
||||
GetAuthorizationCodeByCode(ctx context.Context, code string) (*models.AuthorizationCode, error)
|
||||
|
||||
// GetLicenseByKey 根据许可证密钥获取许可证信息
|
||||
GetLicenseByKey(ctx context.Context, licenseKey string) (*models.License, error)
|
||||
|
||||
// GetActiveLicenseCount 获取指定授权码的激活许可证数量
|
||||
GetActiveLicenseCount(ctx context.Context, authCodeID string) (int64, error)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"license-manager/internal/models"
|
||||
)
|
||||
|
||||
type licenseRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewLicenseRepository 创建许可证数据访问实例
|
||||
func NewLicenseRepository(db *gorm.DB) LicenseRepository {
|
||||
return &licenseRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLicenseList 查询许可证列表
|
||||
func (r *licenseRepository) GetLicenseList(ctx context.Context, req *models.LicenseListRequest) (*models.LicenseListResponse, error) {
|
||||
// 设置默认值
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
if req.PageSize > 100 {
|
||||
req.PageSize = 100
|
||||
}
|
||||
if req.Sort == "" {
|
||||
req.Sort = "created_at"
|
||||
}
|
||||
if req.Order == "" {
|
||||
req.Order = "desc"
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := r.db.Model(&models.License{}).
|
||||
Select(`licenses.id, licenses.license_key, licenses.authorization_code_id,
|
||||
authorization_codes.code as authorization_code, customers.customer_name,
|
||||
licenses.hardware_fingerprint, licenses.status, licenses.activation_ip,
|
||||
licenses.last_online_ip, licenses.activated_at, licenses.last_heartbeat`).
|
||||
Joins("LEFT JOIN authorization_codes ON licenses.authorization_code_id = authorization_codes.id").
|
||||
Joins("LEFT JOIN customers ON licenses.customer_id = customers.id")
|
||||
|
||||
// 授权码ID筛选
|
||||
if req.AuthorizationCodeID != "" {
|
||||
query = query.Where("licenses.authorization_code_id = ?", req.AuthorizationCodeID)
|
||||
}
|
||||
|
||||
// 客户ID筛选
|
||||
if req.CustomerID != "" {
|
||||
query = query.Where("licenses.customer_id = ?", req.CustomerID)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if req.Status != "" {
|
||||
query = query.Where("licenses.status = ?", req.Status)
|
||||
}
|
||||
|
||||
// 在线状态筛选
|
||||
if req.IsOnline != nil {
|
||||
// 在线状态:最后心跳时间在5分钟内
|
||||
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||
if *req.IsOnline {
|
||||
query = query.Where("licenses.last_heartbeat >= ?", fiveMinutesAgo)
|
||||
} else {
|
||||
query = query.Where("licenses.last_heartbeat < ? OR licenses.last_heartbeat IS NULL", fiveMinutesAgo)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 排序和分页
|
||||
orderBy := "licenses." + req.Sort + " " + req.Order
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
|
||||
var licenses []struct {
|
||||
ID string `json:"id"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
AuthorizationCodeID string `json:"authorization_code_id"`
|
||||
AuthorizationCode string `json:"authorization_code"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
HardwareFingerprint string `json:"hardware_fingerprint"`
|
||||
Status string `json:"status"`
|
||||
ActivationIP *string `json:"activation_ip"`
|
||||
LastOnlineIP *string `json:"last_online_ip"`
|
||||
ActivatedAt *time.Time `json:"activated_at"`
|
||||
LastHeartbeat *time.Time `json:"last_heartbeat"`
|
||||
}
|
||||
|
||||
if err := query.Order(orderBy).Limit(req.PageSize).Offset(offset).Scan(&licenses).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
list := make([]models.LicenseListItem, len(licenses))
|
||||
for i, license := range licenses {
|
||||
// 计算在线状态
|
||||
isOnline := false
|
||||
if license.LastHeartbeat != nil {
|
||||
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||
isOnline = license.LastHeartbeat.After(fiveMinutesAgo)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
var activatedAtStr, lastHeartbeatStr *string
|
||||
if license.ActivatedAt != nil {
|
||||
str := license.ActivatedAt.Format(time.RFC3339)
|
||||
activatedAtStr = &str
|
||||
}
|
||||
if license.LastHeartbeat != nil {
|
||||
str := license.LastHeartbeat.Format(time.RFC3339)
|
||||
lastHeartbeatStr = &str
|
||||
}
|
||||
|
||||
list[i] = models.LicenseListItem{
|
||||
ID: license.ID,
|
||||
LicenseKey: license.LicenseKey,
|
||||
AuthorizationCodeID: license.AuthorizationCodeID,
|
||||
AuthorizationCode: license.AuthorizationCode,
|
||||
CustomerName: license.CustomerName,
|
||||
HardwareFingerprint: license.HardwareFingerprint,
|
||||
Status: license.Status,
|
||||
IsOnline: isOnline,
|
||||
ActivationIP: license.ActivationIP,
|
||||
LastOnlineIP: license.LastOnlineIP,
|
||||
ActivatedAt: activatedAtStr,
|
||||
LastHeartbeat: lastHeartbeatStr,
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
totalPages := int(math.Ceil(float64(total) / float64(req.PageSize)))
|
||||
|
||||
return &models.LicenseListResponse{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算在线状态
|
||||
if license.LastHeartbeat != nil {
|
||||
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||
license.IsOnline = license.LastHeartbeat.After(fiveMinutesAgo)
|
||||
}
|
||||
|
||||
return &license, nil
|
||||
}
|
||||
|
||||
// CreateLicense 创建许可证
|
||||
func (r *licenseRepository) CreateLicense(ctx context.Context, license *models.License) error {
|
||||
return r.db.Create(license).Error
|
||||
}
|
||||
|
||||
// UpdateLicense 更新许可证信息
|
||||
func (r *licenseRepository) UpdateLicense(ctx context.Context, license *models.License) error {
|
||||
return r.db.Save(license).Error
|
||||
}
|
||||
|
||||
// CheckAuthorizationCodeExists 检查授权码是否存在
|
||||
func (r *licenseRepository) CheckAuthorizationCodeExists(ctx context.Context, authCodeID string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.AuthorizationCode{}).Where("id = ?", authCodeID).Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetAuthorizationCodeByID 根据ID获取授权码信息
|
||||
func (r *licenseRepository) GetAuthorizationCodeByID(ctx context.Context, authCodeID string) (*models.AuthorizationCode, error) {
|
||||
var authCode models.AuthorizationCode
|
||||
err := r.db.Where("id = ?", authCodeID).First(&authCode).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, ErrAuthorizationCodeNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &authCode, nil
|
||||
}
|
||||
|
||||
// GetAuthorizationCodeByCode 根据授权码获取授权码信息
|
||||
func (r *licenseRepository) GetAuthorizationCodeByCode(ctx context.Context, code string) (*models.AuthorizationCode, error) {
|
||||
var authCode models.AuthorizationCode
|
||||
err := r.db.Where("code = ?", code).First(&authCode).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, ErrAuthorizationCodeNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &authCode, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算在线状态
|
||||
if license.LastHeartbeat != nil {
|
||||
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||
license.IsOnline = license.LastHeartbeat.After(fiveMinutesAgo)
|
||||
}
|
||||
|
||||
return &license, nil
|
||||
}
|
||||
|
||||
// GetActiveLicenseCount 获取指定授权码的激活许可证数量
|
||||
func (r *licenseRepository) GetActiveLicenseCount(ctx context.Context, authCodeID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.License{}).
|
||||
Where("authorization_code_id = ? AND status = ?", authCodeID, "active").
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -2,12 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/repository"
|
||||
pkgcontext "license-manager/pkg/context"
|
||||
"license-manager/pkg/i18n"
|
||||
"license-manager/pkg/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -26,42 +27,49 @@ func NewAuthService(userRepo repository.UserRepository) AuthService {
|
||||
|
||||
// Login 用户登录
|
||||
func (s *authService) Login(ctx context.Context, req *models.LoginRequest, clientIP string) (*models.LoginData, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil {
|
||||
return nil, errors.New("配置未初始化")
|
||||
return nil, i18n.NewI18nError("900004", lang, "配置未初始化")
|
||||
}
|
||||
|
||||
// 业务逻辑:参数验证
|
||||
if req == nil {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 根据用户名查找用户
|
||||
user, err := s.userRepo.GetUserByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
return nil, i18n.NewI18nError("100003", lang) // 用户名或密码错误
|
||||
}
|
||||
return nil, errors.New("查询用户失败")
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if user.Status == "disabled" {
|
||||
return nil, errors.New("账号已被禁用")
|
||||
return nil, i18n.NewI18nError("100005", lang) // 权限不足/账号已被禁用
|
||||
}
|
||||
|
||||
// 检查账号是否被锁定
|
||||
if user.IsAccountLocked() {
|
||||
return nil, errors.New("账号已被锁定,请稍后重试")
|
||||
return nil, i18n.NewI18nError("100001", lang) // 认证已过期/账号已被锁定
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !utils.CheckPasswordHash(req.Password, user.PasswordHash) {
|
||||
// 增加登录失败次数
|
||||
s.userRepo.IncrementLoginAttempts(ctx, user.ID)
|
||||
|
||||
|
||||
// 检查是否需要锁定账号
|
||||
if user.LoginAttempts >= 4 { // 第5次失败时锁定
|
||||
s.userRepo.LockUser(ctx, user.ID, 30) // 锁定30分钟
|
||||
return nil, errors.New("密码错误次数过多,账号已被锁定30分钟")
|
||||
s.userRepo.LockUser(ctx, user.ID, 30) // 锁定30分钟
|
||||
return nil, i18n.NewI18nError("100001", lang) // 密码错误次数过多,账号已被锁定
|
||||
}
|
||||
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
|
||||
return nil, i18n.NewI18nError("100003", lang) // 用户名或密码错误
|
||||
}
|
||||
|
||||
// 登录成功,重置登录失败次数并更新登录信息
|
||||
@@ -77,7 +85,7 @@ func (s *authService) Login(ctx context.Context, req *models.LoginRequest, clien
|
||||
// 生成JWT Token
|
||||
token, err := utils.GenerateToken(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
return nil, errors.New("生成令牌失败")
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 计算过期时间(秒)
|
||||
@@ -104,7 +112,7 @@ func (s *authService) RefreshToken(token string) (string, error) {
|
||||
// 生成新Token
|
||||
newToken, err := utils.GenerateToken(claims.UserID, claims.Username, claims.Role)
|
||||
if err != nil {
|
||||
return "", errors.New("生成新令牌失败")
|
||||
return "", err
|
||||
}
|
||||
|
||||
return newToken, nil
|
||||
|
||||
@@ -457,3 +457,44 @@ func (s *authorizationCodeService) generateChecksum(input string) string {
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
// GetAuthorizationChangeList 查询授权变更历史列表
|
||||
func (s *authorizationCodeService) GetAuthorizationChangeList(ctx context.Context, authCodeID string, req *models.AuthorizationChangeListRequest) (*models.AuthorizationChangeListResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 业务逻辑:参数验证
|
||||
if authCodeID == "" {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
if req == nil {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 验证授权码是否存在
|
||||
_, err := s.authCodeRepo.GetAuthorizationCodeByID(ctx, authCodeID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrAuthorizationCodeNotFound) {
|
||||
return nil, i18n.NewI18nError("300001", lang) // 授权码不存在
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 委托给Repository层进行数据访问
|
||||
result, err := s.authCodeRepo.GetAuthorizationChangeList(ctx, authCodeID, req)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 业务逻辑:添加多语言显示字段
|
||||
for i := range result.List {
|
||||
s.fillChangeDisplayFields(&result.List[i], lang)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fillChangeDisplayFields 填充变更历史列表项多语言显示字段
|
||||
func (s *authorizationCodeService) fillChangeDisplayFields(item *models.AuthorizationChangeListItem, lang string) {
|
||||
// 填充变更类型显示字段
|
||||
item.ChangeTypeDisplay = i18n.GetEnumMessage("authorization_change_type", item.ChangeType, lang)
|
||||
}
|
||||
@@ -42,4 +42,21 @@ type AuthorizationCodeService interface {
|
||||
UpdateAuthorizationCode(ctx context.Context, id string, req *models.AuthorizationCodeUpdateRequest) (*models.AuthorizationCode, error)
|
||||
LockUnlockAuthorizationCode(ctx context.Context, id string, req *models.AuthorizationCodeLockRequest) (*models.AuthorizationCode, error)
|
||||
DeleteAuthorizationCode(ctx context.Context, id string) error
|
||||
GetAuthorizationChangeList(ctx context.Context, authCodeID string, req *models.AuthorizationChangeListRequest) (*models.AuthorizationChangeListResponse, error)
|
||||
}
|
||||
|
||||
// LicenseService 许可证服务接口
|
||||
type LicenseService interface {
|
||||
GetLicenseList(ctx context.Context, req *models.LicenseListRequest) (*models.LicenseListResponse, error)
|
||||
GetLicense(ctx context.Context, id string) (*models.LicenseDetailResponse, error)
|
||||
CreateLicense(ctx context.Context, req *models.LicenseCreateRequest) (*models.License, error)
|
||||
RevokeLicense(ctx context.Context, id string, req *models.LicenseRevokeRequest) (*models.License, error)
|
||||
GenerateLicenseFile(ctx context.Context, id string) ([]byte, string, error)
|
||||
|
||||
// 客户端激活和心跳接口
|
||||
ActivateLicense(ctx context.Context, req *models.ActivateRequest, clientIP string) (*models.ActivateResponse, error)
|
||||
Heartbeat(ctx context.Context, req *models.HeartbeatRequest, clientIP string) (*models.HeartbeatResponse, error)
|
||||
|
||||
// 统计接口
|
||||
GetStatsOverview(ctx context.Context) (*models.StatsOverviewResponse, error)
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/repository"
|
||||
pkgcontext "license-manager/pkg/context"
|
||||
"license-manager/pkg/i18n"
|
||||
)
|
||||
|
||||
type licenseService struct {
|
||||
licenseRepo repository.LicenseRepository
|
||||
db *gorm.DB
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewLicenseService 创建许可证服务实例
|
||||
func NewLicenseService(licenseRepo repository.LicenseRepository, db *gorm.DB, logger *logrus.Logger) LicenseService {
|
||||
return &licenseService{
|
||||
licenseRepo: licenseRepo,
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLicenseList 查询许可证列表
|
||||
func (s *licenseService) GetLicenseList(ctx context.Context, req *models.LicenseListRequest) (*models.LicenseListResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 业务逻辑:参数验证
|
||||
if req == nil {
|
||||
return nil, i18n.NewI18nError("900001", lang) // 业务错误,不覆盖多语言message
|
||||
}
|
||||
|
||||
// 委托给Repository层进行数据访问
|
||||
result, err := s.licenseRepo.GetLicenseList(ctx, req)
|
||||
if err != nil {
|
||||
// 数据库相关错误,使用系统错误码,覆盖message显示详细信息
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 业务逻辑:添加多语言显示字段
|
||||
for i := range result.List {
|
||||
s.fillDisplayFields(&result.List[i], lang)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fillDisplayFields 填充列表项多语言显示字段
|
||||
func (s *licenseService) fillDisplayFields(item *models.LicenseListItem, lang string) {
|
||||
// 填充状态显示字段
|
||||
item.StatusDisplay = i18n.GetEnumMessage("license_status", item.Status, lang)
|
||||
|
||||
// 填充在线状态显示字段
|
||||
if item.IsOnline {
|
||||
item.IsOnlineDisplay = i18n.GetEnumMessage("license_online_status", "online", lang)
|
||||
} else {
|
||||
item.IsOnlineDisplay = i18n.GetEnumMessage("license_online_status", "offline", lang)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLicense 获取单个许可证详情
|
||||
func (s *licenseService) GetLicense(ctx context.Context, id string) (*models.LicenseDetailResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 业务逻辑:参数验证
|
||||
if id == "" {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 委托给Repository层进行数据访问
|
||||
license, err := s.licenseRepo.GetLicenseByID(ctx, id)
|
||||
if err != nil {
|
||||
// 根据Repository错误类型包装为完整的I18nError
|
||||
if errors.Is(err, repository.ErrLicenseNotFound) {
|
||||
return nil, i18n.NewI18nError("300006", lang) // 许可证不存在
|
||||
}
|
||||
|
||||
// 数据库相关错误,使用系统错误码,覆盖message显示详细信息
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 转换为详情响应格式
|
||||
response := s.convertToDetailResponse(license, lang)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CreateLicense 创建许可证
|
||||
func (s *licenseService) CreateLicense(ctx context.Context, req *models.LicenseCreateRequest) (*models.License, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 业务逻辑:参数验证
|
||||
if req == nil {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 获取授权码信息以获取客户ID
|
||||
authCode, err := s.licenseRepo.GetAuthorizationCodeByID(ctx, req.AuthorizationCodeID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrAuthorizationCodeNotFound) {
|
||||
return nil, i18n.NewI18nError("300001", lang) // 授权码不存在
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 生成许可证密钥
|
||||
licenseKey, err := s.generateLicenseKey()
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 构建许可证实体
|
||||
license := &models.License{
|
||||
LicenseKey: licenseKey,
|
||||
AuthorizationCodeID: req.AuthorizationCodeID,
|
||||
CustomerID: authCode.CustomerID, // 从授权码获取客户ID
|
||||
HardwareFingerprint: req.HardwareFingerprint,
|
||||
ActivationIP: req.ActivationIP,
|
||||
Status: "inactive", // 手动创建的许可证默认为未激活状态
|
||||
}
|
||||
|
||||
// 设置设备信息
|
||||
if req.DeviceInfo != nil {
|
||||
deviceInfoBytes, err := json.Marshal(req.DeviceInfo)
|
||||
if err == nil {
|
||||
license.DeviceInfo = models.JSON(deviceInfoBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// 委托给Repository层进行数据创建
|
||||
if err := s.licenseRepo.CreateLicense(ctx, license); err != nil {
|
||||
// 根据错误类型包装为完整的I18nError
|
||||
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
|
||||
return nil, i18n.NewI18nError("300007", lang) // 许可证已存在
|
||||
}
|
||||
|
||||
// 数据库相关错误
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
return license, nil
|
||||
}
|
||||
|
||||
// convertToDetailResponse 转换为详情响应格式
|
||||
func (s *licenseService) convertToDetailResponse(license *models.License, lang string) *models.LicenseDetailResponse {
|
||||
response := &models.LicenseDetailResponse{
|
||||
ID: license.ID,
|
||||
LicenseKey: license.LicenseKey,
|
||||
AuthorizationCodeID: license.AuthorizationCodeID,
|
||||
CustomerID: license.CustomerID,
|
||||
HardwareFingerprint: license.HardwareFingerprint,
|
||||
ActivationIP: license.ActivationIP,
|
||||
Status: license.Status,
|
||||
IsOnline: license.IsOnline,
|
||||
LastOnlineIP: license.LastOnlineIP,
|
||||
CreatedAt: license.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: license.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// 格式化时间字段
|
||||
if license.ActivatedAt != nil {
|
||||
activatedAt := license.ActivatedAt.Format(time.RFC3339)
|
||||
response.ActivatedAt = &activatedAt
|
||||
}
|
||||
if license.LastHeartbeat != nil {
|
||||
lastHeartbeat := license.LastHeartbeat.Format(time.RFC3339)
|
||||
response.LastHeartbeat = &lastHeartbeat
|
||||
}
|
||||
if license.ConfigUpdatedAt != nil {
|
||||
configUpdatedAt := license.ConfigUpdatedAt.Format(time.RFC3339)
|
||||
response.ConfigUpdatedAt = &configUpdatedAt
|
||||
}
|
||||
|
||||
// 设置关联数据
|
||||
if license.AuthorizationCode != nil {
|
||||
response.AuthorizationCode = license.AuthorizationCode.Code
|
||||
}
|
||||
if license.Customer != nil {
|
||||
response.CustomerName = license.Customer.CustomerName
|
||||
}
|
||||
|
||||
// 解析JSON字段
|
||||
if len(license.DeviceInfo) > 0 {
|
||||
var deviceInfo map[string]interface{}
|
||||
if err := json.Unmarshal(license.DeviceInfo, &deviceInfo); err == nil {
|
||||
response.DeviceInfo = deviceInfo
|
||||
}
|
||||
}
|
||||
if len(license.UsageData) > 0 {
|
||||
var usageData map[string]interface{}
|
||||
if err := json.Unmarshal(license.UsageData, &usageData); err == nil {
|
||||
response.UsageData = usageData
|
||||
}
|
||||
}
|
||||
|
||||
// 填充多语言显示字段
|
||||
response.StatusDisplay = i18n.GetEnumMessage("license_status", license.Status, lang)
|
||||
if license.IsOnline {
|
||||
response.IsOnlineDisplay = i18n.GetEnumMessage("license_online_status", "online", lang)
|
||||
} else {
|
||||
response.IsOnlineDisplay = i18n.GetEnumMessage("license_online_status", "offline", lang)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// RevokeLicense 撤销许可证
|
||||
func (s *licenseService) RevokeLicense(ctx context.Context, id string, req *models.LicenseRevokeRequest) (*models.License, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 业务逻辑:参数验证
|
||||
if id == "" {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
if req == nil {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 先查询现有许可证
|
||||
existingLicense, err := s.licenseRepo.GetLicenseByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrLicenseNotFound) {
|
||||
return nil, i18n.NewI18nError("300006", lang) // 许可证不存在
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 检查许可证是否已经被撤销
|
||||
if existingLicense.Status == "revoked" {
|
||||
return nil, i18n.NewI18nError("300007", lang) // 许可证已被撤销
|
||||
}
|
||||
|
||||
// 更新许可证状态为撤销
|
||||
existingLicense.Status = "revoked"
|
||||
|
||||
// 如果提供了撤销原因,保存到使用数据中
|
||||
if req.Reason != "" {
|
||||
revokeData := map[string]interface{}{
|
||||
"revoked_at": time.Now().Format(time.RFC3339),
|
||||
"revoke_reason": req.Reason,
|
||||
}
|
||||
|
||||
// 保留原有的使用数据
|
||||
if len(existingLicense.UsageData) > 0 {
|
||||
var existingUsageData map[string]interface{}
|
||||
if err := json.Unmarshal(existingLicense.UsageData, &existingUsageData); err == nil {
|
||||
for k, v := range existingUsageData {
|
||||
revokeData[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usageDataBytes, err := json.Marshal(revokeData)
|
||||
if err == nil {
|
||||
existingLicense.UsageData = models.JSON(usageDataBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// 委托给Repository层进行数据更新
|
||||
if err := s.licenseRepo.UpdateLicense(ctx, existingLicense); err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
return existingLicense, nil
|
||||
}
|
||||
|
||||
// GenerateLicenseFile 生成许可证文件
|
||||
func (s *licenseService) GenerateLicenseFile(ctx context.Context, id string) ([]byte, string, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
s.logger.Infof("[GenerateLicenseFile] 开始生成许可证文件,license_id: %s", id)
|
||||
|
||||
// 业务逻辑:参数验证
|
||||
if id == "" {
|
||||
s.logger.Error("[GenerateLicenseFile] 错误:许可证ID为空")
|
||||
return nil, "", i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 获取许可证信息
|
||||
s.logger.Infof("[GenerateLicenseFile] 查询许可证信息,license_id: %s", id)
|
||||
license, err := s.licenseRepo.GetLicenseByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Errorf("[GenerateLicenseFile] 查询许可证失败,license_id: %s, error: %v", id, err)
|
||||
if errors.Is(err, repository.ErrLicenseNotFound) {
|
||||
return nil, "", i18n.NewI18nError("300006", lang) // 许可证不存在
|
||||
}
|
||||
return nil, "", i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
s.logger.Infof("[GenerateLicenseFile] 许可证信息查询成功,license_key: %s, status: %s", license.LicenseKey, license.Status)
|
||||
|
||||
// 检查许可证状态
|
||||
if license.Status == "revoked" {
|
||||
s.logger.Warnf("[GenerateLicenseFile] 许可证已被撤销,license_id: %s", id)
|
||||
return nil, "", i18n.NewI18nError("300007", lang) // 许可证已被撤销
|
||||
}
|
||||
|
||||
// 构建许可证文件内容
|
||||
licenseFileData := map[string]interface{}{
|
||||
"license_key": license.LicenseKey,
|
||||
"authorization_code_id": license.AuthorizationCodeID,
|
||||
"hardware_fingerprint": license.HardwareFingerprint,
|
||||
"status": license.Status,
|
||||
"activated_at": license.ActivatedAt,
|
||||
"config_updated_at": license.ConfigUpdatedAt,
|
||||
"generated_at": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// 包含授权码信息
|
||||
if license.AuthorizationCode != nil {
|
||||
licenseFileData["authorization_code"] = license.AuthorizationCode.Code
|
||||
licenseFileData["start_date"] = license.AuthorizationCode.StartDate
|
||||
licenseFileData["end_date"] = license.AuthorizationCode.EndDate
|
||||
licenseFileData["deployment_type"] = license.AuthorizationCode.DeploymentType
|
||||
licenseFileData["encryption_type"] = license.AuthorizationCode.EncryptionType
|
||||
licenseFileData["max_activations"] = license.AuthorizationCode.MaxActivations
|
||||
|
||||
// 包含功能配置
|
||||
if len(license.AuthorizationCode.FeatureConfig) > 0 {
|
||||
var featureConfig map[string]interface{}
|
||||
if err := json.Unmarshal(license.AuthorizationCode.FeatureConfig, &featureConfig); err == nil {
|
||||
licenseFileData["feature_config"] = featureConfig
|
||||
}
|
||||
}
|
||||
|
||||
// 包含使用限制
|
||||
if len(license.AuthorizationCode.UsageLimits) > 0 {
|
||||
var usageLimits map[string]interface{}
|
||||
if err := json.Unmarshal(license.AuthorizationCode.UsageLimits, &usageLimits); err == nil {
|
||||
licenseFileData["usage_limits"] = usageLimits
|
||||
}
|
||||
}
|
||||
|
||||
// 包含自定义参数
|
||||
if len(license.AuthorizationCode.CustomParameters) > 0 {
|
||||
var customParameters map[string]interface{}
|
||||
if err := json.Unmarshal(license.AuthorizationCode.CustomParameters, &customParameters); err == nil {
|
||||
licenseFileData["custom_parameters"] = customParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化许可证数据
|
||||
licenseJSON, err := json.Marshal(licenseFileData)
|
||||
if err != nil {
|
||||
return nil, "", i18n.NewI18nError("300009", lang) // 许可证文件生成失败
|
||||
}
|
||||
|
||||
// 加密许可证文件
|
||||
encryptedData, err := s.encryptLicenseFile(licenseJSON)
|
||||
if err != nil {
|
||||
return nil, "", i18n.NewI18nError("300009", lang) // 许可证文件生成失败
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
fileName := fmt.Sprintf("license_%s.lic", license.LicenseKey)
|
||||
|
||||
return encryptedData, fileName, nil
|
||||
}
|
||||
|
||||
// generateLicenseKey 生成许可证密钥
|
||||
func (s *licenseService) generateLicenseKey() (string, error) {
|
||||
// 生成12字节随机数据
|
||||
bytes := make([]byte, 12)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 转换为16进制字符串并格式化
|
||||
hex := strings.ToUpper(hex.EncodeToString(bytes))
|
||||
return fmt.Sprintf("LIC-DEVICE-%s", hex), nil
|
||||
}
|
||||
|
||||
// ActivateLicense 激活许可证
|
||||
func (s *licenseService) ActivateLicense(ctx context.Context, req *models.ActivateRequest, clientIP string) (*models.ActivateResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 参数验证
|
||||
if req == nil {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 获取授权码信息
|
||||
authCode, err := s.licenseRepo.GetAuthorizationCodeByCode(ctx, req.AuthorizationCode)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrAuthorizationCodeNotFound) {
|
||||
return nil, i18n.NewI18nError("300001", lang) // 授权码不存在
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 检查授权码状态和有效期
|
||||
now := time.Now()
|
||||
if authCode.IsLocked {
|
||||
return nil, i18n.NewI18nError("300003", lang) // 授权码已被锁定
|
||||
}
|
||||
if now.Before(authCode.StartDate) || now.After(authCode.EndDate) {
|
||||
return nil, i18n.NewI18nError("300001", lang) // 授权码已过期
|
||||
}
|
||||
|
||||
// 使用事务确保并发安全
|
||||
var response *models.ActivateResponse
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 检查当前激活数量
|
||||
count, err := s.licenseRepo.GetActiveLicenseCount(ctx, authCode.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否已存在相同硬件指纹的许可证
|
||||
var existingLicense models.License
|
||||
err = tx.Where("authorization_code_id = ? AND hardware_fingerprint = ?",
|
||||
authCode.ID, req.HardwareFingerprint).First(&existingLicense).Error
|
||||
|
||||
if err == nil {
|
||||
// 已存在,直接激活
|
||||
existingLicense.Status = "active"
|
||||
existingLicense.ActivationIP = &clientIP
|
||||
now := time.Now()
|
||||
existingLicense.ActivatedAt = &now
|
||||
existingLicense.LastHeartbeat = &now
|
||||
existingLicense.LastOnlineIP = &clientIP
|
||||
|
||||
if err := tx.Save(&existingLicense).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成许可证文件
|
||||
licenseFile, err := s.generateLicenseFileContent(&existingLicense, authCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response = &models.ActivateResponse{
|
||||
LicenseKey: existingLicense.LicenseKey,
|
||||
LicenseFile: licenseFile,
|
||||
HeartbeatInterval: 300, // 5分钟
|
||||
}
|
||||
return nil
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查激活数量限制
|
||||
if count >= int64(authCode.MaxActivations) {
|
||||
return repository.ErrLicenseNotFound // 使用已有错误,表示激活数量已达上限
|
||||
}
|
||||
|
||||
// 生成新的许可证
|
||||
licenseKey, err := s.generateLicenseKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
license := &models.License{
|
||||
LicenseKey: licenseKey,
|
||||
AuthorizationCodeID: authCode.ID,
|
||||
CustomerID: authCode.CustomerID,
|
||||
HardwareFingerprint: req.HardwareFingerprint,
|
||||
ActivationIP: &clientIP,
|
||||
Status: "active",
|
||||
ActivatedAt: &now,
|
||||
LastHeartbeat: &now,
|
||||
LastOnlineIP: &clientIP,
|
||||
}
|
||||
|
||||
// 设置设备信息
|
||||
if req.DeviceInfo != nil {
|
||||
deviceInfoBytes, err := json.Marshal(req.DeviceInfo)
|
||||
if err == nil {
|
||||
license.DeviceInfo = models.JSON(deviceInfoBytes)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Create(license).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成许可证文件
|
||||
licenseFile, err := s.generateLicenseFileContent(license, authCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response = &models.ActivateResponse{
|
||||
LicenseKey: license.LicenseKey,
|
||||
LicenseFile: licenseFile,
|
||||
HeartbeatInterval: 300,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrLicenseNotFound) {
|
||||
return nil, i18n.NewI18nError("300004", lang) // 激活数量已达上限
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Heartbeat 心跳检测
|
||||
func (s *licenseService) Heartbeat(ctx context.Context, req *models.HeartbeatRequest, clientIP string) (*models.HeartbeatResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 参数验证
|
||||
if req == nil {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 获取许可证信息
|
||||
license, err := s.licenseRepo.GetLicenseByKey(ctx, req.LicenseKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrLicenseNotFound) {
|
||||
return nil, i18n.NewI18nError("300006", lang) // 许可证不存在
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 检查许可证状态
|
||||
if license.Status == "revoked" {
|
||||
return nil, i18n.NewI18nError("300007", lang) // 许可证已被撤销
|
||||
}
|
||||
|
||||
// 更新心跳时间和使用数据
|
||||
now := time.Now()
|
||||
license.LastHeartbeat = &now
|
||||
license.LastOnlineIP = &clientIP
|
||||
|
||||
// 更新使用数据
|
||||
if req.UsageData != nil {
|
||||
usageDataBytes, err := json.Marshal(req.UsageData)
|
||||
if err == nil {
|
||||
license.UsageData = models.JSON(usageDataBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查配置是否有更新
|
||||
configUpdated := false
|
||||
var licenseFile *string
|
||||
|
||||
if req.ConfigUpdatedAt != nil && license.AuthorizationCode != nil {
|
||||
clientConfigTime, err := time.Parse(time.RFC3339, *req.ConfigUpdatedAt)
|
||||
if err == nil && license.AuthorizationCode.UpdatedAt.After(clientConfigTime) {
|
||||
configUpdated = true
|
||||
// 生成新的许可证文件
|
||||
fileContent, err := s.generateLicenseFileContent(license, license.AuthorizationCode)
|
||||
if err == nil {
|
||||
licenseFile = &fileContent
|
||||
license.ConfigUpdatedAt = &now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
if err := s.licenseRepo.UpdateLicense(ctx, license); err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
response := &models.HeartbeatResponse{
|
||||
Status: license.Status,
|
||||
ConfigUpdated: configUpdated,
|
||||
LicenseFile: licenseFile,
|
||||
HeartbeatInterval: 300,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// generateLicenseFileContent 生成许可证文件内容
|
||||
func (s *licenseService) generateLicenseFileContent(license *models.License, authCode *models.AuthorizationCode) (string, error) {
|
||||
// 构建许可证文件内容
|
||||
licenseFileData := map[string]interface{}{
|
||||
"license_key": license.LicenseKey,
|
||||
"authorization_code_id": license.AuthorizationCodeID,
|
||||
"hardware_fingerprint": license.HardwareFingerprint,
|
||||
"status": license.Status,
|
||||
"activated_at": license.ActivatedAt,
|
||||
"config_updated_at": license.ConfigUpdatedAt,
|
||||
"generated_at": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if authCode != nil {
|
||||
licenseFileData["authorization_code"] = authCode.Code
|
||||
licenseFileData["start_date"] = authCode.StartDate
|
||||
licenseFileData["end_date"] = authCode.EndDate
|
||||
licenseFileData["deployment_type"] = authCode.DeploymentType
|
||||
licenseFileData["encryption_type"] = authCode.EncryptionType
|
||||
licenseFileData["max_activations"] = authCode.MaxActivations
|
||||
|
||||
// 包含功能配置等
|
||||
if len(authCode.FeatureConfig) > 0 {
|
||||
var featureConfig map[string]interface{}
|
||||
if err := json.Unmarshal(authCode.FeatureConfig, &featureConfig); err == nil {
|
||||
licenseFileData["feature_config"] = featureConfig
|
||||
}
|
||||
}
|
||||
|
||||
if len(authCode.UsageLimits) > 0 {
|
||||
var usageLimits map[string]interface{}
|
||||
if err := json.Unmarshal(authCode.UsageLimits, &usageLimits); err == nil {
|
||||
licenseFileData["usage_limits"] = usageLimits
|
||||
}
|
||||
}
|
||||
|
||||
if len(authCode.CustomParameters) > 0 {
|
||||
var customParameters map[string]interface{}
|
||||
if err := json.Unmarshal(authCode.CustomParameters, &customParameters); err == nil {
|
||||
licenseFileData["custom_parameters"] = customParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化并加密
|
||||
licenseJSON, err := json.Marshal(licenseFileData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
encryptedData, err := s.encryptLicenseFile(licenseJSON)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(encryptedData), nil
|
||||
}
|
||||
|
||||
// encryptLicenseFile 加密许可证文件
|
||||
func (s *licenseService) encryptLicenseFile(data []byte) ([]byte, error) {
|
||||
// 从配置文件获取加密密钥
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("configuration not initialized")
|
||||
}
|
||||
|
||||
key := []byte(cfg.License.EncryptionKey)
|
||||
if len(key) != 16 && len(key) != 24 && len(key) != 32 {
|
||||
return nil, fmt.Errorf("invalid encryption key length: %d bytes, must be 16, 24, or 32 bytes", len(key))
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// 使用GCM模式进行加密
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成随机nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加密数据
|
||||
ciphertext := gcm.Seal(nonce, nonce, data, nil)
|
||||
|
||||
// 使用base64编码
|
||||
encoded := base64.StdEncoding.EncodeToString(ciphertext)
|
||||
|
||||
return []byte(encoded), nil
|
||||
}
|
||||
|
||||
// GetStatsOverview 获取授权概览统计
|
||||
func (s *licenseService) GetStatsOverview(ctx context.Context) (*models.StatsOverviewResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
// 获取配置信息
|
||||
cfg := config.GetConfig()
|
||||
offlineTimeout := time.Duration(cfg.License.HeartbeatTimeout) * time.Second
|
||||
|
||||
// 计算时间点
|
||||
now := time.Now()
|
||||
onlineThreshold := now.Add(-offlineTimeout)
|
||||
lastMonth := now.AddDate(0, -1, 0)
|
||||
|
||||
var stats models.StatsOverviewResponse
|
||||
|
||||
// 执行统计查询
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 总授权码数量
|
||||
if err := tx.Model(&models.AuthorizationCode{}).Count(&stats.TotalAuthCodes).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 活跃许可证数量(状态为active)
|
||||
if err := tx.Model(&models.License{}).Where("status = ?", "active").Count(&stats.ActiveLicenses).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 即将过期数量(授权码在30天内过期,且状态为active)
|
||||
expireThreshold := now.AddDate(0, 0, 30)
|
||||
if err := tx.Model(&models.AuthorizationCode{}).
|
||||
Where("end_date <= ? AND end_date > ? AND is_locked = false", expireThreshold, now).
|
||||
Count(&stats.ExpiringSoon).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 异常告警数量(许可证超时未心跳)
|
||||
if err := tx.Model(&models.License{}).
|
||||
Where("status = 'active' AND (last_heartbeat < ? OR last_heartbeat IS NULL)", onlineThreshold).
|
||||
Count(&stats.AbnormalAlerts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 计算上月的授权码和许可证数量(用于计算增长率)
|
||||
var lastMonthAuthCodes, lastMonthLicenses int64
|
||||
|
||||
// 上月授权码数量
|
||||
if err := tx.Model(&models.AuthorizationCode{}).
|
||||
Where("created_at <= ?", lastMonth).
|
||||
Count(&lastMonthAuthCodes).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上月许可证数量
|
||||
if err := tx.Model(&models.License{}).
|
||||
Where("created_at <= ?", lastMonth).
|
||||
Count(&lastMonthLicenses).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算增长率
|
||||
if lastMonthAuthCodes > 0 {
|
||||
stats.GrowthRate.AuthCodes = float64(stats.TotalAuthCodes-lastMonthAuthCodes) / float64(lastMonthAuthCodes) * 100
|
||||
} else {
|
||||
stats.GrowthRate.AuthCodes = 0
|
||||
}
|
||||
|
||||
if lastMonthLicenses > 0 {
|
||||
stats.GrowthRate.Licenses = float64(stats.ActiveLicenses-lastMonthLicenses) / float64(lastMonthLicenses) * 100
|
||||
} else {
|
||||
stats.GrowthRate.Licenses = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 395 KiB |
Reference in new issue
Block a user