feat: 添加客户详情接口的授权统计信息,更新相关文档和数据模型以支持新统计字段

This commit is contained in:
wood911010 committed 2025-11-01 21:46:38 +08:00
1 parent 10000681cf
commit a7b28022c8
10 files changed
+459 -109

No files matched your search

@@ -0,0 +1,177 @@
# 客户详情接口改造需求
## 一、接口信息
**接口路径**: `GET /api/customers/{id}`
**接口说明**: 获取客户详细信息,在原有客户基本信息基础上,新增客户授权统计信息
---
## 二、需求概述
在客户详情接口的响应数据中,新增一个 `authorization_stats` 对象,包含该客户的授权码和许可证相关统计数据,用于在客户详情页面展示客户的授权使用情况。
---
## 三、数据模型设计
### 3.1 响应结构
```json
{
"code": "000000",
"message": "success",
"data": {
// ... 原有客户基本信息字段 ...
// 新增:授权统计信息
"authorization_stats": {
"total_auth_codes": 15, // 总授权码数量
"expired_auth_codes": 3, // 已过期授权码数量
"expiring_soon_auth_codes": 2, // 30日内即将到期授权码数量
"total_licenses": 42, // 总许可证数量
"active_licenses": 28, // 已激活许可证数量
"inactive_licenses": 14, // 未激活许可证数量
"expired_licenses": 8 // 已过期许可证数量
}
}
}
```
### 3.2 统计字段详细说明
#### 授权码统计(基于 `authorization_codes` 表)
| 字段名 | 类型 | 说明 | 计算规则 |
|--------|------|------|----------|
| `total_auth_codes` | int64 | 总授权码数量 | 统计该客户(`customer_id`)的所有授权码总数 |
| `expired_auth_codes` | int64 | 已过期授权码数量 | `end_date < NOW()` 的授权码数量(不考虑锁定状态) |
| `expiring_soon_auth_codes` | int64 | 30日内即将到期授权码数量 | `end_date >= NOW()` 且 `end_date <= DATE_ADD(NOW(), INTERVAL 30 DAY)` 的授权码数量(不考虑锁定状态) |
#### 许可证统计(基于 `licenses` 表)
| 字段名 | 类型 | 说明 | 计算规则 |
|--------|------|------|----------|
| `total_licenses` | int64 | 总许可证数量 | 统计该客户(`customer_id`)的所有许可证总数 |
| `active_licenses` | int64 | 已激活许可证数量 | `status = 'active'` 的许可证数量 |
| `inactive_licenses` | int64 | 未激活许可证数量 | `total_licenses - active_licenses` |
| `expired_licenses` | int64 | 已过期许可证数量 | 关联的授权码已过期(`authorization_code.end_date < NOW()`)的许可证总数 |
---
## 四、计算逻辑说明
### 4.1 授权码状态判断
- **正常(normal)**: `NOW() >= start_date AND NOW() <= end_date AND is_locked = false`
- **已过期(expired)**: `NOW() > end_date`(无论是否锁定)
- **已锁定(locked)**: `is_locked = true`(锁定状态不影响有效期统计)
**注意**:
- **过期和即将到期统计不考虑锁定状态**:只要授权码的 `end_date` 满足条件,无论是否锁定都应计入统计
- 例如:即使授权码已锁定,只要 `end_date < NOW()`,仍应计入"已过期授权码数量"
### 4.2 许可证过期判断
许可证的过期状态**依赖于关联的授权码**:
- 许可证本身没有 `end_date` 字段
- 如果许可证关联的授权码已过期(`authorization_codes.end_date < NOW()`),则视为该许可证已过期
### 4.3 边界条件
1. **空数据情况**: 如果客户没有任何授权码或许可证,所有统计值应返回 `0`
2. **软删除**: 只统计未删除的记录(`deleted_at IS NULL`)
3. **时间计算**: 使用服务器当前时间(`NOW()`)作为基准进行判断
---
## 五、相关数据库表
### 5.1 涉及表结构
- **`customers`**: 客户表(主表)
- **`authorization_codes`**: 授权码表
- `customer_id`: 关联客户ID
- `start_date`: 授权开始时间
- `end_date`: 授权结束时间
- `is_locked`: 是否锁定
- **`licenses`**: 许可证表
- `customer_id`: 关联客户ID(冗余字段)
- `authorization_code_id`: 关联授权码ID(外键)
- `status`: 许可证状态(`active`/`inactive`/`revoked`)
### 5.2 关联关系
```
customers (1) ──< (N) authorization_codes
│
│ (1)
│
└──< (N) licenses
```
---
## 六、实现建议
### 6.1 查询优化
建议使用单次 SQL 查询或有限次查询完成统计,避免 N+1 查询问题:
1. **授权码统计**: 使用 `COUNT` + `CASE WHEN` 在一条 SQL 中完成
2. **许可证统计**:
- `total_licenses` 和 `active_licenses`: 单次查询统计
- `expired_licenses`: 需要关联 `authorization_codes` 表判断过期状态
### 6.2 性能考虑
- 确保相关字段已建立索引(如 `customer_id`, `end_date`, `status`, `is_locked`)
- 对于高频访问的接口,可考虑缓存统计结果(需注意数据一致性)
---
## 七、示例数据
假设某客户(ID: `customer-001`)有以下数据:
**授权码数据**:
- 授权码A: `end_date = '2025-12-31'`, `is_locked = false` → 正常
- 授权码B: `end_date = '2025-01-25'`, `is_locked = false` → 30日内即将到期
- 授权码C: `end_date = '2024-12-31'`, `is_locked = false` → 已过期
- 授权码D: `end_date = '2025-12-31'`, `is_locked = true` → 已锁定(正常范围内)
- 授权码E: `end_date = '2024-11-30'`, `is_locked = true` → 已锁定且已过期(仍计入过期统计)
- 授权码F: `end_date = '2025-01-20'`, `is_locked = true` → 已锁定且即将到期(仍计入即将到期统计)
**许可证数据**:
- 授权码A关联: 10个许可证(8个active, 2个inactive)
- 授权码B关联: 5个许可证(4个active, 1个inactive)
- 授权码C关联: 3个许可证(已过期授权码下的所有许可证,均为active)
- 授权码D关联: 2个许可证(1个active, 1个inactive)
- 授权码E关联: 2个许可证(1个active, 1个inactive,因授权码已过期,这2个许可证计入过期许可证)
- 授权码F关联: 1个许可证(1个active)
**统计结果**:
```json
{
"authorization_stats": {
"total_auth_codes": 6,
"expired_auth_codes": 2, // 授权码C(已过期)+ 授权码E(已锁定但已过期)
"expiring_soon_auth_codes": 2, // 授权码B(即将到期)+ 授权码F(已锁定但即将到期)
"total_licenses": 23, // 10+5+3+2+2+1 = 23
"active_licenses": 18, // 8+4+3+1+1+1 = 18
"inactive_licenses": 5, // 23-18 = 5
"expired_licenses": 5 // 授权码C的3个 + 授权码E的2个 = 5
}
}
```
---
## 八、验收标准
1. ✅ 接口响应包含 `authorization_stats` 对象
2. ✅ 所有统计字段类型为 `int64`,空数据返回 `0`
3. ✅ 已过期和即将到期授权码统计不考虑锁定状态(仅基于 `end_date` 判断)
4. ✅ 许可证过期判断基于关联授权码的有效期
5. ✅ 计算逻辑准确,边界情况处理正确
6. ✅ 查询性能良好,无明显的性能问题
+63 -40
View File
@@ -392,7 +392,7 @@ const docTemplate = `{
"BearerAuth": []
}
],
"description": "软删除客户记录",
"description": "直接删除客户记录,删除前会检查是否有关联的授权码或许可证,如有则提示先删除授权",
"consumes": [
"application/json"
],
@@ -437,6 +437,12 @@ const docTemplate = `{
"$ref": "#/definitions/models.ErrorResponse"
}
},
"409": {
"description": "客户仍有授权,无法删除",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
@@ -2590,9 +2596,9 @@ const docTemplate = `{
"description": "使用限制(JSON对象)"
},
"validity_days": {
"description": "有效天数(1-36500天)",
"description": "有效天数(1-365000天,365000代表永久有效)",
"type": "integer",
"maximum": 36500,
"maximum": 365000,
"minimum": 1
}
}
@@ -2795,19 +2801,60 @@ const docTemplate = `{
"description": "使用限制"
},
"validity_days": {
"description": "有效天数",
"description": "有效天数(1-365000天,365000代表永久有效)",
"type": "integer",
"maximum": 36500,
"maximum": 365000,
"minimum": 1
}
}
},
"models.AuthorizationStats": {
"type": "object",
"properties": {
"active_licenses": {
"description": "已激活许可证数量",
"type": "integer"
},
"expired_auth_codes": {
"description": "已过期授权码数量",
"type": "integer"
},
"expired_licenses": {
"description": "已过期许可证数量",
"type": "integer"
},
"expiring_soon_auth_codes": {
"description": "30日内即将到期授权码数量",
"type": "integer"
},
"inactive_licenses": {
"description": "未激活许可证数量",
"type": "integer"
},
"total_auth_codes": {
"description": "总授权码数量",
"type": "integer"
},
"total_licenses": {
"description": "总许可证数量",
"type": "integer"
}
}
},
"models.Customer": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"authorization_stats": {
"description": "授权统计信息(仅在详情接口返回)",
"allOf": [
{
"$ref": "#/definitions/models.AuthorizationStats"
}
]
},
"company_size": {
"type": "string"
},
@@ -2887,13 +2934,7 @@ const docTemplate = `{
},
"company_size": {
"description": "企业规模,可选",
"type": "string",
"enum": [
"small",
"medium",
"large",
"enterprise"
]
"type": "string"
},
"contact_person": {
"description": "联系人姓名,必填",
@@ -2907,13 +2948,7 @@ const docTemplate = `{
},
"customer_level": {
"description": "客户等级,必填",
"type": "string",
"enum": [
"normal",
"vip",
"enterprise",
"strategic"
]
"type": "string"
},
"customer_name": {
"description": "客户名称,必填",
@@ -3082,13 +3117,7 @@ const docTemplate = `{
},
"company_size": {
"description": "企业规模,可选",
"type": "string",
"enum": [
"small",
"medium",
"large",
"enterprise"
]
"type": "string"
},
"contact_person": {
"description": "联系人姓名,可选",
@@ -3102,13 +3131,7 @@ const docTemplate = `{
},
"customer_level": {
"description": "客户等级,可选",
"type": "string",
"enum": [
"normal",
"vip",
"enterprise",
"strategic"
]
"type": "string"
},
"customer_name": {
"description": "客户名称,可选",
@@ -3256,11 +3279,11 @@ const docTemplate = `{
"type": "object",
"properties": {
"auth_codes": {
"description": "授权码增长率(%)",
"description": "授权码增长率(%)(当前总授权码数相比一个月前总授权码数的增长率)",
"type": "number"
},
"licenses": {
"description": "许可证增长率(%)",
"description": "许可证增长率(%)(当前活跃许可证数相比一个月前活跃许可证数的增长率)",
"type": "number"
}
}
@@ -3713,19 +3736,19 @@ const docTemplate = `{
"type": "object",
"properties": {
"abnormal_alerts": {
"description": "异常告警数量",
"description": "异常告警数量(活跃许可证中心跳超时的数量,超过配置的心跳超时时间未收到心跳)",
"type": "integer"
},
"active_licenses": {
"description": "活跃许可证数量",
"description": "活跃许可证数量(状态为active的许可证总数)",
"type": "integer"
},
"expiring_soon": {
"description": "即将过期数量",
"description": "即将过期数量(30天内即将过期的授权码,且未被锁定)",
"type": "integer"
},
"growth_rate": {
"description": "增长率",
"description": "增长率(同比上月)",
"allOf": [
{
"$ref": "#/definitions/models.GrowthRate"
@@ -3733,7 +3756,7 @@ const docTemplate = `{
]
},
"total_auth_codes": {
"description": "总授权码数量",
"description": "总授权码数量(所有授权码的累计总数)",
"type": "integer"
}
}
+63 -40
View File
@@ -389,7 +389,7 @@
"BearerAuth": []
}
],
"description": "软删除客户记录",
"description": "直接删除客户记录,删除前会检查是否有关联的授权码或许可证,如有则提示先删除授权",
"consumes": [
"application/json"
],
@@ -434,6 +434,12 @@
"$ref": "#/definitions/models.ErrorResponse"
}
},
"409": {
"description": "客户仍有授权,无法删除",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
@@ -2587,9 +2593,9 @@
"description": "使用限制(JSON对象)"
},
"validity_days": {
"description": "有效天数(1-36500天)",
"description": "有效天数(1-365000天,365000代表永久有效)",
"type": "integer",
"maximum": 36500,
"maximum": 365000,
"minimum": 1
}
}
@@ -2792,19 +2798,60 @@
"description": "使用限制"
},
"validity_days": {
"description": "有效天数",
"description": "有效天数(1-365000天,365000代表永久有效)",
"type": "integer",
"maximum": 36500,
"maximum": 365000,
"minimum": 1
}
}
},
"models.AuthorizationStats": {
"type": "object",
"properties": {
"active_licenses": {
"description": "已激活许可证数量",
"type": "integer"
},
"expired_auth_codes": {
"description": "已过期授权码数量",
"type": "integer"
},
"expired_licenses": {
"description": "已过期许可证数量",
"type": "integer"
},
"expiring_soon_auth_codes": {
"description": "30日内即将到期授权码数量",
"type": "integer"
},
"inactive_licenses": {
"description": "未激活许可证数量",
"type": "integer"
},
"total_auth_codes": {
"description": "总授权码数量",
"type": "integer"
},
"total_licenses": {
"description": "总许可证数量",
"type": "integer"
}
}
},
"models.Customer": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"authorization_stats": {
"description": "授权统计信息(仅在详情接口返回)",
"allOf": [
{
"$ref": "#/definitions/models.AuthorizationStats"
}
]
},
"company_size": {
"type": "string"
},
@@ -2884,13 +2931,7 @@
},
"company_size": {
"description": "企业规模,可选",
"type": "string",
"enum": [
"small",
"medium",
"large",
"enterprise"
]
"type": "string"
},
"contact_person": {
"description": "联系人姓名,必填",
@@ -2904,13 +2945,7 @@
},
"customer_level": {
"description": "客户等级,必填",
"type": "string",
"enum": [
"normal",
"vip",
"enterprise",
"strategic"
]
"type": "string"
},
"customer_name": {
"description": "客户名称,必填",
@@ -3079,13 +3114,7 @@
},
"company_size": {
"description": "企业规模,可选",
"type": "string",
"enum": [
"small",
"medium",
"large",
"enterprise"
]
"type": "string"
},
"contact_person": {
"description": "联系人姓名,可选",
@@ -3099,13 +3128,7 @@
},
"customer_level": {
"description": "客户等级,可选",
"type": "string",
"enum": [
"normal",
"vip",
"enterprise",
"strategic"
]
"type": "string"
},
"customer_name": {
"description": "客户名称,可选",
@@ -3253,11 +3276,11 @@
"type": "object",
"properties": {
"auth_codes": {
"description": "授权码增长率(%)",
"description": "授权码增长率(%)(当前总授权码数相比一个月前总授权码数的增长率)",
"type": "number"
},
"licenses": {
"description": "许可证增长率(%)",
"description": "许可证增长率(%)(当前活跃许可证数相比一个月前活跃许可证数的增长率)",
"type": "number"
}
}
@@ -3710,19 +3733,19 @@
"type": "object",
"properties": {
"abnormal_alerts": {
"description": "异常告警数量",
"description": "异常告警数量(活跃许可证中心跳超时的数量,超过配置的心跳超时时间未收到心跳)",
"type": "integer"
},
"active_licenses": {
"description": "活跃许可证数量",
"description": "活跃许可证数量(状态为active的许可证总数)",
"type": "integer"
},
"expiring_soon": {
"description": "即将过期数量",
"description": "即将过期数量(30天内即将过期的授权码,且未被锁定)",
"type": "integer"
},
"growth_rate": {
"description": "增长率",
"description": "增长率(同比上月)",
"allOf": [
{
"$ref": "#/definitions/models.GrowthRate"
@@ -3730,7 +3753,7 @@
]
},
"total_auth_codes": {
"description": "总授权码数量",
"description": "总授权码数量(所有授权码的累计总数)",
"type": "integer"
}
}
+44 -12
View File
@@ -218,8 +218,8 @@ definitions:
usage_limits:
description: 使用限制(JSON对象)
validity_days:
description: 有效天数(1-36500天)
maximum: 36500
description: 有效天数(1-365000天,365000代表永久有效)
maximum: 365000
minimum: 1
type: integer
required:
@@ -373,17 +373,45 @@ definitions:
usage_limits:
description: 使用限制
validity_days:
description: 有效天数
maximum: 36500
description: 有效天数(1-365000天,365000代表永久有效)
maximum: 365000
minimum: 1
type: integer
required:
- change_type
type: object
models.AuthorizationStats:
properties:
active_licenses:
description: 已激活许可证数量
type: integer
expired_auth_codes:
description: 已过期授权码数量
type: integer
expired_licenses:
description: 已过期许可证数量
type: integer
expiring_soon_auth_codes:
description: 30日内即将到期授权码数量
type: integer
inactive_licenses:
description: 未激活许可证数量
type: integer
total_auth_codes:
description: 总授权码数量
type: integer
total_licenses:
description: 总许可证数量
type: integer
type: object
models.Customer:
properties:
address:
type: string
authorization_stats:
allOf:
- $ref: '#/definitions/models.AuthorizationStats'
description: 授权统计信息(仅在详情接口返回)
company_size:
type: string
company_size_display:
@@ -683,10 +711,10 @@ definitions:
models.GrowthRate:
properties:
auth_codes:
description: 授权码增长率(%)
description: 授权码增长率(%)(当前总授权码数相比一个月前总授权码数的增长率)
type: number
licenses:
description: 许可证增长率(%)
description: 许可证增长率(%)(当前活跃许可证数相比一个月前活跃许可证数的增长率)
type: number
type: object
models.HealthResponse:
@@ -1007,20 +1035,20 @@ definitions:
models.StatsOverviewResponse:
properties:
abnormal_alerts:
description: 异常告警数量
description: 异常告警数量(活跃许可证中心跳超时的数量,超过配置的心跳超时时间未收到心跳)
type: integer
active_licenses:
description: 活跃许可证数量
description: 活跃许可证数量(状态为active的许可证总数)
type: integer
expiring_soon:
description: 即将过期数量
description: 即将过期数量(30天内即将过期的授权码,且未被锁定)
type: integer
growth_rate:
allOf:
- $ref: '#/definitions/models.GrowthRate'
description: 增长率
description: 增长率(同比上月)
total_auth_codes:
description: 总授权码数量
description: 总授权码数量(所有授权码的累计总数)
type: integer
type: object
models.SystemInfo:
@@ -1233,7 +1261,7 @@ paths:
delete:
consumes:
- application/json
description: 软删除客户记录
description: 直接删除客户记录,删除前会检查是否有关联的授权码或许可证,如有则提示先删除授权
parameters:
- description: 客户ID
in: path
@@ -1259,6 +1287,10 @@ paths:
description: 客户不存在
schema:
$ref: '#/definitions/models.ErrorResponse'
"409":
description: 客户仍有授权,无法删除
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
+14
View File
@@ -31,6 +31,9 @@ type Customer struct {
CreatedBy string `gorm:"type:varchar(36);not null;index" json:"created_by"`
UpdatedBy *string `gorm:"type:varchar(36)" json:"updated_by"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 授权统计信息(仅在详情接口返回)
AuthorizationStats *AuthorizationStats `gorm:"-" json:"authorization_stats,omitempty"`
}
// CustomerCodeSequence 客户编码序列模型
@@ -146,3 +149,14 @@ type CustomerUpdateRequest struct {
type CustomerStatusUpdateRequest struct {
Status string `json:"status" binding:"required,oneof=active disabled"` // 状态,必填
}
// AuthorizationStats 授权统计信息
type AuthorizationStats struct {
TotalAuthCodes int64 `json:"total_auth_codes"` // 总授权码数量
ExpiredAuthCodes int64 `json:"expired_auth_codes"` // 已过期授权码数量
ExpiringSoonAuthCodes int64 `json:"expiring_soon_auth_codes"` // 30日内即将到期授权码数量
TotalLicenses int64 `json:"total_licenses"` // 总许可证数量
ActiveLicenses int64 `json:"active_licenses"` // 已激活许可证数量
InactiveLicenses int64 `json:"inactive_licenses"` // 未激活许可证数量
ExpiredLicenses int64 `json:"expired_licenses"` // 已过期许可证数量
}
+8 -8
View File
@@ -171,15 +171,15 @@ type HeartbeatResponse struct {
// StatsOverviewResponse 授权概览统计响应结构
type StatsOverviewResponse struct {
TotalAuthCodes int64 `json:"total_auth_codes"` // 总授权码数量
ActiveLicenses int64 `json:"active_licenses"` // 活跃许可证数量
ExpiringSoon int64 `json:"expiring_soon"` // 即将过期数量
AbnormalAlerts int64 `json:"abnormal_alerts"` // 异常告警数量
GrowthRate GrowthRate `json:"growth_rate"` // 增长率
TotalAuthCodes int64 `json:"total_auth_codes"` // 总授权码数量(所有授权码的累计总数)
ActiveLicenses int64 `json:"active_licenses"` // 活跃许可证数量(状态为active的许可证总数)
ExpiringSoon int64 `json:"expiring_soon"` // 即将过期数量(30天内即将过期的授权码,且未被锁定)
AbnormalAlerts int64 `json:"abnormal_alerts"` // 异常告警数量(活跃许可证中心跳超时的数量,超过配置的心跳超时时间未收到心跳)
GrowthRate GrowthRate `json:"growth_rate"` // 增长率(同比上月)
}
// GrowthRate 增长率结构
// GrowthRate 增长率结构(同比上月:当前值相比一个月前同一时刻值的增长率)
type GrowthRate struct {
AuthCodes float64 `json:"auth_codes"` // 授权码增长率(%)
Licenses float64 `json:"licenses"` // 许可证增长率(%)
AuthCodes float64 `json:"auth_codes"` // 授权码增长率(%)(当前总授权码数相比一个月前总授权码数的增长率)
Licenses float64 `json:"licenses"` // 许可证增长率(%)(当前活跃许可证数相比一个月前活跃许可证数的增长率)
}
@@ -236,4 +236,72 @@ func (r *customerRepository) GetCustomerCount(ctx context.Context, filters map[s
}
return count, nil
}
// GetCustomerAuthorizationStats 获取客户授权统计信息
func (r *customerRepository) GetCustomerAuthorizationStats(ctx context.Context, customerID string) (*models.AuthorizationStats, error) {
stats := &models.AuthorizationStats{}
// 1. 授权码统计
// 总授权码数量
var totalAuthCodes int64
if err := r.db.Model(&models.AuthorizationCode{}).
Where("customer_id = ?", customerID).
Count(&totalAuthCodes).Error; err != nil {
return nil, fmt.Errorf("failed to count total auth codes: %w", err)
}
stats.TotalAuthCodes = totalAuthCodes
// 已过期授权码数量(end_date < NOW(),不考虑锁定状态)
var expiredAuthCodes int64
if err := r.db.Model(&models.AuthorizationCode{}).
Where("customer_id = ? AND end_date < NOW()", customerID).
Count(&expiredAuthCodes).Error; err != nil {
return nil, fmt.Errorf("failed to count expired auth codes: %w", err)
}
stats.ExpiredAuthCodes = expiredAuthCodes
// 30日内即将到期授权码数量(end_date >= NOW() AND end_date <= DATE_ADD(NOW(), INTERVAL 30 DAY),不考虑锁定状态)
var expiringSoonAuthCodes int64
if err := r.db.Model(&models.AuthorizationCode{}).
Where("customer_id = ? AND end_date >= NOW() AND end_date <= DATE_ADD(NOW(), INTERVAL 30 DAY)", customerID).
Count(&expiringSoonAuthCodes).Error; err != nil {
return nil, fmt.Errorf("failed to count expiring soon auth codes: %w", err)
}
stats.ExpiringSoonAuthCodes = expiringSoonAuthCodes
// 2. 许可证统计
// 总许可证数量
var totalLicenses int64
if err := r.db.Model(&models.License{}).
Where("customer_id = ? AND deleted_at IS NULL", customerID).
Count(&totalLicenses).Error; err != nil {
return nil, fmt.Errorf("failed to count total licenses: %w", err)
}
stats.TotalLicenses = totalLicenses
// 已激活许可证数量(status = 'active')
var activeLicenses int64
if err := r.db.Model(&models.License{}).
Where("customer_id = ? AND status = 'active' AND deleted_at IS NULL", customerID).
Count(&activeLicenses).Error; err != nil {
return nil, fmt.Errorf("failed to count active licenses: %w", err)
}
stats.ActiveLicenses = activeLicenses
// 未激活许可证数量(total - active)
stats.InactiveLicenses = totalLicenses - activeLicenses
// 已过期许可证数量(关联的授权码已过期)
// 需要通过JOIN authorization_codes表来判断
var expiredLicenses int64
if err := r.db.Model(&models.License{}).
Joins("INNER JOIN authorization_codes ON licenses.authorization_code_id = authorization_codes.id").
Where("licenses.customer_id = ? AND authorization_codes.end_date < NOW() AND licenses.deleted_at IS NULL", customerID).
Count(&expiredLicenses).Error; err != nil {
return nil, fmt.Errorf("failed to count expired licenses: %w", err)
}
stats.ExpiredLicenses = expiredLicenses
return stats, nil
}
@@ -31,6 +31,9 @@ type CustomerRepository interface {
// CheckCustomerHasLicenses 检查客户是否有关联的许可证
CheckCustomerHasLicenses(ctx context.Context, customerID string) (bool, error)
// GetCustomerAuthorizationStats 获取客户授权统计信息
GetCustomerAuthorizationStats(ctx context.Context, customerID string) (*models.AuthorizationStats, error)
}
// UserRepository 用户数据访问接口
@@ -72,6 +72,16 @@ func (s *customerService) GetCustomer(ctx context.Context, id string) (*models.C
return nil, i18n.NewI18nError("900004", lang, err.Error())
}
// 获取授权统计信息
stats, err := s.customerRepo.GetCustomerAuthorizationStats(ctx, id)
if err != nil {
// 统计信息获取失败不影响主流程,记录错误但继续执行
// 如果获取失败,stats 为 nil,客户详情仍然可以返回,只是没有统计信息
// 根据需求文档,空数据时应该返回0,所以这里创建一个空的统计对象
stats = &models.AuthorizationStats{}
}
customer.AuthorizationStats = stats
// 填充多语言显示字段
s.fillCustomerDisplayFields(customer, lang)
+9 -9
View File
@@ -803,32 +803,32 @@ func (s *licenseService) GetStatsOverview(ctx context.Context) (*models.StatsOve
return err
}
// 5. 计算上月的授权码和许可证数量(用于计算增长率)
var lastMonthAuthCodes, lastMonthLicenses int64
// 5. 计算同比上月的授权码和许可证数量(用于计算增长率)
var lastMonthAuthCodes, lastMonthActiveLicenses int64
// 上月授权码数量
// 上月同期授权码总数(一个月前同一时刻的累计总数)
if err := tx.Model(&models.AuthorizationCode{}).
Where("created_at <= ?", lastMonth).
Count(&lastMonthAuthCodes).Error; err != nil {
return err
}
// 上月许可证数量
// 上月同期活跃许可证总数(一个月前同一时刻的状态为active的许可证数)
if err := tx.Model(&models.License{}).
Where("created_at <= ?", lastMonth).
Count(&lastMonthLicenses).Error; err != nil {
Where("status = ? AND created_at <= ?", "active", lastMonth).
Count(&lastMonthActiveLicenses).Error; err != nil {
return err
}
// 计算增长率
// 计算增长率(同比上月:当前总数相比一个月前总数的增长率)
if lastMonthAuthCodes > 0 {
stats.GrowthRate.AuthCodes = float64(stats.TotalAuthCodes-lastMonthAuthCodes) / float64(lastMonthAuthCodes) * 100
} else {
stats.GrowthRate.AuthCodes = 0
}
if lastMonthLicenses > 0 {
stats.GrowthRate.Licenses = float64(stats.ActiveLicenses-lastMonthLicenses) / float64(lastMonthLicenses) * 100
if lastMonthActiveLicenses > 0 {
stats.GrowthRate.Licenses = float64(stats.ActiveLicenses-lastMonthActiveLicenses) / float64(lastMonthActiveLicenses) * 100
} else {
stats.GrowthRate.Licenses = 0
}