From 188406c517987d5e415375eff3f3d2580281aff8 Mon Sep 17 00:00:00 2001 From: wood911010 Date: Wed, 17 Sep 2025 00:01:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=88=E6=9D=83=E7=A0=81=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E3=80=81=E5=88=9B=E5=BB=BA=E6=8E=88=E6=9D=83=E7=A0=81?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/configs/i18n/errors/en-US.yaml | 32 +- backend/configs/i18n/errors/ja-JP.yaml | 56 ++- backend/configs/i18n/errors/zh-CN.yaml | 32 +- backend/docs/modules_design/auth_api.md | 425 ++++++++++++++++++ backend/docs/swagger/docs.go | 143 ++++++ backend/docs/swagger/swagger.json | 143 ++++++ backend/docs/swagger/swagger.yaml | 94 ++++ .../handlers/authorization_code_handler.go | 156 +++++++ backend/internal/api/routes/router.go | 7 + backend/internal/database/migration.go | 1 + backend/internal/models/auth.go | 128 ++++++ backend/internal/models/common.go | 53 +++ .../authorization_code_repository.go | 150 +++++++ backend/internal/repository/errors.go | 7 + backend/internal/repository/interfaces.go | 15 + .../service/authorization_code_service.go | 225 ++++++++++ backend/internal/service/interfaces.go | 6 + frontend/.env.development | 2 +- 18 files changed, 1656 insertions(+), 19 deletions(-) create mode 100644 backend/docs/modules_design/auth_api.md create mode 100644 backend/internal/api/handlers/authorization_code_handler.go create mode 100644 backend/internal/repository/authorization_code_repository.go create mode 100644 backend/internal/service/authorization_code_service.go diff --git a/backend/configs/i18n/errors/en-US.yaml b/backend/configs/i18n/errors/en-US.yaml index a027c90..0493e64 100644 --- a/backend/configs/i18n/errors/en-US.yaml +++ b/backend/configs/i18n/errors/en-US.yaml @@ -22,12 +22,18 @@ errors: "200004": "Invalid customer information format" "200005": "Failed to query customer list" - # License module (30xxxx) - Reserved - license: - "300001": "Invalid license key" - "300002": "License expired" - "300003": "License key is required" - "300004": "License key already used" + # Authorization module (30xxxx) + authorization: + "300001": "Authorization code not found" + "300002": "Authorization code already exists" + "300003": "Authorization code is locked" + "300004": "Maximum activation limit reached" + "300005": "Invalid hardware fingerprint format" + "300006": "License not found" + "300007": "License has been revoked" + "300008": "Hardware fingerprint mismatch" + "300009": "Failed to generate license file" + "300010": "Invalid configuration parameters" # System general (90xxxx) system: @@ -59,6 +65,20 @@ enums: "medium": "Medium" "large": "Large" "enterprise": "Enterprise" + + deployment_type: + "standalone": "Standalone" + "cloud": "Cloud" + "hybrid": "Hybrid" + + encryption_type: + "standard": "Standard" + "advanced": "Advanced" + + authorization_status: + "normal": "Normal" + "locked": "Locked" + "expired": "Expired" # Default error message default_error: "Unknown error" \ No newline at end of file diff --git a/backend/configs/i18n/errors/ja-JP.yaml b/backend/configs/i18n/errors/ja-JP.yaml index a4bef1f..0a93344 100644 --- a/backend/configs/i18n/errors/ja-JP.yaml +++ b/backend/configs/i18n/errors/ja-JP.yaml @@ -22,12 +22,18 @@ errors: "200004": "無効な顧客情報形式" "200005": "顧客リストの照会に失敗しました" - # ライセンスモジュール (30xxxx) - 予約済み - license: - "300001": "無効なライセンスキー" - "300002": "ライセンスが期限切れです" - "300003": "ライセンスキーは必須です" - "300004": "ライセンスキーは既に使用されています" + # 認可モジュール (30xxxx) + authorization: + "300001": "認可コードが見つかりません" + "300002": "認可コードが既に存在します" + "300003": "認可コードがロックされています" + "300004": "最大アクティベーション数に達しました" + "300005": "無効なハードウェア指紋形式" + "300006": "ライセンスが見つかりません" + "300007": "ライセンスが取り消されました" + "300008": "ハードウェア指紋が一致しません" + "300009": "ライセンスファイルの生成に失敗しました" + "300010": "無効な設定パラメータ" # システム共通 (90xxxx) system: @@ -36,5 +42,43 @@ errors: "900003": "リソースの競合" "900004": "内部サーバーエラー" +# 列挙値多言語 +enums: + customer_type: + "individual": "個人顧客" + "enterprise": "企業顧客" + "government": "政府顧客" + "education": "教育顧客" + + customer_level: + "normal": "一般顧客" + "vip": "VIP顧客" + "enterprise": "企業顧客" + "strategic": "戦略顧客" + + customer_status: + "active": "アクティブ" + "disabled": "無効" + + company_size: + "small": "小企業" + "medium": "中企業" + "large": "大企業" + "enterprise": "超大企業" + + deployment_type: + "standalone": "スタンドアロン" + "cloud": "クラウド" + "hybrid": "ハイブリッド" + + encryption_type: + "standard": "標準暗号化" + "advanced": "高度暗号化" + + authorization_status: + "normal": "正常" + "locked": "ロック済み" + "expired": "期限切れ" + # デフォルトエラーメッセージ default_error: "不明なエラー" \ No newline at end of file diff --git a/backend/configs/i18n/errors/zh-CN.yaml b/backend/configs/i18n/errors/zh-CN.yaml index 448166e..260a78e 100644 --- a/backend/configs/i18n/errors/zh-CN.yaml +++ b/backend/configs/i18n/errors/zh-CN.yaml @@ -22,12 +22,18 @@ errors: "200004": "客户信息格式无效" "200005": "客户列表查询失败" - # 授权模块 (30xxxx) - 预留 - license: - "300001": "授权码无效" - "300002": "授权已过期" - "300003": "授权码不能为空" - "300004": "授权已被使用" + # 授权模块 (30xxxx) + authorization: + "300001": "授权码不存在" + "300002": "授权码已存在" + "300003": "授权码已被锁定" + "300004": "激活数量已达上限" + "300005": "硬件指纹格式错误" + "300006": "许可证不存在" + "300007": "许可证已被撤销" + "300008": "硬件指纹不匹配" + "300009": "许可证文件生成失败" + "300010": "配置参数错误" # 系统通用 (90xxxx) system: @@ -59,6 +65,20 @@ enums: "medium": "中型企业" "large": "大型企业" "enterprise": "超大型企业" + + deployment_type: + "standalone": "单机版" + "cloud": "云端版" + "hybrid": "混合版" + + encryption_type: + "standard": "标准加密" + "advanced": "高级加密" + + authorization_status: + "normal": "正常" + "locked": "已锁定" + "expired": "已过期" # 默认错误信息 default_error: "未知错误" \ No newline at end of file diff --git a/backend/docs/modules_design/auth_api.md b/backend/docs/modules_design/auth_api.md new file mode 100644 index 0000000..f0307eb --- /dev/null +++ b/backend/docs/modules_design/auth_api.md @@ -0,0 +1,425 @@ +# 授权模块API设计 + +## 1. 授权码管理 API + +### 1.1 授权码列表 +```http +GET /api/v1/authorization-codes +``` + +**查询参数** +- `customer_id` - 客户ID +- `status` - 状态 (normal/locked/expired) +- `page` - 页码 +- `limit` - 每页数量 +- `start_date` - 创建开始时间 +- `end_date` - 创建结束时间 + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "list": [ + { + "id": 1, + "code": "LIC-COMP001-A7B9X2-C8F4", + "customer_id": 1, + "customer_name": "张三公司", + "customer_name_display": "张三公司", + "status": "normal", + "status_display": "正常", + "start_date": "2024-01-01", + "end_date": "2024-12-31", + "max_activations": 10, + "current_activations": 7, + "deployment_type": "standalone", + "deployment_type_display": "单机版", + "is_locked": false, + "description": "企业版授权", + "created_at": "2024-01-01T10:00:00Z" + } + ], + "total": 100, + "page": 1, + "limit": 20 + } +} +``` + +### 1.2 创建授权码 +```http +POST /api/v1/authorization-codes +``` + +**说明:** 有效期从当天00:00:00开始,到指定天数后的23:59:59结束。例如:validity_days=1表示今天一整天有效。 + +**请求体** +```json +{ + "customer_id": "customer-uuid-123", + "software_id": "erp-system", + "description": "企业版授权", + "validity_days": 365, + "deployment_type": "standalone", + "encryption_type": "standard", + "software_version": "1.0.0", + "max_activations": 10, + "feature_config": { + "modules": ["user_mgmt", "inventory", "finance"] + }, + "usage_limits": { + "max_users": 100, + "api_calls_per_day": 10000 + }, + "custom_parameters": {} +} +``` + +**参数说明:** +- `customer_id`: 客户ID(必填) +- `software_id`: 软件产品ID(可选) +- `description`: 授权描述(可选) +- `validity_days`: 有效期天数,范围1-36500天(必填) +- `deployment_type`: 部署类型,枚举值:standalone/cloud/hybrid(必填) +- `encryption_type`: 加密类型,枚举值:standard/advanced(可选,默认standard) +- `software_version`: 软件版本(可选) +- `max_activations`: 最大激活数量(必填,最小值1) +- `feature_config`: 功能配置JSON(可选) +- `usage_limits`: 使用限制JSON(可选) +- `custom_parameters`: 自定义参数JSON(可选) + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "code": "LIC-COMP001-A7B9X2-C8F4" + } +} +``` + +### 1.3 授权码详情 +```http +GET /api/v1/authorization-codes/{id} +``` + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "code": "LIC-COMP001-A7B9X2-C8F4", + "customer_id": 1, + "customer_name": "张三公司", + "status": "normal", + "status_display": "正常", + "start_date": "2024-01-01", + "end_date": "2024-12-31", + "max_activations": 10, + "current_activations": 7, + "deployment_type": "standalone", + "deployment_type_display": "单机版", + "encryption_type": "standard", + "encryption_type_display": "标准加密", + "feature_config": { + "modules": ["user_mgmt", "inventory", "finance"] + }, + "usage_limits": { + "max_users": 100, + "api_calls_per_day": 10000 + }, + "custom_parameters": {}, + "is_locked": false, + "lock_reason": null, + "description": "企业版授权", + "created_at": "2024-01-01T10:00:00Z", + "updated_at": "2024-01-01T10:00:00Z" + } +} +``` + +### 1.4 更新授权码 +```http +PUT /api/v1/authorization-codes/{id} +``` + +**请求体** +```json +{ + "start_date": "2024-01-01", + "end_date": "2025-12-31", + "max_activations": 20, + "feature_config": { + "modules": ["user_mgmt", "inventory", "finance", "crm"] + }, + "usage_limits": { + "max_users": 200, + "api_calls_per_day": 20000 + }, + "change_type": "upgrade", + "reason": "客户升级套餐" +} +``` + +### 1.5 锁定/解锁授权码 +```http +PUT /api/v1/authorization-codes/{id}/lock +``` + +**请求体** +```json +{ + "is_locked": true, + "lock_reason": "违规使用" +} +``` + +### 1.6 删除授权码 +```http +DELETE /api/v1/authorization-codes/{id} +``` + +## 2. 许可证管理 API + +### 2.1 许可证列表 +```http +GET /api/v1/licenses +``` + +**查询参数** +- `authorization_code_id` - 授权码ID +- `customer_id` - 客户ID +- `status` - 状态 (active/inactive/revoked) +- `is_online` - 在线状态 + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "list": [ + { + "id": 1, + "license_key": "LIC-DEVICE-ABC123", + "authorization_code_id": 1, + "authorization_code": "LIC-COMP001-A7B9X2-C8F4", + "customer_name": "张三公司", + "hardware_fingerprint": "CPU:ABC123,MB:DEF456", + "status": "active", + "status_display": "激活", + "is_online": true, + "is_online_display": "在线", + "activation_ip": "192.168.1.100", + "last_online_ip": "192.168.1.100", + "activated_at": "2024-01-01T10:00:00Z", + "last_heartbeat": "2024-01-01T14:30:00Z" + } + ], + "total": 50, + "page": 1, + "limit": 20 + } +} +``` + +### 2.2 许可证详情 +```http +GET /api/v1/licenses/{id} +``` + +### 2.3 手动添加许可证 +```http +POST /api/v1/licenses +``` + +**请求体** +```json +{ + "authorization_code_id": 1, + "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" +} +``` + +### 2.4 撤销许可证 +```http +PUT /api/v1/licenses/{id}/revoke +``` + +**请求体** +```json +{ + "reason": "设备更换" +} +``` + +### 2.5 下载许可证文件 +```http +GET /api/v1/licenses/{id}/download +``` + +**响应**: 返回加密的许可证文件 + +## 3. 客户端激活 API + +### 3.1 软件激活 +```http +POST /api/v1/activate +``` + +**请求体** +```json +{ + "authorization_code": "LIC-COMP001-A7B9X2-C8F4", + "hardware_fingerprint": "CPU:ABC123,MB:DEF456,MAC:00:11:22:33:44:55", + "device_info": { + "cpu": "Intel i7-8700", + "memory": "16GB", + "os": "Windows 10 Pro" + }, + "software_version": "1.0.0" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "license_key": "LIC-DEVICE-ABC123", + "license_file": "base64编码的加密许可证文件", + "heartbeat_interval": 300 + } +} +``` + +### 3.2 心跳检测 +```http +POST /api/v1/heartbeat +``` + +**请求体** +```json +{ + "license_key": "LIC-DEVICE-ABC123", + "hardware_fingerprint": "CPU:ABC123,MB:DEF456", + "config_updated_at": "2024-01-01T10:00:00Z", + "usage_data": { + "active_users": 50, + "api_calls_today": 5000 + }, + "software_version": "1.0.0" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "status": "active", + "config_updated": true, + "license_file": "base64编码的新许可证文件", + "heartbeat_interval": 300 + } +} +``` + +## 4. 统计报表 API + +### 4.1 授权概览统计 +```http +GET /api/v1/stats/overview +``` + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "total_auth_codes": 1234, + "active_licenses": 856, + "expiring_soon": 23, + "abnormal_alerts": 5, + "growth_rate": { + "auth_codes": 5.2, + "licenses": 3.1 + } + } +} +``` + +### 4.2 授权变更历史 +```http +GET /api/v1/authorization-codes/{id}/changes +``` + +**响应** +```json +{ + "code": 0, + "message": "success", + "data": { + "list": [ + { + "id": 1, + "change_type": "upgrade", + "change_type_display": "升级", + "reason": "客户升级套餐", + "operator_name": "张三", + "created_at": "2024-01-01T10:00:00Z" + } + ] + } +} +``` + +## 5. 错误码定义 + +- `300001` - 授权码不存在 +- `300002` - 授权码已存在 +- `300003` - 授权码已被锁定 +- `300004` - 激活数量已达上限 +- `300005` - 硬件指纹格式错误 +- `300006` - 许可证不存在 +- `300007` - 许可证已被撤销 +- `300008` - 硬件指纹不匹配 +- `300009` - 许可证文件生成失败 +- `300010` - 配置参数错误 + +## 6. 状态说明 + +### 6.1 授权码状态 +- `normal` - 正常:当前时间在有效期内且未被锁定 +- `locked` - 已锁定:is_locked=true +- `expired` - 已过期:当前时间超过end_date + +### 6.2 部署类型 +- `standalone` - 单机版:独立部署的软件 +- `cloud` - 云端版:基于云的SaaS服务 +- `hybrid` - 混合版:结合本地和云端的部署 + +### 6.3 加密类型 +- `standard` - 标准加密:基础安全级别 +- `advanced` - 高级加密:增强安全级别 + +### 6.4 许可证状态 +- `active` - 激活:正常使用中 +- `inactive` - 未激活:已创建但尚未激活 +- `revoked` - 已撤销:被管理员撤销或因违规被停用 \ No newline at end of file diff --git a/backend/docs/swagger/docs.go b/backend/docs/swagger/docs.go index 8cc4118..69b1805 100644 --- a/backend/docs/swagger/docs.go +++ b/backend/docs/swagger/docs.go @@ -733,6 +733,87 @@ const docTemplate = `{ } } }, + "/api/v1/authorization-codes": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的授权码,自动生成授权码字符串。有效期从当天00:00:00开始,到指定天数后的23:59:59结束", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "授权码管理" + ], + "summary": "创建授权码", + "parameters": [ + { + "description": "授权码信息", + "name": "authorization_code", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.AuthorizationCodeCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.AuthorizationCodeCreateResponse" + } + } + } + ] + } + }, + "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" + } + } + } + } + }, "/api/v1/login": { "post": { "description": "管理员用户登录接口", @@ -859,6 +940,68 @@ const docTemplate = `{ } } }, + "models.AuthorizationCodeCreateRequest": { + "type": "object", + "required": [ + "customer_id", + "deployment_type", + "max_activations", + "validity_days" + ], + "properties": { + "custom_parameters": {}, + "customer_id": { + "type": "string" + }, + "deployment_type": { + "type": "string", + "enum": [ + "standalone", + "cloud", + "hybrid" + ] + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "encryption_type": { + "type": "string", + "enum": [ + "standard", + "advanced" + ] + }, + "feature_config": {}, + "max_activations": { + "type": "integer", + "minimum": 1 + }, + "software_id": { + "type": "string" + }, + "software_version": { + "type": "string" + }, + "usage_limits": {}, + "validity_days": { + "type": "integer", + "maximum": 36500, + "minimum": 1 + } + } + }, + "models.AuthorizationCodeCreateResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, "models.Customer": { "type": "object", "properties": { diff --git a/backend/docs/swagger/swagger.json b/backend/docs/swagger/swagger.json index f1cbb3c..57770ff 100644 --- a/backend/docs/swagger/swagger.json +++ b/backend/docs/swagger/swagger.json @@ -730,6 +730,87 @@ } } }, + "/api/v1/authorization-codes": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的授权码,自动生成授权码字符串。有效期从当天00:00:00开始,到指定天数后的23:59:59结束", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "授权码管理" + ], + "summary": "创建授权码", + "parameters": [ + { + "description": "授权码信息", + "name": "authorization_code", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.AuthorizationCodeCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.AuthorizationCodeCreateResponse" + } + } + } + ] + } + }, + "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" + } + } + } + } + }, "/api/v1/login": { "post": { "description": "管理员用户登录接口", @@ -856,6 +937,68 @@ } } }, + "models.AuthorizationCodeCreateRequest": { + "type": "object", + "required": [ + "customer_id", + "deployment_type", + "max_activations", + "validity_days" + ], + "properties": { + "custom_parameters": {}, + "customer_id": { + "type": "string" + }, + "deployment_type": { + "type": "string", + "enum": [ + "standalone", + "cloud", + "hybrid" + ] + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "encryption_type": { + "type": "string", + "enum": [ + "standard", + "advanced" + ] + }, + "feature_config": {}, + "max_activations": { + "type": "integer", + "minimum": 1 + }, + "software_id": { + "type": "string" + }, + "software_version": { + "type": "string" + }, + "usage_limits": {}, + "validity_days": { + "type": "integer", + "maximum": 36500, + "minimum": 1 + } + } + }, + "models.AuthorizationCodeCreateResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, "models.Customer": { "type": "object", "properties": { diff --git a/backend/docs/swagger/swagger.yaml b/backend/docs/swagger/swagger.yaml index e3fbcc8..e2c8738 100644 --- a/backend/docs/swagger/swagger.yaml +++ b/backend/docs/swagger/swagger.yaml @@ -10,6 +10,51 @@ definitions: timestamp: type: string type: object + models.AuthorizationCodeCreateRequest: + properties: + custom_parameters: {} + customer_id: + type: string + deployment_type: + enum: + - standalone + - cloud + - hybrid + type: string + description: + maxLength: 1000 + type: string + encryption_type: + enum: + - standard + - advanced + type: string + feature_config: {} + max_activations: + minimum: 1 + type: integer + software_id: + type: string + software_version: + type: string + usage_limits: {} + validity_days: + maximum: 36500 + minimum: 1 + type: integer + required: + - customer_id + - deployment_type + - max_activations + - validity_days + type: object + models.AuthorizationCodeCreateResponse: + properties: + code: + type: string + id: + type: string + type: object models.Customer: properties: address: @@ -778,6 +823,55 @@ paths: summary: 刷新Token tags: - 认证 + /api/v1/authorization-codes: + post: + consumes: + - application/json + description: 创建新的授权码,自动生成授权码字符串。有效期从当天00:00:00开始,到指定天数后的23:59:59结束 + parameters: + - description: 授权码信息 + in: body + name: authorization_code + required: true + schema: + $ref: '#/definitions/models.AuthorizationCodeCreateRequest' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/models.APIResponse' + - properties: + data: + $ref: '#/definitions/models.AuthorizationCodeCreateResponse' + 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: diff --git a/backend/internal/api/handlers/authorization_code_handler.go b/backend/internal/api/handlers/authorization_code_handler.go new file mode 100644 index 0000000..cc1b52e --- /dev/null +++ b/backend/internal/api/handlers/authorization_code_handler.go @@ -0,0 +1,156 @@ +package handlers + +import ( + "errors" + "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 AuthorizationCodeHandler struct { + authCodeService service.AuthorizationCodeService +} + +// NewAuthorizationCodeHandler 创建授权码处理器 +func NewAuthorizationCodeHandler(authCodeService service.AuthorizationCodeService) *AuthorizationCodeHandler { + return &AuthorizationCodeHandler{ + authCodeService: authCodeService, + } +} + +// CreateAuthorizationCode 创建授权码 +// @Summary 创建授权码 +// @Description 创建新的授权码,自动生成授权码字符串。有效期从当天00:00:00开始,到指定天数后的23:59:59结束 +// @Tags 授权码管理 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param authorization_code body models.AuthorizationCodeCreateRequest true "授权码信息" +// @Success 200 {object} models.APIResponse{data=models.AuthorizationCodeCreateResponse} "创建成功" +// @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/authorization-codes [post] +func (h *AuthorizationCodeHandler) CreateAuthorizationCode(c *gin.Context) { + var req models.AuthorizationCodeCreateRequest + 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.authCodeService.CreateAuthorizationCode(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, + }) +} + +// GetAuthorizationCodeList 查询授权码列表 +// @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 customer_id query string false "客户ID筛选" +// @Param status query string false "状态筛选" Enums(normal, locked, expired) +// @Param start_date query string false "创建开始时间" +// @Param end_date query string false "创建结束时间" +// @Param sort query string false "排序字段,默认created_at" Enums(created_at, updated_at, code) +// @Param order query string false "排序方向,默认desc" Enums(asc, desc) +// @Success 200 {object} models.APIResponse{data=models.AuthorizationCodeListResponse} "查询成功" +// @Failure 400 {object} models.ErrorResponse "请求参数无效" +// @Failure 401 {object} models.ErrorResponse "未认证" +// @Failure 500 {object} models.ErrorResponse "服务器内部错误" +// @Router /api/v1/authorization-codes [get] +func (h *AuthorizationCodeHandler) GetAuthorizationCodeList(c *gin.Context) { + var req models.AuthorizationCodeListRequest + 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.GetAuthorizationCodeList(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, + }) +} \ No newline at end of file diff --git a/backend/internal/api/routes/router.go b/backend/internal/api/routes/router.go index e6d7ac9..a44fcdb 100644 --- a/backend/internal/api/routes/router.go +++ b/backend/internal/api/routes/router.go @@ -37,18 +37,21 @@ func SetupRouter() *gin.Engine { db := database.GetDB() customerRepo := repository.NewCustomerRepository(db) userRepo := repository.NewUserRepository(db) + authCodeRepo := repository.NewAuthorizationCodeRepository(db) // 初始化服务层 authService := service.NewAuthService(userRepo) systemService := service.NewSystemService() customerService := service.NewCustomerService(customerRepo) enumService := service.NewEnumService() + authCodeService := service.NewAuthorizationCodeService(authCodeRepo) // 初始化处理器层 authHandler := handlers.NewAuthHandler(authService) systemHandler := handlers.NewSystemHandler(systemService) customerHandler := handlers.NewCustomerHandler(customerService) enumHandler := handlers.NewEnumHandler(enumService) + authCodeHandler := handlers.NewAuthorizationCodeHandler(authCodeService) // 健康检测接口(无需认证) router.GET("/health", systemHandler.HealthCheck) @@ -84,6 +87,10 @@ func SetupRouter() *gin.Engine { // 枚举管理 auth.GET("/enums", enumHandler.GetAllEnums) auth.GET("/enums/:type", enumHandler.GetEnumsByType) + + // 授权码管理 + auth.GET("/v1/authorization-codes", authCodeHandler.GetAuthorizationCodeList) + auth.POST("/v1/authorization-codes", authCodeHandler.CreateAuthorizationCode) } // 管理员接口 diff --git a/backend/internal/database/migration.go b/backend/internal/database/migration.go index 0d464ab..2a113f9 100644 --- a/backend/internal/database/migration.go +++ b/backend/internal/database/migration.go @@ -23,6 +23,7 @@ func AutoMigrate() error { &models.Customer{}, &models.CustomerCodeSequence{}, &models.User{}, + &models.AuthorizationCode{}, ) if err != nil { return fmt.Errorf("failed to migrate database: %w", err) diff --git a/backend/internal/models/auth.go b/backend/internal/models/auth.go index 7305fe9..4811014 100644 --- a/backend/internal/models/auth.go +++ b/backend/internal/models/auth.go @@ -1,5 +1,12 @@ package models +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + // LoginRequest 登录请求结构 type LoginRequest struct { Username string `json:"username" binding:"required"` @@ -24,4 +31,125 @@ type LoginData struct { type UserInfo struct { Username string `json:"username"` Role string `json:"role"` +} + +// AuthorizationCode 授权码模型 +type AuthorizationCode struct { + ID string `gorm:"type:varchar(36);primaryKey" json:"id"` + Code string `gorm:"type:varchar(100);uniqueIndex;not null" json:"code"` + CustomerID string `gorm:"type:varchar(36);not null;index" json:"customer_id"` + CustomerName string `gorm:"-" json:"customer_name,omitempty"` + CustomerNameDisplay string `gorm:"-" json:"customer_name_display,omitempty"` + CreatedBy string `gorm:"type:varchar(36);not null" json:"created_by"` + SoftwareID *string `gorm:"type:varchar(50)" json:"software_id"` + Description *string `gorm:"type:text" json:"description"` + StartDate time.Time `gorm:"type:datetime(3);not null" json:"start_date"` + EndDate time.Time `gorm:"type:datetime(3);not null" json:"end_date"` + DeploymentType string `gorm:"type:varchar(20);not null;default:'standalone'" json:"deployment_type"` + DeploymentTypeDisplay string `gorm:"-" json:"deployment_type_display,omitempty"` + EncryptionType *string `gorm:"type:varchar(20);default:'standard'" json:"encryption_type"` + EncryptionTypeDisplay string `gorm:"-" json:"encryption_type_display,omitempty"` + SoftwareVersion *string `gorm:"type:varchar(50)" json:"software_version"` + MaxActivations int `gorm:"not null;default:1" json:"max_activations"` + CurrentActivations int `gorm:"-" json:"current_activations,omitempty"` + IsLocked bool `gorm:"not null;default:false" json:"is_locked"` + LockReason *string `gorm:"type:text" json:"lock_reason"` + LockedAt *time.Time `gorm:"type:datetime(3)" json:"locked_at"` + LockedBy *string `gorm:"type:varchar(36)" json:"locked_by"` + FeatureConfig JSON `gorm:"type:json" json:"feature_config"` + UsageLimits JSON `gorm:"type:json" json:"usage_limits"` + CustomParameters JSON `gorm:"type:json" json:"custom_parameters"` + Status string `gorm:"-" json:"status,omitempty"` + StatusDisplay string `gorm:"-" json:"status_display,omitempty"` + CreatedAt time.Time `gorm:"type:datetime(3);not null" json:"created_at"` + UpdatedAt time.Time `gorm:"type:datetime(3);not null" json:"updated_at"` +} + +// TableName 指定表名 +func (AuthorizationCode) TableName() string { + return "authorization_codes" +} + +// BeforeCreate 创建前自动设置时间戳和ID +func (a *AuthorizationCode) BeforeCreate(tx *gorm.DB) error { + if a.ID == "" { + a.ID = uuid.New().String() + } + + now := time.Now() + if a.CreatedAt.IsZero() { + a.CreatedAt = now + } + if a.UpdatedAt.IsZero() { + a.UpdatedAt = now + } + return nil +} + +// BeforeUpdate 更新前自动刷新更新时间 +func (a *AuthorizationCode) BeforeUpdate(tx *gorm.DB) error { + a.UpdatedAt = time.Now() + return nil +} + +// AuthorizationCodeCreateRequest 创建授权码请求结构 +type AuthorizationCodeCreateRequest struct { + CustomerID string `json:"customer_id" binding:"required"` + SoftwareID *string `json:"software_id" binding:"omitempty"` + Description *string `json:"description" binding:"omitempty,max=1000"` + ValidityDays int `json:"validity_days" binding:"required,min=1,max=36500"` + DeploymentType string `json:"deployment_type" binding:"required,oneof=standalone cloud hybrid"` + EncryptionType *string `json:"encryption_type" binding:"omitempty,oneof=standard advanced"` + SoftwareVersion *string `json:"software_version" binding:"omitempty"` + MaxActivations int `json:"max_activations" binding:"required,min=1"` + FeatureConfig interface{} `json:"feature_config" binding:"omitempty"` + UsageLimits interface{} `json:"usage_limits" binding:"omitempty"` + CustomParameters interface{} `json:"custom_parameters" binding:"omitempty"` +} + +// AuthorizationCodeCreateResponse 创建授权码响应结构 +type AuthorizationCodeCreateResponse struct { + ID string `json:"id"` + Code string `json:"code"` +} + +// AuthorizationCodeListRequest 授权码列表查询请求结构 +type AuthorizationCodeListRequest struct { + Page int `form:"page" binding:"omitempty,min=1"` // 页码,默认1 + PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"` // 每页条数,默认20,最大100 + CustomerID string `form:"customer_id" binding:"omitempty"` // 客户ID筛选 + Status string `form:"status" binding:"omitempty,oneof=normal locked expired"` // 状态筛选 + StartDate string `form:"start_date" binding:"omitempty"` // 创建开始时间 + EndDate string `form:"end_date" binding:"omitempty"` // 创建结束时间 + Sort string `form:"sort" binding:"omitempty,oneof=created_at updated_at code"` // 排序字段,默认created_at + Order string `form:"order" binding:"omitempty,oneof=asc desc"` // 排序方向,默认desc +} + +// AuthorizationCodeListItem 授权码列表项结构 +type AuthorizationCodeListItem struct { + ID string `json:"id"` + Code string `json:"code"` + CustomerID string `json:"customer_id"` + CustomerName string `json:"customer_name"` + CustomerNameDisplay string `json:"customer_name_display,omitempty"` + Status string `json:"status"` + StatusDisplay string `json:"status_display,omitempty"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + MaxActivations int `json:"max_activations"` + CurrentActivations int `json:"current_activations"` + DeploymentType string `json:"deployment_type"` + DeploymentTypeDisplay string `json:"deployment_type_display,omitempty"` + IsLocked bool `json:"is_locked"` + Description *string `json:"description"` + CreatedAt string `json:"created_at"` +} + +// AuthorizationCodeListResponse 授权码列表响应结构 +type AuthorizationCodeListResponse struct { + List []AuthorizationCodeListItem `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` } \ No newline at end of file diff --git a/backend/internal/models/common.go b/backend/internal/models/common.go index a8f7a87..9aa062b 100644 --- a/backend/internal/models/common.go +++ b/backend/internal/models/common.go @@ -1,5 +1,58 @@ package models +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +// JSON 类型用于GORM JSON字段 +type JSON json.RawMessage + +// 实现 driver.Valuer 接口 +func (j JSON) Value() (driver.Value, error) { + if len(j) == 0 { + return nil, nil + } + return json.RawMessage(j).MarshalJSON() +} + +// 实现 sql.Scanner 接口 +func (j *JSON) Scan(value interface{}) error { + if value == nil { + *j = nil + return nil + } + var bytes []byte + switch v := value.(type) { + case []byte: + bytes = v + case string: + bytes = []byte(v) + default: + return errors.New("cannot scan into JSON") + } + *j = append((*j)[0:0], bytes...) + return nil +} + +// MarshalJSON 实现 json.Marshaler 接口 +func (j JSON) MarshalJSON() ([]byte, error) { + if len(j) == 0 { + return []byte("null"), nil + } + return j, nil +} + +// UnmarshalJSON 实现 json.Unmarshaler 接口 +func (j *JSON) UnmarshalJSON(data []byte) error { + if j == nil { + return errors.New("JSON receiver is nil") + } + *j = append((*j)[0:0], data...) + return nil +} + // APIResponse 通用API响应结构 type APIResponse struct { Code string `json:"code"` diff --git a/backend/internal/repository/authorization_code_repository.go b/backend/internal/repository/authorization_code_repository.go new file mode 100644 index 0000000..cbad522 --- /dev/null +++ b/backend/internal/repository/authorization_code_repository.go @@ -0,0 +1,150 @@ +package repository + +import ( + "context" + "license-manager/internal/models" + + "gorm.io/gorm" +) + +type authorizationCodeRepository struct { + db *gorm.DB +} + +// NewAuthorizationCodeRepository 创建授权码仓储实例 +func NewAuthorizationCodeRepository(db *gorm.DB) AuthorizationCodeRepository { + return &authorizationCodeRepository{ + db: db, + } +} + +// CreateAuthorizationCode 创建授权码 +func (r *authorizationCodeRepository) CreateAuthorizationCode(ctx context.Context, authCode *models.AuthorizationCode) error { + return r.db.WithContext(ctx).Create(authCode).Error +} + +// GetAuthorizationCodeByID 根据ID获取授权码 +func (r *authorizationCodeRepository) GetAuthorizationCodeByID(ctx context.Context, id string) (*models.AuthorizationCode, error) { + var authCode models.AuthorizationCode + err := r.db.WithContext(ctx).First(&authCode, "id = ?", id).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrAuthorizationCodeNotFound + } + return nil, err + } + return &authCode, nil +} + +// GetAuthorizationCodeList 查询授权码列表 +func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Context, req *models.AuthorizationCodeListRequest) (*models.AuthorizationCodeListResponse, error) { + // 设置默认值 + page := req.Page + if page <= 0 { + page = 1 + } + pageSize := req.PageSize + if pageSize <= 0 { + pageSize = 20 + } + sort := req.Sort + if sort == "" { + sort = "created_at" + } + order := req.Order + if order == "" { + order = "desc" + } + + // 构建查询 + query := r.db.WithContext(ctx).Table("authorization_codes ac"). + 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") + + // 添加筛选条件 + if req.CustomerID != "" { + query = query.Where("ac.customer_id = ?", req.CustomerID) + } + + // 状态筛选需要在应用层处理,因为status是计算字段 + if req.StartDate != "" { + query = query.Where("ac.created_at >= ?", req.StartDate) + } + if req.EndDate != "" { + query = query.Where("ac.created_at <= ?", req.EndDate) + } + + // 获取总数 + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, err + } + + // 分页查询 + var results []struct { + ID string `json:"id"` + Code string `json:"code"` + CustomerID string `json:"customer_id"` + CustomerName *string `json:"customer_name"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + MaxActivations int `json:"max_activations"` + DeploymentType string `json:"deployment_type"` + IsLocked bool `json:"is_locked"` + Description *string `json:"description"` + CreatedAt string `json:"created_at"` + } + + offset := (page - 1) * pageSize + orderClause := sort + " " + order + if err := query.Order(orderClause).Limit(pageSize).Offset(offset).Scan(&results).Error; err != nil { + return nil, err + } + + // 转换为响应格式 + list := make([]models.AuthorizationCodeListItem, len(results)) + for i, result := range results { + customerName := "" + if result.CustomerName != nil { + customerName = *result.CustomerName + } + + list[i] = models.AuthorizationCodeListItem{ + ID: result.ID, + Code: result.Code, + CustomerID: result.CustomerID, + CustomerName: customerName, + StartDate: result.StartDate, + EndDate: result.EndDate, + MaxActivations: result.MaxActivations, + CurrentActivations: 0, // TODO: 从licenses表统计 + DeploymentType: result.DeploymentType, + IsLocked: result.IsLocked, + Description: result.Description, + CreatedAt: result.CreatedAt, + } + } + + totalPages := int((total + int64(pageSize) - 1) / int64(pageSize)) + + return &models.AuthorizationCodeListResponse{ + List: list, + Total: total, + Page: page, + PageSize: pageSize, + TotalPages: totalPages, + }, nil +} + +// CheckCustomerExists 检查客户是否存在 +func (r *authorizationCodeRepository) CheckCustomerExists(ctx context.Context, customerID string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&models.Customer{}).Where("id = ? AND deleted_at IS NULL", customerID).Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} \ No newline at end of file diff --git a/backend/internal/repository/errors.go b/backend/internal/repository/errors.go index 5e4cd08..2477ed2 100644 --- a/backend/internal/repository/errors.go +++ b/backend/internal/repository/errors.go @@ -9,6 +9,13 @@ var ( ErrCustomerCodeDuplicate = errors.New("customer code already exists") ) +// 授权码领域的业务错误 +var ( + ErrAuthorizationCodeNotFound = errors.New("authorization code not found") + ErrAuthorizationCodeAlreadyExists = errors.New("authorization code already exists") + ErrAuthorizationCodeDuplicate = errors.New("authorization code already exists") +) + // 通用的数据库/系统错误(给开发者看的) var ( ErrDatabaseConnection = errors.New("database connection failed") diff --git a/backend/internal/repository/interfaces.go b/backend/internal/repository/interfaces.go index 2289ae5..a37d444 100644 --- a/backend/internal/repository/interfaces.go +++ b/backend/internal/repository/interfaces.go @@ -45,4 +45,19 @@ type UserRepository interface { // LockUser 锁定用户账号 LockUser(ctx context.Context, id string, lockDuration int) error +} + +// AuthorizationCodeRepository 授权码数据访问接口 +type AuthorizationCodeRepository interface { + // CreateAuthorizationCode 创建授权码 + CreateAuthorizationCode(ctx context.Context, authCode *models.AuthorizationCode) error + + // GetAuthorizationCodeByID 根据ID获取授权码 + GetAuthorizationCodeByID(ctx context.Context, id string) (*models.AuthorizationCode, error) + + // GetAuthorizationCodeList 查询授权码列表 + GetAuthorizationCodeList(ctx context.Context, req *models.AuthorizationCodeListRequest) (*models.AuthorizationCodeListResponse, error) + + // CheckCustomerExists 检查客户是否存在 + CheckCustomerExists(ctx context.Context, customerID string) (bool, error) } \ No newline at end of file diff --git a/backend/internal/service/authorization_code_service.go b/backend/internal/service/authorization_code_service.go new file mode 100644 index 0000000..c9e2408 --- /dev/null +++ b/backend/internal/service/authorization_code_service.go @@ -0,0 +1,225 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/json" + "fmt" + "strings" + "time" + + "license-manager/internal/models" + "license-manager/internal/repository" + pkgcontext "license-manager/pkg/context" + "license-manager/pkg/i18n" +) + +type authorizationCodeService struct { + authCodeRepo repository.AuthorizationCodeRepository +} + +// NewAuthorizationCodeService 创建授权码服务实例 +func NewAuthorizationCodeService(authCodeRepo repository.AuthorizationCodeRepository) AuthorizationCodeService { + return &authorizationCodeService{ + authCodeRepo: authCodeRepo, + } +} + +// CreateAuthorizationCode 创建授权码 +func (s *authorizationCodeService) CreateAuthorizationCode(ctx context.Context, req *models.AuthorizationCodeCreateRequest) (*models.AuthorizationCodeCreateResponse, error) { + lang := pkgcontext.GetLanguageFromContext(ctx) + + // 业务逻辑:参数验证 + if req == nil { + return nil, i18n.NewI18nError("900001", lang) + } + + // 验证客户是否存在 + exists, err := s.authCodeRepo.CheckCustomerExists(ctx, req.CustomerID) + if err != nil { + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + if !exists { + return nil, i18n.NewI18nError("200001", lang) // 客户不存在 + } + + // 业务逻辑:计算开始时间和结束时间 + now := time.Now() + startDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // 当天00:00:00开始 + endDate := startDate.AddDate(0, 0, req.ValidityDays-1).Add(23*time.Hour + 59*time.Minute + 59*time.Second) // 有效期最后一天的23:59:59结束 + + // 获取当前用户ID + currentUserID := "admin_uuid" // TODO: 从JWT token中获取 + + // 生成授权码 + authCode, err := s.generateAuthorizationCode(req.CustomerID) + if err != nil { + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + // 处理JSON字段 + var featureConfig, usageLimits, customParameters models.JSON + if req.FeatureConfig != nil { + featureConfigBytes, err := json.Marshal(req.FeatureConfig) + if err != nil { + return nil, i18n.NewI18nError("300010", lang) // 配置参数错误 + } + featureConfig = models.JSON(featureConfigBytes) + } + if req.UsageLimits != nil { + usageLimitsBytes, err := json.Marshal(req.UsageLimits) + if err != nil { + return nil, i18n.NewI18nError("300010", lang) // 配置参数错误 + } + usageLimits = models.JSON(usageLimitsBytes) + } + if req.CustomParameters != nil { + customParametersBytes, err := json.Marshal(req.CustomParameters) + if err != nil { + return nil, i18n.NewI18nError("300010", lang) // 配置参数错误 + } + customParameters = models.JSON(customParametersBytes) + } + + // 设置默认加密类型 + encryptionType := req.EncryptionType + if encryptionType == nil { + defaultEncryption := "standard" + encryptionType = &defaultEncryption + } + + // 构建授权码实体 + authCodeEntity := &models.AuthorizationCode{ + Code: authCode, + CustomerID: req.CustomerID, + CreatedBy: currentUserID, + SoftwareID: req.SoftwareID, + Description: req.Description, + StartDate: startDate, + EndDate: endDate, + DeploymentType: req.DeploymentType, + EncryptionType: encryptionType, + SoftwareVersion: req.SoftwareVersion, + MaxActivations: req.MaxActivations, + IsLocked: false, + FeatureConfig: featureConfig, + UsageLimits: usageLimits, + CustomParameters: customParameters, + } + + // 委托给Repository层进行数据创建 + if err := s.authCodeRepo.CreateAuthorizationCode(ctx, authCodeEntity); err != nil { + // 根据错误类型包装为完整的I18nError + if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") { + return nil, i18n.NewI18nError("300002", lang) // 授权码已存在 + } + + // 数据库相关错误 + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + return &models.AuthorizationCodeCreateResponse{ + ID: authCodeEntity.ID, + Code: authCodeEntity.Code, + }, nil +} + +// GetAuthorizationCodeList 查询授权码列表 +func (s *authorizationCodeService) GetAuthorizationCodeList(ctx context.Context, req *models.AuthorizationCodeListRequest) (*models.AuthorizationCodeListResponse, error) { + lang := pkgcontext.GetLanguageFromContext(ctx) + + // 业务逻辑:参数验证 + if req == nil { + return nil, i18n.NewI18nError("900001", lang) + } + + // 委托给Repository层进行数据访问 + result, err := s.authCodeRepo.GetAuthorizationCodeList(ctx, req) + if err != nil { + // 数据库相关错误 + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + // 业务逻辑:添加多语言显示字段和状态计算 + now := time.Now() + for i := range result.List { + item := &result.List[i] + s.fillAuthCodeDisplayFields(item, lang, now) + + // 根据状态筛选 + if req.Status != "" && item.Status != req.Status { + // 移除不匹配的项 - 这里简化处理,实际应该在查询时过滤 + continue + } + } + + return result, nil +} + +// fillAuthCodeDisplayFields 填充授权码列表项多语言显示字段 +func (s *authorizationCodeService) fillAuthCodeDisplayFields(item *models.AuthorizationCodeListItem, lang string, now time.Time) { + // 计算状态 + endDate, _ := time.Parse("2006-01-02T15:04:05Z07:00", item.EndDate) + + if item.IsLocked { + item.Status = "locked" + } else if now.After(endDate) { + item.Status = "expired" + } else { + item.Status = "normal" + } + + // 添加多语言显示字段 + item.StatusDisplay = i18n.GetEnumMessage("authorization_status", item.Status, lang) + item.DeploymentTypeDisplay = i18n.GetEnumMessage("deployment_type", item.DeploymentType, lang) + item.CustomerNameDisplay = item.CustomerName // 客户名称暂时不需要翻译 +} + +// generateAuthorizationCode 生成授权码 +func (s *authorizationCodeService) generateAuthorizationCode(customerID string) (string, error) { + // 获取客户编码的前4位作为前缀 + customerCode := "COMP" // 默认前缀 + if len(customerID) >= 4 { + customerCode = strings.ToUpper(customerID[:4]) + } + + // 生成8位随机字符串 + randomStr, err := s.generateRandomString(8) + if err != nil { + return "", err + } + + // 生成4位校验码 + checksum := s.generateChecksum(customerCode + randomStr) + + // 格式: LIC-{customer_code}-{random}-{checksum} + return fmt.Sprintf("LIC-%s-%s-%s", customerCode, randomStr, checksum), nil +} + +// generateRandomString 生成随机字符串 +func (s *authorizationCodeService) generateRandomString(length int) (string, error) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + for i, b := range bytes { + bytes[i] = chars[b%byte(len(chars))] + } + return string(bytes), nil +} + +// generateChecksum 生成校验码 +func (s *authorizationCodeService) generateChecksum(input string) string { + sum := 0 + for _, char := range input { + sum += int(char) + } + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + checksum := "" + for i := 0; i < 4; i++ { + checksum += string(chars[sum%len(chars)]) + sum = sum / len(chars) + } + return checksum +} diff --git a/backend/internal/service/interfaces.go b/backend/internal/service/interfaces.go index 4e4eccf..ddfd0de 100644 --- a/backend/internal/service/interfaces.go +++ b/backend/internal/service/interfaces.go @@ -33,3 +33,9 @@ type EnumService interface { GetAllEnums(ctx context.Context) (*models.EnumListResponse, error) GetEnumsByType(ctx context.Context, enumType string) (*models.EnumTypeResponse, error) } + +// AuthorizationCodeService 授权码服务接口 +type AuthorizationCodeService interface { + CreateAuthorizationCode(ctx context.Context, req *models.AuthorizationCodeCreateRequest) (*models.AuthorizationCodeCreateResponse, error) + GetAuthorizationCodeList(ctx context.Context, req *models.AuthorizationCodeListRequest) (*models.AuthorizationCodeListResponse, error) +} diff --git a/frontend/.env.development b/frontend/.env.development index 2384acc..d20afdc 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -7,7 +7,7 @@ # @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE ### # 开发环境接口地址 -VITE_API_BASE_URL = 'http://lm.cedar-v.com:18888' +VITE_API_BASE_URL = 'http://localhost:18888' # # 开发环境打包名称 # VITE_OUTDIR='dev'