feat: 授权变更历史、许可证增删改查、详情、下载、授权概览统计、激活许可证、心跳检查

This commit is contained in:
wood911010 committed 2025-09-22 14:54:37 +08:00
1 parent 3e0dd9a4cf
commit 9352b113dd
28 files changed
+5505 -55

No files matched your search

+14
View File
@@ -8,6 +8,15 @@
软件授权管理平台是一个独立的软件授权管理系统,为软件系统提供授权码生成、分发、验证和管理服务。支持在线和离线两种授权模式,基于硬件绑定的授权机制确保安全性。
## Demo环境
访问地址:[lm.cedar-v.com](http://lm.cedar-v.com)
**登录凭证:**
- 用户名:`admin`
- 密码:`admin@123`
> 💡 这是一个当前功能的演示环境,您可以体验已开发的核心特性。我们正在持续开发中,更多功能将陆续上线。
## 系统界面
### 登录界面
@@ -16,6 +25,11 @@
### 管理仪表盘
![系统主页](docs/images/home.png)
### 客户管理
![客户管理页面](docs/images/customer.png)
客户管理模块提供完整的客户信息管理功能,包括客户信息的增删改查、状态管理、授权关联等核心功能。
## 核心功能
- 🔧 **客户管理**:完整的客户信息管理和状态控制
+14
View File
@@ -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
![Dashboard](docs/images/home.png)
### Customer Management
![Customer Management Page](docs/images/customer.png)
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
+7 -1
View File
@@ -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天内到期算即将过期
+7 -1
View File
@@ -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天内到期算即将过期
+18
View File
@@ -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"
+18
View File
@@ -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: "不明なエラー"
+18
View File
@@ -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: "未知错误"
+124 -38
View File
@@ -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
+762
View File
@@ -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,
})
}
+22
View File
@@ -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)
}
// 管理员接口
+14
View File
@@ -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 {
+2
View File
@@ -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)
+67
View File
@@ -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"`
}
+185
View File
@@ -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
}
+7
View File
@@ -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")
+33
View File
@@ -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
}
+21 -13
View File
@@ -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)
}
+17
View File
@@ -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)
}
+763
View File
@@ -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