This commit is contained in:
2025-09-12 01:15:09 +08:00
parent c563b2266b
commit e05ad9e223
103 changed files with 20034 additions and 1041 deletions

View File

@@ -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 # 预警冷却时间(小时)
# ===========================================
# 🌍 西部数据配置
# ===========================================

View File

@@ -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 # 开发环境禁用代理检查

View File

@@ -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 # 启用代理检查

View File

@@ -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
}

View File

@@ -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

View 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
View File

@@ -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
View File

@@ -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=

View File

@@ -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

View File

@@ -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"`
}

View 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
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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)
// ================ 查询用例 ================
// 获取认证详情

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 {

View 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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View 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
}

View 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:"错误信息"`
}

View File

@@ -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

View File

@@ -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 支付宝充值赠送规则

View File

@@ -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()

View File

@@ -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"`

View File

@@ -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 指定数据库表名

View File

@@ -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 == "" {

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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
}

View File

@@ -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:"创建时间"`

View File

@@ -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"`
// 扣款信息

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}

View 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)
}

View File

@@ -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))
}
}

View 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"`
}

View 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}
}

View 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"`
}

View 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,
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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]
}
}
}
}

View File

@@ -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]
}
}
}
}

View File

@@ -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
}

View File

@@ -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 短信验证码仓储接口

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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(),
}
}

View File

@@ -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 {

View File

@@ -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{}, "测试短信发送成功")
}

View File

@@ -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 管理员获取认证申请列表

View File

@@ -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)
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}

View File

@@ -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路由注册完成")

View File

@@ -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},

View File

@@ -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)
}
}
}

View 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("统计路由注册完成")
}

View 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
}

View 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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1 @@

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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...))
}

View 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
}

View 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)
}

View 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)
}

View 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))
}
}

View 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
}

View File

@@ -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
}

View File

@@ -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不提供任务列表查询请使用数据库状态管理")
}

View File

@@ -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()
}

View File

@@ -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)),
)
}

View 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"
}
}

View 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
}

View File

@@ -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)
}

View 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
}

View File

@@ -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
}

View File

@@ -1,7 +0,0 @@
package task
// 任务类型常量
const (
// TaskTypeArticlePublish 文章定时发布任务
TaskTypeArticlePublish = "article:publish"
)

View 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)
}

View 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
}

View 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
}

View 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