new
This commit is contained in:
10
config.yaml
10
config.yaml
@@ -22,8 +22,8 @@ database:
|
||||
name: "tyapi_dev"
|
||||
sslmode: "disable"
|
||||
timezone: "Asia/Shanghai"
|
||||
max_open_conns: 25
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 50
|
||||
max_idle_conns: 20
|
||||
conn_max_lifetime: 300s
|
||||
auto_migrate: true
|
||||
|
||||
@@ -276,6 +276,12 @@ wallet:
|
||||
- recharge_amount: 10000.00 # 充值10000元
|
||||
bonus_amount: 800.00 # 赠送800元
|
||||
|
||||
# 余额预警配置
|
||||
balance_alert:
|
||||
default_enabled: true # 默认启用余额预警
|
||||
default_threshold: 200.00 # 默认预警阈值
|
||||
alert_cooldown_hours: 24 # 预警冷却时间(小时)
|
||||
|
||||
# ===========================================
|
||||
# 🌍 西部数据配置
|
||||
# ===========================================
|
||||
|
||||
@@ -111,3 +111,35 @@ development:
|
||||
cors_allowed_origins: "http://localhost:5173,http://localhost:8080"
|
||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||
|
||||
# ===========================================
|
||||
# 🚀 开发环境频率限制配置(放宽限制)
|
||||
# ===========================================
|
||||
daily_ratelimit:
|
||||
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
||||
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
||||
max_concurrent: 50 # 开发环境最大并发请求数
|
||||
|
||||
# 排除频率限制的路径
|
||||
exclude_paths:
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
# 排除频率限制的域名
|
||||
exclude_domains:
|
||||
- "api.*" # API二级域名不受频率限制
|
||||
- "*.api.*" # 支持多级API域名
|
||||
|
||||
# 开发环境安全配置(放宽限制)
|
||||
enable_ip_whitelist: true # 启用IP白名单
|
||||
ip_whitelist: # 开发环境IP白名单
|
||||
- "127.0.0.1" # 本地回环
|
||||
- "localhost" # 本地主机
|
||||
- "192.168.*" # 内网IP段
|
||||
- "10.*" # 内网IP段
|
||||
- "172.16.*" # 内网IP段
|
||||
|
||||
enable_ip_blacklist: false # 开发环境禁用IP黑名单
|
||||
enable_user_agent: false # 开发环境禁用User-Agent检查
|
||||
enable_referer: false # 开发环境禁用Referer检查
|
||||
enable_proxy_check: false # 开发环境禁用代理检查
|
||||
@@ -118,3 +118,42 @@ wallet:
|
||||
- recharge_amount: 10000.00 # 充值10000元
|
||||
bonus_amount: 800.00 # 赠送800元
|
||||
|
||||
# ===========================================
|
||||
# 🚦 频率限制配置 - 生产环境
|
||||
# ===========================================
|
||||
daily_ratelimit:
|
||||
max_requests_per_day: 50000 # 生产环境每日最大请求次数
|
||||
max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数
|
||||
max_concurrent: 200 # 生产环境最大并发请求数
|
||||
|
||||
# 排除频率限制的路径
|
||||
exclude_paths:
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
# 排除频率限制的域名
|
||||
exclude_domains:
|
||||
- "api.*" # API二级域名不受频率限制
|
||||
- "*.api.*" # 支持多级API域名
|
||||
|
||||
# 生产环境安全配置(严格限制)
|
||||
enable_ip_whitelist: false # 生产环境不启用IP白名单
|
||||
enable_ip_blacklist: true # 启用IP黑名单
|
||||
ip_blacklist: # 生产环境IP黑名单
|
||||
- "192.168.1.100" # 示例:被禁止的IP
|
||||
- "10.0.0.50" # 示例:被禁止的IP
|
||||
|
||||
enable_user_agent: true # 启用User-Agent检查
|
||||
blocked_user_agents: # 被阻止的User-Agent
|
||||
- "curl" # 阻止curl请求
|
||||
- "wget" # 阻止wget请求
|
||||
- "python-requests" # 阻止Python requests
|
||||
|
||||
enable_referer: true # 启用Referer检查
|
||||
allowed_referers: # 允许的Referer
|
||||
- "https://console.tianyuanapi.com"
|
||||
- "https://consoletest.tianyuanapi.com"
|
||||
|
||||
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
|
||||
enable_proxy_check: true # 启用代理检查
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// 测试发票申请通过的事件系统
|
||||
func main() {
|
||||
fmt.Println("🔍 开始测试发票事件系统...")
|
||||
|
||||
// 1. 首先获取待处理的发票申请列表
|
||||
fmt.Println("\n📋 步骤1: 获取待处理的发票申请列表")
|
||||
applications, err := getPendingApplications()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 获取申请列表失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(applications) == 0 {
|
||||
fmt.Println("⚠️ 没有待处理的发票申请")
|
||||
return
|
||||
}
|
||||
|
||||
// 选择第一个申请进行测试
|
||||
application := applications[0]
|
||||
fmt.Printf("✅ 找到申请: ID=%s, 公司=%s, 金额=%s\n",
|
||||
application["id"], application["company_name"], application["amount"])
|
||||
|
||||
// 2. 创建一个测试PDF文件
|
||||
fmt.Println("\n📄 步骤2: 创建测试PDF文件")
|
||||
testFile, err := createTestPDF()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 创建测试文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(testFile)
|
||||
|
||||
// 3. 通过发票申请(上传文件)
|
||||
fmt.Println("\n📤 步骤3: 通过发票申请并上传文件")
|
||||
err = approveInvoiceApplication(application["id"].(string), testFile)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 通过申请失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ 发票申请通过成功!")
|
||||
fmt.Println("📧 请检查日志中的邮件发送情况...")
|
||||
}
|
||||
|
||||
// 获取待处理的发票申请列表
|
||||
func getPendingApplications() ([]map[string]interface{}, error) {
|
||||
url := "http://localhost:8080/api/v1/admin/invoices/pending"
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result["code"] != float64(200) {
|
||||
return nil, fmt.Errorf("API返回错误: %s", result["message"])
|
||||
}
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
applications := data["applications"].([]interface{})
|
||||
|
||||
applicationsList := make([]map[string]interface{}, len(applications))
|
||||
for i, app := range applications {
|
||||
applicationsList[i] = app.(map[string]interface{})
|
||||
}
|
||||
|
||||
return applicationsList, nil
|
||||
}
|
||||
|
||||
// 创建测试PDF文件
|
||||
func createTestPDF() (string, error) {
|
||||
// 创建一个简单的PDF内容(这里只是示例)
|
||||
pdfContent := []byte("%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 12 Tf\n72 720 Td\n(Test Invoice) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000204 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n297\n%%EOF\n")
|
||||
|
||||
tempFile := filepath.Join(os.TempDir(), "test_invoice.pdf")
|
||||
err := os.WriteFile(tempFile, pdfContent, 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tempFile, nil
|
||||
}
|
||||
|
||||
// 通过发票申请
|
||||
func approveInvoiceApplication(applicationID, filePath string) error {
|
||||
url := fmt.Sprintf("http://localhost:8080/api/v1/admin/invoices/%s/approve", applicationID)
|
||||
|
||||
// 创建multipart表单
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
// 添加文件
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
part, err := writer.CreateFormFile("file", "test_invoice.pdf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(part, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 添加备注
|
||||
writer.WriteField("admin_notes", "测试通过 - 调试事件系统")
|
||||
|
||||
writer.Close()
|
||||
|
||||
// 发送请求
|
||||
req, err := http.NewRequest("POST", url, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result["code"] != float64(200) {
|
||||
return fmt.Errorf("API返回错误: %s", result["message"])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -60,27 +60,6 @@ services:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# TYAPI Worker (定时任务处理) - 开发环境
|
||||
tyapi-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.worker
|
||||
container_name: tyapi-worker-dev
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
ENV: development
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- .:/app # 开发环境挂载代码目录
|
||||
networks:
|
||||
- tyapi-network
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# Jaeger 链路追踪
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:1.70.0
|
||||
|
||||
603
docs/api/statistics/api_documentation.md
Normal file
603
docs/api/statistics/api_documentation.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# 统计功能API文档
|
||||
|
||||
## 概述
|
||||
|
||||
统计功能API提供了完整的统计数据分析和管理功能,包括指标管理、实时统计、历史统计、仪表板管理、报告生成、数据导出等功能。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础URL**: `/api/v1/statistics`
|
||||
- **认证方式**: Bearer Token
|
||||
- **内容类型**: `application/json`
|
||||
- **字符编码**: `UTF-8`
|
||||
|
||||
## 认证和权限
|
||||
|
||||
### 认证方式
|
||||
所有API请求都需要在请求头中包含有效的JWT令牌:
|
||||
```
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
### 权限级别
|
||||
- **公开访问**: 无需认证的接口
|
||||
- **用户权限**: 需要用户或管理员权限
|
||||
- **管理员权限**: 仅管理员可访问
|
||||
|
||||
## API接口
|
||||
|
||||
### 1. 指标管理
|
||||
|
||||
#### 1.1 创建统计指标
|
||||
- **URL**: `POST /api/v1/statistics/metrics`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 创建新的统计指标
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"metric_type": "api_calls",
|
||||
"metric_name": "total_count",
|
||||
"dimension": "realtime",
|
||||
"value": 100.0,
|
||||
"metadata": "{\"source\": \"api_gateway\"}",
|
||||
"date": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "指标创建成功",
|
||||
"data": {
|
||||
"id": "uuid",
|
||||
"metric_type": "api_calls",
|
||||
"metric_name": "total_count",
|
||||
"dimension": "realtime",
|
||||
"value": 100.0,
|
||||
"metadata": "{\"source\": \"api_gateway\"}",
|
||||
"date": "2024-01-01T00:00:00Z",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 更新统计指标
|
||||
- **URL**: `PUT /api/v1/statistics/metrics`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 更新现有统计指标的值
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"value": 150.0
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 删除统计指标
|
||||
- **URL**: `DELETE /api/v1/statistics/metrics`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 删除指定的统计指标
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 获取单个指标
|
||||
- **URL**: `GET /api/v1/statistics/metrics/{id}`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据ID获取指定的统计指标
|
||||
|
||||
#### 1.5 获取指标列表
|
||||
- **URL**: `GET /api/v1/statistics/metrics`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据条件获取统计指标列表
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string): 指标类型
|
||||
- `metric_name` (string): 指标名称
|
||||
- `dimension` (string): 统计维度
|
||||
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||
- `limit` (int): 限制数量 (默认20, 最大1000)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `sort_by` (string): 排序字段 (默认created_at)
|
||||
- `sort_order` (string): 排序顺序 (默认desc)
|
||||
|
||||
### 2. 实时统计
|
||||
|
||||
#### 2.1 获取实时指标
|
||||
- **URL**: `GET /api/v1/statistics/realtime`
|
||||
- **权限**: 公开
|
||||
- **描述**: 获取指定类型的实时统计指标
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `time_range` (string): 时间范围 (last_hour, last_day, last_week)
|
||||
- `dimension` (string): 统计维度
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取实时指标成功",
|
||||
"data": {
|
||||
"metric_type": "api_calls",
|
||||
"metrics": {
|
||||
"total_count": 1000,
|
||||
"success_count": 950,
|
||||
"failed_count": 50,
|
||||
"success_rate": 95.0
|
||||
},
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"metadata": {
|
||||
"time_range": "last_hour",
|
||||
"dimension": "realtime"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 历史统计
|
||||
|
||||
#### 3.1 获取历史指标
|
||||
- **URL**: `GET /api/v1/statistics/historical`
|
||||
- **权限**: 公开
|
||||
- **描述**: 获取指定时间范围的历史统计指标
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `metric_name` (string): 指标名称
|
||||
- `dimension` (string): 统计维度
|
||||
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
|
||||
- `period` (string): 统计周期
|
||||
- `limit` (int): 限制数量 (默认20)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `aggregate_by` (string): 聚合维度
|
||||
- `group_by` (string): 分组维度
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取历史指标成功",
|
||||
"data": {
|
||||
"metric_type": "api_calls",
|
||||
"metric_name": "total_count",
|
||||
"dimension": "daily",
|
||||
"data_points": [
|
||||
{
|
||||
"date": "2024-01-01T00:00:00Z",
|
||||
"value": 1000,
|
||||
"label": "total_count"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 30000,
|
||||
"average": 1000,
|
||||
"max": 1500,
|
||||
"min": 500,
|
||||
"count": 30,
|
||||
"growth_rate": 5.2,
|
||||
"trend": "increasing"
|
||||
},
|
||||
"metadata": {
|
||||
"period": "daily",
|
||||
"aggregate_by": "day",
|
||||
"group_by": "metric_name"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 仪表板管理
|
||||
|
||||
#### 4.1 创建仪表板
|
||||
- **URL**: `POST /api/v1/statistics/dashboards`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 创建新的统计仪表板
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "用户仪表板",
|
||||
"description": "普通用户专用仪表板",
|
||||
"user_role": "user",
|
||||
"layout": "{\"columns\": 2, \"rows\": 3}",
|
||||
"widgets": "[{\"type\": \"api_calls\", \"position\": {\"x\": 0, \"y\": 0}}]",
|
||||
"settings": "{\"theme\": \"light\", \"auto_refresh\": false}",
|
||||
"refresh_interval": 600,
|
||||
"access_level": "private",
|
||||
"created_by": "user_id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 获取仪表板列表
|
||||
- **URL**: `GET /api/v1/statistics/dashboards`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据条件获取统计仪表板列表
|
||||
|
||||
**查询参数**:
|
||||
- `user_role` (string): 用户角色
|
||||
- `is_default` (bool): 是否默认
|
||||
- `is_active` (bool): 是否激活
|
||||
- `access_level` (string): 访问级别
|
||||
- `created_by` (string): 创建者ID
|
||||
- `name` (string): 仪表板名称
|
||||
- `limit` (int): 限制数量 (默认20)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `sort_by` (string): 排序字段 (默认created_at)
|
||||
- `sort_order` (string): 排序顺序 (默认desc)
|
||||
|
||||
#### 4.3 获取单个仪表板
|
||||
- **URL**: `GET /api/v1/statistics/dashboards/{id}`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据ID获取指定的统计仪表板
|
||||
|
||||
#### 4.4 获取仪表板数据
|
||||
- **URL**: `GET /api/v1/statistics/dashboards/data`
|
||||
- **权限**: 公开
|
||||
- **描述**: 获取指定角色的仪表板数据
|
||||
|
||||
**查询参数**:
|
||||
- `user_role` (string, 必需): 用户角色
|
||||
- `period` (string): 统计周期 (today, week, month)
|
||||
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||
- `metric_types` (string): 指标类型列表
|
||||
- `dimensions` (string): 统计维度列表
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取仪表板数据成功",
|
||||
"data": {
|
||||
"api_calls": {
|
||||
"total_count": 10000,
|
||||
"success_count": 9500,
|
||||
"failed_count": 500,
|
||||
"success_rate": 95.0,
|
||||
"avg_response_time": 150.5
|
||||
},
|
||||
"users": {
|
||||
"total_count": 1000,
|
||||
"certified_count": 800,
|
||||
"active_count": 750,
|
||||
"certification_rate": 80.0,
|
||||
"retention_rate": 75.0
|
||||
},
|
||||
"finance": {
|
||||
"total_amount": 50000.0,
|
||||
"recharge_amount": 60000.0,
|
||||
"deduct_amount": 10000.0,
|
||||
"net_amount": 50000.0
|
||||
},
|
||||
"period": {
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-01-01",
|
||||
"period": "today"
|
||||
},
|
||||
"metadata": {
|
||||
"generated_at": "2024-01-01 12:00:00",
|
||||
"user_role": "user",
|
||||
"data_version": "1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 报告管理
|
||||
|
||||
#### 5.1 生成报告
|
||||
- **URL**: `POST /api/v1/statistics/reports`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 生成指定类型的统计报告
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"report_type": "summary",
|
||||
"title": "月度汇总报告",
|
||||
"period": "month",
|
||||
"user_role": "admin",
|
||||
"start_date": "2024-01-01T00:00:00Z",
|
||||
"end_date": "2024-01-31T23:59:59Z",
|
||||
"filters": {
|
||||
"metric_types": ["api_calls", "users"],
|
||||
"dimensions": ["daily", "weekly"]
|
||||
},
|
||||
"generated_by": "admin_id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2 获取报告列表
|
||||
- **URL**: `GET /api/v1/statistics/reports`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据条件获取统计报告列表
|
||||
|
||||
**查询参数**:
|
||||
- `report_type` (string): 报告类型
|
||||
- `user_role` (string): 用户角色
|
||||
- `status` (string): 报告状态
|
||||
- `period` (string): 统计周期
|
||||
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||
- `limit` (int): 限制数量 (默认20)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `sort_by` (string): 排序字段 (默认created_at)
|
||||
- `sort_order` (string): 排序顺序 (默认desc)
|
||||
- `generated_by` (string): 生成者ID
|
||||
|
||||
#### 5.3 获取单个报告
|
||||
- **URL**: `GET /api/v1/statistics/reports/{id}`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据ID获取指定的统计报告
|
||||
|
||||
### 6. 统计分析
|
||||
|
||||
#### 6.1 计算增长率
|
||||
- **URL**: `GET /api/v1/statistics/analysis/growth-rate`
|
||||
- **权限**: 用户
|
||||
- **描述**: 计算指定指标的增长率
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `metric_name` (string, 必需): 指标名称
|
||||
- `current_period` (string, 必需): 当前周期 (YYYY-MM-DD)
|
||||
- `previous_period` (string, 必需): 上一周期 (YYYY-MM-DD)
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "计算增长率成功",
|
||||
"data": {
|
||||
"growth_rate": 15.5,
|
||||
"current_value": 1150,
|
||||
"previous_value": 1000,
|
||||
"period": "daily"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 计算趋势
|
||||
- **URL**: `GET /api/v1/statistics/analysis/trend`
|
||||
- **权限**: 用户
|
||||
- **描述**: 计算指定指标的趋势
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `metric_name` (string, 必需): 指标名称
|
||||
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "计算趋势成功",
|
||||
"data": {
|
||||
"trend": "increasing",
|
||||
"trend_strength": 0.8,
|
||||
"data_points": 30,
|
||||
"correlation": 0.75
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 数据导出
|
||||
|
||||
#### 7.1 导出数据
|
||||
- **URL**: `POST /api/v1/statistics/export`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 导出指定格式的统计数据
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"format": "excel",
|
||||
"metric_type": "api_calls",
|
||||
"start_date": "2024-01-01T00:00:00Z",
|
||||
"end_date": "2024-01-31T23:59:59Z",
|
||||
"dimension": "daily",
|
||||
"group_by": "metric_name",
|
||||
"filters": {
|
||||
"status": "success"
|
||||
},
|
||||
"columns": ["date", "metric_name", "value"],
|
||||
"include_charts": true,
|
||||
"exported_by": "admin_id"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "数据导出成功",
|
||||
"data": {
|
||||
"download_url": "https://api.example.com/downloads/export_123.xlsx",
|
||||
"file_name": "api_calls_export_20240101_20240131.xlsx",
|
||||
"file_size": 1024000,
|
||||
"expires_at": "2024-01-02T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. 定时任务管理
|
||||
|
||||
#### 8.1 手动触发小时聚合
|
||||
- **URL**: `POST /api/v1/statistics/cron/hourly-aggregation`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 手动触发指定时间的小时级数据聚合
|
||||
|
||||
**查询参数**:
|
||||
- `target_hour` (string, 必需): 目标小时 (YYYY-MM-DDTHH:MM:SSZ)
|
||||
|
||||
#### 8.2 手动触发日聚合
|
||||
- **URL**: `POST /api/v1/statistics/cron/daily-aggregation`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 手动触发指定时间的日级数据聚合
|
||||
|
||||
**查询参数**:
|
||||
- `target_date` (string, 必需): 目标日期 (YYYY-MM-DD)
|
||||
|
||||
#### 8.3 手动触发数据清理
|
||||
- **URL**: `POST /api/v1/statistics/cron/data-cleanup`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 手动触发过期数据清理任务
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | HTTP状态码 | 描述 |
|
||||
|--------|------------|------|
|
||||
| 400 | 400 Bad Request | 请求参数错误 |
|
||||
| 401 | 401 Unauthorized | 未认证或认证失败 |
|
||||
| 403 | 403 Forbidden | 权限不足 |
|
||||
| 404 | 404 Not Found | 资源不存在 |
|
||||
| 422 | 422 Unprocessable Entity | 参数验证失败 |
|
||||
| 429 | 429 Too Many Requests | 请求频率过高 |
|
||||
| 500 | 500 Internal Server Error | 服务器内部错误 |
|
||||
|
||||
## 响应格式
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
// 响应数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述",
|
||||
"error": "详细错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 列表响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "查询成功",
|
||||
"data": [
|
||||
// 数据列表
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 100,
|
||||
"pages": 5,
|
||||
"has_next": true,
|
||||
"has_prev": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 统计指标 (StatisticsMetric)
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"metric_type": "string",
|
||||
"metric_name": "string",
|
||||
"dimension": "string",
|
||||
"value": "number",
|
||||
"metadata": "string",
|
||||
"date": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### 统计报告 (StatisticsReport)
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"report_type": "string",
|
||||
"title": "string",
|
||||
"content": "string",
|
||||
"period": "string",
|
||||
"user_role": "string",
|
||||
"status": "string",
|
||||
"generated_by": "string",
|
||||
"generated_at": "string",
|
||||
"expires_at": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### 统计仪表板 (StatisticsDashboard)
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"user_role": "string",
|
||||
"is_default": "boolean",
|
||||
"is_active": "boolean",
|
||||
"layout": "string",
|
||||
"widgets": "string",
|
||||
"settings": "string",
|
||||
"refresh_interval": "number",
|
||||
"created_by": "string",
|
||||
"access_level": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 获取今日API调用统计
|
||||
```bash
|
||||
curl -X GET "https://api.example.com/api/v1/statistics/realtime?metric_type=api_calls&time_range=last_hour" \
|
||||
-H "Authorization: Bearer your-jwt-token"
|
||||
```
|
||||
|
||||
### 获取历史用户数据
|
||||
```bash
|
||||
curl -X GET "https://api.example.com/api/v1/statistics/historical?metric_type=users&start_date=2024-01-01&end_date=2024-01-31" \
|
||||
-H "Authorization: Bearer your-jwt-token"
|
||||
```
|
||||
|
||||
### 生成月度报告
|
||||
```bash
|
||||
curl -X POST "https://api.example.com/api/v1/statistics/reports" \
|
||||
-H "Authorization: Bearer your-jwt-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"report_type": "summary",
|
||||
"title": "月度汇总报告",
|
||||
"period": "month",
|
||||
"user_role": "admin",
|
||||
"generated_by": "admin_id"
|
||||
}'
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **日期格式**: 所有日期参数都使用 `YYYY-MM-DD` 格式
|
||||
2. **时间戳**: 所有时间戳都使用 ISO 8601 格式
|
||||
3. **分页**: 默认每页20条记录,最大1000条
|
||||
4. **限流**: API有请求频率限制,超出限制会返回429错误
|
||||
5. **缓存**: 部分接口支持缓存,响应头会包含缓存信息
|
||||
6. **权限**: 不同接口需要不同的权限级别,请确保有相应权限
|
||||
7. **数据量**: 查询大量数据时建议使用分页和日期范围限制
|
||||
|
||||
8
go.mod
8
go.mod
@@ -15,6 +15,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/qiniu/go-sdk/v7 v7.25.4
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/smartwalle/alipay/v3 v3.2.25
|
||||
github.com/spf13/viper v1.20.1
|
||||
@@ -23,6 +24,7 @@ require (
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/xuri/excelize/v2 v2.9.1
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
||||
go.opentelemetry.io/otel/sdk v1.37.0
|
||||
@@ -85,7 +87,8 @@ require (
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
||||
github.com/smartwalle/ngx v1.0.9 // indirect
|
||||
@@ -97,8 +100,11 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
|
||||
15
go.sum
15
go.sum
@@ -191,6 +191,11 @@ github.com/qiniu/go-sdk/v7 v7.25.4/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peq
|
||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
@@ -245,6 +250,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
|
||||
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
@@ -253,6 +260,12 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
@@ -300,6 +313,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
|
||||
@@ -25,8 +25,12 @@ import (
|
||||
// 文章域实体
|
||||
articleEntities "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
// 统计域实体
|
||||
statisticsEntities "tyapi-server/internal/domains/statistics/entities"
|
||||
|
||||
apiEntities "tyapi-server/internal/domains/api/entities"
|
||||
"tyapi-server/internal/infrastructure/database"
|
||||
taskEntities "tyapi-server/internal/infrastructure/task/entities"
|
||||
)
|
||||
|
||||
// Application 应用程序结构
|
||||
@@ -242,9 +246,17 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&articleEntities.Tag{},
|
||||
&articleEntities.ScheduledTask{},
|
||||
|
||||
// 统计域
|
||||
&statisticsEntities.StatisticsMetric{},
|
||||
&statisticsEntities.StatisticsDashboard{},
|
||||
&statisticsEntities.StatisticsReport{},
|
||||
|
||||
// api
|
||||
&apiEntities.ApiUser{},
|
||||
&apiEntities.ApiCall{},
|
||||
|
||||
// 任务域
|
||||
&taskEntities.AsyncTask{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -323,11 +335,3 @@ func (a *Application) RunCommand(command string, args ...string) error {
|
||||
return fmt.Errorf("unknown command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
// GetArticleService 获取文章服务 (用于 Worker)
|
||||
func (app *Application) GetArticleService() interface{} {
|
||||
// 这里需要从容器中获取文章服务
|
||||
// 由于循环导入问题,暂时返回 nil
|
||||
// 实际使用时需要通过其他方式获取
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,3 +24,48 @@ type DecryptCommand struct {
|
||||
EncryptedData string `json:"encrypted_data" binding:"required"`
|
||||
SecretKey string `json:"secret_key" binding:"required"`
|
||||
}
|
||||
|
||||
// SaveApiCallCommand 保存API调用命令
|
||||
type SaveApiCallCommand struct {
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
Status string `json:"status"`
|
||||
Cost float64 `json:"cost"`
|
||||
ErrorType string `json:"error_type"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
}
|
||||
|
||||
// ProcessDeductionCommand 处理扣款命令
|
||||
type ProcessDeductionCommand struct {
|
||||
UserID string `json:"user_id"`
|
||||
Amount string `json:"amount"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
}
|
||||
|
||||
// UpdateUsageStatsCommand 更新使用统计命令
|
||||
type UpdateUsageStatsCommand struct {
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Increment int `json:"increment"`
|
||||
}
|
||||
|
||||
// RecordApiLogCommand 记录API日志命令
|
||||
type RecordApiLogCommand struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiName string `json:"api_name"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
ResponseSize int64 `json:"response_size"`
|
||||
}
|
||||
|
||||
// ProcessCompensationCommand 处理补偿命令
|
||||
type ProcessCompensationCommand struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
96
internal/application/api/dto/api_call_validation.go
Normal file
96
internal/application/api/dto/api_call_validation.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
api_entities "tyapi-server/internal/domains/api/entities"
|
||||
product_entities "tyapi-server/internal/domains/product/entities"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// ApiCallValidationResult API调用验证结果
|
||||
type ApiCallValidationResult struct {
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
|
||||
// 新增字段
|
||||
ContractCode string `json:"contract_code"`
|
||||
ApiCall *api_entities.ApiCall `json:"api_call"`
|
||||
RequestParams map[string]interface{} `json:"request_params"`
|
||||
Product *product_entities.Product `json:"product"`
|
||||
}
|
||||
|
||||
// GetUserID 获取用户ID
|
||||
func (r *ApiCallValidationResult) GetUserID() string {
|
||||
return r.UserID
|
||||
}
|
||||
|
||||
// GetProductID 获取产品ID
|
||||
func (r *ApiCallValidationResult) GetProductID() string {
|
||||
return r.ProductID
|
||||
}
|
||||
|
||||
// GetSubscriptionID 获取订阅ID
|
||||
func (r *ApiCallValidationResult) GetSubscriptionID() string {
|
||||
return r.SubscriptionID
|
||||
}
|
||||
|
||||
// GetAmount 获取金额
|
||||
func (r *ApiCallValidationResult) GetAmount() decimal.Decimal {
|
||||
return r.Amount
|
||||
}
|
||||
|
||||
// GetSecretKey 获取密钥
|
||||
func (r *ApiCallValidationResult) GetSecretKey() string {
|
||||
return r.SecretKey
|
||||
}
|
||||
|
||||
// IsValidResult 检查是否有效
|
||||
func (r *ApiCallValidationResult) IsValidResult() bool {
|
||||
return r.IsValid
|
||||
}
|
||||
|
||||
// GetErrorMessage 获取错误消息
|
||||
func (r *ApiCallValidationResult) GetErrorMessage() string {
|
||||
return r.ErrorMessage
|
||||
}
|
||||
|
||||
// NewApiCallValidationResult 创建新的API调用验证结果
|
||||
func NewApiCallValidationResult() *ApiCallValidationResult {
|
||||
return &ApiCallValidationResult{
|
||||
IsValid: true,
|
||||
RequestParams: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetApiUser 设置API用户
|
||||
func (r *ApiCallValidationResult) SetApiUser(apiUser *api_entities.ApiUser) {
|
||||
r.UserID = apiUser.UserId
|
||||
r.SecretKey = apiUser.SecretKey
|
||||
}
|
||||
|
||||
// SetProduct 设置产品
|
||||
func (r *ApiCallValidationResult) SetProduct(product *product_entities.Product) {
|
||||
r.ProductID = product.ID
|
||||
r.Amount = product.Price
|
||||
r.Product = product
|
||||
}
|
||||
|
||||
// SetApiCall 设置API调用
|
||||
func (r *ApiCallValidationResult) SetApiCall(apiCall *api_entities.ApiCall) {
|
||||
r.ApiCall = apiCall
|
||||
}
|
||||
|
||||
// SetRequestParams 设置请求参数
|
||||
func (r *ApiCallValidationResult) SetRequestParams(params map[string]interface{}) {
|
||||
r.RequestParams = params
|
||||
}
|
||||
|
||||
// SetContractCode 设置合同代码
|
||||
func (r *ApiCallValidationResult) SetContractCode(code string) {
|
||||
r.ContractCode = code
|
||||
}
|
||||
@@ -14,9 +14,13 @@ var (
|
||||
ErrInvalidAccessId = errors.New("未经授权的AccessId")
|
||||
ErrFrozenAccount = errors.New("账户已冻结")
|
||||
ErrArrears = errors.New("账户余额不足,无法请求")
|
||||
ErrInsufficientBalance = errors.New("钱包余额不足")
|
||||
ErrProductNotFound = errors.New("产品不存在")
|
||||
ErrProductDisabled = errors.New("产品已停用")
|
||||
ErrNotSubscribed = errors.New("未订阅此产品")
|
||||
ErrProductNotSubscribed = errors.New("未订阅此产品")
|
||||
ErrSubscriptionExpired = errors.New("订阅已过期")
|
||||
ErrSubscriptionSuspended = errors.New("订阅已暂停")
|
||||
ErrBusiness = errors.New("业务失败")
|
||||
)
|
||||
|
||||
@@ -32,9 +36,13 @@ var ErrorCodeMap = map[error]int{
|
||||
ErrInvalidAccessId: 1006,
|
||||
ErrFrozenAccount: 1007,
|
||||
ErrArrears: 1007,
|
||||
ErrInsufficientBalance: 1007,
|
||||
ErrProductNotFound: 1008,
|
||||
ErrProductDisabled: 1008,
|
||||
ErrNotSubscribed: 1008,
|
||||
ErrProductNotSubscribed: 1008,
|
||||
ErrSubscriptionExpired: 1008,
|
||||
ErrSubscriptionSuspended: 1008,
|
||||
ErrBusiness: 2001,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
|
||||
"tyapi-server/internal/domains/article/services"
|
||||
"tyapi-server/internal/infrastructure/task"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
task_entities "tyapi-server/internal/infrastructure/task/entities"
|
||||
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
|
||||
shared_interfaces "tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ type ArticleApplicationServiceImpl struct {
|
||||
categoryRepo repositories.CategoryRepository
|
||||
tagRepo repositories.TagRepository
|
||||
articleService *services.ArticleService
|
||||
asynqClient *task.AsynqClient
|
||||
taskManager task_interfaces.TaskManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ func NewArticleApplicationService(
|
||||
categoryRepo repositories.CategoryRepository,
|
||||
tagRepo repositories.TagRepository,
|
||||
articleService *services.ArticleService,
|
||||
asynqClient *task.AsynqClient,
|
||||
taskManager task_interfaces.TaskManager,
|
||||
logger *zap.Logger,
|
||||
) ArticleApplicationService {
|
||||
return &ArticleApplicationServiceImpl{
|
||||
@@ -40,7 +41,7 @@ func NewArticleApplicationService(
|
||||
categoryRepo: categoryRepo,
|
||||
tagRepo: tagRepo,
|
||||
articleService: articleService,
|
||||
asynqClient: asynqClient,
|
||||
taskManager: taskManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -337,32 +338,37 @@ func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Conte
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 如果已有定时任务,先取消
|
||||
if article.TaskID != "" {
|
||||
if err := s.asynqClient.CancelScheduledTask(ctx, article.TaskID); err != nil {
|
||||
s.logger.Warn("取消旧定时任务失败", zap.String("task_id", article.TaskID), zap.Error(err))
|
||||
}
|
||||
// 3. 取消旧任务
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消旧任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 4. 调度定时发布任务
|
||||
taskID, err := s.asynqClient.ScheduleArticlePublish(ctx, cmd.ID, scheduledTime)
|
||||
if err != nil {
|
||||
s.logger.Error("调度定时发布任务失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("调度定时发布任务失败: %w", err)
|
||||
// 4. 创建任务工厂
|
||||
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||
|
||||
// 5. 创建并异步入队文章发布任务
|
||||
if err := taskFactory.CreateAndEnqueueArticlePublishTask(
|
||||
ctx,
|
||||
cmd.ID,
|
||||
scheduledTime,
|
||||
"system", // 暂时使用系统用户ID
|
||||
); err != nil {
|
||||
s.logger.Error("创建并入队文章发布任务失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 设置定时发布
|
||||
if err := article.SchedulePublish(scheduledTime, taskID); err != nil {
|
||||
// 6. 设置定时发布
|
||||
if err := article.SchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 6. 保存更新
|
||||
// 7. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", scheduledTime), zap.String("task_id", taskID))
|
||||
s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -381,10 +387,9 @@ func (s *ArticleApplicationServiceImpl) CancelSchedulePublishArticle(ctx context
|
||||
}
|
||||
|
||||
// 3. 取消定时任务
|
||||
if article.TaskID != "" {
|
||||
if err := s.asynqClient.CancelScheduledTask(ctx, article.TaskID); err != nil {
|
||||
s.logger.Warn("取消定时任务失败", zap.String("task_id", article.TaskID), zap.Error(err))
|
||||
}
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消定时任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
|
||||
// 不返回错误,继续执行取消定时发布
|
||||
}
|
||||
|
||||
// 4. 取消定时发布
|
||||
@@ -613,7 +618,7 @@ func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, que
|
||||
// ListCategories 获取分类列表
|
||||
func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*responses.CategoryListResponse, error) {
|
||||
// 1. 获取分类列表
|
||||
categories, err := s.categoryRepo.List(ctx, interfaces.ListOptions{})
|
||||
categories, err := s.categoryRepo.List(ctx, shared_interfaces.ListOptions{})
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类列表失败: %w", err)
|
||||
@@ -730,7 +735,7 @@ func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *a
|
||||
// ListTags 获取标签列表
|
||||
func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*responses.TagListResponse, error) {
|
||||
// 1. 获取标签列表
|
||||
tags, err := s.tagRepo.List(ctx, interfaces.ListOptions{})
|
||||
tags, err := s.tagRepo.List(ctx, shared_interfaces.ListOptions{})
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取标签列表失败: %w", err)
|
||||
@@ -776,15 +781,14 @@ func (s *ArticleApplicationServiceImpl) UpdateSchedulePublishArticle(ctx context
|
||||
return fmt.Errorf("文章未设置定时发布,无法修改时间")
|
||||
}
|
||||
|
||||
// 4. 重新调度定时发布任务
|
||||
newTaskID, err := s.asynqClient.RescheduleArticlePublish(ctx, cmd.ID, article.TaskID, scheduledTime)
|
||||
if err != nil {
|
||||
s.logger.Error("重新调度定时发布任务失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
// 4. 更新数据库中的任务调度时间
|
||||
if err := s.taskManager.UpdateTaskSchedule(ctx, cmd.ID, scheduledTime); err != nil {
|
||||
s.logger.Error("更新任务调度时间失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 更新定时发布
|
||||
if err := article.UpdateSchedulePublish(scheduledTime, newTaskID); err != nil {
|
||||
if err := article.UpdateSchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("更新定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -796,8 +800,7 @@ func (s *ArticleApplicationServiceImpl) UpdateSchedulePublishArticle(ctx context
|
||||
|
||||
s.logger.Info("修改定时发布时间成功",
|
||||
zap.String("id", article.ID),
|
||||
zap.Time("new_scheduled_time", scheduledTime),
|
||||
zap.String("new_task_id", newTaskID))
|
||||
zap.Time("new_scheduled_time", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ type CertificationApplicationService interface {
|
||||
// 申请合同签署
|
||||
ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error)
|
||||
|
||||
// OCR营业执照识别
|
||||
RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*responses.BusinessLicenseResult, error)
|
||||
|
||||
// ================ 查询用例 ================
|
||||
|
||||
// 获取认证详情
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/storage"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/esign"
|
||||
sharedOCR "tyapi-server/internal/shared/ocr"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -40,6 +41,7 @@ type CertificationApplicationServiceImpl struct {
|
||||
walletAggregateService finance_service.WalletAggregateService
|
||||
apiUserAggregateService api_service.ApiUserAggregateService
|
||||
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService
|
||||
ocrService sharedOCR.OCRService
|
||||
// 仓储依赖
|
||||
queryRepository repositories.CertificationQueryRepository
|
||||
enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository
|
||||
@@ -62,6 +64,7 @@ func NewCertificationApplicationService(
|
||||
walletAggregateService finance_service.WalletAggregateService,
|
||||
apiUserAggregateService api_service.ApiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService,
|
||||
ocrService sharedOCR.OCRService,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
) CertificationApplicationService {
|
||||
@@ -78,6 +81,7 @@ func NewCertificationApplicationService(
|
||||
walletAggregateService: walletAggregateService,
|
||||
apiUserAggregateService: apiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService,
|
||||
ocrService: ocrService,
|
||||
txManager: txManager,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -987,3 +991,33 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// RecognizeBusinessLicense OCR识别营业执照
|
||||
func (s *CertificationApplicationServiceImpl) RecognizeBusinessLicense(
|
||||
ctx context.Context,
|
||||
imageBytes []byte,
|
||||
) (*responses.BusinessLicenseResult, error) {
|
||||
s.logger.Info("开始OCR识别营业执照", zap.Int("image_size", len(imageBytes)))
|
||||
|
||||
// 调用OCR服务识别营业执照
|
||||
result, err := s.ocrService.RecognizeBusinessLicense(ctx, imageBytes)
|
||||
if err != nil {
|
||||
s.logger.Error("OCR识别营业执照失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("营业执照识别失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证识别结果
|
||||
if err := s.ocrService.ValidateBusinessLicense(result); err != nil {
|
||||
s.logger.Error("营业执照识别结果验证失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("营业执照识别结果不完整: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("营业执照OCR识别成功",
|
||||
zap.String("company_name", result.CompanyName),
|
||||
zap.String("unified_social_code", result.UnifiedSocialCode),
|
||||
zap.String("legal_person_name", result.LegalPersonName),
|
||||
zap.Float64("confidence", result.Confidence),
|
||||
)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package finance
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"tyapi-server/internal/application/finance/dto/commands"
|
||||
"tyapi-server/internal/application/finance/dto/queries"
|
||||
"tyapi-server/internal/application/finance/dto/responses"
|
||||
@@ -12,29 +11,34 @@ import (
|
||||
|
||||
// FinanceApplicationService 财务应用服务接口
|
||||
type FinanceApplicationService interface {
|
||||
|
||||
// 钱包管理
|
||||
CreateWallet(ctx context.Context, cmd *commands.CreateWalletCommand) (*responses.WalletResponse, error)
|
||||
GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error)
|
||||
|
||||
// 充值管理
|
||||
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
|
||||
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
|
||||
// 交易记录
|
||||
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
|
||||
// 导出功能
|
||||
ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
|
||||
// 支付宝回调处理
|
||||
HandleAlipayCallback(ctx context.Context, r *http.Request) error
|
||||
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
||||
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
||||
|
||||
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
|
||||
// 获取用户钱包交易记录
|
||||
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
|
||||
// 管理端消费记录
|
||||
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
|
||||
// 获取用户充值记录
|
||||
// 充值记录
|
||||
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
|
||||
// 管理员获取充值记录
|
||||
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
|
||||
// 获取充值配置
|
||||
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/export"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/payment"
|
||||
|
||||
@@ -30,6 +31,7 @@ type FinanceApplicationServiceImpl struct {
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
txManager *database.TransactionManager
|
||||
exportManager *export.ExportManager
|
||||
logger *zap.Logger
|
||||
config *config.Config
|
||||
}
|
||||
@@ -45,6 +47,7 @@ func NewFinanceApplicationService(
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
config *config.Config,
|
||||
exportManager *export.ExportManager,
|
||||
) FinanceApplicationService {
|
||||
return &FinanceApplicationServiceImpl{
|
||||
aliPayClient: aliPayClient,
|
||||
@@ -54,6 +57,7 @@ func NewFinanceApplicationService(
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
userRepo: userRepo,
|
||||
txManager: txManager,
|
||||
exportManager: exportManager,
|
||||
logger: logger,
|
||||
config: config,
|
||||
}
|
||||
@@ -344,6 +348,290 @@ func (s *FinanceApplicationServiceImpl) GetAdminWalletTransactions(ctx context.C
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExportAdminWalletTransactions 导出管理端钱包交易记录
|
||||
func (s *FinanceApplicationServiceImpl) ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error) {
|
||||
const batchSize = 1000 // 每批处理1000条记录
|
||||
var allTransactions []*finance_entities.WalletTransaction
|
||||
var productNameMap map[string]string
|
||||
|
||||
// 分批获取数据
|
||||
page := 1
|
||||
for {
|
||||
// 查询当前批次的数据
|
||||
batchProductNameMap, transactions, _, err := s.walletTransactionRepository.ListWithFiltersAndProductName(ctx, filters, interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: batchSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("查询导出钱包交易记录失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 合并产品名称映射
|
||||
if productNameMap == nil {
|
||||
productNameMap = batchProductNameMap
|
||||
} else {
|
||||
for k, v := range batchProductNameMap {
|
||||
productNameMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到总数据中
|
||||
allTransactions = append(allTransactions, transactions...)
|
||||
|
||||
// 如果当前批次数据少于批次大小,说明已经是最后一批
|
||||
if len(transactions) < batchSize {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
// 检查是否有数据
|
||||
if len(allTransactions) == 0 {
|
||||
return nil, fmt.Errorf("没有找到符合条件的数据")
|
||||
}
|
||||
|
||||
// 批量获取企业名称映射,避免N+1查询问题
|
||||
companyNameMap, err := s.batchGetCompanyNames(ctx, allTransactions)
|
||||
if err != nil {
|
||||
companyNameMap = make(map[string]string)
|
||||
}
|
||||
|
||||
// 准备导出数据
|
||||
headers := []string{"交易ID", "企业名称", "产品名称", "消费金额", "消费时间"}
|
||||
columnWidths := []float64{20, 25, 20, 15, 20}
|
||||
|
||||
data := make([][]interface{}, len(allTransactions))
|
||||
for i, transaction := range allTransactions {
|
||||
companyName := companyNameMap[transaction.UserID]
|
||||
if companyName == "" {
|
||||
companyName = "未知企业"
|
||||
}
|
||||
|
||||
productName := productNameMap[transaction.ProductID]
|
||||
if productName == "" {
|
||||
productName = "未知产品"
|
||||
}
|
||||
|
||||
data[i] = []interface{}{
|
||||
transaction.TransactionID,
|
||||
companyName,
|
||||
productName,
|
||||
transaction.Amount.String(),
|
||||
transaction.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
// 创建导出配置
|
||||
config := &export.ExportConfig{
|
||||
SheetName: "消费记录",
|
||||
Headers: headers,
|
||||
Data: data,
|
||||
ColumnWidths: columnWidths,
|
||||
}
|
||||
|
||||
// 使用导出管理器生成文件
|
||||
return s.exportManager.Export(ctx, config, format)
|
||||
}
|
||||
|
||||
// batchGetCompanyNames 批量获取企业名称映射
|
||||
func (s *FinanceApplicationServiceImpl) batchGetCompanyNames(ctx context.Context, transactions []*finance_entities.WalletTransaction) (map[string]string, error) {
|
||||
// 收集所有唯一的用户ID
|
||||
userIDSet := make(map[string]bool)
|
||||
for _, transaction := range transactions {
|
||||
userIDSet[transaction.UserID] = true
|
||||
}
|
||||
|
||||
// 转换为切片
|
||||
userIDs := make([]string, 0, len(userIDSet))
|
||||
for userID := range userIDSet {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
|
||||
// 批量查询用户信息
|
||||
users, err := s.userRepo.BatchGetByIDsWithEnterpriseInfo(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建企业名称映射
|
||||
companyNameMap := make(map[string]string)
|
||||
for _, user := range users {
|
||||
companyName := "未知企业"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
companyNameMap[user.ID] = companyName
|
||||
}
|
||||
|
||||
return companyNameMap, nil
|
||||
}
|
||||
|
||||
// ExportAdminRechargeRecords 导出管理端充值记录
|
||||
func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error) {
|
||||
const batchSize = 1000 // 每批处理1000条记录
|
||||
var allRecords []finance_entities.RechargeRecord
|
||||
|
||||
// 分批获取数据
|
||||
page := 1
|
||||
for {
|
||||
// 查询当前批次的数据
|
||||
records, err := s.rechargeRecordService.GetAll(ctx, filters, interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: batchSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("查询导出充值记录失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加到总数据中
|
||||
allRecords = append(allRecords, records...)
|
||||
|
||||
// 如果当前批次数据少于批次大小,说明已经是最后一批
|
||||
if len(records) < batchSize {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
// 批量获取企业名称映射,避免N+1查询问题
|
||||
companyNameMap, err := s.batchGetCompanyNamesForRechargeRecords(ctx, convertToRechargeRecordPointers(allRecords))
|
||||
if err != nil {
|
||||
s.logger.Warn("批量获取企业名称失败,使用默认值", zap.Error(err))
|
||||
companyNameMap = make(map[string]string)
|
||||
}
|
||||
|
||||
// 准备导出数据
|
||||
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "转账订单号", "备注", "充值时间"}
|
||||
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20}
|
||||
|
||||
data := make([][]interface{}, len(allRecords))
|
||||
for i, record := range allRecords {
|
||||
// 从映射中获取企业名称
|
||||
companyName := companyNameMap[record.UserID]
|
||||
if companyName == "" {
|
||||
companyName = "未知企业"
|
||||
}
|
||||
|
||||
// 获取订单号
|
||||
alipayOrderID := ""
|
||||
if record.AlipayOrderID != nil && *record.AlipayOrderID != "" {
|
||||
alipayOrderID = *record.AlipayOrderID
|
||||
}
|
||||
transferOrderID := ""
|
||||
if record.TransferOrderID != nil && *record.TransferOrderID != "" {
|
||||
transferOrderID = *record.TransferOrderID
|
||||
}
|
||||
|
||||
// 获取备注
|
||||
notes := ""
|
||||
if record.Notes != "" {
|
||||
notes = record.Notes
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
createdAt := record.CreatedAt.Format("2006-01-02 15:04:05")
|
||||
|
||||
data[i] = []interface{}{
|
||||
companyName,
|
||||
record.Amount.String(),
|
||||
translateRechargeType(record.RechargeType),
|
||||
translateRechargeStatus(record.Status),
|
||||
alipayOrderID,
|
||||
transferOrderID,
|
||||
notes,
|
||||
createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建导出配置
|
||||
config := &export.ExportConfig{
|
||||
SheetName: "充值记录",
|
||||
Headers: headers,
|
||||
Data: data,
|
||||
ColumnWidths: columnWidths,
|
||||
}
|
||||
|
||||
// 使用导出管理器生成文件
|
||||
return s.exportManager.Export(ctx, config, format)
|
||||
}
|
||||
|
||||
// translateRechargeType 翻译充值类型为中文
|
||||
func translateRechargeType(rechargeType finance_entities.RechargeType) string {
|
||||
switch rechargeType {
|
||||
case finance_entities.RechargeTypeAlipay:
|
||||
return "支付宝充值"
|
||||
case finance_entities.RechargeTypeTransfer:
|
||||
return "对公转账"
|
||||
case finance_entities.RechargeTypeGift:
|
||||
return "赠送"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
// translateRechargeStatus 翻译充值状态为中文
|
||||
func translateRechargeStatus(status finance_entities.RechargeStatus) string {
|
||||
switch status {
|
||||
case finance_entities.RechargeStatusPending:
|
||||
return "待处理"
|
||||
case finance_entities.RechargeStatusSuccess:
|
||||
return "成功"
|
||||
case finance_entities.RechargeStatusFailed:
|
||||
return "失败"
|
||||
case finance_entities.RechargeStatusCancelled:
|
||||
return "已取消"
|
||||
default:
|
||||
return "未知状态"
|
||||
}
|
||||
}
|
||||
|
||||
// convertToRechargeRecordPointers 将RechargeRecord切片转换为指针切片
|
||||
func convertToRechargeRecordPointers(records []finance_entities.RechargeRecord) []*finance_entities.RechargeRecord {
|
||||
pointers := make([]*finance_entities.RechargeRecord, len(records))
|
||||
for i := range records {
|
||||
pointers[i] = &records[i]
|
||||
}
|
||||
return pointers
|
||||
}
|
||||
|
||||
// batchGetCompanyNamesForRechargeRecords 批量获取企业名称映射(用于充值记录)
|
||||
func (s *FinanceApplicationServiceImpl) batchGetCompanyNamesForRechargeRecords(ctx context.Context, records []*finance_entities.RechargeRecord) (map[string]string, error) {
|
||||
// 收集所有唯一的用户ID
|
||||
userIDSet := make(map[string]bool)
|
||||
for _, record := range records {
|
||||
userIDSet[record.UserID] = true
|
||||
}
|
||||
|
||||
// 转换为切片
|
||||
userIDs := make([]string, 0, len(userIDSet))
|
||||
for userID := range userIDSet {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
|
||||
// 批量查询用户信息
|
||||
users, err := s.userRepo.BatchGetByIDsWithEnterpriseInfo(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建企业名称映射
|
||||
companyNameMap := make(map[string]string)
|
||||
for _, user := range users {
|
||||
companyName := "未知企业"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
companyNameMap[user.ID] = companyName
|
||||
}
|
||||
|
||||
return companyNameMap, nil
|
||||
}
|
||||
|
||||
// HandleAlipayCallback 处理支付宝回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context, r *http.Request) error {
|
||||
|
||||
19
internal/application/product/category_application_service.go
Normal file
19
internal/application/product/category_application_service.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
"tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
)
|
||||
|
||||
// CategoryApplicationService 分类应用服务接口
|
||||
type CategoryApplicationService interface {
|
||||
// 分类管理
|
||||
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
|
||||
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
|
||||
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
|
||||
|
||||
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
|
||||
}
|
||||
@@ -46,41 +46,6 @@ type ProductApplicationService interface {
|
||||
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
|
||||
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
||||
DeleteProductApiConfig(ctx context.Context, configID string) error
|
||||
}
|
||||
|
||||
// CategoryApplicationService 分类应用服务接口
|
||||
type CategoryApplicationService interface {
|
||||
// 分类管理
|
||||
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
|
||||
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
|
||||
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
|
||||
|
||||
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
|
||||
}
|
||||
|
||||
// SubscriptionApplicationService 订阅应用服务接口
|
||||
type SubscriptionApplicationService interface {
|
||||
// 订阅管理
|
||||
UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error
|
||||
|
||||
// 订阅管理
|
||||
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
|
||||
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
|
||||
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
|
||||
// 我的订阅(用户专用)
|
||||
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
|
||||
|
||||
// 业务查询
|
||||
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
|
||||
|
||||
// 统计
|
||||
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
|
||||
|
||||
// 一键改价
|
||||
BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -584,3 +584,5 @@ func (s *ProductApplicationServiceImpl) UpdateProductApiConfig(ctx context.Conte
|
||||
func (s *ProductApplicationServiceImpl) DeleteProductApiConfig(ctx context.Context, configID string) error {
|
||||
return s.productApiConfigAppService.DeleteProductApiConfig(ctx, configID)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
"tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
)
|
||||
|
||||
// SubscriptionApplicationService 订阅应用服务接口
|
||||
type SubscriptionApplicationService interface {
|
||||
// 订阅管理
|
||||
UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error
|
||||
|
||||
// 订阅管理
|
||||
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
|
||||
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
|
||||
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
|
||||
// 我的订阅(用户专用)
|
||||
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
|
||||
|
||||
// 业务查询
|
||||
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
|
||||
|
||||
// 统计
|
||||
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
|
||||
|
||||
// 一键改价
|
||||
BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error
|
||||
}
|
||||
412
internal/application/statistics/commands_queries.go
Normal file
412
internal/application/statistics/commands_queries.go
Normal file
@@ -0,0 +1,412 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ================ 命令对象 ================
|
||||
|
||||
// CreateMetricCommand 创建指标命令
|
||||
type CreateMetricCommand struct {
|
||||
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" validate:"required" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
Value float64 `json:"value" validate:"min=0" comment:"指标值"`
|
||||
Metadata string `json:"metadata" comment:"额外维度信息"`
|
||||
Date time.Time `json:"date" validate:"required" comment:"统计日期"`
|
||||
}
|
||||
|
||||
// UpdateMetricCommand 更新指标命令
|
||||
type UpdateMetricCommand struct {
|
||||
ID string `json:"id" validate:"required" comment:"指标ID"`
|
||||
Value float64 `json:"value" validate:"min=0" comment:"新指标值"`
|
||||
}
|
||||
|
||||
// DeleteMetricCommand 删除指标命令
|
||||
type DeleteMetricCommand struct {
|
||||
ID string `json:"id" validate:"required" comment:"指标ID"`
|
||||
}
|
||||
|
||||
// GenerateReportCommand 生成报告命令
|
||||
type GenerateReportCommand struct {
|
||||
ReportType string `json:"report_type" validate:"required" comment:"报告类型"`
|
||||
Title string `json:"title" validate:"required" comment:"报告标题"`
|
||||
Period string `json:"period" validate:"required" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||
GeneratedBy string `json:"generated_by" validate:"required" comment:"生成者ID"`
|
||||
}
|
||||
|
||||
// CreateDashboardCommand 创建仪表板命令
|
||||
type CreateDashboardCommand struct {
|
||||
Name string `json:"name" validate:"required" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
CreatedBy string `json:"created_by" validate:"required" comment:"创建者ID"`
|
||||
}
|
||||
|
||||
// UpdateDashboardCommand 更新仪表板命令
|
||||
type UpdateDashboardCommand struct {
|
||||
ID string `json:"id" validate:"required" comment:"仪表板ID"`
|
||||
Name string `json:"name" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
|
||||
}
|
||||
|
||||
// SetDefaultDashboardCommand 设置默认仪表板命令
|
||||
type SetDefaultDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
|
||||
}
|
||||
|
||||
// ActivateDashboardCommand 激活仪表板命令
|
||||
type ActivateDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
ActivatedBy string `json:"activated_by" validate:"required" comment:"激活者ID"`
|
||||
}
|
||||
|
||||
// DeactivateDashboardCommand 停用仪表板命令
|
||||
type DeactivateDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
DeactivatedBy string `json:"deactivated_by" validate:"required" comment:"停用者ID"`
|
||||
}
|
||||
|
||||
// DeleteDashboardCommand 删除仪表板命令
|
||||
type DeleteDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
DeletedBy string `json:"deleted_by" validate:"required" comment:"删除者ID"`
|
||||
}
|
||||
|
||||
// ExportDataCommand 导出数据命令
|
||||
type ExportDataCommand struct {
|
||||
Format string `json:"format" validate:"required" comment:"导出格式"`
|
||||
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||
StartDate time.Time `json:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" validate:"required" comment:"结束日期"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
GroupBy string `json:"group_by" comment:"分组维度"`
|
||||
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||
Columns []string `json:"columns" comment:"导出列"`
|
||||
IncludeCharts bool `json:"include_charts" comment:"是否包含图表"`
|
||||
ExportedBy string `json:"exported_by" validate:"required" comment:"导出者ID"`
|
||||
}
|
||||
|
||||
// TriggerAggregationCommand 触发数据聚合命令
|
||||
type TriggerAggregationCommand struct {
|
||||
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||
Period string `json:"period" validate:"required" comment:"聚合周期"`
|
||||
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||
Force bool `json:"force" comment:"是否强制重新聚合"`
|
||||
TriggeredBy string `json:"triggered_by" validate:"required" comment:"触发者ID"`
|
||||
}
|
||||
|
||||
// Validate 验证触发聚合命令
|
||||
func (c *TriggerAggregationCommand) Validate() error {
|
||||
if c.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if c.Period == "" {
|
||||
return fmt.Errorf("聚合周期不能为空")
|
||||
}
|
||||
if c.TriggeredBy == "" {
|
||||
return fmt.Errorf("触发者ID不能为空")
|
||||
}
|
||||
// 验证周期类型
|
||||
validPeriods := []string{"hourly", "daily", "weekly", "monthly"}
|
||||
isValidPeriod := false
|
||||
for _, period := range validPeriods {
|
||||
if c.Period == period {
|
||||
isValidPeriod = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValidPeriod {
|
||||
return fmt.Errorf("不支持的聚合周期: %s", c.Period)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 查询对象 ================
|
||||
|
||||
// GetMetricsQuery 获取指标查询
|
||||
type GetMetricsQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
}
|
||||
|
||||
// GetRealtimeMetricsQuery 获取实时指标查询
|
||||
type GetRealtimeMetricsQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
TimeRange string `json:"time_range" form:"time_range" comment:"时间范围"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
}
|
||||
|
||||
// GetHistoricalMetricsQuery 获取历史指标查询
|
||||
type GetHistoricalMetricsQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
AggregateBy string `json:"aggregate_by" form:"aggregate_by" comment:"聚合维度"`
|
||||
GroupBy string `json:"group_by" form:"group_by" comment:"分组维度"`
|
||||
}
|
||||
|
||||
// GetDashboardDataQuery 获取仪表板数据查询
|
||||
type GetDashboardDataQuery struct {
|
||||
UserRole string `json:"user_role" form:"user_role" validate:"required" comment:"用户角色"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
MetricTypes []string `json:"metric_types" form:"metric_types" comment:"指标类型列表"`
|
||||
Dimensions []string `json:"dimensions" form:"dimensions" comment:"统计维度列表"`
|
||||
}
|
||||
|
||||
// GetReportsQuery 获取报告查询
|
||||
type GetReportsQuery struct {
|
||||
ReportType string `json:"report_type" form:"report_type" comment:"报告类型"`
|
||||
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||
Status string `json:"status" form:"status" comment:"报告状态"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
GeneratedBy string `json:"generated_by" form:"generated_by" comment:"生成者ID"`
|
||||
}
|
||||
|
||||
// GetDashboardsQuery 获取仪表板查询
|
||||
type GetDashboardsQuery struct {
|
||||
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||
IsDefault *bool `json:"is_default" form:"is_default" comment:"是否默认"`
|
||||
IsActive *bool `json:"is_active" form:"is_active" comment:"是否激活"`
|
||||
AccessLevel string `json:"access_level" form:"access_level" comment:"访问级别"`
|
||||
CreatedBy string `json:"created_by" form:"created_by" comment:"创建者ID"`
|
||||
Name string `json:"name" form:"name" comment:"仪表板名称"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
}
|
||||
|
||||
// GetReportQuery 获取单个报告查询
|
||||
type GetReportQuery struct {
|
||||
ReportID string `json:"report_id" form:"report_id" validate:"required" comment:"报告ID"`
|
||||
}
|
||||
|
||||
// GetDashboardQuery 获取单个仪表板查询
|
||||
type GetDashboardQuery struct {
|
||||
DashboardID string `json:"dashboard_id" form:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
}
|
||||
|
||||
// GetMetricQuery 获取单个指标查询
|
||||
type GetMetricQuery struct {
|
||||
MetricID string `json:"metric_id" form:"metric_id" validate:"required" comment:"指标ID"`
|
||||
}
|
||||
|
||||
// CalculateGrowthRateQuery 计算增长率查询
|
||||
type CalculateGrowthRateQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
CurrentPeriod time.Time `json:"current_period" form:"current_period" validate:"required" comment:"当前周期"`
|
||||
PreviousPeriod time.Time `json:"previous_period" form:"previous_period" validate:"required" comment:"上一周期"`
|
||||
}
|
||||
|
||||
// CalculateTrendQuery 计算趋势查询
|
||||
type CalculateTrendQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
}
|
||||
|
||||
// CalculateCorrelationQuery 计算相关性查询
|
||||
type CalculateCorrelationQuery struct {
|
||||
MetricType1 string `json:"metric_type1" form:"metric_type1" validate:"required" comment:"指标类型1"`
|
||||
MetricName1 string `json:"metric_name1" form:"metric_name1" validate:"required" comment:"指标名称1"`
|
||||
MetricType2 string `json:"metric_type2" form:"metric_type2" validate:"required" comment:"指标类型2"`
|
||||
MetricName2 string `json:"metric_name2" form:"metric_name2" validate:"required" comment:"指标名称2"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
}
|
||||
|
||||
// CalculateMovingAverageQuery 计算移动平均查询
|
||||
type CalculateMovingAverageQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
WindowSize int `json:"window_size" form:"window_size" validate:"min=1" comment:"窗口大小"`
|
||||
}
|
||||
|
||||
// CalculateSeasonalityQuery 计算季节性查询
|
||||
type CalculateSeasonalityQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
}
|
||||
|
||||
// ================ 响应对象 ================
|
||||
|
||||
// CommandResponse 命令响应
|
||||
type CommandResponse struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data interface{} `json:"data" comment:"响应数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// QueryResponse 查询响应
|
||||
type QueryResponse struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data interface{} `json:"data" comment:"响应数据"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// ListResponse 列表响应
|
||||
type ListResponse struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data ListDataDTO `json:"data" comment:"数据列表"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// ListDataDTO 列表数据DTO
|
||||
type ListDataDTO struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []interface{} `json:"items" comment:"数据列表"`
|
||||
}
|
||||
|
||||
// ================ 验证方法 ================
|
||||
|
||||
// Validate 验证创建指标命令
|
||||
func (c *CreateMetricCommand) Validate() error {
|
||||
if c.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if c.MetricName == "" {
|
||||
return fmt.Errorf("指标名称不能为空")
|
||||
}
|
||||
if c.Value < 0 {
|
||||
return fmt.Errorf("指标值不能为负数")
|
||||
}
|
||||
if c.Date.IsZero() {
|
||||
return fmt.Errorf("统计日期不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证更新指标命令
|
||||
func (c *UpdateMetricCommand) Validate() error {
|
||||
if c.ID == "" {
|
||||
return fmt.Errorf("指标ID不能为空")
|
||||
}
|
||||
if c.Value < 0 {
|
||||
return fmt.Errorf("指标值不能为负数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证生成报告命令
|
||||
func (c *GenerateReportCommand) Validate() error {
|
||||
if c.ReportType == "" {
|
||||
return fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
if c.Title == "" {
|
||||
return fmt.Errorf("报告标题不能为空")
|
||||
}
|
||||
if c.Period == "" {
|
||||
return fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
if c.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if c.GeneratedBy == "" {
|
||||
return fmt.Errorf("生成者ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证创建仪表板命令
|
||||
func (c *CreateDashboardCommand) Validate() error {
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("仪表板名称不能为空")
|
||||
}
|
||||
if c.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if c.CreatedBy == "" {
|
||||
return fmt.Errorf("创建者ID不能为空")
|
||||
}
|
||||
if c.RefreshInterval < 30 {
|
||||
return fmt.Errorf("刷新间隔不能少于30秒")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证更新仪表板命令
|
||||
func (c *UpdateDashboardCommand) Validate() error {
|
||||
if c.ID == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
if c.UpdatedBy == "" {
|
||||
return fmt.Errorf("更新者ID不能为空")
|
||||
}
|
||||
if c.RefreshInterval < 30 {
|
||||
return fmt.Errorf("刷新间隔不能少于30秒")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证导出数据命令
|
||||
func (c *ExportDataCommand) Validate() error {
|
||||
if c.Format == "" {
|
||||
return fmt.Errorf("导出格式不能为空")
|
||||
}
|
||||
if c.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if c.StartDate.IsZero() || c.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if c.StartDate.After(c.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
if c.ExportedBy == "" {
|
||||
return fmt.Errorf("导出者ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
258
internal/application/statistics/dtos.go
Normal file
258
internal/application/statistics/dtos.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatisticsMetricDTO 统计指标DTO
|
||||
type StatisticsMetricDTO struct {
|
||||
ID string `json:"id" comment:"统计指标唯一标识"`
|
||||
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
Value float64 `json:"value" comment:"指标值"`
|
||||
Metadata string `json:"metadata" comment:"额外维度信息"`
|
||||
Date time.Time `json:"date" comment:"统计日期"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// StatisticsReportDTO 统计报告DTO
|
||||
type StatisticsReportDTO struct {
|
||||
ID string `json:"id" comment:"报告唯一标识"`
|
||||
ReportType string `json:"report_type" comment:"报告类型"`
|
||||
Title string `json:"title" comment:"报告标题"`
|
||||
Content string `json:"content" comment:"报告内容"`
|
||||
Period string `json:"period" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
Status string `json:"status" comment:"报告状态"`
|
||||
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
|
||||
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
|
||||
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardDTO 统计仪表板DTO
|
||||
type StatisticsDashboardDTO struct {
|
||||
ID string `json:"id" comment:"仪表板唯一标识"`
|
||||
Name string `json:"name" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
IsDefault bool `json:"is_default" comment:"是否为默认仪表板"`
|
||||
IsActive bool `json:"is_active" comment:"是否激活"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||
CreatedBy string `json:"created_by" comment:"创建者ID"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// DashboardDataDTO 仪表板数据DTO
|
||||
type DashboardDataDTO struct {
|
||||
// API调用统计
|
||||
APICalls struct {
|
||||
TotalCount int64 `json:"total_count" comment:"总调用次数"`
|
||||
SuccessCount int64 `json:"success_count" comment:"成功调用次数"`
|
||||
FailedCount int64 `json:"failed_count" comment:"失败调用次数"`
|
||||
SuccessRate float64 `json:"success_rate" comment:"成功率"`
|
||||
AvgResponseTime float64 `json:"avg_response_time" comment:"平均响应时间"`
|
||||
} `json:"api_calls"`
|
||||
|
||||
// 用户统计
|
||||
Users struct {
|
||||
TotalCount int64 `json:"total_count" comment:"总用户数"`
|
||||
CertifiedCount int64 `json:"certified_count" comment:"认证用户数"`
|
||||
ActiveCount int64 `json:"active_count" comment:"活跃用户数"`
|
||||
CertificationRate float64 `json:"certification_rate" comment:"认证完成率"`
|
||||
RetentionRate float64 `json:"retention_rate" comment:"留存率"`
|
||||
} `json:"users"`
|
||||
|
||||
// 财务统计
|
||||
Finance struct {
|
||||
TotalAmount float64 `json:"total_amount" comment:"总金额"`
|
||||
RechargeAmount float64 `json:"recharge_amount" comment:"充值金额"`
|
||||
DeductAmount float64 `json:"deduct_amount" comment:"扣款金额"`
|
||||
NetAmount float64 `json:"net_amount" comment:"净金额"`
|
||||
} `json:"finance"`
|
||||
|
||||
// 产品统计
|
||||
Products struct {
|
||||
TotalProducts int64 `json:"total_products" comment:"总产品数"`
|
||||
ActiveProducts int64 `json:"active_products" comment:"活跃产品数"`
|
||||
TotalSubscriptions int64 `json:"total_subscriptions" comment:"总订阅数"`
|
||||
ActiveSubscriptions int64 `json:"active_subscriptions" comment:"活跃订阅数"`
|
||||
} `json:"products"`
|
||||
|
||||
// 认证统计
|
||||
Certification struct {
|
||||
TotalCertifications int64 `json:"total_certifications" comment:"总认证数"`
|
||||
CompletedCertifications int64 `json:"completed_certifications" comment:"完成认证数"`
|
||||
PendingCertifications int64 `json:"pending_certifications" comment:"待处理认证数"`
|
||||
FailedCertifications int64 `json:"failed_certifications" comment:"失败认证数"`
|
||||
CompletionRate float64 `json:"completion_rate" comment:"完成率"`
|
||||
} `json:"certification"`
|
||||
|
||||
// 时间信息
|
||||
Period struct {
|
||||
StartDate string `json:"start_date" comment:"开始日期"`
|
||||
EndDate string `json:"end_date" comment:"结束日期"`
|
||||
Period string `json:"period" comment:"统计周期"`
|
||||
} `json:"period"`
|
||||
|
||||
// 元数据
|
||||
Metadata struct {
|
||||
GeneratedAt string `json:"generated_at" comment:"生成时间"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
DataVersion string `json:"data_version" comment:"数据版本"`
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
// RealtimeMetricsDTO 实时指标DTO
|
||||
type RealtimeMetricsDTO struct {
|
||||
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||
Metrics map[string]float64 `json:"metrics" comment:"指标数据"`
|
||||
Timestamp time.Time `json:"timestamp" comment:"时间戳"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
}
|
||||
|
||||
// HistoricalMetricsDTO 历史指标DTO
|
||||
type HistoricalMetricsDTO struct {
|
||||
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
DataPoints []DataPointDTO `json:"data_points" comment:"数据点"`
|
||||
Summary MetricsSummaryDTO `json:"summary" comment:"汇总信息"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
}
|
||||
|
||||
// DataPointDTO 数据点DTO
|
||||
type DataPointDTO struct {
|
||||
Date time.Time `json:"date" comment:"日期"`
|
||||
Value float64 `json:"value" comment:"值"`
|
||||
Label string `json:"label" comment:"标签"`
|
||||
}
|
||||
|
||||
// MetricsSummaryDTO 指标汇总DTO
|
||||
type MetricsSummaryDTO struct {
|
||||
Total float64 `json:"total" comment:"总值"`
|
||||
Average float64 `json:"average" comment:"平均值"`
|
||||
Max float64 `json:"max" comment:"最大值"`
|
||||
Min float64 `json:"min" comment:"最小值"`
|
||||
Count int64 `json:"count" comment:"数据点数量"`
|
||||
GrowthRate float64 `json:"growth_rate" comment:"增长率"`
|
||||
Trend string `json:"trend" comment:"趋势"`
|
||||
}
|
||||
|
||||
// ReportContentDTO 报告内容DTO
|
||||
type ReportContentDTO struct {
|
||||
ReportType string `json:"report_type" comment:"报告类型"`
|
||||
Title string `json:"title" comment:"报告标题"`
|
||||
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
|
||||
Details map[string]interface{} `json:"details" comment:"详细信息"`
|
||||
Charts []ChartDTO `json:"charts" comment:"图表数据"`
|
||||
Tables []TableDTO `json:"tables" comment:"表格数据"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
}
|
||||
|
||||
// ChartDTO 图表DTO
|
||||
type ChartDTO struct {
|
||||
Type string `json:"type" comment:"图表类型"`
|
||||
Title string `json:"title" comment:"图表标题"`
|
||||
Data map[string]interface{} `json:"data" comment:"图表数据"`
|
||||
Options map[string]interface{} `json:"options" comment:"图表选项"`
|
||||
Description string `json:"description" comment:"图表描述"`
|
||||
}
|
||||
|
||||
// TableDTO 表格DTO
|
||||
type TableDTO struct {
|
||||
Title string `json:"title" comment:"表格标题"`
|
||||
Headers []string `json:"headers" comment:"表头"`
|
||||
Rows [][]interface{} `json:"rows" comment:"表格行数据"`
|
||||
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
|
||||
Description string `json:"description" comment:"表格描述"`
|
||||
}
|
||||
|
||||
// ExportDataDTO 导出数据DTO
|
||||
type ExportDataDTO struct {
|
||||
Format string `json:"format" comment:"导出格式"`
|
||||
FileName string `json:"file_name" comment:"文件名"`
|
||||
Data []map[string]interface{} `json:"data" comment:"导出数据"`
|
||||
Headers []string `json:"headers" comment:"表头"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
DownloadURL string `json:"download_url" comment:"下载链接"`
|
||||
}
|
||||
|
||||
// StatisticsQueryDTO 统计查询DTO
|
||||
type StatisticsQueryDTO struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
}
|
||||
|
||||
// ReportGenerationDTO 报告生成DTO
|
||||
type ReportGenerationDTO struct {
|
||||
ReportType string `json:"report_type" comment:"报告类型"`
|
||||
Title string `json:"title" comment:"报告标题"`
|
||||
Period string `json:"period" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||
Format string `json:"format" comment:"输出格式"`
|
||||
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
|
||||
}
|
||||
|
||||
// DashboardConfigDTO 仪表板配置DTO
|
||||
type DashboardConfigDTO struct {
|
||||
Name string `json:"name" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
CreatedBy string `json:"created_by" comment:"创建者ID"`
|
||||
}
|
||||
|
||||
// StatisticsResponseDTO 统计响应DTO
|
||||
type StatisticsResponseDTO struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data interface{} `json:"data" comment:"响应数据"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// PaginationDTO 分页DTO
|
||||
type PaginationDTO struct {
|
||||
Page int `json:"page" comment:"当前页"`
|
||||
PageSize int `json:"page_size" comment:"每页大小"`
|
||||
Total int64 `json:"total" comment:"总数量"`
|
||||
Pages int `json:"pages" comment:"总页数"`
|
||||
HasNext bool `json:"has_next" comment:"是否有下一页"`
|
||||
HasPrev bool `json:"has_prev" comment:"是否有上一页"`
|
||||
}
|
||||
|
||||
// StatisticsListResponseDTO 统计列表响应DTO
|
||||
type StatisticsListResponseDTO struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data []interface{} `json:"data" comment:"数据列表"`
|
||||
Pagination PaginationDTO `json:"pagination" comment:"分页信息"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatisticsApplicationService 统计应用服务接口
|
||||
// 负责统计功能的业务逻辑编排和协调
|
||||
type StatisticsApplicationService interface {
|
||||
// ================ 指标管理 ================
|
||||
|
||||
// CreateMetric 创建统计指标
|
||||
CreateMetric(ctx context.Context, cmd *CreateMetricCommand) (*CommandResponse, error)
|
||||
|
||||
// UpdateMetric 更新统计指标
|
||||
UpdateMetric(ctx context.Context, cmd *UpdateMetricCommand) (*CommandResponse, error)
|
||||
|
||||
// DeleteMetric 删除统计指标
|
||||
DeleteMetric(ctx context.Context, cmd *DeleteMetricCommand) (*CommandResponse, error)
|
||||
|
||||
// GetMetric 获取单个指标
|
||||
GetMetric(ctx context.Context, query *GetMetricQuery) (*QueryResponse, error)
|
||||
|
||||
// GetMetrics 获取指标列表
|
||||
GetMetrics(ctx context.Context, query *GetMetricsQuery) (*ListResponse, error)
|
||||
|
||||
// ================ 实时统计 ================
|
||||
|
||||
// GetRealtimeMetrics 获取实时指标
|
||||
GetRealtimeMetrics(ctx context.Context, query *GetRealtimeMetricsQuery) (*QueryResponse, error)
|
||||
|
||||
// UpdateRealtimeMetric 更新实时指标
|
||||
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
|
||||
|
||||
// ================ 历史统计 ================
|
||||
|
||||
// GetHistoricalMetrics 获取历史指标
|
||||
GetHistoricalMetrics(ctx context.Context, query *GetHistoricalMetricsQuery) (*QueryResponse, error)
|
||||
|
||||
// AggregateMetrics 聚合指标
|
||||
AggregateMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) error
|
||||
|
||||
// ================ 仪表板管理 ================
|
||||
|
||||
// CreateDashboard 创建仪表板
|
||||
CreateDashboard(ctx context.Context, cmd *CreateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// UpdateDashboard 更新仪表板
|
||||
UpdateDashboard(ctx context.Context, cmd *UpdateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// DeleteDashboard 删除仪表板
|
||||
DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// GetDashboard 获取单个仪表板
|
||||
GetDashboard(ctx context.Context, query *GetDashboardQuery) (*QueryResponse, error)
|
||||
|
||||
// GetDashboards 获取仪表板列表
|
||||
GetDashboards(ctx context.Context, query *GetDashboardsQuery) (*ListResponse, error)
|
||||
|
||||
// SetDefaultDashboard 设置默认仪表板
|
||||
SetDefaultDashboard(ctx context.Context, cmd *SetDefaultDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// ActivateDashboard 激活仪表板
|
||||
ActivateDashboard(ctx context.Context, cmd *ActivateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// DeactivateDashboard 停用仪表板
|
||||
DeactivateDashboard(ctx context.Context, cmd *DeactivateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// GetDashboardData 获取仪表板数据
|
||||
GetDashboardData(ctx context.Context, query *GetDashboardDataQuery) (*QueryResponse, error)
|
||||
|
||||
// ================ 报告管理 ================
|
||||
|
||||
// GenerateReport 生成报告
|
||||
GenerateReport(ctx context.Context, cmd *GenerateReportCommand) (*CommandResponse, error)
|
||||
|
||||
// GetReport 获取单个报告
|
||||
GetReport(ctx context.Context, query *GetReportQuery) (*QueryResponse, error)
|
||||
|
||||
// GetReports 获取报告列表
|
||||
GetReports(ctx context.Context, query *GetReportsQuery) (*ListResponse, error)
|
||||
|
||||
// DeleteReport 删除报告
|
||||
DeleteReport(ctx context.Context, reportID string) (*CommandResponse, error)
|
||||
|
||||
// ================ 统计分析 ================
|
||||
|
||||
// CalculateGrowthRate 计算增长率
|
||||
CalculateGrowthRate(ctx context.Context, query *CalculateGrowthRateQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateTrend 计算趋势
|
||||
CalculateTrend(ctx context.Context, query *CalculateTrendQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateCorrelation 计算相关性
|
||||
CalculateCorrelation(ctx context.Context, query *CalculateCorrelationQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateMovingAverage 计算移动平均
|
||||
CalculateMovingAverage(ctx context.Context, query *CalculateMovingAverageQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateSeasonality 计算季节性
|
||||
CalculateSeasonality(ctx context.Context, query *CalculateSeasonalityQuery) (*QueryResponse, error)
|
||||
|
||||
// ================ 数据导出 ================
|
||||
|
||||
// ExportData 导出数据
|
||||
ExportData(ctx context.Context, cmd *ExportDataCommand) (*CommandResponse, error)
|
||||
|
||||
// ================ 定时任务 ================
|
||||
|
||||
// ProcessHourlyAggregation 处理小时级聚合
|
||||
ProcessHourlyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// ProcessDailyAggregation 处理日级聚合
|
||||
ProcessDailyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// ProcessWeeklyAggregation 处理周级聚合
|
||||
ProcessWeeklyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// ProcessMonthlyAggregation 处理月级聚合
|
||||
ProcessMonthlyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// CleanupExpiredData 清理过期数据
|
||||
CleanupExpiredData(ctx context.Context) error
|
||||
|
||||
// ================ 管理员专用方法 ================
|
||||
|
||||
// AdminGetSystemStatistics 管理员获取系统统计
|
||||
AdminGetSystemStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminTriggerAggregation 管理员触发数据聚合
|
||||
AdminTriggerAggregation(ctx context.Context, cmd *TriggerAggregationCommand) (*CommandResponse, error)
|
||||
|
||||
// AdminGetUserStatistics 管理员获取单个用户统计
|
||||
AdminGetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
|
||||
|
||||
// ================ 管理员独立域统计接口 ================
|
||||
|
||||
// AdminGetUserDomainStatistics 管理员获取用户域统计
|
||||
AdminGetUserDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminGetApiDomainStatistics 管理员获取API域统计
|
||||
AdminGetApiDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminGetConsumptionDomainStatistics 管理员获取消费域统计
|
||||
AdminGetConsumptionDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminGetRechargeDomainStatistics 管理员获取充值域统计
|
||||
AdminGetRechargeDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// ================ 公开和用户统计方法 ================
|
||||
|
||||
// GetPublicStatistics 获取公开统计信息
|
||||
GetPublicStatistics(ctx context.Context) (*QueryResponse, error)
|
||||
|
||||
// GetUserStatistics 获取用户统计信息
|
||||
GetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
|
||||
|
||||
// ================ 独立统计接口 ================
|
||||
|
||||
// GetApiCallsStatistics 获取API调用统计
|
||||
GetApiCallsStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||
|
||||
// GetConsumptionStatistics 获取消费统计
|
||||
GetConsumptionStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||
|
||||
// GetRechargeStatistics 获取充值统计
|
||||
GetRechargeStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||
|
||||
// GetLatestProducts 获取最新产品推荐
|
||||
GetLatestProducts(ctx context.Context, limit int) (*QueryResponse, error)
|
||||
|
||||
// ================ 管理员排行榜接口 ================
|
||||
|
||||
// AdminGetUserCallRanking 获取用户调用排行榜
|
||||
AdminGetUserCallRanking(ctx context.Context, rankingType, period string, limit int) (*QueryResponse, error)
|
||||
|
||||
// AdminGetRechargeRanking 获取充值排行榜
|
||||
AdminGetRechargeRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
|
||||
|
||||
// AdminGetApiPopularityRanking 获取API受欢迎程度排行榜
|
||||
AdminGetApiPopularityRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
|
||||
|
||||
// AdminGetTodayCertifiedEnterprises 获取今日认证企业列表
|
||||
AdminGetTodayCertifiedEnterprises(ctx context.Context, limit int) (*QueryResponse, error)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -139,6 +139,10 @@ type DailyRateLimitConfig struct {
|
||||
BlockedCountries []string `mapstructure:"blocked_countries"` // 被阻止的国家/地区
|
||||
EnableProxyCheck bool `mapstructure:"enable_proxy_check"` // 是否检查代理
|
||||
MaxConcurrent int `mapstructure:"max_concurrent"` // 最大并发请求数
|
||||
// 路径排除配置
|
||||
ExcludePaths []string `mapstructure:"exclude_paths"` // 排除频率限制的路径
|
||||
// 域名排除配置
|
||||
ExcludeDomains []string `mapstructure:"exclude_domains"` // 排除频率限制的域名
|
||||
}
|
||||
|
||||
// MonitoringConfig 监控配置
|
||||
@@ -314,6 +318,14 @@ type WalletConfig struct {
|
||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
||||
BalanceAlert BalanceAlertConfig `mapstructure:"balance_alert"`
|
||||
}
|
||||
|
||||
// BalanceAlertConfig 余额预警配置
|
||||
type BalanceAlertConfig struct {
|
||||
DefaultEnabled bool `mapstructure:"default_enabled"` // 默认启用余额预警
|
||||
DefaultThreshold float64 `mapstructure:"default_threshold"` // 默认预警阈值
|
||||
AlertCooldownHours int `mapstructure:"alert_cooldown_hours"` // 预警冷却时间(小时)
|
||||
}
|
||||
|
||||
// AliPayRechargeBonusRule 支付宝充值赠送规则
|
||||
|
||||
@@ -14,8 +14,10 @@ import (
|
||||
"tyapi-server/internal/application/certification"
|
||||
"tyapi-server/internal/application/finance"
|
||||
"tyapi-server/internal/application/product"
|
||||
"tyapi-server/internal/application/statistics"
|
||||
"tyapi-server/internal/application/user"
|
||||
"tyapi-server/internal/config"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
domain_article_repo "tyapi-server/internal/domains/article/repositories"
|
||||
article_service "tyapi-server/internal/domains/article/services"
|
||||
domain_certification_repo "tyapi-server/internal/domains/certification/repositories"
|
||||
@@ -24,6 +26,7 @@ import (
|
||||
finance_service "tyapi-server/internal/domains/finance/services"
|
||||
domain_product_repo "tyapi-server/internal/domains/product/repositories"
|
||||
product_service "tyapi-server/internal/domains/product/services"
|
||||
statistics_service "tyapi-server/internal/domains/statistics/services"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/infrastructure/cache"
|
||||
"tyapi-server/internal/infrastructure/database"
|
||||
@@ -44,9 +47,14 @@ import (
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
"tyapi-server/internal/infrastructure/http/routes"
|
||||
"tyapi-server/internal/infrastructure/task"
|
||||
task_implementations "tyapi-server/internal/infrastructure/task/implementations"
|
||||
asynq "tyapi-server/internal/infrastructure/task/implementations/asynq"
|
||||
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
|
||||
task_repositories "tyapi-server/internal/infrastructure/task/repositories"
|
||||
shared_database "tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/esign"
|
||||
"tyapi-server/internal/shared/events"
|
||||
shared_events "tyapi-server/internal/shared/events"
|
||||
"tyapi-server/internal/shared/export"
|
||||
"tyapi-server/internal/shared/health"
|
||||
"tyapi-server/internal/shared/hooks"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
@@ -64,12 +72,18 @@ import (
|
||||
domain_user_repo "tyapi-server/internal/domains/user/repositories"
|
||||
user_repo "tyapi-server/internal/infrastructure/database/repositories/user"
|
||||
|
||||
hibiken_asynq "github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
api_app "tyapi-server/internal/application/api"
|
||||
domain_api_repo "tyapi-server/internal/domains/api/repositories"
|
||||
api_service "tyapi-server/internal/domains/api/services"
|
||||
api_services "tyapi-server/internal/domains/api/services"
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_services "tyapi-server/internal/domains/product/services"
|
||||
domain_statistics_repo "tyapi-server/internal/domains/statistics/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
api_repo "tyapi-server/internal/infrastructure/database/repositories/api"
|
||||
statistics_repo "tyapi-server/internal/infrastructure/database/repositories/statistics"
|
||||
)
|
||||
|
||||
// Container 应用容器
|
||||
@@ -197,7 +211,7 @@ func NewContainer() *Container {
|
||||
return 5 // 默认5个工作协程
|
||||
},
|
||||
fx.Annotate(
|
||||
events.NewMemoryEventBus,
|
||||
shared_events.NewMemoryEventBus,
|
||||
fx.As(new(interfaces.EventBus)),
|
||||
),
|
||||
// 健康检查
|
||||
@@ -288,6 +302,10 @@ func NewContainer() *Container {
|
||||
}
|
||||
return payment.NewAliPayService(config)
|
||||
},
|
||||
// 导出管理器
|
||||
func(logger *zap.Logger) *export.ExportManager {
|
||||
return export.NewExportManager(logger)
|
||||
},
|
||||
),
|
||||
|
||||
// 高级特性模块
|
||||
@@ -377,6 +395,10 @@ func NewContainer() *Container {
|
||||
EnableGeoBlock: cfg.DailyRateLimit.EnableGeoBlock,
|
||||
BlockedCountries: cfg.DailyRateLimit.BlockedCountries,
|
||||
EnableProxyCheck: cfg.DailyRateLimit.EnableProxyCheck,
|
||||
// 排除路径配置
|
||||
ExcludePaths: cfg.DailyRateLimit.ExcludePaths,
|
||||
// 排除域名配置
|
||||
ExcludeDomains: cfg.DailyRateLimit.ExcludeDomains,
|
||||
}
|
||||
return middleware.NewDailyRateLimitMiddleware(cfg, redis, response, logger, limitConfig)
|
||||
},
|
||||
@@ -553,6 +575,22 @@ func NewContainer() *Container {
|
||||
),
|
||||
),
|
||||
|
||||
// 统计域仓储层
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
statistics_repo.NewGormStatisticsRepository,
|
||||
fx.As(new(domain_statistics_repo.StatisticsRepository)),
|
||||
),
|
||||
fx.Annotate(
|
||||
statistics_repo.NewGormStatisticsReportRepository,
|
||||
fx.As(new(domain_statistics_repo.StatisticsReportRepository)),
|
||||
),
|
||||
fx.Annotate(
|
||||
statistics_repo.NewGormStatisticsDashboardRepository,
|
||||
fx.As(new(domain_statistics_repo.StatisticsDashboardRepository)),
|
||||
),
|
||||
),
|
||||
|
||||
// 领域服务
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
@@ -565,6 +603,25 @@ func NewContainer() *Container {
|
||||
product_service.NewProductSubscriptionService,
|
||||
product_service.NewProductApiConfigService,
|
||||
product_service.NewProductDocumentationService,
|
||||
fx.Annotate(
|
||||
func(
|
||||
apiUserRepo api_repositories.ApiUserRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
|
||||
smsService *sms.AliSMSService,
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
) finance_service.BalanceAlertService {
|
||||
return finance_service.NewBalanceAlertService(
|
||||
apiUserRepo,
|
||||
userRepo,
|
||||
enterpriseInfoRepo,
|
||||
smsService,
|
||||
config,
|
||||
logger,
|
||||
)
|
||||
},
|
||||
),
|
||||
finance_service.NewWalletAggregateService,
|
||||
finance_service.NewRechargeRecordService,
|
||||
// 发票领域服务
|
||||
@@ -608,28 +665,122 @@ func NewContainer() *Container {
|
||||
certification_service.NewEnterpriseInfoSubmitRecordService,
|
||||
// 文章领域服务
|
||||
article_service.NewArticleService,
|
||||
// 统计领域服务
|
||||
statistics_service.NewStatisticsAggregateService,
|
||||
statistics_service.NewStatisticsCalculationService,
|
||||
statistics_service.NewStatisticsReportService,
|
||||
),
|
||||
|
||||
// API域服务层
|
||||
fx.Provide(
|
||||
api_service.NewApiUserAggregateService,
|
||||
api_service.NewApiCallAggregateService,
|
||||
api_service.NewApiRequestService,
|
||||
api_service.NewFormConfigService,
|
||||
fx.Annotate(
|
||||
api_services.NewApiUserAggregateService,
|
||||
),
|
||||
api_services.NewApiCallAggregateService,
|
||||
api_services.NewApiRequestService,
|
||||
api_services.NewFormConfigService,
|
||||
),
|
||||
|
||||
// API域应用服务
|
||||
fx.Provide(
|
||||
api_app.NewApiApplicationService,
|
||||
// API应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
func(
|
||||
apiCallService api_services.ApiCallAggregateService,
|
||||
apiUserService api_services.ApiUserAggregateService,
|
||||
apiRequestService *api_services.ApiRequestService,
|
||||
formConfigService api_services.FormConfigService,
|
||||
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||
productManagementService *product_services.ProductManagementService,
|
||||
userRepo user_repositories.UserRepository,
|
||||
txManager *shared_database.TransactionManager,
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
contractInfoService user_repositories.ContractInfoRepository,
|
||||
taskManager task_interfaces.TaskManager,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
subscriptionService *product_services.ProductSubscriptionService,
|
||||
exportManager *export.ExportManager,
|
||||
balanceAlertService finance_services.BalanceAlertService,
|
||||
) api_app.ApiApplicationService {
|
||||
return api_app.NewApiApplicationService(
|
||||
apiCallService,
|
||||
apiUserService,
|
||||
apiRequestService,
|
||||
formConfigService,
|
||||
apiCallRepository,
|
||||
productManagementService,
|
||||
userRepo,
|
||||
txManager,
|
||||
config,
|
||||
logger,
|
||||
contractInfoService,
|
||||
taskManager,
|
||||
walletService,
|
||||
subscriptionService,
|
||||
exportManager,
|
||||
balanceAlertService,
|
||||
)
|
||||
},
|
||||
fx.As(new(api_app.ApiApplicationService)),
|
||||
),
|
||||
),
|
||||
|
||||
// 任务系统
|
||||
fx.Provide(
|
||||
// Asynq 客户端
|
||||
func(cfg *config.Config, scheduledTaskRepo domain_article_repo.ScheduledTaskRepository, logger *zap.Logger) *task.AsynqClient {
|
||||
// Asynq 客户端 (github.com/hibiken/asynq)
|
||||
func(cfg *config.Config) *hibiken_asynq.Client {
|
||||
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||
return hibiken_asynq.NewClient(hibiken_asynq.RedisClientOpt{Addr: redisAddr})
|
||||
},
|
||||
// 自定义Asynq客户端 (用于文章任务)
|
||||
func(cfg *config.Config, scheduledTaskRepo domain_article_repo.ScheduledTaskRepository, logger *zap.Logger) *asynq.AsynqClient {
|
||||
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||
return task.NewAsynqClient(redisAddr, scheduledTaskRepo, logger)
|
||||
},
|
||||
// 文章任务队列
|
||||
func(cfg *config.Config, logger *zap.Logger) task_interfaces.ArticleTaskQueue {
|
||||
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||
return task.NewArticleTaskQueue(redisAddr, logger)
|
||||
},
|
||||
// AsyncTask 仓库
|
||||
task_repositories.NewAsyncTaskRepository,
|
||||
// TaskManager - 统一任务管理器
|
||||
func(
|
||||
asynqClient *hibiken_asynq.Client,
|
||||
asyncTaskRepo task_repositories.AsyncTaskRepository,
|
||||
logger *zap.Logger,
|
||||
config *config.Config,
|
||||
) task_interfaces.TaskManager {
|
||||
taskConfig := &task_interfaces.TaskManagerConfig{
|
||||
RedisAddr: fmt.Sprintf("%s:%s", config.Redis.Host, config.Redis.Port),
|
||||
MaxRetries: 5,
|
||||
RetryInterval: 5 * time.Minute,
|
||||
CleanupDays: 30,
|
||||
}
|
||||
return task_implementations.NewTaskManager(asynqClient, asyncTaskRepo, logger, taskConfig)
|
||||
},
|
||||
// AsynqWorker - 任务处理器
|
||||
func(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
articleApplicationService article.ArticleApplicationService,
|
||||
apiApplicationService api_app.ApiApplicationService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
subscriptionService *product_services.ProductSubscriptionService,
|
||||
asyncTaskRepo task_repositories.AsyncTaskRepository,
|
||||
) *asynq.AsynqWorker {
|
||||
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||
return asynq.NewAsynqWorker(
|
||||
redisAddr,
|
||||
logger,
|
||||
articleApplicationService,
|
||||
apiApplicationService,
|
||||
walletService,
|
||||
subscriptionService,
|
||||
asyncTaskRepo,
|
||||
)
|
||||
},
|
||||
),
|
||||
|
||||
// 应用服务
|
||||
@@ -641,12 +792,70 @@ func NewContainer() *Container {
|
||||
),
|
||||
// 认证应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
certification.NewCertificationApplicationService,
|
||||
func(
|
||||
aggregateService certification_service.CertificationAggregateService,
|
||||
userAggregateService user_service.UserAggregateService,
|
||||
queryRepository domain_certification_repo.CertificationQueryRepository,
|
||||
enterpriseInfoSubmitRecordRepo domain_certification_repo.EnterpriseInfoSubmitRecordRepository,
|
||||
smsCodeService *user_service.SMSCodeService,
|
||||
esignClient *esign.Client,
|
||||
esignConfig *esign.Config,
|
||||
qiniuStorageService *storage.QiNiuStorageService,
|
||||
contractAggregateService user_service.ContractAggregateService,
|
||||
walletAggregateService finance_services.WalletAggregateService,
|
||||
apiUserAggregateService api_services.ApiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService *certification_service.EnterpriseInfoSubmitRecordService,
|
||||
ocrService sharedOCR.OCRService,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
) certification.CertificationApplicationService {
|
||||
return certification.NewCertificationApplicationService(
|
||||
aggregateService,
|
||||
userAggregateService,
|
||||
queryRepository,
|
||||
enterpriseInfoSubmitRecordRepo,
|
||||
smsCodeService,
|
||||
esignClient,
|
||||
esignConfig,
|
||||
qiniuStorageService,
|
||||
contractAggregateService,
|
||||
walletAggregateService,
|
||||
apiUserAggregateService,
|
||||
enterpriseInfoSubmitRecordService,
|
||||
ocrService,
|
||||
txManager,
|
||||
logger,
|
||||
)
|
||||
},
|
||||
fx.As(new(certification.CertificationApplicationService)),
|
||||
),
|
||||
// 财务应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
finance.NewFinanceApplicationService,
|
||||
func(
|
||||
aliPayClient *payment.AliPayService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
rechargeRecordService finance_services.RechargeRecordService,
|
||||
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
userRepo domain_user_repo.UserRepository,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
config *config.Config,
|
||||
exportManager *export.ExportManager,
|
||||
) finance.FinanceApplicationService {
|
||||
return finance.NewFinanceApplicationService(
|
||||
aliPayClient,
|
||||
walletService,
|
||||
rechargeRecordService,
|
||||
walletTransactionRepo,
|
||||
alipayOrderRepo,
|
||||
userRepo,
|
||||
txManager,
|
||||
logger,
|
||||
config,
|
||||
exportManager,
|
||||
)
|
||||
},
|
||||
fx.As(new(finance.FinanceApplicationService)),
|
||||
),
|
||||
// 发票应用服务 - 绑定到接口
|
||||
@@ -692,7 +901,7 @@ func NewContainer() *Container {
|
||||
categoryRepo domain_article_repo.CategoryRepository,
|
||||
tagRepo domain_article_repo.TagRepository,
|
||||
articleService *article_service.ArticleService,
|
||||
asynqClient *task.AsynqClient,
|
||||
taskManager task_interfaces.TaskManager,
|
||||
logger *zap.Logger,
|
||||
) article.ArticleApplicationService {
|
||||
return article.NewArticleApplicationService(
|
||||
@@ -700,12 +909,47 @@ func NewContainer() *Container {
|
||||
categoryRepo,
|
||||
tagRepo,
|
||||
articleService,
|
||||
asynqClient,
|
||||
taskManager,
|
||||
logger,
|
||||
)
|
||||
},
|
||||
fx.As(new(article.ArticleApplicationService)),
|
||||
),
|
||||
// 统计应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
func(
|
||||
aggregateService statistics_service.StatisticsAggregateService,
|
||||
calculationService statistics_service.StatisticsCalculationService,
|
||||
reportService statistics_service.StatisticsReportService,
|
||||
metricRepo domain_statistics_repo.StatisticsRepository,
|
||||
reportRepo domain_statistics_repo.StatisticsReportRepository,
|
||||
dashboardRepo domain_statistics_repo.StatisticsDashboardRepository,
|
||||
userRepo domain_user_repo.UserRepository,
|
||||
apiCallRepo domain_api_repo.ApiCallRepository,
|
||||
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
productRepo domain_product_repo.ProductRepository,
|
||||
certificationRepo domain_certification_repo.CertificationQueryRepository,
|
||||
logger *zap.Logger,
|
||||
) statistics.StatisticsApplicationService {
|
||||
return statistics.NewStatisticsApplicationService(
|
||||
aggregateService,
|
||||
calculationService,
|
||||
reportService,
|
||||
metricRepo,
|
||||
reportRepo,
|
||||
dashboardRepo,
|
||||
userRepo,
|
||||
apiCallRepo,
|
||||
walletTransactionRepo,
|
||||
rechargeRecordRepo,
|
||||
productRepo,
|
||||
certificationRepo,
|
||||
logger,
|
||||
)
|
||||
},
|
||||
fx.As(new(statistics.StatisticsApplicationService)),
|
||||
),
|
||||
),
|
||||
|
||||
// HTTP处理器
|
||||
@@ -722,6 +966,8 @@ func NewContainer() *Container {
|
||||
handlers.NewProductAdminHandler,
|
||||
// API Handler
|
||||
handlers.NewApiHandler,
|
||||
// 统计HTTP处理器
|
||||
handlers.NewStatisticsHandler,
|
||||
// 文章HTTP处理器
|
||||
func(
|
||||
appService article.ArticleApplicationService,
|
||||
@@ -745,10 +991,12 @@ func NewContainer() *Container {
|
||||
routes.NewProductRoutes,
|
||||
// 产品管理员路由
|
||||
routes.NewProductAdminRoutes,
|
||||
// API路由
|
||||
// 文章路由
|
||||
routes.NewArticleRoutes,
|
||||
// API路由
|
||||
routes.NewApiRoutes,
|
||||
// 统计路由
|
||||
routes.NewStatisticsRoutes,
|
||||
),
|
||||
|
||||
// 应用生命周期
|
||||
@@ -783,6 +1031,7 @@ func (c *Container) Stop() error {
|
||||
func RegisterLifecycleHooks(
|
||||
lifecycle fx.Lifecycle,
|
||||
logger *zap.Logger,
|
||||
asynqWorker *asynq.AsynqWorker,
|
||||
) {
|
||||
lifecycle.Append(fx.Hook{
|
||||
OnStart: func(context.Context) error {
|
||||
@@ -793,10 +1042,23 @@ func RegisterLifecycleHooks(
|
||||
validator.InitGlobalValidator()
|
||||
logger.Info("全局校验器初始化完成")
|
||||
|
||||
// 启动AsynqWorker
|
||||
if err := asynqWorker.Start(); err != nil {
|
||||
logger.Error("启动AsynqWorker失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
logger.Info("AsynqWorker启动成功")
|
||||
|
||||
return nil
|
||||
},
|
||||
OnStop: func(context.Context) error {
|
||||
logger.Info("应用关闭中...")
|
||||
|
||||
// 停止AsynqWorker
|
||||
asynqWorker.Stop()
|
||||
asynqWorker.Shutdown()
|
||||
logger.Info("AsynqWorker已停止")
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
@@ -843,6 +1105,7 @@ func RegisterRoutes(
|
||||
productAdminRoutes *routes.ProductAdminRoutes,
|
||||
articleRoutes *routes.ArticleRoutes,
|
||||
apiRoutes *routes.ApiRoutes,
|
||||
statisticsRoutes *routes.StatisticsRoutes,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) {
|
||||
@@ -858,6 +1121,7 @@ func RegisterRoutes(
|
||||
productRoutes.Register(router)
|
||||
productAdminRoutes.Register(router)
|
||||
articleRoutes.Register(router)
|
||||
statisticsRoutes.Register(router)
|
||||
|
||||
// 打印注册的路由信息
|
||||
router.PrintRoutes()
|
||||
|
||||
@@ -271,6 +271,12 @@ type IVYZ5E3FReq struct {
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type IVYZ7F3AReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type YYSY4F2EReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -56,7 +52,7 @@ type ApiCall struct {
|
||||
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
|
||||
UserId *string `gorm:"type:varchar(36);index" json:"user_id,omitempty"`
|
||||
ProductId *string `gorm:"type:varchar(64);index" json:"product_id,omitempty"`
|
||||
TransactionId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id"`
|
||||
TransactionId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id"`
|
||||
ClientIp string `gorm:"type:varchar(64);not null;index" json:"client_ip"`
|
||||
RequestParams string `gorm:"type:text" json:"request_params"`
|
||||
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
|
||||
@@ -145,40 +141,9 @@ func (a *ApiCall) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 全局计数器,用于确保TransactionID的唯一性
|
||||
var (
|
||||
transactionCounter int64
|
||||
counterMutex sync.Mutex
|
||||
)
|
||||
|
||||
// GenerateTransactionID 生成16位数的交易单号
|
||||
// GenerateTransactionID 生成UUID格式的交易单号
|
||||
func GenerateTransactionID() string {
|
||||
// 使用互斥锁确保计数器的线程安全
|
||||
counterMutex.Lock()
|
||||
transactionCounter++
|
||||
currentCounter := transactionCounter
|
||||
counterMutex.Unlock()
|
||||
|
||||
// 获取当前时间戳(微秒精度)
|
||||
timestamp := time.Now().UnixMicro()
|
||||
|
||||
// 组合时间戳和计数器,确保唯一性
|
||||
combined := fmt.Sprintf("%d%06d", timestamp, currentCounter%1000000)
|
||||
|
||||
// 如果长度超出16位,截断;如果不够,填充随机字符
|
||||
if len(combined) >= 16 {
|
||||
return combined[:16]
|
||||
}
|
||||
|
||||
// 如果长度不够,使用随机字节填充
|
||||
if len(combined) < 16 {
|
||||
randomBytes := make([]byte, 8)
|
||||
rand.Read(randomBytes)
|
||||
randomHex := hex.EncodeToString(randomBytes)
|
||||
combined += randomHex[:16-len(combined)]
|
||||
}
|
||||
|
||||
return combined
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
|
||||
@@ -26,6 +26,14 @@ type ApiUser struct {
|
||||
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
|
||||
WhiteList []string `gorm:"type:json;serializer:json;default:'[]'" json:"white_list"` // 支持多个白名单
|
||||
|
||||
// 余额预警配置
|
||||
BalanceAlertEnabled bool `gorm:"default:true" json:"balance_alert_enabled" comment:"是否启用余额预警"`
|
||||
BalanceAlertThreshold float64 `gorm:"default:200.00" json:"balance_alert_threshold" comment:"余额预警阈值"`
|
||||
AlertPhone string `gorm:"type:varchar(20)" json:"alert_phone" comment:"预警手机号"`
|
||||
LastLowBalanceAlert *time.Time `json:"last_low_balance_alert" comment:"最后低余额预警时间"`
|
||||
LastArrearsAlert *time.Time `json:"last_arrears_alert" comment:"最后欠费预警时间"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
@@ -51,7 +59,7 @@ func (u *ApiUser) IsFrozen() bool {
|
||||
}
|
||||
|
||||
// NewApiUser 工厂方法
|
||||
func NewApiUser(userId string) (*ApiUser, error) {
|
||||
func NewApiUser(userId string, defaultAlertEnabled bool, defaultAlertThreshold float64) (*ApiUser, error) {
|
||||
if userId == "" {
|
||||
return nil, errors.New("用户ID不能为空")
|
||||
}
|
||||
@@ -70,6 +78,8 @@ func NewApiUser(userId string) (*ApiUser, error) {
|
||||
SecretKey: secretKey,
|
||||
Status: ApiUserStatusNormal,
|
||||
WhiteList: []string{},
|
||||
BalanceAlertEnabled: defaultAlertEnabled,
|
||||
BalanceAlertThreshold: defaultAlertThreshold,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -124,6 +134,68 @@ func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 余额预警相关方法
|
||||
|
||||
// UpdateBalanceAlertSettings 更新余额预警设置
|
||||
func (u *ApiUser) UpdateBalanceAlertSettings(enabled bool, threshold float64, phone string) error {
|
||||
if threshold < 0 {
|
||||
return errors.New("预警阈值不能为负数")
|
||||
}
|
||||
if phone != "" && len(phone) != 11 {
|
||||
return errors.New("手机号格式不正确")
|
||||
}
|
||||
|
||||
u.BalanceAlertEnabled = enabled
|
||||
u.BalanceAlertThreshold = threshold
|
||||
u.AlertPhone = phone
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldSendLowBalanceAlert 是否应该发送低余额预警(24小时冷却期)
|
||||
func (u *ApiUser) ShouldSendLowBalanceAlert(balance float64) bool {
|
||||
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 余额低于阈值
|
||||
if balance < u.BalanceAlertThreshold {
|
||||
// 检查是否已经发送过预警(避免频繁发送)
|
||||
if u.LastLowBalanceAlert != nil {
|
||||
// 如果距离上次预警不足24小时,不发送
|
||||
if time.Since(*u.LastLowBalanceAlert) < 24*time.Hour {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ShouldSendArrearsAlert 是否应该发送欠费预警(不受冷却期限制)
|
||||
func (u *ApiUser) ShouldSendArrearsAlert(balance float64) bool {
|
||||
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 余额为负数(欠费)- 欠费预警不受冷却期限制
|
||||
if balance < 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarkLowBalanceAlertSent 标记低余额预警已发送
|
||||
func (u *ApiUser) MarkLowBalanceAlertSent() {
|
||||
now := time.Now()
|
||||
u.LastLowBalanceAlert = &now
|
||||
}
|
||||
|
||||
// MarkArrearsAlertSent 标记欠费预警已发送
|
||||
func (u *ApiUser) MarkArrearsAlertSent() {
|
||||
now := time.Now()
|
||||
u.LastArrearsAlert = &now
|
||||
}
|
||||
|
||||
// Validate 校验ApiUser聚合根的业务规则
|
||||
func (u *ApiUser) Validate() error {
|
||||
if u.UserId == "" {
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
@@ -27,6 +28,20 @@ type ApiCallRepository interface {
|
||||
// 新增:根据TransactionID查询
|
||||
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
|
||||
|
||||
// 统计相关方法
|
||||
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 管理端:根据条件筛选所有API调用记录(包含产品名称)
|
||||
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalCalls(ctx context.Context) (int64, error)
|
||||
GetSystemCallsByDateRange(ctx context.Context, startDate, endDate time.Time) (int64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// API受欢迎程度排行榜
|
||||
GetApiPopularityRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZ2A8B": ivyz.ProcessIVYZ2A8BRequest,
|
||||
"IVYZ7C9D": ivyz.ProcessIVYZ7C9DRequest,
|
||||
"IVYZ5E3F": ivyz.ProcessIVYZ5E3FRequest,
|
||||
"IVYZ7F3A": ivyz.ProcessIVYZ7F3ARequest,
|
||||
|
||||
// COMB系列处理器
|
||||
"COMB298Y": comb.ProcessCOMB298YRequest,
|
||||
|
||||
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
repo "tyapi-server/internal/domains/api/repositories"
|
||||
)
|
||||
@@ -20,14 +21,15 @@ type ApiUserAggregateService interface {
|
||||
|
||||
type ApiUserAggregateServiceImpl struct {
|
||||
repo repo.ApiUserRepository
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewApiUserAggregateService(repo repo.ApiUserRepository) ApiUserAggregateService {
|
||||
return &ApiUserAggregateServiceImpl{repo: repo}
|
||||
func NewApiUserAggregateService(repo repo.ApiUserRepository, cfg *config.Config) ApiUserAggregateService {
|
||||
return &ApiUserAggregateServiceImpl{repo: repo, cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *ApiUserAggregateServiceImpl) CreateApiUser(ctx context.Context, apiUserId string) error {
|
||||
apiUser, err := entities.NewApiUser(apiUserId)
|
||||
apiUser, err := entities.NewApiUser(apiUserId, s.cfg.Wallet.BalanceAlert.DefaultEnabled, s.cfg.Wallet.BalanceAlert.DefaultThreshold)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZ7F3ARequest IVYZ7F3A API处理方法 - 身份二要素认证(ZCI004)
|
||||
func ProcessIVYZ7F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ7F3AReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI004", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -35,7 +35,6 @@ type Article struct {
|
||||
IsFeatured bool `gorm:"default:false" json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
TaskID string `gorm:"type:varchar(100)" json:"task_id" comment:"定时任务ID"`
|
||||
|
||||
// 统计信息
|
||||
ViewCount int `gorm:"default:0" json:"view_count" comment:"阅读量"`
|
||||
@@ -120,7 +119,7 @@ func (a *Article) Publish() error {
|
||||
}
|
||||
|
||||
// SchedulePublish 定时发布文章
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error {
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
@@ -131,13 +130,12 @@ func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error
|
||||
|
||||
a.Status = ArticleStatusDraft // 保持草稿状态,等待定时发布
|
||||
a.ScheduledAt = &scheduledTime
|
||||
a.TaskID = taskID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSchedulePublish 更新定时发布时间
|
||||
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string) error {
|
||||
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
@@ -147,7 +145,6 @@ func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string)
|
||||
}
|
||||
|
||||
a.ScheduledAt = &scheduledTime
|
||||
a.TaskID = taskID
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -159,7 +156,6 @@ func (a *Article) CancelSchedulePublish() error {
|
||||
}
|
||||
|
||||
a.ScheduledAt = nil
|
||||
a.TaskID = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ type Wallet struct {
|
||||
// 钱包状态 - 钱包的基本状态信息
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"钱包是否激活"`
|
||||
Balance decimal.Decimal `gorm:"type:decimal(20,8);default:0" json:"balance" comment:"钱包余额(精确到8位小数)"`
|
||||
Version int64 `gorm:"version" json:"version" comment:"乐观锁版本号"`
|
||||
Version int64 `gorm:"default:0" json:"version" comment:"乐观锁版本号"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
|
||||
@@ -15,7 +15,7 @@ type WalletTransaction struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"交易记录唯一标识"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"扣款用户ID"`
|
||||
ApiCallID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"api_call_id" comment:"关联API调用ID"`
|
||||
TransactionID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id" comment:"交易ID"`
|
||||
TransactionID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id" comment:"交易ID"`
|
||||
ProductID string `gorm:"type:varchar(64);not null;index" json:"product_id" comment:"产品ID"`
|
||||
|
||||
// 扣款信息
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
@@ -20,4 +21,16 @@ type RechargeRecordRepository interface {
|
||||
// 管理员查询方法
|
||||
List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
|
||||
Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
|
||||
|
||||
// 统计相关方法
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// FinanceStats 财务统计信息
|
||||
@@ -25,7 +27,9 @@ type WalletRepository interface {
|
||||
GetByUserID(ctx context.Context, userID string) (*entities.Wallet, error)
|
||||
|
||||
// 乐观锁更新(自动重试)
|
||||
UpdateBalanceWithVersion(ctx context.Context, walletID string, newBalance string, oldVersion int64) (bool, error)
|
||||
UpdateBalanceWithVersion(ctx context.Context, walletID string, amount decimal.Decimal, operation string) (bool, error)
|
||||
// 乐观锁更新(通过用户ID直接更新,避免重复查询)
|
||||
UpdateBalanceByUserID(ctx context.Context, userID string, amount decimal.Decimal, operation string) (bool, error)
|
||||
|
||||
// 状态操作
|
||||
ActivateWallet(ctx context.Context, walletID string) error
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
@@ -26,6 +27,22 @@ type WalletTransactionRepository interface {
|
||||
// 新增:统计用户钱包交易次数
|
||||
CountByUserId(ctx context.Context, userId string) (int64, error)
|
||||
|
||||
// 统计相关方法
|
||||
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 管理端:根据条件筛选所有钱包交易记录(包含产品名称)
|
||||
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error)
|
||||
|
||||
// 管理端:导出钱包交易记录(包含产品名称和企业信息)
|
||||
ExportWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}) ([]*entities.WalletTransaction, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
}
|
||||
186
internal/domains/finance/services/balance_alert_service.go
Normal file
186
internal/domains/finance/services/balance_alert_service.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
)
|
||||
|
||||
// BalanceAlertService 余额预警服务接口
|
||||
type BalanceAlertService interface {
|
||||
CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error
|
||||
}
|
||||
|
||||
// BalanceAlertServiceImpl 余额预警服务实现
|
||||
type BalanceAlertServiceImpl struct {
|
||||
apiUserRepo api_repositories.ApiUserRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository
|
||||
smsService *sms.AliSMSService
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewBalanceAlertService 创建余额预警服务
|
||||
func NewBalanceAlertService(
|
||||
apiUserRepo api_repositories.ApiUserRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
|
||||
smsService *sms.AliSMSService,
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
) BalanceAlertService {
|
||||
return &BalanceAlertServiceImpl{
|
||||
apiUserRepo: apiUserRepo,
|
||||
userRepo: userRepo,
|
||||
enterpriseInfoRepo: enterpriseInfoRepo,
|
||||
smsService: smsService,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAndSendAlert 检查余额并发送预警
|
||||
func (s *BalanceAlertServiceImpl) CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error {
|
||||
// 1. 获取API用户信息
|
||||
apiUser, err := s.apiUserRepo.FindByUserId(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取API用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取API用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
if apiUser == nil {
|
||||
s.logger.Debug("API用户不存在,跳过余额预警检查", zap.String("user_id", userID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. 兼容性处理:如果API用户没有配置预警信息,从用户表获取并更新
|
||||
needUpdate := false
|
||||
if apiUser.AlertPhone == "" {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
if user.Phone != "" {
|
||||
apiUser.AlertPhone = user.Phone
|
||||
needUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 兼容性处理:如果API用户没有配置预警阈值,使用默认值
|
||||
if apiUser.BalanceAlertThreshold == 0 {
|
||||
apiUser.BalanceAlertThreshold = s.config.Wallet.BalanceAlert.DefaultThreshold
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
// 4. 如果需要更新API用户信息,保存到数据库
|
||||
if needUpdate {
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警配置失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
// 不返回错误,继续执行预警检查
|
||||
}
|
||||
}
|
||||
|
||||
balanceFloat, _ := balance.Float64()
|
||||
|
||||
// 5. 检查是否需要发送欠费预警(不受冷却期限制)
|
||||
if apiUser.ShouldSendArrearsAlert(balanceFloat) {
|
||||
if err := s.sendArrearsAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送欠费预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 欠费预警不受冷却期限制,不需要更新LastArrearsAlert时间
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. 检查是否需要发送低余额预警
|
||||
if apiUser.ShouldSendLowBalanceAlert(balanceFloat) {
|
||||
if err := s.sendLowBalanceAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送低余额预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 标记预警已发送
|
||||
apiUser.MarkLowBalanceAlertSent()
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警时间失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendArrearsAlert 发送欠费预警
|
||||
func (s *BalanceAlertServiceImpl) sendArrearsAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", "天远数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "天远数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送欠费预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", enterpriseName)
|
||||
}
|
||||
|
||||
// sendLowBalanceAlert 发送低余额预警
|
||||
func (s *BalanceAlertServiceImpl) sendLowBalanceAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", "天远数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "天远数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送低余额预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.Float64("threshold", apiUser.BalanceAlertThreshold),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", enterpriseName)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
@@ -25,21 +26,27 @@ type WalletAggregateService interface {
|
||||
|
||||
// WalletAggregateServiceImpl 钱包聚合服务实现
|
||||
type WalletAggregateServiceImpl struct {
|
||||
db *gorm.DB
|
||||
walletRepo repositories.WalletRepository
|
||||
transactionRepo repositories.WalletTransactionRepository
|
||||
balanceAlertSvc BalanceAlertService
|
||||
logger *zap.Logger
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewWalletAggregateService(
|
||||
db *gorm.DB,
|
||||
walletRepo repositories.WalletRepository,
|
||||
transactionRepo repositories.WalletTransactionRepository,
|
||||
balanceAlertSvc BalanceAlertService,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) WalletAggregateService {
|
||||
return &WalletAggregateServiceImpl{
|
||||
db: db,
|
||||
walletRepo: walletRepo,
|
||||
transactionRepo: transactionRepo,
|
||||
balanceAlertSvc: balanceAlertSvc,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
}
|
||||
@@ -62,18 +69,13 @@ func (s *WalletAggregateServiceImpl) CreateWallet(ctx context.Context, userID st
|
||||
return &created, nil
|
||||
}
|
||||
|
||||
// Recharge 充值
|
||||
// Recharge 充值 - 使用事务确保一致性
|
||||
func (s *WalletAggregateServiceImpl) Recharge(ctx context.Context, userID string, amount decimal.Decimal) error {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
// 使用数据库事务确保一致性
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(ctx, userID, amount, "add")
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
|
||||
// 更新钱包余额
|
||||
w.AddBalance(amount)
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新钱包余额失败: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下充值失败,请重试")
|
||||
@@ -81,53 +83,45 @@ func (s *WalletAggregateServiceImpl) Recharge(ctx context.Context, userID string
|
||||
|
||||
s.logger.Info("钱包充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("wallet_id", w.ID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("balance_after", w.Balance.String()))
|
||||
zap.String("amount", amount.String()))
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
// Deduct 扣款,含欠费规则
|
||||
|
||||
// Deduct 扣款,含欠费规则 - 使用事务确保一致性
|
||||
func (s *WalletAggregateServiceImpl) Deduct(ctx context.Context, userID string, amount decimal.Decimal, apiCallID, transactionID, productID string) error {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
// 使用数据库事务确保一致性
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 使用乐观锁更新余额(通过用户ID直接更新,避免重复查询)
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(ctx, userID, amount, "subtract")
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
|
||||
// 扣减余额
|
||||
if err := w.SubtractBalance(amount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新钱包余额
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新钱包余额失败: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下扣款失败,请重试")
|
||||
}
|
||||
|
||||
// 创建扣款记录
|
||||
// 2. 创建扣款记录(检查是否已存在)
|
||||
transaction := entities.NewWalletTransaction(userID, apiCallID, transactionID, productID, amount)
|
||||
_, err = s.transactionRepo.Create(ctx, *transaction)
|
||||
if err != nil {
|
||||
s.logger.Error("创建扣款记录失败", zap.Error(err))
|
||||
// 不返回错误,因为钱包余额已经更新成功
|
||||
|
||||
if err := tx.Create(transaction).Error; err != nil {
|
||||
return fmt.Errorf("创建扣款记录失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("钱包扣款成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("wallet_id", w.ID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("balance_after", w.Balance.String()),
|
||||
zap.String("api_call_id", apiCallID))
|
||||
zap.String("api_call_id", apiCallID),
|
||||
zap.String("transaction_id", transactionID))
|
||||
|
||||
// 3. 扣费成功后异步检查余额预警
|
||||
go s.checkBalanceAlertAsync(context.Background(), userID)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// GetBalance 查询余额
|
||||
func (s *WalletAggregateServiceImpl) GetBalance(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
@@ -140,3 +134,22 @@ func (s *WalletAggregateServiceImpl) GetBalance(ctx context.Context, userID stri
|
||||
func (s *WalletAggregateServiceImpl) LoadWalletByUserId(ctx context.Context, userID string) (*entities.Wallet, error) {
|
||||
return s.walletRepo.GetByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
// checkBalanceAlertAsync 异步检查余额预警
|
||||
func (s *WalletAggregateServiceImpl) checkBalanceAlertAsync(ctx context.Context, userID string) {
|
||||
// 获取最新余额
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取钱包余额失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查并发送预警
|
||||
if err := s.balanceAlertSvc.CheckAndSendAlert(ctx, userID, wallet.Balance); err != nil {
|
||||
s.logger.Error("余额预警检查失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
434
internal/domains/statistics/entities/statistics_dashboard.go
Normal file
434
internal/domains/statistics/entities/statistics_dashboard.go
Normal file
@@ -0,0 +1,434 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StatisticsDashboard 仪表板配置实体
|
||||
// 用于存储仪表板的配置信息
|
||||
type StatisticsDashboard struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"仪表板唯一标识"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name" comment:"仪表板名称"`
|
||||
Description string `gorm:"type:text" json:"description" comment:"仪表板描述"`
|
||||
UserRole string `gorm:"type:varchar(20);not null;index" json:"user_role" comment:"用户角色"`
|
||||
IsDefault bool `gorm:"default:false" json:"is_default" comment:"是否为默认仪表板"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"是否激活"`
|
||||
|
||||
// 仪表板配置
|
||||
Layout string `gorm:"type:json" json:"layout" comment:"布局配置"`
|
||||
Widgets string `gorm:"type:json" json:"widgets" comment:"组件配置"`
|
||||
Settings string `gorm:"type:json" json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `gorm:"default:300" json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||
|
||||
// 权限和访问控制
|
||||
CreatedBy string `gorm:"type:varchar(36);not null" json:"created_by" comment:"创建者ID"`
|
||||
AccessLevel string `gorm:"type:varchar(20);default:'private'" json:"access_level" comment:"访问级别"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (StatisticsDashboard) TableName() string {
|
||||
return "statistics_dashboards"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (s *StatisticsDashboard) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (s *StatisticsDashboard) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (s *StatisticsDashboard) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (s *StatisticsDashboard) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证仪表板配置信息
|
||||
// 检查仪表板必填字段是否完整,确保数据的有效性
|
||||
func (s *StatisticsDashboard) Validate() error {
|
||||
if s.Name == "" {
|
||||
return NewValidationError("仪表板名称不能为空")
|
||||
}
|
||||
if s.UserRole == "" {
|
||||
return NewValidationError("用户角色不能为空")
|
||||
}
|
||||
if s.CreatedBy == "" {
|
||||
return NewValidationError("创建者ID不能为空")
|
||||
}
|
||||
|
||||
// 验证用户角色
|
||||
if !s.IsValidUserRole() {
|
||||
return NewValidationError("无效的用户角色")
|
||||
}
|
||||
|
||||
// 验证访问级别
|
||||
if !s.IsValidAccessLevel() {
|
||||
return NewValidationError("无效的访问级别")
|
||||
}
|
||||
|
||||
// 验证刷新间隔
|
||||
if s.RefreshInterval < 30 {
|
||||
return NewValidationError("刷新间隔不能少于30秒")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidUserRole 检查用户角色是否有效
|
||||
func (s *StatisticsDashboard) IsValidUserRole() bool {
|
||||
validRoles := []string{
|
||||
"admin", // 管理员
|
||||
"user", // 普通用户
|
||||
"manager", // 经理
|
||||
"analyst", // 分析师
|
||||
}
|
||||
|
||||
for _, validRole := range validRoles {
|
||||
if s.UserRole == validRole {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidAccessLevel 检查访问级别是否有效
|
||||
func (s *StatisticsDashboard) IsValidAccessLevel() bool {
|
||||
validLevels := []string{
|
||||
"private", // 私有
|
||||
"public", // 公开
|
||||
"shared", // 共享
|
||||
}
|
||||
|
||||
for _, validLevel := range validLevels {
|
||||
if s.AccessLevel == validLevel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetUserRoleName 获取用户角色的中文名称
|
||||
func (s *StatisticsDashboard) GetUserRoleName() string {
|
||||
roleNames := map[string]string{
|
||||
"admin": "管理员",
|
||||
"user": "普通用户",
|
||||
"manager": "经理",
|
||||
"analyst": "分析师",
|
||||
}
|
||||
|
||||
if name, exists := roleNames[s.UserRole]; exists {
|
||||
return name
|
||||
}
|
||||
return s.UserRole
|
||||
}
|
||||
|
||||
// GetAccessLevelName 获取访问级别的中文名称
|
||||
func (s *StatisticsDashboard) GetAccessLevelName() string {
|
||||
levelNames := map[string]string{
|
||||
"private": "私有",
|
||||
"public": "公开",
|
||||
"shared": "共享",
|
||||
}
|
||||
|
||||
if name, exists := levelNames[s.AccessLevel]; exists {
|
||||
return name
|
||||
}
|
||||
return s.AccessLevel
|
||||
}
|
||||
|
||||
// NewStatisticsDashboard 工厂方法 - 创建仪表板配置
|
||||
func NewStatisticsDashboard(name, description, userRole, createdBy string) (*StatisticsDashboard, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("仪表板名称不能为空")
|
||||
}
|
||||
if userRole == "" {
|
||||
return nil, errors.New("用户角色不能为空")
|
||||
}
|
||||
if createdBy == "" {
|
||||
return nil, errors.New("创建者ID不能为空")
|
||||
}
|
||||
|
||||
dashboard := &StatisticsDashboard{
|
||||
Name: name,
|
||||
Description: description,
|
||||
UserRole: userRole,
|
||||
CreatedBy: createdBy,
|
||||
IsDefault: false,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 300, // 默认5分钟
|
||||
domainEvents: make([]interface{}, 0),
|
||||
}
|
||||
|
||||
// 验证仪表板
|
||||
if err := dashboard.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
dashboard.addDomainEvent(&StatisticsDashboardCreatedEvent{
|
||||
DashboardID: dashboard.ID,
|
||||
Name: name,
|
||||
UserRole: userRole,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
// SetAsDefault 设置为默认仪表板
|
||||
func (s *StatisticsDashboard) SetAsDefault() error {
|
||||
if !s.IsActive {
|
||||
return NewValidationError("只有激活状态的仪表板才能设置为默认")
|
||||
}
|
||||
|
||||
s.IsDefault = true
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardSetAsDefaultEvent{
|
||||
DashboardID: s.ID,
|
||||
SetAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAsDefault 取消默认状态
|
||||
func (s *StatisticsDashboard) RemoveAsDefault() error {
|
||||
if !s.IsDefault {
|
||||
return NewValidationError("当前仪表板不是默认仪表板")
|
||||
}
|
||||
|
||||
s.IsDefault = false
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardRemovedAsDefaultEvent{
|
||||
DashboardID: s.ID,
|
||||
RemovedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate 激活仪表板
|
||||
func (s *StatisticsDashboard) Activate() error {
|
||||
if s.IsActive {
|
||||
return NewValidationError("仪表板已经是激活状态")
|
||||
}
|
||||
|
||||
s.IsActive = true
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardActivatedEvent{
|
||||
DashboardID: s.ID,
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate 停用仪表板
|
||||
func (s *StatisticsDashboard) Deactivate() error {
|
||||
if !s.IsActive {
|
||||
return NewValidationError("仪表板已经是停用状态")
|
||||
}
|
||||
|
||||
s.IsActive = false
|
||||
|
||||
// 如果是默认仪表板,需要先取消默认状态
|
||||
if s.IsDefault {
|
||||
s.IsDefault = false
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardDeactivatedEvent{
|
||||
DashboardID: s.ID,
|
||||
DeactivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLayout 更新布局配置
|
||||
func (s *StatisticsDashboard) UpdateLayout(layout string) error {
|
||||
if layout == "" {
|
||||
return NewValidationError("布局配置不能为空")
|
||||
}
|
||||
|
||||
s.Layout = layout
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardLayoutUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWidgets 更新组件配置
|
||||
func (s *StatisticsDashboard) UpdateWidgets(widgets string) error {
|
||||
if widgets == "" {
|
||||
return NewValidationError("组件配置不能为空")
|
||||
}
|
||||
|
||||
s.Widgets = widgets
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardWidgetsUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSettings 更新设置配置
|
||||
func (s *StatisticsDashboard) UpdateSettings(settings string) error {
|
||||
s.Settings = settings
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardSettingsUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRefreshInterval 更新刷新间隔
|
||||
func (s *StatisticsDashboard) UpdateRefreshInterval(interval int) error {
|
||||
if interval < 30 {
|
||||
return NewValidationError("刷新间隔不能少于30秒")
|
||||
}
|
||||
|
||||
oldInterval := s.RefreshInterval
|
||||
s.RefreshInterval = interval
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardRefreshIntervalUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
OldInterval: oldInterval,
|
||||
NewInterval: interval,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanBeModified 检查仪表板是否可以被修改
|
||||
func (s *StatisticsDashboard) CanBeModified() bool {
|
||||
return s.IsActive
|
||||
}
|
||||
|
||||
// CanBeDeleted 检查仪表板是否可以被删除
|
||||
func (s *StatisticsDashboard) CanBeDeleted() bool {
|
||||
return !s.IsDefault && s.IsActive
|
||||
}
|
||||
|
||||
// ================ 领域事件管理 ================
|
||||
|
||||
// addDomainEvent 添加领域事件
|
||||
func (s *StatisticsDashboard) addDomainEvent(event interface{}) {
|
||||
if s.domainEvents == nil {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
s.domainEvents = append(s.domainEvents, event)
|
||||
}
|
||||
|
||||
// GetDomainEvents 获取领域事件
|
||||
func (s *StatisticsDashboard) GetDomainEvents() []interface{} {
|
||||
return s.domainEvents
|
||||
}
|
||||
|
||||
// ClearDomainEvents 清除领域事件
|
||||
func (s *StatisticsDashboard) ClearDomainEvents() {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
|
||||
// ================ 领域事件定义 ================
|
||||
|
||||
// StatisticsDashboardCreatedEvent 仪表板创建事件
|
||||
type StatisticsDashboardCreatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
Name string `json:"name"`
|
||||
UserRole string `json:"user_role"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardSetAsDefaultEvent 仪表板设置为默认事件
|
||||
type StatisticsDashboardSetAsDefaultEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
SetAt time.Time `json:"set_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardRemovedAsDefaultEvent 仪表板取消默认事件
|
||||
type StatisticsDashboardRemovedAsDefaultEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
RemovedAt time.Time `json:"removed_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardActivatedEvent 仪表板激活事件
|
||||
type StatisticsDashboardActivatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
ActivatedAt time.Time `json:"activated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardDeactivatedEvent 仪表板停用事件
|
||||
type StatisticsDashboardDeactivatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
DeactivatedAt time.Time `json:"deactivated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardLayoutUpdatedEvent 仪表板布局更新事件
|
||||
type StatisticsDashboardLayoutUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardWidgetsUpdatedEvent 仪表板组件更新事件
|
||||
type StatisticsDashboardWidgetsUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardSettingsUpdatedEvent 仪表板设置更新事件
|
||||
type StatisticsDashboardSettingsUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardRefreshIntervalUpdatedEvent 仪表板刷新间隔更新事件
|
||||
type StatisticsDashboardRefreshIntervalUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
OldInterval int `json:"old_interval"`
|
||||
NewInterval int `json:"new_interval"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
244
internal/domains/statistics/entities/statistics_metric.go
Normal file
244
internal/domains/statistics/entities/statistics_metric.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StatisticsMetric 统计指标实体
|
||||
// 用于存储各种统计指标数据,支持多维度统计
|
||||
type StatisticsMetric struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"统计指标唯一标识"`
|
||||
MetricType string `gorm:"type:varchar(50);not null;index" json:"metric_type" comment:"指标类型"`
|
||||
MetricName string `gorm:"type:varchar(100);not null" json:"metric_name" comment:"指标名称"`
|
||||
Dimension string `gorm:"type:varchar(50)" json:"dimension" comment:"统计维度"`
|
||||
Value float64 `gorm:"type:decimal(20,4);not null" json:"value" comment:"指标值"`
|
||||
Metadata string `gorm:"type:json" json:"metadata" comment:"额外维度信息"`
|
||||
Date time.Time `gorm:"type:date;index" json:"date" comment:"统计日期"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (StatisticsMetric) TableName() string {
|
||||
return "statistics_metrics"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (s *StatisticsMetric) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (s *StatisticsMetric) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (s *StatisticsMetric) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (s *StatisticsMetric) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证统计指标信息
|
||||
// 检查统计指标必填字段是否完整,确保数据的有效性
|
||||
func (s *StatisticsMetric) Validate() error {
|
||||
if s.MetricType == "" {
|
||||
return NewValidationError("指标类型不能为空")
|
||||
}
|
||||
if s.MetricName == "" {
|
||||
return NewValidationError("指标名称不能为空")
|
||||
}
|
||||
if s.Value < 0 {
|
||||
return NewValidationError("指标值不能为负数")
|
||||
}
|
||||
if s.Date.IsZero() {
|
||||
return NewValidationError("统计日期不能为空")
|
||||
}
|
||||
|
||||
// 验证指标类型
|
||||
if !s.IsValidMetricType() {
|
||||
return NewValidationError("无效的指标类型")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidMetricType 检查指标类型是否有效
|
||||
func (s *StatisticsMetric) IsValidMetricType() bool {
|
||||
validTypes := []string{
|
||||
"api_calls", // API调用统计
|
||||
"users", // 用户统计
|
||||
"finance", // 财务统计
|
||||
"products", // 产品统计
|
||||
"certification", // 认证统计
|
||||
}
|
||||
|
||||
for _, validType := range validTypes {
|
||||
if s.MetricType == validType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetMetricTypeName 获取指标类型的中文名称
|
||||
func (s *StatisticsMetric) GetMetricTypeName() string {
|
||||
typeNames := map[string]string{
|
||||
"api_calls": "API调用统计",
|
||||
"users": "用户统计",
|
||||
"finance": "财务统计",
|
||||
"products": "产品统计",
|
||||
"certification": "认证统计",
|
||||
}
|
||||
|
||||
if name, exists := typeNames[s.MetricType]; exists {
|
||||
return name
|
||||
}
|
||||
return s.MetricType
|
||||
}
|
||||
|
||||
// GetFormattedValue 获取格式化的指标值
|
||||
func (s *StatisticsMetric) GetFormattedValue() string {
|
||||
// 根据指标类型格式化数值
|
||||
switch s.MetricType {
|
||||
case "api_calls", "users":
|
||||
return fmt.Sprintf("%.0f", s.Value)
|
||||
case "finance":
|
||||
return fmt.Sprintf("%.2f", s.Value)
|
||||
default:
|
||||
return fmt.Sprintf("%.4f", s.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// NewStatisticsMetric 工厂方法 - 创建统计指标
|
||||
func NewStatisticsMetric(metricType, metricName, dimension string, value float64, date time.Time) (*StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, errors.New("指标类型不能为空")
|
||||
}
|
||||
if metricName == "" {
|
||||
return nil, errors.New("指标名称不能为空")
|
||||
}
|
||||
if value < 0 {
|
||||
return nil, errors.New("指标值不能为负数")
|
||||
}
|
||||
if date.IsZero() {
|
||||
return nil, errors.New("统计日期不能为空")
|
||||
}
|
||||
|
||||
metric := &StatisticsMetric{
|
||||
MetricType: metricType,
|
||||
MetricName: metricName,
|
||||
Dimension: dimension,
|
||||
Value: value,
|
||||
Date: date,
|
||||
domainEvents: make([]interface{}, 0),
|
||||
}
|
||||
|
||||
// 验证指标
|
||||
if err := metric.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
metric.addDomainEvent(&StatisticsMetricCreatedEvent{
|
||||
MetricID: metric.ID,
|
||||
MetricType: metricType,
|
||||
MetricName: metricName,
|
||||
Value: value,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return metric, nil
|
||||
}
|
||||
|
||||
// UpdateValue 更新指标值
|
||||
func (s *StatisticsMetric) UpdateValue(newValue float64) error {
|
||||
if newValue < 0 {
|
||||
return NewValidationError("指标值不能为负数")
|
||||
}
|
||||
|
||||
oldValue := s.Value
|
||||
s.Value = newValue
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsMetricUpdatedEvent{
|
||||
MetricID: s.ID,
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 领域事件管理 ================
|
||||
|
||||
// addDomainEvent 添加领域事件
|
||||
func (s *StatisticsMetric) addDomainEvent(event interface{}) {
|
||||
if s.domainEvents == nil {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
s.domainEvents = append(s.domainEvents, event)
|
||||
}
|
||||
|
||||
// GetDomainEvents 获取领域事件
|
||||
func (s *StatisticsMetric) GetDomainEvents() []interface{} {
|
||||
return s.domainEvents
|
||||
}
|
||||
|
||||
// ClearDomainEvents 清除领域事件
|
||||
func (s *StatisticsMetric) ClearDomainEvents() {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
|
||||
// ================ 领域事件定义 ================
|
||||
|
||||
// StatisticsMetricCreatedEvent 统计指标创建事件
|
||||
type StatisticsMetricCreatedEvent struct {
|
||||
MetricID string `json:"metric_id"`
|
||||
MetricType string `json:"metric_type"`
|
||||
MetricName string `json:"metric_name"`
|
||||
Value float64 `json:"value"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// StatisticsMetricUpdatedEvent 统计指标更新事件
|
||||
type StatisticsMetricUpdatedEvent struct {
|
||||
MetricID string `json:"metric_id"`
|
||||
OldValue float64 `json:"old_value"`
|
||||
NewValue float64 `json:"new_value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ValidationError 验证错误
|
||||
type ValidationError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewValidationError(message string) *ValidationError {
|
||||
return &ValidationError{Message: message}
|
||||
}
|
||||
343
internal/domains/statistics/entities/statistics_report.go
Normal file
343
internal/domains/statistics/entities/statistics_report.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StatisticsReport 统计报告实体
|
||||
// 用于存储生成的统计报告数据
|
||||
type StatisticsReport struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"报告唯一标识"`
|
||||
ReportType string `gorm:"type:varchar(50);not null;index" json:"report_type" comment:"报告类型"`
|
||||
Title string `gorm:"type:varchar(200);not null" json:"title" comment:"报告标题"`
|
||||
Content string `gorm:"type:json" json:"content" comment:"报告内容"`
|
||||
Period string `gorm:"type:varchar(20)" json:"period" comment:"统计周期"`
|
||||
UserRole string `gorm:"type:varchar(20)" json:"user_role" comment:"用户角色"`
|
||||
Status string `gorm:"type:varchar(20);default:'draft'" json:"status" comment:"报告状态"`
|
||||
|
||||
// 报告元数据
|
||||
GeneratedBy string `gorm:"type:varchar(36)" json:"generated_by" comment:"生成者ID"`
|
||||
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
|
||||
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (StatisticsReport) TableName() string {
|
||||
return "statistics_reports"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (s *StatisticsReport) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (s *StatisticsReport) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (s *StatisticsReport) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (s *StatisticsReport) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证统计报告信息
|
||||
// 检查统计报告必填字段是否完整,确保数据的有效性
|
||||
func (s *StatisticsReport) Validate() error {
|
||||
if s.ReportType == "" {
|
||||
return NewValidationError("报告类型不能为空")
|
||||
}
|
||||
if s.Title == "" {
|
||||
return NewValidationError("报告标题不能为空")
|
||||
}
|
||||
if s.Period == "" {
|
||||
return NewValidationError("统计周期不能为空")
|
||||
}
|
||||
|
||||
// 验证报告类型
|
||||
if !s.IsValidReportType() {
|
||||
return NewValidationError("无效的报告类型")
|
||||
}
|
||||
|
||||
// 验证报告状态
|
||||
if !s.IsValidStatus() {
|
||||
return NewValidationError("无效的报告状态")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidReportType 检查报告类型是否有效
|
||||
func (s *StatisticsReport) IsValidReportType() bool {
|
||||
validTypes := []string{
|
||||
"dashboard", // 仪表板报告
|
||||
"summary", // 汇总报告
|
||||
"detailed", // 详细报告
|
||||
"custom", // 自定义报告
|
||||
}
|
||||
|
||||
for _, validType := range validTypes {
|
||||
if s.ReportType == validType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidStatus 检查报告状态是否有效
|
||||
func (s *StatisticsReport) IsValidStatus() bool {
|
||||
validStatuses := []string{
|
||||
"draft", // 草稿
|
||||
"generating", // 生成中
|
||||
"completed", // 已完成
|
||||
"failed", // 生成失败
|
||||
"expired", // 已过期
|
||||
}
|
||||
|
||||
for _, validStatus := range validStatuses {
|
||||
if s.Status == validStatus {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetReportTypeName 获取报告类型的中文名称
|
||||
func (s *StatisticsReport) GetReportTypeName() string {
|
||||
typeNames := map[string]string{
|
||||
"dashboard": "仪表板报告",
|
||||
"summary": "汇总报告",
|
||||
"detailed": "详细报告",
|
||||
"custom": "自定义报告",
|
||||
}
|
||||
|
||||
if name, exists := typeNames[s.ReportType]; exists {
|
||||
return name
|
||||
}
|
||||
return s.ReportType
|
||||
}
|
||||
|
||||
// GetStatusName 获取报告状态的中文名称
|
||||
func (s *StatisticsReport) GetStatusName() string {
|
||||
statusNames := map[string]string{
|
||||
"draft": "草稿",
|
||||
"generating": "生成中",
|
||||
"completed": "已完成",
|
||||
"failed": "生成失败",
|
||||
"expired": "已过期",
|
||||
}
|
||||
|
||||
if name, exists := statusNames[s.Status]; exists {
|
||||
return name
|
||||
}
|
||||
return s.Status
|
||||
}
|
||||
|
||||
// NewStatisticsReport 工厂方法 - 创建统计报告
|
||||
func NewStatisticsReport(reportType, title, period, userRole string) (*StatisticsReport, error) {
|
||||
if reportType == "" {
|
||||
return nil, errors.New("报告类型不能为空")
|
||||
}
|
||||
if title == "" {
|
||||
return nil, errors.New("报告标题不能为空")
|
||||
}
|
||||
if period == "" {
|
||||
return nil, errors.New("统计周期不能为空")
|
||||
}
|
||||
|
||||
report := &StatisticsReport{
|
||||
ReportType: reportType,
|
||||
Title: title,
|
||||
Period: period,
|
||||
UserRole: userRole,
|
||||
Status: "draft",
|
||||
domainEvents: make([]interface{}, 0),
|
||||
}
|
||||
|
||||
// 验证报告
|
||||
if err := report.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
report.addDomainEvent(&StatisticsReportCreatedEvent{
|
||||
ReportID: report.ID,
|
||||
ReportType: reportType,
|
||||
Title: title,
|
||||
Period: period,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// StartGeneration 开始生成报告
|
||||
func (s *StatisticsReport) StartGeneration(generatedBy string) error {
|
||||
if s.Status != "draft" {
|
||||
return NewValidationError("只有草稿状态的报告才能开始生成")
|
||||
}
|
||||
|
||||
s.Status = "generating"
|
||||
s.GeneratedBy = generatedBy
|
||||
now := time.Now()
|
||||
s.GeneratedAt = &now
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportGenerationStartedEvent{
|
||||
ReportID: s.ID,
|
||||
GeneratedBy: generatedBy,
|
||||
StartedAt: now,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteGeneration 完成报告生成
|
||||
func (s *StatisticsReport) CompleteGeneration(content string) error {
|
||||
if s.Status != "generating" {
|
||||
return NewValidationError("只有生成中状态的报告才能完成生成")
|
||||
}
|
||||
|
||||
s.Status = "completed"
|
||||
s.Content = content
|
||||
|
||||
// 设置过期时间(默认7天)
|
||||
expiresAt := time.Now().Add(7 * 24 * time.Hour)
|
||||
s.ExpiresAt = &expiresAt
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportCompletedEvent{
|
||||
ReportID: s.ID,
|
||||
CompletedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailGeneration 报告生成失败
|
||||
func (s *StatisticsReport) FailGeneration(reason string) error {
|
||||
if s.Status != "generating" {
|
||||
return NewValidationError("只有生成中状态的报告才能标记为失败")
|
||||
}
|
||||
|
||||
s.Status = "failed"
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportFailedEvent{
|
||||
ReportID: s.ID,
|
||||
Reason: reason,
|
||||
FailedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpired 检查报告是否已过期
|
||||
func (s *StatisticsReport) IsExpired() bool {
|
||||
if s.ExpiresAt == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().After(*s.ExpiresAt)
|
||||
}
|
||||
|
||||
// MarkAsExpired 标记报告为过期
|
||||
func (s *StatisticsReport) MarkAsExpired() error {
|
||||
if s.Status != "completed" {
|
||||
return NewValidationError("只有已完成状态的报告才能标记为过期")
|
||||
}
|
||||
|
||||
s.Status = "expired"
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportExpiredEvent{
|
||||
ReportID: s.ID,
|
||||
ExpiredAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanBeRegenerated 检查报告是否可以重新生成
|
||||
func (s *StatisticsReport) CanBeRegenerated() bool {
|
||||
return s.Status == "failed" || s.Status == "expired"
|
||||
}
|
||||
|
||||
// ================ 领域事件管理 ================
|
||||
|
||||
// addDomainEvent 添加领域事件
|
||||
func (s *StatisticsReport) addDomainEvent(event interface{}) {
|
||||
if s.domainEvents == nil {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
s.domainEvents = append(s.domainEvents, event)
|
||||
}
|
||||
|
||||
// GetDomainEvents 获取领域事件
|
||||
func (s *StatisticsReport) GetDomainEvents() []interface{} {
|
||||
return s.domainEvents
|
||||
}
|
||||
|
||||
// ClearDomainEvents 清除领域事件
|
||||
func (s *StatisticsReport) ClearDomainEvents() {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
|
||||
// ================ 领域事件定义 ================
|
||||
|
||||
// StatisticsReportCreatedEvent 统计报告创建事件
|
||||
type StatisticsReportCreatedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
ReportType string `json:"report_type"`
|
||||
Title string `json:"title"`
|
||||
Period string `json:"period"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportGenerationStartedEvent 统计报告生成开始事件
|
||||
type StatisticsReportGenerationStartedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
GeneratedBy string `json:"generated_by"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportCompletedEvent 统计报告完成事件
|
||||
type StatisticsReportCompletedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportFailedEvent 统计报告失败事件
|
||||
type StatisticsReportFailedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
Reason string `json:"reason"`
|
||||
FailedAt time.Time `json:"failed_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportExpiredEvent 统计报告过期事件
|
||||
type StatisticsReportExpiredEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
ExpiredAt time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
572
internal/domains/statistics/events/statistics_events.go
Normal file
572
internal/domains/statistics/events/statistics_events.go
Normal file
@@ -0,0 +1,572 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// StatisticsEventType 统计事件类型
|
||||
type StatisticsEventType string
|
||||
|
||||
const (
|
||||
// 指标相关事件
|
||||
MetricCreatedEventType StatisticsEventType = "statistics.metric.created"
|
||||
MetricUpdatedEventType StatisticsEventType = "statistics.metric.updated"
|
||||
MetricAggregatedEventType StatisticsEventType = "statistics.metric.aggregated"
|
||||
|
||||
// 报告相关事件
|
||||
ReportCreatedEventType StatisticsEventType = "statistics.report.created"
|
||||
ReportGenerationStartedEventType StatisticsEventType = "statistics.report.generation_started"
|
||||
ReportCompletedEventType StatisticsEventType = "statistics.report.completed"
|
||||
ReportFailedEventType StatisticsEventType = "statistics.report.failed"
|
||||
ReportExpiredEventType StatisticsEventType = "statistics.report.expired"
|
||||
|
||||
// 仪表板相关事件
|
||||
DashboardCreatedEventType StatisticsEventType = "statistics.dashboard.created"
|
||||
DashboardUpdatedEventType StatisticsEventType = "statistics.dashboard.updated"
|
||||
DashboardActivatedEventType StatisticsEventType = "statistics.dashboard.activated"
|
||||
DashboardDeactivatedEventType StatisticsEventType = "statistics.dashboard.deactivated"
|
||||
)
|
||||
|
||||
// BaseStatisticsEvent 统计事件基础结构
|
||||
type BaseStatisticsEvent struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Source string `json:"source"`
|
||||
AggregateID string `json:"aggregate_id"`
|
||||
AggregateType string `json:"aggregate_type"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Payload interface{} `json:"payload"`
|
||||
|
||||
// DDD特有字段
|
||||
DomainVersion string `json:"domain_version"`
|
||||
CausationID string `json:"causation_id"`
|
||||
CorrelationID string `json:"correlation_id"`
|
||||
}
|
||||
|
||||
// 实现 Event 接口
|
||||
func (e *BaseStatisticsEvent) GetID() string {
|
||||
return e.ID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetType() string {
|
||||
return e.Type
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetVersion() string {
|
||||
return e.Version
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetTimestamp() time.Time {
|
||||
return e.Timestamp
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetPayload() interface{} {
|
||||
return e.Payload
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetMetadata() map[string]interface{} {
|
||||
return e.Metadata
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetSource() string {
|
||||
return e.Source
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetAggregateID() string {
|
||||
return e.AggregateID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetAggregateType() string {
|
||||
return e.AggregateType
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetDomainVersion() string {
|
||||
return e.DomainVersion
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetCausationID() string {
|
||||
return e.CausationID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetCorrelationID() string {
|
||||
return e.CorrelationID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) Marshal() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) Unmarshal(data []byte) error {
|
||||
return json.Unmarshal(data, e)
|
||||
}
|
||||
|
||||
// ================ 指标相关事件 ================
|
||||
|
||||
// MetricCreatedEvent 指标创建事件
|
||||
type MetricCreatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
MetricID string `json:"metric_id"`
|
||||
MetricType string `json:"metric_type"`
|
||||
MetricName string `json:"metric_name"`
|
||||
Value float64 `json:"value"`
|
||||
Dimension string `json:"dimension"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
func NewMetricCreatedEvent(metricID, metricType, metricName, dimension string, value float64, date time.Time, correlationID string) *MetricCreatedEvent {
|
||||
return &MetricCreatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(MetricCreatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: metricID,
|
||||
AggregateType: "StatisticsMetric",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"metric_id": metricID,
|
||||
"metric_type": metricType,
|
||||
"metric_name": metricName,
|
||||
"dimension": dimension,
|
||||
},
|
||||
},
|
||||
MetricID: metricID,
|
||||
MetricType: metricType,
|
||||
MetricName: metricName,
|
||||
Value: value,
|
||||
Dimension: dimension,
|
||||
Date: date,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MetricCreatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"metric_id": e.MetricID,
|
||||
"metric_type": e.MetricType,
|
||||
"metric_name": e.MetricName,
|
||||
"value": e.Value,
|
||||
"dimension": e.Dimension,
|
||||
"date": e.Date,
|
||||
}
|
||||
}
|
||||
|
||||
// MetricUpdatedEvent 指标更新事件
|
||||
type MetricUpdatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
MetricID string `json:"metric_id"`
|
||||
OldValue float64 `json:"old_value"`
|
||||
NewValue float64 `json:"new_value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func NewMetricUpdatedEvent(metricID string, oldValue, newValue float64, correlationID string) *MetricUpdatedEvent {
|
||||
return &MetricUpdatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(MetricUpdatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: metricID,
|
||||
AggregateType: "StatisticsMetric",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"metric_id": metricID,
|
||||
},
|
||||
},
|
||||
MetricID: metricID,
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MetricUpdatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"metric_id": e.MetricID,
|
||||
"old_value": e.OldValue,
|
||||
"new_value": e.NewValue,
|
||||
"updated_at": e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// MetricAggregatedEvent 指标聚合事件
|
||||
type MetricAggregatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
MetricType string `json:"metric_type"`
|
||||
Dimension string `json:"dimension"`
|
||||
AggregatedAt time.Time `json:"aggregated_at"`
|
||||
RecordCount int `json:"record_count"`
|
||||
TotalValue float64 `json:"total_value"`
|
||||
}
|
||||
|
||||
func NewMetricAggregatedEvent(metricType, dimension string, recordCount int, totalValue float64, correlationID string) *MetricAggregatedEvent {
|
||||
return &MetricAggregatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(MetricAggregatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: uuid.New().String(),
|
||||
AggregateType: "StatisticsMetric",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"metric_type": metricType,
|
||||
"dimension": dimension,
|
||||
},
|
||||
},
|
||||
MetricType: metricType,
|
||||
Dimension: dimension,
|
||||
AggregatedAt: time.Now(),
|
||||
RecordCount: recordCount,
|
||||
TotalValue: totalValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MetricAggregatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"metric_type": e.MetricType,
|
||||
"dimension": e.Dimension,
|
||||
"aggregated_at": e.AggregatedAt,
|
||||
"record_count": e.RecordCount,
|
||||
"total_value": e.TotalValue,
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 报告相关事件 ================
|
||||
|
||||
// ReportCreatedEvent 报告创建事件
|
||||
type ReportCreatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
ReportType string `json:"report_type"`
|
||||
Title string `json:"title"`
|
||||
Period string `json:"period"`
|
||||
UserRole string `json:"user_role"`
|
||||
}
|
||||
|
||||
func NewReportCreatedEvent(reportID, reportType, title, period, userRole, correlationID string) *ReportCreatedEvent {
|
||||
return &ReportCreatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportCreatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
"report_type": reportType,
|
||||
"user_role": userRole,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
ReportType: reportType,
|
||||
Title: title,
|
||||
Period: period,
|
||||
UserRole: userRole,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportCreatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"report_type": e.ReportType,
|
||||
"title": e.Title,
|
||||
"period": e.Period,
|
||||
"user_role": e.UserRole,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportGenerationStartedEvent 报告生成开始事件
|
||||
type ReportGenerationStartedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
GeneratedBy string `json:"generated_by"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
func NewReportGenerationStartedEvent(reportID, generatedBy, correlationID string) *ReportGenerationStartedEvent {
|
||||
return &ReportGenerationStartedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportGenerationStartedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
"generated_by": generatedBy,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
GeneratedBy: generatedBy,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportGenerationStartedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"generated_by": e.GeneratedBy,
|
||||
"started_at": e.StartedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportCompletedEvent 报告完成事件
|
||||
type ReportCompletedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
ContentSize int `json:"content_size"`
|
||||
}
|
||||
|
||||
func NewReportCompletedEvent(reportID string, contentSize int, correlationID string) *ReportCompletedEvent {
|
||||
return &ReportCompletedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportCompletedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
CompletedAt: time.Now(),
|
||||
ContentSize: contentSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportCompletedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"completed_at": e.CompletedAt,
|
||||
"content_size": e.ContentSize,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportFailedEvent 报告失败事件
|
||||
type ReportFailedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
Reason string `json:"reason"`
|
||||
FailedAt time.Time `json:"failed_at"`
|
||||
}
|
||||
|
||||
func NewReportFailedEvent(reportID, reason, correlationID string) *ReportFailedEvent {
|
||||
return &ReportFailedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportFailedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
Reason: reason,
|
||||
FailedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportFailedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"reason": e.Reason,
|
||||
"failed_at": e.FailedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 仪表板相关事件 ================
|
||||
|
||||
// DashboardCreatedEvent 仪表板创建事件
|
||||
type DashboardCreatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
Name string `json:"name"`
|
||||
UserRole string `json:"user_role"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
}
|
||||
|
||||
func NewDashboardCreatedEvent(dashboardID, name, userRole, createdBy, correlationID string) *DashboardCreatedEvent {
|
||||
return &DashboardCreatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardCreatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"user_role": userRole,
|
||||
"created_by": createdBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
Name: name,
|
||||
UserRole: userRole,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardCreatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"name": e.Name,
|
||||
"user_role": e.UserRole,
|
||||
"created_by": e.CreatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardUpdatedEvent 仪表板更新事件
|
||||
type DashboardUpdatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Changes map[string]interface{} `json:"changes"`
|
||||
}
|
||||
|
||||
func NewDashboardUpdatedEvent(dashboardID, updatedBy string, changes map[string]interface{}, correlationID string) *DashboardUpdatedEvent {
|
||||
return &DashboardUpdatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardUpdatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"updated_by": updatedBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
UpdatedBy: updatedBy,
|
||||
UpdatedAt: time.Now(),
|
||||
Changes: changes,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardUpdatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"updated_by": e.UpdatedBy,
|
||||
"updated_at": e.UpdatedAt,
|
||||
"changes": e.Changes,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardActivatedEvent 仪表板激活事件
|
||||
type DashboardActivatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
ActivatedBy string `json:"activated_by"`
|
||||
ActivatedAt time.Time `json:"activated_at"`
|
||||
}
|
||||
|
||||
func NewDashboardActivatedEvent(dashboardID, activatedBy, correlationID string) *DashboardActivatedEvent {
|
||||
return &DashboardActivatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardActivatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"activated_by": activatedBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
ActivatedBy: activatedBy,
|
||||
ActivatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardActivatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"activated_by": e.ActivatedBy,
|
||||
"activated_at": e.ActivatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardDeactivatedEvent 仪表板停用事件
|
||||
type DashboardDeactivatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
DeactivatedBy string `json:"deactivated_by"`
|
||||
DeactivatedAt time.Time `json:"deactivated_at"`
|
||||
}
|
||||
|
||||
func NewDashboardDeactivatedEvent(dashboardID, deactivatedBy, correlationID string) *DashboardDeactivatedEvent {
|
||||
return &DashboardDeactivatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardDeactivatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"deactivated_by": deactivatedBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
DeactivatedBy: deactivatedBy,
|
||||
DeactivatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardDeactivatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"deactivated_by": e.DeactivatedBy,
|
||||
"deactivated_at": e.DeactivatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatisticsQuery 统计查询对象
|
||||
type StatisticsQuery struct {
|
||||
// 基础查询条件
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
MetricName string `json:"metric_name" form:"metric_name"` // 指标名称
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 排序参数
|
||||
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
|
||||
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
|
||||
|
||||
// 过滤条件
|
||||
MinValue float64 `json:"min_value" form:"min_value"` // 最小值
|
||||
MaxValue float64 `json:"max_value" form:"max_value"` // 最大值
|
||||
|
||||
// 聚合参数
|
||||
AggregateBy string `json:"aggregate_by" form:"aggregate_by"` // 聚合维度 (hour/day/week/month)
|
||||
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
|
||||
}
|
||||
|
||||
// StatisticsReportQuery 统计报告查询对象
|
||||
type StatisticsReportQuery struct {
|
||||
// 基础查询条件
|
||||
ReportType string `json:"report_type" form:"report_type"` // 报告类型
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
Status string `json:"status" form:"status"` // 报告状态
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 排序参数
|
||||
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
|
||||
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
|
||||
|
||||
// 过滤条件
|
||||
GeneratedBy string `json:"generated_by" form:"generated_by"` // 生成者ID
|
||||
AccessLevel string `json:"access_level" form:"access_level"` // 访问级别
|
||||
}
|
||||
|
||||
// StatisticsDashboardQuery 统计仪表板查询对象
|
||||
type StatisticsDashboardQuery struct {
|
||||
// 基础查询条件
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
IsDefault *bool `json:"is_default" form:"is_default"` // 是否默认
|
||||
IsActive *bool `json:"is_active" form:"is_active"` // 是否激活
|
||||
AccessLevel string `json:"access_level" form:"access_level"` // 访问级别
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 排序参数
|
||||
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
|
||||
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
|
||||
|
||||
// 过滤条件
|
||||
CreatedBy string `json:"created_by" form:"created_by"` // 创建者ID
|
||||
Name string `json:"name" form:"name"` // 仪表板名称
|
||||
}
|
||||
|
||||
// RealtimeStatisticsQuery 实时统计查询对象
|
||||
type RealtimeStatisticsQuery struct {
|
||||
// 查询条件
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
TimeRange string `json:"time_range" form:"time_range"` // 时间范围 (last_hour/last_day/last_week)
|
||||
|
||||
// 过滤条件
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
}
|
||||
|
||||
// HistoricalStatisticsQuery 历史统计查询对象
|
||||
type HistoricalStatisticsQuery struct {
|
||||
// 查询条件
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 聚合参数
|
||||
AggregateBy string `json:"aggregate_by" form:"aggregate_by"` // 聚合维度
|
||||
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
|
||||
|
||||
// 过滤条件
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
MinValue float64 `json:"min_value" form:"min_value"` // 最小值
|
||||
MaxValue float64 `json:"max_value" form:"max_value"` // 最大值
|
||||
}
|
||||
|
||||
// DashboardDataQuery 仪表板数据查询对象
|
||||
type DashboardDataQuery struct {
|
||||
// 查询条件
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
|
||||
// 时间范围
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 过滤条件
|
||||
MetricTypes []string `json:"metric_types" form:"metric_types"` // 指标类型列表
|
||||
Dimensions []string `json:"dimensions" form:"dimensions"` // 统计维度列表
|
||||
}
|
||||
|
||||
// ReportGenerationQuery 报告生成查询对象
|
||||
type ReportGenerationQuery struct {
|
||||
// 报告配置
|
||||
ReportType string `json:"report_type" form:"report_type"` // 报告类型
|
||||
Title string `json:"title" form:"title"` // 报告标题
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
|
||||
// 时间范围
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 过滤条件
|
||||
Filters map[string]interface{} `json:"filters" form:"filters"` // 过滤条件
|
||||
|
||||
// 生成配置
|
||||
GeneratedBy string `json:"generated_by" form:"generated_by"` // 生成者ID
|
||||
Format string `json:"format" form:"format"` // 输出格式 (json/pdf/excel)
|
||||
}
|
||||
|
||||
// ExportQuery 导出查询对象
|
||||
type ExportQuery struct {
|
||||
// 导出配置
|
||||
Format string `json:"format" form:"format"` // 导出格式 (excel/csv/pdf)
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
|
||||
// 时间范围
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 过滤条件
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
|
||||
|
||||
// 导出配置
|
||||
IncludeCharts bool `json:"include_charts" form:"include_charts"` // 是否包含图表
|
||||
Columns []string `json:"columns" form:"columns"` // 导出列
|
||||
}
|
||||
|
||||
// Validate 验证统计查询对象
|
||||
func (q *StatisticsQuery) Validate() error {
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.StartDate.IsZero() || q.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if q.StartDate.After(q.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
|
||||
q.SortOrder = "desc" // 默认降序
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证统计报告查询对象
|
||||
func (q *StatisticsReportQuery) Validate() error {
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
|
||||
q.SortOrder = "desc" // 默认降序
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证统计仪表板查询对象
|
||||
func (q *StatisticsDashboardQuery) Validate() error {
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
|
||||
q.SortOrder = "desc" // 默认降序
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证实时统计查询对象
|
||||
func (q *RealtimeStatisticsQuery) Validate() error {
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.TimeRange == "" {
|
||||
q.TimeRange = "last_hour" // 默认最近1小时
|
||||
}
|
||||
validTimeRanges := []string{"last_hour", "last_day", "last_week"}
|
||||
for _, validRange := range validTimeRanges {
|
||||
if q.TimeRange == validRange {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("无效的时间范围: %s", q.TimeRange)
|
||||
}
|
||||
|
||||
// Validate 验证历史统计查询对象
|
||||
func (q *HistoricalStatisticsQuery) Validate() error {
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.StartDate.IsZero() || q.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if q.StartDate.After(q.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证仪表板数据查询对象
|
||||
func (q *DashboardDataQuery) Validate() error {
|
||||
if q.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if q.Period == "" {
|
||||
q.Period = "today" // 默认今天
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证报告生成查询对象
|
||||
func (q *ReportGenerationQuery) Validate() error {
|
||||
if q.ReportType == "" {
|
||||
return fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
if q.Title == "" {
|
||||
return fmt.Errorf("报告标题不能为空")
|
||||
}
|
||||
if q.Period == "" {
|
||||
return fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
if q.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证导出查询对象
|
||||
func (q *ExportQuery) Validate() error {
|
||||
if q.Format == "" {
|
||||
return fmt.Errorf("导出格式不能为空")
|
||||
}
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.StartDate.IsZero() || q.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if q.StartDate.After(q.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
validFormats := []string{"excel", "csv", "pdf"}
|
||||
for _, validFormat := range validFormats {
|
||||
if q.Format == validFormat {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("无效的导出格式: %s", q.Format)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
)
|
||||
|
||||
// StatisticsRepository 统计指标仓储接口
|
||||
type StatisticsRepository interface {
|
||||
// 基础CRUD操作
|
||||
Save(ctx context.Context, metric *entities.StatisticsMetric) error
|
||||
FindByID(ctx context.Context, id string) (*entities.StatisticsMetric, error)
|
||||
FindByType(ctx context.Context, metricType string, limit, offset int) ([]*entities.StatisticsMetric, error)
|
||||
Update(ctx context.Context, metric *entities.StatisticsMetric) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// 按类型和日期范围查询
|
||||
FindByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
FindByTypeDimensionAndDateRange(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
FindByTypeNameAndDateRange(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
|
||||
// 聚合查询
|
||||
GetAggregatedMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) (map[string]float64, error)
|
||||
GetMetricsByDimension(ctx context.Context, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
|
||||
// 统计查询
|
||||
CountByType(ctx context.Context, metricType string) (int64, error)
|
||||
CountByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) (int64, error)
|
||||
|
||||
// 批量操作
|
||||
BatchSave(ctx context.Context, metrics []*entities.StatisticsMetric) error
|
||||
BatchDelete(ctx context.Context, ids []string) error
|
||||
|
||||
// 清理操作
|
||||
DeleteByDateRange(ctx context.Context, startDate, endDate time.Time) error
|
||||
DeleteByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) error
|
||||
}
|
||||
|
||||
// StatisticsReportRepository 统计报告仓储接口
|
||||
type StatisticsReportRepository interface {
|
||||
// 基础CRUD操作
|
||||
Save(ctx context.Context, report *entities.StatisticsReport) error
|
||||
FindByID(ctx context.Context, id string) (*entities.StatisticsReport, error)
|
||||
FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
FindByStatus(ctx context.Context, status string) ([]*entities.StatisticsReport, error)
|
||||
Update(ctx context.Context, report *entities.StatisticsReport) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// 按类型查询
|
||||
FindByType(ctx context.Context, reportType string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
FindByTypeAndPeriod(ctx context.Context, reportType, period string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
|
||||
// 按日期范围查询
|
||||
FindByDateRange(ctx context.Context, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
FindByUserAndDateRange(ctx context.Context, userID string, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
|
||||
// 统计查询
|
||||
CountByUser(ctx context.Context, userID string) (int64, error)
|
||||
CountByType(ctx context.Context, reportType string) (int64, error)
|
||||
CountByStatus(ctx context.Context, status string) (int64, error)
|
||||
|
||||
// 批量操作
|
||||
BatchSave(ctx context.Context, reports []*entities.StatisticsReport) error
|
||||
BatchDelete(ctx context.Context, ids []string) error
|
||||
|
||||
// 清理操作
|
||||
DeleteExpiredReports(ctx context.Context, expiredBefore time.Time) error
|
||||
DeleteByStatus(ctx context.Context, status string) error
|
||||
}
|
||||
|
||||
// StatisticsDashboardRepository 统计仪表板仓储接口
|
||||
type StatisticsDashboardRepository interface {
|
||||
// 基础CRUD操作
|
||||
Save(ctx context.Context, dashboard *entities.StatisticsDashboard) error
|
||||
FindByID(ctx context.Context, id string) (*entities.StatisticsDashboard, error)
|
||||
FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
FindByUserRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
Update(ctx context.Context, dashboard *entities.StatisticsDashboard) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// 按角色查询
|
||||
FindByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
FindDefaultByRole(ctx context.Context, userRole string) (*entities.StatisticsDashboard, error)
|
||||
FindActiveByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
|
||||
// 按状态查询
|
||||
FindByStatus(ctx context.Context, isActive bool, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
FindByAccessLevel(ctx context.Context, accessLevel string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
|
||||
// 统计查询
|
||||
CountByUser(ctx context.Context, userID string) (int64, error)
|
||||
CountByRole(ctx context.Context, userRole string) (int64, error)
|
||||
CountByStatus(ctx context.Context, isActive bool) (int64, error)
|
||||
|
||||
// 批量操作
|
||||
BatchSave(ctx context.Context, dashboards []*entities.StatisticsDashboard) error
|
||||
BatchDelete(ctx context.Context, ids []string) error
|
||||
|
||||
// 特殊操作
|
||||
SetDefaultDashboard(ctx context.Context, dashboardID string) error
|
||||
RemoveDefaultDashboard(ctx context.Context, userRole string) error
|
||||
ActivateDashboard(ctx context.Context, dashboardID string) error
|
||||
DeactivateDashboard(ctx context.Context, dashboardID string) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// StatisticsAggregateService 统计聚合服务接口
|
||||
// 负责统计数据的聚合和计算
|
||||
type StatisticsAggregateService interface {
|
||||
// 实时统计
|
||||
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
|
||||
GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error)
|
||||
|
||||
// 历史统计聚合
|
||||
AggregateHourlyMetrics(ctx context.Context, date time.Time) error
|
||||
AggregateDailyMetrics(ctx context.Context, date time.Time) error
|
||||
AggregateWeeklyMetrics(ctx context.Context, date time.Time) error
|
||||
AggregateMonthlyMetrics(ctx context.Context, date time.Time) error
|
||||
|
||||
// 统计查询
|
||||
GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
|
||||
// 统计计算
|
||||
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
|
||||
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
|
||||
}
|
||||
|
||||
// StatisticsAggregateServiceImpl 统计聚合服务实现
|
||||
type StatisticsAggregateServiceImpl struct {
|
||||
metricRepo repositories.StatisticsRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsAggregateService 创建统计聚合服务
|
||||
func NewStatisticsAggregateService(
|
||||
metricRepo repositories.StatisticsRepository,
|
||||
logger *zap.Logger,
|
||||
) StatisticsAggregateService {
|
||||
return &StatisticsAggregateServiceImpl{
|
||||
metricRepo: metricRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateRealtimeMetric 更新实时统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error {
|
||||
if metricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if metricName == "" {
|
||||
return fmt.Errorf("指标名称不能为空")
|
||||
}
|
||||
|
||||
// 创建或更新实时指标
|
||||
metric, err := entities.NewStatisticsMetric(metricType, metricName, "realtime", value, time.Now())
|
||||
if err != nil {
|
||||
s.logger.Error("创建统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("创建统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
err = s.metricRepo.Save(ctx, metric)
|
||||
if err != nil {
|
||||
s.logger.Error("保存统计指标失败",
|
||||
zap.String("metric_id", metric.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("实时统计指标更新成功",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("value", value))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRealtimeMetrics 获取实时统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
// 获取今天的实时指标
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("查询实时统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询实时统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为map格式
|
||||
result := make(map[string]float64)
|
||||
for _, metric := range metrics {
|
||||
if metric.Dimension == "realtime" {
|
||||
result[metric.MetricName] = metric.Value
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AggregateHourlyMetrics 聚合小时级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateHourlyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合小时级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定小时的所有实时指标
|
||||
startTime := date.Truncate(time.Hour)
|
||||
endTime := startTime.Add(time.Hour)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "hourly")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合小时级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合小时级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("小时级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AggregateDailyMetrics 聚合日级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateDailyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合日级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定日期的所有小时级指标
|
||||
startTime := date.Truncate(24 * time.Hour)
|
||||
endTime := startTime.Add(24 * time.Hour)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "daily")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合日级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合日级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("日级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AggregateWeeklyMetrics 聚合周级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateWeeklyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合周级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定周的所有日级指标
|
||||
startTime := date.Truncate(24 * time.Hour)
|
||||
endTime := startTime.Add(7 * 24 * time.Hour)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "weekly")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合周级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合周级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("周级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AggregateMonthlyMetrics 聚合月级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateMonthlyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合月级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定月的所有日级指标
|
||||
startTime := date.Truncate(24 * time.Hour)
|
||||
endTime := startTime.AddDate(0, 1, 0)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "monthly")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合月级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合月级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("月级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetricsByType 根据类型获取统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// GetMetricsByDimension 根据维度获取统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if dimension == "" {
|
||||
return nil, fmt.Errorf("统计维度不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, dimension, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("dimension", dimension),
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// CalculateGrowthRate 计算增长率
|
||||
func (s *StatisticsAggregateServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取当前周期的指标值
|
||||
currentMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询当前周期指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取上一周期的指标值
|
||||
previousMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询上一周期指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 计算总值
|
||||
var currentValue, previousValue float64
|
||||
for _, metric := range currentMetrics {
|
||||
currentValue += metric.Value
|
||||
}
|
||||
for _, metric := range previousMetrics {
|
||||
previousValue += metric.Value
|
||||
}
|
||||
|
||||
// 计算增长率
|
||||
if previousValue == 0 {
|
||||
if currentValue > 0 {
|
||||
return 100, nil // 从0增长到正数,增长率为100%
|
||||
}
|
||||
return 0, nil // 都是0,增长率为0%
|
||||
}
|
||||
|
||||
growthRate := ((currentValue - previousValue) / previousValue) * 100
|
||||
return growthRate, nil
|
||||
}
|
||||
|
||||
// CalculateTrend 计算趋势
|
||||
func (s *StatisticsAggregateServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return "", fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取时间范围内的指标
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < 2 {
|
||||
return "insufficient_data", nil // 数据不足
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sortMetricsByDate(metrics)
|
||||
|
||||
// 计算趋势
|
||||
firstValue := metrics[0].Value
|
||||
lastValue := metrics[len(metrics)-1].Value
|
||||
|
||||
if lastValue > firstValue {
|
||||
return "increasing", nil // 上升趋势
|
||||
} else if lastValue < firstValue {
|
||||
return "decreasing", nil // 下降趋势
|
||||
} else {
|
||||
return "stable", nil // 稳定趋势
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateMetricsByType 按类型聚合指标
|
||||
func (s *StatisticsAggregateServiceImpl) aggregateMetricsByType(ctx context.Context, metricType string, startTime, endTime time.Time, dimension string) error {
|
||||
// 获取源数据(实时或小时级数据)
|
||||
sourceDimension := "realtime"
|
||||
if dimension == "daily" {
|
||||
sourceDimension = "hourly"
|
||||
} else if dimension == "weekly" || dimension == "monthly" {
|
||||
sourceDimension = "daily"
|
||||
}
|
||||
|
||||
// 查询源数据
|
||||
sourceMetrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, sourceDimension, startTime, endTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询源数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 按指标名称分组聚合
|
||||
metricGroups := make(map[string][]*entities.StatisticsMetric)
|
||||
for _, metric := range sourceMetrics {
|
||||
metricGroups[metric.MetricName] = append(metricGroups[metric.MetricName], metric)
|
||||
}
|
||||
|
||||
// 聚合每个指标
|
||||
for metricName, metrics := range metricGroups {
|
||||
var totalValue float64
|
||||
for _, metric := range metrics {
|
||||
totalValue += metric.Value
|
||||
}
|
||||
|
||||
// 创建聚合后的指标
|
||||
aggregatedMetric, err := entities.NewStatisticsMetric(metricType, metricName, dimension, totalValue, startTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建聚合指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存聚合指标
|
||||
err = s.metricRepo.Save(ctx, aggregatedMetric)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存聚合指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sortMetricsByDate 按日期排序指标
|
||||
func sortMetricsByDate(metrics []*entities.StatisticsMetric) {
|
||||
// 简单的冒泡排序
|
||||
n := len(metrics)
|
||||
for i := 0; i < n-1; i++ {
|
||||
for j := 0; j < n-i-1; j++ {
|
||||
if metrics[j].Date.After(metrics[j+1].Date) {
|
||||
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// StatisticsCalculationService 统计计算服务接口
|
||||
// 负责各种统计计算和分析
|
||||
type StatisticsCalculationService interface {
|
||||
// 基础统计计算
|
||||
CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
|
||||
// 高级统计计算
|
||||
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
|
||||
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
|
||||
CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error)
|
||||
|
||||
// 业务指标计算
|
||||
CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
|
||||
// 时间序列分析
|
||||
CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error)
|
||||
CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error)
|
||||
}
|
||||
|
||||
// StatisticsCalculationServiceImpl 统计计算服务实现
|
||||
type StatisticsCalculationServiceImpl struct {
|
||||
metricRepo repositories.StatisticsRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsCalculationService 创建统计计算服务
|
||||
func NewStatisticsCalculationService(
|
||||
metricRepo repositories.StatisticsRepository,
|
||||
logger *zap.Logger,
|
||||
) StatisticsCalculationService {
|
||||
return &StatisticsCalculationServiceImpl{
|
||||
metricRepo: metricRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTotal 计算总值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, metric := range metrics {
|
||||
total += metric.Value
|
||||
}
|
||||
|
||||
s.logger.Info("计算总值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("total", total))
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// CalculateAverage 计算平均值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, metric := range metrics {
|
||||
total += metric.Value
|
||||
}
|
||||
|
||||
average := total / float64(len(metrics))
|
||||
|
||||
s.logger.Info("计算平均值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("average", average))
|
||||
|
||||
return average, nil
|
||||
}
|
||||
|
||||
// CalculateMax 计算最大值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
max := metrics[0].Value
|
||||
for _, metric := range metrics {
|
||||
if metric.Value > max {
|
||||
max = metric.Value
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("计算最大值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("max", max))
|
||||
|
||||
return max, nil
|
||||
}
|
||||
|
||||
// CalculateMin 计算最小值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
min := metrics[0].Value
|
||||
for _, metric := range metrics {
|
||||
if metric.Value < min {
|
||||
min = metric.Value
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("计算最小值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("min", min))
|
||||
|
||||
return min, nil
|
||||
}
|
||||
|
||||
// CalculateGrowthRate 计算增长率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取当前周期的总值
|
||||
currentTotal, err := s.CalculateTotal(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算当前周期总值失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取上一周期的总值
|
||||
previousTotal, err := s.CalculateTotal(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算上一周期总值失败: %w", err)
|
||||
}
|
||||
|
||||
// 计算增长率
|
||||
if previousTotal == 0 {
|
||||
if currentTotal > 0 {
|
||||
return 100, nil // 从0增长到正数,增长率为100%
|
||||
}
|
||||
return 0, nil // 都是0,增长率为0%
|
||||
}
|
||||
|
||||
growthRate := ((currentTotal - previousTotal) / previousTotal) * 100
|
||||
|
||||
s.logger.Info("计算增长率完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("growth_rate", growthRate))
|
||||
|
||||
return growthRate, nil
|
||||
}
|
||||
|
||||
// CalculateTrend 计算趋势
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return "", fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return "", fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < 2 {
|
||||
return "insufficient_data", nil // 数据不足
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sortMetricsByDateCalc(metrics)
|
||||
|
||||
// 计算趋势
|
||||
firstValue := metrics[0].Value
|
||||
lastValue := metrics[len(metrics)-1].Value
|
||||
|
||||
var trend string
|
||||
if lastValue > firstValue {
|
||||
trend = "increasing" // 上升趋势
|
||||
} else if lastValue < firstValue {
|
||||
trend = "decreasing" // 下降趋势
|
||||
} else {
|
||||
trend = "stable" // 稳定趋势
|
||||
}
|
||||
|
||||
s.logger.Info("计算趋势完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.String("trend", trend))
|
||||
|
||||
return trend, nil
|
||||
}
|
||||
|
||||
// CalculateCorrelation 计算相关性
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType1 == "" || metricName1 == "" || metricType2 == "" || metricName2 == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取两个指标的数据
|
||||
metrics1, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType1, metricName1, startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询指标1失败: %w", err)
|
||||
}
|
||||
|
||||
metrics2, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType2, metricName2, startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询指标2失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics1) != len(metrics2) || len(metrics1) < 2 {
|
||||
return 0, fmt.Errorf("数据点数量不足或不对称")
|
||||
}
|
||||
|
||||
// 计算皮尔逊相关系数
|
||||
correlation := s.calculatePearsonCorrelation(metrics1, metrics2)
|
||||
|
||||
s.logger.Info("计算相关性完成",
|
||||
zap.String("metric1", metricType1+"."+metricName1),
|
||||
zap.String("metric2", metricType2+"."+metricName2),
|
||||
zap.Float64("correlation", correlation))
|
||||
|
||||
return correlation, nil
|
||||
}
|
||||
|
||||
// CalculateSuccessRate 计算成功率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
// 获取成功调用次数
|
||||
successTotal, err := s.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算成功调用次数失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取总调用次数
|
||||
totalCalls, err := s.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算总调用次数失败: %w", err)
|
||||
}
|
||||
|
||||
if totalCalls == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
successRate := (successTotal / totalCalls) * 100
|
||||
|
||||
s.logger.Info("计算成功率完成",
|
||||
zap.Float64("success_rate", successRate))
|
||||
|
||||
return successRate, nil
|
||||
}
|
||||
|
||||
// CalculateConversionRate 计算转化率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
// 获取认证用户数
|
||||
certifiedUsers, err := s.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算认证用户数失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取总用户数
|
||||
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算总用户数失败: %w", err)
|
||||
}
|
||||
|
||||
if totalUsers == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
conversionRate := (certifiedUsers / totalUsers) * 100
|
||||
|
||||
s.logger.Info("计算转化率完成",
|
||||
zap.Float64("conversion_rate", conversionRate))
|
||||
|
||||
return conversionRate, nil
|
||||
}
|
||||
|
||||
// CalculateRetentionRate 计算留存率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
// 获取活跃用户数
|
||||
activeUsers, err := s.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算活跃用户数失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取总用户数
|
||||
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算总用户数失败: %w", err)
|
||||
}
|
||||
|
||||
if totalUsers == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
retentionRate := (activeUsers / totalUsers) * 100
|
||||
|
||||
s.logger.Info("计算留存率完成",
|
||||
zap.Float64("retention_rate", retentionRate))
|
||||
|
||||
return retentionRate, nil
|
||||
}
|
||||
|
||||
// CalculateMovingAverage 计算移动平均
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return nil, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
if windowSize <= 0 {
|
||||
return nil, fmt.Errorf("窗口大小必须大于0")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < windowSize {
|
||||
return nil, fmt.Errorf("数据点数量不足")
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sortMetricsByDateCalc(metrics)
|
||||
|
||||
// 计算移动平均
|
||||
var movingAverages []float64
|
||||
for i := windowSize - 1; i < len(metrics); i++ {
|
||||
var sum float64
|
||||
for j := i - windowSize + 1; j <= i; j++ {
|
||||
sum += metrics[j].Value
|
||||
}
|
||||
average := sum / float64(windowSize)
|
||||
movingAverages = append(movingAverages, average)
|
||||
}
|
||||
|
||||
s.logger.Info("计算移动平均完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Int("window_size", windowSize),
|
||||
zap.Int("result_count", len(movingAverages)))
|
||||
|
||||
return movingAverages, nil
|
||||
}
|
||||
|
||||
// CalculateSeasonality 计算季节性
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return nil, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < 7 {
|
||||
return nil, fmt.Errorf("数据点数量不足,至少需要7个数据点")
|
||||
}
|
||||
|
||||
// 按星期几分组
|
||||
weeklyAverages := make(map[string][]float64)
|
||||
for _, metric := range metrics {
|
||||
weekday := metric.Date.Weekday().String()
|
||||
weeklyAverages[weekday] = append(weeklyAverages[weekday], metric.Value)
|
||||
}
|
||||
|
||||
// 计算每个星期几的平均值
|
||||
seasonality := make(map[string]float64)
|
||||
for weekday, values := range weeklyAverages {
|
||||
var sum float64
|
||||
for _, value := range values {
|
||||
sum += value
|
||||
}
|
||||
seasonality[weekday] = sum / float64(len(values))
|
||||
}
|
||||
|
||||
s.logger.Info("计算季节性完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Int("weekday_count", len(seasonality)))
|
||||
|
||||
return seasonality, nil
|
||||
}
|
||||
|
||||
// calculatePearsonCorrelation 计算皮尔逊相关系数
|
||||
func (s *StatisticsCalculationServiceImpl) calculatePearsonCorrelation(metrics1, metrics2 []*entities.StatisticsMetric) float64 {
|
||||
n := len(metrics1)
|
||||
if n < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算均值
|
||||
var sum1, sum2 float64
|
||||
for i := 0; i < n; i++ {
|
||||
sum1 += metrics1[i].Value
|
||||
sum2 += metrics2[i].Value
|
||||
}
|
||||
mean1 := sum1 / float64(n)
|
||||
mean2 := sum2 / float64(n)
|
||||
|
||||
// 计算协方差和方差
|
||||
var numerator, denominator1, denominator2 float64
|
||||
for i := 0; i < n; i++ {
|
||||
diff1 := metrics1[i].Value - mean1
|
||||
diff2 := metrics2[i].Value - mean2
|
||||
numerator += diff1 * diff2
|
||||
denominator1 += diff1 * diff1
|
||||
denominator2 += diff2 * diff2
|
||||
}
|
||||
|
||||
// 计算相关系数
|
||||
if denominator1 == 0 || denominator2 == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
correlation := numerator / math.Sqrt(denominator1*denominator2)
|
||||
return correlation
|
||||
}
|
||||
|
||||
// sortMetricsByDateCalc 按日期排序指标
|
||||
func sortMetricsByDateCalc(metrics []*entities.StatisticsMetric) {
|
||||
// 简单的冒泡排序
|
||||
n := len(metrics)
|
||||
for i := 0; i < n-1; i++ {
|
||||
for j := 0; j < n-i-1; j++ {
|
||||
if metrics[j].Date.After(metrics[j+1].Date) {
|
||||
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// StatisticsReportService 报告生成服务接口
|
||||
// 负责统计报告的生成和管理
|
||||
type StatisticsReportService interface {
|
||||
// 报告生成
|
||||
GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error)
|
||||
GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error)
|
||||
GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error)
|
||||
|
||||
// 报告管理
|
||||
GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error)
|
||||
GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
DeleteReport(ctx context.Context, reportID string) error
|
||||
|
||||
// 报告状态管理
|
||||
StartReportGeneration(ctx context.Context, reportID, generatedBy string) error
|
||||
CompleteReportGeneration(ctx context.Context, reportID string, content string) error
|
||||
FailReportGeneration(ctx context.Context, reportID string, reason string) error
|
||||
|
||||
// 报告清理
|
||||
CleanupExpiredReports(ctx context.Context) error
|
||||
}
|
||||
|
||||
// StatisticsReportServiceImpl 报告生成服务实现
|
||||
type StatisticsReportServiceImpl struct {
|
||||
reportRepo repositories.StatisticsReportRepository
|
||||
metricRepo repositories.StatisticsRepository
|
||||
calcService StatisticsCalculationService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsReportService 创建报告生成服务
|
||||
func NewStatisticsReportService(
|
||||
reportRepo repositories.StatisticsReportRepository,
|
||||
metricRepo repositories.StatisticsRepository,
|
||||
calcService StatisticsCalculationService,
|
||||
logger *zap.Logger,
|
||||
) StatisticsReportService {
|
||||
return &StatisticsReportServiceImpl{
|
||||
reportRepo: reportRepo,
|
||||
metricRepo: metricRepo,
|
||||
calcService: calcService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateDashboardReport 生成仪表板报告
|
||||
func (s *StatisticsReportServiceImpl) GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error) {
|
||||
if userRole == "" {
|
||||
return nil, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if period == "" {
|
||||
return nil, fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
|
||||
// 创建报告实体
|
||||
title := fmt.Sprintf("%s仪表板报告 - %s", s.getRoleDisplayName(userRole), s.getPeriodDisplayName(period))
|
||||
report, err := entities.NewStatisticsReport("dashboard", title, period, userRole)
|
||||
if err != nil {
|
||||
s.logger.Error("创建仪表板报告失败",
|
||||
zap.String("user_role", userRole),
|
||||
zap.String("period", period),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("创建仪表板报告失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存报告
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存仪表板报告失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("保存仪表板报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("仪表板报告创建成功",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.String("user_role", userRole),
|
||||
zap.String("period", period))
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GenerateSummaryReport 生成汇总报告
|
||||
func (s *StatisticsReportServiceImpl) GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error) {
|
||||
if period == "" {
|
||||
return nil, fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
|
||||
// 创建报告实体
|
||||
title := fmt.Sprintf("汇总报告 - %s (%s 至 %s)",
|
||||
s.getPeriodDisplayName(period),
|
||||
startDate.Format("2006-01-02"),
|
||||
endDate.Format("2006-01-02"))
|
||||
report, err := entities.NewStatisticsReport("summary", title, period, "admin")
|
||||
if err != nil {
|
||||
s.logger.Error("创建汇总报告失败",
|
||||
zap.String("period", period),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("创建汇总报告失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成报告内容
|
||||
content, err := s.generateSummaryContent(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("生成汇总报告内容失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("生成汇总报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
// 完成报告生成
|
||||
err = report.CompleteGeneration(content)
|
||||
if err != nil {
|
||||
s.logger.Error("完成汇总报告生成失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("完成汇总报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存报告
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存汇总报告失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("保存汇总报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("汇总报告生成成功",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.String("period", period))
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GenerateDetailedReport 生成详细报告
|
||||
func (s *StatisticsReportServiceImpl) GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error) {
|
||||
if reportType == "" {
|
||||
return nil, fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
|
||||
// 创建报告实体
|
||||
title := fmt.Sprintf("详细报告 - %s (%s 至 %s)",
|
||||
reportType,
|
||||
startDate.Format("2006-01-02"),
|
||||
endDate.Format("2006-01-02"))
|
||||
report, err := entities.NewStatisticsReport("detailed", title, "custom", "admin")
|
||||
if err != nil {
|
||||
s.logger.Error("创建详细报告失败",
|
||||
zap.String("report_type", reportType),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("创建详细报告失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成报告内容
|
||||
content, err := s.generateDetailedContent(ctx, reportType, startDate, endDate, filters)
|
||||
if err != nil {
|
||||
s.logger.Error("生成详细报告内容失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("生成详细报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
// 完成报告生成
|
||||
err = report.CompleteGeneration(content)
|
||||
if err != nil {
|
||||
s.logger.Error("完成详细报告生成失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("完成详细报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存报告
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存详细报告失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("保存详细报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("详细报告生成成功",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.String("report_type", reportType))
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GetReport 获取报告
|
||||
func (s *StatisticsReportServiceImpl) GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error) {
|
||||
if reportID == "" {
|
||||
return nil, fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询报告失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GetReportsByUser 获取用户的报告列表
|
||||
func (s *StatisticsReportServiceImpl) GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error) {
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
reports, err := s.reportRepo.FindByUser(ctx, userID, limit, offset)
|
||||
if err != nil {
|
||||
s.logger.Error("查询用户报告失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询用户报告失败: %w", err)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// DeleteReport 删除报告
|
||||
func (s *StatisticsReportServiceImpl) DeleteReport(ctx context.Context, reportID string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
err := s.reportRepo.Delete(ctx, reportID)
|
||||
if err != nil {
|
||||
s.logger.Error("删除报告失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("删除报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告删除成功", zap.String("report_id", reportID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartReportGeneration 开始报告生成
|
||||
func (s *StatisticsReportServiceImpl) StartReportGeneration(ctx context.Context, reportID, generatedBy string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
if generatedBy == "" {
|
||||
return fmt.Errorf("生成者ID不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
err = report.StartGeneration(generatedBy)
|
||||
if err != nil {
|
||||
s.logger.Error("开始报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("开始报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存报告状态失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存报告状态失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告生成开始",
|
||||
zap.String("report_id", reportID),
|
||||
zap.String("generated_by", generatedBy))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteReportGeneration 完成报告生成
|
||||
func (s *StatisticsReportServiceImpl) CompleteReportGeneration(ctx context.Context, reportID string, content string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
if content == "" {
|
||||
return fmt.Errorf("报告内容不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
err = report.CompleteGeneration(content)
|
||||
if err != nil {
|
||||
s.logger.Error("完成报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("完成报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存报告内容失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告生成完成", zap.String("report_id", reportID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailReportGeneration 报告生成失败
|
||||
func (s *StatisticsReportServiceImpl) FailReportGeneration(ctx context.Context, reportID string, reason string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
err = report.FailGeneration(reason)
|
||||
if err != nil {
|
||||
s.logger.Error("标记报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("标记报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存报告状态失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存报告状态失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.String("reason", reason))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupExpiredReports 清理过期报告
|
||||
func (s *StatisticsReportServiceImpl) CleanupExpiredReports(ctx context.Context) error {
|
||||
s.logger.Info("开始清理过期报告")
|
||||
|
||||
// 获取所有已完成的报告
|
||||
reports, err := s.reportRepo.FindByStatus(ctx, "completed")
|
||||
if err != nil {
|
||||
s.logger.Error("查询已完成报告失败", zap.Error(err))
|
||||
return fmt.Errorf("查询已完成报告失败: %w", err)
|
||||
}
|
||||
|
||||
var deletedCount int
|
||||
for _, report := range reports {
|
||||
if report.IsExpired() {
|
||||
err = report.MarkAsExpired()
|
||||
if err != nil {
|
||||
s.logger.Error("标记报告过期失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存过期报告状态失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
deletedCount++
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("过期报告清理完成", zap.Int("deleted_count", deletedCount))
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSummaryContent 生成汇总报告内容
|
||||
func (s *StatisticsReportServiceImpl) generateSummaryContent(ctx context.Context, startDate, endDate time.Time) (string, error) {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// API调用统计
|
||||
apiCallsTotal, err := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算API调用总数失败", zap.Error(err))
|
||||
}
|
||||
apiCallsSuccess, err := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算API调用成功数失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 用户统计
|
||||
usersTotal, err := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算用户总数失败", zap.Error(err))
|
||||
}
|
||||
usersCertified, err := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算认证用户数失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 财务统计
|
||||
financeTotal, err := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算财务总额失败", zap.Error(err))
|
||||
}
|
||||
|
||||
content["api_calls"] = map[string]interface{}{
|
||||
"total": apiCallsTotal,
|
||||
"success": apiCallsSuccess,
|
||||
"rate": s.calculateRate(apiCallsSuccess, apiCallsTotal),
|
||||
}
|
||||
|
||||
content["users"] = map[string]interface{}{
|
||||
"total": usersTotal,
|
||||
"certified": usersCertified,
|
||||
"rate": s.calculateRate(usersCertified, usersTotal),
|
||||
}
|
||||
|
||||
content["finance"] = map[string]interface{}{
|
||||
"total_amount": financeTotal,
|
||||
}
|
||||
|
||||
content["period"] = map[string]interface{}{
|
||||
"start_date": startDate.Format("2006-01-02"),
|
||||
"end_date": endDate.Format("2006-01-02"),
|
||||
}
|
||||
|
||||
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
// 转换为JSON字符串
|
||||
jsonContent, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
return string(jsonContent), nil
|
||||
}
|
||||
|
||||
// generateDetailedContent 生成详细报告内容
|
||||
func (s *StatisticsReportServiceImpl) generateDetailedContent(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (string, error) {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 根据报告类型生成不同的内容
|
||||
switch reportType {
|
||||
case "api_calls":
|
||||
content = s.generateApiCallsDetailedContent(ctx, startDate, endDate, filters)
|
||||
case "users":
|
||||
content = s.generateUsersDetailedContent(ctx, startDate, endDate, filters)
|
||||
case "finance":
|
||||
content = s.generateFinanceDetailedContent(ctx, startDate, endDate, filters)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的报告类型: %s", reportType)
|
||||
}
|
||||
|
||||
content["report_type"] = reportType
|
||||
content["period"] = map[string]interface{}{
|
||||
"start_date": startDate.Format("2006-01-02"),
|
||||
"end_date": endDate.Format("2006-01-02"),
|
||||
}
|
||||
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
// 转换为JSON字符串
|
||||
jsonContent, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
return string(jsonContent), nil
|
||||
}
|
||||
|
||||
// generateApiCallsDetailedContent 生成API调用详细内容
|
||||
func (s *StatisticsReportServiceImpl) generateApiCallsDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 获取API调用统计数据
|
||||
totalCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
|
||||
successCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
|
||||
failedCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "failed_count", startDate, endDate)
|
||||
avgResponseTime, _ := s.calcService.CalculateAverage(ctx, "api_calls", "response_time", startDate, endDate)
|
||||
|
||||
content["total_calls"] = totalCalls
|
||||
content["success_calls"] = successCalls
|
||||
content["failed_calls"] = failedCalls
|
||||
content["success_rate"] = s.calculateRate(successCalls, totalCalls)
|
||||
content["avg_response_time"] = avgResponseTime
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// generateUsersDetailedContent 生成用户详细内容
|
||||
func (s *StatisticsReportServiceImpl) generateUsersDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 获取用户统计数据
|
||||
totalUsers, _ := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
certifiedUsers, _ := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
|
||||
activeUsers, _ := s.calcService.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
|
||||
|
||||
content["total_users"] = totalUsers
|
||||
content["certified_users"] = certifiedUsers
|
||||
content["active_users"] = activeUsers
|
||||
content["certification_rate"] = s.calculateRate(certifiedUsers, totalUsers)
|
||||
content["retention_rate"] = s.calculateRate(activeUsers, totalUsers)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// generateFinanceDetailedContent 生成财务详细内容
|
||||
func (s *StatisticsReportServiceImpl) generateFinanceDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 获取财务统计数据
|
||||
totalAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
|
||||
rechargeAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "recharge_amount", startDate, endDate)
|
||||
deductAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "deduct_amount", startDate, endDate)
|
||||
|
||||
content["total_amount"] = totalAmount
|
||||
content["recharge_amount"] = rechargeAmount
|
||||
content["deduct_amount"] = deductAmount
|
||||
content["net_amount"] = rechargeAmount - deductAmount
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// calculateRate 计算比率
|
||||
func (s *StatisticsReportServiceImpl) calculateRate(numerator, denominator float64) float64 {
|
||||
if denominator == 0 {
|
||||
return 0
|
||||
}
|
||||
return (numerator / denominator) * 100
|
||||
}
|
||||
|
||||
// getRoleDisplayName 获取角色显示名称
|
||||
func (s *StatisticsReportServiceImpl) getRoleDisplayName(role string) string {
|
||||
roleNames := map[string]string{
|
||||
"admin": "管理员",
|
||||
"user": "用户",
|
||||
"manager": "经理",
|
||||
"analyst": "分析师",
|
||||
}
|
||||
if name, exists := roleNames[role]; exists {
|
||||
return name
|
||||
}
|
||||
return role
|
||||
}
|
||||
|
||||
// getPeriodDisplayName 获取周期显示名称
|
||||
func (s *StatisticsReportServiceImpl) getPeriodDisplayName(period string) string {
|
||||
periodNames := map[string]string{
|
||||
"today": "今日",
|
||||
"week": "本周",
|
||||
"month": "本月",
|
||||
"quarter": "本季度",
|
||||
"year": "本年",
|
||||
}
|
||||
if name, exists := periodNames[period]; exists {
|
||||
return name
|
||||
}
|
||||
return period
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/repositories/queries"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
@@ -27,6 +28,7 @@ type UserRepository interface {
|
||||
|
||||
// 关联查询
|
||||
GetByIDWithEnterpriseInfo(ctx context.Context, id string) (entities.User, error)
|
||||
BatchGetByIDsWithEnterpriseInfo(ctx context.Context, ids []string) ([]*entities.User, error)
|
||||
|
||||
// 企业信息查询
|
||||
ExistsByUnifiedSocialCode(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error)
|
||||
@@ -46,6 +48,17 @@ type UserRepository interface {
|
||||
// 统计信息
|
||||
GetStats(ctx context.Context) (*UserStats, error)
|
||||
GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemUserStats(ctx context.Context) (*UserStats, error)
|
||||
GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*UserStats, error)
|
||||
GetSystemDailyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 排行榜查询方法
|
||||
GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
GetUserCallRankingByConsumption(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
GetRechargeRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// SMSCodeRepository 短信验证码仓储接口
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
"tyapi-server/internal/domains/api/repositories"
|
||||
@@ -228,6 +229,61 @@ func (r *GormApiCallRepository) CountByUserId(ctx context.Context, userId string
|
||||
return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ?", userId)
|
||||
}
|
||||
|
||||
// CountByUserIdAndDateRange 按用户ID和日期范围统计API调用次数
|
||||
func (r *GormApiCallRepository) CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error) {
|
||||
return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ? AND created_at >= ? AND created_at < ?", userId, startDate, endDate)
|
||||
}
|
||||
|
||||
// GetDailyStatsByUserId 获取用户每日API调用统计
|
||||
func (r *GormApiCallRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
// 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as calls
|
||||
FROM api_calls
|
||||
WHERE user_id = $1
|
||||
AND DATE(created_at) >= $2
|
||||
AND DATE(created_at) <= $3
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetMonthlyStatsByUserId 获取用户每月API调用统计
|
||||
func (r *GormApiCallRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
// 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
COUNT(*) as calls
|
||||
FROM api_calls
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
AND created_at <= $3
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *GormApiCallRepository) FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error) {
|
||||
var call entities.ApiCall
|
||||
err := r.FindOne(ctx, &call, "transaction_id = ?", transactionId)
|
||||
@@ -330,3 +386,134 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
|
||||
|
||||
return productNameMap, calls, total, nil
|
||||
}
|
||||
|
||||
// GetSystemTotalCalls 获取系统总API调用次数
|
||||
func (r *GormApiCallRepository) GetSystemTotalCalls(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(&entities.ApiCall{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetSystemCallsByDateRange 获取系统指定时间范围内的API调用次数
|
||||
func (r *GormApiCallRepository) GetSystemCallsByDateRange(ctx context.Context, startDate, endDate time.Time) (int64, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(&entities.ApiCall{}).
|
||||
Where("created_at >= ? AND created_at <= ?", startDate, endDate).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetSystemDailyStats 获取系统每日API调用统计
|
||||
func (r *GormApiCallRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as calls
|
||||
FROM api_calls
|
||||
WHERE DATE(created_at) >= $1
|
||||
AND DATE(created_at) <= $2
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemMonthlyStats 获取系统每月API调用统计
|
||||
func (r *GormApiCallRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
COUNT(*) as calls
|
||||
FROM api_calls
|
||||
WHERE created_at >= $1
|
||||
AND created_at <= $2
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetApiPopularityRanking 获取API受欢迎程度排行榜
|
||||
func (r *GormApiCallRepository) GetApiPopularityRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
var args []interface{}
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
sql = `
|
||||
SELECT
|
||||
p.id as product_id,
|
||||
p.name as api_name,
|
||||
p.description as api_description,
|
||||
COUNT(ac.id) as call_count
|
||||
FROM product p
|
||||
LEFT JOIN api_calls ac ON p.id = ac.product_id
|
||||
AND DATE(ac.created_at) = CURRENT_DATE
|
||||
WHERE p.deleted_at IS NULL
|
||||
GROUP BY p.id, p.name, p.description
|
||||
HAVING COUNT(ac.id) > 0
|
||||
ORDER BY call_count DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "month":
|
||||
sql = `
|
||||
SELECT
|
||||
p.id as product_id,
|
||||
p.name as api_name,
|
||||
p.description as api_description,
|
||||
COUNT(ac.id) as call_count
|
||||
FROM product p
|
||||
LEFT JOIN api_calls ac ON p.id = ac.product_id
|
||||
AND DATE_TRUNC('month', ac.created_at) = DATE_TRUNC('month', CURRENT_DATE)
|
||||
WHERE p.deleted_at IS NULL
|
||||
GROUP BY p.id, p.name, p.description
|
||||
HAVING COUNT(ac.id) > 0
|
||||
ORDER BY call_count DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "total":
|
||||
sql = `
|
||||
SELECT
|
||||
p.id as product_id,
|
||||
p.name as api_name,
|
||||
p.description as api_description,
|
||||
COUNT(ac.id) as call_count
|
||||
FROM product p
|
||||
LEFT JOIN api_calls ac ON p.id = ac.product_id
|
||||
WHERE p.deleted_at IS NULL
|
||||
GROUP BY p.id, p.name, p.description
|
||||
HAVING COUNT(ac.id) > 0
|
||||
ORDER BY call_count DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的时间周期: %s", period)
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package repositories
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
@@ -110,9 +112,16 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa
|
||||
|
||||
if options.Filters != nil {
|
||||
for key, value := range options.Filters {
|
||||
// 特殊处理 user_ids 过滤器
|
||||
if key == "user_ids" {
|
||||
if userIds, ok := value.(string); ok && userIds != "" {
|
||||
query = query.Where("user_id IN ?", strings.Split(userIds, ","))
|
||||
}
|
||||
} else {
|
||||
query = query.Where(key+" = ?", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.Search != "" {
|
||||
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
|
||||
@@ -176,3 +185,143 @@ func (r *GormRechargeRecordRepository) SoftDelete(ctx context.Context, id string
|
||||
func (r *GormRechargeRecordRepository) Restore(ctx context.Context, id string) error {
|
||||
return r.RestoreEntity(ctx, id, &entities.RechargeRecord{})
|
||||
}
|
||||
|
||||
// GetTotalAmountByUserId 获取用户总充值金额
|
||||
func (r *GormRechargeRecordRepository) GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.RechargeRecord{}).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Where("user_id = ? AND status = ?", userId, entities.RechargeStatusSuccess).
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetTotalAmountByUserIdAndDateRange 按用户ID和日期范围获取总充值金额
|
||||
func (r *GormRechargeRecordRepository) GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.RechargeRecord{}).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Where("user_id = ? AND status = ? AND created_at >= ? AND created_at < ?", userId, entities.RechargeStatusSuccess, startDate, endDate).
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetDailyStatsByUserId 获取用户每日充值统计
|
||||
func (r *GormRechargeRecordRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
// 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM recharge_records
|
||||
WHERE user_id = $1
|
||||
AND status = $2
|
||||
AND DATE(created_at) >= $3
|
||||
AND DATE(created_at) <= $4
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, userId, entities.RechargeStatusSuccess, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetMonthlyStatsByUserId 获取用户每月充值统计
|
||||
func (r *GormRechargeRecordRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
// 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM recharge_records
|
||||
WHERE user_id = $1
|
||||
AND status = $2
|
||||
AND created_at >= $3
|
||||
AND created_at <= $4
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, userId, entities.RechargeStatusSuccess, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemTotalAmount 获取系统总充值金额
|
||||
func (r *GormRechargeRecordRepository) GetSystemTotalAmount(ctx context.Context) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.RechargeRecord{}).
|
||||
Where("status = ?", entities.RechargeStatusSuccess).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetSystemAmountByDateRange 获取系统指定时间范围内的充值金额
|
||||
func (r *GormRechargeRecordRepository) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.RechargeRecord{}).
|
||||
Where("status = ? AND created_at >= ? AND created_at <= ?", entities.RechargeStatusSuccess, startDate, endDate).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetSystemDailyStats 获取系统每日充值统计
|
||||
func (r *GormRechargeRecordRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM recharge_records
|
||||
WHERE status = $1
|
||||
AND DATE(created_at) >= $2
|
||||
AND DATE(created_at) <= $3
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, entities.RechargeStatusSuccess, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemMonthlyStats 获取系统每月充值统计
|
||||
func (r *GormRechargeRecordRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM recharge_records
|
||||
WHERE status = $1
|
||||
AND created_at >= $2
|
||||
AND created_at <= $3
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, entities.RechargeStatusSuccess, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
@@ -184,31 +185,102 @@ func (r *GormWalletRepository) GetByUserID(ctx context.Context, userID string) (
|
||||
return &wallet, nil
|
||||
}
|
||||
|
||||
// UpdateBalanceWithVersionRetry 乐观锁自动重试,最大重试maxRetry次
|
||||
func (r *GormWalletRepository) UpdateBalanceWithVersion(ctx context.Context, walletID string, newBalance string, oldVersion int64) (bool, error) {
|
||||
// UpdateBalanceWithVersion 乐观锁自动重试,最大重试maxRetry次
|
||||
func (r *GormWalletRepository) UpdateBalanceWithVersion(ctx context.Context, walletID string, amount decimal.Decimal, operation string) (bool, error) {
|
||||
maxRetry := 10
|
||||
for i := 0; i < maxRetry; i++ {
|
||||
result := r.GetDB(ctx).Model(&entities.Wallet{}).
|
||||
Where("id = ? AND version = ?", walletID, oldVersion).
|
||||
Updates(map[string]interface{}{
|
||||
"balance": newBalance,
|
||||
"version": oldVersion + 1,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return false, result.Error
|
||||
}
|
||||
if result.RowsAffected == 1 {
|
||||
return true, nil
|
||||
}
|
||||
// 并发冲突,重试前重新查version
|
||||
// 每次重试都重新获取最新的钱包信息
|
||||
var wallet entities.Wallet
|
||||
err := r.GetDB(ctx).Where("id = ?", walletID).First(&wallet).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("获取钱包信息失败: %w", err)
|
||||
}
|
||||
oldVersion = wallet.Version
|
||||
|
||||
// 重新计算新余额
|
||||
var newBalance decimal.Decimal
|
||||
switch operation {
|
||||
case "add":
|
||||
newBalance = wallet.Balance.Add(amount)
|
||||
case "subtract":
|
||||
newBalance = wallet.Balance.Sub(amount)
|
||||
default:
|
||||
return false, fmt.Errorf("不支持的操作类型: %s", operation)
|
||||
}
|
||||
return false, fmt.Errorf("高并发下余额变动失败,请重试")
|
||||
|
||||
// 乐观锁更新
|
||||
result := r.GetDB(ctx).Model(&entities.Wallet{}).
|
||||
Where("id = ? AND version = ?", walletID, wallet.Version).
|
||||
Updates(map[string]interface{}{
|
||||
"balance": newBalance.String(),
|
||||
"version": wallet.Version + 1,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return false, fmt.Errorf("更新钱包余额失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 乐观锁冲突,继续重试
|
||||
// 注意:这里可以添加日志记录,但需要确保logger可用
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("高并发下余额变动失败,已达到最大重试次数 %d", maxRetry)
|
||||
}
|
||||
|
||||
// UpdateBalanceByUserID 乐观锁更新(通过用户ID直接更新,使用原生SQL)
|
||||
func (r *GormWalletRepository) UpdateBalanceByUserID(ctx context.Context, userID string, amount decimal.Decimal, operation string) (bool, error) {
|
||||
maxRetry := 20 // 增加重试次数
|
||||
baseDelay := 1 // 基础延迟毫秒
|
||||
|
||||
for i := 0; i < maxRetry; i++ {
|
||||
// 每次重试都重新获取最新的钱包信息
|
||||
var wallet entities.Wallet
|
||||
err := r.GetDB(ctx).Where("user_id = ?", userID).First(&wallet).Error
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取钱包信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 重新计算新余额
|
||||
var newBalance decimal.Decimal
|
||||
switch operation {
|
||||
case "add":
|
||||
newBalance = wallet.Balance.Add(amount)
|
||||
case "subtract":
|
||||
newBalance = wallet.Balance.Sub(amount)
|
||||
default:
|
||||
return false, fmt.Errorf("不支持的操作类型: %s", operation)
|
||||
}
|
||||
|
||||
// 使用原生SQL进行乐观锁更新
|
||||
newVersion := wallet.Version + 1
|
||||
result := r.GetDB(ctx).Exec(`
|
||||
UPDATE wallets
|
||||
SET balance = ?, version = ?, updated_at = NOW()
|
||||
WHERE user_id = ? AND version = ?
|
||||
`, newBalance.String(), newVersion, userID, wallet.Version)
|
||||
|
||||
if result.Error != nil {
|
||||
return false, fmt.Errorf("更新钱包余额失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 乐观锁冲突,添加指数退避延迟
|
||||
if i < maxRetry-1 {
|
||||
delay := baseDelay * (1 << i) // 指数退避: 1ms, 2ms, 4ms, 8ms...
|
||||
if delay > 50 {
|
||||
delay = 50 // 最大延迟50ms
|
||||
}
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("高并发下余额变动失败,已达到最大重试次数 %d", maxRetry)
|
||||
}
|
||||
|
||||
func (r *GormWalletRepository) UpdateBalance(ctx context.Context, walletID string, balance string) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
|
||||
@@ -150,6 +151,81 @@ func (r *GormWalletTransactionRepository) CountByUserId(ctx context.Context, use
|
||||
return r.CountWhere(ctx, &entities.WalletTransaction{}, "user_id = ?", userId)
|
||||
}
|
||||
|
||||
// CountByUserIdAndDateRange 按用户ID和日期范围统计钱包交易次数
|
||||
func (r *GormWalletTransactionRepository) CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error) {
|
||||
return r.CountWhere(ctx, &entities.WalletTransaction{}, "user_id = ? AND created_at >= ? AND created_at < ?", userId, startDate, endDate)
|
||||
}
|
||||
|
||||
// GetTotalAmountByUserId 获取用户总消费金额
|
||||
func (r *GormWalletTransactionRepository) GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.WalletTransaction{}).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Where("user_id = ?", userId).
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetTotalAmountByUserIdAndDateRange 按用户ID和日期范围获取总消费金额
|
||||
func (r *GormWalletTransactionRepository) GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.WalletTransaction{}).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Where("user_id = ? AND created_at >= ? AND created_at < ?", userId, startDate, endDate).
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetDailyStatsByUserId 获取用户每日消费统计
|
||||
func (r *GormWalletTransactionRepository) GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
// 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM wallet_transactions
|
||||
WHERE user_id = $1
|
||||
AND DATE(created_at) >= $2
|
||||
AND DATE(created_at) <= $3
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, userId, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetMonthlyStatsByUserId 获取用户每月消费统计
|
||||
func (r *GormWalletTransactionRepository) GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
// 构建SQL查询 - 使用PostgreSQL语法,使用具体的日期范围
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM wallet_transactions
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
AND created_at <= $3
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, userId, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// 实现interfaces.Repository接口的其他方法
|
||||
func (r *GormWalletTransactionRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.DeleteEntity(ctx, id, &entities.WalletTransaction{})
|
||||
@@ -392,3 +468,152 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont
|
||||
|
||||
return productNameMap, transactions, total, nil
|
||||
}
|
||||
|
||||
// ExportWithFiltersAndProductName 导出钱包交易记录(包含产品名称和企业信息)
|
||||
func (r *GormWalletTransactionRepository) ExportWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}) ([]*entities.WalletTransaction, error) {
|
||||
var transactionsWithProduct []WalletTransactionWithProduct
|
||||
|
||||
// 构建查询
|
||||
query := r.GetDB(ctx).Table("wallet_transactions wt").
|
||||
Select("wt.*, p.name as product_name").
|
||||
Joins("LEFT JOIN product p ON wt.product_id = p.id")
|
||||
|
||||
// 构建WHERE条件
|
||||
var whereConditions []string
|
||||
var whereArgs []interface{}
|
||||
|
||||
// 用户ID筛选
|
||||
if userIds, ok := filters["user_ids"].(string); ok && userIds != "" {
|
||||
whereConditions = append(whereConditions, "wt.user_id IN (?)")
|
||||
whereArgs = append(whereArgs, strings.Split(userIds, ","))
|
||||
} else if userId, ok := filters["user_id"].(string); ok && userId != "" {
|
||||
whereConditions = append(whereConditions, "wt.user_id = ?")
|
||||
whereArgs = append(whereArgs, userId)
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime, ok := filters["start_time"].(time.Time); ok {
|
||||
whereConditions = append(whereConditions, "wt.created_at >= ?")
|
||||
whereArgs = append(whereArgs, startTime)
|
||||
}
|
||||
if endTime, ok := filters["end_time"].(time.Time); ok {
|
||||
whereConditions = append(whereConditions, "wt.created_at <= ?")
|
||||
whereArgs = append(whereArgs, endTime)
|
||||
}
|
||||
|
||||
// 交易ID筛选
|
||||
if transactionId, ok := filters["transaction_id"].(string); ok && transactionId != "" {
|
||||
whereConditions = append(whereConditions, "wt.transaction_id LIKE ?")
|
||||
whereArgs = append(whereArgs, "%"+transactionId+"%")
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if productName, ok := filters["product_name"].(string); ok && productName != "" {
|
||||
whereConditions = append(whereConditions, "p.name LIKE ?")
|
||||
whereArgs = append(whereArgs, "%"+productName+"%")
|
||||
}
|
||||
|
||||
// 产品ID列表筛选
|
||||
if productIds, ok := filters["product_ids"].(string); ok && productIds != "" {
|
||||
whereConditions = append(whereConditions, "wt.product_id IN (?)")
|
||||
whereArgs = append(whereArgs, strings.Split(productIds, ","))
|
||||
}
|
||||
|
||||
// 金额范围筛选
|
||||
if minAmount, ok := filters["min_amount"].(string); ok && minAmount != "" {
|
||||
whereConditions = append(whereConditions, "wt.amount >= ?")
|
||||
whereArgs = append(whereArgs, minAmount)
|
||||
}
|
||||
if maxAmount, ok := filters["max_amount"].(string); ok && maxAmount != "" {
|
||||
whereConditions = append(whereConditions, "wt.amount <= ?")
|
||||
whereArgs = append(whereArgs, maxAmount)
|
||||
}
|
||||
|
||||
// 应用WHERE条件
|
||||
if len(whereConditions) > 0 {
|
||||
query = query.Where(strings.Join(whereConditions, " AND "), whereArgs...)
|
||||
}
|
||||
|
||||
// 排序
|
||||
query = query.Order("wt.created_at DESC")
|
||||
|
||||
// 执行查询
|
||||
err := query.Find(&transactionsWithProduct).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为entities.WalletTransaction
|
||||
var transactions []*entities.WalletTransaction
|
||||
for _, t := range transactionsWithProduct {
|
||||
transaction := t.WalletTransaction
|
||||
transactions = append(transactions, &transaction)
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
// GetSystemTotalAmount 获取系统总消费金额
|
||||
func (r *GormWalletTransactionRepository) GetSystemTotalAmount(ctx context.Context) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.WalletTransaction{}).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetSystemAmountByDateRange 获取系统指定时间范围内的消费金额
|
||||
func (r *GormWalletTransactionRepository) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
var total float64
|
||||
err := r.GetDB(ctx).Model(&entities.WalletTransaction{}).
|
||||
Where("created_at >= ? AND created_at <= ?", startDate, endDate).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetSystemDailyStats 获取系统每日消费统计
|
||||
func (r *GormWalletTransactionRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM wallet_transactions
|
||||
WHERE DATE(created_at) >= $1
|
||||
AND DATE(created_at) <= $2
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemMonthlyStats 获取系统每月消费统计
|
||||
func (r *GormWalletTransactionRepository) GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
COALESCE(SUM(amount), 0) as amount
|
||||
FROM wallet_transactions
|
||||
WHERE created_at >= $1
|
||||
AND created_at <= $2
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// GormStatisticsDashboardRepository GORM统计仪表板仓储实现
|
||||
type GormStatisticsDashboardRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormStatisticsDashboardRepository 创建GORM统计仪表板仓储
|
||||
func NewGormStatisticsDashboardRepository(db *gorm.DB) repositories.StatisticsDashboardRepository {
|
||||
return &GormStatisticsDashboardRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Save 保存统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) Save(ctx context.Context, dashboard *entities.StatisticsDashboard) error {
|
||||
if dashboard == nil {
|
||||
return fmt.Errorf("统计仪表板不能为空")
|
||||
}
|
||||
|
||||
// 验证仪表板
|
||||
if err := dashboard.Validate(); err != nil {
|
||||
return fmt.Errorf("统计仪表板验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
result := r.db.WithContext(ctx).Save(dashboard)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("保存统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindByID(ctx context.Context, id string) (*entities.StatisticsDashboard, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
var dashboard entities.StatisticsDashboard
|
||||
result := r.db.WithContext(ctx).Where("id = ?", id).First(&dashboard)
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("统计仪表板不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("查询统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
// FindByUser 根据用户查找统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsDashboard, error) {
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
var dashboards []*entities.StatisticsDashboard
|
||||
query := r.db.WithContext(ctx).Where("created_by = ?", userID)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&dashboards)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// FindByUserRole 根据用户角色查找统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindByUserRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error) {
|
||||
if userRole == "" {
|
||||
return nil, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
var dashboards []*entities.StatisticsDashboard
|
||||
query := r.db.WithContext(ctx).Where("user_role = ?", userRole)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&dashboards)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// Update 更新统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) Update(ctx context.Context, dashboard *entities.StatisticsDashboard) error {
|
||||
if dashboard == nil {
|
||||
return fmt.Errorf("统计仪表板不能为空")
|
||||
}
|
||||
|
||||
if dashboard.ID == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
// 验证仪表板
|
||||
if err := dashboard.Validate(); err != nil {
|
||||
return fmt.Errorf("统计仪表板验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
result := r.db.WithContext(ctx).Save(dashboard)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("更新统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) Delete(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Delete(&entities.StatisticsDashboard{}, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("删除统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("统计仪表板不存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByRole 根据角色查找统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error) {
|
||||
if userRole == "" {
|
||||
return nil, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
var dashboards []*entities.StatisticsDashboard
|
||||
query := r.db.WithContext(ctx).Where("user_role = ?", userRole)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&dashboards)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// FindDefaultByRole 根据角色查找默认统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindDefaultByRole(ctx context.Context, userRole string) (*entities.StatisticsDashboard, error) {
|
||||
if userRole == "" {
|
||||
return nil, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
var dashboard entities.StatisticsDashboard
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("user_role = ? AND is_default = ? AND is_active = ?", userRole, true, true).
|
||||
First(&dashboard)
|
||||
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("默认统计仪表板不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("查询默认统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
// FindActiveByRole 根据角色查找激活的统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindActiveByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error) {
|
||||
if userRole == "" {
|
||||
return nil, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
var dashboards []*entities.StatisticsDashboard
|
||||
query := r.db.WithContext(ctx).
|
||||
Where("user_role = ? AND is_active = ?", userRole, true)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&dashboards)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询激活统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// FindByStatus 根据状态查找统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindByStatus(ctx context.Context, isActive bool, limit, offset int) ([]*entities.StatisticsDashboard, error) {
|
||||
var dashboards []*entities.StatisticsDashboard
|
||||
query := r.db.WithContext(ctx).Where("is_active = ?", isActive)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&dashboards)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// FindByAccessLevel 根据访问级别查找统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) FindByAccessLevel(ctx context.Context, accessLevel string, limit, offset int) ([]*entities.StatisticsDashboard, error) {
|
||||
if accessLevel == "" {
|
||||
return nil, fmt.Errorf("访问级别不能为空")
|
||||
}
|
||||
|
||||
var dashboards []*entities.StatisticsDashboard
|
||||
query := r.db.WithContext(ctx).Where("access_level = ?", accessLevel)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&dashboards)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// CountByUser 根据用户统计数量
|
||||
func (r *GormStatisticsDashboardRepository) CountByUser(ctx context.Context, userID string) (int64, error) {
|
||||
if userID == "" {
|
||||
return 0, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsDashboard{}).
|
||||
Where("created_by = ?", userID).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计仪表板数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByRole 根据角色统计数量
|
||||
func (r *GormStatisticsDashboardRepository) CountByRole(ctx context.Context, userRole string) (int64, error) {
|
||||
if userRole == "" {
|
||||
return 0, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsDashboard{}).
|
||||
Where("user_role = ?", userRole).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计仪表板数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByStatus 根据状态统计数量
|
||||
func (r *GormStatisticsDashboardRepository) CountByStatus(ctx context.Context, isActive bool) (int64, error) {
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsDashboard{}).
|
||||
Where("is_active = ?", isActive).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计仪表板数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// BatchSave 批量保存统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) BatchSave(ctx context.Context, dashboards []*entities.StatisticsDashboard) error {
|
||||
if len(dashboards) == 0 {
|
||||
return fmt.Errorf("统计仪表板列表不能为空")
|
||||
}
|
||||
|
||||
// 验证所有仪表板
|
||||
for _, dashboard := range dashboards {
|
||||
if err := dashboard.Validate(); err != nil {
|
||||
return fmt.Errorf("统计仪表板验证失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
result := r.db.WithContext(ctx).CreateInBatches(dashboards, 100)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("批量保存统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除统计仪表板
|
||||
func (r *GormStatisticsDashboardRepository) BatchDelete(ctx context.Context, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return fmt.Errorf("仪表板ID列表不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Delete(&entities.StatisticsDashboard{}, "id IN ?", ids)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("批量删除统计仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultDashboard 设置默认仪表板
|
||||
func (r *GormStatisticsDashboardRepository) SetDefaultDashboard(ctx context.Context, dashboardID string) error {
|
||||
if dashboardID == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 先取消同角色的所有默认状态
|
||||
var dashboard entities.StatisticsDashboard
|
||||
if err := tx.Where("id = ?", dashboardID).First(&dashboard).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("查询仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 取消同角色的所有默认状态
|
||||
if err := tx.Model(&entities.StatisticsDashboard{}).
|
||||
Where("user_role = ? AND is_default = ?", dashboard.UserRole, true).
|
||||
Update("is_default", false).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("取消默认状态失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置新的默认状态
|
||||
if err := tx.Model(&entities.StatisticsDashboard{}).
|
||||
Where("id = ?", dashboardID).
|
||||
Update("is_default", true).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("设置默认状态失败: %w", err)
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return fmt.Errorf("提交事务失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDefaultDashboard 移除默认仪表板
|
||||
func (r *GormStatisticsDashboardRepository) RemoveDefaultDashboard(ctx context.Context, userRole string) error {
|
||||
if userRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsDashboard{}).
|
||||
Where("user_role = ? AND is_default = ?", userRole, true).
|
||||
Update("is_default", false)
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("移除默认仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActivateDashboard 激活仪表板
|
||||
func (r *GormStatisticsDashboardRepository) ActivateDashboard(ctx context.Context, dashboardID string) error {
|
||||
if dashboardID == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsDashboard{}).
|
||||
Where("id = ?", dashboardID).
|
||||
Update("is_active", true)
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("激活仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("仪表板不存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeactivateDashboard 停用仪表板
|
||||
func (r *GormStatisticsDashboardRepository) DeactivateDashboard(ctx context.Context, dashboardID string) error {
|
||||
if dashboardID == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsDashboard{}).
|
||||
Where("id = ?", dashboardID).
|
||||
Update("is_active", false)
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("停用仪表板失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("仪表板不存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// GormStatisticsReportRepository GORM统计报告仓储实现
|
||||
type GormStatisticsReportRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormStatisticsReportRepository 创建GORM统计报告仓储
|
||||
func NewGormStatisticsReportRepository(db *gorm.DB) repositories.StatisticsReportRepository {
|
||||
return &GormStatisticsReportRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Save 保存统计报告
|
||||
func (r *GormStatisticsReportRepository) Save(ctx context.Context, report *entities.StatisticsReport) error {
|
||||
if report == nil {
|
||||
return fmt.Errorf("统计报告不能为空")
|
||||
}
|
||||
|
||||
// 验证报告
|
||||
if err := report.Validate(); err != nil {
|
||||
return fmt.Errorf("统计报告验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
result := r.db.WithContext(ctx).Save(report)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("保存统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找统计报告
|
||||
func (r *GormStatisticsReportRepository) FindByID(ctx context.Context, id string) (*entities.StatisticsReport, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
var report entities.StatisticsReport
|
||||
result := r.db.WithContext(ctx).Where("id = ?", id).First(&report)
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("统计报告不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("查询统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
// FindByUser 根据用户查找统计报告
|
||||
func (r *GormStatisticsReportRepository) FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error) {
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
var reports []*entities.StatisticsReport
|
||||
query := r.db.WithContext(ctx).Where("generated_by = ?", userID)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&reports)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// FindByStatus 根据状态查找统计报告
|
||||
func (r *GormStatisticsReportRepository) FindByStatus(ctx context.Context, status string) ([]*entities.StatisticsReport, error) {
|
||||
if status == "" {
|
||||
return nil, fmt.Errorf("报告状态不能为空")
|
||||
}
|
||||
|
||||
var reports []*entities.StatisticsReport
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("status = ?", status).
|
||||
Order("created_at DESC").
|
||||
Find(&reports)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// Update 更新统计报告
|
||||
func (r *GormStatisticsReportRepository) Update(ctx context.Context, report *entities.StatisticsReport) error {
|
||||
if report == nil {
|
||||
return fmt.Errorf("统计报告不能为空")
|
||||
}
|
||||
|
||||
if report.ID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
// 验证报告
|
||||
if err := report.Validate(); err != nil {
|
||||
return fmt.Errorf("统计报告验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
result := r.db.WithContext(ctx).Save(report)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("更新统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除统计报告
|
||||
func (r *GormStatisticsReportRepository) Delete(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Delete(&entities.StatisticsReport{}, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("删除统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("统计报告不存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByType 根据类型查找统计报告
|
||||
func (r *GormStatisticsReportRepository) FindByType(ctx context.Context, reportType string, limit, offset int) ([]*entities.StatisticsReport, error) {
|
||||
if reportType == "" {
|
||||
return nil, fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
|
||||
var reports []*entities.StatisticsReport
|
||||
query := r.db.WithContext(ctx).Where("report_type = ?", reportType)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&reports)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// FindByTypeAndPeriod 根据类型和周期查找统计报告
|
||||
func (r *GormStatisticsReportRepository) FindByTypeAndPeriod(ctx context.Context, reportType, period string, limit, offset int) ([]*entities.StatisticsReport, error) {
|
||||
if reportType == "" {
|
||||
return nil, fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
|
||||
if period == "" {
|
||||
return nil, fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
|
||||
var reports []*entities.StatisticsReport
|
||||
query := r.db.WithContext(ctx).
|
||||
Where("report_type = ? AND period = ?", reportType, period)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&reports)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// FindByDateRange 根据日期范围查找统计报告
|
||||
func (r *GormStatisticsReportRepository) FindByDateRange(ctx context.Context, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error) {
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return nil, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
var reports []*entities.StatisticsReport
|
||||
query := r.db.WithContext(ctx).
|
||||
Where("created_at >= ? AND created_at < ?", startDate, endDate)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&reports)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// FindByUserAndDateRange 根据用户和日期范围查找统计报告
|
||||
func (r *GormStatisticsReportRepository) FindByUserAndDateRange(ctx context.Context, userID string, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error) {
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return nil, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
var reports []*entities.StatisticsReport
|
||||
query := r.db.WithContext(ctx).
|
||||
Where("generated_by = ? AND created_at >= ? AND created_at < ?", userID, startDate, endDate)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&reports)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// CountByUser 根据用户统计数量
|
||||
func (r *GormStatisticsReportRepository) CountByUser(ctx context.Context, userID string) (int64, error) {
|
||||
if userID == "" {
|
||||
return 0, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsReport{}).
|
||||
Where("generated_by = ?", userID).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计报告数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByType 根据类型统计数量
|
||||
func (r *GormStatisticsReportRepository) CountByType(ctx context.Context, reportType string) (int64, error) {
|
||||
if reportType == "" {
|
||||
return 0, fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsReport{}).
|
||||
Where("report_type = ?", reportType).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计报告数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByStatus 根据状态统计数量
|
||||
func (r *GormStatisticsReportRepository) CountByStatus(ctx context.Context, status string) (int64, error) {
|
||||
if status == "" {
|
||||
return 0, fmt.Errorf("报告状态不能为空")
|
||||
}
|
||||
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsReport{}).
|
||||
Where("status = ?", status).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计报告数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// BatchSave 批量保存统计报告
|
||||
func (r *GormStatisticsReportRepository) BatchSave(ctx context.Context, reports []*entities.StatisticsReport) error {
|
||||
if len(reports) == 0 {
|
||||
return fmt.Errorf("统计报告列表不能为空")
|
||||
}
|
||||
|
||||
// 验证所有报告
|
||||
for _, report := range reports {
|
||||
if err := report.Validate(); err != nil {
|
||||
return fmt.Errorf("统计报告验证失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
result := r.db.WithContext(ctx).CreateInBatches(reports, 100)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("批量保存统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除统计报告
|
||||
func (r *GormStatisticsReportRepository) BatchDelete(ctx context.Context, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return fmt.Errorf("报告ID列表不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Delete(&entities.StatisticsReport{}, "id IN ?", ids)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("批量删除统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExpiredReports 删除过期报告
|
||||
func (r *GormStatisticsReportRepository) DeleteExpiredReports(ctx context.Context, expiredBefore time.Time) error {
|
||||
if expiredBefore.IsZero() {
|
||||
return fmt.Errorf("过期时间不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Delete(&entities.StatisticsReport{}, "expires_at IS NOT NULL AND expires_at < ?", expiredBefore)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("删除过期报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByStatus 根据状态删除统计报告
|
||||
func (r *GormStatisticsReportRepository) DeleteByStatus(ctx context.Context, status string) error {
|
||||
if status == "" {
|
||||
return fmt.Errorf("报告状态不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Delete(&entities.StatisticsReport{}, "status = ?", status)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("根据状态删除统计报告失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// GormStatisticsRepository GORM统计指标仓储实现
|
||||
type GormStatisticsRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormStatisticsRepository 创建GORM统计指标仓储
|
||||
func NewGormStatisticsRepository(db *gorm.DB) repositories.StatisticsRepository {
|
||||
return &GormStatisticsRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Save 保存统计指标
|
||||
func (r *GormStatisticsRepository) Save(ctx context.Context, metric *entities.StatisticsMetric) error {
|
||||
if metric == nil {
|
||||
return fmt.Errorf("统计指标不能为空")
|
||||
}
|
||||
|
||||
// 验证指标
|
||||
if err := metric.Validate(); err != nil {
|
||||
return fmt.Errorf("统计指标验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
result := r.db.WithContext(ctx).Create(metric)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("保存统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找统计指标
|
||||
func (r *GormStatisticsRepository) FindByID(ctx context.Context, id string) (*entities.StatisticsMetric, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("指标ID不能为空")
|
||||
}
|
||||
|
||||
var metric entities.StatisticsMetric
|
||||
result := r.db.WithContext(ctx).Where("id = ?", id).First(&metric)
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("统计指标不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return &metric, nil
|
||||
}
|
||||
|
||||
// FindByType 根据类型查找统计指标
|
||||
func (r *GormStatisticsRepository) FindByType(ctx context.Context, metricType string, limit, offset int) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
var metrics []*entities.StatisticsMetric
|
||||
query := r.db.WithContext(ctx).Where("metric_type = ?", metricType)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
result := query.Order("created_at DESC").Find(&metrics)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// Update 更新统计指标
|
||||
func (r *GormStatisticsRepository) Update(ctx context.Context, metric *entities.StatisticsMetric) error {
|
||||
if metric == nil {
|
||||
return fmt.Errorf("统计指标不能为空")
|
||||
}
|
||||
|
||||
if metric.ID == "" {
|
||||
return fmt.Errorf("指标ID不能为空")
|
||||
}
|
||||
|
||||
// 验证指标
|
||||
if err := metric.Validate(); err != nil {
|
||||
return fmt.Errorf("统计指标验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
result := r.db.WithContext(ctx).Save(metric)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("更新统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除统计指标
|
||||
func (r *GormStatisticsRepository) Delete(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("指标ID不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Delete(&entities.StatisticsMetric{}, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("删除统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("统计指标不存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByTypeAndDateRange 根据类型和日期范围查找统计指标
|
||||
func (r *GormStatisticsRepository) FindByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return nil, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
var metrics []*entities.StatisticsMetric
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("metric_type = ? AND date >= ? AND date < ?", metricType, startDate, endDate).
|
||||
Order("date ASC").
|
||||
Find(&metrics)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// FindByTypeDimensionAndDateRange 根据类型、维度和日期范围查找统计指标
|
||||
func (r *GormStatisticsRepository) FindByTypeDimensionAndDateRange(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return nil, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
var metrics []*entities.StatisticsMetric
|
||||
query := r.db.WithContext(ctx).
|
||||
Where("metric_type = ? AND date >= ? AND date < ?", metricType, startDate, endDate)
|
||||
|
||||
if dimension != "" {
|
||||
query = query.Where("dimension = ?", dimension)
|
||||
}
|
||||
|
||||
result := query.Order("date ASC").Find(&metrics)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// FindByTypeNameAndDateRange 根据类型、名称和日期范围查找统计指标
|
||||
func (r *GormStatisticsRepository) FindByTypeNameAndDateRange(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
if metricName == "" {
|
||||
return nil, fmt.Errorf("指标名称不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return nil, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
var metrics []*entities.StatisticsMetric
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("metric_type = ? AND metric_name = ? AND date >= ? AND date < ?",
|
||||
metricType, metricName, startDate, endDate).
|
||||
Order("date ASC").
|
||||
Find(&metrics)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// GetAggregatedMetrics 获取聚合指标
|
||||
func (r *GormStatisticsRepository) GetAggregatedMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) (map[string]float64, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return nil, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
type AggregatedResult struct {
|
||||
MetricName string `json:"metric_name"`
|
||||
TotalValue float64 `json:"total_value"`
|
||||
}
|
||||
|
||||
var results []AggregatedResult
|
||||
query := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsMetric{}).
|
||||
Select("metric_name, SUM(value) as total_value").
|
||||
Where("metric_type = ? AND date >= ? AND date < ?", metricType, startDate, endDate).
|
||||
Group("metric_name")
|
||||
|
||||
if dimension != "" {
|
||||
query = query.Where("dimension = ?", dimension)
|
||||
}
|
||||
|
||||
result := query.Find(&results)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询聚合指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
// 转换为map
|
||||
aggregated := make(map[string]float64)
|
||||
for _, res := range results {
|
||||
aggregated[res.MetricName] = res.TotalValue
|
||||
}
|
||||
|
||||
return aggregated, nil
|
||||
}
|
||||
|
||||
// GetMetricsByDimension 根据维度获取指标
|
||||
func (r *GormStatisticsRepository) GetMetricsByDimension(ctx context.Context, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if dimension == "" {
|
||||
return nil, fmt.Errorf("统计维度不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return nil, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
var metrics []*entities.StatisticsMetric
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("dimension = ? AND date >= ? AND date < ?", dimension, startDate, endDate).
|
||||
Order("date ASC").
|
||||
Find(&metrics)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// CountByType 根据类型统计数量
|
||||
func (r *GormStatisticsRepository) CountByType(ctx context.Context, metricType string) (int64, error) {
|
||||
if metricType == "" {
|
||||
return 0, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsMetric{}).
|
||||
Where("metric_type = ?", metricType).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计指标数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByTypeAndDateRange 根据类型和日期范围统计数量
|
||||
func (r *GormStatisticsRepository) CountByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) (int64, error) {
|
||||
if metricType == "" {
|
||||
return 0, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return 0, fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
var count int64
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.StatisticsMetric{}).
|
||||
Where("metric_type = ? AND date >= ? AND date < ?", metricType, startDate, endDate).
|
||||
Count(&count)
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("统计指标数量失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// BatchSave 批量保存统计指标
|
||||
func (r *GormStatisticsRepository) BatchSave(ctx context.Context, metrics []*entities.StatisticsMetric) error {
|
||||
if len(metrics) == 0 {
|
||||
return fmt.Errorf("统计指标列表不能为空")
|
||||
}
|
||||
|
||||
// 验证所有指标
|
||||
for _, metric := range metrics {
|
||||
if err := metric.Validate(); err != nil {
|
||||
return fmt.Errorf("统计指标验证失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
result := r.db.WithContext(ctx).CreateInBatches(metrics, 100)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("批量保存统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除统计指标
|
||||
func (r *GormStatisticsRepository) BatchDelete(ctx context.Context, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return fmt.Errorf("指标ID列表不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Delete(&entities.StatisticsMetric{}, "id IN ?", ids)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("批量删除统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByDateRange 根据日期范围删除统计指标
|
||||
func (r *GormStatisticsRepository) DeleteByDateRange(ctx context.Context, startDate, endDate time.Time) error {
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Delete(&entities.StatisticsMetric{}, "date >= ? AND date < ?", startDate, endDate)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("根据日期范围删除统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByTypeAndDateRange 根据类型和日期范围删除统计指标
|
||||
func (r *GormStatisticsRepository) DeleteByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) error {
|
||||
if metricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
if startDate.IsZero() || endDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Delete(&entities.StatisticsMetric{}, "metric_type = ? AND date >= ? AND date < ?",
|
||||
metricType, startDate, endDate)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("根据类型和日期范围删除统计指标失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package repositories
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -71,6 +72,20 @@ func (r *GormUserRepository) GetByIDWithEnterpriseInfo(ctx context.Context, id s
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *GormUserRepository) BatchGetByIDsWithEnterpriseInfo(ctx context.Context, ids []string) ([]*entities.User, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*entities.User{}, nil
|
||||
}
|
||||
|
||||
var users []*entities.User
|
||||
if err := r.GetDB(ctx).Preload("EnterpriseInfo").Where("id IN ?", ids).Find(&users).Error; err != nil {
|
||||
r.GetLogger().Error("批量查询用户失败", zap.Error(err), zap.Strings("ids", ids))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *GormUserRepository) ExistsByUnifiedSocialCode(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error) {
|
||||
var count int64
|
||||
query := r.GetDB(ctx).Model(&entities.User{}).
|
||||
@@ -337,3 +352,315 @@ func (r *GormUserRepository) GetStatsByDateRange(ctx context.Context, startDate,
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetSystemUserStats 获取系统用户统计信息
|
||||
func (r *GormUserRepository) GetSystemUserStats(ctx context.Context) (*repositories.UserStats, error) {
|
||||
var stats repositories.UserStats
|
||||
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
// 总用户数
|
||||
if err := db.Model(&entities.User{}).Count(&stats.TotalUsers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 活跃用户数(最近30天有登录)
|
||||
thirtyDaysAgo := time.Now().AddDate(0, 0, -30)
|
||||
if err := db.Model(&entities.User{}).Where("last_login_at >= ?", thirtyDaysAgo).Count(&stats.ActiveUsers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 已认证用户数
|
||||
if err := db.Model(&entities.User{}).Where("is_certified = ?", true).Count(&stats.CertifiedUsers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 今日注册数
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
if err := db.Model(&entities.User{}).Where("created_at >= ?", today).Count(&stats.TodayRegistrations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 今日登录数
|
||||
if err := db.Model(&entities.User{}).Where("last_login_at >= ?", today).Count(&stats.TodayLogins).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetSystemUserStatsByDateRange 获取系统指定时间范围内的用户统计信息
|
||||
func (r *GormUserRepository) GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*repositories.UserStats, error) {
|
||||
var stats repositories.UserStats
|
||||
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
// 指定时间范围内的注册数
|
||||
if err := db.Model(&entities.User{}).
|
||||
Where("created_at >= ? AND created_at <= ?", startDate, endDate).
|
||||
Count(&stats.TodayRegistrations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 指定时间范围内的登录数
|
||||
if err := db.Model(&entities.User{}).
|
||||
Where("last_login_at >= ? AND last_login_at <= ?", startDate, endDate).
|
||||
Count(&stats.TodayLogins).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetSystemDailyUserStats 获取系统每日用户统计
|
||||
func (r *GormUserRepository) GetSystemDailyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as count
|
||||
FROM users
|
||||
WHERE DATE(created_at) >= $1
|
||||
AND DATE(created_at) <= $2
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemMonthlyUserStats 获取系统每月用户统计
|
||||
func (r *GormUserRepository) GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
COUNT(*) as count
|
||||
FROM users
|
||||
WHERE created_at >= $1
|
||||
AND created_at <= $2
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetUserCallRankingByCalls 按调用次数获取用户排行
|
||||
func (r *GormUserRepository) GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
var args []interface{}
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COUNT(ac.id) as calls
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN api_calls ac ON u.id = ac.user_id
|
||||
AND DATE(ac.created_at) = CURRENT_DATE
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COUNT(ac.id) > 0
|
||||
ORDER BY calls DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "month":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COUNT(ac.id) as calls
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN api_calls ac ON u.id = ac.user_id
|
||||
AND DATE_TRUNC('month', ac.created_at) = DATE_TRUNC('month', CURRENT_DATE)
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COUNT(ac.id) > 0
|
||||
ORDER BY calls DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "total":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COUNT(ac.id) as calls
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN api_calls ac ON u.id = ac.user_id
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COUNT(ac.id) > 0
|
||||
ORDER BY calls DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的时间周期: %s", period)
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetUserCallRankingByConsumption 按消费金额获取用户排行
|
||||
func (r *GormUserRepository) GetUserCallRankingByConsumption(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
var args []interface{}
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COALESCE(SUM(wt.amount), 0) as consumption
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN wallet_transactions wt ON u.id = wt.user_id
|
||||
AND DATE(wt.created_at) = CURRENT_DATE
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COALESCE(SUM(wt.amount), 0) > 0
|
||||
ORDER BY consumption DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "month":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COALESCE(SUM(wt.amount), 0) as consumption
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN wallet_transactions wt ON u.id = wt.user_id
|
||||
AND DATE_TRUNC('month', wt.created_at) = DATE_TRUNC('month', CURRENT_DATE)
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COALESCE(SUM(wt.amount), 0) > 0
|
||||
ORDER BY consumption DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "total":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COALESCE(SUM(wt.amount), 0) as consumption
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN wallet_transactions wt ON u.id = wt.user_id
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COALESCE(SUM(wt.amount), 0) > 0
|
||||
ORDER BY consumption DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的时间周期: %s", period)
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetRechargeRanking 获取充值排行
|
||||
func (r *GormUserRepository) GetRechargeRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
var args []interface{}
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COALESCE(SUM(rr.amount), 0) as amount
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN recharge_records rr ON u.id = rr.user_id
|
||||
AND DATE(rr.created_at) = CURRENT_DATE
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COALESCE(SUM(rr.amount), 0) > 0
|
||||
ORDER BY amount DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "month":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COALESCE(SUM(rr.amount), 0) as amount
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN recharge_records rr ON u.id = rr.user_id
|
||||
AND DATE_TRUNC('month', rr.created_at) = DATE_TRUNC('month', CURRENT_DATE)
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COALESCE(SUM(rr.amount), 0) > 0
|
||||
ORDER BY amount DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
case "total":
|
||||
sql = `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COALESCE(ei.company_name, u.username, u.phone) as username,
|
||||
COALESCE(SUM(rr.amount), 0) as amount
|
||||
FROM users u
|
||||
LEFT JOIN enterprise_infos ei ON u.id = ei.user_id
|
||||
LEFT JOIN recharge_records rr ON u.id = rr.user_id
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, ei.company_name, u.username, u.phone
|
||||
HAVING COALESCE(SUM(rr.amount), 0) > 0
|
||||
ORDER BY amount DESC
|
||||
LIMIT $1
|
||||
`
|
||||
args = []interface{}{limit}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的时间周期: %s", period)
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -302,6 +302,12 @@ func (s *BaiduOCRService) parseBusinessLicenseResult(result map[string]interface
|
||||
registeredCapital = registeredCapitalObj["words"].(string)
|
||||
}
|
||||
|
||||
// 提取企业地址
|
||||
address := ""
|
||||
if addressObj, ok := wordsResult["地址"].(map[string]interface{}); ok {
|
||||
address = addressObj["words"].(string)
|
||||
}
|
||||
|
||||
// 计算置信度(这里简化处理,实际应该从OCR结果中获取)
|
||||
confidence := 0.9 // 默认置信度
|
||||
|
||||
@@ -309,8 +315,11 @@ func (s *BaiduOCRService) parseBusinessLicenseResult(result map[string]interface
|
||||
CompanyName: companyName,
|
||||
UnifiedSocialCode: unifiedSocialCode,
|
||||
LegalPersonName: legalPersonName,
|
||||
LegalPersonID: "", // 营业执照上没有法人身份证号
|
||||
RegisteredCapital: registeredCapital,
|
||||
Address: address,
|
||||
Confidence: confidence,
|
||||
ProcessedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||
"go.uber.org/zap"
|
||||
@@ -64,6 +65,73 @@ func (s *AliSMSService) SendVerificationCode(ctx context.Context, phone string,
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendBalanceAlert 发送余额预警短信
|
||||
func (s *AliSMSService) SendBalanceAlert(ctx context.Context, phone string, balance float64, threshold float64, alertType string, enterpriseName ...string) error {
|
||||
request := dysmsapi.CreateSendSmsRequest()
|
||||
request.Scheme = "https"
|
||||
request.PhoneNumbers = phone
|
||||
request.SignName = s.config.SignName
|
||||
|
||||
var templateCode string
|
||||
var templateParam string
|
||||
|
||||
if alertType == "low_balance" {
|
||||
// 低余额预警也使用欠费预警模板
|
||||
templateCode = "SMS_494605047" // 阿里云欠费预警模板
|
||||
|
||||
// 使用传入的企业名称,如果没有则使用默认值
|
||||
name := "天远数据用户"
|
||||
if len(enterpriseName) > 0 && enterpriseName[0] != "" {
|
||||
name = enterpriseName[0]
|
||||
}
|
||||
|
||||
templateParam = fmt.Sprintf(`{"name":"%s","time":"%s","money":"%.2f"}`,
|
||||
name, time.Now().Format("2006-01-02 15:04:05"), threshold)
|
||||
} else if alertType == "arrears" {
|
||||
// 欠费预警模板
|
||||
templateCode = "SMS_494605047" // 阿里云欠费预警模板
|
||||
|
||||
// 使用传入的企业名称,如果没有则使用默认值
|
||||
name := "天远数据用户"
|
||||
if len(enterpriseName) > 0 && enterpriseName[0] != "" {
|
||||
name = enterpriseName[0]
|
||||
}
|
||||
|
||||
templateParam = fmt.Sprintf(`{"name":"%s","time":"%s","money":"%.2f"}`,
|
||||
name, time.Now().Format("2006-01-02 15:04:05"), balance)
|
||||
} else {
|
||||
return fmt.Errorf("不支持的预警类型: %s", alertType)
|
||||
}
|
||||
|
||||
request.TemplateCode = templateCode
|
||||
request.TemplateParam = templateParam
|
||||
|
||||
response, err := s.client.SendSms(request)
|
||||
if err != nil {
|
||||
s.logger.Error("发送余额预警短信失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("alert_type", alertType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("短信发送失败: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != "OK" {
|
||||
s.logger.Error("余额预警短信发送失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("alert_type", alertType),
|
||||
zap.String("code", response.Code),
|
||||
zap.String("message", response.Message))
|
||||
return fmt.Errorf("短信发送失败: %s - %s", response.Code, response.Message)
|
||||
}
|
||||
|
||||
s.logger.Info("余额预警短信发送成功",
|
||||
zap.String("phone", phone),
|
||||
zap.String("alert_type", alertType),
|
||||
zap.String("bizId", response.BizId))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateCode 生成验证码
|
||||
func (s *AliSMSService) GenerateCode(length int) string {
|
||||
if length <= 0 {
|
||||
|
||||
@@ -468,6 +468,77 @@ func (h *ApiHandler) GetAdminApiCalls(c *gin.Context) {
|
||||
h.responseBuilder.Success(c, result, "获取API调用记录成功")
|
||||
}
|
||||
|
||||
// ExportAdminApiCalls 导出管理端API调用记录
|
||||
// @Summary 导出管理端API调用记录
|
||||
// @Description 管理员导出API调用记录,支持Excel和CSV格式
|
||||
// @Tags API调用管理
|
||||
// @Accept json
|
||||
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
|
||||
// @Security Bearer
|
||||
// @Param user_ids query string false "用户ID列表,逗号分隔"
|
||||
// @Param product_ids query string false "产品ID列表,逗号分隔"
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
// @Param format query string false "导出格式" Enums(excel, csv) default(excel)
|
||||
// @Success 200 {file} file "导出文件"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/api-calls/export [get]
|
||||
func (h *ApiHandler) ExportAdminApiCalls(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userIds := c.Query("user_ids"); userIds != "" {
|
||||
filters["user_ids"] = userIds
|
||||
}
|
||||
|
||||
// 产品ID筛选
|
||||
if productIds := c.Query("product_ids"); productIds != "" {
|
||||
filters["product_ids"] = productIds
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导出格式,默认为excel
|
||||
format := c.DefaultQuery("format", "excel")
|
||||
if format != "excel" && format != "csv" {
|
||||
h.responseBuilder.BadRequest(c, "不支持的导出格式")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用应用服务导出数据
|
||||
fileData, err := h.appService.ExportAdminApiCalls(c.Request.Context(), filters, format)
|
||||
if err != nil {
|
||||
h.logger.Error("导出API调用记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "导出API调用记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
contentType := "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
filename := "API调用记录.xlsx"
|
||||
if format == "csv" {
|
||||
contentType = "text/csv;charset=utf-8"
|
||||
filename = "API调用记录.csv"
|
||||
}
|
||||
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Data(200, contentType, fileData)
|
||||
}
|
||||
|
||||
// getIntQuery 获取整数查询参数
|
||||
func (h *ApiHandler) getIntQuery(c *gin.Context, key string, defaultValue int) int {
|
||||
if value := c.Query(key); value != "" {
|
||||
@@ -477,3 +548,116 @@ func (h *ApiHandler) getIntQuery(c *gin.Context, key string, defaultValue int) i
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetUserBalanceAlertSettings 获取用户余额预警设置
|
||||
// @Summary 获取用户余额预警设置
|
||||
// @Description 获取当前用户的余额预警配置
|
||||
// @Tags 用户设置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "获取成功"
|
||||
// @Failure 401 {object} map[string]interface{} "未授权"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/user/balance-alert/settings [get]
|
||||
func (h *ApiHandler) GetUserBalanceAlertSettings(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.appService.GetUserBalanceAlertSettings(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取用户余额预警设置失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
h.responseBuilder.InternalError(c, "获取预警设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, settings, "获取成功")
|
||||
}
|
||||
|
||||
// UpdateUserBalanceAlertSettings 更新用户余额预警设置
|
||||
// @Summary 更新用户余额预警设置
|
||||
// @Description 更新当前用户的余额预警配置
|
||||
// @Tags 用户设置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body map[string]interface{} true "预警设置"
|
||||
// @Success 200 {object} map[string]interface{} "更新成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未授权"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/user/balance-alert/settings [put]
|
||||
func (h *ApiHandler) UpdateUserBalanceAlertSettings(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Enabled bool `json:"enabled" binding:"required"`
|
||||
Threshold float64 `json:"threshold" binding:"required,min=0"`
|
||||
AlertPhone string `json:"alert_phone" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err := h.appService.UpdateUserBalanceAlertSettings(c.Request.Context(), userID, request.Enabled, request.Threshold, request.AlertPhone)
|
||||
if err != nil {
|
||||
h.logger.Error("更新用户余额预警设置失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
h.responseBuilder.InternalError(c, "更新预警设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, gin.H{}, "更新成功")
|
||||
}
|
||||
|
||||
// TestBalanceAlertSms 测试余额预警短信
|
||||
// @Summary 测试余额预警短信
|
||||
// @Description 发送测试预警短信到指定手机号
|
||||
// @Tags 用户设置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body map[string]interface{} true "测试参数"
|
||||
// @Success 200 {object} map[string]interface{} "发送成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未授权"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/user/balance-alert/test-sms [post]
|
||||
func (h *ApiHandler) TestBalanceAlertSms(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Phone string `json:"phone" binding:"required,len=11"`
|
||||
Balance float64 `json:"balance" binding:"required"`
|
||||
AlertType string `json:"alert_type" binding:"required,oneof=low_balance arrears"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
h.responseBuilder.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err := h.appService.TestBalanceAlertSms(c.Request.Context(), userID, request.Phone, request.Balance, request.AlertType)
|
||||
if err != nil {
|
||||
h.logger.Error("发送测试预警短信失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
h.responseBuilder.InternalError(c, "发送测试短信失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, gin.H{}, "测试短信发送成功")
|
||||
}
|
||||
|
||||
@@ -215,6 +215,86 @@ func (h *CertificationHandler) ApplyContract(c *gin.Context) {
|
||||
h.response.Success(c, result, "合同申请成功")
|
||||
}
|
||||
|
||||
// RecognizeBusinessLicense OCR识别营业执照
|
||||
// @Summary OCR识别营业执照
|
||||
// @Description 上传营业执照图片进行OCR识别,自动填充企业信息
|
||||
// @Tags 认证管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param image formData file true "营业执照图片文件"
|
||||
// @Success 200 {object} responses.BusinessLicenseResult "营业执照识别成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/certifications/ocr/business-license [post]
|
||||
func (h *CertificationHandler) RecognizeBusinessLicense(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("image")
|
||||
if err != nil {
|
||||
h.logger.Error("获取上传文件失败", zap.Error(err), zap.String("user_id", userID))
|
||||
h.response.BadRequest(c, "请选择要上传的营业执照图片")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/png": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
if !allowedTypes[file.Header.Get("Content-Type")] {
|
||||
h.response.BadRequest(c, "只支持JPG、PNG、WEBP格式的图片")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小(限制为5MB)
|
||||
if file.Size > 5*1024*1024 {
|
||||
h.response.BadRequest(c, "图片大小不能超过5MB")
|
||||
return
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
h.logger.Error("打开上传文件失败", zap.Error(err), zap.String("user_id", userID))
|
||||
h.response.BadRequest(c, "文件读取失败")
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 读取文件内容
|
||||
imageBytes, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
h.logger.Error("读取文件内容失败", zap.Error(err), zap.String("user_id", userID))
|
||||
h.response.BadRequest(c, "文件读取失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用OCR服务识别营业执照
|
||||
result, err := h.appService.RecognizeBusinessLicense(c.Request.Context(), imageBytes)
|
||||
if err != nil {
|
||||
h.logger.Error("营业执照OCR识别失败", zap.Error(err), zap.String("user_id", userID))
|
||||
h.response.BadRequest(c, "营业执照识别失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("营业执照OCR识别成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("company_name", result.CompanyName),
|
||||
zap.Float64("confidence", result.Confidence),
|
||||
)
|
||||
|
||||
h.response.Success(c, result, "营业执照识别成功")
|
||||
}
|
||||
|
||||
// ListCertifications 获取认证列表(管理员)
|
||||
// @Summary 获取认证列表
|
||||
// @Description 管理员获取认证申请列表
|
||||
|
||||
@@ -1199,6 +1199,102 @@ func (h *ProductAdminHandler) GetAdminWalletTransactions(c *gin.Context) {
|
||||
h.responseBuilder.Success(c, result, "获取消费记录成功")
|
||||
}
|
||||
|
||||
// ExportAdminWalletTransactions 导出管理端消费记录
|
||||
// @Summary 导出管理端消费记录
|
||||
// @Description 管理员导出消费记录,支持Excel和CSV格式
|
||||
// @Tags 财务管理
|
||||
// @Accept json
|
||||
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
|
||||
// @Security Bearer
|
||||
// @Param user_ids query string false "用户ID列表,逗号分隔"
|
||||
// @Param user_id query string false "单个用户ID"
|
||||
// @Param transaction_id query string false "交易ID"
|
||||
// @Param product_name query string false "产品名称"
|
||||
// @Param product_ids query string false "产品ID列表,逗号分隔"
|
||||
// @Param min_amount query string false "最小金额"
|
||||
// @Param max_amount query string false "最大金额"
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
// @Param format query string false "导出格式" Enums(excel, csv) default(excel)
|
||||
// @Success 200 {file} file "导出文件"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/wallet-transactions/export [get]
|
||||
func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) {
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userIds := c.Query("user_ids"); userIds != "" {
|
||||
filters["user_ids"] = userIds
|
||||
} else if userId := c.Query("user_id"); userId != "" {
|
||||
filters["user_id"] = userId
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 交易ID筛选
|
||||
if transactionId := c.Query("transaction_id"); transactionId != "" {
|
||||
filters["transaction_id"] = transactionId
|
||||
}
|
||||
|
||||
// 产品名称筛选
|
||||
if productName := c.Query("product_name"); productName != "" {
|
||||
filters["product_name"] = productName
|
||||
}
|
||||
|
||||
// 产品ID列表筛选
|
||||
if productIds := c.Query("product_ids"); productIds != "" {
|
||||
filters["product_ids"] = productIds
|
||||
}
|
||||
|
||||
// 金额范围筛选
|
||||
if minAmount := c.Query("min_amount"); minAmount != "" {
|
||||
filters["min_amount"] = minAmount
|
||||
}
|
||||
if maxAmount := c.Query("max_amount"); maxAmount != "" {
|
||||
filters["max_amount"] = maxAmount
|
||||
}
|
||||
|
||||
// 获取导出格式
|
||||
format := c.DefaultQuery("format", "excel")
|
||||
if format != "excel" && format != "csv" {
|
||||
h.responseBuilder.BadRequest(c, "不支持的导出格式")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用导出服务
|
||||
fileData, err := h.financeAppService.ExportAdminWalletTransactions(c.Request.Context(), filters, format)
|
||||
if err != nil {
|
||||
h.logger.Error("导出消费记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "导出消费记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
contentType := "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
filename := "消费记录.xlsx"
|
||||
if format == "csv" {
|
||||
contentType = "text/csv;charset=utf-8"
|
||||
filename = "消费记录.csv"
|
||||
}
|
||||
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Data(200, contentType, fileData)
|
||||
}
|
||||
|
||||
// GetAdminRechargeRecords 获取管理端充值记录
|
||||
// @Summary 获取管理端充值记录
|
||||
// @Description 管理员获取充值记录,支持筛选和分页
|
||||
@@ -1282,3 +1378,184 @@ func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) {
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取充值记录成功")
|
||||
}
|
||||
|
||||
// ExportAdminRechargeRecords 导出管理端充值记录
|
||||
// @Summary 导出管理端充值记录
|
||||
// @Description 管理员导出充值记录,支持Excel和CSV格式
|
||||
// @Tags 财务管理
|
||||
// @Accept json
|
||||
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
|
||||
// @Security Bearer
|
||||
// @Param user_ids query string false "用户ID列表,逗号分隔"
|
||||
// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift)
|
||||
// @Param status query string false "状态" Enums(pending, success, failed)
|
||||
// @Param start_time query string false "开始时间" format(date-time)
|
||||
// @Param end_time query string false "结束时间" format(date-time)
|
||||
// @Param format query string false "导出格式" Enums(excel, csv) default(excel)
|
||||
// @Success 200 {file} file "导出文件"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/recharge-records/export [get]
|
||||
func (h *ProductAdminHandler) ExportAdminRechargeRecords(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userIds := c.Query("user_ids"); userIds != "" {
|
||||
filters["user_ids"] = userIds
|
||||
}
|
||||
|
||||
// 充值类型筛选
|
||||
if rechargeType := c.Query("recharge_type"); rechargeType != "" {
|
||||
filters["recharge_type"] = rechargeType
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters["status"] = status
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导出格式,默认为excel
|
||||
format := c.DefaultQuery("format", "excel")
|
||||
if format != "excel" && format != "csv" {
|
||||
h.responseBuilder.BadRequest(c, "不支持的导出格式")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用应用服务导出数据
|
||||
fileData, err := h.financeAppService.ExportAdminRechargeRecords(c.Request.Context(), filters, format)
|
||||
if err != nil {
|
||||
h.logger.Error("导出充值记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "导出充值记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
contentType := "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
filename := "充值记录.xlsx"
|
||||
if format == "csv" {
|
||||
contentType = "text/csv;charset=utf-8"
|
||||
filename = "充值记录.csv"
|
||||
}
|
||||
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Data(200, contentType, fileData)
|
||||
}
|
||||
|
||||
// GetAdminApiCalls 获取管理端API调用记录
|
||||
func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userIds := c.Query("user_ids"); userIds != "" {
|
||||
filters["user_ids"] = userIds
|
||||
}
|
||||
|
||||
// 产品ID筛选
|
||||
if productIds := c.Query("product_ids"); productIds != "" {
|
||||
filters["product_ids"] = productIds
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 构建分页选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
|
||||
result, err := h.apiAppService.GetAdminApiCalls(c.Request.Context(), filters, options)
|
||||
if err != nil {
|
||||
h.logger.Error("获取管理端API调用记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "获取API调用记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取API调用记录成功")
|
||||
}
|
||||
|
||||
// ExportAdminApiCalls 导出管理端API调用记录
|
||||
func (h *ProductAdminHandler) ExportAdminApiCalls(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
// 用户ID筛选
|
||||
if userIds := c.Query("user_ids"); userIds != "" {
|
||||
filters["user_ids"] = userIds
|
||||
}
|
||||
|
||||
// 产品ID筛选
|
||||
if productIds := c.Query("product_ids"); productIds != "" {
|
||||
filters["product_ids"] = productIds
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if startTime := c.Query("start_time"); startTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||
filters["start_time"] = t
|
||||
}
|
||||
}
|
||||
if endTime := c.Query("end_time"); endTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||
filters["end_time"] = t
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导出格式,默认为excel
|
||||
format := c.DefaultQuery("format", "excel")
|
||||
if format != "excel" && format != "csv" {
|
||||
h.responseBuilder.BadRequest(c, "不支持的导出格式")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用应用服务导出数据
|
||||
fileData, err := h.apiAppService.ExportAdminApiCalls(c.Request.Context(), filters, format)
|
||||
if err != nil {
|
||||
h.logger.Error("导出API调用记录失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, "导出API调用记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
contentType := "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
filename := "API调用记录.xlsx"
|
||||
if format == "csv" {
|
||||
contentType = "text/csv;charset=utf-8"
|
||||
filename = "API调用记录.csv"
|
||||
}
|
||||
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Data(200, contentType, fileData)
|
||||
}
|
||||
|
||||
1502
internal/infrastructure/http/handlers/statistics_handler.go
Normal file
1502
internal/infrastructure/http/handlers/statistics_handler.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -274,7 +274,6 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 构建查询参数
|
||||
query := &queries.ListUsersQuery{
|
||||
Page: 1,
|
||||
@@ -289,7 +288,7 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
}
|
||||
|
||||
if pageSize := c.Query("page_size"); pageSize != "" {
|
||||
if size, err := strconv.Atoi(pageSize); err == nil && size > 0 && size <= 100 {
|
||||
if size, err := strconv.Atoi(pageSize); err == nil && size > 0 && size <= 1000 {
|
||||
query.PageSize = size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func (r *ApiRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
apiGroup := engine.Group("/api/v1")
|
||||
|
||||
{
|
||||
// API调用接口 - 不受频率限制(业务核心接口)
|
||||
apiGroup.POST("/:api_name", r.domainAuthMiddleware.Handle(""), r.apiHandler.HandleApiCall)
|
||||
|
||||
// Console专用接口 - 使用JWT认证,不需要域名认证
|
||||
@@ -62,6 +63,11 @@ func (r *ApiRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
|
||||
// API调用记录接口
|
||||
apiGroup.GET("/my/api-calls", r.authMiddleware.Handle(), r.apiHandler.GetUserApiCalls)
|
||||
|
||||
// 余额预警设置接口
|
||||
apiGroup.GET("/user/balance-alert/settings", r.authMiddleware.Handle(), r.apiHandler.GetUserBalanceAlertSettings)
|
||||
apiGroup.PUT("/user/balance-alert/settings", r.authMiddleware.Handle(), r.apiHandler.UpdateUserBalanceAlertSettings)
|
||||
apiGroup.POST("/user/balance-alert/test-sms", r.authMiddleware.Handle(), r.apiHandler.TestBalanceAlertSms)
|
||||
}
|
||||
|
||||
r.logger.Info("API路由注册完成")
|
||||
|
||||
@@ -54,6 +54,9 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
|
||||
// 2. 提交企业信息(应用每日限流)
|
||||
authGroup.POST("/enterprise-info", r.dailyRateLimit.Handle(), r.handler.SubmitEnterpriseInfo)
|
||||
|
||||
// OCR营业执照识别接口
|
||||
authGroup.POST("/ocr/business-license", r.handler.RecognizeBusinessLicense)
|
||||
|
||||
// 3. 申请合同签署
|
||||
authGroup.POST("/apply-contract", r.handler.ApplyContract)
|
||||
|
||||
@@ -84,6 +87,7 @@ func (r *CertificationRoutes) GetRoutes() []RouteInfo {
|
||||
{Method: "GET", Path: "/api/v1/certifications", Handler: "ListCertifications", Auth: true},
|
||||
{Method: "GET", Path: "/api/v1/certifications/statistics", Handler: "GetCertificationStatistics", Auth: true},
|
||||
{Method: "POST", Path: "/api/v1/certifications/:id/enterprise-info", Handler: "SubmitEnterpriseInfo", Auth: true},
|
||||
{Method: "POST", Path: "/api/v1/certifications/ocr/business-license", Handler: "RecognizeBusinessLicense", Auth: true},
|
||||
{Method: "POST", Path: "/api/v1/certifications/apply-contract", Handler: "ApplyContract", Auth: true},
|
||||
{Method: "POST", Path: "/api/v1/certifications/retry", Handler: "RetryOperation", Auth: true},
|
||||
{Method: "POST", Path: "/api/v1/certifications/force-transition", Handler: "ForceTransitionStatus", Auth: true},
|
||||
|
||||
@@ -87,12 +87,21 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
walletTransactions := adminGroup.Group("/wallet-transactions")
|
||||
{
|
||||
walletTransactions.GET("", r.handler.GetAdminWalletTransactions)
|
||||
walletTransactions.GET("/export", r.handler.ExportAdminWalletTransactions)
|
||||
}
|
||||
|
||||
// API调用记录管理
|
||||
apiCalls := adminGroup.Group("/api-calls")
|
||||
{
|
||||
apiCalls.GET("", r.handler.GetAdminApiCalls)
|
||||
apiCalls.GET("/export", r.handler.ExportAdminApiCalls)
|
||||
}
|
||||
|
||||
// 充值记录管理
|
||||
rechargeRecords := adminGroup.Group("/recharge-records")
|
||||
{
|
||||
rechargeRecords.GET("", r.handler.GetAdminRechargeRecords)
|
||||
rechargeRecords.GET("/export", r.handler.ExportAdminRechargeRecords)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
165
internal/infrastructure/http/routes/statistics_routes.go
Normal file
165
internal/infrastructure/http/routes/statistics_routes.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// StatisticsRoutes 统计路由
|
||||
type StatisticsRoutes struct {
|
||||
statisticsHandler *handlers.StatisticsHandler
|
||||
auth *middleware.JWTAuthMiddleware
|
||||
optionalAuth *middleware.OptionalAuthMiddleware
|
||||
admin *middleware.AdminAuthMiddleware
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsRoutes 创建统计路由
|
||||
func NewStatisticsRoutes(
|
||||
statisticsHandler *handlers.StatisticsHandler,
|
||||
auth *middleware.JWTAuthMiddleware,
|
||||
optionalAuth *middleware.OptionalAuthMiddleware,
|
||||
admin *middleware.AdminAuthMiddleware,
|
||||
logger *zap.Logger,
|
||||
) *StatisticsRoutes {
|
||||
return &StatisticsRoutes{
|
||||
statisticsHandler: statisticsHandler,
|
||||
auth: auth,
|
||||
optionalAuth: optionalAuth,
|
||||
admin: admin,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册统计相关路由
|
||||
func (r *StatisticsRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
engine := router.GetEngine()
|
||||
|
||||
// ================ 用户端统计路由 ================
|
||||
|
||||
// 统计公开接口
|
||||
statistics := engine.Group("/api/v1/statistics")
|
||||
{
|
||||
// 获取公开统计信息
|
||||
statistics.GET("/public", r.statisticsHandler.GetPublicStatistics)
|
||||
}
|
||||
|
||||
// 用户统计接口 - 需要认证
|
||||
userStats := engine.Group("/api/v1/statistics", r.auth.Handle())
|
||||
{
|
||||
// 获取用户统计信息
|
||||
userStats.GET("/user", r.statisticsHandler.GetUserStatistics)
|
||||
|
||||
// 独立统计接口(用户只能查询自己的数据)
|
||||
userStats.GET("/api-calls", r.statisticsHandler.GetApiCallsStatistics)
|
||||
userStats.GET("/consumption", r.statisticsHandler.GetConsumptionStatistics)
|
||||
userStats.GET("/recharge", r.statisticsHandler.GetRechargeStatistics)
|
||||
|
||||
// 获取最新产品推荐
|
||||
userStats.GET("/latest-products", r.statisticsHandler.GetLatestProducts)
|
||||
|
||||
// 获取指标列表
|
||||
userStats.GET("/metrics", r.statisticsHandler.GetMetrics)
|
||||
|
||||
// 获取指标详情
|
||||
userStats.GET("/metrics/:id", r.statisticsHandler.GetMetricDetail)
|
||||
|
||||
// 获取仪表板列表
|
||||
userStats.GET("/dashboards", r.statisticsHandler.GetDashboards)
|
||||
|
||||
// 获取仪表板详情
|
||||
userStats.GET("/dashboards/:id", r.statisticsHandler.GetDashboardDetail)
|
||||
|
||||
// 获取仪表板数据
|
||||
userStats.GET("/dashboards/:id/data", r.statisticsHandler.GetDashboardData)
|
||||
|
||||
// 获取报告列表
|
||||
userStats.GET("/reports", r.statisticsHandler.GetReports)
|
||||
|
||||
// 获取报告详情
|
||||
userStats.GET("/reports/:id", r.statisticsHandler.GetReportDetail)
|
||||
|
||||
// 创建报告
|
||||
userStats.POST("/reports", r.statisticsHandler.CreateReport)
|
||||
}
|
||||
|
||||
// ================ 管理员统计路由 ================
|
||||
|
||||
// 管理员路由组
|
||||
adminGroup := engine.Group("/api/v1/admin")
|
||||
adminGroup.Use(r.admin.Handle()) // 管理员权限验证
|
||||
{
|
||||
// 统计指标管理
|
||||
metrics := adminGroup.Group("/statistics/metrics")
|
||||
{
|
||||
metrics.GET("", r.statisticsHandler.AdminGetMetrics)
|
||||
metrics.POST("", r.statisticsHandler.AdminCreateMetric)
|
||||
metrics.PUT("/:id", r.statisticsHandler.AdminUpdateMetric)
|
||||
metrics.DELETE("/:id", r.statisticsHandler.AdminDeleteMetric)
|
||||
}
|
||||
|
||||
// 仪表板管理
|
||||
dashboards := adminGroup.Group("/statistics/dashboards")
|
||||
{
|
||||
dashboards.GET("", r.statisticsHandler.AdminGetDashboards)
|
||||
dashboards.POST("", r.statisticsHandler.AdminCreateDashboard)
|
||||
dashboards.PUT("/:id", r.statisticsHandler.AdminUpdateDashboard)
|
||||
dashboards.DELETE("/:id", r.statisticsHandler.AdminDeleteDashboard)
|
||||
}
|
||||
|
||||
// 报告管理
|
||||
reports := adminGroup.Group("/statistics/reports")
|
||||
{
|
||||
reports.GET("", r.statisticsHandler.AdminGetReports)
|
||||
}
|
||||
|
||||
// 系统统计
|
||||
system := adminGroup.Group("/statistics/system")
|
||||
{
|
||||
system.GET("", r.statisticsHandler.AdminGetSystemStatistics)
|
||||
}
|
||||
|
||||
// 独立域统计接口
|
||||
domainStats := adminGroup.Group("/statistics")
|
||||
{
|
||||
domainStats.GET("/user-domain", r.statisticsHandler.AdminGetUserDomainStatistics)
|
||||
domainStats.GET("/api-domain", r.statisticsHandler.AdminGetApiDomainStatistics)
|
||||
domainStats.GET("/consumption-domain", r.statisticsHandler.AdminGetConsumptionDomainStatistics)
|
||||
domainStats.GET("/recharge-domain", r.statisticsHandler.AdminGetRechargeDomainStatistics)
|
||||
}
|
||||
|
||||
// 排行榜接口
|
||||
rankings := adminGroup.Group("/statistics")
|
||||
{
|
||||
rankings.GET("/user-call-ranking", r.statisticsHandler.AdminGetUserCallRanking)
|
||||
rankings.GET("/recharge-ranking", r.statisticsHandler.AdminGetRechargeRanking)
|
||||
rankings.GET("/api-popularity-ranking", r.statisticsHandler.AdminGetApiPopularityRanking)
|
||||
rankings.GET("/today-certified-enterprises", r.statisticsHandler.AdminGetTodayCertifiedEnterprises)
|
||||
}
|
||||
|
||||
// 用户统计
|
||||
userStats := adminGroup.Group("/statistics/users")
|
||||
{
|
||||
userStats.GET("/:user_id", r.statisticsHandler.AdminGetUserStatistics)
|
||||
}
|
||||
|
||||
// 独立统计接口(管理员可查询任意用户)
|
||||
independentStats := adminGroup.Group("/statistics")
|
||||
{
|
||||
independentStats.GET("/api-calls", r.statisticsHandler.GetApiCallsStatistics)
|
||||
independentStats.GET("/consumption", r.statisticsHandler.GetConsumptionStatistics)
|
||||
independentStats.GET("/recharge", r.statisticsHandler.GetRechargeStatistics)
|
||||
}
|
||||
|
||||
// 数据聚合
|
||||
aggregation := adminGroup.Group("/statistics/aggregation")
|
||||
{
|
||||
aggregation.POST("/trigger", r.statisticsHandler.AdminTriggerAggregation)
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.Info("统计路由注册完成")
|
||||
}
|
||||
584
internal/infrastructure/statistics/cache/redis_statistics_cache.go
vendored
Normal file
584
internal/infrastructure/statistics/cache/redis_statistics_cache.go
vendored
Normal file
@@ -0,0 +1,584 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
)
|
||||
|
||||
// RedisStatisticsCache Redis统计缓存实现
|
||||
type RedisStatisticsCache struct {
|
||||
client *redis.Client
|
||||
prefix string
|
||||
}
|
||||
|
||||
// NewRedisStatisticsCache 创建Redis统计缓存
|
||||
func NewRedisStatisticsCache(client *redis.Client) *RedisStatisticsCache {
|
||||
return &RedisStatisticsCache{
|
||||
client: client,
|
||||
prefix: "statistics:",
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 指标缓存 ================
|
||||
|
||||
// SetMetric 设置指标缓存
|
||||
func (c *RedisStatisticsCache) SetMetric(ctx context.Context, metric *entities.StatisticsMetric, expiration time.Duration) error {
|
||||
if metric == nil {
|
||||
return fmt.Errorf("统计指标不能为空")
|
||||
}
|
||||
|
||||
key := c.getMetricKey(metric.ID)
|
||||
data, err := json.Marshal(metric)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化指标失败: %w", err)
|
||||
}
|
||||
|
||||
err = c.client.Set(ctx, key, data, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置指标缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetric 获取指标缓存
|
||||
func (c *RedisStatisticsCache) GetMetric(ctx context.Context, metricID string) (*entities.StatisticsMetric, error) {
|
||||
if metricID == "" {
|
||||
return nil, fmt.Errorf("指标ID不能为空")
|
||||
}
|
||||
|
||||
key := c.getMetricKey(metricID)
|
||||
data, err := c.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return nil, nil // 缓存未命中
|
||||
}
|
||||
return nil, fmt.Errorf("获取指标缓存失败: %w", err)
|
||||
}
|
||||
|
||||
var metric entities.StatisticsMetric
|
||||
err = json.Unmarshal([]byte(data), &metric)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("反序列化指标失败: %w", err)
|
||||
}
|
||||
|
||||
return &metric, nil
|
||||
}
|
||||
|
||||
// DeleteMetric 删除指标缓存
|
||||
func (c *RedisStatisticsCache) DeleteMetric(ctx context.Context, metricID string) error {
|
||||
if metricID == "" {
|
||||
return fmt.Errorf("指标ID不能为空")
|
||||
}
|
||||
|
||||
key := c.getMetricKey(metricID)
|
||||
err := c.client.Del(ctx, key).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除指标缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMetricsByType 设置按类型分组的指标缓存
|
||||
func (c *RedisStatisticsCache) SetMetricsByType(ctx context.Context, metricType string, metrics []*entities.StatisticsMetric, expiration time.Duration) error {
|
||||
if metricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
key := c.getMetricsByTypeKey(metricType)
|
||||
data, err := json.Marshal(metrics)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化指标列表失败: %w", err)
|
||||
}
|
||||
|
||||
err = c.client.Set(ctx, key, data, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置指标列表缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetricsByType 获取按类型分组的指标缓存
|
||||
func (c *RedisStatisticsCache) GetMetricsByType(ctx context.Context, metricType string) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
key := c.getMetricsByTypeKey(metricType)
|
||||
data, err := c.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return nil, nil // 缓存未命中
|
||||
}
|
||||
return nil, fmt.Errorf("获取指标列表缓存失败: %w", err)
|
||||
}
|
||||
|
||||
var metrics []*entities.StatisticsMetric
|
||||
err = json.Unmarshal([]byte(data), &metrics)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("反序列化指标列表失败: %w", err)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// DeleteMetricsByType 删除按类型分组的指标缓存
|
||||
func (c *RedisStatisticsCache) DeleteMetricsByType(ctx context.Context, metricType string) error {
|
||||
if metricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
key := c.getMetricsByTypeKey(metricType)
|
||||
err := c.client.Del(ctx, key).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除指标列表缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 实时指标缓存 ================
|
||||
|
||||
// SetRealtimeMetrics 设置实时指标缓存
|
||||
func (c *RedisStatisticsCache) SetRealtimeMetrics(ctx context.Context, metricType string, metrics map[string]float64, expiration time.Duration) error {
|
||||
if metricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
key := c.getRealtimeMetricsKey(metricType)
|
||||
data, err := json.Marshal(metrics)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化实时指标失败: %w", err)
|
||||
}
|
||||
|
||||
err = c.client.Set(ctx, key, data, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置实时指标缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRealtimeMetrics 获取实时指标缓存
|
||||
func (c *RedisStatisticsCache) GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
key := c.getRealtimeMetricsKey(metricType)
|
||||
data, err := c.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return nil, nil // 缓存未命中
|
||||
}
|
||||
return nil, fmt.Errorf("获取实时指标缓存失败: %w", err)
|
||||
}
|
||||
|
||||
var metrics map[string]float64
|
||||
err = json.Unmarshal([]byte(data), &metrics)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("反序列化实时指标失败: %w", err)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// UpdateRealtimeMetric 更新实时指标
|
||||
func (c *RedisStatisticsCache) UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64, expiration time.Duration) error {
|
||||
if metricType == "" || metricName == "" {
|
||||
return fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取现有指标
|
||||
metrics, err := c.GetRealtimeMetrics(ctx, metricType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取实时指标失败: %w", err)
|
||||
}
|
||||
|
||||
if metrics == nil {
|
||||
metrics = make(map[string]float64)
|
||||
}
|
||||
|
||||
// 更新指标值
|
||||
metrics[metricName] = value
|
||||
|
||||
// 保存更新后的指标
|
||||
err = c.SetRealtimeMetrics(ctx, metricType, metrics, expiration)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新实时指标失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 报告缓存 ================
|
||||
|
||||
// SetReport 设置报告缓存
|
||||
func (c *RedisStatisticsCache) SetReport(ctx context.Context, report *entities.StatisticsReport, expiration time.Duration) error {
|
||||
if report == nil {
|
||||
return fmt.Errorf("统计报告不能为空")
|
||||
}
|
||||
|
||||
key := c.getReportKey(report.ID)
|
||||
data, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化报告失败: %w", err)
|
||||
}
|
||||
|
||||
err = c.client.Set(ctx, key, data, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置报告缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetReport 获取报告缓存
|
||||
func (c *RedisStatisticsCache) GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error) {
|
||||
if reportID == "" {
|
||||
return nil, fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
key := c.getReportKey(reportID)
|
||||
data, err := c.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return nil, nil // 缓存未命中
|
||||
}
|
||||
return nil, fmt.Errorf("获取报告缓存失败: %w", err)
|
||||
}
|
||||
|
||||
var report entities.StatisticsReport
|
||||
err = json.Unmarshal([]byte(data), &report)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("反序列化报告失败: %w", err)
|
||||
}
|
||||
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
// DeleteReport 删除报告缓存
|
||||
func (c *RedisStatisticsCache) DeleteReport(ctx context.Context, reportID string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
key := c.getReportKey(reportID)
|
||||
err := c.client.Del(ctx, key).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除报告缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 仪表板缓存 ================
|
||||
|
||||
// SetDashboard 设置仪表板缓存
|
||||
func (c *RedisStatisticsCache) SetDashboard(ctx context.Context, dashboard *entities.StatisticsDashboard, expiration time.Duration) error {
|
||||
if dashboard == nil {
|
||||
return fmt.Errorf("统计仪表板不能为空")
|
||||
}
|
||||
|
||||
key := c.getDashboardKey(dashboard.ID)
|
||||
data, err := json.Marshal(dashboard)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
err = c.client.Set(ctx, key, data, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置仪表板缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDashboard 获取仪表板缓存
|
||||
func (c *RedisStatisticsCache) GetDashboard(ctx context.Context, dashboardID string) (*entities.StatisticsDashboard, error) {
|
||||
if dashboardID == "" {
|
||||
return nil, fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
key := c.getDashboardKey(dashboardID)
|
||||
data, err := c.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return nil, nil // 缓存未命中
|
||||
}
|
||||
return nil, fmt.Errorf("获取仪表板缓存失败: %w", err)
|
||||
}
|
||||
|
||||
var dashboard entities.StatisticsDashboard
|
||||
err = json.Unmarshal([]byte(data), &dashboard)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("反序列化仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
// DeleteDashboard 删除仪表板缓存
|
||||
func (c *RedisStatisticsCache) DeleteDashboard(ctx context.Context, dashboardID string) error {
|
||||
if dashboardID == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
|
||||
key := c.getDashboardKey(dashboardID)
|
||||
err := c.client.Del(ctx, key).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除仪表板缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDashboardData 设置仪表板数据缓存
|
||||
func (c *RedisStatisticsCache) SetDashboardData(ctx context.Context, userRole string, data interface{}, expiration time.Duration) error {
|
||||
if userRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
key := c.getDashboardDataKey(userRole)
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化仪表板数据失败: %w", err)
|
||||
}
|
||||
|
||||
err = c.client.Set(ctx, key, jsonData, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置仪表板数据缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDashboardData 获取仪表板数据缓存
|
||||
func (c *RedisStatisticsCache) GetDashboardData(ctx context.Context, userRole string) (interface{}, error) {
|
||||
if userRole == "" {
|
||||
return nil, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
key := c.getDashboardDataKey(userRole)
|
||||
data, err := c.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return nil, nil // 缓存未命中
|
||||
}
|
||||
return nil, fmt.Errorf("获取仪表板数据缓存失败: %w", err)
|
||||
}
|
||||
|
||||
var result interface{}
|
||||
err = json.Unmarshal([]byte(data), &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("反序列化仪表板数据失败: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteDashboardData 删除仪表板数据缓存
|
||||
func (c *RedisStatisticsCache) DeleteDashboardData(ctx context.Context, userRole string) error {
|
||||
if userRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
|
||||
key := c.getDashboardDataKey(userRole)
|
||||
err := c.client.Del(ctx, key).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除仪表板数据缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 缓存键生成 ================
|
||||
|
||||
// getMetricKey 获取指标缓存键
|
||||
func (c *RedisStatisticsCache) getMetricKey(metricID string) string {
|
||||
return c.prefix + "metric:" + metricID
|
||||
}
|
||||
|
||||
// getMetricsByTypeKey 获取按类型分组的指标缓存键
|
||||
func (c *RedisStatisticsCache) getMetricsByTypeKey(metricType string) string {
|
||||
return c.prefix + "metrics:type:" + metricType
|
||||
}
|
||||
|
||||
// getRealtimeMetricsKey 获取实时指标缓存键
|
||||
func (c *RedisStatisticsCache) getRealtimeMetricsKey(metricType string) string {
|
||||
return c.prefix + "realtime:" + metricType
|
||||
}
|
||||
|
||||
// getReportKey 获取报告缓存键
|
||||
func (c *RedisStatisticsCache) getReportKey(reportID string) string {
|
||||
return c.prefix + "report:" + reportID
|
||||
}
|
||||
|
||||
// getDashboardKey 获取仪表板缓存键
|
||||
func (c *RedisStatisticsCache) getDashboardKey(dashboardID string) string {
|
||||
return c.prefix + "dashboard:" + dashboardID
|
||||
}
|
||||
|
||||
// getDashboardDataKey 获取仪表板数据缓存键
|
||||
func (c *RedisStatisticsCache) getDashboardDataKey(userRole string) string {
|
||||
return c.prefix + "dashboard:data:" + userRole
|
||||
}
|
||||
|
||||
// ================ 批量操作 ================
|
||||
|
||||
// BatchDeleteMetrics 批量删除指标缓存
|
||||
func (c *RedisStatisticsCache) BatchDeleteMetrics(ctx context.Context, metricIDs []string) error {
|
||||
if len(metricIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, len(metricIDs))
|
||||
for i, id := range metricIDs {
|
||||
keys[i] = c.getMetricKey(id)
|
||||
}
|
||||
|
||||
err := c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("批量删除指标缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDeleteReports 批量删除报告缓存
|
||||
func (c *RedisStatisticsCache) BatchDeleteReports(ctx context.Context, reportIDs []string) error {
|
||||
if len(reportIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, len(reportIDs))
|
||||
for i, id := range reportIDs {
|
||||
keys[i] = c.getReportKey(id)
|
||||
}
|
||||
|
||||
err := c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("批量删除报告缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDeleteDashboards 批量删除仪表板缓存
|
||||
func (c *RedisStatisticsCache) BatchDeleteDashboards(ctx context.Context, dashboardIDs []string) error {
|
||||
if len(dashboardIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, len(dashboardIDs))
|
||||
for i, id := range dashboardIDs {
|
||||
keys[i] = c.getDashboardKey(id)
|
||||
}
|
||||
|
||||
err := c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("批量删除仪表板缓存失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 缓存清理 ================
|
||||
|
||||
// ClearAllStatisticsCache 清理所有统计缓存
|
||||
func (c *RedisStatisticsCache) ClearAllStatisticsCache(ctx context.Context) error {
|
||||
pattern := c.prefix + "*"
|
||||
keys, err := c.client.Keys(ctx, pattern).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取缓存键失败: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
err = c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理统计缓存失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearMetricsCache 清理指标缓存
|
||||
func (c *RedisStatisticsCache) ClearMetricsCache(ctx context.Context) error {
|
||||
pattern := c.prefix + "metric:*"
|
||||
keys, err := c.client.Keys(ctx, pattern).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取指标缓存键失败: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
err = c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理指标缓存失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearRealtimeCache 清理实时缓存
|
||||
func (c *RedisStatisticsCache) ClearRealtimeCache(ctx context.Context) error {
|
||||
pattern := c.prefix + "realtime:*"
|
||||
keys, err := c.client.Keys(ctx, pattern).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取实时缓存键失败: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
err = c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理实时缓存失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearReportsCache 清理报告缓存
|
||||
func (c *RedisStatisticsCache) ClearReportsCache(ctx context.Context) error {
|
||||
pattern := c.prefix + "report:*"
|
||||
keys, err := c.client.Keys(ctx, pattern).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取报告缓存键失败: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
err = c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理报告缓存失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearDashboardsCache 清理仪表板缓存
|
||||
func (c *RedisStatisticsCache) ClearDashboardsCache(ctx context.Context) error {
|
||||
pattern := c.prefix + "dashboard:*"
|
||||
keys, err := c.client.Keys(ctx, pattern).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取仪表板缓存键失败: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
err = c.client.Del(ctx, keys...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理仪表板缓存失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
404
internal/infrastructure/statistics/cron/statistics_cron_job.go
Normal file
404
internal/infrastructure/statistics/cron/statistics_cron_job.go
Normal file
@@ -0,0 +1,404 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/statistics"
|
||||
)
|
||||
|
||||
// StatisticsCronJob 统计定时任务
|
||||
type StatisticsCronJob struct {
|
||||
appService statistics.StatisticsApplicationService
|
||||
logger *zap.Logger
|
||||
cron *cron.Cron
|
||||
}
|
||||
|
||||
// NewStatisticsCronJob 创建统计定时任务
|
||||
func NewStatisticsCronJob(
|
||||
appService statistics.StatisticsApplicationService,
|
||||
logger *zap.Logger,
|
||||
) *StatisticsCronJob {
|
||||
return &StatisticsCronJob{
|
||||
appService: appService,
|
||||
logger: logger,
|
||||
cron: cron.New(cron.WithLocation(time.UTC)),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动定时任务
|
||||
func (j *StatisticsCronJob) Start() error {
|
||||
j.logger.Info("启动统计定时任务")
|
||||
|
||||
// 每小时聚合任务 - 每小时的第5分钟执行
|
||||
_, err := j.cron.AddFunc("5 * * * *", j.hourlyAggregationJob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加小时聚合任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 每日聚合任务 - 每天凌晨1点执行
|
||||
_, err = j.cron.AddFunc("0 1 * * *", j.dailyAggregationJob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加日聚合任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 每周聚合任务 - 每周一凌晨2点执行
|
||||
_, err = j.cron.AddFunc("0 2 * * 1", j.weeklyAggregationJob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加周聚合任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 每月聚合任务 - 每月1号凌晨3点执行
|
||||
_, err = j.cron.AddFunc("0 3 1 * *", j.monthlyAggregationJob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加月聚合任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 数据清理任务 - 每天凌晨4点执行
|
||||
_, err = j.cron.AddFunc("0 4 * * *", j.dataCleanupJob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加数据清理任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 缓存预热任务 - 每天早上6点执行
|
||||
_, err = j.cron.AddFunc("0 6 * * *", j.cacheWarmupJob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加缓存预热任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 启动定时器
|
||||
j.cron.Start()
|
||||
|
||||
j.logger.Info("统计定时任务启动成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止定时任务
|
||||
func (j *StatisticsCronJob) Stop() {
|
||||
j.logger.Info("停止统计定时任务")
|
||||
j.cron.Stop()
|
||||
j.logger.Info("统计定时任务已停止")
|
||||
}
|
||||
|
||||
// ================ 定时任务实现 ================
|
||||
|
||||
// hourlyAggregationJob 小时聚合任务
|
||||
func (j *StatisticsCronJob) hourlyAggregationJob() {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
// 聚合上一小时的数据
|
||||
lastHour := now.Add(-1 * time.Hour).Truncate(time.Hour)
|
||||
|
||||
j.logger.Info("开始执行小时聚合任务", zap.Time("target_hour", lastHour))
|
||||
|
||||
err := j.appService.ProcessHourlyAggregation(ctx, lastHour)
|
||||
if err != nil {
|
||||
j.logger.Error("小时聚合任务执行失败",
|
||||
zap.Time("target_hour", lastHour),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
j.logger.Info("小时聚合任务执行成功", zap.Time("target_hour", lastHour))
|
||||
}
|
||||
|
||||
// dailyAggregationJob 日聚合任务
|
||||
func (j *StatisticsCronJob) dailyAggregationJob() {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
// 聚合昨天的数据
|
||||
yesterday := now.AddDate(0, 0, -1).Truncate(24 * time.Hour)
|
||||
|
||||
j.logger.Info("开始执行日聚合任务", zap.Time("target_date", yesterday))
|
||||
|
||||
err := j.appService.ProcessDailyAggregation(ctx, yesterday)
|
||||
if err != nil {
|
||||
j.logger.Error("日聚合任务执行失败",
|
||||
zap.Time("target_date", yesterday),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
j.logger.Info("日聚合任务执行成功", zap.Time("target_date", yesterday))
|
||||
}
|
||||
|
||||
// weeklyAggregationJob 周聚合任务
|
||||
func (j *StatisticsCronJob) weeklyAggregationJob() {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
// 聚合上一周的数据
|
||||
lastWeek := now.AddDate(0, 0, -7).Truncate(24 * time.Hour)
|
||||
|
||||
j.logger.Info("开始执行周聚合任务", zap.Time("target_week", lastWeek))
|
||||
|
||||
err := j.appService.ProcessWeeklyAggregation(ctx, lastWeek)
|
||||
if err != nil {
|
||||
j.logger.Error("周聚合任务执行失败",
|
||||
zap.Time("target_week", lastWeek),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
j.logger.Info("周聚合任务执行成功", zap.Time("target_week", lastWeek))
|
||||
}
|
||||
|
||||
// monthlyAggregationJob 月聚合任务
|
||||
func (j *StatisticsCronJob) monthlyAggregationJob() {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
// 聚合上个月的数据
|
||||
lastMonth := now.AddDate(0, -1, 0).Truncate(24 * time.Hour)
|
||||
|
||||
j.logger.Info("开始执行月聚合任务", zap.Time("target_month", lastMonth))
|
||||
|
||||
err := j.appService.ProcessMonthlyAggregation(ctx, lastMonth)
|
||||
if err != nil {
|
||||
j.logger.Error("月聚合任务执行失败",
|
||||
zap.Time("target_month", lastMonth),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
j.logger.Info("月聚合任务执行成功", zap.Time("target_month", lastMonth))
|
||||
}
|
||||
|
||||
// dataCleanupJob 数据清理任务
|
||||
func (j *StatisticsCronJob) dataCleanupJob() {
|
||||
ctx := context.Background()
|
||||
|
||||
j.logger.Info("开始执行数据清理任务")
|
||||
|
||||
err := j.appService.CleanupExpiredData(ctx)
|
||||
if err != nil {
|
||||
j.logger.Error("数据清理任务执行失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
j.logger.Info("数据清理任务执行成功")
|
||||
}
|
||||
|
||||
// cacheWarmupJob 缓存预热任务
|
||||
func (j *StatisticsCronJob) cacheWarmupJob() {
|
||||
ctx := context.Background()
|
||||
|
||||
j.logger.Info("开始执行缓存预热任务")
|
||||
|
||||
// 预热仪表板数据
|
||||
err := j.warmupDashboardCache(ctx)
|
||||
if err != nil {
|
||||
j.logger.Error("仪表板缓存预热失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 预热实时指标
|
||||
err = j.warmupRealtimeMetricsCache(ctx)
|
||||
if err != nil {
|
||||
j.logger.Error("实时指标缓存预热失败", zap.Error(err))
|
||||
}
|
||||
|
||||
j.logger.Info("缓存预热任务执行完成")
|
||||
}
|
||||
|
||||
// ================ 缓存预热辅助方法 ================
|
||||
|
||||
// warmupDashboardCache 预热仪表板缓存
|
||||
func (j *StatisticsCronJob) warmupDashboardCache(ctx context.Context) error {
|
||||
// 获取所有用户角色
|
||||
userRoles := []string{"admin", "user", "manager", "analyst"}
|
||||
|
||||
for _, role := range userRoles {
|
||||
// 获取仪表板数据
|
||||
query := &statistics.GetDashboardDataQuery{
|
||||
UserRole: role,
|
||||
Period: "today",
|
||||
StartDate: time.Now().Truncate(24 * time.Hour),
|
||||
EndDate: time.Now(),
|
||||
}
|
||||
|
||||
_, err := j.appService.GetDashboardData(ctx, query)
|
||||
if err != nil {
|
||||
j.logger.Error("预热仪表板缓存失败",
|
||||
zap.String("user_role", role),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
j.logger.Info("仪表板缓存预热成功", zap.String("user_role", role))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// warmupRealtimeMetricsCache 预热实时指标缓存
|
||||
func (j *StatisticsCronJob) warmupRealtimeMetricsCache(ctx context.Context) error {
|
||||
// 获取所有指标类型
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
// 获取实时指标
|
||||
query := &statistics.GetRealtimeMetricsQuery{
|
||||
MetricType: metricType,
|
||||
TimeRange: "last_hour",
|
||||
}
|
||||
|
||||
_, err := j.appService.GetRealtimeMetrics(ctx, query)
|
||||
if err != nil {
|
||||
j.logger.Error("预热实时指标缓存失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
j.logger.Info("实时指标缓存预热成功", zap.String("metric_type", metricType))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 手动触发任务 ================
|
||||
|
||||
// TriggerHourlyAggregation 手动触发小时聚合
|
||||
func (j *StatisticsCronJob) TriggerHourlyAggregation(targetHour time.Time) error {
|
||||
ctx := context.Background()
|
||||
|
||||
j.logger.Info("手动触发小时聚合任务", zap.Time("target_hour", targetHour))
|
||||
|
||||
err := j.appService.ProcessHourlyAggregation(ctx, targetHour)
|
||||
if err != nil {
|
||||
j.logger.Error("手动小时聚合任务执行失败",
|
||||
zap.Time("target_hour", targetHour),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
j.logger.Info("手动小时聚合任务执行成功", zap.Time("target_hour", targetHour))
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerDailyAggregation 手动触发日聚合
|
||||
func (j *StatisticsCronJob) TriggerDailyAggregation(targetDate time.Time) error {
|
||||
ctx := context.Background()
|
||||
|
||||
j.logger.Info("手动触发日聚合任务", zap.Time("target_date", targetDate))
|
||||
|
||||
err := j.appService.ProcessDailyAggregation(ctx, targetDate)
|
||||
if err != nil {
|
||||
j.logger.Error("手动日聚合任务执行失败",
|
||||
zap.Time("target_date", targetDate),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
j.logger.Info("手动日聚合任务执行成功", zap.Time("target_date", targetDate))
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerWeeklyAggregation 手动触发周聚合
|
||||
func (j *StatisticsCronJob) TriggerWeeklyAggregation(targetWeek time.Time) error {
|
||||
ctx := context.Background()
|
||||
|
||||
j.logger.Info("手动触发周聚合任务", zap.Time("target_week", targetWeek))
|
||||
|
||||
err := j.appService.ProcessWeeklyAggregation(ctx, targetWeek)
|
||||
if err != nil {
|
||||
j.logger.Error("手动周聚合任务执行失败",
|
||||
zap.Time("target_week", targetWeek),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
j.logger.Info("手动周聚合任务执行成功", zap.Time("target_week", targetWeek))
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerMonthlyAggregation 手动触发月聚合
|
||||
func (j *StatisticsCronJob) TriggerMonthlyAggregation(targetMonth time.Time) error {
|
||||
ctx := context.Background()
|
||||
|
||||
j.logger.Info("手动触发月聚合任务", zap.Time("target_month", targetMonth))
|
||||
|
||||
err := j.appService.ProcessMonthlyAggregation(ctx, targetMonth)
|
||||
if err != nil {
|
||||
j.logger.Error("手动月聚合任务执行失败",
|
||||
zap.Time("target_month", targetMonth),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
j.logger.Info("手动月聚合任务执行成功", zap.Time("target_month", targetMonth))
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerDataCleanup 手动触发数据清理
|
||||
func (j *StatisticsCronJob) TriggerDataCleanup() error {
|
||||
ctx := context.Background()
|
||||
|
||||
j.logger.Info("手动触发数据清理任务")
|
||||
|
||||
err := j.appService.CleanupExpiredData(ctx)
|
||||
if err != nil {
|
||||
j.logger.Error("手动数据清理任务执行失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
j.logger.Info("手动数据清理任务执行成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerCacheWarmup 手动触发缓存预热
|
||||
func (j *StatisticsCronJob) TriggerCacheWarmup() error {
|
||||
j.logger.Info("手动触发缓存预热任务")
|
||||
|
||||
// 预热仪表板缓存
|
||||
err := j.warmupDashboardCache(context.Background())
|
||||
if err != nil {
|
||||
j.logger.Error("手动仪表板缓存预热失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 预热实时指标缓存
|
||||
err = j.warmupRealtimeMetricsCache(context.Background())
|
||||
if err != nil {
|
||||
j.logger.Error("手动实时指标缓存预热失败", zap.Error(err))
|
||||
}
|
||||
|
||||
j.logger.Info("手动缓存预热任务执行完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 任务状态查询 ================
|
||||
|
||||
// GetCronEntries 获取定时任务条目
|
||||
func (j *StatisticsCronJob) GetCronEntries() []cron.Entry {
|
||||
return j.cron.Entries()
|
||||
}
|
||||
|
||||
// GetNextRunTime 获取下次运行时间
|
||||
func (j *StatisticsCronJob) GetNextRunTime() time.Time {
|
||||
entries := j.cron.Entries()
|
||||
if len(entries) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// 返回最近的运行时间
|
||||
nextRun := entries[0].Next
|
||||
for _, entry := range entries[1:] {
|
||||
if entry.Next.Before(nextRun) {
|
||||
nextRun = entry.Next
|
||||
}
|
||||
}
|
||||
|
||||
return nextRun
|
||||
}
|
||||
|
||||
// IsRunning 检查任务是否正在运行
|
||||
func (j *StatisticsCronJob) IsRunning() bool {
|
||||
return j.cron != nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/events"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
"tyapi-server/internal/infrastructure/statistics/cache"
|
||||
)
|
||||
|
||||
// StatisticsEventHandler 统计事件处理器
|
||||
type StatisticsEventHandler struct {
|
||||
metricRepo repositories.StatisticsRepository
|
||||
reportRepo repositories.StatisticsReportRepository
|
||||
dashboardRepo repositories.StatisticsDashboardRepository
|
||||
cache *cache.RedisStatisticsCache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsEventHandler 创建统计事件处理器
|
||||
func NewStatisticsEventHandler(
|
||||
metricRepo repositories.StatisticsRepository,
|
||||
reportRepo repositories.StatisticsReportRepository,
|
||||
dashboardRepo repositories.StatisticsDashboardRepository,
|
||||
cache *cache.RedisStatisticsCache,
|
||||
logger *zap.Logger,
|
||||
) *StatisticsEventHandler {
|
||||
return &StatisticsEventHandler{
|
||||
metricRepo: metricRepo,
|
||||
reportRepo: reportRepo,
|
||||
dashboardRepo: dashboardRepo,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMetricCreatedEvent 处理指标创建事件
|
||||
func (h *StatisticsEventHandler) HandleMetricCreatedEvent(ctx context.Context, event *events.MetricCreatedEvent) error {
|
||||
h.logger.Info("处理指标创建事件",
|
||||
zap.String("metric_id", event.MetricID),
|
||||
zap.String("metric_type", event.MetricType),
|
||||
zap.String("metric_name", event.MetricName),
|
||||
zap.Float64("value", event.Value))
|
||||
|
||||
// 更新实时指标缓存
|
||||
err := h.cache.UpdateRealtimeMetric(ctx, event.MetricType, event.MetricName, event.Value, 1*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("更新实时指标缓存失败", zap.Error(err))
|
||||
// 不返回错误,避免影响主流程
|
||||
}
|
||||
|
||||
// 清理相关缓存
|
||||
err = h.cache.DeleteMetricsByType(ctx, event.MetricType)
|
||||
if err != nil {
|
||||
h.logger.Error("清理指标类型缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleMetricUpdatedEvent 处理指标更新事件
|
||||
func (h *StatisticsEventHandler) HandleMetricUpdatedEvent(ctx context.Context, event *events.MetricUpdatedEvent) error {
|
||||
h.logger.Info("处理指标更新事件",
|
||||
zap.String("metric_id", event.MetricID),
|
||||
zap.Float64("old_value", event.OldValue),
|
||||
zap.Float64("new_value", event.NewValue))
|
||||
|
||||
// 获取指标信息
|
||||
metric, err := h.metricRepo.FindByID(ctx, event.MetricID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询指标失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新实时指标缓存
|
||||
err = h.cache.UpdateRealtimeMetric(ctx, metric.MetricType, metric.MetricName, event.NewValue, 1*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("更新实时指标缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理相关缓存
|
||||
err = h.cache.DeleteMetric(ctx, event.MetricID)
|
||||
if err != nil {
|
||||
h.logger.Error("清理指标缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
err = h.cache.DeleteMetricsByType(ctx, metric.MetricType)
|
||||
if err != nil {
|
||||
h.logger.Error("清理指标类型缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleMetricAggregatedEvent 处理指标聚合事件
|
||||
func (h *StatisticsEventHandler) HandleMetricAggregatedEvent(ctx context.Context, event *events.MetricAggregatedEvent) error {
|
||||
h.logger.Info("处理指标聚合事件",
|
||||
zap.String("metric_type", event.MetricType),
|
||||
zap.String("dimension", event.Dimension),
|
||||
zap.Int("record_count", event.RecordCount),
|
||||
zap.Float64("total_value", event.TotalValue))
|
||||
|
||||
// 清理相关缓存
|
||||
err := h.cache.ClearRealtimeCache(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("清理实时缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
err = h.cache.ClearMetricsCache(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("清理指标缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleReportCreatedEvent 处理报告创建事件
|
||||
func (h *StatisticsEventHandler) HandleReportCreatedEvent(ctx context.Context, event *events.ReportCreatedEvent) error {
|
||||
h.logger.Info("处理报告创建事件",
|
||||
zap.String("report_id", event.ReportID),
|
||||
zap.String("report_type", event.ReportType),
|
||||
zap.String("title", event.Title))
|
||||
|
||||
// 获取报告信息
|
||||
report, err := h.reportRepo.FindByID(ctx, event.ReportID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询报告失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置报告缓存
|
||||
err = h.cache.SetReport(ctx, report, 24*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("设置报告缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleReportGenerationStartedEvent 处理报告生成开始事件
|
||||
func (h *StatisticsEventHandler) HandleReportGenerationStartedEvent(ctx context.Context, event *events.ReportGenerationStartedEvent) error {
|
||||
h.logger.Info("处理报告生成开始事件",
|
||||
zap.String("report_id", event.ReportID),
|
||||
zap.String("generated_by", event.GeneratedBy))
|
||||
|
||||
// 获取报告信息
|
||||
report, err := h.reportRepo.FindByID(ctx, event.ReportID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询报告失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新报告缓存
|
||||
err = h.cache.SetReport(ctx, report, 24*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("更新报告缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleReportCompletedEvent 处理报告完成事件
|
||||
func (h *StatisticsEventHandler) HandleReportCompletedEvent(ctx context.Context, event *events.ReportCompletedEvent) error {
|
||||
h.logger.Info("处理报告完成事件",
|
||||
zap.String("report_id", event.ReportID),
|
||||
zap.Int("content_size", event.ContentSize))
|
||||
|
||||
// 获取报告信息
|
||||
report, err := h.reportRepo.FindByID(ctx, event.ReportID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询报告失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新报告缓存
|
||||
err = h.cache.SetReport(ctx, report, 7*24*time.Hour) // 报告完成后缓存7天
|
||||
if err != nil {
|
||||
h.logger.Error("更新报告缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleReportFailedEvent 处理报告失败事件
|
||||
func (h *StatisticsEventHandler) HandleReportFailedEvent(ctx context.Context, event *events.ReportFailedEvent) error {
|
||||
h.logger.Info("处理报告失败事件",
|
||||
zap.String("report_id", event.ReportID),
|
||||
zap.String("reason", event.Reason))
|
||||
|
||||
// 获取报告信息
|
||||
report, err := h.reportRepo.FindByID(ctx, event.ReportID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询报告失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新报告缓存
|
||||
err = h.cache.SetReport(ctx, report, 1*time.Hour) // 失败报告只缓存1小时
|
||||
if err != nil {
|
||||
h.logger.Error("更新报告缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleDashboardCreatedEvent 处理仪表板创建事件
|
||||
func (h *StatisticsEventHandler) HandleDashboardCreatedEvent(ctx context.Context, event *events.DashboardCreatedEvent) error {
|
||||
h.logger.Info("处理仪表板创建事件",
|
||||
zap.String("dashboard_id", event.DashboardID),
|
||||
zap.String("name", event.Name),
|
||||
zap.String("user_role", event.UserRole))
|
||||
|
||||
// 获取仪表板信息
|
||||
dashboard, err := h.dashboardRepo.FindByID(ctx, event.DashboardID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询仪表板失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置仪表板缓存
|
||||
err = h.cache.SetDashboard(ctx, dashboard, 24*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("设置仪表板缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理仪表板数据缓存
|
||||
err = h.cache.DeleteDashboardData(ctx, event.UserRole)
|
||||
if err != nil {
|
||||
h.logger.Error("清理仪表板数据缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleDashboardUpdatedEvent 处理仪表板更新事件
|
||||
func (h *StatisticsEventHandler) HandleDashboardUpdatedEvent(ctx context.Context, event *events.DashboardUpdatedEvent) error {
|
||||
h.logger.Info("处理仪表板更新事件",
|
||||
zap.String("dashboard_id", event.DashboardID),
|
||||
zap.String("updated_by", event.UpdatedBy))
|
||||
|
||||
// 获取仪表板信息
|
||||
dashboard, err := h.dashboardRepo.FindByID(ctx, event.DashboardID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询仪表板失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新仪表板缓存
|
||||
err = h.cache.SetDashboard(ctx, dashboard, 24*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("更新仪表板缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理仪表板数据缓存
|
||||
err = h.cache.DeleteDashboardData(ctx, dashboard.UserRole)
|
||||
if err != nil {
|
||||
h.logger.Error("清理仪表板数据缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleDashboardActivatedEvent 处理仪表板激活事件
|
||||
func (h *StatisticsEventHandler) HandleDashboardActivatedEvent(ctx context.Context, event *events.DashboardActivatedEvent) error {
|
||||
h.logger.Info("处理仪表板激活事件",
|
||||
zap.String("dashboard_id", event.DashboardID),
|
||||
zap.String("activated_by", event.ActivatedBy))
|
||||
|
||||
// 获取仪表板信息
|
||||
dashboard, err := h.dashboardRepo.FindByID(ctx, event.DashboardID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询仪表板失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新仪表板缓存
|
||||
err = h.cache.SetDashboard(ctx, dashboard, 24*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("更新仪表板缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理仪表板数据缓存
|
||||
err = h.cache.DeleteDashboardData(ctx, dashboard.UserRole)
|
||||
if err != nil {
|
||||
h.logger.Error("清理仪表板数据缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleDashboardDeactivatedEvent 处理仪表板停用事件
|
||||
func (h *StatisticsEventHandler) HandleDashboardDeactivatedEvent(ctx context.Context, event *events.DashboardDeactivatedEvent) error {
|
||||
h.logger.Info("处理仪表板停用事件",
|
||||
zap.String("dashboard_id", event.DashboardID),
|
||||
zap.String("deactivated_by", event.DeactivatedBy))
|
||||
|
||||
// 获取仪表板信息
|
||||
dashboard, err := h.dashboardRepo.FindByID(ctx, event.DashboardID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询仪表板失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新仪表板缓存
|
||||
err = h.cache.SetDashboard(ctx, dashboard, 24*time.Hour)
|
||||
if err != nil {
|
||||
h.logger.Error("更新仪表板缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理仪表板数据缓存
|
||||
err = h.cache.DeleteDashboardData(ctx, dashboard.UserRole)
|
||||
if err != nil {
|
||||
h.logger.Error("清理仪表板数据缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 事件分发器 ================
|
||||
|
||||
// EventDispatcher 事件分发器
|
||||
type EventDispatcher struct {
|
||||
handlers map[string][]func(context.Context, interface{}) error
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewEventDispatcher 创建事件分发器
|
||||
func NewEventDispatcher(logger *zap.Logger) *EventDispatcher {
|
||||
return &EventDispatcher{
|
||||
handlers: make(map[string][]func(context.Context, interface{}) error),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHandler 注册事件处理器
|
||||
func (d *EventDispatcher) RegisterHandler(eventType string, handler func(context.Context, interface{}) error) {
|
||||
if d.handlers[eventType] == nil {
|
||||
d.handlers[eventType] = make([]func(context.Context, interface{}) error, 0)
|
||||
}
|
||||
d.handlers[eventType] = append(d.handlers[eventType], handler)
|
||||
}
|
||||
|
||||
// Dispatch 分发事件
|
||||
func (d *EventDispatcher) Dispatch(ctx context.Context, event interface{}) error {
|
||||
// 获取事件类型
|
||||
eventType := d.getEventType(event)
|
||||
if eventType == "" {
|
||||
return fmt.Errorf("无法确定事件类型")
|
||||
}
|
||||
|
||||
// 获取处理器
|
||||
handlers := d.handlers[eventType]
|
||||
if len(handlers) == 0 {
|
||||
d.logger.Warn("没有找到事件处理器", zap.String("event_type", eventType))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行所有处理器
|
||||
for _, handler := range handlers {
|
||||
err := handler(ctx, event)
|
||||
if err != nil {
|
||||
d.logger.Error("事件处理器执行失败",
|
||||
zap.String("event_type", eventType),
|
||||
zap.Error(err))
|
||||
// 继续执行其他处理器
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEventType 获取事件类型
|
||||
func (d *EventDispatcher) getEventType(event interface{}) string {
|
||||
switch event.(type) {
|
||||
case *events.MetricCreatedEvent:
|
||||
return string(events.MetricCreatedEventType)
|
||||
case *events.MetricUpdatedEvent:
|
||||
return string(events.MetricUpdatedEventType)
|
||||
case *events.MetricAggregatedEvent:
|
||||
return string(events.MetricAggregatedEventType)
|
||||
case *events.ReportCreatedEvent:
|
||||
return string(events.ReportCreatedEventType)
|
||||
case *events.ReportGenerationStartedEvent:
|
||||
return string(events.ReportGenerationStartedEventType)
|
||||
case *events.ReportCompletedEvent:
|
||||
return string(events.ReportCompletedEventType)
|
||||
case *events.ReportFailedEvent:
|
||||
return string(events.ReportFailedEventType)
|
||||
case *events.DashboardCreatedEvent:
|
||||
return string(events.DashboardCreatedEventType)
|
||||
case *events.DashboardUpdatedEvent:
|
||||
return string(events.DashboardUpdatedEventType)
|
||||
case *events.DashboardActivatedEvent:
|
||||
return string(events.DashboardActivatedEventType)
|
||||
case *events.DashboardDeactivatedEvent:
|
||||
return string(events.DashboardDeactivatedEventType)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 事件监听器 ================
|
||||
|
||||
// EventListener 事件监听器
|
||||
type EventListener struct {
|
||||
dispatcher *EventDispatcher
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewEventListener 创建事件监听器
|
||||
func NewEventListener(dispatcher *EventDispatcher, logger *zap.Logger) *EventListener {
|
||||
return &EventListener{
|
||||
dispatcher: dispatcher,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Listen 监听事件
|
||||
func (l *EventListener) Listen(ctx context.Context, eventData []byte) error {
|
||||
// 解析事件数据
|
||||
var baseEvent events.BaseStatisticsEvent
|
||||
err := json.Unmarshal(eventData, &baseEvent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析事件数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据事件类型创建具体事件
|
||||
event, err := l.createEventByType(baseEvent.Type, eventData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建事件失败: %w", err)
|
||||
}
|
||||
|
||||
// 分发事件
|
||||
err = l.dispatcher.Dispatch(ctx, event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分发事件失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createEventByType 根据事件类型创建具体事件
|
||||
func (l *EventListener) createEventByType(eventType string, eventData []byte) (interface{}, error) {
|
||||
switch eventType {
|
||||
case string(events.MetricCreatedEventType):
|
||||
var event events.MetricCreatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.MetricUpdatedEventType):
|
||||
var event events.MetricUpdatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.MetricAggregatedEventType):
|
||||
var event events.MetricAggregatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.ReportCreatedEventType):
|
||||
var event events.ReportCreatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.ReportGenerationStartedEventType):
|
||||
var event events.ReportGenerationStartedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.ReportCompletedEventType):
|
||||
var event events.ReportCompletedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.ReportFailedEventType):
|
||||
var event events.ReportFailedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.DashboardCreatedEventType):
|
||||
var event events.DashboardCreatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.DashboardUpdatedEventType):
|
||||
var event events.DashboardUpdatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.DashboardActivatedEventType):
|
||||
var event events.DashboardActivatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
case string(events.DashboardDeactivatedEventType):
|
||||
var event events.DashboardDeactivatedEvent
|
||||
err := json.Unmarshal(eventData, &event)
|
||||
return &event, err
|
||||
default:
|
||||
return nil, fmt.Errorf("未知的事件类型: %s", eventType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
)
|
||||
|
||||
// StatisticsMigration 统计模块数据迁移
|
||||
type StatisticsMigration struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewStatisticsMigration 创建统计模块数据迁移
|
||||
func NewStatisticsMigration(db *gorm.DB) *StatisticsMigration {
|
||||
return &StatisticsMigration{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate 执行数据迁移
|
||||
func (m *StatisticsMigration) Migrate() error {
|
||||
fmt.Println("开始执行统计模块数据迁移...")
|
||||
|
||||
// 迁移统计指标表
|
||||
err := m.migrateStatisticsMetrics()
|
||||
if err != nil {
|
||||
return fmt.Errorf("迁移统计指标表失败: %w", err)
|
||||
}
|
||||
|
||||
// 迁移统计报告表
|
||||
err = m.migrateStatisticsReports()
|
||||
if err != nil {
|
||||
return fmt.Errorf("迁移统计报告表失败: %w", err)
|
||||
}
|
||||
|
||||
// 迁移统计仪表板表
|
||||
err = m.migrateStatisticsDashboards()
|
||||
if err != nil {
|
||||
return fmt.Errorf("迁移统计仪表板表失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
err = m.createIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 插入初始数据
|
||||
err = m.insertInitialData()
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入初始数据失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块数据迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateStatisticsMetrics 迁移统计指标表
|
||||
func (m *StatisticsMigration) migrateStatisticsMetrics() error {
|
||||
fmt.Println("迁移统计指标表...")
|
||||
|
||||
// 自动迁移表结构
|
||||
err := m.db.AutoMigrate(&entities.StatisticsMetric{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("自动迁移统计指标表失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计指标表迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateStatisticsReports 迁移统计报告表
|
||||
func (m *StatisticsMigration) migrateStatisticsReports() error {
|
||||
fmt.Println("迁移统计报告表...")
|
||||
|
||||
// 自动迁移表结构
|
||||
err := m.db.AutoMigrate(&entities.StatisticsReport{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("自动迁移统计报告表失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计报告表迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateStatisticsDashboards 迁移统计仪表板表
|
||||
func (m *StatisticsMigration) migrateStatisticsDashboards() error {
|
||||
fmt.Println("迁移统计仪表板表...")
|
||||
|
||||
// 自动迁移表结构
|
||||
err := m.db.AutoMigrate(&entities.StatisticsDashboard{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("自动迁移统计仪表板表失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计仪表板表迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createIndexes 创建索引
|
||||
func (m *StatisticsMigration) createIndexes() error {
|
||||
fmt.Println("创建统计模块索引...")
|
||||
|
||||
// 统计指标表索引
|
||||
err := m.createStatisticsMetricsIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建统计指标表索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 统计报告表索引
|
||||
err = m.createStatisticsReportsIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建统计报告表索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 统计仪表板表索引
|
||||
err = m.createStatisticsDashboardsIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建统计仪表板表索引失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块索引创建完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStatisticsMetricsIndexes 创建统计指标表索引
|
||||
func (m *StatisticsMigration) createStatisticsMetricsIndexes() error {
|
||||
// 复合索引:metric_type + date
|
||||
err := m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_metrics_type_date
|
||||
ON statistics_metrics (metric_type, date)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 复合索引:metric_type + dimension + date
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_metrics_type_dimension_date
|
||||
ON statistics_metrics (metric_type, dimension, date)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 复合索引:metric_type + metric_name + date
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_metrics_type_name_date
|
||||
ON statistics_metrics (metric_type, metric_name, date)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 单列索引:dimension
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_metrics_dimension
|
||||
ON statistics_metrics (dimension)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建维度索引失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStatisticsReportsIndexes 创建统计报告表索引
|
||||
func (m *StatisticsMigration) createStatisticsReportsIndexes() error {
|
||||
// 复合索引:report_type + created_at
|
||||
err := m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_reports_type_created
|
||||
ON statistics_reports (report_type, created_at)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 复合索引:user_role + created_at
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_reports_role_created
|
||||
ON statistics_reports (user_role, created_at)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 复合索引:status + created_at
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_reports_status_created
|
||||
ON statistics_reports (status, created_at)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 单列索引:generated_by
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_reports_generated_by
|
||||
ON statistics_reports (generated_by)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建生成者索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 单列索引:expires_at
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_reports_expires_at
|
||||
ON statistics_reports (expires_at)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建过期时间索引失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStatisticsDashboardsIndexes 创建统计仪表板表索引
|
||||
func (m *StatisticsMigration) createStatisticsDashboardsIndexes() error {
|
||||
// 复合索引:user_role + is_active
|
||||
err := m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_role_active
|
||||
ON statistics_dashboards (user_role, is_active)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 复合索引:user_role + is_default
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_role_default
|
||||
ON statistics_dashboards (user_role, is_default)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建复合索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 单列索引:created_by
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_created_by
|
||||
ON statistics_dashboards (created_by)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建创建者索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 单列索引:access_level
|
||||
err = m.db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_access_level
|
||||
ON statistics_dashboards (access_level)
|
||||
`).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建访问级别索引失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertInitialData 插入初始数据
|
||||
func (m *StatisticsMigration) insertInitialData() error {
|
||||
fmt.Println("插入统计模块初始数据...")
|
||||
|
||||
// 插入默认仪表板
|
||||
err := m.insertDefaultDashboards()
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入默认仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 插入初始指标数据
|
||||
err = m.insertInitialMetrics()
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入初始指标数据失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块初始数据插入完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertDefaultDashboards 插入默认仪表板
|
||||
func (m *StatisticsMigration) insertDefaultDashboards() error {
|
||||
// 管理员默认仪表板
|
||||
adminDashboard := &entities.StatisticsDashboard{
|
||||
Name: "管理员仪表板",
|
||||
Description: "系统管理员专用仪表板,包含所有统计信息",
|
||||
UserRole: "admin",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 300,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 3, "rows": 4}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}, {"type": "finance", "position": {"x": 2, "y": 0}}]`,
|
||||
Settings: `{"theme": "dark", "auto_refresh": true}`,
|
||||
}
|
||||
|
||||
err := m.db.Create(adminDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建管理员仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 用户默认仪表板
|
||||
userDashboard := &entities.StatisticsDashboard{
|
||||
Name: "用户仪表板",
|
||||
Description: "普通用户专用仪表板,包含基础统计信息",
|
||||
UserRole: "user",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 600,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 2, "rows": 3}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}]`,
|
||||
Settings: `{"theme": "light", "auto_refresh": false}`,
|
||||
}
|
||||
|
||||
err = m.db.Create(userDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建用户仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 经理默认仪表板
|
||||
managerDashboard := &entities.StatisticsDashboard{
|
||||
Name: "经理仪表板",
|
||||
Description: "经理专用仪表板,包含管理相关统计信息",
|
||||
UserRole: "manager",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 300,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 3, "rows": 3}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}, {"type": "finance", "position": {"x": 2, "y": 0}}]`,
|
||||
Settings: `{"theme": "dark", "auto_refresh": true}`,
|
||||
}
|
||||
|
||||
err = m.db.Create(managerDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建经理仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 分析师默认仪表板
|
||||
analystDashboard := &entities.StatisticsDashboard{
|
||||
Name: "分析师仪表板",
|
||||
Description: "数据分析师专用仪表板,包含详细分析信息",
|
||||
UserRole: "analyst",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 180,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 4, "rows": 4}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}, {"type": "finance", "position": {"x": 2, "y": 0}}, {"type": "products", "position": {"x": 3, "y": 0}}]`,
|
||||
Settings: `{"theme": "dark", "auto_refresh": true, "show_trends": true}`,
|
||||
}
|
||||
|
||||
err = m.db.Create(analystDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建分析师仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("默认仪表板创建完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertInitialMetrics 插入初始指标数据
|
||||
func (m *StatisticsMigration) insertInitialMetrics() error {
|
||||
now := time.Now()
|
||||
today := now.Truncate(24 * time.Hour)
|
||||
|
||||
// 插入初始API调用指标
|
||||
apiMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "total_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "success_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "failed_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "response_time",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始用户指标
|
||||
userMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "users",
|
||||
MetricName: "total_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "users",
|
||||
MetricName: "certified_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "users",
|
||||
MetricName: "active_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始财务指标
|
||||
financeMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "total_amount",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "recharge_amount",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "deduct_amount",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始产品指标
|
||||
productMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "total_products",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "active_products",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "total_subscriptions",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "active_subscriptions",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始认证指标
|
||||
certificationMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "total_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "completed_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "pending_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "failed_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 批量插入所有指标
|
||||
allMetrics := append(apiMetrics, userMetrics...)
|
||||
allMetrics = append(allMetrics, financeMetrics...)
|
||||
allMetrics = append(allMetrics, productMetrics...)
|
||||
allMetrics = append(allMetrics, certificationMetrics...)
|
||||
|
||||
err := m.db.CreateInBatches(allMetrics, 100).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("批量插入初始指标失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("初始指标数据创建完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback 回滚迁移
|
||||
func (m *StatisticsMigration) Rollback() error {
|
||||
fmt.Println("开始回滚统计模块数据迁移...")
|
||||
|
||||
// 删除表
|
||||
err := m.db.Migrator().DropTable(&entities.StatisticsDashboard{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除统计仪表板表失败: %w", err)
|
||||
}
|
||||
|
||||
err = m.db.Migrator().DropTable(&entities.StatisticsReport{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除统计报告表失败: %w", err)
|
||||
}
|
||||
|
||||
err = m.db.Migrator().DropTable(&entities.StatisticsMetric{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除统计指标表失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块数据迁移回滚完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
)
|
||||
|
||||
// StatisticsMigrationComplete 统计模块完整数据迁移
|
||||
type StatisticsMigrationComplete struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewStatisticsMigrationComplete 创建统计模块完整数据迁移
|
||||
func NewStatisticsMigrationComplete(db *gorm.DB) *StatisticsMigrationComplete {
|
||||
return &StatisticsMigrationComplete{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate 执行完整的数据迁移
|
||||
func (m *StatisticsMigrationComplete) Migrate() error {
|
||||
fmt.Println("开始执行统计模块完整数据迁移...")
|
||||
|
||||
// 1. 迁移表结构
|
||||
err := m.migrateTables()
|
||||
if err != nil {
|
||||
return fmt.Errorf("迁移表结构失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建索引
|
||||
err = m.createIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 插入初始数据
|
||||
err = m.insertInitialData()
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入初始数据失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块完整数据迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateTables 迁移表结构
|
||||
func (m *StatisticsMigrationComplete) migrateTables() error {
|
||||
fmt.Println("迁移统计模块表结构...")
|
||||
|
||||
// 迁移统计指标表
|
||||
err := m.db.AutoMigrate(&entities.StatisticsMetric{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("迁移统计指标表失败: %w", err)
|
||||
}
|
||||
|
||||
// 迁移统计报告表
|
||||
err = m.db.AutoMigrate(&entities.StatisticsReport{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("迁移统计报告表失败: %w", err)
|
||||
}
|
||||
|
||||
// 迁移统计仪表板表
|
||||
err = m.db.AutoMigrate(&entities.StatisticsDashboard{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("迁移统计仪表板表失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块表结构迁移完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createIndexes 创建索引
|
||||
func (m *StatisticsMigrationComplete) createIndexes() error {
|
||||
fmt.Println("创建统计模块索引...")
|
||||
|
||||
// 统计指标表索引
|
||||
err := m.createStatisticsMetricsIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建统计指标表索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 统计报告表索引
|
||||
err = m.createStatisticsReportsIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建统计报告表索引失败: %w", err)
|
||||
}
|
||||
|
||||
// 统计仪表板表索引
|
||||
err = m.createStatisticsDashboardsIndexes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建统计仪表板表索引失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块索引创建完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStatisticsMetricsIndexes 创建统计指标表索引
|
||||
func (m *StatisticsMigrationComplete) createStatisticsMetricsIndexes() error {
|
||||
indexes := []string{
|
||||
// 复合索引:metric_type + date
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_metrics_type_date
|
||||
ON statistics_metrics (metric_type, date)`,
|
||||
|
||||
// 复合索引:metric_type + dimension + date
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_metrics_type_dimension_date
|
||||
ON statistics_metrics (metric_type, dimension, date)`,
|
||||
|
||||
// 复合索引:metric_type + metric_name + date
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_metrics_type_name_date
|
||||
ON statistics_metrics (metric_type, metric_name, date)`,
|
||||
|
||||
// 单列索引:dimension
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_metrics_dimension
|
||||
ON statistics_metrics (dimension)`,
|
||||
|
||||
// 单列索引:metric_name
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_metrics_name
|
||||
ON statistics_metrics (metric_name)`,
|
||||
|
||||
// 单列索引:value(用于范围查询)
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_metrics_value
|
||||
ON statistics_metrics (value)`,
|
||||
}
|
||||
|
||||
for _, indexSQL := range indexes {
|
||||
err := m.db.Exec(indexSQL).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStatisticsReportsIndexes 创建统计报告表索引
|
||||
func (m *StatisticsMigrationComplete) createStatisticsReportsIndexes() error {
|
||||
indexes := []string{
|
||||
// 复合索引:report_type + created_at
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_reports_type_created
|
||||
ON statistics_reports (report_type, created_at)`,
|
||||
|
||||
// 复合索引:user_role + created_at
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_reports_role_created
|
||||
ON statistics_reports (user_role, created_at)`,
|
||||
|
||||
// 复合索引:status + created_at
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_reports_status_created
|
||||
ON statistics_reports (status, created_at)`,
|
||||
|
||||
// 单列索引:generated_by
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_reports_generated_by
|
||||
ON statistics_reports (generated_by)`,
|
||||
|
||||
// 单列索引:expires_at
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_reports_expires_at
|
||||
ON statistics_reports (expires_at)`,
|
||||
|
||||
// 单列索引:period
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_reports_period
|
||||
ON statistics_reports (period)`,
|
||||
}
|
||||
|
||||
for _, indexSQL := range indexes {
|
||||
err := m.db.Exec(indexSQL).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createStatisticsDashboardsIndexes 创建统计仪表板表索引
|
||||
func (m *StatisticsMigrationComplete) createStatisticsDashboardsIndexes() error {
|
||||
indexes := []string{
|
||||
// 复合索引:user_role + is_active
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_role_active
|
||||
ON statistics_dashboards (user_role, is_active)`,
|
||||
|
||||
// 复合索引:user_role + is_default
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_role_default
|
||||
ON statistics_dashboards (user_role, is_default)`,
|
||||
|
||||
// 单列索引:created_by
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_created_by
|
||||
ON statistics_dashboards (created_by)`,
|
||||
|
||||
// 单列索引:access_level
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_access_level
|
||||
ON statistics_dashboards (access_level)`,
|
||||
|
||||
// 单列索引:name(用于搜索)
|
||||
`CREATE INDEX IF NOT EXISTS idx_statistics_dashboards_name
|
||||
ON statistics_dashboards (name)`,
|
||||
}
|
||||
|
||||
for _, indexSQL := range indexes {
|
||||
err := m.db.Exec(indexSQL).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertInitialData 插入初始数据
|
||||
func (m *StatisticsMigrationComplete) insertInitialData() error {
|
||||
fmt.Println("插入统计模块初始数据...")
|
||||
|
||||
// 插入默认仪表板
|
||||
err := m.insertDefaultDashboards()
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入默认仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 插入初始指标数据
|
||||
err = m.insertInitialMetrics()
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入初始指标数据失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块初始数据插入完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertDefaultDashboards 插入默认仪表板
|
||||
func (m *StatisticsMigrationComplete) insertDefaultDashboards() error {
|
||||
// 检查是否已存在默认仪表板
|
||||
var count int64
|
||||
err := m.db.Model(&entities.StatisticsDashboard{}).Where("is_default = ?", true).Count(&count).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查默认仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果已存在默认仪表板,跳过插入
|
||||
if count > 0 {
|
||||
fmt.Println("默认仪表板已存在,跳过插入")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 管理员默认仪表板
|
||||
adminDashboard := &entities.StatisticsDashboard{
|
||||
Name: "管理员仪表板",
|
||||
Description: "系统管理员专用仪表板,包含所有统计信息",
|
||||
UserRole: "admin",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 300,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 3, "rows": 4}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}, {"type": "finance", "position": {"x": 2, "y": 0}}]`,
|
||||
Settings: `{"theme": "dark", "auto_refresh": true}`,
|
||||
}
|
||||
|
||||
err = m.db.Create(adminDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建管理员仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 用户默认仪表板
|
||||
userDashboard := &entities.StatisticsDashboard{
|
||||
Name: "用户仪表板",
|
||||
Description: "普通用户专用仪表板,包含基础统计信息",
|
||||
UserRole: "user",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 600,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 2, "rows": 3}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}]`,
|
||||
Settings: `{"theme": "light", "auto_refresh": false}`,
|
||||
}
|
||||
|
||||
err = m.db.Create(userDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建用户仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 经理默认仪表板
|
||||
managerDashboard := &entities.StatisticsDashboard{
|
||||
Name: "经理仪表板",
|
||||
Description: "经理专用仪表板,包含管理相关统计信息",
|
||||
UserRole: "manager",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 300,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 3, "rows": 3}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}, {"type": "finance", "position": {"x": 2, "y": 0}}]`,
|
||||
Settings: `{"theme": "dark", "auto_refresh": true}`,
|
||||
}
|
||||
|
||||
err = m.db.Create(managerDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建经理仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
// 分析师默认仪表板
|
||||
analystDashboard := &entities.StatisticsDashboard{
|
||||
Name: "分析师仪表板",
|
||||
Description: "数据分析师专用仪表板,包含详细分析信息",
|
||||
UserRole: "analyst",
|
||||
IsDefault: true,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 180,
|
||||
CreatedBy: "system",
|
||||
Layout: `{"columns": 4, "rows": 4}`,
|
||||
Widgets: `[{"type": "api_calls", "position": {"x": 0, "y": 0}}, {"type": "users", "position": {"x": 1, "y": 0}}, {"type": "finance", "position": {"x": 2, "y": 0}}, {"type": "products", "position": {"x": 3, "y": 0}}]`,
|
||||
Settings: `{"theme": "dark", "auto_refresh": true, "show_trends": true}`,
|
||||
}
|
||||
|
||||
err = m.db.Create(analystDashboard).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建分析师仪表板失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("默认仪表板创建完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertInitialMetrics 插入初始指标数据
|
||||
func (m *StatisticsMigrationComplete) insertInitialMetrics() error {
|
||||
now := time.Now()
|
||||
today := now.Truncate(24 * time.Hour)
|
||||
|
||||
// 检查是否已存在今日指标数据
|
||||
var count int64
|
||||
err := m.db.Model(&entities.StatisticsMetric{}).Where("date = ?", today).Count(&count).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查指标数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果已存在今日指标数据,跳过插入
|
||||
if count > 0 {
|
||||
fmt.Println("今日指标数据已存在,跳过插入")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 插入初始API调用指标
|
||||
apiMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "total_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "success_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "failed_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "response_time",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "api_calls",
|
||||
MetricName: "avg_response_time",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始用户指标
|
||||
userMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "users",
|
||||
MetricName: "total_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "users",
|
||||
MetricName: "certified_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "users",
|
||||
MetricName: "active_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "users",
|
||||
MetricName: "new_users_today",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始财务指标
|
||||
financeMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "total_amount",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "recharge_amount",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "deduct_amount",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "recharge_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "finance",
|
||||
MetricName: "deduct_count",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始产品指标
|
||||
productMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "total_products",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "active_products",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "total_subscriptions",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "active_subscriptions",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "products",
|
||||
MetricName: "new_subscriptions_today",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入初始认证指标
|
||||
certificationMetrics := []*entities.StatisticsMetric{
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "total_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "completed_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "pending_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "failed_certifications",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
{
|
||||
MetricType: "certification",
|
||||
MetricName: "certification_rate",
|
||||
Dimension: "realtime",
|
||||
Value: 0,
|
||||
Date: today,
|
||||
},
|
||||
}
|
||||
|
||||
// 批量插入所有指标
|
||||
allMetrics := append(apiMetrics, userMetrics...)
|
||||
allMetrics = append(allMetrics, financeMetrics...)
|
||||
allMetrics = append(allMetrics, productMetrics...)
|
||||
allMetrics = append(allMetrics, certificationMetrics...)
|
||||
|
||||
err = m.db.CreateInBatches(allMetrics, 100).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("批量插入初始指标失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("初始指标数据创建完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback 回滚迁移
|
||||
func (m *StatisticsMigrationComplete) Rollback() error {
|
||||
fmt.Println("开始回滚统计模块数据迁移...")
|
||||
|
||||
// 删除表
|
||||
err := m.db.Migrator().DropTable(&entities.StatisticsDashboard{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除统计仪表板表失败: %w", err)
|
||||
}
|
||||
|
||||
err = m.db.Migrator().DropTable(&entities.StatisticsReport{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除统计报告表失败: %w", err)
|
||||
}
|
||||
|
||||
err = m.db.Migrator().DropTable(&entities.StatisticsMetric{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除统计指标表失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("统计模块数据迁移回滚完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTableInfo 获取表信息
|
||||
func (m *StatisticsMigrationComplete) GetTableInfo() map[string]interface{} {
|
||||
info := make(map[string]interface{})
|
||||
|
||||
// 获取表统计信息
|
||||
tables := []string{"statistics_metrics", "statistics_reports", "statistics_dashboards"}
|
||||
|
||||
for _, table := range tables {
|
||||
var count int64
|
||||
m.db.Table(table).Count(&count)
|
||||
info[table] = count
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
1
internal/infrastructure/task/README.md
Normal file
1
internal/infrastructure/task/README.md
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ArticlePublisher 文章发布接口
|
||||
type ArticlePublisher interface {
|
||||
PublishArticleByID(ctx context.Context, articleID string) error
|
||||
}
|
||||
|
||||
// ArticleTaskHandler 文章任务处理器
|
||||
type ArticleTaskHandler struct {
|
||||
publisher ArticlePublisher
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewArticleTaskHandler 创建文章任务处理器
|
||||
func NewArticleTaskHandler(
|
||||
publisher ArticlePublisher,
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository,
|
||||
logger *zap.Logger,
|
||||
) *ArticleTaskHandler {
|
||||
return &ArticleTaskHandler{
|
||||
publisher: publisher,
|
||||
scheduledTaskRepo: scheduledTaskRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleArticlePublish 处理文章定时发布任务
|
||||
func (h *ArticleTaskHandler) HandleArticlePublish(ctx context.Context, t *asynq.Task) error {
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析任务载荷失败", zap.Error(err))
|
||||
return fmt.Errorf("解析任务载荷失败: %w", err)
|
||||
}
|
||||
|
||||
articleID, ok := payload["article_id"].(string)
|
||||
if !ok {
|
||||
h.logger.Error("任务载荷中缺少文章ID")
|
||||
return fmt.Errorf("任务载荷中缺少文章ID")
|
||||
}
|
||||
|
||||
// 获取任务状态记录
|
||||
task, err := h.scheduledTaskRepo.GetByTaskID(ctx, t.ResultWriter().TaskID())
|
||||
if err != nil {
|
||||
h.logger.Error("获取任务状态记录失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(err))
|
||||
// 继续执行,不阻断任务
|
||||
} else {
|
||||
// 检查任务是否已取消
|
||||
if task.IsCancelled() {
|
||||
h.logger.Info("任务已取消,跳过执行", zap.String("task_id", t.ResultWriter().TaskID()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 标记任务为正在执行
|
||||
task.MarkAsRunning()
|
||||
if err := h.scheduledTaskRepo.Update(ctx, task); err != nil {
|
||||
h.logger.Warn("更新任务状态失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 执行文章发布
|
||||
if err := h.publisher.PublishArticleByID(ctx, articleID); err != nil {
|
||||
// 更新任务状态为失败
|
||||
if task.ID != "" {
|
||||
task.MarkAsFailed(err.Error())
|
||||
if updateErr := h.scheduledTaskRepo.Update(ctx, task); updateErr != nil {
|
||||
h.logger.Warn("更新任务失败状态失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(updateErr))
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Error("定时发布文章失败",
|
||||
zap.String("article_id", articleID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("定时发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新任务状态为已完成
|
||||
if task.ID != "" {
|
||||
task.MarkAsCompleted()
|
||||
if err := h.scheduledTaskRepo.Update(ctx, task); err != nil {
|
||||
h.logger.Warn("更新任务完成状态失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("定时发布文章成功", zap.String("article_id", articleID))
|
||||
return nil
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AsynqClient Asynq 客户端
|
||||
type AsynqClient struct {
|
||||
client *asynq.Client
|
||||
logger *zap.Logger
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository
|
||||
}
|
||||
|
||||
// NewAsynqClient 创建 Asynq 客户端
|
||||
func NewAsynqClient(redisAddr string, scheduledTaskRepo repositories.ScheduledTaskRepository, logger *zap.Logger) *AsynqClient {
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
|
||||
return &AsynqClient{
|
||||
client: client,
|
||||
logger: logger,
|
||||
scheduledTaskRepo: scheduledTaskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭客户端
|
||||
func (c *AsynqClient) Close() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
// ScheduleArticlePublish 调度文章定时发布任务
|
||||
func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID string, publishTime time.Time) (string, error) {
|
||||
payload := map[string]interface{}{
|
||||
"article_id": articleID,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
c.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return "", fmt.Errorf("创建任务失败: %w", err)
|
||||
}
|
||||
|
||||
task := asynq.NewTask(TaskTypeArticlePublish, payloadBytes)
|
||||
|
||||
// 计算延迟时间
|
||||
delay := publishTime.Sub(time.Now())
|
||||
if delay <= 0 {
|
||||
return "", fmt.Errorf("定时发布时间不能早于当前时间")
|
||||
}
|
||||
|
||||
// 设置任务选项
|
||||
opts := []asynq.Option{
|
||||
asynq.ProcessIn(delay),
|
||||
asynq.MaxRetry(3),
|
||||
asynq.Timeout(5 * time.Minute),
|
||||
}
|
||||
|
||||
info, err := c.client.Enqueue(task, opts...)
|
||||
if err != nil {
|
||||
c.logger.Error("调度定时发布任务失败",
|
||||
zap.String("article_id", articleID),
|
||||
zap.Time("publish_time", publishTime),
|
||||
zap.Error(err))
|
||||
return "", fmt.Errorf("调度任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建任务状态记录
|
||||
scheduledTask := entities.ScheduledTask{
|
||||
TaskID: info.ID,
|
||||
TaskType: TaskTypeArticlePublish,
|
||||
ArticleID: articleID,
|
||||
Status: entities.TaskStatusPending,
|
||||
ScheduledAt: publishTime,
|
||||
}
|
||||
|
||||
if _, err := c.scheduledTaskRepo.Create(ctx, scheduledTask); err != nil {
|
||||
c.logger.Error("创建任务状态记录失败", zap.String("task_id", info.ID), zap.Error(err))
|
||||
// 不返回错误,因为Asynq任务已经创建成功
|
||||
}
|
||||
|
||||
c.logger.Info("定时发布任务调度成功",
|
||||
zap.String("article_id", articleID),
|
||||
zap.Time("publish_time", publishTime),
|
||||
zap.String("task_id", info.ID))
|
||||
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
// CancelScheduledTask 取消已调度的任务
|
||||
func (c *AsynqClient) CancelScheduledTask(ctx context.Context, taskID string) error {
|
||||
c.logger.Info("标记定时任务为已取消",
|
||||
zap.String("task_id", taskID))
|
||||
|
||||
// 标记数据库中的任务状态为已取消
|
||||
if err := c.scheduledTaskRepo.MarkAsCancelled(ctx, taskID); err != nil {
|
||||
c.logger.Warn("标记任务状态为已取消失败", zap.String("task_id", taskID), zap.Error(err))
|
||||
// 不返回错误,因为Asynq任务可能已经执行完成
|
||||
}
|
||||
|
||||
// Asynq不支持直接取消任务,我们通过数据库状态来标记
|
||||
// 任务执行时会检查文章状态,如果已取消则跳过执行
|
||||
return nil
|
||||
}
|
||||
|
||||
// RescheduleArticlePublish 重新调度文章定时发布任务
|
||||
func (c *AsynqClient) RescheduleArticlePublish(ctx context.Context, articleID string, oldTaskID string, newPublishTime time.Time) (string, error) {
|
||||
// 1. 取消旧任务(标记为已取消)
|
||||
if err := c.CancelScheduledTask(ctx, oldTaskID); err != nil {
|
||||
c.logger.Warn("取消旧任务失败",
|
||||
zap.String("old_task_id", oldTaskID),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
// 2. 创建新任务
|
||||
newTaskID, err := c.ScheduleArticlePublish(ctx, articleID, newPublishTime)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("重新调度任务失败: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("重新调度定时发布任务成功",
|
||||
zap.String("article_id", articleID),
|
||||
zap.String("old_task_id", oldTaskID),
|
||||
zap.String("new_task_id", newTaskID),
|
||||
zap.Time("new_publish_time", newPublishTime))
|
||||
|
||||
return newTaskID, nil
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AsynqWorker Asynq Worker
|
||||
type AsynqWorker struct {
|
||||
server *asynq.Server
|
||||
mux *asynq.ServeMux
|
||||
taskHandler *ArticleTaskHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAsynqWorker 创建 Asynq Worker
|
||||
func NewAsynqWorker(
|
||||
redisAddr string,
|
||||
taskHandler *ArticleTaskHandler,
|
||||
logger *zap.Logger,
|
||||
) *AsynqWorker {
|
||||
server := asynq.NewServer(
|
||||
asynq.RedisClientOpt{Addr: redisAddr},
|
||||
asynq.Config{
|
||||
Concurrency: 10, // 并发数
|
||||
Queues: map[string]int{
|
||||
"critical": 6,
|
||||
"default": 3,
|
||||
"low": 1,
|
||||
},
|
||||
Logger: NewAsynqLogger(logger),
|
||||
},
|
||||
)
|
||||
|
||||
mux := asynq.NewServeMux()
|
||||
|
||||
return &AsynqWorker{
|
||||
server: server,
|
||||
mux: mux,
|
||||
taskHandler: taskHandler,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHandlers 注册任务处理器
|
||||
func (w *AsynqWorker) RegisterHandlers() {
|
||||
// 注册文章定时发布任务处理器
|
||||
w.mux.HandleFunc(TaskTypeArticlePublish, w.taskHandler.HandleArticlePublish)
|
||||
|
||||
w.logger.Info("任务处理器注册完成")
|
||||
}
|
||||
|
||||
// Start 启动 Worker
|
||||
func (w *AsynqWorker) Start() error {
|
||||
w.RegisterHandlers()
|
||||
|
||||
w.logger.Info("启动 Asynq Worker")
|
||||
return w.server.Run(w.mux)
|
||||
}
|
||||
|
||||
// Stop 停止 Worker
|
||||
func (w *AsynqWorker) Stop() {
|
||||
w.logger.Info("停止 Asynq Worker")
|
||||
w.server.Stop()
|
||||
w.server.Shutdown()
|
||||
}
|
||||
|
||||
// AsynqLogger Asynq 日志适配器
|
||||
type AsynqLogger struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAsynqLogger 创建 Asynq 日志适配器
|
||||
func NewAsynqLogger(logger *zap.Logger) *AsynqLogger {
|
||||
return &AsynqLogger{logger: logger}
|
||||
}
|
||||
|
||||
func (l *AsynqLogger) Debug(args ...interface{}) {
|
||||
l.logger.Debug(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (l *AsynqLogger) Info(args ...interface{}) {
|
||||
l.logger.Info(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (l *AsynqLogger) Warn(args ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (l *AsynqLogger) Error(args ...interface{}) {
|
||||
l.logger.Error(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (l *AsynqLogger) Fatal(args ...interface{}) {
|
||||
l.logger.Fatal(fmt.Sprint(args...))
|
||||
}
|
||||
68
internal/infrastructure/task/entities/async_task.go
Normal file
68
internal/infrastructure/task/entities/async_task.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TaskStatus 任务状态
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
TaskStatusPending TaskStatus = "pending"
|
||||
TaskStatusRunning TaskStatus = "running"
|
||||
TaskStatusCompleted TaskStatus = "completed"
|
||||
TaskStatusFailed TaskStatus = "failed"
|
||||
TaskStatusCancelled TaskStatus = "cancelled"
|
||||
)
|
||||
|
||||
// AsyncTask 异步任务实体
|
||||
type AsyncTask struct {
|
||||
ID string `gorm:"type:char(36);primaryKey"`
|
||||
Type string `gorm:"not null;index"`
|
||||
Payload string `gorm:"type:text"`
|
||||
Status TaskStatus `gorm:"not null;index"`
|
||||
ScheduledAt *time.Time `gorm:"index"`
|
||||
StartedAt *time.Time
|
||||
CompletedAt *time.Time
|
||||
ErrorMsg string
|
||||
RetryCount int `gorm:"default:0"`
|
||||
MaxRetries int `gorm:"default:5"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (AsyncTask) TableName() string {
|
||||
return "async_tasks"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子,在创建前生成UUID
|
||||
func (t *AsyncTask) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.ID == "" {
|
||||
t.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsCompleted 检查任务是否已完成
|
||||
func (t *AsyncTask) IsCompleted() bool {
|
||||
return t.Status == TaskStatusCompleted
|
||||
}
|
||||
|
||||
// IsFailed 检查任务是否失败
|
||||
func (t *AsyncTask) IsFailed() bool {
|
||||
return t.Status == TaskStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查任务是否已取消
|
||||
func (t *AsyncTask) IsCancelled() bool {
|
||||
return t.Status == TaskStatusCancelled
|
||||
}
|
||||
|
||||
// CanRetry 检查任务是否可以重试
|
||||
func (t *AsyncTask) CanRetry() bool {
|
||||
return t.Status == TaskStatusFailed && t.RetryCount < t.MaxRetries
|
||||
}
|
||||
335
internal/infrastructure/task/entities/task_factory.go
Normal file
335
internal/infrastructure/task/entities/task_factory.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// TaskFactory 任务工厂
|
||||
type TaskFactory struct {
|
||||
taskManager interface{} // 使用interface{}避免循环导入
|
||||
}
|
||||
|
||||
// NewTaskFactory 创建任务工厂
|
||||
func NewTaskFactory() *TaskFactory {
|
||||
return &TaskFactory{}
|
||||
}
|
||||
|
||||
// NewTaskFactoryWithManager 创建带管理器的任务工厂
|
||||
func NewTaskFactoryWithManager(taskManager interface{}) *TaskFactory {
|
||||
return &TaskFactory{
|
||||
taskManager: taskManager,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateArticlePublishTask 创建文章发布任务
|
||||
func (f *TaskFactory) CreateArticlePublishTask(articleID string, publishAt time.Time, userID string) (*AsyncTask, error) {
|
||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||
task := &AsyncTask{
|
||||
Type: string(types.TaskTypeArticlePublish),
|
||||
Status: TaskStatusPending,
|
||||
ScheduledAt: &publishAt,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 在payload中添加任务ID(将在保存后更新)
|
||||
payloadWithID := map[string]interface{}{
|
||||
"article_id": articleID,
|
||||
"publish_at": publishAt,
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Payload = string(payloadDataWithID)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateArticleCancelTask 创建文章取消任务
|
||||
func (f *TaskFactory) CreateArticleCancelTask(articleID string, userID string) (*AsyncTask, error) {
|
||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||
task := &AsyncTask{
|
||||
Type: string(types.TaskTypeArticleCancel),
|
||||
Status: TaskStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 在payload中添加任务数据
|
||||
payloadWithID := map[string]interface{}{
|
||||
"article_id": articleID,
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Payload = string(payloadDataWithID)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateArticleModifyTask 创建文章修改任务
|
||||
func (f *TaskFactory) CreateArticleModifyTask(articleID string, newPublishAt time.Time, userID string) (*AsyncTask, error) {
|
||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||
task := &AsyncTask{
|
||||
Type: string(types.TaskTypeArticleModify),
|
||||
Status: TaskStatusPending,
|
||||
ScheduledAt: &newPublishAt,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 在payload中添加任务数据
|
||||
payloadWithID := map[string]interface{}{
|
||||
"article_id": articleID,
|
||||
"new_publish_at": newPublishAt,
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Payload = string(payloadDataWithID)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateApiCallTask 创建API调用任务
|
||||
func (f *TaskFactory) CreateApiCallTask(apiCallID string, userID string, productID string, amount string) (*AsyncTask, error) {
|
||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||
task := &AsyncTask{
|
||||
Type: string(types.TaskTypeApiCall),
|
||||
Status: TaskStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 在payload中添加任务数据
|
||||
payloadWithID := map[string]interface{}{
|
||||
"api_call_id": apiCallID,
|
||||
"user_id": userID,
|
||||
"product_id": productID,
|
||||
"amount": amount,
|
||||
}
|
||||
|
||||
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Payload = string(payloadDataWithID)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateDeductionTask 创建扣款任务
|
||||
func (f *TaskFactory) CreateDeductionTask(apiCallID string, userID string, productID string, amount string, transactionID string) (*AsyncTask, error) {
|
||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||
task := &AsyncTask{
|
||||
Type: string(types.TaskTypeDeduction),
|
||||
Status: TaskStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 在payload中添加任务数据
|
||||
payloadWithID := map[string]interface{}{
|
||||
"api_call_id": apiCallID,
|
||||
"user_id": userID,
|
||||
"product_id": productID,
|
||||
"amount": amount,
|
||||
"transaction_id": transactionID,
|
||||
}
|
||||
|
||||
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Payload = string(payloadDataWithID)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateApiCallLogTask 创建API调用日志任务
|
||||
func (f *TaskFactory) CreateApiCallLogTask(transactionID string, userID string, apiName string, productID string) (*AsyncTask, error) {
|
||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||
task := &AsyncTask{
|
||||
Type: string(types.TaskTypeApiLog),
|
||||
Status: TaskStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 在payload中添加任务数据
|
||||
payloadWithID := map[string]interface{}{
|
||||
"transaction_id": transactionID,
|
||||
"user_id": userID,
|
||||
"api_name": apiName,
|
||||
"product_id": productID,
|
||||
}
|
||||
|
||||
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Payload = string(payloadDataWithID)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateUsageStatsTask 创建使用统计任务
|
||||
func (f *TaskFactory) CreateUsageStatsTask(subscriptionID string, userID string, productID string, increment int) (*AsyncTask, error) {
|
||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||
task := &AsyncTask{
|
||||
Type: string(types.TaskTypeUsageStats),
|
||||
Status: TaskStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 在payload中添加任务数据
|
||||
payloadWithID := map[string]interface{}{
|
||||
"subscription_id": subscriptionID,
|
||||
"user_id": userID,
|
||||
"product_id": productID,
|
||||
"increment": increment,
|
||||
}
|
||||
|
||||
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Payload = string(payloadDataWithID)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CreateAndEnqueueArticlePublishTask 创建并入队文章发布任务
|
||||
func (f *TaskFactory) CreateAndEnqueueArticlePublishTask(ctx context.Context, articleID string, publishAt time.Time, userID string) error {
|
||||
if f.taskManager == nil {
|
||||
return fmt.Errorf("TaskManager未初始化")
|
||||
}
|
||||
|
||||
task, err := f.CreateArticlePublishTask(articleID, publishAt, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delay := publishAt.Sub(time.Now())
|
||||
if delay < 0 {
|
||||
delay = 0
|
||||
}
|
||||
|
||||
// 使用类型断言调用TaskManager方法
|
||||
if tm, ok := f.taskManager.(interface {
|
||||
CreateAndEnqueueDelayedTask(ctx context.Context, task *AsyncTask, delay time.Duration) error
|
||||
}); ok {
|
||||
return tm.CreateAndEnqueueDelayedTask(ctx, task, delay)
|
||||
}
|
||||
|
||||
return fmt.Errorf("TaskManager类型不匹配")
|
||||
}
|
||||
|
||||
// CreateAndEnqueueApiLogTask 创建并入队API日志任务
|
||||
func (f *TaskFactory) CreateAndEnqueueApiLogTask(ctx context.Context, transactionID string, userID string, apiName string, productID string) error {
|
||||
if f.taskManager == nil {
|
||||
return fmt.Errorf("TaskManager未初始化")
|
||||
}
|
||||
|
||||
task, err := f.CreateApiCallLogTask(transactionID, userID, apiName, productID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用类型断言调用TaskManager方法
|
||||
if tm, ok := f.taskManager.(interface {
|
||||
CreateAndEnqueueTask(ctx context.Context, task *AsyncTask) error
|
||||
}); ok {
|
||||
return tm.CreateAndEnqueueTask(ctx, task)
|
||||
}
|
||||
|
||||
return fmt.Errorf("TaskManager类型不匹配")
|
||||
}
|
||||
|
||||
// CreateAndEnqueueApiCallTask 创建并入队API调用任务
|
||||
func (f *TaskFactory) CreateAndEnqueueApiCallTask(ctx context.Context, apiCallID string, userID string, productID string, amount string) error {
|
||||
if f.taskManager == nil {
|
||||
return fmt.Errorf("TaskManager未初始化")
|
||||
}
|
||||
|
||||
task, err := f.CreateApiCallTask(apiCallID, userID, productID, amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用类型断言调用TaskManager方法
|
||||
if tm, ok := f.taskManager.(interface {
|
||||
CreateAndEnqueueTask(ctx context.Context, task *AsyncTask) error
|
||||
}); ok {
|
||||
return tm.CreateAndEnqueueTask(ctx, task)
|
||||
}
|
||||
|
||||
return fmt.Errorf("TaskManager类型不匹配")
|
||||
}
|
||||
|
||||
// CreateAndEnqueueDeductionTask 创建并入队扣款任务
|
||||
func (f *TaskFactory) CreateAndEnqueueDeductionTask(ctx context.Context, apiCallID string, userID string, productID string, amount string, transactionID string) error {
|
||||
if f.taskManager == nil {
|
||||
return fmt.Errorf("TaskManager未初始化")
|
||||
}
|
||||
|
||||
task, err := f.CreateDeductionTask(apiCallID, userID, productID, amount, transactionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用类型断言调用TaskManager方法
|
||||
if tm, ok := f.taskManager.(interface {
|
||||
CreateAndEnqueueTask(ctx context.Context, task *AsyncTask) error
|
||||
}); ok {
|
||||
return tm.CreateAndEnqueueTask(ctx, task)
|
||||
}
|
||||
|
||||
return fmt.Errorf("TaskManager类型不匹配")
|
||||
}
|
||||
|
||||
// CreateAndEnqueueUsageStatsTask 创建并入队使用统计任务
|
||||
func (f *TaskFactory) CreateAndEnqueueUsageStatsTask(ctx context.Context, subscriptionID string, userID string, productID string, increment int) error {
|
||||
if f.taskManager == nil {
|
||||
return fmt.Errorf("TaskManager未初始化")
|
||||
}
|
||||
|
||||
task, err := f.CreateUsageStatsTask(subscriptionID, userID, productID, increment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用类型断言调用TaskManager方法
|
||||
if tm, ok := f.taskManager.(interface {
|
||||
CreateAndEnqueueTask(ctx context.Context, task *AsyncTask) error
|
||||
}); ok {
|
||||
return tm.CreateAndEnqueueTask(ctx, task)
|
||||
}
|
||||
|
||||
return fmt.Errorf("TaskManager类型不匹配")
|
||||
}
|
||||
|
||||
// generateRandomString 生成随机字符串
|
||||
func generateRandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
45
internal/infrastructure/task/factory.go
Normal file
45
internal/infrastructure/task/factory.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/infrastructure/task/implementations/asynq"
|
||||
"tyapi-server/internal/infrastructure/task/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TaskFactory 任务工厂
|
||||
type TaskFactory struct{}
|
||||
|
||||
// NewTaskFactory 创建任务工厂
|
||||
func NewTaskFactory() *TaskFactory {
|
||||
return &TaskFactory{}
|
||||
}
|
||||
|
||||
// CreateApiTaskQueue 创建API任务队列
|
||||
func (f *TaskFactory) CreateApiTaskQueue(redisAddr string, logger interface{}) interfaces.ApiTaskQueue {
|
||||
// 这里可以根据配置选择不同的实现
|
||||
// 目前使用Asynq实现
|
||||
return asynq.NewAsynqApiTaskQueue(redisAddr, logger.(*zap.Logger))
|
||||
}
|
||||
|
||||
// CreateArticleTaskQueue 创建文章任务队列
|
||||
func (f *TaskFactory) CreateArticleTaskQueue(redisAddr string, logger interface{}) interfaces.ArticleTaskQueue {
|
||||
// 这里可以根据配置选择不同的实现
|
||||
// 目前使用Asynq实现
|
||||
return asynq.NewAsynqArticleTaskQueue(redisAddr, logger.(*zap.Logger))
|
||||
}
|
||||
|
||||
// NewApiTaskQueue 创建API任务队列(包级别函数)
|
||||
func NewApiTaskQueue(redisAddr string, logger *zap.Logger) interfaces.ApiTaskQueue {
|
||||
return asynq.NewAsynqApiTaskQueue(redisAddr, logger)
|
||||
}
|
||||
|
||||
// NewAsynqClient 创建Asynq客户端(包级别函数)
|
||||
func NewAsynqClient(redisAddr string, scheduledTaskRepo interface{}, logger *zap.Logger) *asynq.AsynqClient {
|
||||
return asynq.NewAsynqClient(redisAddr, logger)
|
||||
}
|
||||
|
||||
// NewArticleTaskQueue 创建文章任务队列(包级别函数)
|
||||
func NewArticleTaskQueue(redisAddr string, logger *zap.Logger) interfaces.ArticleTaskQueue {
|
||||
return asynq.NewAsynqArticleTaskQueue(redisAddr, logger)
|
||||
}
|
||||
285
internal/infrastructure/task/handlers/api_task_handler.go
Normal file
285
internal/infrastructure/task/handlers/api_task_handler.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/api"
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_services "tyapi-server/internal/domains/product/services"
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/repositories"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// ApiTaskHandler API任务处理器
|
||||
type ApiTaskHandler struct {
|
||||
logger *zap.Logger
|
||||
apiApplicationService api.ApiApplicationService
|
||||
walletService finance_services.WalletAggregateService
|
||||
subscriptionService *product_services.ProductSubscriptionService
|
||||
asyncTaskRepo repositories.AsyncTaskRepository
|
||||
}
|
||||
|
||||
// NewApiTaskHandler 创建API任务处理器
|
||||
func NewApiTaskHandler(
|
||||
logger *zap.Logger,
|
||||
apiApplicationService api.ApiApplicationService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
subscriptionService *product_services.ProductSubscriptionService,
|
||||
asyncTaskRepo repositories.AsyncTaskRepository,
|
||||
) *ApiTaskHandler {
|
||||
return &ApiTaskHandler{
|
||||
logger: logger,
|
||||
apiApplicationService: apiApplicationService,
|
||||
walletService: walletService,
|
||||
subscriptionService: subscriptionService,
|
||||
asyncTaskRepo: asyncTaskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleApiCall 处理API调用任务
|
||||
func (h *ApiTaskHandler) HandleApiCall(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理API调用任务")
|
||||
|
||||
var payload types.ApiCallPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析API调用任务载荷失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "解析任务载荷失败")
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理API调用任务",
|
||||
zap.String("api_call_id", payload.ApiCallID),
|
||||
zap.String("user_id", payload.UserID),
|
||||
zap.String("product_id", payload.ProductID))
|
||||
|
||||
// 这里实现API调用的具体逻辑
|
||||
// 例如:记录API调用、更新使用统计等
|
||||
|
||||
// 更新任务状态为成功
|
||||
h.updateTaskStatus(ctx, t, "completed", "")
|
||||
h.logger.Info("API调用任务处理完成", zap.String("api_call_id", payload.ApiCallID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleDeduction 处理扣款任务
|
||||
func (h *ApiTaskHandler) HandleDeduction(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理扣款任务")
|
||||
|
||||
var payload types.DeductionPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析扣款任务载荷失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "解析任务载荷失败")
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理扣款任务",
|
||||
zap.String("user_id", payload.UserID),
|
||||
zap.String("amount", payload.Amount),
|
||||
zap.String("transaction_id", payload.TransactionID))
|
||||
|
||||
// 调用钱包服务进行扣款
|
||||
if h.walletService != nil {
|
||||
amount, err := decimal.NewFromString(payload.Amount)
|
||||
if err != nil {
|
||||
h.logger.Error("金额格式错误", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "金额格式错误")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.walletService.Deduct(ctx, payload.UserID, amount, payload.ApiCallID, payload.TransactionID, payload.ProductID); err != nil {
|
||||
h.logger.Error("扣款处理失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "扣款处理失败: "+err.Error())
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("钱包服务未初始化,跳过扣款", zap.String("user_id", payload.UserID))
|
||||
h.updateTaskStatus(ctx, t, "failed", "钱包服务未初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新任务状态为成功
|
||||
h.updateTaskStatus(ctx, t, "completed", "")
|
||||
h.logger.Info("扣款任务处理完成", zap.String("transaction_id", payload.TransactionID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleCompensation 处理补偿任务
|
||||
func (h *ApiTaskHandler) HandleCompensation(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理补偿任务")
|
||||
|
||||
var payload types.CompensationPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析补偿任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理补偿任务",
|
||||
zap.String("transaction_id", payload.TransactionID),
|
||||
zap.String("type", payload.Type))
|
||||
|
||||
// 这里实现补偿的具体逻辑
|
||||
// 例如:调用钱包服务进行退款等
|
||||
|
||||
h.logger.Info("补偿任务处理完成", zap.String("transaction_id", payload.TransactionID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleUsageStats 处理使用统计任务
|
||||
func (h *ApiTaskHandler) HandleUsageStats(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理使用统计任务")
|
||||
|
||||
var payload types.UsageStatsPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析使用统计任务载荷失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "解析任务载荷失败")
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理使用统计任务",
|
||||
zap.String("subscription_id", payload.SubscriptionID),
|
||||
zap.String("user_id", payload.UserID),
|
||||
zap.Int("increment", payload.Increment))
|
||||
|
||||
// 调用订阅服务更新使用统计
|
||||
if h.subscriptionService != nil {
|
||||
if err := h.subscriptionService.IncrementSubscriptionAPIUsage(ctx, payload.SubscriptionID, int64(payload.Increment)); err != nil {
|
||||
h.logger.Error("更新使用统计失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "更新使用统计失败: "+err.Error())
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("订阅服务未初始化,跳过使用统计更新", zap.String("subscription_id", payload.SubscriptionID))
|
||||
h.updateTaskStatus(ctx, t, "failed", "订阅服务未初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新任务状态为成功
|
||||
h.updateTaskStatus(ctx, t, "completed", "")
|
||||
h.logger.Info("使用统计任务处理完成", zap.String("subscription_id", payload.SubscriptionID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleApiLog 处理API日志任务
|
||||
func (h *ApiTaskHandler) HandleApiLog(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理API日志任务")
|
||||
|
||||
var payload types.ApiLogPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析API日志任务载荷失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "解析任务载荷失败")
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理API日志任务",
|
||||
zap.String("transaction_id", payload.TransactionID),
|
||||
zap.String("user_id", payload.UserID),
|
||||
zap.String("api_name", payload.ApiName),
|
||||
zap.String("product_id", payload.ProductID))
|
||||
|
||||
// 记录结构化日志
|
||||
h.logger.Info("API调用日志",
|
||||
zap.String("transaction_id", payload.TransactionID),
|
||||
zap.String("user_id", payload.UserID),
|
||||
zap.String("api_name", payload.ApiName),
|
||||
zap.String("product_id", payload.ProductID),
|
||||
zap.Time("timestamp", time.Now()))
|
||||
|
||||
// 这里可以添加其他日志记录逻辑
|
||||
// 例如:写入专门的日志文件、发送到日志系统、写入数据库等
|
||||
|
||||
// 更新任务状态为成功
|
||||
h.updateTaskStatus(ctx, t, "completed", "")
|
||||
h.logger.Info("API日志任务处理完成", zap.String("transaction_id", payload.TransactionID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateTaskStatus 更新任务状态
|
||||
func (h *ApiTaskHandler) updateTaskStatus(ctx context.Context, t *asynq.Task, status string, errorMsg string) {
|
||||
// 从任务载荷中提取任务ID
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析任务载荷失败,无法更新状态", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试从payload中获取任务ID
|
||||
taskID, ok := payload["task_id"].(string)
|
||||
if !ok {
|
||||
h.logger.Error("无法从任务载荷中获取任务ID")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据状态决定更新方式
|
||||
if status == "failed" {
|
||||
// 失败时:需要检查是否达到最大重试次数
|
||||
h.handleTaskFailure(ctx, taskID, errorMsg)
|
||||
} else if status == "completed" {
|
||||
// 成功时:清除错误信息并更新状态
|
||||
if err := h.asyncTaskRepo.UpdateStatusWithSuccess(ctx, taskID, entities.TaskStatus(status)); err != nil {
|
||||
h.logger.Error("更新任务状态失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", status),
|
||||
zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
// 其他状态:只更新状态
|
||||
if err := h.asyncTaskRepo.UpdateStatus(ctx, taskID, entities.TaskStatus(status)); err != nil {
|
||||
h.logger.Error("更新任务状态失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", status),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("任务状态已更新",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", status),
|
||||
zap.String("error_msg", errorMsg))
|
||||
}
|
||||
|
||||
// handleTaskFailure 处理任务失败
|
||||
func (h *ApiTaskHandler) handleTaskFailure(ctx context.Context, taskID string, errorMsg string) {
|
||||
// 获取当前任务信息
|
||||
task, err := h.asyncTaskRepo.GetByID(ctx, taskID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取任务信息失败", zap.String("task_id", taskID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 增加重试次数
|
||||
newRetryCount := task.RetryCount + 1
|
||||
|
||||
// 检查是否达到最大重试次数
|
||||
if newRetryCount >= task.MaxRetries {
|
||||
// 达到最大重试次数,标记为最终失败
|
||||
if err := h.asyncTaskRepo.UpdateStatusWithRetryAndError(ctx, taskID, entities.TaskStatusFailed, errorMsg); err != nil {
|
||||
h.logger.Error("更新任务状态失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", "failed"),
|
||||
zap.Error(err))
|
||||
}
|
||||
h.logger.Info("任务最终失败,已达到最大重试次数",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Int("retry_count", newRetryCount),
|
||||
zap.Int("max_retries", task.MaxRetries))
|
||||
} else {
|
||||
// 未达到最大重试次数,保持pending状态,记录错误信息
|
||||
if err := h.asyncTaskRepo.UpdateRetryCountAndError(ctx, taskID, newRetryCount, errorMsg); err != nil {
|
||||
h.logger.Error("更新任务重试次数失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Int("retry_count", newRetryCount),
|
||||
zap.Error(err))
|
||||
}
|
||||
h.logger.Info("任务失败,准备重试",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Int("retry_count", newRetryCount),
|
||||
zap.Int("max_retries", task.MaxRetries))
|
||||
}
|
||||
}
|
||||
304
internal/infrastructure/task/handlers/article_task_handler.go
Normal file
304
internal/infrastructure/task/handlers/article_task_handler.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/article"
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/repositories"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// ArticleTaskHandler 文章任务处理器
|
||||
type ArticleTaskHandler struct {
|
||||
logger *zap.Logger
|
||||
articleApplicationService article.ArticleApplicationService
|
||||
asyncTaskRepo repositories.AsyncTaskRepository
|
||||
}
|
||||
|
||||
// NewArticleTaskHandler 创建文章任务处理器
|
||||
func NewArticleTaskHandler(logger *zap.Logger, articleApplicationService article.ArticleApplicationService, asyncTaskRepo repositories.AsyncTaskRepository) *ArticleTaskHandler {
|
||||
return &ArticleTaskHandler{
|
||||
logger: logger,
|
||||
articleApplicationService: articleApplicationService,
|
||||
asyncTaskRepo: asyncTaskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleArticlePublish 处理文章发布任务
|
||||
func (h *ArticleTaskHandler) HandleArticlePublish(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理文章发布任务")
|
||||
|
||||
var payload ArticlePublishPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析文章发布任务载荷失败", zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "解析任务载荷失败")
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理文章发布任务",
|
||||
zap.String("article_id", payload.ArticleID),
|
||||
zap.Time("publish_at", payload.PublishAt))
|
||||
|
||||
// 检查任务是否已被取消
|
||||
if err := h.checkTaskStatus(ctx, t); err != nil {
|
||||
h.logger.Info("任务已被取消,跳过执行", zap.String("article_id", payload.ArticleID))
|
||||
return nil // 静默返回,不报错
|
||||
}
|
||||
|
||||
// 调用文章应用服务发布文章
|
||||
if h.articleApplicationService != nil {
|
||||
err := h.articleApplicationService.PublishArticleByID(ctx, payload.ArticleID)
|
||||
if err != nil {
|
||||
h.logger.Error("文章发布失败", zap.String("article_id", payload.ArticleID), zap.Error(err))
|
||||
h.updateTaskStatus(ctx, t, "failed", "文章发布失败: "+err.Error())
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("文章应用服务未初始化,跳过发布", zap.String("article_id", payload.ArticleID))
|
||||
h.updateTaskStatus(ctx, t, "failed", "文章应用服务未初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新任务状态为成功
|
||||
h.updateTaskStatus(ctx, t, "completed", "")
|
||||
h.logger.Info("文章发布任务处理完成", zap.String("article_id", payload.ArticleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleArticleCancel 处理文章取消任务
|
||||
func (h *ArticleTaskHandler) HandleArticleCancel(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理文章取消任务")
|
||||
|
||||
var payload ArticleCancelPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析文章取消任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理文章取消任务", zap.String("article_id", payload.ArticleID))
|
||||
|
||||
// 这里实现文章取消的具体逻辑
|
||||
// 例如:更新文章状态、取消定时发布等
|
||||
|
||||
h.logger.Info("文章取消任务处理完成", zap.String("article_id", payload.ArticleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleArticleModify 处理文章修改任务
|
||||
func (h *ArticleTaskHandler) HandleArticleModify(ctx context.Context, t *asynq.Task) error {
|
||||
h.logger.Info("开始处理文章修改任务")
|
||||
|
||||
var payload ArticleModifyPayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析文章修改任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("处理文章修改任务",
|
||||
zap.String("article_id", payload.ArticleID),
|
||||
zap.Time("new_publish_at", payload.NewPublishAt))
|
||||
|
||||
// 这里实现文章修改的具体逻辑
|
||||
// 例如:更新文章发布时间、重新调度任务等
|
||||
|
||||
h.logger.Info("文章修改任务处理完成", zap.String("article_id", payload.ArticleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArticlePublishPayload 文章发布任务载荷
|
||||
type ArticlePublishPayload struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
PublishAt time.Time `json:"publish_at"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ArticlePublishPayload) GetType() types.TaskType {
|
||||
return types.TaskTypeArticlePublish
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ArticlePublishPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ArticlePublishPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// ArticleCancelPayload 文章取消任务载荷
|
||||
type ArticleCancelPayload struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ArticleCancelPayload) GetType() types.TaskType {
|
||||
return types.TaskTypeArticleCancel
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ArticleCancelPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ArticleCancelPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// ArticleModifyPayload 文章修改任务载荷
|
||||
type ArticleModifyPayload struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
NewPublishAt time.Time `json:"new_publish_at"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ArticleModifyPayload) GetType() types.TaskType {
|
||||
return types.TaskTypeArticleModify
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ArticleModifyPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ArticleModifyPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// updateTaskStatus 更新任务状态
|
||||
func (h *ArticleTaskHandler) updateTaskStatus(ctx context.Context, t *asynq.Task, status string, errorMsg string) {
|
||||
// 从任务载荷中提取任务ID
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析任务载荷失败,无法更新状态", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试从payload中获取任务ID
|
||||
taskID, ok := payload["task_id"].(string)
|
||||
if !ok {
|
||||
// 如果没有task_id,尝试从article_id生成
|
||||
if articleID, ok := payload["article_id"].(string); ok {
|
||||
taskID = fmt.Sprintf("article-publish-%s", articleID)
|
||||
} else {
|
||||
h.logger.Error("无法从任务载荷中获取任务ID")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态决定更新方式
|
||||
if status == "failed" {
|
||||
// 失败时:需要检查是否达到最大重试次数
|
||||
h.handleTaskFailure(ctx, taskID, errorMsg)
|
||||
} else if status == "completed" {
|
||||
// 成功时:清除错误信息并更新状态
|
||||
if err := h.asyncTaskRepo.UpdateStatusWithSuccess(ctx, taskID, entities.TaskStatus(status)); err != nil {
|
||||
h.logger.Error("更新任务状态失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", status),
|
||||
zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
// 其他状态:只更新状态
|
||||
if err := h.asyncTaskRepo.UpdateStatus(ctx, taskID, entities.TaskStatus(status)); err != nil {
|
||||
h.logger.Error("更新任务状态失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", status),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("任务状态已更新",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", status),
|
||||
zap.String("error_msg", errorMsg))
|
||||
}
|
||||
|
||||
// handleTaskFailure 处理任务失败
|
||||
func (h *ArticleTaskHandler) handleTaskFailure(ctx context.Context, taskID string, errorMsg string) {
|
||||
// 获取当前任务信息
|
||||
task, err := h.asyncTaskRepo.GetByID(ctx, taskID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取任务信息失败", zap.String("task_id", taskID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 增加重试次数
|
||||
newRetryCount := task.RetryCount + 1
|
||||
|
||||
// 检查是否达到最大重试次数
|
||||
if newRetryCount >= task.MaxRetries {
|
||||
// 达到最大重试次数,标记为最终失败
|
||||
if err := h.asyncTaskRepo.UpdateStatusWithRetryAndError(ctx, taskID, entities.TaskStatusFailed, errorMsg); err != nil {
|
||||
h.logger.Error("更新任务状态失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("status", "failed"),
|
||||
zap.Error(err))
|
||||
}
|
||||
h.logger.Info("任务最终失败,已达到最大重试次数",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Int("retry_count", newRetryCount),
|
||||
zap.Int("max_retries", task.MaxRetries))
|
||||
} else {
|
||||
// 未达到最大重试次数,保持pending状态,记录错误信息
|
||||
if err := h.asyncTaskRepo.UpdateRetryCountAndError(ctx, taskID, newRetryCount, errorMsg); err != nil {
|
||||
h.logger.Error("更新任务重试次数失败",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Int("retry_count", newRetryCount),
|
||||
zap.Error(err))
|
||||
}
|
||||
h.logger.Info("任务失败,准备重试",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Int("retry_count", newRetryCount),
|
||||
zap.Int("max_retries", task.MaxRetries))
|
||||
}
|
||||
}
|
||||
|
||||
// checkTaskStatus 检查任务状态
|
||||
func (h *ArticleTaskHandler) checkTaskStatus(ctx context.Context, t *asynq.Task) error {
|
||||
// 从任务载荷中提取任务ID
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析任务载荷失败,无法检查状态", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 尝试从payload中获取任务ID
|
||||
taskID, ok := payload["task_id"].(string)
|
||||
if !ok {
|
||||
// 如果没有task_id,尝试从article_id生成
|
||||
if articleID, ok := payload["article_id"].(string); ok {
|
||||
taskID = fmt.Sprintf("article-publish-%s", articleID)
|
||||
} else {
|
||||
h.logger.Error("无法从任务载荷中获取任务ID")
|
||||
return fmt.Errorf("无法获取任务ID")
|
||||
}
|
||||
}
|
||||
|
||||
// 查询任务状态
|
||||
task, err := h.asyncTaskRepo.GetByID(ctx, taskID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询任务状态失败", zap.String("task_id", taskID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查任务是否已被取消
|
||||
if task.Status == entities.TaskStatusCancelled {
|
||||
h.logger.Info("任务已被取消", zap.String("task_id", taskID))
|
||||
return fmt.Errorf("任务已被取消")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package asynq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/interfaces"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// AsynqApiTaskQueue Asynq API任务队列实现
|
||||
type AsynqApiTaskQueue struct {
|
||||
client *asynq.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAsynqApiTaskQueue 创建Asynq API任务队列
|
||||
func NewAsynqApiTaskQueue(redisAddr string, logger *zap.Logger) interfaces.ApiTaskQueue {
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
|
||||
return &AsynqApiTaskQueue{
|
||||
client: client,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue 入队任务
|
||||
func (q *AsynqApiTaskQueue) Enqueue(ctx context.Context, taskType types.TaskType, payload types.TaskPayload) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
q.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = q.client.EnqueueContext(ctx, task)
|
||||
if err != nil {
|
||||
q.logger.Error("入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
q.logger.Info("任务入队成功", zap.String("task_type", string(taskType)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnqueueDelayed 延时入队任务
|
||||
func (q *AsynqApiTaskQueue) EnqueueDelayed(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, delay time.Duration) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
q.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = q.client.EnqueueContext(ctx, task, asynq.ProcessIn(delay))
|
||||
if err != nil {
|
||||
q.logger.Error("延时入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
q.logger.Info("延时任务入队成功", zap.String("task_type", string(taskType)), zap.Duration("delay", delay))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnqueueAt 指定时间入队任务
|
||||
func (q *AsynqApiTaskQueue) EnqueueAt(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, scheduledAt time.Time) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
q.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = q.client.EnqueueContext(ctx, task, asynq.ProcessAt(scheduledAt))
|
||||
if err != nil {
|
||||
q.logger.Error("定时入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
q.logger.Info("定时任务入队成功", zap.String("task_type", string(taskType)), zap.Time("scheduled_at", scheduledAt))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancel 取消任务
|
||||
func (q *AsynqApiTaskQueue) Cancel(ctx context.Context, taskID string) error {
|
||||
// Asynq本身不支持直接取消任务,这里返回错误提示
|
||||
return fmt.Errorf("Asynq不支持直接取消任务,请使用数据库状态管理")
|
||||
}
|
||||
|
||||
// ModifySchedule 修改任务调度时间
|
||||
func (q *AsynqApiTaskQueue) ModifySchedule(ctx context.Context, taskID string, newScheduledAt time.Time) error {
|
||||
// Asynq本身不支持修改调度时间,这里返回错误提示
|
||||
return fmt.Errorf("Asynq不支持修改任务调度时间,请使用数据库状态管理")
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
func (q *AsynqApiTaskQueue) GetTaskStatus(ctx context.Context, taskID string) (*entities.AsyncTask, error) {
|
||||
// Asynq本身不提供任务状态查询,这里返回错误提示
|
||||
return nil, fmt.Errorf("Asynq不提供任务状态查询,请使用数据库状态管理")
|
||||
}
|
||||
|
||||
// ListTasks 列出任务
|
||||
func (q *AsynqApiTaskQueue) ListTasks(ctx context.Context, taskType types.TaskType, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error) {
|
||||
// Asynq本身不提供任务列表查询,这里返回错误提示
|
||||
return nil, fmt.Errorf("Asynq不提供任务列表查询,请使用数据库状态管理")
|
||||
}
|
||||
|
||||
// EnqueueTask 入队任务
|
||||
func (q *AsynqApiTaskQueue) EnqueueTask(ctx context.Context, task *entities.AsyncTask) error {
|
||||
// 创建Asynq任务
|
||||
asynqTask := asynq.NewTask(task.Type, []byte(task.Payload))
|
||||
|
||||
// 入队任务
|
||||
_, err := q.client.EnqueueContext(ctx, asynqTask)
|
||||
if err != nil {
|
||||
q.logger.Error("入队任务失败", zap.String("task_id", task.ID), zap.String("task_type", task.Type), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
q.logger.Info("入队任务成功", zap.String("task_id", task.ID), zap.String("task_type", task.Type))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package asynq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/interfaces"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// AsynqArticleTaskQueue Asynq文章任务队列实现
|
||||
type AsynqArticleTaskQueue struct {
|
||||
client *asynq.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAsynqArticleTaskQueue 创建Asynq文章任务队列
|
||||
func NewAsynqArticleTaskQueue(redisAddr string, logger *zap.Logger) interfaces.ArticleTaskQueue {
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
|
||||
return &AsynqArticleTaskQueue{
|
||||
client: client,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue 入队任务
|
||||
func (q *AsynqArticleTaskQueue) Enqueue(ctx context.Context, taskType types.TaskType, payload types.TaskPayload) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
q.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = q.client.EnqueueContext(ctx, task)
|
||||
if err != nil {
|
||||
q.logger.Error("入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
q.logger.Info("任务入队成功", zap.String("task_type", string(taskType)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnqueueDelayed 延时入队任务
|
||||
func (q *AsynqArticleTaskQueue) EnqueueDelayed(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, delay time.Duration) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
q.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = q.client.EnqueueContext(ctx, task, asynq.ProcessIn(delay))
|
||||
if err != nil {
|
||||
q.logger.Error("延时入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
q.logger.Info("延时任务入队成功", zap.String("task_type", string(taskType)), zap.Duration("delay", delay))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnqueueAt 指定时间入队任务
|
||||
func (q *AsynqArticleTaskQueue) EnqueueAt(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, scheduledAt time.Time) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
q.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = q.client.EnqueueContext(ctx, task, asynq.ProcessAt(scheduledAt))
|
||||
if err != nil {
|
||||
q.logger.Error("定时入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
q.logger.Info("定时任务入队成功", zap.String("task_type", string(taskType)), zap.Time("scheduled_at", scheduledAt))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancel 取消任务
|
||||
func (q *AsynqArticleTaskQueue) Cancel(ctx context.Context, taskID string) error {
|
||||
// Asynq本身不支持直接取消任务,但我们可以通过以下方式实现:
|
||||
// 1. 在数据库中标记任务为已取消
|
||||
// 2. 任务执行时检查状态,如果已取消则跳过执行
|
||||
|
||||
q.logger.Info("标记任务为已取消", zap.String("task_id", taskID))
|
||||
|
||||
// 这里应该更新数据库中的任务状态为cancelled
|
||||
// 由于我们没有直接访问repository,暂时只记录日志
|
||||
// 实际实现中应该调用AsyncTaskRepository.UpdateStatus
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModifySchedule 修改任务调度时间
|
||||
func (q *AsynqArticleTaskQueue) ModifySchedule(ctx context.Context, taskID string, newScheduledAt time.Time) error {
|
||||
// Asynq本身不支持修改调度时间,但我们可以通过以下方式实现:
|
||||
// 1. 取消旧任务
|
||||
// 2. 创建新任务
|
||||
|
||||
q.logger.Info("修改任务调度时间",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Time("new_scheduled_at", newScheduledAt))
|
||||
|
||||
// 这里应该:
|
||||
// 1. 调用Cancel取消旧任务
|
||||
// 2. 根据任务类型重新创建任务
|
||||
// 由于没有直接访问repository,暂时只记录日志
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
func (q *AsynqArticleTaskQueue) GetTaskStatus(ctx context.Context, taskID string) (*entities.AsyncTask, error) {
|
||||
// Asynq本身不提供任务状态查询,这里返回错误提示
|
||||
return nil, fmt.Errorf("Asynq不提供任务状态查询,请使用数据库状态管理")
|
||||
}
|
||||
|
||||
// ListTasks 列出任务
|
||||
func (q *AsynqArticleTaskQueue) ListTasks(ctx context.Context, taskType types.TaskType, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error) {
|
||||
// Asynq本身不提供任务列表查询,这里返回错误提示
|
||||
return nil, fmt.Errorf("Asynq不提供任务列表查询,请使用数据库状态管理")
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package asynq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// AsynqClient Asynq客户端实现
|
||||
type AsynqClient struct {
|
||||
client *asynq.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAsynqClient 创建Asynq客户端
|
||||
func NewAsynqClient(redisAddr string, logger *zap.Logger) *AsynqClient {
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
|
||||
return &AsynqClient{
|
||||
client: client,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue 入队任务
|
||||
func (c *AsynqClient) Enqueue(ctx context.Context, taskType types.TaskType, payload types.TaskPayload) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
c.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = c.client.EnqueueContext(ctx, task)
|
||||
if err != nil {
|
||||
c.logger.Error("入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Info("任务入队成功", zap.String("task_type", string(taskType)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnqueueDelayed 延时入队任务
|
||||
func (c *AsynqClient) EnqueueDelayed(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, delay time.Duration) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
c.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = c.client.EnqueueContext(ctx, task, asynq.ProcessIn(delay))
|
||||
if err != nil {
|
||||
c.logger.Error("延时入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Info("延时任务入队成功", zap.String("task_type", string(taskType)), zap.Duration("delay", delay))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnqueueAt 指定时间入队任务
|
||||
func (c *AsynqClient) EnqueueAt(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, scheduledAt time.Time) error {
|
||||
payloadData, err := payload.ToJSON()
|
||||
if err != nil {
|
||||
c.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
task := asynq.NewTask(string(taskType), payloadData)
|
||||
_, err = c.client.EnqueueContext(ctx, task, asynq.ProcessAt(scheduledAt))
|
||||
if err != nil {
|
||||
c.logger.Error("定时入队任务失败", zap.String("task_type", string(taskType)), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Info("定时任务入队成功", zap.String("task_type", string(taskType)), zap.Time("scheduled_at", scheduledAt))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭客户端
|
||||
func (c *AsynqClient) Close() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package asynq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/api"
|
||||
"tyapi-server/internal/application/article"
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_services "tyapi-server/internal/domains/product/services"
|
||||
"tyapi-server/internal/infrastructure/task/handlers"
|
||||
"tyapi-server/internal/infrastructure/task/repositories"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// AsynqWorker Asynq Worker实现
|
||||
type AsynqWorker struct {
|
||||
server *asynq.Server
|
||||
mux *asynq.ServeMux
|
||||
logger *zap.Logger
|
||||
articleHandler *handlers.ArticleTaskHandler
|
||||
apiHandler *handlers.ApiTaskHandler
|
||||
}
|
||||
|
||||
// NewAsynqWorker 创建Asynq Worker
|
||||
func NewAsynqWorker(
|
||||
redisAddr string,
|
||||
logger *zap.Logger,
|
||||
articleApplicationService article.ArticleApplicationService,
|
||||
apiApplicationService api.ApiApplicationService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
subscriptionService *product_services.ProductSubscriptionService,
|
||||
asyncTaskRepo repositories.AsyncTaskRepository,
|
||||
) *AsynqWorker {
|
||||
server := asynq.NewServer(
|
||||
asynq.RedisClientOpt{Addr: redisAddr},
|
||||
asynq.Config{
|
||||
Concurrency: 6, // 降低总并发数
|
||||
Queues: map[string]int{
|
||||
"default": 2, // 2个goroutine
|
||||
"api": 3, // 3个goroutine (扣款任务)
|
||||
"article": 1, // 1个goroutine
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 创建任务处理器
|
||||
articleHandler := handlers.NewArticleTaskHandler(logger, articleApplicationService, asyncTaskRepo)
|
||||
apiHandler := handlers.NewApiTaskHandler(logger, apiApplicationService, walletService, subscriptionService, asyncTaskRepo)
|
||||
|
||||
// 创建ServeMux
|
||||
mux := asynq.NewServeMux()
|
||||
|
||||
return &AsynqWorker{
|
||||
server: server,
|
||||
mux: mux,
|
||||
logger: logger,
|
||||
articleHandler: articleHandler,
|
||||
apiHandler: apiHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHandler 注册任务处理器
|
||||
func (w *AsynqWorker) RegisterHandler(taskType types.TaskType, handler func(context.Context, *asynq.Task) error) {
|
||||
// 简化实现,避免API兼容性问题
|
||||
w.logger.Info("注册任务处理器", zap.String("task_type", string(taskType)))
|
||||
}
|
||||
|
||||
// Start 启动Worker
|
||||
func (w *AsynqWorker) Start() error {
|
||||
w.logger.Info("启动Asynq Worker")
|
||||
|
||||
// 注册所有任务处理器
|
||||
w.registerAllHandlers()
|
||||
|
||||
// 启动Worker服务器
|
||||
go func() {
|
||||
if err := w.server.Run(w.mux); err != nil {
|
||||
w.logger.Error("Worker运行失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
w.logger.Info("Asynq Worker启动成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止Worker
|
||||
func (w *AsynqWorker) Stop() {
|
||||
w.logger.Info("停止Asynq Worker")
|
||||
w.server.Stop()
|
||||
}
|
||||
|
||||
// Shutdown 优雅关闭Worker
|
||||
func (w *AsynqWorker) Shutdown() {
|
||||
w.logger.Info("优雅关闭Asynq Worker")
|
||||
w.server.Shutdown()
|
||||
}
|
||||
|
||||
// registerAllHandlers 注册所有任务处理器
|
||||
func (w *AsynqWorker) registerAllHandlers() {
|
||||
// 注册文章任务处理器
|
||||
w.mux.HandleFunc(string(types.TaskTypeArticlePublish), w.articleHandler.HandleArticlePublish)
|
||||
w.mux.HandleFunc(string(types.TaskTypeArticleCancel), w.articleHandler.HandleArticleCancel)
|
||||
w.mux.HandleFunc(string(types.TaskTypeArticleModify), w.articleHandler.HandleArticleModify)
|
||||
|
||||
// 注册API任务处理器
|
||||
w.mux.HandleFunc(string(types.TaskTypeApiCall), w.apiHandler.HandleApiCall)
|
||||
w.mux.HandleFunc(string(types.TaskTypeApiLog), w.apiHandler.HandleApiLog)
|
||||
w.mux.HandleFunc(string(types.TaskTypeDeduction), w.apiHandler.HandleDeduction)
|
||||
w.mux.HandleFunc(string(types.TaskTypeCompensation), w.apiHandler.HandleCompensation)
|
||||
w.mux.HandleFunc(string(types.TaskTypeUsageStats), w.apiHandler.HandleUsageStats)
|
||||
|
||||
w.logger.Info("所有任务处理器注册完成",
|
||||
zap.String("article_publish", string(types.TaskTypeArticlePublish)),
|
||||
zap.String("article_cancel", string(types.TaskTypeArticleCancel)),
|
||||
zap.String("article_modify", string(types.TaskTypeArticleModify)),
|
||||
zap.String("api_call", string(types.TaskTypeApiCall)),
|
||||
zap.String("api_log", string(types.TaskTypeApiLog)),
|
||||
)
|
||||
}
|
||||
374
internal/infrastructure/task/implementations/task_manager.go
Normal file
374
internal/infrastructure/task/implementations/task_manager.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package implementations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/interfaces"
|
||||
"tyapi-server/internal/infrastructure/task/repositories"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// TaskManagerImpl 任务管理器实现
|
||||
type TaskManagerImpl struct {
|
||||
asynqClient *asynq.Client
|
||||
asyncTaskRepo repositories.AsyncTaskRepository
|
||||
logger *zap.Logger
|
||||
config *interfaces.TaskManagerConfig
|
||||
}
|
||||
|
||||
// NewTaskManager 创建任务管理器
|
||||
func NewTaskManager(
|
||||
asynqClient *asynq.Client,
|
||||
asyncTaskRepo repositories.AsyncTaskRepository,
|
||||
logger *zap.Logger,
|
||||
config *interfaces.TaskManagerConfig,
|
||||
) interfaces.TaskManager {
|
||||
return &TaskManagerImpl{
|
||||
asynqClient: asynqClient,
|
||||
asyncTaskRepo: asyncTaskRepo,
|
||||
logger: logger,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAndEnqueueTask 创建并入队任务
|
||||
func (tm *TaskManagerImpl) CreateAndEnqueueTask(ctx context.Context, task *entities.AsyncTask) error {
|
||||
// 1. 保存任务到数据库(GORM会自动生成UUID)
|
||||
if err := tm.asyncTaskRepo.Create(ctx, task); err != nil {
|
||||
tm.logger.Error("保存任务到数据库失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新payload中的task_id
|
||||
if err := tm.updatePayloadTaskID(task); err != nil {
|
||||
tm.logger.Error("更新payload中的任务ID失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("更新payload中的任务ID失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 更新数据库中的payload
|
||||
if err := tm.asyncTaskRepo.Update(ctx, task); err != nil {
|
||||
tm.logger.Error("更新任务payload失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("更新任务payload失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 入队到Asynq
|
||||
if err := tm.enqueueTaskWithDelay(ctx, task, 0); err != nil {
|
||||
// 如果入队失败,更新任务状态为失败
|
||||
tm.asyncTaskRepo.UpdateStatusWithError(ctx, task.ID, entities.TaskStatusFailed, "任务入队失败")
|
||||
return fmt.Errorf("任务入队失败: %w", err)
|
||||
}
|
||||
|
||||
tm.logger.Info("任务创建并入队成功",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.String("task_type", task.Type))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAndEnqueueDelayedTask 创建并入队延时任务
|
||||
func (tm *TaskManagerImpl) CreateAndEnqueueDelayedTask(ctx context.Context, task *entities.AsyncTask, delay time.Duration) error {
|
||||
// 1. 设置调度时间
|
||||
scheduledAt := time.Now().Add(delay)
|
||||
task.ScheduledAt = &scheduledAt
|
||||
|
||||
// 2. 保存任务到数据库(GORM会自动生成UUID)
|
||||
if err := tm.asyncTaskRepo.Create(ctx, task); err != nil {
|
||||
tm.logger.Error("保存延时任务到数据库失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存延时任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 更新payload中的task_id
|
||||
if err := tm.updatePayloadTaskID(task); err != nil {
|
||||
tm.logger.Error("更新payload中的任务ID失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("更新payload中的任务ID失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 更新数据库中的payload
|
||||
if err := tm.asyncTaskRepo.Update(ctx, task); err != nil {
|
||||
tm.logger.Error("更新任务payload失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("更新任务payload失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 入队到Asynq延时队列
|
||||
if err := tm.enqueueTaskWithDelay(ctx, task, delay); err != nil {
|
||||
// 如果入队失败,更新任务状态为失败
|
||||
tm.asyncTaskRepo.UpdateStatusWithError(ctx, task.ID, entities.TaskStatusFailed, "延时任务入队失败")
|
||||
return fmt.Errorf("延时任务入队失败: %w", err)
|
||||
}
|
||||
|
||||
tm.logger.Info("延时任务创建并入队成功",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.String("task_type", task.Type),
|
||||
zap.Duration("delay", delay))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelTask 取消任务
|
||||
func (tm *TaskManagerImpl) CancelTask(ctx context.Context, taskID string) error {
|
||||
task, err := tm.findTask(ctx, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tm.asyncTaskRepo.UpdateStatus(ctx, task.ID, entities.TaskStatusCancelled); err != nil {
|
||||
tm.logger.Error("更新任务状态为取消失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("更新任务状态失败: %w", err)
|
||||
}
|
||||
|
||||
tm.logger.Info("任务已标记为取消",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.String("task_type", task.Type))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTaskSchedule 更新任务调度时间
|
||||
func (tm *TaskManagerImpl) UpdateTaskSchedule(ctx context.Context, taskID string, newScheduledAt time.Time) error {
|
||||
// 1. 查找任务
|
||||
task, err := tm.findTask(ctx, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tm.logger.Info("找到要更新的任务",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.String("current_status", string(task.Status)),
|
||||
zap.Time("current_scheduled_at", *task.ScheduledAt))
|
||||
|
||||
// 2. 取消旧任务
|
||||
if err := tm.asyncTaskRepo.UpdateStatus(ctx, task.ID, entities.TaskStatusCancelled); err != nil {
|
||||
tm.logger.Error("取消旧任务失败",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("取消旧任务失败: %w", err)
|
||||
}
|
||||
|
||||
tm.logger.Info("旧任务已标记为取消", zap.String("task_id", task.ID))
|
||||
|
||||
// 3. 创建并保存新任务
|
||||
newTask, err := tm.createAndSaveTask(ctx, task, newScheduledAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tm.logger.Info("新任务已创建",
|
||||
zap.String("new_task_id", newTask.ID),
|
||||
zap.Time("new_scheduled_at", newScheduledAt))
|
||||
|
||||
// 4. 计算延时并入队
|
||||
delay := newScheduledAt.Sub(time.Now())
|
||||
if delay < 0 {
|
||||
delay = 0 // 如果时间已过,立即执行
|
||||
}
|
||||
|
||||
if err := tm.enqueueTaskWithDelay(ctx, newTask, delay); err != nil {
|
||||
// 如果入队失败,删除新创建的任务记录
|
||||
tm.asyncTaskRepo.Delete(ctx, newTask.ID)
|
||||
return fmt.Errorf("重新入队任务失败: %w", err)
|
||||
}
|
||||
|
||||
tm.logger.Info("任务调度时间更新成功",
|
||||
zap.String("old_task_id", task.ID),
|
||||
zap.String("new_task_id", newTask.ID),
|
||||
zap.Time("new_scheduled_at", newScheduledAt))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
func (tm *TaskManagerImpl) GetTaskStatus(ctx context.Context, taskID string) (*entities.AsyncTask, error) {
|
||||
return tm.asyncTaskRepo.GetByID(ctx, taskID)
|
||||
}
|
||||
|
||||
// UpdateTaskStatus 更新任务状态
|
||||
func (tm *TaskManagerImpl) UpdateTaskStatus(ctx context.Context, taskID string, status entities.TaskStatus, errorMsg string) error {
|
||||
if errorMsg != "" {
|
||||
return tm.asyncTaskRepo.UpdateStatusWithError(ctx, taskID, status, errorMsg)
|
||||
}
|
||||
return tm.asyncTaskRepo.UpdateStatus(ctx, taskID, status)
|
||||
}
|
||||
|
||||
// RetryTask 重试任务
|
||||
func (tm *TaskManagerImpl) RetryTask(ctx context.Context, taskID string) error {
|
||||
// 1. 获取任务信息
|
||||
task, err := tm.asyncTaskRepo.GetByID(ctx, taskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取任务信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以重试
|
||||
if !task.CanRetry() {
|
||||
return fmt.Errorf("任务已达到最大重试次数")
|
||||
}
|
||||
|
||||
// 3. 增加重试次数并重置状态
|
||||
task.RetryCount++
|
||||
task.Status = entities.TaskStatusPending
|
||||
|
||||
// 4. 更新数据库
|
||||
if err := tm.asyncTaskRepo.Update(ctx, task); err != nil {
|
||||
return fmt.Errorf("更新任务重试次数失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 重新入队
|
||||
if err := tm.enqueueTaskWithDelay(ctx, task, 0); err != nil {
|
||||
return fmt.Errorf("重试任务入队失败: %w", err)
|
||||
}
|
||||
|
||||
tm.logger.Info("任务重试成功",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Int("retry_count", task.RetryCount))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupExpiredTasks 清理过期任务
|
||||
func (tm *TaskManagerImpl) CleanupExpiredTasks(ctx context.Context, olderThan time.Time) error {
|
||||
// 这里可以实现清理逻辑,比如删除超过一定时间的已完成任务
|
||||
tm.logger.Info("开始清理过期任务", zap.Time("older_than", olderThan))
|
||||
|
||||
// TODO: 实现清理逻辑
|
||||
return nil
|
||||
}
|
||||
|
||||
// updatePayloadTaskID 更新payload中的task_id
|
||||
func (tm *TaskManagerImpl) updatePayloadTaskID(task *entities.AsyncTask) error {
|
||||
// 解析payload
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(task.Payload), &payload); err != nil {
|
||||
return fmt.Errorf("解析payload失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新task_id
|
||||
payload["task_id"] = task.ID
|
||||
|
||||
// 重新序列化
|
||||
newPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化payload失败: %w", err)
|
||||
}
|
||||
|
||||
task.Payload = string(newPayload)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// findTask 查找任务(支持taskID和articleID双重查找)
|
||||
func (tm *TaskManagerImpl) findTask(ctx context.Context, taskID string) (*entities.AsyncTask, error) {
|
||||
// 先尝试通过任务ID查找
|
||||
task, err := tm.asyncTaskRepo.GetByID(ctx, taskID)
|
||||
if err == nil {
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// 如果通过任务ID找不到,尝试通过文章ID查找
|
||||
tm.logger.Info("通过任务ID查找失败,尝试通过文章ID查找", zap.String("task_id", taskID))
|
||||
|
||||
tasks, err := tm.asyncTaskRepo.GetByArticleID(ctx, taskID)
|
||||
if err != nil || len(tasks) == 0 {
|
||||
tm.logger.Error("通过文章ID也找不到任务",
|
||||
zap.String("article_id", taskID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("获取任务信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用找到的第一个任务
|
||||
task = tasks[0]
|
||||
tm.logger.Info("通过文章ID找到任务",
|
||||
zap.String("article_id", taskID),
|
||||
zap.String("task_id", task.ID))
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// createAndSaveTask 创建并保存新任务
|
||||
func (tm *TaskManagerImpl) createAndSaveTask(ctx context.Context, originalTask *entities.AsyncTask, newScheduledAt time.Time) (*entities.AsyncTask, error) {
|
||||
// 创建新任务
|
||||
newTask := &entities.AsyncTask{
|
||||
Type: originalTask.Type,
|
||||
Payload: originalTask.Payload,
|
||||
Status: entities.TaskStatusPending,
|
||||
ScheduledAt: &newScheduledAt,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 保存到数据库(GORM会自动生成UUID)
|
||||
if err := tm.asyncTaskRepo.Create(ctx, newTask); err != nil {
|
||||
tm.logger.Error("创建新任务失败",
|
||||
zap.String("new_task_id", newTask.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("创建新任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新payload中的task_id
|
||||
if err := tm.updatePayloadTaskID(newTask); err != nil {
|
||||
tm.logger.Error("更新payload中的任务ID失败",
|
||||
zap.String("new_task_id", newTask.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("更新payload中的任务ID失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新数据库中的payload
|
||||
if err := tm.asyncTaskRepo.Update(ctx, newTask); err != nil {
|
||||
tm.logger.Error("更新新任务payload失败",
|
||||
zap.String("new_task_id", newTask.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("更新新任务payload失败: %w", err)
|
||||
}
|
||||
|
||||
return newTask, nil
|
||||
}
|
||||
|
||||
// enqueueTaskWithDelay 入队任务到Asynq(支持延时)
|
||||
func (tm *TaskManagerImpl) enqueueTaskWithDelay(ctx context.Context, task *entities.AsyncTask, delay time.Duration) error {
|
||||
queueName := tm.getQueueName(task.Type)
|
||||
asynqTask := asynq.NewTask(task.Type, []byte(task.Payload))
|
||||
|
||||
var err error
|
||||
if delay > 0 {
|
||||
_, err = tm.asynqClient.EnqueueContext(ctx, asynqTask,
|
||||
asynq.Queue(queueName),
|
||||
asynq.ProcessIn(delay))
|
||||
} else {
|
||||
_, err = tm.asynqClient.EnqueueContext(ctx, asynqTask, asynq.Queue(queueName))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getQueueName 根据任务类型获取队列名称
|
||||
func (tm *TaskManagerImpl) getQueueName(taskType string) string {
|
||||
switch taskType {
|
||||
case string(types.TaskTypeArticlePublish), string(types.TaskTypeArticleCancel), string(types.TaskTypeArticleModify):
|
||||
return "article"
|
||||
case string(types.TaskTypeApiCall), string(types.TaskTypeApiLog), string(types.TaskTypeDeduction), string(types.TaskTypeUsageStats):
|
||||
return "api"
|
||||
case string(types.TaskTypeCompensation):
|
||||
return "finance"
|
||||
default:
|
||||
return "default"
|
||||
}
|
||||
}
|
||||
35
internal/infrastructure/task/interfaces/api_task_queue.go
Normal file
35
internal/infrastructure/task/interfaces/api_task_queue.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// ApiTaskQueue API任务队列接口
|
||||
type ApiTaskQueue interface {
|
||||
// Enqueue 入队任务
|
||||
Enqueue(ctx context.Context, taskType types.TaskType, payload types.TaskPayload) error
|
||||
|
||||
// EnqueueDelayed 延时入队任务
|
||||
EnqueueDelayed(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, delay time.Duration) error
|
||||
|
||||
// EnqueueAt 指定时间入队任务
|
||||
EnqueueAt(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, scheduledAt time.Time) error
|
||||
|
||||
// Cancel 取消任务
|
||||
Cancel(ctx context.Context, taskID string) error
|
||||
|
||||
// ModifySchedule 修改任务调度时间
|
||||
ModifySchedule(ctx context.Context, taskID string, newScheduledAt time.Time) error
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
GetTaskStatus(ctx context.Context, taskID string) (*entities.AsyncTask, error)
|
||||
|
||||
// ListTasks 列出任务
|
||||
ListTasks(ctx context.Context, taskType types.TaskType, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error)
|
||||
|
||||
// EnqueueTask 入队任务(简化版本)
|
||||
EnqueueTask(ctx context.Context, task *entities.AsyncTask) error
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// ArticleTaskQueue 文章任务队列接口
|
||||
type ArticleTaskQueue interface {
|
||||
// Enqueue 入队任务
|
||||
Enqueue(ctx context.Context, taskType types.TaskType, payload types.TaskPayload) error
|
||||
|
||||
// EnqueueDelayed 延时入队任务
|
||||
EnqueueDelayed(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, delay time.Duration) error
|
||||
|
||||
// EnqueueAt 指定时间入队任务
|
||||
EnqueueAt(ctx context.Context, taskType types.TaskType, payload types.TaskPayload, scheduledAt time.Time) error
|
||||
|
||||
// Cancel 取消任务
|
||||
Cancel(ctx context.Context, taskID string) error
|
||||
|
||||
// ModifySchedule 修改任务调度时间
|
||||
ModifySchedule(ctx context.Context, taskID string, newScheduledAt time.Time) error
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
GetTaskStatus(ctx context.Context, taskID string) (*entities.AsyncTask, error)
|
||||
|
||||
// ListTasks 列出任务
|
||||
ListTasks(ctx context.Context, taskType types.TaskType, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error)
|
||||
}
|
||||
44
internal/infrastructure/task/interfaces/task_manager.go
Normal file
44
internal/infrastructure/task/interfaces/task_manager.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
)
|
||||
|
||||
// TaskManager 任务管理器接口
|
||||
// 统一管理Asynq任务和AsyncTask实体的操作
|
||||
type TaskManager interface {
|
||||
// 创建并入队任务
|
||||
CreateAndEnqueueTask(ctx context.Context, task *entities.AsyncTask) error
|
||||
|
||||
// 创建并入队延时任务
|
||||
CreateAndEnqueueDelayedTask(ctx context.Context, task *entities.AsyncTask, delay time.Duration) error
|
||||
|
||||
// 取消任务
|
||||
CancelTask(ctx context.Context, taskID string) error
|
||||
|
||||
// 更新任务调度时间
|
||||
UpdateTaskSchedule(ctx context.Context, taskID string, newScheduledAt time.Time) error
|
||||
|
||||
// 获取任务状态
|
||||
GetTaskStatus(ctx context.Context, taskID string) (*entities.AsyncTask, error)
|
||||
|
||||
// 更新任务状态
|
||||
UpdateTaskStatus(ctx context.Context, taskID string, status entities.TaskStatus, errorMsg string) error
|
||||
|
||||
// 重试任务
|
||||
RetryTask(ctx context.Context, taskID string) error
|
||||
|
||||
// 清理过期任务
|
||||
CleanupExpiredTasks(ctx context.Context, olderThan time.Time) error
|
||||
}
|
||||
|
||||
// TaskManagerConfig 任务管理器配置
|
||||
type TaskManagerConfig struct {
|
||||
RedisAddr string
|
||||
MaxRetries int
|
||||
RetryInterval time.Duration
|
||||
CleanupDays int
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/infrastructure/task/entities"
|
||||
"tyapi-server/internal/infrastructure/task/types"
|
||||
)
|
||||
|
||||
// AsyncTaskRepository 异步任务仓库接口
|
||||
type AsyncTaskRepository interface {
|
||||
// 基础CRUD操作
|
||||
Create(ctx context.Context, task *entities.AsyncTask) error
|
||||
GetByID(ctx context.Context, id string) (*entities.AsyncTask, error)
|
||||
Update(ctx context.Context, task *entities.AsyncTask) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// 查询操作
|
||||
ListByType(ctx context.Context, taskType types.TaskType, limit int) ([]*entities.AsyncTask, error)
|
||||
ListByStatus(ctx context.Context, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error)
|
||||
ListByTypeAndStatus(ctx context.Context, taskType types.TaskType, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error)
|
||||
ListScheduledTasks(ctx context.Context, before time.Time) ([]*entities.AsyncTask, error)
|
||||
|
||||
// 状态更新操作
|
||||
UpdateStatus(ctx context.Context, id string, status entities.TaskStatus) error
|
||||
UpdateStatusWithError(ctx context.Context, id string, status entities.TaskStatus, errorMsg string) error
|
||||
UpdateStatusWithRetryAndError(ctx context.Context, id string, status entities.TaskStatus, errorMsg string) error
|
||||
UpdateStatusWithSuccess(ctx context.Context, id string, status entities.TaskStatus) error
|
||||
UpdateRetryCountAndError(ctx context.Context, id string, retryCount int, errorMsg string) error
|
||||
UpdateScheduledAt(ctx context.Context, id string, scheduledAt time.Time) error
|
||||
IncrementRetryCount(ctx context.Context, id string) error
|
||||
|
||||
// 批量操作
|
||||
UpdateStatusBatch(ctx context.Context, ids []string, status entities.TaskStatus) error
|
||||
DeleteBatch(ctx context.Context, ids []string) error
|
||||
|
||||
// 文章任务专用方法
|
||||
GetArticlePublishTask(ctx context.Context, articleID string) (*entities.AsyncTask, error)
|
||||
GetByArticleID(ctx context.Context, articleID string) ([]*entities.AsyncTask, error)
|
||||
CancelArticlePublishTask(ctx context.Context, articleID string) error
|
||||
UpdateArticlePublishTaskSchedule(ctx context.Context, articleID string, newScheduledAt time.Time) error
|
||||
}
|
||||
|
||||
// AsyncTaskRepositoryImpl 异步任务仓库实现
|
||||
type AsyncTaskRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAsyncTaskRepository 创建异步任务仓库
|
||||
func NewAsyncTaskRepository(db *gorm.DB) AsyncTaskRepository {
|
||||
return &AsyncTaskRepositoryImpl{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建任务
|
||||
func (r *AsyncTaskRepositoryImpl) Create(ctx context.Context, task *entities.AsyncTask) error {
|
||||
return r.db.WithContext(ctx).Create(task).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取任务
|
||||
func (r *AsyncTaskRepositoryImpl) GetByID(ctx context.Context, id string) (*entities.AsyncTask, error) {
|
||||
var task entities.AsyncTask
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&task).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// Update 更新任务
|
||||
func (r *AsyncTaskRepositoryImpl) Update(ctx context.Context, task *entities.AsyncTask) error {
|
||||
return r.db.WithContext(ctx).Save(task).Error
|
||||
}
|
||||
|
||||
// Delete 删除任务
|
||||
func (r *AsyncTaskRepositoryImpl) Delete(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&entities.AsyncTask{}).Error
|
||||
}
|
||||
|
||||
// ListByType 根据类型列出任务
|
||||
func (r *AsyncTaskRepositoryImpl) ListByType(ctx context.Context, taskType types.TaskType, limit int) ([]*entities.AsyncTask, error) {
|
||||
var tasks []*entities.AsyncTask
|
||||
query := r.db.WithContext(ctx).Where("type = ?", taskType)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
err := query.Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// ListByStatus 根据状态列出任务
|
||||
func (r *AsyncTaskRepositoryImpl) ListByStatus(ctx context.Context, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error) {
|
||||
var tasks []*entities.AsyncTask
|
||||
query := r.db.WithContext(ctx).Where("status = ?", status)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
err := query.Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// ListByTypeAndStatus 根据类型和状态列出任务
|
||||
func (r *AsyncTaskRepositoryImpl) ListByTypeAndStatus(ctx context.Context, taskType types.TaskType, status entities.TaskStatus, limit int) ([]*entities.AsyncTask, error) {
|
||||
var tasks []*entities.AsyncTask
|
||||
query := r.db.WithContext(ctx).Where("type = ? AND status = ?", taskType, status)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
err := query.Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// ListScheduledTasks 列出已到期的调度任务
|
||||
func (r *AsyncTaskRepositoryImpl) ListScheduledTasks(ctx context.Context, before time.Time) ([]*entities.AsyncTask, error) {
|
||||
var tasks []*entities.AsyncTask
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("status = ? AND scheduled_at IS NOT NULL AND scheduled_at <= ?", entities.TaskStatusPending, before).
|
||||
Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// UpdateStatus 更新任务状态
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateStatus(ctx context.Context, id string, status entities.TaskStatus) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateStatusWithError 更新任务状态并记录错误
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateStatusWithError(ctx context.Context, id string, status entities.TaskStatus, errorMsg string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"error_msg": errorMsg,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateStatusWithRetryAndError 更新任务状态、增加重试次数并记录错误
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateStatusWithRetryAndError(ctx context.Context, id string, status entities.TaskStatus, errorMsg string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"error_msg": errorMsg,
|
||||
"retry_count": gorm.Expr("retry_count + 1"),
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateStatusWithSuccess 更新任务状态为成功,清除错误信息
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateStatusWithSuccess(ctx context.Context, id string, status entities.TaskStatus) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"error_msg": "", // 清除错误信息
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateRetryCountAndError 更新重试次数和错误信息,保持pending状态
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateRetryCountAndError(ctx context.Context, id string, retryCount int, errorMsg string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"retry_count": retryCount,
|
||||
"error_msg": errorMsg,
|
||||
"updated_at": time.Now(),
|
||||
// 注意:不更新status,保持pending状态
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateScheduledAt 更新任务调度时间
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateScheduledAt(ctx context.Context, id string, scheduledAt time.Time) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id = ?", id).
|
||||
Update("scheduled_at", scheduledAt).Error
|
||||
}
|
||||
|
||||
// IncrementRetryCount 增加重试次数
|
||||
func (r *AsyncTaskRepositoryImpl) IncrementRetryCount(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id = ?", id).
|
||||
Update("retry_count", gorm.Expr("retry_count + 1")).Error
|
||||
}
|
||||
|
||||
// UpdateStatusBatch 批量更新状态
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateStatusBatch(ctx context.Context, ids []string, status entities.TaskStatus) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("id IN ?", ids).
|
||||
Update("status", status).Error
|
||||
}
|
||||
|
||||
// DeleteBatch 批量删除
|
||||
func (r *AsyncTaskRepositoryImpl) DeleteBatch(ctx context.Context, ids []string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("id IN ?", ids).
|
||||
Delete(&entities.AsyncTask{}).Error
|
||||
}
|
||||
|
||||
// GetArticlePublishTask 获取文章发布任务
|
||||
func (r *AsyncTaskRepositoryImpl) GetArticlePublishTask(ctx context.Context, articleID string) (*entities.AsyncTask, error) {
|
||||
var task entities.AsyncTask
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("type = ? AND payload LIKE ? AND status IN ?",
|
||||
types.TaskTypeArticlePublish,
|
||||
"%\"article_id\":\""+articleID+"\"%",
|
||||
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
||||
First(&task).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// GetByArticleID 根据文章ID获取所有相关任务
|
||||
func (r *AsyncTaskRepositoryImpl) GetByArticleID(ctx context.Context, articleID string) ([]*entities.AsyncTask, error) {
|
||||
var tasks []*entities.AsyncTask
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("payload LIKE ? AND status IN ?",
|
||||
"%\"article_id\":\""+articleID+"\"%",
|
||||
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
||||
Find(&tasks).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// CancelArticlePublishTask 取消文章发布任务
|
||||
func (r *AsyncTaskRepositoryImpl) CancelArticlePublishTask(ctx context.Context, articleID string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("type = ? AND payload LIKE ? AND status IN ?",
|
||||
types.TaskTypeArticlePublish,
|
||||
"%\"article_id\":\""+articleID+"\"%",
|
||||
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
||||
Update("status", entities.TaskStatusCancelled).Error
|
||||
}
|
||||
|
||||
// UpdateArticlePublishTaskSchedule 更新文章发布任务调度时间
|
||||
func (r *AsyncTaskRepositoryImpl) UpdateArticlePublishTaskSchedule(ctx context.Context, articleID string, newScheduledAt time.Time) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entities.AsyncTask{}).
|
||||
Where("type = ? AND payload LIKE ? AND status IN ?",
|
||||
types.TaskTypeArticlePublish,
|
||||
"%\"article_id\":\""+articleID+"\"%",
|
||||
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
||||
Update("scheduled_at", newScheduledAt).Error
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package task
|
||||
|
||||
// 任务类型常量
|
||||
const (
|
||||
// TaskTypeArticlePublish 文章定时发布任务
|
||||
TaskTypeArticlePublish = "article:publish"
|
||||
)
|
||||
196
internal/infrastructure/task/types/queue_types.go
Normal file
196
internal/infrastructure/task/types/queue_types.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// QueueType 队列类型
|
||||
type QueueType string
|
||||
|
||||
const (
|
||||
QueueTypeDefault QueueType = "default"
|
||||
QueueTypeApi QueueType = "api"
|
||||
QueueTypeArticle QueueType = "article"
|
||||
QueueTypeFinance QueueType = "finance"
|
||||
QueueTypeProduct QueueType = "product"
|
||||
)
|
||||
|
||||
// ArticlePublishPayload 文章发布任务载荷
|
||||
type ArticlePublishPayload struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
PublishAt time.Time `json:"publish_at"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ArticlePublishPayload) GetType() TaskType {
|
||||
return TaskTypeArticlePublish
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ArticlePublishPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ArticlePublishPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// ArticleCancelPayload 文章取消任务载荷
|
||||
type ArticleCancelPayload struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ArticleCancelPayload) GetType() TaskType {
|
||||
return TaskTypeArticleCancel
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ArticleCancelPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ArticleCancelPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// ArticleModifyPayload 文章修改任务载荷
|
||||
type ArticleModifyPayload struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
NewPublishAt time.Time `json:"new_publish_at"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ArticleModifyPayload) GetType() TaskType {
|
||||
return TaskTypeArticleModify
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ArticleModifyPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ArticleModifyPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// ApiCallPayload API调用任务载荷
|
||||
type ApiCallPayload struct {
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Amount string `json:"amount"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ApiCallPayload) GetType() TaskType {
|
||||
return TaskTypeApiCall
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ApiCallPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ApiCallPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// DeductionPayload 扣款任务载荷
|
||||
type DeductionPayload struct {
|
||||
UserID string `json:"user_id"`
|
||||
Amount string `json:"amount"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *DeductionPayload) GetType() TaskType {
|
||||
return TaskTypeDeduction
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *DeductionPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *DeductionPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// CompensationPayload 补偿任务载荷
|
||||
type CompensationPayload struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *CompensationPayload) GetType() TaskType {
|
||||
return TaskTypeCompensation
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *CompensationPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *CompensationPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// UsageStatsPayload 使用统计任务载荷
|
||||
type UsageStatsPayload struct {
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Increment int `json:"increment"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *UsageStatsPayload) GetType() TaskType {
|
||||
return TaskTypeUsageStats
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *UsageStatsPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *UsageStatsPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
// ApiLogPayload API日志任务载荷
|
||||
type ApiLogPayload struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiName string `json:"api_name"`
|
||||
ProductID string `json:"product_id"`
|
||||
}
|
||||
|
||||
// GetType 获取任务类型
|
||||
func (p *ApiLogPayload) GetType() TaskType {
|
||||
return TaskTypeApiLog
|
||||
}
|
||||
|
||||
// ToJSON 序列化为JSON
|
||||
func (p *ApiLogPayload) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON反序列化
|
||||
func (p *ApiLogPayload) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, p)
|
||||
}
|
||||
29
internal/infrastructure/task/types/task_types.go
Normal file
29
internal/infrastructure/task/types/task_types.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package types
|
||||
|
||||
// TaskType 任务类型
|
||||
type TaskType string
|
||||
|
||||
const (
|
||||
// 文章相关任务
|
||||
TaskTypeArticlePublish TaskType = "article_publish"
|
||||
TaskTypeArticleCancel TaskType = "article_cancel"
|
||||
TaskTypeArticleModify TaskType = "article_modify"
|
||||
|
||||
// API相关任务
|
||||
TaskTypeApiCall TaskType = "api_call"
|
||||
TaskTypeApiLog TaskType = "api_log"
|
||||
|
||||
// 财务相关任务
|
||||
TaskTypeDeduction TaskType = "deduction"
|
||||
TaskTypeCompensation TaskType = "compensation"
|
||||
|
||||
// 产品相关任务
|
||||
TaskTypeUsageStats TaskType = "usage_stats"
|
||||
)
|
||||
|
||||
// TaskPayload 任务载荷接口
|
||||
type TaskPayload interface {
|
||||
GetType() TaskType
|
||||
ToJSON() ([]byte, error)
|
||||
FromJSON(data []byte) error
|
||||
}
|
||||
100
internal/infrastructure/task/utils/asynq_logger.go
Normal file
100
internal/infrastructure/task/utils/asynq_logger.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AsynqLogger Asynq日志适配器
|
||||
type AsynqLogger struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAsynqLogger 创建Asynq日志适配器
|
||||
func NewAsynqLogger(logger *zap.Logger) *AsynqLogger {
|
||||
return &AsynqLogger{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Debug 调试日志
|
||||
func (l *AsynqLogger) Debug(args ...interface{}) {
|
||||
l.logger.Debug("", zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Info 信息日志
|
||||
func (l *AsynqLogger) Info(args ...interface{}) {
|
||||
l.logger.Info("", zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Warn 警告日志
|
||||
func (l *AsynqLogger) Warn(args ...interface{}) {
|
||||
l.logger.Warn("", zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Error 错误日志
|
||||
func (l *AsynqLogger) Error(args ...interface{}) {
|
||||
l.logger.Error("", zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Fatal 致命错误日志
|
||||
func (l *AsynqLogger) Fatal(args ...interface{}) {
|
||||
l.logger.Fatal("", zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Debugf 格式化调试日志
|
||||
func (l *AsynqLogger) Debugf(format string, args ...interface{}) {
|
||||
l.logger.Debug("", zap.String("format", format), zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Infof 格式化信息日志
|
||||
func (l *AsynqLogger) Infof(format string, args ...interface{}) {
|
||||
l.logger.Info("", zap.String("format", format), zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Warnf 格式化警告日志
|
||||
func (l *AsynqLogger) Warnf(format string, args ...interface{}) {
|
||||
l.logger.Warn("", zap.String("format", format), zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Errorf 格式化错误日志
|
||||
func (l *AsynqLogger) Errorf(format string, args ...interface{}) {
|
||||
l.logger.Error("", zap.String("format", format), zap.Any("args", args))
|
||||
}
|
||||
|
||||
// Fatalf 格式化致命错误日志
|
||||
func (l *AsynqLogger) Fatalf(format string, args ...interface{}) {
|
||||
l.logger.Fatal("", zap.String("format", format), zap.Any("args", args))
|
||||
}
|
||||
|
||||
// WithField 添加字段
|
||||
func (l *AsynqLogger) WithField(key string, value interface{}) asynq.Logger {
|
||||
return &AsynqLogger{
|
||||
logger: l.logger.With(zap.Any(key, value)),
|
||||
}
|
||||
}
|
||||
|
||||
// WithFields 添加多个字段
|
||||
func (l *AsynqLogger) WithFields(fields map[string]interface{}) asynq.Logger {
|
||||
zapFields := make([]zap.Field, 0, len(fields))
|
||||
for k, v := range fields {
|
||||
zapFields = append(zapFields, zap.Any(k, v))
|
||||
}
|
||||
return &AsynqLogger{
|
||||
logger: l.logger.With(zapFields...),
|
||||
}
|
||||
}
|
||||
|
||||
// WithError 添加错误字段
|
||||
func (l *AsynqLogger) WithError(err error) asynq.Logger {
|
||||
return &AsynqLogger{
|
||||
logger: l.logger.With(zap.Error(err)),
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext 添加上下文
|
||||
func (l *AsynqLogger) WithContext(ctx context.Context) asynq.Logger {
|
||||
return l
|
||||
}
|
||||
17
internal/infrastructure/task/utils/task_id.go
Normal file
17
internal/infrastructure/task/utils/task_id.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// GenerateTaskID 生成统一格式的任务ID (UUID)
|
||||
func GenerateTaskID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// GenerateTaskIDWithPrefix 生成带前缀的任务ID (UUID)
|
||||
func GenerateTaskIDWithPrefix(prefix string) string {
|
||||
return fmt.Sprintf("%s-%s", prefix, uuid.New().String())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user