feat: 添加用户订单和设备汇总接口设计文档,支持获取当前登录用户的订单和设备统计信息

This commit is contained in:
wood911010 committed 2026-01-13 23:47:42 +08:00
1 parent 194accefd8
commit 6bfde74e21
15 files changed
+575 -3

No files matched your search

@@ -0,0 +1,179 @@
# 用户端设备汇总接口设计文档
## 需求概述
提供用户端设备汇总统计接口,返回当前登录客户的设备总数、在线设备数、离线设备数。
## 接口路径
```
GET /api/cu/devices/summary
```
## 认证要求
- 需要 Bearer Token 认证(C端用户JWT)
- 基于当前登录用户的客户ID进行统计
## 请求参数
无查询参数
## 响应数据结构
### 成功响应
```json
{
"code": "000000",
"message": "成功",
"data": {
"total_devices": 100,
"online_devices": 85,
"offline_devices": 15
}
}
```
### 响应字段说明
| 字段 | 类型 | 必填 | 描述 |
|------|------|------|------|
| total_devices | int64 | 是 | 设备总数(活跃许可证数) |
| online_devices | int64 | 是 | 在线设备数 |
| offline_devices | int64 | 是 | 离线设备数 |
### 在线状态判断逻辑
参考现有代码实现:
- 通过 `last_heartbeat` 时间戳判断设备是否在线
- 心跳超时时间从配置获取,默认5分钟(300秒)
- 如果 `last_heartbeat + heartbeat_timeout > 当前时间`,则设备在线
### 业务规则
- 统计范围:当前登录用户的客户ID下的所有活跃设备(`status = 'active'` 且 `deleted_at IS NULL`)
- 权限控制:只能查看自己客户ID下的设备数据
- 数据一致性:`total_devices = online_devices + offline_devices`
## 错误响应
| 错误码 | HTTP状态码 | 描述 |
|--------|------------|------|
| 500011 | 401 | 认证令牌无效 |
| 500012 | 401 | 认证令牌已过期 |
| 500010 | 401 | 认证令牌缺失 |
| 900004 | 500 | 服务器内部错误 |
## Swagger 文档
```go
// GetDeviceSummary 获取设备汇总统计
// @Summary 获取设备汇总统计
// @Description 获取当前登录用户的设备汇总统计信息(总数、在线数、离线数)
// @Tags 客户设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.APIResponse{data=models.DeviceSummaryResponse} "获取成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/cu/devices/summary [get]
```
## 技术实现方案
### 1. 新增响应模型(models/license.go)
```go
// DeviceSummaryResponse 设备汇总响应结构
type DeviceSummaryResponse struct {
TotalDevices int64 `json:"total_devices"` // 设备总数
OnlineDevices int64 `json:"online_devices"` // 在线设备数
OfflineDevices int64 `json:"offline_devices"` // 离线设备数
}
```
### 2. 服务层接口扩展(service/interfaces.go)
在 `CuDeviceService` 接口中添加:
```go
// 获取设备汇总统计
GetDeviceSummary(ctx context.Context, customerID string) (*models.DeviceSummaryResponse, error)
```
### 3. Repository层接口扩展(repository/interfaces.go)
在 `LicenseRepository` 接口中添加:
```go
// GetCustomerDeviceSummary 获取客户设备汇总统计
GetCustomerDeviceSummary(ctx context.Context, customerID string) (*models.DeviceSummaryResponse, error)
```
### 4. 处理器扩展(handlers/cu_device_handler.go)
添加新的处理器方法:
```go
func (h *CuDeviceHandler) GetDeviceSummary(c *gin.Context) {
// 从JWT Token中获取用户ID和客户ID
claims := c.MustGet("cu_user").(*utils.CuClaims)
// 调用服务层
result, err := h.cuDeviceService.GetDeviceSummary(c.Request.Context(), claims.CustomerID)
if err != nil {
// err已经是i18n.I18nError,直接使用
i18nErr, ok := err.(*i18n.I18nError)
if ok {
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
Code: i18nErr.Code,
Message: i18nErr.Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 如果不是i18n错误,使用通用错误
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMessage := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMessage,
Data: result,
})
}
```
### 5. 路由配置(routes/router.go)
在 cuAuth 路由组中添加:
```go
cuAuth.GET("/devices/summary", cuDeviceHandler.GetDeviceSummary)
```
## 数据库查询逻辑
基于现有 `licenses` 表结构,统计逻辑:
1. **设备总数**:查询 `customer_id = ? AND status = 'active' AND deleted_at IS NULL` 的记录数
2. **在线设备数**:在总数基础上,筛选 `last_heartbeat > (NOW() - INTERVAL heartbeat_timeout SECOND)` 的记录数
3. **离线设备数**:总数减去在线设备数
## 性能优化建议
- 建议对该接口使用缓存,避免频繁的数据库查询
- 可以使用 Redis 缓存,缓存时间建议为 1-5 分钟
- 缓存键设计:`device_summary:{customer_id}`
@@ -0,0 +1,167 @@
# 用户订单汇总接口设计文档
## 需求概述
提供用户订单汇总统计接口,返回当前登录客户的订单总数、待支付订单数、已支付订单数。
## 接口路径
```
GET /api/cu/orders/summary
```
## 认证要求
- 需要 Bearer Token 认证(C端用户JWT)
- 基于当前登录用户的客户ID进行统计
## 请求参数
无查询参数
## 响应数据结构
### 成功响应
```json
{
"code": "000000",
"message": "成功",
"data": {
"total_orders": 100,
"pending_orders": 15,
"paid_orders": 85
}
}
```
### 响应字段说明
| 字段 | 类型 | 必填 | 描述 |
|------|------|------|------|
| total_orders | int64 | 是 | 订单总数 |
| pending_orders | int64 | 是 | 待支付订单数 |
| paid_orders | int64 | 是 | 已支付订单数 |
## 业务规则
- 统计范围:当前登录用户的客户ID下的所有订单(包含已删除订单)
- 订单状态:
- `pending`: 待支付订单
- `paid`: 已支付订单
- 数据一致性:`total_orders = pending_orders + paid_orders`
## 错误响应
| 错误码 | HTTP状态码 | 描述 |
|--------|------------|------|
| 500011 | 401 | 认证令牌无效 |
| 500012 | 401 | 认证令牌已过期 |
| 500010 | 401 | 认证令牌缺失 |
| 900004 | 500 | 服务器内部错误 |
## Swagger 文档
```go
// GetOrderSummary 获取订单汇总统计
// @Summary 获取订单汇总统计
// @Description 获取当前登录用户的订单汇总统计信息(总数、待支付数、已支付数)
// @Tags 客户订单管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.APIResponse{data=models.OrderSummaryResponse} "获取成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/cu/orders/summary [get]
```
## 技术实现方案
### 1. 新增响应模型(models/cu_order.go)
```go
// OrderSummaryResponse 订单汇总响应结构
type OrderSummaryResponse struct {
TotalOrders int64 `json:"total_orders"` // 订单总数
PendingOrders int64 `json:"pending_orders"` // 待支付订单数
PaidOrders int64 `json:"paid_orders"` // 已支付订单数
}
```
### 2. 服务层接口扩展(service/interfaces.go)
在 `CuOrderService` 接口中添加:
```go
// 获取订单汇总统计
GetOrderSummary(ctx context.Context, customerID string) (*models.OrderSummaryResponse, error)
```
### 3. Repository层接口扩展(repository/interfaces.go)
在 `CuOrderRepository` 接口中添加:
```go
// GetCustomerOrderSummary 获取客户订单汇总统计
GetCustomerOrderSummary(ctx context.Context, customerID string) (*models.OrderSummaryResponse, error)
```
### 4. 处理器扩展(handlers/cu_order_handler.go)
添加新的处理器方法:
```go
func (h *CuOrderHandler) GetOrderSummary(c *gin.Context) {
// 从JWT Token中获取用户ID和客户ID
claims := c.MustGet("cu_user").(*utils.CuClaims)
// 调用服务层
result, err := h.cuOrderService.GetOrderSummary(c.Request.Context(), claims.CustomerID)
if err != nil {
// err已经是i18n.I18nError,直接使用
i18nErr, ok := err.(*i18n.I18nError)
if ok {
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
Code: i18nErr.Code,
Message: i18nErr.Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 如果不是i18n错误,使用通用错误
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMessage := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMessage,
Data: result,
})
}
```
### 5. 路由配置(routes/router.go)
在 cuAuth 路由组中添加:
```go
cuAuth.GET("/orders/summary", cuOrderHandler.GetOrderSummary)
```
## 数据库查询逻辑
基于现有 `cu_orders` 表结构,统计逻辑:
1. **订单总数**:查询 `customer_id = ?` 的记录数(包含所有状态)
2. **待支付订单数**:查询 `customer_id = ? AND status = 'pending'` 的记录数
3. **已支付订单数**:查询 `customer_id = ? AND status = 'paid'` 的记录数
+5
View File
@@ -0,0 +1,5 @@
# 提示词
## 接口设计提示词
请参考至少三个当前已经实现的接口,熟悉接口设计规范,然后设计新的接口。
@@ -152,3 +152,51 @@ func (h *CuDeviceHandler) UnbindDevice(c *gin.Context) {
Data: nil,
})
}
// GetDeviceSummary 获取设备汇总统计
// @Summary 获取设备汇总统计
// @Description 获取当前登录用户的设备汇总统计信息(总数、在线数、离线数)
// @Tags 客户设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.APIResponse{data=models.DeviceSummaryResponse} "获取成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/cu/devices/summary [get]
func (h *CuDeviceHandler) GetDeviceSummary(c *gin.Context) {
// 从JWT Token中获取用户ID和客户ID
claims := c.MustGet("cu_user").(*utils.CuClaims)
// 调用服务层
result, err := h.cuDeviceService.GetDeviceSummary(c.Request.Context(), claims.CustomerID)
if err != nil {
// err已经是i18n.I18nError,直接使用
i18nErr, ok := err.(*i18n.I18nError)
if ok {
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
Code: i18nErr.Code,
Message: i18nErr.Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 如果不是i18n错误,使用通用错误
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMessage := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMessage,
Data: result,
})
}
@@ -8,6 +8,7 @@ import (
"license-manager/internal/models"
"license-manager/internal/service"
"license-manager/pkg/i18n"
"license-manager/pkg/utils"
"github.com/gin-gonic/gin"
)
@@ -430,3 +431,51 @@ func (h *CuOrderHandler) GetUserOrders(c *gin.Context) {
Timestamp: getCurrentTimestamp(),
})
}
// GetOrderSummary 获取订单汇总统计
// @Summary 获取订单汇总统计
// @Description 获取当前登录用户的订单汇总统计信息(总数、待支付数、已支付数)
// @Tags 客户订单管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.APIResponse{data=models.OrderSummaryResponse} "获取成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/cu/orders/summary [get]
func (h *CuOrderHandler) GetOrderSummary(c *gin.Context) {
// 从JWT Token中获取用户ID和客户ID
claims := c.MustGet("cu_user").(*utils.CuClaims)
// 调用服务层
result, err := h.cuOrderService.GetOrderSummary(c.Request.Context(), claims.CustomerID)
if err != nil {
// err已经是i18n.I18nError,直接使用
i18nErr, ok := err.(*i18n.I18nError)
if ok {
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
Code: i18nErr.Code,
Message: i18nErr.Message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 如果不是i18n错误,使用通用错误
lang := middleware.GetLanguage(c)
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lang := middleware.GetLanguage(c)
successMessage := i18n.GetI18nErrorMessage("000000", lang)
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: successMessage,
Data: result,
})
}
+2
View File
@@ -217,6 +217,7 @@ func SetupRouter() *gin.Engine {
cuAuth.POST("/orders", cuOrderHandler.CreateOrder)
cuAuth.GET("/orders/:order_id", cuOrderHandler.GetOrder)
cuAuth.GET("/orders", cuOrderHandler.GetUserOrders)
cuAuth.GET("/orders/summary", cuOrderHandler.GetOrderSummary)
// 支付管理
cuAuth.GET("/payment/:payment_no/status", paymentHandler.GetPaymentStatus)
@@ -227,6 +228,7 @@ func SetupRouter() *gin.Engine {
// 设备管理
cuAuth.GET("/devices", cuDeviceHandler.GetDevices)
cuAuth.GET("/devices/summary", cuDeviceHandler.GetDeviceSummary)
cuAuth.DELETE("/devices/:id", cuDeviceHandler.UnbindDevice)
}
}
+7
View File
@@ -111,3 +111,10 @@ type CuOrderListResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// OrderSummaryResponse 订单汇总响应结构
type OrderSummaryResponse struct {
TotalOrders int64 `json:"total_orders"` // 订单总数
PendingOrders int64 `json:"pending_orders"` // 待支付订单数
PaidOrders int64 `json:"paid_orders"` // 已支付订单数
}
+7
View File
@@ -218,3 +218,10 @@ type DeviceListResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页数量
}
// DeviceSummaryResponse 设备汇总响应结构
type DeviceSummaryResponse struct {
TotalDevices int64 `json:"total_devices"` // 设备总数
OnlineDevices int64 `json:"online_devices"` // 在线设备数
OfflineDevices int64 `json:"offline_devices"` // 离线设备数
}
@@ -1,6 +1,7 @@
package repository
import (
"context"
"license-manager/internal/models"
"gorm.io/gorm"
@@ -12,6 +13,7 @@ type CuOrderRepository interface {
GetByOrderNo(orderNo string) (*models.CuOrder, error)
GetByCuUserID(cuUserID string, offset, limit int) ([]*models.CuOrder, int64, error)
GetByCustomerID(customerID string, offset, limit int) ([]*models.CuOrder, int64, error)
GetCustomerOrderSummary(ctx context.Context, customerID string) (*models.OrderSummaryResponse, error)
Update(order *models.CuOrder) error
Delete(id string) error
CheckTrialOrderExists(cuUserID string, currentMonth string) (bool, error)
@@ -111,3 +113,39 @@ func (r *cuOrderRepository) CheckTrialOrderExists(cuUserID string, currentMonth
return count > 0, err
}
// GetCustomerOrderSummary 获取客户订单汇总统计
func (r *cuOrderRepository) GetCustomerOrderSummary(ctx context.Context, customerID string) (*models.OrderSummaryResponse, error) {
// 查询订单总数
var totalOrders int64
err := r.db.Model(&models.CuOrder{}).
Where("customer_id = ?", customerID).
Count(&totalOrders).Error
if err != nil {
return nil, err
}
// 查询待支付订单数
var pendingOrders int64
err = r.db.Model(&models.CuOrder{}).
Where("customer_id = ? AND status = ?", customerID, "pending").
Count(&pendingOrders).Error
if err != nil {
return nil, err
}
// 查询已支付订单数
var paidOrders int64
err = r.db.Model(&models.CuOrder{}).
Where("customer_id = ? AND status = ?", customerID, "paid").
Count(&paidOrders).Error
if err != nil {
return nil, err
}
return &models.OrderSummaryResponse{
TotalOrders: totalOrders,
PendingOrders: pendingOrders,
PaidOrders: paidOrders,
}, nil
}
+4 -1
View File
@@ -126,6 +126,9 @@ type LicenseRepository interface {
// GetCustomerDeviceList 查询客户设备列表(关联授权码信息)
GetCustomerDeviceList(ctx context.Context, customerID string, req *models.DeviceListRequest) (*models.DeviceListResponse, error)
// GetCustomerDeviceSummary 获取客户设备汇总统计
GetCustomerDeviceSummary(ctx context.Context, customerID string) (*models.DeviceSummaryResponse, error)
// DeleteLicenseByID 根据ID删除许可证(物理删除,用于设备解绑)
DeleteLicenseByID(ctx context.Context, id string) error
@@ -170,4 +173,4 @@ type PaymentRepository interface {
// GetExpiredPayments 获取过期的支付订单
GetExpiredPayments(ctx context.Context) ([]*models.Payment, error)
}
}
@@ -387,6 +387,44 @@ func (r *licenseRepository) DeleteLicenseByID(ctx context.Context, id string) er
return r.db.Unscoped().Delete(&models.License{}, "id = ?", id).Error
}
// GetCustomerDeviceSummary 获取客户设备汇总统计
func (r *licenseRepository) GetCustomerDeviceSummary(ctx context.Context, customerID string) (*models.DeviceSummaryResponse, error) {
// 获取心跳超时配置(秒)
cfg := config.GetConfig()
heartbeatTimeoutSeconds := cfg.License.HeartbeatTimeout
if heartbeatTimeoutSeconds <= 0 {
heartbeatTimeoutSeconds = 300 // 默认5分钟
}
// 查询设备总数
var totalDevices int64
err := r.db.Model(&models.License{}).
Where("customer_id = ? AND status = ? AND deleted_at IS NULL", customerID, "active").
Count(&totalDevices).Error
if err != nil {
return nil, err
}
// 查询在线设备数
var onlineDevices int64
err = r.db.Model(&models.License{}).
Where("customer_id = ? AND status = ? AND deleted_at IS NULL AND last_heartbeat > ?",
customerID, "active", time.Now().Add(-time.Duration(heartbeatTimeoutSeconds)*time.Second)).
Count(&onlineDevices).Error
if err != nil {
return nil, err
}
// 计算离线设备数
offlineDevices := totalDevices - onlineDevices
return &models.DeviceSummaryResponse{
TotalDevices: totalDevices,
OnlineDevices: onlineDevices,
OfflineDevices: offlineDevices,
}, nil
}
// CheckLicenseBelongsToCustomer 检查许可证是否属于指定客户
func (r *licenseRepository) CheckLicenseBelongsToCustomer(ctx context.Context, licenseID, customerID string) (bool, error) {
var count int64
@@ -36,6 +36,19 @@ func (s *cuDeviceService) GetDeviceList(ctx context.Context, customerID string,
return s.licenseRepo.GetCustomerDeviceList(ctx, customerID, req)
}
// GetDeviceSummary 获取设备汇总统计
func (s *cuDeviceService) GetDeviceSummary(ctx context.Context, customerID string) (*models.DeviceSummaryResponse, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
// 参数验证
if customerID == "" {
return nil, i18n.NewI18nError("900001", lang) // 业务错误,不覆盖多语言message
}
// 委托给Repository层进行数据访问
return s.licenseRepo.GetCustomerDeviceSummary(ctx, customerID)
}
// UnbindDevice 解绑设备
func (s *cuDeviceService) UnbindDevice(ctx context.Context, customerID, licenseID string) error {
lang := pkgcontext.GetLanguageFromContext(ctx)
@@ -20,6 +20,7 @@ type CuOrderService interface {
CreatePendingOrder(ctx context.Context, cuUserID, customerID string, req *models.CuOrderCreateRequest) (*models.CuOrder, error)
GetOrder(ctx context.Context, orderID, cuUserID string) (*models.CuOrder, error)
GetUserOrders(ctx context.Context, cuUserID string, offset, limit int) ([]*models.CuOrder, int64, error)
GetOrderSummary(ctx context.Context, customerID string) (*models.OrderSummaryResponse, error)
CalculatePrice(ctx context.Context, packageID string, licenseCount int) (*PriceCalculationResult, error)
UpdateOrderStatus(ctx context.Context, orderID string, status string) error
}
@@ -356,6 +357,19 @@ func (s *cuOrderService) GetUserOrders(ctx context.Context, cuUserID string, off
return orders, total, nil
}
// GetOrderSummary 获取订单汇总统计
func (s *cuOrderService) GetOrderSummary(ctx context.Context, customerID string) (*models.OrderSummaryResponse, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
// 参数验证
if customerID == "" {
return nil, i18n.NewI18nError("900001", lang) // 业务错误,不覆盖多语言message
}
// 委托给Repository层进行数据访问
return s.repo.GetCustomerOrderSummary(ctx, customerID)
}
func (s *cuOrderService) CalculatePrice(ctx context.Context, packageID string, licenseCount int) (*PriceCalculationResult, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
+3 -1
View File
@@ -79,7 +79,9 @@ type CuDeviceService interface {
// 获取设备列表
GetDeviceList(ctx context.Context, customerID string, req *models.DeviceListRequest) (*models.DeviceListResponse, error)
// 获取设备汇总统计
GetDeviceSummary(ctx context.Context, customerID string) (*models.DeviceSummaryResponse, error)
// 解绑设备
UnbindDevice(ctx context.Context, customerID, licenseID string) error
}
+1 -1
View File
@@ -40,7 +40,7 @@ const (
SMSLimitKeyPrefix = "sms:limit"
SMSCodeExpireTime = 5 * time.Minute
SMSLimitTimeWindow = time.Hour
SMSLimitMaxCount = 25
SMSLimitMaxCount = 5
)
// SMSService 短信服务接口