From 795458f6d8baa27fbcfddc000d73e3c382b673e5 Mon Sep 17 00:00:00 2001 From: wood911010 Date: Tue, 23 Sep 2025 14:20:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E8=B6=8B=E5=8A=BF=E3=80=81=E6=9C=80=E8=BF=91=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/configs/i18n/errors/en-US.yaml | 21 ++ backend/configs/i18n/errors/ja-JP.yaml | 21 ++ backend/configs/i18n/errors/zh-CN.yaml | 21 ++ backend/docs/api_development_guide.md | 5 +- backend/docs/backend_design.md | 17 +- backend/docs/demand/2025.9.23-仪表盘api.md | 125 +++++++ backend/docs/swagger/docs.go | 343 ++++++++++++++++++ backend/docs/swagger/swagger.json | 343 ++++++++++++++++++ backend/docs/swagger/swagger.yaml | 225 ++++++++++++ .../api/handlers/dashboard_handler.go | 158 ++++++++ backend/internal/api/routes/router.go | 7 + backend/internal/models/dashboard.go | 72 ++++ .../repository/dashboard_repository.go | 206 +++++++++++ backend/internal/repository/interfaces.go | 10 + backend/internal/service/dashboard_service.go | 174 +++++++++ backend/internal/service/interfaces.go | 9 + 16 files changed, 1755 insertions(+), 2 deletions(-) create mode 100644 backend/docs/demand/2025.9.23-仪表盘api.md create mode 100644 backend/internal/api/handlers/dashboard_handler.go create mode 100644 backend/internal/models/dashboard.go create mode 100644 backend/internal/repository/dashboard_repository.go create mode 100644 backend/internal/service/dashboard_service.go diff --git a/backend/configs/i18n/errors/en-US.yaml b/backend/configs/i18n/errors/en-US.yaml index eae2924..9c5e27b 100644 --- a/backend/configs/i18n/errors/en-US.yaml +++ b/backend/configs/i18n/errors/en-US.yaml @@ -35,6 +35,15 @@ errors: "300009": "Failed to generate license file" "300010": "Invalid configuration parameters" + # Dashboard module (40xxxx) + dashboard: + "400001": "Invalid time type parameter" + "400002": "Invalid start date format" + "400003": "Invalid end date format" + "400004": "Time range exceeds limit (over one year)" + "400005": "Start date cannot be later than end date" + "400006": "Invalid limit parameter" + # System general (90xxxx) system: "900001": "Invalid request parameters" @@ -42,14 +51,26 @@ errors: "900003": "Resource conflict" "900004": "Internal server error" +# Common text +common: + success: "Success" + # Enum translations enums: + dashboard_period: + week: "This Week" + month: "This Month" customer_type: "individual": "Individual" "enterprise": "Enterprise" "government": "Government" "education": "Education" + authorization_code_status: + normal: "Normal" + locked: "Locked" + expired: "Expired" + customer_level: "normal": "Normal" "vip": "VIP" diff --git a/backend/configs/i18n/errors/ja-JP.yaml b/backend/configs/i18n/errors/ja-JP.yaml index 3a5a28e..155d718 100644 --- a/backend/configs/i18n/errors/ja-JP.yaml +++ b/backend/configs/i18n/errors/ja-JP.yaml @@ -35,6 +35,15 @@ errors: "300009": "ライセンスファイルの生成に失敗しました" "300010": "無効な設定パラメータ" + # ダッシュボードモジュール (40xxxx) + dashboard: + "400001": "無効な時間タイプパラメータ" + "400002": "無効な開始日付形式" + "400003": "無効な終了日付形式" + "400004": "時間範囲が制限を超えています(1年以上)" + "400005": "開始日は終了日より後にできません" + "400006": "無効な制限パラメータ" + # システム共通 (90xxxx) system: "900001": "無効なリクエストパラメータ" @@ -42,14 +51,26 @@ errors: "900003": "リソースの競合" "900004": "内部サーバーエラー" +# 共通テキスト +common: + success: "成功" + # 列挙値多言語 enums: + dashboard_period: + week: "今週" + month: "今月" customer_type: "individual": "個人顧客" "enterprise": "企業顧客" "government": "政府顧客" "education": "教育顧客" + authorization_code_status: + normal: "正常" + locked: "ロック済み" + expired: "期限切れ" + customer_level: "normal": "一般顧客" "vip": "VIP顧客" diff --git a/backend/configs/i18n/errors/zh-CN.yaml b/backend/configs/i18n/errors/zh-CN.yaml index 2ec0acc..c3bda6d 100644 --- a/backend/configs/i18n/errors/zh-CN.yaml +++ b/backend/configs/i18n/errors/zh-CN.yaml @@ -35,6 +35,15 @@ errors: "300009": "许可证文件生成失败" "300010": "配置参数错误" + # 仪表盘模块 (40xxxx) + dashboard: + "400001": "时间类型参数错误" + "400002": "开始日期格式错误" + "400003": "结束日期格式错误" + "400004": "时间范围超过限制(超过一年)" + "400005": "开始日期不能晚于结束日期" + "400006": "数量限制参数错误" + # 系统通用 (90xxxx) system: "900001": "请求参数无效" @@ -42,14 +51,26 @@ errors: "900003": "资源冲突" "900004": "服务器内部错误" +# 通用文本 +common: + success: "成功" + # 枚举值多语言 enums: + dashboard_period: + week: "本周" + month: "本月" customer_type: "individual": "个人客户" "enterprise": "企业客户" "government": "政府客户" "education": "教育客户" + authorization_code_status: + normal: "正常" + locked: "已锁定" + expired: "已过期" + customer_level: "normal": "普通客户" "vip": "VIP客户" diff --git a/backend/docs/api_development_guide.md b/backend/docs/api_development_guide.md index 34ab897..78ab709 100644 --- a/backend/docs/api_development_guide.md +++ b/backend/docs/api_development_guide.md @@ -2,6 +2,8 @@ ## 核心要点 +一定要阅读customer模块的实现,看看其他模块的实,明白编码细节,现然后再进行开发。 + ### 1. 分层架构 ``` internal/ @@ -77,4 +79,5 @@ internal/ - 分层职责是否清晰 - 多语言字段是否完整 - 错误处理是否统一 -- Swagger注解是否完整 \ No newline at end of file +- Swagger注解是否完整 +- 避免不必要的冗余,能写到工具函数的不要写在模块内 \ No newline at end of file diff --git a/backend/docs/backend_design.md b/backend/docs/backend_design.md index e5937df..dd04b5a 100644 --- a/backend/docs/backend_design.md +++ b/backend/docs/backend_design.md @@ -25,8 +25,10 @@ backend/ │ ├── api/ │ │ ├── handlers/ # HTTP处理器 │ │ │ ├── auth_handler.go +│ │ │ ├── authorization_code_handler.go │ │ │ ├── customer_handler.go │ │ │ ├── enum_handler.go +│ │ │ ├── license_handler.go │ │ │ └── system_handler.go │ │ ├── middleware/ # 中间件 │ │ │ ├── auth.go @@ -37,19 +39,26 @@ backend/ │ │ └── router.go │ ├── service/ # 业务逻辑层 │ │ ├── auth_service.go +│ │ ├── authorization_code_service.go │ │ ├── customer_service.go │ │ ├── enum_service.go +│ │ ├── license_service.go │ │ ├── system_service.go │ │ └── interfaces.go # 接口定义 │ ├── repository/ # 数据访问层 +│ │ ├── authorization_code_repository.go │ │ ├── customer_repository.go +│ │ ├── license_repository.go +│ │ ├── user_repository.go │ │ ├── errors.go │ │ └── interfaces.go │ ├── models/ # 数据模型 │ │ ├── auth.go +│ │ ├── common.go │ │ ├── customer.go +│ │ ├── license.go │ │ ├── system.go -│ │ └── common.go +│ │ └── user.go │ ├── config/ # 配置管理 │ │ └── config.go │ └── database/ # 数据库连接和迁移 @@ -85,9 +94,11 @@ backend/ │ │ ├── db_install.md │ │ └── swagger_install.md │ ├── modules_design/ +│ │ ├── auth_api.md │ │ ├── customer-api.md │ │ ├── customer.md │ │ ├── login&auth.md +│ │ ├── user.md │ │ ├── user_story.md │ │ ├── 产品原型设计方案.md │ │ └── 授权模块属性设计.md @@ -98,6 +109,10 @@ backend/ ├── migrations/ # 数据库迁移 │ ├── 001_create_customers_table.sql │ ├── 002_insert_sample_data.sql +│ ├── 003_create_authorization_codes_table.sql +│ ├── 004_create_licenses_table.sql +│ ├── 005_create_authorization_changes_table.sql +│ ├── 006_create_users_table.sql │ └── README.md ├── go.mod └── go.sum diff --git a/backend/docs/demand/2025.9.23-仪表盘api.md b/backend/docs/demand/2025.9.23-仪表盘api.md new file mode 100644 index 0000000..b007fc0 --- /dev/null +++ b/backend/docs/demand/2025.9.23-仪表盘api.md @@ -0,0 +1,125 @@ +# 仪表盘 API 设计 + +## 1. 授权趋势统计 API + +### 1.1 获取授权趋势数据 +```http +GET /api/v1/dashboard/authorization-trend +``` + +**查询参数** +- `type` - 时间类型 (week/month/custom) 必填 +- `start_date` - 开始日期 (YYYY-MM-DD格式,当type为custom时必填) +- `end_date` - 结束日期 (YYYY-MM-DD格式,当type为custom时必填) +- `timezone` - 时区 (可选,如:Asia/Shanghai、UTC等,默认使用服务器本地时区) + +**参数说明:** +- `week`: 返回本周每日的授权总数(周一到周日) +- `month`: 返回本月每日的授权总数(1号到当月最后一天) +- `custom`: 返回指定时间段内每日授权总数,时间间隔不超过一年 +- `timezone`: 指定时区,影响"本周"、"本月"以及自定义日期的计算基准。支持标准时区名称如Asia/Shanghai、America/New_York、UTC等 + +**响应** +```json +{ + "code": "000000", + "message": "成功", + "data": { + "period": { + "type": "week", + "start_date": "2024-01-01", + "end_date": "2024-01-07", + "description_display": "本周" + }, + "trend_data": [ + { + "date": "2024-01-01", + "total_authorizations": 15, + "new_authorizations": 3, + "expired_authorizations": 1 + }, + { + "date": "2024-01-02", + "total_authorizations": 17, + "new_authorizations": 2, + "expired_authorizations": 0 + } + ], + "summary": { + "total_count": 120, + "new_count": 15, + "expired_count": 5, + "growth_rate": 5.2 + } + } +} +``` + +**响应字段说明:** +- `description_display` - 多语言 + +## 2. 最近授权列表 API + +### 2.1 获取最近授权列表 +```http +GET /api/v1/dashboard/recent-authorizations +``` + +**查询参数** +- `limit` - 返回数量限制(默认20,最大100) +- `customer_id` - 客户ID筛选(可选) +- `status` - 状态筛选 (normal/locked/expired) 可选 + +**响应** +```json +{ + "code": "000000", + "message": "成功", + "data": { + "list": [ + { + "id": "uuid-string", + "code": "LIC-COMP001-A7B9X2-C8F4", + "customer_id": "customer-uuid", + "customer_name": "张三公司", + "description": "企业版授权", + "status": "normal", + "status_display": "正常", + "start_date": "2024-01-01T00:00:00Z", + "end_date": "2024-12-31T23:59:59Z", + "max_activations": 10, + "current_activations": 7, + "created_at": "2024-01-01T10:00:00Z", + "updated_at": "2024-01-01T10:00:00Z" + } + ], + "total": 20 + } +} +``` + +**响应字段说明:** +- `total` - 符合条件的授权总数量 +- `list` - 当前返回的授权列表数据 +- `status_display` - 多语言 + +## 3. 状态说明 + +### 3.1 授权码状态 +- `normal` - 正常:当前时间在有效期内且未被锁定 +- `locked` - 已锁定:is_locked=true +- `expired` - 已过期:当前时间超过end_date + +### 3.2 时间类型 +- `week` - 本周:从周一00:00:00到周日23:59:59 (基于指定时区) +- `month` - 本月:从1号00:00:00到当月最后一天23:59:59 (基于指定时区) +- `custom` - 自定义:指定开始和结束日期,最大跨度365天 (基于指定时区) + +## 4. 错误码定义 + +- `400001` - 时间类型参数错误 +- `400002` - 开始日期格式错误 +- `400003` - 结束日期格式错误 +- `400004` - 时间范围超过限制(超过一年) +- `400005` - 开始日期不能晚于结束日期 +- `400006` - 数量限制参数错误 \ No newline at end of file diff --git a/backend/docs/swagger/docs.go b/backend/docs/swagger/docs.go index 4dd404d..a248c82 100644 --- a/backend/docs/swagger/docs.go +++ b/backend/docs/swagger/docs.go @@ -1439,6 +1439,185 @@ const docTemplate = `{ } } }, + "/api/v1/dashboard/authorization-trend": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据时间类型(本周/本月/自定义)获取授权趋势统计数据", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "仪表盘" + ], + "summary": "获取授权趋势数据", + "parameters": [ + { + "enum": [ + "week", + "month", + "custom" + ], + "type": "string", + "description": "时间类型", + "name": "type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "开始日期(YYYY-MM-DD格式,当type为custom时必填)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "结束日期(YYYY-MM-DD格式,当type为custom时必填)", + "name": "end_date", + "in": "query" + } + ], + "responses": { + "200": { + "description": "授权趋势数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.DashboardAuthorizationTrendResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/dashboard/recent-authorizations": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取最近创建的授权码列表,支持按客户ID和状态筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "仪表盘" + ], + "summary": "获取最近授权列表", + "parameters": [ + { + "type": "integer", + "description": "返回数量限制(默认20,最大100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "客户ID筛选", + "name": "customer_id", + "in": "query" + }, + { + "enum": [ + "normal", + "locked", + "expired" + ], + "type": "string", + "description": "状态筛选", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "最近授权列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.DashboardRecentAuthorizationsResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/api/v1/heartbeat": { "post": { "description": "客户端定期发送心跳,更新在线状态和使用数据", @@ -2824,6 +3003,50 @@ const docTemplate = `{ } } }, + "models.DashboardAuthorizationTrendResponse": { + "type": "object", + "properties": { + "period": { + "description": "时间段信息", + "allOf": [ + { + "$ref": "#/definitions/models.TrendPeriod" + } + ] + }, + "summary": { + "description": "汇总信息", + "allOf": [ + { + "$ref": "#/definitions/models.TrendSummary" + } + ] + }, + "trend_data": { + "description": "趋势数据", + "type": "array", + "items": { + "$ref": "#/definitions/models.TrendData" + } + } + } + }, + "models.DashboardRecentAuthorizationsResponse": { + "type": "object", + "properties": { + "list": { + "description": "授权列表", + "type": "array", + "items": { + "$ref": "#/definitions/models.RecentAuthorization" + } + }, + "total": { + "description": "符合条件的授权总数量", + "type": "integer" + } + } + }, "models.EnumItem": { "type": "object", "properties": { @@ -3242,6 +3465,63 @@ const docTemplate = `{ } } }, + "models.RecentAuthorization": { + "type": "object", + "properties": { + "code": { + "description": "授权码", + "type": "string" + }, + "created_at": { + "description": "创建时间", + "type": "string" + }, + "current_activations": { + "description": "当前激活数量", + "type": "integer" + }, + "customer_id": { + "description": "客户ID", + "type": "string" + }, + "customer_name": { + "description": "客户名称", + "type": "string" + }, + "description": { + "description": "授权描述", + "type": "string" + }, + "end_date": { + "description": "到期时间", + "type": "string" + }, + "id": { + "description": "授权码ID", + "type": "string" + }, + "max_activations": { + "description": "最大激活数量", + "type": "integer" + }, + "start_date": { + "description": "生效时间", + "type": "string" + }, + "status": { + "description": "状态 (normal/locked/expired)", + "type": "string" + }, + "status_display": { + "description": "状态显示文本(多语言)", + "type": "string" + }, + "updated_at": { + "description": "更新时间", + "type": "string" + } + } + }, "models.StatsOverviewResponse": { "type": "object", "properties": { @@ -3290,6 +3570,69 @@ const docTemplate = `{ "type": "string" } } + }, + "models.TrendData": { + "type": "object", + "properties": { + "date": { + "description": "日期 (YYYY-MM-DD)", + "type": "string" + }, + "expired_authorizations": { + "description": "当日过期授权数", + "type": "integer" + }, + "new_authorizations": { + "description": "当日新增授权数", + "type": "integer" + }, + "total_authorizations": { + "description": "当日授权总数", + "type": "integer" + } + } + }, + "models.TrendPeriod": { + "type": "object", + "properties": { + "description_display": { + "description": "多语言描述", + "type": "string" + }, + "end_date": { + "description": "结束日期", + "type": "string" + }, + "start_date": { + "description": "开始日期", + "type": "string" + }, + "type": { + "description": "时间类型", + "type": "string" + } + } + }, + "models.TrendSummary": { + "type": "object", + "properties": { + "expired_count": { + "description": "期间过期授权数", + "type": "integer" + }, + "growth_rate": { + "description": "增长率(百分比)", + "type": "number" + }, + "new_count": { + "description": "期间新增授权数", + "type": "integer" + }, + "total_count": { + "description": "期间总授权数", + "type": "integer" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger/swagger.json b/backend/docs/swagger/swagger.json index 67a5f03..d392ea0 100644 --- a/backend/docs/swagger/swagger.json +++ b/backend/docs/swagger/swagger.json @@ -1436,6 +1436,185 @@ } } }, + "/api/v1/dashboard/authorization-trend": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据时间类型(本周/本月/自定义)获取授权趋势统计数据", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "仪表盘" + ], + "summary": "获取授权趋势数据", + "parameters": [ + { + "enum": [ + "week", + "month", + "custom" + ], + "type": "string", + "description": "时间类型", + "name": "type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "开始日期(YYYY-MM-DD格式,当type为custom时必填)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "结束日期(YYYY-MM-DD格式,当type为custom时必填)", + "name": "end_date", + "in": "query" + } + ], + "responses": { + "200": { + "description": "授权趋势数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.DashboardAuthorizationTrendResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/dashboard/recent-authorizations": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取最近创建的授权码列表,支持按客户ID和状态筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "仪表盘" + ], + "summary": "获取最近授权列表", + "parameters": [ + { + "type": "integer", + "description": "返回数量限制(默认20,最大100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "客户ID筛选", + "name": "customer_id", + "in": "query" + }, + { + "enum": [ + "normal", + "locked", + "expired" + ], + "type": "string", + "description": "状态筛选", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "最近授权列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.DashboardRecentAuthorizationsResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/api/v1/heartbeat": { "post": { "description": "客户端定期发送心跳,更新在线状态和使用数据", @@ -2821,6 +3000,50 @@ } } }, + "models.DashboardAuthorizationTrendResponse": { + "type": "object", + "properties": { + "period": { + "description": "时间段信息", + "allOf": [ + { + "$ref": "#/definitions/models.TrendPeriod" + } + ] + }, + "summary": { + "description": "汇总信息", + "allOf": [ + { + "$ref": "#/definitions/models.TrendSummary" + } + ] + }, + "trend_data": { + "description": "趋势数据", + "type": "array", + "items": { + "$ref": "#/definitions/models.TrendData" + } + } + } + }, + "models.DashboardRecentAuthorizationsResponse": { + "type": "object", + "properties": { + "list": { + "description": "授权列表", + "type": "array", + "items": { + "$ref": "#/definitions/models.RecentAuthorization" + } + }, + "total": { + "description": "符合条件的授权总数量", + "type": "integer" + } + } + }, "models.EnumItem": { "type": "object", "properties": { @@ -3239,6 +3462,63 @@ } } }, + "models.RecentAuthorization": { + "type": "object", + "properties": { + "code": { + "description": "授权码", + "type": "string" + }, + "created_at": { + "description": "创建时间", + "type": "string" + }, + "current_activations": { + "description": "当前激活数量", + "type": "integer" + }, + "customer_id": { + "description": "客户ID", + "type": "string" + }, + "customer_name": { + "description": "客户名称", + "type": "string" + }, + "description": { + "description": "授权描述", + "type": "string" + }, + "end_date": { + "description": "到期时间", + "type": "string" + }, + "id": { + "description": "授权码ID", + "type": "string" + }, + "max_activations": { + "description": "最大激活数量", + "type": "integer" + }, + "start_date": { + "description": "生效时间", + "type": "string" + }, + "status": { + "description": "状态 (normal/locked/expired)", + "type": "string" + }, + "status_display": { + "description": "状态显示文本(多语言)", + "type": "string" + }, + "updated_at": { + "description": "更新时间", + "type": "string" + } + } + }, "models.StatsOverviewResponse": { "type": "object", "properties": { @@ -3287,6 +3567,69 @@ "type": "string" } } + }, + "models.TrendData": { + "type": "object", + "properties": { + "date": { + "description": "日期 (YYYY-MM-DD)", + "type": "string" + }, + "expired_authorizations": { + "description": "当日过期授权数", + "type": "integer" + }, + "new_authorizations": { + "description": "当日新增授权数", + "type": "integer" + }, + "total_authorizations": { + "description": "当日授权总数", + "type": "integer" + } + } + }, + "models.TrendPeriod": { + "type": "object", + "properties": { + "description_display": { + "description": "多语言描述", + "type": "string" + }, + "end_date": { + "description": "结束日期", + "type": "string" + }, + "start_date": { + "description": "开始日期", + "type": "string" + }, + "type": { + "description": "时间类型", + "type": "string" + } + } + }, + "models.TrendSummary": { + "type": "object", + "properties": { + "expired_count": { + "description": "期间过期授权数", + "type": "integer" + }, + "growth_rate": { + "description": "增长率(百分比)", + "type": "number" + }, + "new_count": { + "description": "期间新增授权数", + "type": "integer" + }, + "total_count": { + "description": "期间总授权数", + "type": "integer" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger/swagger.yaml b/backend/docs/swagger/swagger.yaml index 41632c3..77078f3 100644 --- a/backend/docs/swagger/swagger.yaml +++ b/backend/docs/swagger/swagger.yaml @@ -513,6 +513,33 @@ definitions: - disabled type: string type: object + models.DashboardAuthorizationTrendResponse: + properties: + period: + allOf: + - $ref: '#/definitions/models.TrendPeriod' + description: 时间段信息 + summary: + allOf: + - $ref: '#/definitions/models.TrendSummary' + description: 汇总信息 + trend_data: + description: 趋势数据 + items: + $ref: '#/definitions/models.TrendData' + type: array + type: object + models.DashboardRecentAuthorizationsResponse: + properties: + list: + description: 授权列表 + items: + $ref: '#/definitions/models.RecentAuthorization' + type: array + total: + description: 符合条件的授权总数量 + type: integer + type: object models.EnumItem: properties: display: @@ -796,6 +823,48 @@ definitions: message: type: string type: object + models.RecentAuthorization: + properties: + code: + description: 授权码 + type: string + created_at: + description: 创建时间 + type: string + current_activations: + description: 当前激活数量 + type: integer + customer_id: + description: 客户ID + type: string + customer_name: + description: 客户名称 + type: string + description: + description: 授权描述 + type: string + end_date: + description: 到期时间 + type: string + id: + description: 授权码ID + type: string + max_activations: + description: 最大激活数量 + type: integer + start_date: + description: 生效时间 + type: string + status: + description: 状态 (normal/locked/expired) + type: string + status_display: + description: 状态显示文本(多语言) + type: string + updated_at: + description: 更新时间 + type: string + type: object models.StatsOverviewResponse: properties: abnormal_alerts: @@ -828,6 +897,51 @@ definitions: version: type: string type: object + models.TrendData: + properties: + date: + description: 日期 (YYYY-MM-DD) + type: string + expired_authorizations: + description: 当日过期授权数 + type: integer + new_authorizations: + description: 当日新增授权数 + type: integer + total_authorizations: + description: 当日授权总数 + type: integer + type: object + models.TrendPeriod: + properties: + description_display: + description: 多语言描述 + type: string + end_date: + description: 结束日期 + type: string + start_date: + description: 开始日期 + type: string + type: + description: 时间类型 + type: string + type: object + models.TrendSummary: + properties: + expired_count: + description: 期间过期授权数 + type: integer + growth_rate: + description: 增长率(百分比) + type: number + new_count: + description: 期间新增授权数 + type: integer + total_count: + description: 期间总授权数 + type: integer + type: object info: contact: email: support@swagger.io @@ -1724,6 +1838,117 @@ paths: summary: 锁定/解锁授权码 tags: - 授权码管理 + /api/v1/dashboard/authorization-trend: + get: + consumes: + - application/json + description: 根据时间类型(本周/本月/自定义)获取授权趋势统计数据 + parameters: + - description: 时间类型 + enum: + - week + - month + - custom + in: query + name: type + required: true + type: string + - description: 开始日期(YYYY-MM-DD格式,当type为custom时必填) + in: query + name: start_date + type: string + - description: 结束日期(YYYY-MM-DD格式,当type为custom时必填) + in: query + name: end_date + type: string + produces: + - application/json + responses: + "200": + description: 授权趋势数据 + schema: + allOf: + - $ref: '#/definitions/models.APIResponse' + - properties: + data: + $ref: '#/definitions/models.DashboardAuthorizationTrendResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/models.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 服务器内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取授权趋势数据 + tags: + - 仪表盘 + /api/v1/dashboard/recent-authorizations: + get: + consumes: + - application/json + description: 获取最近创建的授权码列表,支持按客户ID和状态筛选 + parameters: + - description: 返回数量限制(默认20,最大100) + in: query + name: limit + type: integer + - description: 客户ID筛选 + in: query + name: customer_id + type: string + - description: 状态筛选 + enum: + - normal + - locked + - expired + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: 最近授权列表 + schema: + allOf: + - $ref: '#/definitions/models.APIResponse' + - properties: + data: + $ref: '#/definitions/models.DashboardRecentAuthorizationsResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/models.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 服务器内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取最近授权列表 + tags: + - 仪表盘 /api/v1/heartbeat: post: consumes: diff --git a/backend/internal/api/handlers/dashboard_handler.go b/backend/internal/api/handlers/dashboard_handler.go new file mode 100644 index 0000000..88ada6a --- /dev/null +++ b/backend/internal/api/handlers/dashboard_handler.go @@ -0,0 +1,158 @@ +package handlers + +import ( + "errors" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "license-manager/internal/api/middleware" + "license-manager/internal/models" + "license-manager/internal/service" + "license-manager/pkg/i18n" +) + +type DashboardHandler struct { + dashboardService service.DashboardService +} + +// NewDashboardHandler 创建仪表盘处理器 +func NewDashboardHandler(dashboardService service.DashboardService) *DashboardHandler { + return &DashboardHandler{ + dashboardService: dashboardService, + } +} + +// GetAuthorizationTrend 获取授权趋势数据 +// @Summary 获取授权趋势数据 +// @Description 根据时间类型(本周/本月/自定义)获取授权趋势统计数据 +// @Tags 仪表盘 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param type query string true "时间类型" Enums(week,month,custom) +// @Param start_date query string false "开始日期(YYYY-MM-DD格式,当type为custom时必填)" +// @Param end_date query string false "结束日期(YYYY-MM-DD格式,当type为custom时必填)" +// @Success 200 {object} models.APIResponse{data=models.DashboardAuthorizationTrendResponse} "授权趋势数据" +// @Failure 400 {object} models.ErrorResponse "请求参数错误" +// @Failure 401 {object} models.ErrorResponse "未认证" +// @Failure 403 {object} models.ErrorResponse "权限不足" +// @Failure 500 {object} models.ErrorResponse "服务器内部错误" +// @Router /api/v1/dashboard/authorization-trend [get] +func (h *DashboardHandler) GetAuthorizationTrend(c *gin.Context) { + // 绑定查询参数 + var req models.DashboardAuthorizationTrendRequest + 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) + + // 调用服务层获取数据 + response, err := h.dashboardService.GetAuthorizationTrend(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: response, + }) +} + +// GetRecentAuthorizations 获取最近授权列表 +// @Summary 获取最近授权列表 +// @Description 获取最近创建的授权码列表,支持按客户ID和状态筛选 +// @Tags 仪表盘 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param limit query int false "返回数量限制(默认20,最大100)" +// @Param customer_id query string false "客户ID筛选" +// @Param status query string false "状态筛选" Enums(normal,locked,expired) +// @Success 200 {object} models.APIResponse{data=models.DashboardRecentAuthorizationsResponse} "最近授权列表" +// @Failure 400 {object} models.ErrorResponse "请求参数错误" +// @Failure 401 {object} models.ErrorResponse "未认证" +// @Failure 403 {object} models.ErrorResponse "权限不足" +// @Failure 500 {object} models.ErrorResponse "服务器内部错误" +// @Router /api/v1/dashboard/recent-authorizations [get] +func (h *DashboardHandler) GetRecentAuthorizations(c *gin.Context) { + // 绑定查询参数 + var req models.DashboardRecentAuthorizationsRequest + 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) + + // 调用服务层获取数据 + response, err := h.dashboardService.GetRecentAuthorizations(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: response, + }) +} \ No newline at end of file diff --git a/backend/internal/api/routes/router.go b/backend/internal/api/routes/router.go index b580657..273a95c 100644 --- a/backend/internal/api/routes/router.go +++ b/backend/internal/api/routes/router.go @@ -40,6 +40,7 @@ func SetupRouter() *gin.Engine { userRepo := repository.NewUserRepository(db) authCodeRepo := repository.NewAuthorizationCodeRepository(db) licenseRepo := repository.NewLicenseRepository(db) + dashboardRepo := repository.NewDashboardRepository(db) // 获取logger实例 log := logger.GetLogger() @@ -51,6 +52,7 @@ func SetupRouter() *gin.Engine { enumService := service.NewEnumService() authCodeService := service.NewAuthorizationCodeService(authCodeRepo) licenseService := service.NewLicenseService(licenseRepo, db, log) + dashboardService := service.NewDashboardService(dashboardRepo) // 初始化处理器层 authHandler := handlers.NewAuthHandler(authService) @@ -59,6 +61,7 @@ func SetupRouter() *gin.Engine { enumHandler := handlers.NewEnumHandler(enumService) authCodeHandler := handlers.NewAuthorizationCodeHandler(authCodeService) licenseHandler := handlers.NewLicenseHandler(licenseService) + dashboardHandler := handlers.NewDashboardHandler(dashboardService) // 健康检测接口(无需认证) router.GET("/health", systemHandler.HealthCheck) @@ -117,6 +120,10 @@ func SetupRouter() *gin.Engine { // 统计分析 auth.GET("/v1/stats/overview", licenseHandler.GetStatsOverview) + + // 仪表盘接口 + auth.GET("/v1/dashboard/authorization-trend", dashboardHandler.GetAuthorizationTrend) + auth.GET("/v1/dashboard/recent-authorizations", dashboardHandler.GetRecentAuthorizations) } // 管理员接口 diff --git a/backend/internal/models/dashboard.go b/backend/internal/models/dashboard.go new file mode 100644 index 0000000..60a5ca4 --- /dev/null +++ b/backend/internal/models/dashboard.go @@ -0,0 +1,72 @@ +package models + +import "time" + +// DashboardAuthorizationTrendRequest 授权趋势查询请求 +type DashboardAuthorizationTrendRequest struct { + Type string `form:"type" binding:"required,oneof=week month custom" json:"type"` // 时间类型: week/month/custom + StartDate string `form:"start_date" json:"start_date,omitempty"` // 开始日期 (YYYY-MM-DD格式,当type为custom时必填) + EndDate string `form:"end_date" json:"end_date,omitempty"` // 结束日期 (YYYY-MM-DD格式,当type为custom时必填) + Timezone string `form:"timezone" json:"timezone,omitempty"` // 时区 (如: Asia/Shanghai, UTC等,默认使用服务器本地时区) +} + +// DashboardAuthorizationTrendResponse 授权趋势响应 +type DashboardAuthorizationTrendResponse struct { + Period TrendPeriod `json:"period"` // 时间段信息 + TrendData []TrendData `json:"trend_data"` // 趋势数据 + Summary TrendSummary `json:"summary"` // 汇总信息 +} + +// TrendPeriod 时间段信息 +type TrendPeriod struct { + Type string `json:"type"` // 时间类型 + StartDate string `json:"start_date"` // 开始日期 + EndDate string `json:"end_date"` // 结束日期 + DescriptionDisplay string `json:"description_display"` // 多语言描述 +} + +// TrendData 单日趋势数据 +type TrendData struct { + Date string `json:"date"` // 日期 (YYYY-MM-DD) + TotalAuthorizations int64 `json:"total_authorizations"` // 当日授权总数 + NewAuthorizations int64 `json:"new_authorizations"` // 当日新增授权数 + ExpiredAuthorizations int64 `json:"expired_authorizations"` // 当日过期授权数 +} + +// TrendSummary 趋势汇总信息 +type TrendSummary struct { + TotalCount int64 `json:"total_count"` // 期间总授权数 + NewCount int64 `json:"new_count"` // 期间新增授权数 + ExpiredCount int64 `json:"expired_count"` // 期间过期授权数 + GrowthRate float64 `json:"growth_rate"` // 增长率(百分比) +} + +// DashboardRecentAuthorizationsRequest 最近授权列表查询请求 +type DashboardRecentAuthorizationsRequest struct { + Limit int `form:"limit" json:"limit,omitempty"` // 返回数量限制(默认20,最大100) + CustomerID string `form:"customer_id" json:"customer_id,omitempty"` // 客户ID筛选(可选) + Status string `form:"status" json:"status,omitempty"` // 状态筛选 (normal/locked/expired) 可选 +} + +// DashboardRecentAuthorizationsResponse 最近授权列表响应 +type DashboardRecentAuthorizationsResponse struct { + List []RecentAuthorization `json:"list"` // 授权列表 + Total int64 `json:"total"` // 符合条件的授权总数量 +} + +// RecentAuthorization 最近授权信息 +type RecentAuthorization struct { + ID string `json:"id"` // 授权码ID + Code string `json:"code"` // 授权码 + CustomerID string `json:"customer_id"` // 客户ID + CustomerName string `json:"customer_name"` // 客户名称 + Description string `json:"description"` // 授权描述 + Status string `json:"status"` // 状态 (normal/locked/expired) + StatusDisplay string `json:"status_display"` // 状态显示文本(多语言) + StartDate time.Time `json:"start_date"` // 生效时间 + EndDate time.Time `json:"end_date"` // 到期时间 + MaxActivations int `json:"max_activations"` // 最大激活数量 + CurrentActivations int `json:"current_activations"` // 当前激活数量 + CreatedAt time.Time `json:"created_at"` // 创建时间 + UpdatedAt time.Time `json:"updated_at"` // 更新时间 +} \ No newline at end of file diff --git a/backend/internal/repository/dashboard_repository.go b/backend/internal/repository/dashboard_repository.go new file mode 100644 index 0000000..3815658 --- /dev/null +++ b/backend/internal/repository/dashboard_repository.go @@ -0,0 +1,206 @@ +package repository + +import ( + "context" + "time" + + "license-manager/internal/models" + pkgcontext "license-manager/pkg/context" + "license-manager/pkg/i18n" + + "gorm.io/gorm" +) + +type dashboardRepository struct { + db *gorm.DB +} + +// NewDashboardRepository 创建仪表盘数据访问层实例 +func NewDashboardRepository(db *gorm.DB) DashboardRepository { + return &dashboardRepository{ + db: db, + } +} + +// GetAuthorizationTrendData 获取授权趋势数据 +func (r *dashboardRepository) GetAuthorizationTrendData(ctx context.Context, startDate, endDate time.Time) ([]models.TrendData, error) { + lang := pkgcontext.GetLanguageFromContext(ctx) + var trendData []models.TrendData + + // 生成日期范围 + dates := make([]time.Time, 0) + for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) { + dates = append(dates, d) + } + + // 查询每日数据 + for _, date := range dates { + dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) + dayEnd := dayStart.Add(24*time.Hour - time.Nanosecond) + + // 统计当日的授权数据 + var totalCount, newCount, expiredCount int64 + + // 当日总授权数(截至当日24:00的累计有效授权数) + err := r.db.WithContext(ctx).Model(&models.AuthorizationCode{}). + Where("created_at <= ?", dayEnd). + Count(&totalCount).Error + if err != nil { + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + // 当日新增授权数 + err = r.db.WithContext(ctx).Model(&models.AuthorizationCode{}). + Where("created_at >= ? AND created_at <= ?", dayStart, dayEnd). + Count(&newCount).Error + if err != nil { + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + // 当日过期授权数 + err = r.db.WithContext(ctx).Model(&models.AuthorizationCode{}). + Where("end_date >= ? AND end_date < ?", dayStart, dayEnd). + Count(&expiredCount).Error + if err != nil { + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + trendData = append(trendData, models.TrendData{ + Date: date.Format("2006-01-02"), + TotalAuthorizations: totalCount, + NewAuthorizations: newCount, + ExpiredAuthorizations: expiredCount, + }) + } + + return trendData, nil +} + +// GetRecentAuthorizations 获取最近授权列表 +func (r *dashboardRepository) GetRecentAuthorizations(ctx context.Context, req *models.DashboardRecentAuthorizationsRequest) (*models.DashboardRecentAuthorizationsResponse, error) { + lang := pkgcontext.GetLanguageFromContext(ctx) + + // 设置默认值 + limit := req.Limit + if limit <= 0 { + limit = 20 + } + if limit > 100 { + limit = 100 + } + + // 构建查询 + query := r.db.WithContext(ctx).Table("authorization_codes ac"). + Select(`ac.id, ac.code, ac.customer_id, c.customer_name as customer_name, + ac.description, ac.start_date, ac.end_date, ac.max_activations, + COALESCE(l.active_count, 0) as current_activations, + ac.created_at, ac.updated_at, ac.is_locked`). + Joins("LEFT JOIN customers c ON ac.customer_id = c.id"). + Joins(`LEFT JOIN ( + SELECT authorization_code_id, COUNT(*) as active_count + FROM licenses + WHERE status = 'active' AND deleted_at IS NULL + GROUP BY authorization_code_id + ) l ON ac.id = l.authorization_code_id`). + Where("1=1"). + Order("ac.created_at DESC") + + // 添加筛选条件 + if req.CustomerID != "" { + query = query.Where("ac.customer_id = ?", req.CustomerID) + } + + // 状态筛选(虚字段) + now := time.Now() + switch req.Status { + case "normal": + query = query.Where("ac.is_locked = ? AND ac.end_date > ?", false, now) + case "locked": + query = query.Where("ac.is_locked = ?", true) + case "expired": + query = query.Where("ac.end_date <= ?", now) + } + + // 查询总数 - 使用简化查询避免JOIN复杂性 + var total int64 + countQuery := r.db.WithContext(ctx).Model(&models.AuthorizationCode{}).Where("1=1") + + // 添加相同的筛选条件 + if req.CustomerID != "" { + countQuery = countQuery.Where("customer_id = ?", req.CustomerID) + } + + // 状态筛选 + switch req.Status { + case "normal": + countQuery = countQuery.Where("is_locked = ? AND end_date > ?", false, now) + case "locked": + countQuery = countQuery.Where("is_locked = ?", true) + case "expired": + countQuery = countQuery.Where("end_date <= ?", now) + } + + err := countQuery.Count(&total).Error + if err != nil { + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + // 查询列表数据 + type queryResult struct { + ID string `gorm:"column:id"` + Code string `gorm:"column:code"` + CustomerID string `gorm:"column:customer_id"` + CustomerName string `gorm:"column:customer_name"` + Description string `gorm:"column:description"` + StartDate time.Time `gorm:"column:start_date"` + EndDate time.Time `gorm:"column:end_date"` + MaxActivations int `gorm:"column:max_activations"` + CurrentActivations int `gorm:"column:current_activations"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + IsLocked bool `gorm:"column:is_locked"` + } + + var results []queryResult + err = query.Limit(limit).Find(&results).Error + if err != nil { + return nil, i18n.NewI18nError("900004", lang, err.Error()) + } + + // 转换为响应格式 + authorizations := make([]models.RecentAuthorization, 0, len(results)) + for _, result := range results { + // 计算状态 + status := "normal" + statusDisplay := i18n.GetEnumMessage("authorization_code_status", "normal", lang) + + if result.IsLocked { + status = "locked" + statusDisplay = i18n.GetEnumMessage("authorization_code_status", "locked", lang) + } else if result.EndDate.Before(now) { + status = "expired" + statusDisplay = i18n.GetEnumMessage("authorization_code_status", "expired", lang) + } + + authorizations = append(authorizations, models.RecentAuthorization{ + ID: result.ID, + Code: result.Code, + CustomerID: result.CustomerID, + CustomerName: result.CustomerName, + Description: result.Description, + Status: status, + StatusDisplay: statusDisplay, + StartDate: result.StartDate, + EndDate: result.EndDate, + MaxActivations: result.MaxActivations, + CurrentActivations: result.CurrentActivations, + CreatedAt: result.CreatedAt, + UpdatedAt: result.UpdatedAt, + }) + } + + return &models.DashboardRecentAuthorizationsResponse{ + List: authorizations, + Total: total, + }, nil +} diff --git a/backend/internal/repository/interfaces.go b/backend/internal/repository/interfaces.go index 3c5f69b..8fbcab1 100644 --- a/backend/internal/repository/interfaces.go +++ b/backend/internal/repository/interfaces.go @@ -2,6 +2,7 @@ package repository import ( "context" + "time" "license-manager/internal/models" ) @@ -99,4 +100,13 @@ type LicenseRepository interface { // GetActiveLicenseCount 获取指定授权码的激活许可证数量 GetActiveLicenseCount(ctx context.Context, authCodeID string) (int64, error) +} + +// DashboardRepository 仪表盘数据访问接口 +type DashboardRepository interface { + // GetAuthorizationTrendData 获取授权趋势数据 + GetAuthorizationTrendData(ctx context.Context, startDate, endDate time.Time) ([]models.TrendData, error) + + // GetRecentAuthorizations 获取最近授权列表 + GetRecentAuthorizations(ctx context.Context, req *models.DashboardRecentAuthorizationsRequest) (*models.DashboardRecentAuthorizationsResponse, error) } \ No newline at end of file diff --git a/backend/internal/service/dashboard_service.go b/backend/internal/service/dashboard_service.go new file mode 100644 index 0000000..43fd38d --- /dev/null +++ b/backend/internal/service/dashboard_service.go @@ -0,0 +1,174 @@ +package service + +import ( + "context" + "fmt" + "time" + + "license-manager/internal/models" + "license-manager/internal/repository" + pkgcontext "license-manager/pkg/context" + "license-manager/pkg/i18n" +) + +type dashboardService struct { + dashboardRepo repository.DashboardRepository +} + +// NewDashboardService 创建仪表盘服务实例 +func NewDashboardService(dashboardRepo repository.DashboardRepository) DashboardService { + return &dashboardService{ + dashboardRepo: dashboardRepo, + } +} + +// GetAuthorizationTrend 获取授权趋势数据 +func (s *dashboardService) GetAuthorizationTrend(ctx context.Context, req *models.DashboardAuthorizationTrendRequest) (*models.DashboardAuthorizationTrendResponse, error) { + lang := pkgcontext.GetLanguageFromContext(ctx) + + // 业务逻辑:参数验证 + if req == nil { + return nil, i18n.NewI18nError("900001", lang) + } + + var startDate, endDate time.Time + var descriptionDisplay string + + // 获取时区,默认使用本地时区 + loc := time.Local + if req.Timezone != "" { + var err error + loc, err = time.LoadLocation(req.Timezone) + if err != nil { + // 时区解析失败,使用本地时区 + loc = time.Local + } + } + + now := time.Now().In(loc) + + // 根据类型计算时间范围 + switch req.Type { + case "week": + // 本周:从周一到周日 + weekday := int(now.Weekday()) + if weekday == 0 { // Sunday + weekday = 7 + } + startDate = now.AddDate(0, 0, -(weekday-1)) + startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, loc) + endDate = startDate.AddDate(0, 0, 6) + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, loc) + descriptionDisplay = i18n.GetEnumMessage("dashboard_period", "week", lang) + + case "month": + // 本月:从1号到月末 + startDate = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc) + endDate = startDate.AddDate(0, 1, -1) + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, loc) + descriptionDisplay = i18n.GetEnumMessage("dashboard_period", "month", lang) + + case "custom": + // 自定义时间范围 + if req.StartDate == "" || req.EndDate == "" { + return nil, i18n.NewI18nError("400002", lang) // 开始日期和结束日期都是必填的 + } + + var err error + startDate, err = time.ParseInLocation("2006-01-02", req.StartDate, loc) + if err != nil { + return nil, i18n.NewI18nError("400002", lang) // 开始日期格式错误 + } + + endDate, err = time.ParseInLocation("2006-01-02", req.EndDate, loc) + if err != nil { + return nil, i18n.NewI18nError("400003", lang) // 结束日期格式错误 + } + + // 验证时间范围 + if startDate.After(endDate) { + return nil, i18n.NewI18nError("400005", lang) // 开始日期不能晚于结束日期 + } + + // 验证时间跨度不超过一年 + if endDate.Sub(startDate) > 365*24*time.Hour { + return nil, i18n.NewI18nError("400004", lang) // 时间范围超过限制 + } + + // 设置为全天 + startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, startDate.Location()) + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, endDate.Location()) + descriptionDisplay = fmt.Sprintf("%s - %s", req.StartDate, req.EndDate) + + default: + return nil, i18n.NewI18nError("400001", lang) // 时间类型参数错误 + } + + // 获取趋势数据 + trendData, err := s.dashboardRepo.GetAuthorizationTrendData(ctx, startDate, endDate) + if err != nil { + return nil, err + } + + // 计算汇总信息 + var totalCount, newCount, expiredCount int64 + for _, data := range trendData { + newCount += data.NewAuthorizations + expiredCount += data.ExpiredAuthorizations + } + + // 获取期间结束时的总授权数 + if len(trendData) > 0 { + totalCount = trendData[len(trendData)-1].TotalAuthorizations + } + + // 计算增长率(本期新增相对于期初的百分比) + var growthRate float64 + if totalCount > 0 && newCount > 0 { + periodStartCount := totalCount - newCount + if periodStartCount > 0 { + growthRate = float64(newCount) / float64(periodStartCount) * 100 + } else { + growthRate = 100.0 // 如果期初为0,则增长率为100% + } + } + + return &models.DashboardAuthorizationTrendResponse{ + Period: models.TrendPeriod{ + Type: req.Type, + StartDate: startDate.Format("2006-01-02"), + EndDate: endDate.Format("2006-01-02"), + DescriptionDisplay: descriptionDisplay, + }, + TrendData: trendData, + Summary: models.TrendSummary{ + TotalCount: totalCount, + NewCount: newCount, + ExpiredCount: expiredCount, + GrowthRate: growthRate, + }, + }, nil +} + +// GetRecentAuthorizations 获取最近授权列表 +func (s *dashboardService) GetRecentAuthorizations(ctx context.Context, req *models.DashboardRecentAuthorizationsRequest) (*models.DashboardRecentAuthorizationsResponse, error) { + lang := pkgcontext.GetLanguageFromContext(ctx) + + // 业务逻辑:参数验证 + if req == nil { + return nil, i18n.NewI18nError("900001", lang) + } + + // 验证limit参数 + if req.Limit < 0 || req.Limit > 100 { + return nil, i18n.NewI18nError("400006", lang) // 数量限制参数错误 + } + + // 验证status参数 + if req.Status != "" && req.Status != "normal" && req.Status != "locked" && req.Status != "expired" { + return nil, i18n.NewI18nError("400001", lang) // 状态参数错误 + } + + // 调用repository获取数据 + return s.dashboardRepo.GetRecentAuthorizations(ctx, req) +} \ No newline at end of file diff --git a/backend/internal/service/interfaces.go b/backend/internal/service/interfaces.go index fbe084c..5d029c3 100644 --- a/backend/internal/service/interfaces.go +++ b/backend/internal/service/interfaces.go @@ -60,3 +60,12 @@ type LicenseService interface { // 统计接口 GetStatsOverview(ctx context.Context) (*models.StatsOverviewResponse, error) } + +// DashboardService 仪表盘服务接口 +type DashboardService interface { + // 获取授权趋势数据 + GetAuthorizationTrend(ctx context.Context, req *models.DashboardAuthorizationTrendRequest) (*models.DashboardAuthorizationTrendResponse, error) + + // 获取最近授权列表 + GetRecentAuthorizations(ctx context.Context, req *models.DashboardRecentAuthorizationsRequest) (*models.DashboardRecentAuthorizationsResponse, error) +}