new
This commit is contained in:
@@ -271,6 +271,12 @@ type IVYZ5E3FReq struct {
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type IVYZ7F3AReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type YYSY4F2EReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -56,7 +52,7 @@ type ApiCall struct {
|
||||
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
|
||||
UserId *string `gorm:"type:varchar(36);index" json:"user_id,omitempty"`
|
||||
ProductId *string `gorm:"type:varchar(64);index" json:"product_id,omitempty"`
|
||||
TransactionId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id"`
|
||||
TransactionId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id"`
|
||||
ClientIp string `gorm:"type:varchar(64);not null;index" json:"client_ip"`
|
||||
RequestParams string `gorm:"type:text" json:"request_params"`
|
||||
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
|
||||
@@ -145,40 +141,9 @@ func (a *ApiCall) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 全局计数器,用于确保TransactionID的唯一性
|
||||
var (
|
||||
transactionCounter int64
|
||||
counterMutex sync.Mutex
|
||||
)
|
||||
|
||||
// GenerateTransactionID 生成16位数的交易单号
|
||||
// GenerateTransactionID 生成UUID格式的交易单号
|
||||
func GenerateTransactionID() string {
|
||||
// 使用互斥锁确保计数器的线程安全
|
||||
counterMutex.Lock()
|
||||
transactionCounter++
|
||||
currentCounter := transactionCounter
|
||||
counterMutex.Unlock()
|
||||
|
||||
// 获取当前时间戳(微秒精度)
|
||||
timestamp := time.Now().UnixMicro()
|
||||
|
||||
// 组合时间戳和计数器,确保唯一性
|
||||
combined := fmt.Sprintf("%d%06d", timestamp, currentCounter%1000000)
|
||||
|
||||
// 如果长度超出16位,截断;如果不够,填充随机字符
|
||||
if len(combined) >= 16 {
|
||||
return combined[:16]
|
||||
}
|
||||
|
||||
// 如果长度不够,使用随机字节填充
|
||||
if len(combined) < 16 {
|
||||
randomBytes := make([]byte, 8)
|
||||
rand.Read(randomBytes)
|
||||
randomHex := hex.EncodeToString(randomBytes)
|
||||
combined += randomHex[:16-len(combined)]
|
||||
}
|
||||
|
||||
return combined
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
|
||||
@@ -20,12 +20,20 @@ const (
|
||||
|
||||
// ApiUser API用户(聚合根)
|
||||
type ApiUser struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
|
||||
UserId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_id"`
|
||||
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
|
||||
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"` // 支持多个白名单
|
||||
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
|
||||
UserId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_id"`
|
||||
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
|
||||
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不能为空")
|
||||
}
|
||||
@@ -64,12 +72,14 @@ func NewApiUser(userId string) (*ApiUser, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &ApiUser{
|
||||
ID: uuid.New().String(),
|
||||
UserId: userId,
|
||||
AccessId: accessId,
|
||||
SecretKey: secretKey,
|
||||
Status: ApiUserStatusNormal,
|
||||
WhiteList: []string{},
|
||||
ID: uuid.New().String(),
|
||||
UserId: userId,
|
||||
AccessId: accessId,
|
||||
SecretKey: secretKey,
|
||||
Status: ApiUserStatusNormal,
|
||||
WhiteList: []string{},
|
||||
BalanceAlertEnabled: defaultAlertEnabled,
|
||||
BalanceAlertThreshold: defaultAlertThreshold,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -124,6 +134,68 @@ func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 余额预警相关方法
|
||||
|
||||
// UpdateBalanceAlertSettings 更新余额预警设置
|
||||
func (u *ApiUser) UpdateBalanceAlertSettings(enabled bool, threshold float64, phone string) error {
|
||||
if threshold < 0 {
|
||||
return errors.New("预警阈值不能为负数")
|
||||
}
|
||||
if phone != "" && len(phone) != 11 {
|
||||
return errors.New("手机号格式不正确")
|
||||
}
|
||||
|
||||
u.BalanceAlertEnabled = enabled
|
||||
u.BalanceAlertThreshold = threshold
|
||||
u.AlertPhone = phone
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldSendLowBalanceAlert 是否应该发送低余额预警(24小时冷却期)
|
||||
func (u *ApiUser) ShouldSendLowBalanceAlert(balance float64) bool {
|
||||
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 余额低于阈值
|
||||
if balance < u.BalanceAlertThreshold {
|
||||
// 检查是否已经发送过预警(避免频繁发送)
|
||||
if u.LastLowBalanceAlert != nil {
|
||||
// 如果距离上次预警不足24小时,不发送
|
||||
if time.Since(*u.LastLowBalanceAlert) < 24*time.Hour {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ShouldSendArrearsAlert 是否应该发送欠费预警(不受冷却期限制)
|
||||
func (u *ApiUser) ShouldSendArrearsAlert(balance float64) bool {
|
||||
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 余额为负数(欠费)- 欠费预警不受冷却期限制
|
||||
if balance < 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarkLowBalanceAlertSent 标记低余额预警已发送
|
||||
func (u *ApiUser) MarkLowBalanceAlertSent() {
|
||||
now := time.Now()
|
||||
u.LastLowBalanceAlert = &now
|
||||
}
|
||||
|
||||
// MarkArrearsAlertSent 标记欠费预警已发送
|
||||
func (u *ApiUser) MarkArrearsAlertSent() {
|
||||
now := time.Now()
|
||||
u.LastArrearsAlert = &now
|
||||
}
|
||||
|
||||
// Validate 校验ApiUser聚合根的业务规则
|
||||
func (u *ApiUser) Validate() error {
|
||||
if u.UserId == "" {
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
@@ -27,6 +28,20 @@ type ApiCallRepository interface {
|
||||
// 新增:根据TransactionID查询
|
||||
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
|
||||
|
||||
// 统计相关方法
|
||||
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 管理端:根据条件筛选所有API调用记录(包含产品名称)
|
||||
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalCalls(ctx context.Context) (int64, error)
|
||||
GetSystemCallsByDateRange(ctx context.Context, startDate, endDate time.Time) (int64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// API受欢迎程度排行榜
|
||||
GetApiPopularityRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZ2A8B": ivyz.ProcessIVYZ2A8BRequest,
|
||||
"IVYZ7C9D": ivyz.ProcessIVYZ7C9DRequest,
|
||||
"IVYZ5E3F": ivyz.ProcessIVYZ5E3FRequest,
|
||||
"IVYZ7F3A": ivyz.ProcessIVYZ7F3ARequest,
|
||||
|
||||
// COMB系列处理器
|
||||
"COMB298Y": comb.ProcessCOMB298YRequest,
|
||||
|
||||
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
repo "tyapi-server/internal/domains/api/repositories"
|
||||
)
|
||||
@@ -20,14 +21,15 @@ type ApiUserAggregateService interface {
|
||||
|
||||
type ApiUserAggregateServiceImpl struct {
|
||||
repo repo.ApiUserRepository
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewApiUserAggregateService(repo repo.ApiUserRepository) ApiUserAggregateService {
|
||||
return &ApiUserAggregateServiceImpl{repo: repo}
|
||||
func NewApiUserAggregateService(repo repo.ApiUserRepository, cfg *config.Config) ApiUserAggregateService {
|
||||
return &ApiUserAggregateServiceImpl{repo: repo, cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *ApiUserAggregateServiceImpl) CreateApiUser(ctx context.Context, apiUserId string) error {
|
||||
apiUser, err := entities.NewApiUser(apiUserId)
|
||||
apiUser, err := entities.NewApiUser(apiUserId, s.cfg.Wallet.BalanceAlert.DefaultEnabled, s.cfg.Wallet.BalanceAlert.DefaultThreshold)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZ7F3ARequest IVYZ7F3A API处理方法 - 身份二要素认证(ZCI004)
|
||||
func ProcessIVYZ7F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ7F3AReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI004", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -35,7 +35,6 @@ type Article struct {
|
||||
IsFeatured bool `gorm:"default:false" json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
TaskID string `gorm:"type:varchar(100)" json:"task_id" comment:"定时任务ID"`
|
||||
|
||||
// 统计信息
|
||||
ViewCount int `gorm:"default:0" json:"view_count" comment:"阅读量"`
|
||||
@@ -120,7 +119,7 @@ func (a *Article) Publish() error {
|
||||
}
|
||||
|
||||
// SchedulePublish 定时发布文章
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error {
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
@@ -131,13 +130,12 @@ func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error
|
||||
|
||||
a.Status = ArticleStatusDraft // 保持草稿状态,等待定时发布
|
||||
a.ScheduledAt = &scheduledTime
|
||||
a.TaskID = taskID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSchedulePublish 更新定时发布时间
|
||||
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string) error {
|
||||
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
@@ -147,7 +145,6 @@ func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string)
|
||||
}
|
||||
|
||||
a.ScheduledAt = &scheduledTime
|
||||
a.TaskID = taskID
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -159,7 +156,6 @@ func (a *Article) CancelSchedulePublish() error {
|
||||
}
|
||||
|
||||
a.ScheduledAt = nil
|
||||
a.TaskID = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ type Wallet struct {
|
||||
// 钱包状态 - 钱包的基本状态信息
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"钱包是否激活"`
|
||||
Balance decimal.Decimal `gorm:"type:decimal(20,8);default:0" json:"balance" comment:"钱包余额(精确到8位小数)"`
|
||||
Version int64 `gorm:"version" json:"version" comment:"乐观锁版本号"`
|
||||
Version int64 `gorm:"default:0" json:"version" comment:"乐观锁版本号"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
|
||||
@@ -15,7 +15,7 @@ type WalletTransaction struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"交易记录唯一标识"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"扣款用户ID"`
|
||||
ApiCallID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"api_call_id" comment:"关联API调用ID"`
|
||||
TransactionID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id" comment:"交易ID"`
|
||||
TransactionID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id" comment:"交易ID"`
|
||||
ProductID string `gorm:"type:varchar(64);not null;index" json:"product_id" comment:"产品ID"`
|
||||
|
||||
// 扣款信息
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
@@ -20,4 +21,16 @@ type RechargeRecordRepository interface {
|
||||
// 管理员查询方法
|
||||
List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
|
||||
Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
|
||||
|
||||
// 统计相关方法
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// FinanceStats 财务统计信息
|
||||
@@ -25,7 +27,9 @@ type WalletRepository interface {
|
||||
GetByUserID(ctx context.Context, userID string) (*entities.Wallet, error)
|
||||
|
||||
// 乐观锁更新(自动重试)
|
||||
UpdateBalanceWithVersion(ctx context.Context, walletID string, newBalance string, oldVersion int64) (bool, error)
|
||||
UpdateBalanceWithVersion(ctx context.Context, walletID string, amount decimal.Decimal, operation string) (bool, error)
|
||||
// 乐观锁更新(通过用户ID直接更新,避免重复查询)
|
||||
UpdateBalanceByUserID(ctx context.Context, userID string, amount decimal.Decimal, operation string) (bool, error)
|
||||
|
||||
// 状态操作
|
||||
ActivateWallet(ctx context.Context, walletID string) error
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
@@ -26,6 +27,22 @@ type WalletTransactionRepository interface {
|
||||
// 新增:统计用户钱包交易次数
|
||||
CountByUserId(ctx context.Context, userId string) (int64, error)
|
||||
|
||||
// 统计相关方法
|
||||
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 管理端:根据条件筛选所有钱包交易记录(包含产品名称)
|
||||
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error)
|
||||
|
||||
// 管理端:导出钱包交易记录(包含产品名称和企业信息)
|
||||
ExportWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}) ([]*entities.WalletTransaction, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
}
|
||||
186
internal/domains/finance/services/balance_alert_service.go
Normal file
186
internal/domains/finance/services/balance_alert_service.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
)
|
||||
|
||||
// BalanceAlertService 余额预警服务接口
|
||||
type BalanceAlertService interface {
|
||||
CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error
|
||||
}
|
||||
|
||||
// BalanceAlertServiceImpl 余额预警服务实现
|
||||
type BalanceAlertServiceImpl struct {
|
||||
apiUserRepo api_repositories.ApiUserRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository
|
||||
smsService *sms.AliSMSService
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewBalanceAlertService 创建余额预警服务
|
||||
func NewBalanceAlertService(
|
||||
apiUserRepo api_repositories.ApiUserRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
|
||||
smsService *sms.AliSMSService,
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
) BalanceAlertService {
|
||||
return &BalanceAlertServiceImpl{
|
||||
apiUserRepo: apiUserRepo,
|
||||
userRepo: userRepo,
|
||||
enterpriseInfoRepo: enterpriseInfoRepo,
|
||||
smsService: smsService,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAndSendAlert 检查余额并发送预警
|
||||
func (s *BalanceAlertServiceImpl) CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error {
|
||||
// 1. 获取API用户信息
|
||||
apiUser, err := s.apiUserRepo.FindByUserId(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取API用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取API用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
if apiUser == nil {
|
||||
s.logger.Debug("API用户不存在,跳过余额预警检查", zap.String("user_id", userID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. 兼容性处理:如果API用户没有配置预警信息,从用户表获取并更新
|
||||
needUpdate := false
|
||||
if apiUser.AlertPhone == "" {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
if user.Phone != "" {
|
||||
apiUser.AlertPhone = user.Phone
|
||||
needUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 兼容性处理:如果API用户没有配置预警阈值,使用默认值
|
||||
if apiUser.BalanceAlertThreshold == 0 {
|
||||
apiUser.BalanceAlertThreshold = s.config.Wallet.BalanceAlert.DefaultThreshold
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
// 4. 如果需要更新API用户信息,保存到数据库
|
||||
if needUpdate {
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警配置失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
// 不返回错误,继续执行预警检查
|
||||
}
|
||||
}
|
||||
|
||||
balanceFloat, _ := balance.Float64()
|
||||
|
||||
// 5. 检查是否需要发送欠费预警(不受冷却期限制)
|
||||
if apiUser.ShouldSendArrearsAlert(balanceFloat) {
|
||||
if err := s.sendArrearsAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送欠费预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 欠费预警不受冷却期限制,不需要更新LastArrearsAlert时间
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. 检查是否需要发送低余额预警
|
||||
if apiUser.ShouldSendLowBalanceAlert(balanceFloat) {
|
||||
if err := s.sendLowBalanceAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送低余额预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 标记预警已发送
|
||||
apiUser.MarkLowBalanceAlertSent()
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警时间失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendArrearsAlert 发送欠费预警
|
||||
func (s *BalanceAlertServiceImpl) sendArrearsAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", "天远数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "天远数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送欠费预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", enterpriseName)
|
||||
}
|
||||
|
||||
// sendLowBalanceAlert 发送低余额预警
|
||||
func (s *BalanceAlertServiceImpl) sendLowBalanceAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", "天远数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "天远数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送低余额预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.Float64("threshold", apiUser.BalanceAlertThreshold),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", enterpriseName)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
@@ -25,21 +26,27 @@ type WalletAggregateService interface {
|
||||
|
||||
// WalletAggregateServiceImpl 钱包聚合服务实现
|
||||
type WalletAggregateServiceImpl struct {
|
||||
db *gorm.DB
|
||||
walletRepo repositories.WalletRepository
|
||||
transactionRepo repositories.WalletTransactionRepository
|
||||
balanceAlertSvc BalanceAlertService
|
||||
logger *zap.Logger
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewWalletAggregateService(
|
||||
walletRepo repositories.WalletRepository,
|
||||
transactionRepo repositories.WalletTransactionRepository,
|
||||
logger *zap.Logger,
|
||||
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,72 +69,59 @@ 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
// 使用数据库事务确保一致性
|
||||
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", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下充值失败,请重试")
|
||||
}
|
||||
|
||||
// 更新钱包余额
|
||||
w.AddBalance(amount)
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下充值失败,请重试")
|
||||
}
|
||||
s.logger.Info("钱包充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.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()))
|
||||
|
||||
return nil
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
// 使用数据库事务确保一致性
|
||||
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("更新钱包余额失败: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下扣款失败,请重试")
|
||||
}
|
||||
|
||||
// 扣减余额
|
||||
if err := w.SubtractBalance(amount); err != nil {
|
||||
return err
|
||||
}
|
||||
// 2. 创建扣款记录(检查是否已存在)
|
||||
transaction := entities.NewWalletTransaction(userID, apiCallID, transactionID, productID, amount)
|
||||
|
||||
// 更新钱包余额
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下扣款失败,请重试")
|
||||
}
|
||||
if err := tx.Create(transaction).Error; err != nil {
|
||||
return fmt.Errorf("创建扣款记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建扣款记录
|
||||
transaction := entities.NewWalletTransaction(userID, apiCallID, transactionID, productID, amount)
|
||||
_, err = s.transactionRepo.Create(ctx, *transaction)
|
||||
if err != nil {
|
||||
s.logger.Error("创建扣款记录失败", zap.Error(err))
|
||||
// 不返回错误,因为钱包余额已经更新成功
|
||||
}
|
||||
s.logger.Info("钱包扣款成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("api_call_id", apiCallID),
|
||||
zap.String("transaction_id", transactionID))
|
||||
|
||||
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))
|
||||
// 3. 扣费成功后异步检查余额预警
|
||||
go s.checkBalanceAlertAsync(context.Background(), userID)
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// GetBalance 查询余额
|
||||
func (s *WalletAggregateServiceImpl) GetBalance(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
@@ -140,3 +134,22 @@ func (s *WalletAggregateServiceImpl) GetBalance(ctx context.Context, userID stri
|
||||
func (s *WalletAggregateServiceImpl) LoadWalletByUserId(ctx context.Context, userID string) (*entities.Wallet, error) {
|
||||
return s.walletRepo.GetByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
// checkBalanceAlertAsync 异步检查余额预警
|
||||
func (s *WalletAggregateServiceImpl) checkBalanceAlertAsync(ctx context.Context, userID string) {
|
||||
// 获取最新余额
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取钱包余额失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查并发送预警
|
||||
if err := s.balanceAlertSvc.CheckAndSendAlert(ctx, userID, wallet.Balance); err != nil {
|
||||
s.logger.Error("余额预警检查失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
434
internal/domains/statistics/entities/statistics_dashboard.go
Normal file
434
internal/domains/statistics/entities/statistics_dashboard.go
Normal file
@@ -0,0 +1,434 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StatisticsDashboard 仪表板配置实体
|
||||
// 用于存储仪表板的配置信息
|
||||
type StatisticsDashboard struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"仪表板唯一标识"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name" comment:"仪表板名称"`
|
||||
Description string `gorm:"type:text" json:"description" comment:"仪表板描述"`
|
||||
UserRole string `gorm:"type:varchar(20);not null;index" json:"user_role" comment:"用户角色"`
|
||||
IsDefault bool `gorm:"default:false" json:"is_default" comment:"是否为默认仪表板"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"是否激活"`
|
||||
|
||||
// 仪表板配置
|
||||
Layout string `gorm:"type:json" json:"layout" comment:"布局配置"`
|
||||
Widgets string `gorm:"type:json" json:"widgets" comment:"组件配置"`
|
||||
Settings string `gorm:"type:json" json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `gorm:"default:300" json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||
|
||||
// 权限和访问控制
|
||||
CreatedBy string `gorm:"type:varchar(36);not null" json:"created_by" comment:"创建者ID"`
|
||||
AccessLevel string `gorm:"type:varchar(20);default:'private'" json:"access_level" comment:"访问级别"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (StatisticsDashboard) TableName() string {
|
||||
return "statistics_dashboards"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (s *StatisticsDashboard) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (s *StatisticsDashboard) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (s *StatisticsDashboard) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (s *StatisticsDashboard) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证仪表板配置信息
|
||||
// 检查仪表板必填字段是否完整,确保数据的有效性
|
||||
func (s *StatisticsDashboard) Validate() error {
|
||||
if s.Name == "" {
|
||||
return NewValidationError("仪表板名称不能为空")
|
||||
}
|
||||
if s.UserRole == "" {
|
||||
return NewValidationError("用户角色不能为空")
|
||||
}
|
||||
if s.CreatedBy == "" {
|
||||
return NewValidationError("创建者ID不能为空")
|
||||
}
|
||||
|
||||
// 验证用户角色
|
||||
if !s.IsValidUserRole() {
|
||||
return NewValidationError("无效的用户角色")
|
||||
}
|
||||
|
||||
// 验证访问级别
|
||||
if !s.IsValidAccessLevel() {
|
||||
return NewValidationError("无效的访问级别")
|
||||
}
|
||||
|
||||
// 验证刷新间隔
|
||||
if s.RefreshInterval < 30 {
|
||||
return NewValidationError("刷新间隔不能少于30秒")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidUserRole 检查用户角色是否有效
|
||||
func (s *StatisticsDashboard) IsValidUserRole() bool {
|
||||
validRoles := []string{
|
||||
"admin", // 管理员
|
||||
"user", // 普通用户
|
||||
"manager", // 经理
|
||||
"analyst", // 分析师
|
||||
}
|
||||
|
||||
for _, validRole := range validRoles {
|
||||
if s.UserRole == validRole {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidAccessLevel 检查访问级别是否有效
|
||||
func (s *StatisticsDashboard) IsValidAccessLevel() bool {
|
||||
validLevels := []string{
|
||||
"private", // 私有
|
||||
"public", // 公开
|
||||
"shared", // 共享
|
||||
}
|
||||
|
||||
for _, validLevel := range validLevels {
|
||||
if s.AccessLevel == validLevel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetUserRoleName 获取用户角色的中文名称
|
||||
func (s *StatisticsDashboard) GetUserRoleName() string {
|
||||
roleNames := map[string]string{
|
||||
"admin": "管理员",
|
||||
"user": "普通用户",
|
||||
"manager": "经理",
|
||||
"analyst": "分析师",
|
||||
}
|
||||
|
||||
if name, exists := roleNames[s.UserRole]; exists {
|
||||
return name
|
||||
}
|
||||
return s.UserRole
|
||||
}
|
||||
|
||||
// GetAccessLevelName 获取访问级别的中文名称
|
||||
func (s *StatisticsDashboard) GetAccessLevelName() string {
|
||||
levelNames := map[string]string{
|
||||
"private": "私有",
|
||||
"public": "公开",
|
||||
"shared": "共享",
|
||||
}
|
||||
|
||||
if name, exists := levelNames[s.AccessLevel]; exists {
|
||||
return name
|
||||
}
|
||||
return s.AccessLevel
|
||||
}
|
||||
|
||||
// NewStatisticsDashboard 工厂方法 - 创建仪表板配置
|
||||
func NewStatisticsDashboard(name, description, userRole, createdBy string) (*StatisticsDashboard, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("仪表板名称不能为空")
|
||||
}
|
||||
if userRole == "" {
|
||||
return nil, errors.New("用户角色不能为空")
|
||||
}
|
||||
if createdBy == "" {
|
||||
return nil, errors.New("创建者ID不能为空")
|
||||
}
|
||||
|
||||
dashboard := &StatisticsDashboard{
|
||||
Name: name,
|
||||
Description: description,
|
||||
UserRole: userRole,
|
||||
CreatedBy: createdBy,
|
||||
IsDefault: false,
|
||||
IsActive: true,
|
||||
AccessLevel: "private",
|
||||
RefreshInterval: 300, // 默认5分钟
|
||||
domainEvents: make([]interface{}, 0),
|
||||
}
|
||||
|
||||
// 验证仪表板
|
||||
if err := dashboard.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
dashboard.addDomainEvent(&StatisticsDashboardCreatedEvent{
|
||||
DashboardID: dashboard.ID,
|
||||
Name: name,
|
||||
UserRole: userRole,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
// SetAsDefault 设置为默认仪表板
|
||||
func (s *StatisticsDashboard) SetAsDefault() error {
|
||||
if !s.IsActive {
|
||||
return NewValidationError("只有激活状态的仪表板才能设置为默认")
|
||||
}
|
||||
|
||||
s.IsDefault = true
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardSetAsDefaultEvent{
|
||||
DashboardID: s.ID,
|
||||
SetAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAsDefault 取消默认状态
|
||||
func (s *StatisticsDashboard) RemoveAsDefault() error {
|
||||
if !s.IsDefault {
|
||||
return NewValidationError("当前仪表板不是默认仪表板")
|
||||
}
|
||||
|
||||
s.IsDefault = false
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardRemovedAsDefaultEvent{
|
||||
DashboardID: s.ID,
|
||||
RemovedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate 激活仪表板
|
||||
func (s *StatisticsDashboard) Activate() error {
|
||||
if s.IsActive {
|
||||
return NewValidationError("仪表板已经是激活状态")
|
||||
}
|
||||
|
||||
s.IsActive = true
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardActivatedEvent{
|
||||
DashboardID: s.ID,
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate 停用仪表板
|
||||
func (s *StatisticsDashboard) Deactivate() error {
|
||||
if !s.IsActive {
|
||||
return NewValidationError("仪表板已经是停用状态")
|
||||
}
|
||||
|
||||
s.IsActive = false
|
||||
|
||||
// 如果是默认仪表板,需要先取消默认状态
|
||||
if s.IsDefault {
|
||||
s.IsDefault = false
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardDeactivatedEvent{
|
||||
DashboardID: s.ID,
|
||||
DeactivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLayout 更新布局配置
|
||||
func (s *StatisticsDashboard) UpdateLayout(layout string) error {
|
||||
if layout == "" {
|
||||
return NewValidationError("布局配置不能为空")
|
||||
}
|
||||
|
||||
s.Layout = layout
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardLayoutUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWidgets 更新组件配置
|
||||
func (s *StatisticsDashboard) UpdateWidgets(widgets string) error {
|
||||
if widgets == "" {
|
||||
return NewValidationError("组件配置不能为空")
|
||||
}
|
||||
|
||||
s.Widgets = widgets
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardWidgetsUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSettings 更新设置配置
|
||||
func (s *StatisticsDashboard) UpdateSettings(settings string) error {
|
||||
s.Settings = settings
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardSettingsUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRefreshInterval 更新刷新间隔
|
||||
func (s *StatisticsDashboard) UpdateRefreshInterval(interval int) error {
|
||||
if interval < 30 {
|
||||
return NewValidationError("刷新间隔不能少于30秒")
|
||||
}
|
||||
|
||||
oldInterval := s.RefreshInterval
|
||||
s.RefreshInterval = interval
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsDashboardRefreshIntervalUpdatedEvent{
|
||||
DashboardID: s.ID,
|
||||
OldInterval: oldInterval,
|
||||
NewInterval: interval,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanBeModified 检查仪表板是否可以被修改
|
||||
func (s *StatisticsDashboard) CanBeModified() bool {
|
||||
return s.IsActive
|
||||
}
|
||||
|
||||
// CanBeDeleted 检查仪表板是否可以被删除
|
||||
func (s *StatisticsDashboard) CanBeDeleted() bool {
|
||||
return !s.IsDefault && s.IsActive
|
||||
}
|
||||
|
||||
// ================ 领域事件管理 ================
|
||||
|
||||
// addDomainEvent 添加领域事件
|
||||
func (s *StatisticsDashboard) addDomainEvent(event interface{}) {
|
||||
if s.domainEvents == nil {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
s.domainEvents = append(s.domainEvents, event)
|
||||
}
|
||||
|
||||
// GetDomainEvents 获取领域事件
|
||||
func (s *StatisticsDashboard) GetDomainEvents() []interface{} {
|
||||
return s.domainEvents
|
||||
}
|
||||
|
||||
// ClearDomainEvents 清除领域事件
|
||||
func (s *StatisticsDashboard) ClearDomainEvents() {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
|
||||
// ================ 领域事件定义 ================
|
||||
|
||||
// StatisticsDashboardCreatedEvent 仪表板创建事件
|
||||
type StatisticsDashboardCreatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
Name string `json:"name"`
|
||||
UserRole string `json:"user_role"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardSetAsDefaultEvent 仪表板设置为默认事件
|
||||
type StatisticsDashboardSetAsDefaultEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
SetAt time.Time `json:"set_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardRemovedAsDefaultEvent 仪表板取消默认事件
|
||||
type StatisticsDashboardRemovedAsDefaultEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
RemovedAt time.Time `json:"removed_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardActivatedEvent 仪表板激活事件
|
||||
type StatisticsDashboardActivatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
ActivatedAt time.Time `json:"activated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardDeactivatedEvent 仪表板停用事件
|
||||
type StatisticsDashboardDeactivatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
DeactivatedAt time.Time `json:"deactivated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardLayoutUpdatedEvent 仪表板布局更新事件
|
||||
type StatisticsDashboardLayoutUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardWidgetsUpdatedEvent 仪表板组件更新事件
|
||||
type StatisticsDashboardWidgetsUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardSettingsUpdatedEvent 仪表板设置更新事件
|
||||
type StatisticsDashboardSettingsUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardRefreshIntervalUpdatedEvent 仪表板刷新间隔更新事件
|
||||
type StatisticsDashboardRefreshIntervalUpdatedEvent struct {
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
OldInterval int `json:"old_interval"`
|
||||
NewInterval int `json:"new_interval"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
244
internal/domains/statistics/entities/statistics_metric.go
Normal file
244
internal/domains/statistics/entities/statistics_metric.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StatisticsMetric 统计指标实体
|
||||
// 用于存储各种统计指标数据,支持多维度统计
|
||||
type StatisticsMetric struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"统计指标唯一标识"`
|
||||
MetricType string `gorm:"type:varchar(50);not null;index" json:"metric_type" comment:"指标类型"`
|
||||
MetricName string `gorm:"type:varchar(100);not null" json:"metric_name" comment:"指标名称"`
|
||||
Dimension string `gorm:"type:varchar(50)" json:"dimension" comment:"统计维度"`
|
||||
Value float64 `gorm:"type:decimal(20,4);not null" json:"value" comment:"指标值"`
|
||||
Metadata string `gorm:"type:json" json:"metadata" comment:"额外维度信息"`
|
||||
Date time.Time `gorm:"type:date;index" json:"date" comment:"统计日期"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (StatisticsMetric) TableName() string {
|
||||
return "statistics_metrics"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (s *StatisticsMetric) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (s *StatisticsMetric) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (s *StatisticsMetric) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (s *StatisticsMetric) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证统计指标信息
|
||||
// 检查统计指标必填字段是否完整,确保数据的有效性
|
||||
func (s *StatisticsMetric) Validate() error {
|
||||
if s.MetricType == "" {
|
||||
return NewValidationError("指标类型不能为空")
|
||||
}
|
||||
if s.MetricName == "" {
|
||||
return NewValidationError("指标名称不能为空")
|
||||
}
|
||||
if s.Value < 0 {
|
||||
return NewValidationError("指标值不能为负数")
|
||||
}
|
||||
if s.Date.IsZero() {
|
||||
return NewValidationError("统计日期不能为空")
|
||||
}
|
||||
|
||||
// 验证指标类型
|
||||
if !s.IsValidMetricType() {
|
||||
return NewValidationError("无效的指标类型")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidMetricType 检查指标类型是否有效
|
||||
func (s *StatisticsMetric) IsValidMetricType() bool {
|
||||
validTypes := []string{
|
||||
"api_calls", // API调用统计
|
||||
"users", // 用户统计
|
||||
"finance", // 财务统计
|
||||
"products", // 产品统计
|
||||
"certification", // 认证统计
|
||||
}
|
||||
|
||||
for _, validType := range validTypes {
|
||||
if s.MetricType == validType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetMetricTypeName 获取指标类型的中文名称
|
||||
func (s *StatisticsMetric) GetMetricTypeName() string {
|
||||
typeNames := map[string]string{
|
||||
"api_calls": "API调用统计",
|
||||
"users": "用户统计",
|
||||
"finance": "财务统计",
|
||||
"products": "产品统计",
|
||||
"certification": "认证统计",
|
||||
}
|
||||
|
||||
if name, exists := typeNames[s.MetricType]; exists {
|
||||
return name
|
||||
}
|
||||
return s.MetricType
|
||||
}
|
||||
|
||||
// GetFormattedValue 获取格式化的指标值
|
||||
func (s *StatisticsMetric) GetFormattedValue() string {
|
||||
// 根据指标类型格式化数值
|
||||
switch s.MetricType {
|
||||
case "api_calls", "users":
|
||||
return fmt.Sprintf("%.0f", s.Value)
|
||||
case "finance":
|
||||
return fmt.Sprintf("%.2f", s.Value)
|
||||
default:
|
||||
return fmt.Sprintf("%.4f", s.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// NewStatisticsMetric 工厂方法 - 创建统计指标
|
||||
func NewStatisticsMetric(metricType, metricName, dimension string, value float64, date time.Time) (*StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, errors.New("指标类型不能为空")
|
||||
}
|
||||
if metricName == "" {
|
||||
return nil, errors.New("指标名称不能为空")
|
||||
}
|
||||
if value < 0 {
|
||||
return nil, errors.New("指标值不能为负数")
|
||||
}
|
||||
if date.IsZero() {
|
||||
return nil, errors.New("统计日期不能为空")
|
||||
}
|
||||
|
||||
metric := &StatisticsMetric{
|
||||
MetricType: metricType,
|
||||
MetricName: metricName,
|
||||
Dimension: dimension,
|
||||
Value: value,
|
||||
Date: date,
|
||||
domainEvents: make([]interface{}, 0),
|
||||
}
|
||||
|
||||
// 验证指标
|
||||
if err := metric.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
metric.addDomainEvent(&StatisticsMetricCreatedEvent{
|
||||
MetricID: metric.ID,
|
||||
MetricType: metricType,
|
||||
MetricName: metricName,
|
||||
Value: value,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return metric, nil
|
||||
}
|
||||
|
||||
// UpdateValue 更新指标值
|
||||
func (s *StatisticsMetric) UpdateValue(newValue float64) error {
|
||||
if newValue < 0 {
|
||||
return NewValidationError("指标值不能为负数")
|
||||
}
|
||||
|
||||
oldValue := s.Value
|
||||
s.Value = newValue
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsMetricUpdatedEvent{
|
||||
MetricID: s.ID,
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 领域事件管理 ================
|
||||
|
||||
// addDomainEvent 添加领域事件
|
||||
func (s *StatisticsMetric) addDomainEvent(event interface{}) {
|
||||
if s.domainEvents == nil {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
s.domainEvents = append(s.domainEvents, event)
|
||||
}
|
||||
|
||||
// GetDomainEvents 获取领域事件
|
||||
func (s *StatisticsMetric) GetDomainEvents() []interface{} {
|
||||
return s.domainEvents
|
||||
}
|
||||
|
||||
// ClearDomainEvents 清除领域事件
|
||||
func (s *StatisticsMetric) ClearDomainEvents() {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
|
||||
// ================ 领域事件定义 ================
|
||||
|
||||
// StatisticsMetricCreatedEvent 统计指标创建事件
|
||||
type StatisticsMetricCreatedEvent struct {
|
||||
MetricID string `json:"metric_id"`
|
||||
MetricType string `json:"metric_type"`
|
||||
MetricName string `json:"metric_name"`
|
||||
Value float64 `json:"value"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// StatisticsMetricUpdatedEvent 统计指标更新事件
|
||||
type StatisticsMetricUpdatedEvent struct {
|
||||
MetricID string `json:"metric_id"`
|
||||
OldValue float64 `json:"old_value"`
|
||||
NewValue float64 `json:"new_value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ValidationError 验证错误
|
||||
type ValidationError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewValidationError(message string) *ValidationError {
|
||||
return &ValidationError{Message: message}
|
||||
}
|
||||
343
internal/domains/statistics/entities/statistics_report.go
Normal file
343
internal/domains/statistics/entities/statistics_report.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StatisticsReport 统计报告实体
|
||||
// 用于存储生成的统计报告数据
|
||||
type StatisticsReport struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"报告唯一标识"`
|
||||
ReportType string `gorm:"type:varchar(50);not null;index" json:"report_type" comment:"报告类型"`
|
||||
Title string `gorm:"type:varchar(200);not null" json:"title" comment:"报告标题"`
|
||||
Content string `gorm:"type:json" json:"content" comment:"报告内容"`
|
||||
Period string `gorm:"type:varchar(20)" json:"period" comment:"统计周期"`
|
||||
UserRole string `gorm:"type:varchar(20)" json:"user_role" comment:"用户角色"`
|
||||
Status string `gorm:"type:varchar(20);default:'draft'" json:"status" comment:"报告状态"`
|
||||
|
||||
// 报告元数据
|
||||
GeneratedBy string `gorm:"type:varchar(36)" json:"generated_by" comment:"生成者ID"`
|
||||
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
|
||||
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (StatisticsReport) TableName() string {
|
||||
return "statistics_reports"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (s *StatisticsReport) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (s *StatisticsReport) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (s *StatisticsReport) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (s *StatisticsReport) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证统计报告信息
|
||||
// 检查统计报告必填字段是否完整,确保数据的有效性
|
||||
func (s *StatisticsReport) Validate() error {
|
||||
if s.ReportType == "" {
|
||||
return NewValidationError("报告类型不能为空")
|
||||
}
|
||||
if s.Title == "" {
|
||||
return NewValidationError("报告标题不能为空")
|
||||
}
|
||||
if s.Period == "" {
|
||||
return NewValidationError("统计周期不能为空")
|
||||
}
|
||||
|
||||
// 验证报告类型
|
||||
if !s.IsValidReportType() {
|
||||
return NewValidationError("无效的报告类型")
|
||||
}
|
||||
|
||||
// 验证报告状态
|
||||
if !s.IsValidStatus() {
|
||||
return NewValidationError("无效的报告状态")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidReportType 检查报告类型是否有效
|
||||
func (s *StatisticsReport) IsValidReportType() bool {
|
||||
validTypes := []string{
|
||||
"dashboard", // 仪表板报告
|
||||
"summary", // 汇总报告
|
||||
"detailed", // 详细报告
|
||||
"custom", // 自定义报告
|
||||
}
|
||||
|
||||
for _, validType := range validTypes {
|
||||
if s.ReportType == validType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidStatus 检查报告状态是否有效
|
||||
func (s *StatisticsReport) IsValidStatus() bool {
|
||||
validStatuses := []string{
|
||||
"draft", // 草稿
|
||||
"generating", // 生成中
|
||||
"completed", // 已完成
|
||||
"failed", // 生成失败
|
||||
"expired", // 已过期
|
||||
}
|
||||
|
||||
for _, validStatus := range validStatuses {
|
||||
if s.Status == validStatus {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetReportTypeName 获取报告类型的中文名称
|
||||
func (s *StatisticsReport) GetReportTypeName() string {
|
||||
typeNames := map[string]string{
|
||||
"dashboard": "仪表板报告",
|
||||
"summary": "汇总报告",
|
||||
"detailed": "详细报告",
|
||||
"custom": "自定义报告",
|
||||
}
|
||||
|
||||
if name, exists := typeNames[s.ReportType]; exists {
|
||||
return name
|
||||
}
|
||||
return s.ReportType
|
||||
}
|
||||
|
||||
// GetStatusName 获取报告状态的中文名称
|
||||
func (s *StatisticsReport) GetStatusName() string {
|
||||
statusNames := map[string]string{
|
||||
"draft": "草稿",
|
||||
"generating": "生成中",
|
||||
"completed": "已完成",
|
||||
"failed": "生成失败",
|
||||
"expired": "已过期",
|
||||
}
|
||||
|
||||
if name, exists := statusNames[s.Status]; exists {
|
||||
return name
|
||||
}
|
||||
return s.Status
|
||||
}
|
||||
|
||||
// NewStatisticsReport 工厂方法 - 创建统计报告
|
||||
func NewStatisticsReport(reportType, title, period, userRole string) (*StatisticsReport, error) {
|
||||
if reportType == "" {
|
||||
return nil, errors.New("报告类型不能为空")
|
||||
}
|
||||
if title == "" {
|
||||
return nil, errors.New("报告标题不能为空")
|
||||
}
|
||||
if period == "" {
|
||||
return nil, errors.New("统计周期不能为空")
|
||||
}
|
||||
|
||||
report := &StatisticsReport{
|
||||
ReportType: reportType,
|
||||
Title: title,
|
||||
Period: period,
|
||||
UserRole: userRole,
|
||||
Status: "draft",
|
||||
domainEvents: make([]interface{}, 0),
|
||||
}
|
||||
|
||||
// 验证报告
|
||||
if err := report.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加领域事件
|
||||
report.addDomainEvent(&StatisticsReportCreatedEvent{
|
||||
ReportID: report.ID,
|
||||
ReportType: reportType,
|
||||
Title: title,
|
||||
Period: period,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// StartGeneration 开始生成报告
|
||||
func (s *StatisticsReport) StartGeneration(generatedBy string) error {
|
||||
if s.Status != "draft" {
|
||||
return NewValidationError("只有草稿状态的报告才能开始生成")
|
||||
}
|
||||
|
||||
s.Status = "generating"
|
||||
s.GeneratedBy = generatedBy
|
||||
now := time.Now()
|
||||
s.GeneratedAt = &now
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportGenerationStartedEvent{
|
||||
ReportID: s.ID,
|
||||
GeneratedBy: generatedBy,
|
||||
StartedAt: now,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteGeneration 完成报告生成
|
||||
func (s *StatisticsReport) CompleteGeneration(content string) error {
|
||||
if s.Status != "generating" {
|
||||
return NewValidationError("只有生成中状态的报告才能完成生成")
|
||||
}
|
||||
|
||||
s.Status = "completed"
|
||||
s.Content = content
|
||||
|
||||
// 设置过期时间(默认7天)
|
||||
expiresAt := time.Now().Add(7 * 24 * time.Hour)
|
||||
s.ExpiresAt = &expiresAt
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportCompletedEvent{
|
||||
ReportID: s.ID,
|
||||
CompletedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailGeneration 报告生成失败
|
||||
func (s *StatisticsReport) FailGeneration(reason string) error {
|
||||
if s.Status != "generating" {
|
||||
return NewValidationError("只有生成中状态的报告才能标记为失败")
|
||||
}
|
||||
|
||||
s.Status = "failed"
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportFailedEvent{
|
||||
ReportID: s.ID,
|
||||
Reason: reason,
|
||||
FailedAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpired 检查报告是否已过期
|
||||
func (s *StatisticsReport) IsExpired() bool {
|
||||
if s.ExpiresAt == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().After(*s.ExpiresAt)
|
||||
}
|
||||
|
||||
// MarkAsExpired 标记报告为过期
|
||||
func (s *StatisticsReport) MarkAsExpired() error {
|
||||
if s.Status != "completed" {
|
||||
return NewValidationError("只有已完成状态的报告才能标记为过期")
|
||||
}
|
||||
|
||||
s.Status = "expired"
|
||||
|
||||
// 添加领域事件
|
||||
s.addDomainEvent(&StatisticsReportExpiredEvent{
|
||||
ReportID: s.ID,
|
||||
ExpiredAt: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanBeRegenerated 检查报告是否可以重新生成
|
||||
func (s *StatisticsReport) CanBeRegenerated() bool {
|
||||
return s.Status == "failed" || s.Status == "expired"
|
||||
}
|
||||
|
||||
// ================ 领域事件管理 ================
|
||||
|
||||
// addDomainEvent 添加领域事件
|
||||
func (s *StatisticsReport) addDomainEvent(event interface{}) {
|
||||
if s.domainEvents == nil {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
s.domainEvents = append(s.domainEvents, event)
|
||||
}
|
||||
|
||||
// GetDomainEvents 获取领域事件
|
||||
func (s *StatisticsReport) GetDomainEvents() []interface{} {
|
||||
return s.domainEvents
|
||||
}
|
||||
|
||||
// ClearDomainEvents 清除领域事件
|
||||
func (s *StatisticsReport) ClearDomainEvents() {
|
||||
s.domainEvents = make([]interface{}, 0)
|
||||
}
|
||||
|
||||
// ================ 领域事件定义 ================
|
||||
|
||||
// StatisticsReportCreatedEvent 统计报告创建事件
|
||||
type StatisticsReportCreatedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
ReportType string `json:"report_type"`
|
||||
Title string `json:"title"`
|
||||
Period string `json:"period"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportGenerationStartedEvent 统计报告生成开始事件
|
||||
type StatisticsReportGenerationStartedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
GeneratedBy string `json:"generated_by"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportCompletedEvent 统计报告完成事件
|
||||
type StatisticsReportCompletedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportFailedEvent 统计报告失败事件
|
||||
type StatisticsReportFailedEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
Reason string `json:"reason"`
|
||||
FailedAt time.Time `json:"failed_at"`
|
||||
}
|
||||
|
||||
// StatisticsReportExpiredEvent 统计报告过期事件
|
||||
type StatisticsReportExpiredEvent struct {
|
||||
ReportID string `json:"report_id"`
|
||||
ExpiredAt time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
572
internal/domains/statistics/events/statistics_events.go
Normal file
572
internal/domains/statistics/events/statistics_events.go
Normal file
@@ -0,0 +1,572 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// StatisticsEventType 统计事件类型
|
||||
type StatisticsEventType string
|
||||
|
||||
const (
|
||||
// 指标相关事件
|
||||
MetricCreatedEventType StatisticsEventType = "statistics.metric.created"
|
||||
MetricUpdatedEventType StatisticsEventType = "statistics.metric.updated"
|
||||
MetricAggregatedEventType StatisticsEventType = "statistics.metric.aggregated"
|
||||
|
||||
// 报告相关事件
|
||||
ReportCreatedEventType StatisticsEventType = "statistics.report.created"
|
||||
ReportGenerationStartedEventType StatisticsEventType = "statistics.report.generation_started"
|
||||
ReportCompletedEventType StatisticsEventType = "statistics.report.completed"
|
||||
ReportFailedEventType StatisticsEventType = "statistics.report.failed"
|
||||
ReportExpiredEventType StatisticsEventType = "statistics.report.expired"
|
||||
|
||||
// 仪表板相关事件
|
||||
DashboardCreatedEventType StatisticsEventType = "statistics.dashboard.created"
|
||||
DashboardUpdatedEventType StatisticsEventType = "statistics.dashboard.updated"
|
||||
DashboardActivatedEventType StatisticsEventType = "statistics.dashboard.activated"
|
||||
DashboardDeactivatedEventType StatisticsEventType = "statistics.dashboard.deactivated"
|
||||
)
|
||||
|
||||
// BaseStatisticsEvent 统计事件基础结构
|
||||
type BaseStatisticsEvent struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Source string `json:"source"`
|
||||
AggregateID string `json:"aggregate_id"`
|
||||
AggregateType string `json:"aggregate_type"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Payload interface{} `json:"payload"`
|
||||
|
||||
// DDD特有字段
|
||||
DomainVersion string `json:"domain_version"`
|
||||
CausationID string `json:"causation_id"`
|
||||
CorrelationID string `json:"correlation_id"`
|
||||
}
|
||||
|
||||
// 实现 Event 接口
|
||||
func (e *BaseStatisticsEvent) GetID() string {
|
||||
return e.ID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetType() string {
|
||||
return e.Type
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetVersion() string {
|
||||
return e.Version
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetTimestamp() time.Time {
|
||||
return e.Timestamp
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetPayload() interface{} {
|
||||
return e.Payload
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetMetadata() map[string]interface{} {
|
||||
return e.Metadata
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetSource() string {
|
||||
return e.Source
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetAggregateID() string {
|
||||
return e.AggregateID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetAggregateType() string {
|
||||
return e.AggregateType
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetDomainVersion() string {
|
||||
return e.DomainVersion
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetCausationID() string {
|
||||
return e.CausationID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) GetCorrelationID() string {
|
||||
return e.CorrelationID
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) Marshal() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
func (e *BaseStatisticsEvent) Unmarshal(data []byte) error {
|
||||
return json.Unmarshal(data, e)
|
||||
}
|
||||
|
||||
// ================ 指标相关事件 ================
|
||||
|
||||
// MetricCreatedEvent 指标创建事件
|
||||
type MetricCreatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
MetricID string `json:"metric_id"`
|
||||
MetricType string `json:"metric_type"`
|
||||
MetricName string `json:"metric_name"`
|
||||
Value float64 `json:"value"`
|
||||
Dimension string `json:"dimension"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
func NewMetricCreatedEvent(metricID, metricType, metricName, dimension string, value float64, date time.Time, correlationID string) *MetricCreatedEvent {
|
||||
return &MetricCreatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(MetricCreatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: metricID,
|
||||
AggregateType: "StatisticsMetric",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"metric_id": metricID,
|
||||
"metric_type": metricType,
|
||||
"metric_name": metricName,
|
||||
"dimension": dimension,
|
||||
},
|
||||
},
|
||||
MetricID: metricID,
|
||||
MetricType: metricType,
|
||||
MetricName: metricName,
|
||||
Value: value,
|
||||
Dimension: dimension,
|
||||
Date: date,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MetricCreatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"metric_id": e.MetricID,
|
||||
"metric_type": e.MetricType,
|
||||
"metric_name": e.MetricName,
|
||||
"value": e.Value,
|
||||
"dimension": e.Dimension,
|
||||
"date": e.Date,
|
||||
}
|
||||
}
|
||||
|
||||
// MetricUpdatedEvent 指标更新事件
|
||||
type MetricUpdatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
MetricID string `json:"metric_id"`
|
||||
OldValue float64 `json:"old_value"`
|
||||
NewValue float64 `json:"new_value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func NewMetricUpdatedEvent(metricID string, oldValue, newValue float64, correlationID string) *MetricUpdatedEvent {
|
||||
return &MetricUpdatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(MetricUpdatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: metricID,
|
||||
AggregateType: "StatisticsMetric",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"metric_id": metricID,
|
||||
},
|
||||
},
|
||||
MetricID: metricID,
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MetricUpdatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"metric_id": e.MetricID,
|
||||
"old_value": e.OldValue,
|
||||
"new_value": e.NewValue,
|
||||
"updated_at": e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// MetricAggregatedEvent 指标聚合事件
|
||||
type MetricAggregatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
MetricType string `json:"metric_type"`
|
||||
Dimension string `json:"dimension"`
|
||||
AggregatedAt time.Time `json:"aggregated_at"`
|
||||
RecordCount int `json:"record_count"`
|
||||
TotalValue float64 `json:"total_value"`
|
||||
}
|
||||
|
||||
func NewMetricAggregatedEvent(metricType, dimension string, recordCount int, totalValue float64, correlationID string) *MetricAggregatedEvent {
|
||||
return &MetricAggregatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(MetricAggregatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: uuid.New().String(),
|
||||
AggregateType: "StatisticsMetric",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"metric_type": metricType,
|
||||
"dimension": dimension,
|
||||
},
|
||||
},
|
||||
MetricType: metricType,
|
||||
Dimension: dimension,
|
||||
AggregatedAt: time.Now(),
|
||||
RecordCount: recordCount,
|
||||
TotalValue: totalValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MetricAggregatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"metric_type": e.MetricType,
|
||||
"dimension": e.Dimension,
|
||||
"aggregated_at": e.AggregatedAt,
|
||||
"record_count": e.RecordCount,
|
||||
"total_value": e.TotalValue,
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 报告相关事件 ================
|
||||
|
||||
// ReportCreatedEvent 报告创建事件
|
||||
type ReportCreatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
ReportType string `json:"report_type"`
|
||||
Title string `json:"title"`
|
||||
Period string `json:"period"`
|
||||
UserRole string `json:"user_role"`
|
||||
}
|
||||
|
||||
func NewReportCreatedEvent(reportID, reportType, title, period, userRole, correlationID string) *ReportCreatedEvent {
|
||||
return &ReportCreatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportCreatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
"report_type": reportType,
|
||||
"user_role": userRole,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
ReportType: reportType,
|
||||
Title: title,
|
||||
Period: period,
|
||||
UserRole: userRole,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportCreatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"report_type": e.ReportType,
|
||||
"title": e.Title,
|
||||
"period": e.Period,
|
||||
"user_role": e.UserRole,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportGenerationStartedEvent 报告生成开始事件
|
||||
type ReportGenerationStartedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
GeneratedBy string `json:"generated_by"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
func NewReportGenerationStartedEvent(reportID, generatedBy, correlationID string) *ReportGenerationStartedEvent {
|
||||
return &ReportGenerationStartedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportGenerationStartedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
"generated_by": generatedBy,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
GeneratedBy: generatedBy,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportGenerationStartedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"generated_by": e.GeneratedBy,
|
||||
"started_at": e.StartedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportCompletedEvent 报告完成事件
|
||||
type ReportCompletedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
ContentSize int `json:"content_size"`
|
||||
}
|
||||
|
||||
func NewReportCompletedEvent(reportID string, contentSize int, correlationID string) *ReportCompletedEvent {
|
||||
return &ReportCompletedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportCompletedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
CompletedAt: time.Now(),
|
||||
ContentSize: contentSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportCompletedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"completed_at": e.CompletedAt,
|
||||
"content_size": e.ContentSize,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportFailedEvent 报告失败事件
|
||||
type ReportFailedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
ReportID string `json:"report_id"`
|
||||
Reason string `json:"reason"`
|
||||
FailedAt time.Time `json:"failed_at"`
|
||||
}
|
||||
|
||||
func NewReportFailedEvent(reportID, reason, correlationID string) *ReportFailedEvent {
|
||||
return &ReportFailedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(ReportFailedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: reportID,
|
||||
AggregateType: "StatisticsReport",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"report_id": reportID,
|
||||
},
|
||||
},
|
||||
ReportID: reportID,
|
||||
Reason: reason,
|
||||
FailedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ReportFailedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"report_id": e.ReportID,
|
||||
"reason": e.Reason,
|
||||
"failed_at": e.FailedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 仪表板相关事件 ================
|
||||
|
||||
// DashboardCreatedEvent 仪表板创建事件
|
||||
type DashboardCreatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
Name string `json:"name"`
|
||||
UserRole string `json:"user_role"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
}
|
||||
|
||||
func NewDashboardCreatedEvent(dashboardID, name, userRole, createdBy, correlationID string) *DashboardCreatedEvent {
|
||||
return &DashboardCreatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardCreatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"user_role": userRole,
|
||||
"created_by": createdBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
Name: name,
|
||||
UserRole: userRole,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardCreatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"name": e.Name,
|
||||
"user_role": e.UserRole,
|
||||
"created_by": e.CreatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardUpdatedEvent 仪表板更新事件
|
||||
type DashboardUpdatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Changes map[string]interface{} `json:"changes"`
|
||||
}
|
||||
|
||||
func NewDashboardUpdatedEvent(dashboardID, updatedBy string, changes map[string]interface{}, correlationID string) *DashboardUpdatedEvent {
|
||||
return &DashboardUpdatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardUpdatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"updated_by": updatedBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
UpdatedBy: updatedBy,
|
||||
UpdatedAt: time.Now(),
|
||||
Changes: changes,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardUpdatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"updated_by": e.UpdatedBy,
|
||||
"updated_at": e.UpdatedAt,
|
||||
"changes": e.Changes,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardActivatedEvent 仪表板激活事件
|
||||
type DashboardActivatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
ActivatedBy string `json:"activated_by"`
|
||||
ActivatedAt time.Time `json:"activated_at"`
|
||||
}
|
||||
|
||||
func NewDashboardActivatedEvent(dashboardID, activatedBy, correlationID string) *DashboardActivatedEvent {
|
||||
return &DashboardActivatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardActivatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"activated_by": activatedBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
ActivatedBy: activatedBy,
|
||||
ActivatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardActivatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"activated_by": e.ActivatedBy,
|
||||
"activated_at": e.ActivatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardDeactivatedEvent 仪表板停用事件
|
||||
type DashboardDeactivatedEvent struct {
|
||||
*BaseStatisticsEvent
|
||||
DashboardID string `json:"dashboard_id"`
|
||||
DeactivatedBy string `json:"deactivated_by"`
|
||||
DeactivatedAt time.Time `json:"deactivated_at"`
|
||||
}
|
||||
|
||||
func NewDashboardDeactivatedEvent(dashboardID, deactivatedBy, correlationID string) *DashboardDeactivatedEvent {
|
||||
return &DashboardDeactivatedEvent{
|
||||
BaseStatisticsEvent: &BaseStatisticsEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(DashboardDeactivatedEventType),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "statistics-service",
|
||||
AggregateID: dashboardID,
|
||||
AggregateType: "StatisticsDashboard",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"dashboard_id": dashboardID,
|
||||
"deactivated_by": deactivatedBy,
|
||||
},
|
||||
},
|
||||
DashboardID: dashboardID,
|
||||
DeactivatedBy: deactivatedBy,
|
||||
DeactivatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashboardDeactivatedEvent) GetPayload() interface{} {
|
||||
return map[string]interface{}{
|
||||
"dashboard_id": e.DashboardID,
|
||||
"deactivated_by": e.DeactivatedBy,
|
||||
"deactivated_at": e.DeactivatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatisticsQuery 统计查询对象
|
||||
type StatisticsQuery struct {
|
||||
// 基础查询条件
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
MetricName string `json:"metric_name" form:"metric_name"` // 指标名称
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 排序参数
|
||||
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
|
||||
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
|
||||
|
||||
// 过滤条件
|
||||
MinValue float64 `json:"min_value" form:"min_value"` // 最小值
|
||||
MaxValue float64 `json:"max_value" form:"max_value"` // 最大值
|
||||
|
||||
// 聚合参数
|
||||
AggregateBy string `json:"aggregate_by" form:"aggregate_by"` // 聚合维度 (hour/day/week/month)
|
||||
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
|
||||
}
|
||||
|
||||
// StatisticsReportQuery 统计报告查询对象
|
||||
type StatisticsReportQuery struct {
|
||||
// 基础查询条件
|
||||
ReportType string `json:"report_type" form:"report_type"` // 报告类型
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
Status string `json:"status" form:"status"` // 报告状态
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 排序参数
|
||||
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
|
||||
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
|
||||
|
||||
// 过滤条件
|
||||
GeneratedBy string `json:"generated_by" form:"generated_by"` // 生成者ID
|
||||
AccessLevel string `json:"access_level" form:"access_level"` // 访问级别
|
||||
}
|
||||
|
||||
// StatisticsDashboardQuery 统计仪表板查询对象
|
||||
type StatisticsDashboardQuery struct {
|
||||
// 基础查询条件
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
IsDefault *bool `json:"is_default" form:"is_default"` // 是否默认
|
||||
IsActive *bool `json:"is_active" form:"is_active"` // 是否激活
|
||||
AccessLevel string `json:"access_level" form:"access_level"` // 访问级别
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 排序参数
|
||||
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
|
||||
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
|
||||
|
||||
// 过滤条件
|
||||
CreatedBy string `json:"created_by" form:"created_by"` // 创建者ID
|
||||
Name string `json:"name" form:"name"` // 仪表板名称
|
||||
}
|
||||
|
||||
// RealtimeStatisticsQuery 实时统计查询对象
|
||||
type RealtimeStatisticsQuery struct {
|
||||
// 查询条件
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
TimeRange string `json:"time_range" form:"time_range"` // 时间范围 (last_hour/last_day/last_week)
|
||||
|
||||
// 过滤条件
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
}
|
||||
|
||||
// HistoricalStatisticsQuery 历史统计查询对象
|
||||
type HistoricalStatisticsQuery struct {
|
||||
// 查询条件
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
|
||||
// 分页参数
|
||||
Limit int `json:"limit" form:"limit"` // 限制数量
|
||||
Offset int `json:"offset" form:"offset"` // 偏移量
|
||||
|
||||
// 聚合参数
|
||||
AggregateBy string `json:"aggregate_by" form:"aggregate_by"` // 聚合维度
|
||||
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
|
||||
|
||||
// 过滤条件
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
MinValue float64 `json:"min_value" form:"min_value"` // 最小值
|
||||
MaxValue float64 `json:"max_value" form:"max_value"` // 最大值
|
||||
}
|
||||
|
||||
// DashboardDataQuery 仪表板数据查询对象
|
||||
type DashboardDataQuery struct {
|
||||
// 查询条件
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
|
||||
// 时间范围
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 过滤条件
|
||||
MetricTypes []string `json:"metric_types" form:"metric_types"` // 指标类型列表
|
||||
Dimensions []string `json:"dimensions" form:"dimensions"` // 统计维度列表
|
||||
}
|
||||
|
||||
// ReportGenerationQuery 报告生成查询对象
|
||||
type ReportGenerationQuery struct {
|
||||
// 报告配置
|
||||
ReportType string `json:"report_type" form:"report_type"` // 报告类型
|
||||
Title string `json:"title" form:"title"` // 报告标题
|
||||
Period string `json:"period" form:"period"` // 统计周期
|
||||
UserRole string `json:"user_role" form:"user_role"` // 用户角色
|
||||
|
||||
// 时间范围
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 过滤条件
|
||||
Filters map[string]interface{} `json:"filters" form:"filters"` // 过滤条件
|
||||
|
||||
// 生成配置
|
||||
GeneratedBy string `json:"generated_by" form:"generated_by"` // 生成者ID
|
||||
Format string `json:"format" form:"format"` // 输出格式 (json/pdf/excel)
|
||||
}
|
||||
|
||||
// ExportQuery 导出查询对象
|
||||
type ExportQuery struct {
|
||||
// 导出配置
|
||||
Format string `json:"format" form:"format"` // 导出格式 (excel/csv/pdf)
|
||||
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
|
||||
|
||||
// 时间范围
|
||||
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
|
||||
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
|
||||
|
||||
// 过滤条件
|
||||
Dimension string `json:"dimension" form:"dimension"` // 统计维度
|
||||
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
|
||||
|
||||
// 导出配置
|
||||
IncludeCharts bool `json:"include_charts" form:"include_charts"` // 是否包含图表
|
||||
Columns []string `json:"columns" form:"columns"` // 导出列
|
||||
}
|
||||
|
||||
// Validate 验证统计查询对象
|
||||
func (q *StatisticsQuery) Validate() error {
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.StartDate.IsZero() || q.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if q.StartDate.After(q.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
|
||||
q.SortOrder = "desc" // 默认降序
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证统计报告查询对象
|
||||
func (q *StatisticsReportQuery) Validate() error {
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
|
||||
q.SortOrder = "desc" // 默认降序
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证统计仪表板查询对象
|
||||
func (q *StatisticsDashboardQuery) Validate() error {
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
|
||||
q.SortOrder = "desc" // 默认降序
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证实时统计查询对象
|
||||
func (q *RealtimeStatisticsQuery) Validate() error {
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.TimeRange == "" {
|
||||
q.TimeRange = "last_hour" // 默认最近1小时
|
||||
}
|
||||
validTimeRanges := []string{"last_hour", "last_day", "last_week"}
|
||||
for _, validRange := range validTimeRanges {
|
||||
if q.TimeRange == validRange {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("无效的时间范围: %s", q.TimeRange)
|
||||
}
|
||||
|
||||
// Validate 验证历史统计查询对象
|
||||
func (q *HistoricalStatisticsQuery) Validate() error {
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.StartDate.IsZero() || q.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if q.StartDate.After(q.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20 // 默认限制
|
||||
}
|
||||
if q.Limit > 1000 {
|
||||
q.Limit = 1000 // 最大限制
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证仪表板数据查询对象
|
||||
func (q *DashboardDataQuery) Validate() error {
|
||||
if q.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if q.Period == "" {
|
||||
q.Period = "today" // 默认今天
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证报告生成查询对象
|
||||
func (q *ReportGenerationQuery) Validate() error {
|
||||
if q.ReportType == "" {
|
||||
return fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
if q.Title == "" {
|
||||
return fmt.Errorf("报告标题不能为空")
|
||||
}
|
||||
if q.Period == "" {
|
||||
return fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
if q.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证导出查询对象
|
||||
func (q *ExportQuery) Validate() error {
|
||||
if q.Format == "" {
|
||||
return fmt.Errorf("导出格式不能为空")
|
||||
}
|
||||
if q.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if q.StartDate.IsZero() || q.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if q.StartDate.After(q.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
validFormats := []string{"excel", "csv", "pdf"}
|
||||
for _, validFormat := range validFormats {
|
||||
if q.Format == validFormat {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("无效的导出格式: %s", q.Format)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
)
|
||||
|
||||
// StatisticsRepository 统计指标仓储接口
|
||||
type StatisticsRepository interface {
|
||||
// 基础CRUD操作
|
||||
Save(ctx context.Context, metric *entities.StatisticsMetric) error
|
||||
FindByID(ctx context.Context, id string) (*entities.StatisticsMetric, error)
|
||||
FindByType(ctx context.Context, metricType string, limit, offset int) ([]*entities.StatisticsMetric, error)
|
||||
Update(ctx context.Context, metric *entities.StatisticsMetric) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// 按类型和日期范围查询
|
||||
FindByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
FindByTypeDimensionAndDateRange(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
FindByTypeNameAndDateRange(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
|
||||
// 聚合查询
|
||||
GetAggregatedMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) (map[string]float64, error)
|
||||
GetMetricsByDimension(ctx context.Context, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
|
||||
// 统计查询
|
||||
CountByType(ctx context.Context, metricType string) (int64, error)
|
||||
CountByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) (int64, error)
|
||||
|
||||
// 批量操作
|
||||
BatchSave(ctx context.Context, metrics []*entities.StatisticsMetric) error
|
||||
BatchDelete(ctx context.Context, ids []string) error
|
||||
|
||||
// 清理操作
|
||||
DeleteByDateRange(ctx context.Context, startDate, endDate time.Time) error
|
||||
DeleteByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) error
|
||||
}
|
||||
|
||||
// StatisticsReportRepository 统计报告仓储接口
|
||||
type StatisticsReportRepository interface {
|
||||
// 基础CRUD操作
|
||||
Save(ctx context.Context, report *entities.StatisticsReport) error
|
||||
FindByID(ctx context.Context, id string) (*entities.StatisticsReport, error)
|
||||
FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
FindByStatus(ctx context.Context, status string) ([]*entities.StatisticsReport, error)
|
||||
Update(ctx context.Context, report *entities.StatisticsReport) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// 按类型查询
|
||||
FindByType(ctx context.Context, reportType string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
FindByTypeAndPeriod(ctx context.Context, reportType, period string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
|
||||
// 按日期范围查询
|
||||
FindByDateRange(ctx context.Context, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
FindByUserAndDateRange(ctx context.Context, userID string, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
|
||||
// 统计查询
|
||||
CountByUser(ctx context.Context, userID string) (int64, error)
|
||||
CountByType(ctx context.Context, reportType string) (int64, error)
|
||||
CountByStatus(ctx context.Context, status string) (int64, error)
|
||||
|
||||
// 批量操作
|
||||
BatchSave(ctx context.Context, reports []*entities.StatisticsReport) error
|
||||
BatchDelete(ctx context.Context, ids []string) error
|
||||
|
||||
// 清理操作
|
||||
DeleteExpiredReports(ctx context.Context, expiredBefore time.Time) error
|
||||
DeleteByStatus(ctx context.Context, status string) error
|
||||
}
|
||||
|
||||
// StatisticsDashboardRepository 统计仪表板仓储接口
|
||||
type StatisticsDashboardRepository interface {
|
||||
// 基础CRUD操作
|
||||
Save(ctx context.Context, dashboard *entities.StatisticsDashboard) error
|
||||
FindByID(ctx context.Context, id string) (*entities.StatisticsDashboard, error)
|
||||
FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
FindByUserRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
Update(ctx context.Context, dashboard *entities.StatisticsDashboard) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// 按角色查询
|
||||
FindByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
FindDefaultByRole(ctx context.Context, userRole string) (*entities.StatisticsDashboard, error)
|
||||
FindActiveByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
|
||||
// 按状态查询
|
||||
FindByStatus(ctx context.Context, isActive bool, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
FindByAccessLevel(ctx context.Context, accessLevel string, limit, offset int) ([]*entities.StatisticsDashboard, error)
|
||||
|
||||
// 统计查询
|
||||
CountByUser(ctx context.Context, userID string) (int64, error)
|
||||
CountByRole(ctx context.Context, userRole string) (int64, error)
|
||||
CountByStatus(ctx context.Context, isActive bool) (int64, error)
|
||||
|
||||
// 批量操作
|
||||
BatchSave(ctx context.Context, dashboards []*entities.StatisticsDashboard) error
|
||||
BatchDelete(ctx context.Context, ids []string) error
|
||||
|
||||
// 特殊操作
|
||||
SetDefaultDashboard(ctx context.Context, dashboardID string) error
|
||||
RemoveDefaultDashboard(ctx context.Context, userRole string) error
|
||||
ActivateDashboard(ctx context.Context, dashboardID string) error
|
||||
DeactivateDashboard(ctx context.Context, dashboardID string) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// StatisticsAggregateService 统计聚合服务接口
|
||||
// 负责统计数据的聚合和计算
|
||||
type StatisticsAggregateService interface {
|
||||
// 实时统计
|
||||
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
|
||||
GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error)
|
||||
|
||||
// 历史统计聚合
|
||||
AggregateHourlyMetrics(ctx context.Context, date time.Time) error
|
||||
AggregateDailyMetrics(ctx context.Context, date time.Time) error
|
||||
AggregateWeeklyMetrics(ctx context.Context, date time.Time) error
|
||||
AggregateMonthlyMetrics(ctx context.Context, date time.Time) error
|
||||
|
||||
// 统计查询
|
||||
GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
|
||||
|
||||
// 统计计算
|
||||
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
|
||||
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
|
||||
}
|
||||
|
||||
// StatisticsAggregateServiceImpl 统计聚合服务实现
|
||||
type StatisticsAggregateServiceImpl struct {
|
||||
metricRepo repositories.StatisticsRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsAggregateService 创建统计聚合服务
|
||||
func NewStatisticsAggregateService(
|
||||
metricRepo repositories.StatisticsRepository,
|
||||
logger *zap.Logger,
|
||||
) StatisticsAggregateService {
|
||||
return &StatisticsAggregateServiceImpl{
|
||||
metricRepo: metricRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateRealtimeMetric 更新实时统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error {
|
||||
if metricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if metricName == "" {
|
||||
return fmt.Errorf("指标名称不能为空")
|
||||
}
|
||||
|
||||
// 创建或更新实时指标
|
||||
metric, err := entities.NewStatisticsMetric(metricType, metricName, "realtime", value, time.Now())
|
||||
if err != nil {
|
||||
s.logger.Error("创建统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("创建统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
err = s.metricRepo.Save(ctx, metric)
|
||||
if err != nil {
|
||||
s.logger.Error("保存统计指标失败",
|
||||
zap.String("metric_id", metric.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("实时统计指标更新成功",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("value", value))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRealtimeMetrics 获取实时统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
// 获取今天的实时指标
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("查询实时统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询实时统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为map格式
|
||||
result := make(map[string]float64)
|
||||
for _, metric := range metrics {
|
||||
if metric.Dimension == "realtime" {
|
||||
result[metric.MetricName] = metric.Value
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AggregateHourlyMetrics 聚合小时级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateHourlyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合小时级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定小时的所有实时指标
|
||||
startTime := date.Truncate(time.Hour)
|
||||
endTime := startTime.Add(time.Hour)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "hourly")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合小时级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合小时级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("小时级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AggregateDailyMetrics 聚合日级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateDailyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合日级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定日期的所有小时级指标
|
||||
startTime := date.Truncate(24 * time.Hour)
|
||||
endTime := startTime.Add(24 * time.Hour)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "daily")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合日级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合日级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("日级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AggregateWeeklyMetrics 聚合周级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateWeeklyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合周级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定周的所有日级指标
|
||||
startTime := date.Truncate(24 * time.Hour)
|
||||
endTime := startTime.Add(7 * 24 * time.Hour)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "weekly")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合周级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合周级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("周级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// AggregateMonthlyMetrics 聚合月级统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) AggregateMonthlyMetrics(ctx context.Context, date time.Time) error {
|
||||
s.logger.Info("开始聚合月级统计指标", zap.Time("date", date))
|
||||
|
||||
// 获取指定月的所有日级指标
|
||||
startTime := date.Truncate(24 * time.Hour)
|
||||
endTime := startTime.AddDate(0, 1, 0)
|
||||
|
||||
// 聚合不同类型的指标
|
||||
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
|
||||
|
||||
for _, metricType := range metricTypes {
|
||||
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "monthly")
|
||||
if err != nil {
|
||||
s.logger.Error("聚合月级指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("聚合月级指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("月级统计指标聚合完成", zap.Time("date", date))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetricsByType 根据类型获取统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// GetMetricsByDimension 根据维度获取统计指标
|
||||
func (s *StatisticsAggregateServiceImpl) GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
|
||||
if metricType == "" {
|
||||
return nil, fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if dimension == "" {
|
||||
return nil, fmt.Errorf("统计维度不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, dimension, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询统计指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("dimension", dimension),
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询统计指标失败: %w", err)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// CalculateGrowthRate 计算增长率
|
||||
func (s *StatisticsAggregateServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取当前周期的指标值
|
||||
currentMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询当前周期指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取上一周期的指标值
|
||||
previousMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询上一周期指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 计算总值
|
||||
var currentValue, previousValue float64
|
||||
for _, metric := range currentMetrics {
|
||||
currentValue += metric.Value
|
||||
}
|
||||
for _, metric := range previousMetrics {
|
||||
previousValue += metric.Value
|
||||
}
|
||||
|
||||
// 计算增长率
|
||||
if previousValue == 0 {
|
||||
if currentValue > 0 {
|
||||
return 100, nil // 从0增长到正数,增长率为100%
|
||||
}
|
||||
return 0, nil // 都是0,增长率为0%
|
||||
}
|
||||
|
||||
growthRate := ((currentValue - previousValue) / previousValue) * 100
|
||||
return growthRate, nil
|
||||
}
|
||||
|
||||
// CalculateTrend 计算趋势
|
||||
func (s *StatisticsAggregateServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return "", fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取时间范围内的指标
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < 2 {
|
||||
return "insufficient_data", nil // 数据不足
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sortMetricsByDate(metrics)
|
||||
|
||||
// 计算趋势
|
||||
firstValue := metrics[0].Value
|
||||
lastValue := metrics[len(metrics)-1].Value
|
||||
|
||||
if lastValue > firstValue {
|
||||
return "increasing", nil // 上升趋势
|
||||
} else if lastValue < firstValue {
|
||||
return "decreasing", nil // 下降趋势
|
||||
} else {
|
||||
return "stable", nil // 稳定趋势
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateMetricsByType 按类型聚合指标
|
||||
func (s *StatisticsAggregateServiceImpl) aggregateMetricsByType(ctx context.Context, metricType string, startTime, endTime time.Time, dimension string) error {
|
||||
// 获取源数据(实时或小时级数据)
|
||||
sourceDimension := "realtime"
|
||||
if dimension == "daily" {
|
||||
sourceDimension = "hourly"
|
||||
} else if dimension == "weekly" || dimension == "monthly" {
|
||||
sourceDimension = "daily"
|
||||
}
|
||||
|
||||
// 查询源数据
|
||||
sourceMetrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, sourceDimension, startTime, endTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询源数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 按指标名称分组聚合
|
||||
metricGroups := make(map[string][]*entities.StatisticsMetric)
|
||||
for _, metric := range sourceMetrics {
|
||||
metricGroups[metric.MetricName] = append(metricGroups[metric.MetricName], metric)
|
||||
}
|
||||
|
||||
// 聚合每个指标
|
||||
for metricName, metrics := range metricGroups {
|
||||
var totalValue float64
|
||||
for _, metric := range metrics {
|
||||
totalValue += metric.Value
|
||||
}
|
||||
|
||||
// 创建聚合后的指标
|
||||
aggregatedMetric, err := entities.NewStatisticsMetric(metricType, metricName, dimension, totalValue, startTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建聚合指标失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存聚合指标
|
||||
err = s.metricRepo.Save(ctx, aggregatedMetric)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存聚合指标失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sortMetricsByDate 按日期排序指标
|
||||
func sortMetricsByDate(metrics []*entities.StatisticsMetric) {
|
||||
// 简单的冒泡排序
|
||||
n := len(metrics)
|
||||
for i := 0; i < n-1; i++ {
|
||||
for j := 0; j < n-i-1; j++ {
|
||||
if metrics[j].Date.After(metrics[j+1].Date) {
|
||||
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// StatisticsCalculationService 统计计算服务接口
|
||||
// 负责各种统计计算和分析
|
||||
type StatisticsCalculationService interface {
|
||||
// 基础统计计算
|
||||
CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
|
||||
|
||||
// 高级统计计算
|
||||
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
|
||||
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
|
||||
CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error)
|
||||
|
||||
// 业务指标计算
|
||||
CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
|
||||
// 时间序列分析
|
||||
CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error)
|
||||
CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error)
|
||||
}
|
||||
|
||||
// StatisticsCalculationServiceImpl 统计计算服务实现
|
||||
type StatisticsCalculationServiceImpl struct {
|
||||
metricRepo repositories.StatisticsRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsCalculationService 创建统计计算服务
|
||||
func NewStatisticsCalculationService(
|
||||
metricRepo repositories.StatisticsRepository,
|
||||
logger *zap.Logger,
|
||||
) StatisticsCalculationService {
|
||||
return &StatisticsCalculationServiceImpl{
|
||||
metricRepo: metricRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTotal 计算总值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, metric := range metrics {
|
||||
total += metric.Value
|
||||
}
|
||||
|
||||
s.logger.Info("计算总值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("total", total))
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// CalculateAverage 计算平均值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, metric := range metrics {
|
||||
total += metric.Value
|
||||
}
|
||||
|
||||
average := total / float64(len(metrics))
|
||||
|
||||
s.logger.Info("计算平均值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("average", average))
|
||||
|
||||
return average, nil
|
||||
}
|
||||
|
||||
// CalculateMax 计算最大值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
max := metrics[0].Value
|
||||
for _, metric := range metrics {
|
||||
if metric.Value > max {
|
||||
max = metric.Value
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("计算最大值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("max", max))
|
||||
|
||||
return max, nil
|
||||
}
|
||||
|
||||
// CalculateMin 计算最小值
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
min := metrics[0].Value
|
||||
for _, metric := range metrics {
|
||||
if metric.Value < min {
|
||||
min = metric.Value
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("计算最小值完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("min", min))
|
||||
|
||||
return min, nil
|
||||
}
|
||||
|
||||
// CalculateGrowthRate 计算增长率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取当前周期的总值
|
||||
currentTotal, err := s.CalculateTotal(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算当前周期总值失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取上一周期的总值
|
||||
previousTotal, err := s.CalculateTotal(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算上一周期总值失败: %w", err)
|
||||
}
|
||||
|
||||
// 计算增长率
|
||||
if previousTotal == 0 {
|
||||
if currentTotal > 0 {
|
||||
return 100, nil // 从0增长到正数,增长率为100%
|
||||
}
|
||||
return 0, nil // 都是0,增长率为0%
|
||||
}
|
||||
|
||||
growthRate := ((currentTotal - previousTotal) / previousTotal) * 100
|
||||
|
||||
s.logger.Info("计算增长率完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Float64("growth_rate", growthRate))
|
||||
|
||||
return growthRate, nil
|
||||
}
|
||||
|
||||
// CalculateTrend 计算趋势
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return "", fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return "", fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < 2 {
|
||||
return "insufficient_data", nil // 数据不足
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sortMetricsByDateCalc(metrics)
|
||||
|
||||
// 计算趋势
|
||||
firstValue := metrics[0].Value
|
||||
lastValue := metrics[len(metrics)-1].Value
|
||||
|
||||
var trend string
|
||||
if lastValue > firstValue {
|
||||
trend = "increasing" // 上升趋势
|
||||
} else if lastValue < firstValue {
|
||||
trend = "decreasing" // 下降趋势
|
||||
} else {
|
||||
trend = "stable" // 稳定趋势
|
||||
}
|
||||
|
||||
s.logger.Info("计算趋势完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.String("trend", trend))
|
||||
|
||||
return trend, nil
|
||||
}
|
||||
|
||||
// CalculateCorrelation 计算相关性
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error) {
|
||||
if metricType1 == "" || metricName1 == "" || metricType2 == "" || metricName2 == "" {
|
||||
return 0, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
// 获取两个指标的数据
|
||||
metrics1, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType1, metricName1, startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询指标1失败: %w", err)
|
||||
}
|
||||
|
||||
metrics2, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType2, metricName2, startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查询指标2失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics1) != len(metrics2) || len(metrics1) < 2 {
|
||||
return 0, fmt.Errorf("数据点数量不足或不对称")
|
||||
}
|
||||
|
||||
// 计算皮尔逊相关系数
|
||||
correlation := s.calculatePearsonCorrelation(metrics1, metrics2)
|
||||
|
||||
s.logger.Info("计算相关性完成",
|
||||
zap.String("metric1", metricType1+"."+metricName1),
|
||||
zap.String("metric2", metricType2+"."+metricName2),
|
||||
zap.Float64("correlation", correlation))
|
||||
|
||||
return correlation, nil
|
||||
}
|
||||
|
||||
// CalculateSuccessRate 计算成功率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
// 获取成功调用次数
|
||||
successTotal, err := s.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算成功调用次数失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取总调用次数
|
||||
totalCalls, err := s.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算总调用次数失败: %w", err)
|
||||
}
|
||||
|
||||
if totalCalls == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
successRate := (successTotal / totalCalls) * 100
|
||||
|
||||
s.logger.Info("计算成功率完成",
|
||||
zap.Float64("success_rate", successRate))
|
||||
|
||||
return successRate, nil
|
||||
}
|
||||
|
||||
// CalculateConversionRate 计算转化率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
// 获取认证用户数
|
||||
certifiedUsers, err := s.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算认证用户数失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取总用户数
|
||||
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算总用户数失败: %w", err)
|
||||
}
|
||||
|
||||
if totalUsers == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
conversionRate := (certifiedUsers / totalUsers) * 100
|
||||
|
||||
s.logger.Info("计算转化率完成",
|
||||
zap.Float64("conversion_rate", conversionRate))
|
||||
|
||||
return conversionRate, nil
|
||||
}
|
||||
|
||||
// CalculateRetentionRate 计算留存率
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||
// 获取活跃用户数
|
||||
activeUsers, err := s.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算活跃用户数失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取总用户数
|
||||
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算总用户数失败: %w", err)
|
||||
}
|
||||
|
||||
if totalUsers == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
retentionRate := (activeUsers / totalUsers) * 100
|
||||
|
||||
s.logger.Info("计算留存率完成",
|
||||
zap.Float64("retention_rate", retentionRate))
|
||||
|
||||
return retentionRate, nil
|
||||
}
|
||||
|
||||
// CalculateMovingAverage 计算移动平均
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return nil, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
if windowSize <= 0 {
|
||||
return nil, fmt.Errorf("窗口大小必须大于0")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < windowSize {
|
||||
return nil, fmt.Errorf("数据点数量不足")
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sortMetricsByDateCalc(metrics)
|
||||
|
||||
// 计算移动平均
|
||||
var movingAverages []float64
|
||||
for i := windowSize - 1; i < len(metrics); i++ {
|
||||
var sum float64
|
||||
for j := i - windowSize + 1; j <= i; j++ {
|
||||
sum += metrics[j].Value
|
||||
}
|
||||
average := sum / float64(windowSize)
|
||||
movingAverages = append(movingAverages, average)
|
||||
}
|
||||
|
||||
s.logger.Info("计算移动平均完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Int("window_size", windowSize),
|
||||
zap.Int("result_count", len(movingAverages)))
|
||||
|
||||
return movingAverages, nil
|
||||
}
|
||||
|
||||
// CalculateSeasonality 计算季节性
|
||||
func (s *StatisticsCalculationServiceImpl) CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error) {
|
||||
if metricType == "" || metricName == "" {
|
||||
return nil, fmt.Errorf("指标类型和名称不能为空")
|
||||
}
|
||||
|
||||
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("查询指标失败",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询指标失败: %w", err)
|
||||
}
|
||||
|
||||
if len(metrics) < 7 {
|
||||
return nil, fmt.Errorf("数据点数量不足,至少需要7个数据点")
|
||||
}
|
||||
|
||||
// 按星期几分组
|
||||
weeklyAverages := make(map[string][]float64)
|
||||
for _, metric := range metrics {
|
||||
weekday := metric.Date.Weekday().String()
|
||||
weeklyAverages[weekday] = append(weeklyAverages[weekday], metric.Value)
|
||||
}
|
||||
|
||||
// 计算每个星期几的平均值
|
||||
seasonality := make(map[string]float64)
|
||||
for weekday, values := range weeklyAverages {
|
||||
var sum float64
|
||||
for _, value := range values {
|
||||
sum += value
|
||||
}
|
||||
seasonality[weekday] = sum / float64(len(values))
|
||||
}
|
||||
|
||||
s.logger.Info("计算季节性完成",
|
||||
zap.String("metric_type", metricType),
|
||||
zap.String("metric_name", metricName),
|
||||
zap.Int("weekday_count", len(seasonality)))
|
||||
|
||||
return seasonality, nil
|
||||
}
|
||||
|
||||
// calculatePearsonCorrelation 计算皮尔逊相关系数
|
||||
func (s *StatisticsCalculationServiceImpl) calculatePearsonCorrelation(metrics1, metrics2 []*entities.StatisticsMetric) float64 {
|
||||
n := len(metrics1)
|
||||
if n < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算均值
|
||||
var sum1, sum2 float64
|
||||
for i := 0; i < n; i++ {
|
||||
sum1 += metrics1[i].Value
|
||||
sum2 += metrics2[i].Value
|
||||
}
|
||||
mean1 := sum1 / float64(n)
|
||||
mean2 := sum2 / float64(n)
|
||||
|
||||
// 计算协方差和方差
|
||||
var numerator, denominator1, denominator2 float64
|
||||
for i := 0; i < n; i++ {
|
||||
diff1 := metrics1[i].Value - mean1
|
||||
diff2 := metrics2[i].Value - mean2
|
||||
numerator += diff1 * diff2
|
||||
denominator1 += diff1 * diff1
|
||||
denominator2 += diff2 * diff2
|
||||
}
|
||||
|
||||
// 计算相关系数
|
||||
if denominator1 == 0 || denominator2 == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
correlation := numerator / math.Sqrt(denominator1*denominator2)
|
||||
return correlation
|
||||
}
|
||||
|
||||
// sortMetricsByDateCalc 按日期排序指标
|
||||
func sortMetricsByDateCalc(metrics []*entities.StatisticsMetric) {
|
||||
// 简单的冒泡排序
|
||||
n := len(metrics)
|
||||
for i := 0; i < n-1; i++ {
|
||||
for j := 0; j < n-i-1; j++ {
|
||||
if metrics[j].Date.After(metrics[j+1].Date) {
|
||||
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/statistics/entities"
|
||||
"tyapi-server/internal/domains/statistics/repositories"
|
||||
)
|
||||
|
||||
// StatisticsReportService 报告生成服务接口
|
||||
// 负责统计报告的生成和管理
|
||||
type StatisticsReportService interface {
|
||||
// 报告生成
|
||||
GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error)
|
||||
GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error)
|
||||
GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error)
|
||||
|
||||
// 报告管理
|
||||
GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error)
|
||||
GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error)
|
||||
DeleteReport(ctx context.Context, reportID string) error
|
||||
|
||||
// 报告状态管理
|
||||
StartReportGeneration(ctx context.Context, reportID, generatedBy string) error
|
||||
CompleteReportGeneration(ctx context.Context, reportID string, content string) error
|
||||
FailReportGeneration(ctx context.Context, reportID string, reason string) error
|
||||
|
||||
// 报告清理
|
||||
CleanupExpiredReports(ctx context.Context) error
|
||||
}
|
||||
|
||||
// StatisticsReportServiceImpl 报告生成服务实现
|
||||
type StatisticsReportServiceImpl struct {
|
||||
reportRepo repositories.StatisticsReportRepository
|
||||
metricRepo repositories.StatisticsRepository
|
||||
calcService StatisticsCalculationService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewStatisticsReportService 创建报告生成服务
|
||||
func NewStatisticsReportService(
|
||||
reportRepo repositories.StatisticsReportRepository,
|
||||
metricRepo repositories.StatisticsRepository,
|
||||
calcService StatisticsCalculationService,
|
||||
logger *zap.Logger,
|
||||
) StatisticsReportService {
|
||||
return &StatisticsReportServiceImpl{
|
||||
reportRepo: reportRepo,
|
||||
metricRepo: metricRepo,
|
||||
calcService: calcService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateDashboardReport 生成仪表板报告
|
||||
func (s *StatisticsReportServiceImpl) GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error) {
|
||||
if userRole == "" {
|
||||
return nil, fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if period == "" {
|
||||
return nil, fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
|
||||
// 创建报告实体
|
||||
title := fmt.Sprintf("%s仪表板报告 - %s", s.getRoleDisplayName(userRole), s.getPeriodDisplayName(period))
|
||||
report, err := entities.NewStatisticsReport("dashboard", title, period, userRole)
|
||||
if err != nil {
|
||||
s.logger.Error("创建仪表板报告失败",
|
||||
zap.String("user_role", userRole),
|
||||
zap.String("period", period),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("创建仪表板报告失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存报告
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存仪表板报告失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("保存仪表板报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("仪表板报告创建成功",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.String("user_role", userRole),
|
||||
zap.String("period", period))
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GenerateSummaryReport 生成汇总报告
|
||||
func (s *StatisticsReportServiceImpl) GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error) {
|
||||
if period == "" {
|
||||
return nil, fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
|
||||
// 创建报告实体
|
||||
title := fmt.Sprintf("汇总报告 - %s (%s 至 %s)",
|
||||
s.getPeriodDisplayName(period),
|
||||
startDate.Format("2006-01-02"),
|
||||
endDate.Format("2006-01-02"))
|
||||
report, err := entities.NewStatisticsReport("summary", title, period, "admin")
|
||||
if err != nil {
|
||||
s.logger.Error("创建汇总报告失败",
|
||||
zap.String("period", period),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("创建汇总报告失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成报告内容
|
||||
content, err := s.generateSummaryContent(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("生成汇总报告内容失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("生成汇总报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
// 完成报告生成
|
||||
err = report.CompleteGeneration(content)
|
||||
if err != nil {
|
||||
s.logger.Error("完成汇总报告生成失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("完成汇总报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存报告
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存汇总报告失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("保存汇总报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("汇总报告生成成功",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.String("period", period))
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GenerateDetailedReport 生成详细报告
|
||||
func (s *StatisticsReportServiceImpl) GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error) {
|
||||
if reportType == "" {
|
||||
return nil, fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
|
||||
// 创建报告实体
|
||||
title := fmt.Sprintf("详细报告 - %s (%s 至 %s)",
|
||||
reportType,
|
||||
startDate.Format("2006-01-02"),
|
||||
endDate.Format("2006-01-02"))
|
||||
report, err := entities.NewStatisticsReport("detailed", title, "custom", "admin")
|
||||
if err != nil {
|
||||
s.logger.Error("创建详细报告失败",
|
||||
zap.String("report_type", reportType),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("创建详细报告失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成报告内容
|
||||
content, err := s.generateDetailedContent(ctx, reportType, startDate, endDate, filters)
|
||||
if err != nil {
|
||||
s.logger.Error("生成详细报告内容失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("生成详细报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
// 完成报告生成
|
||||
err = report.CompleteGeneration(content)
|
||||
if err != nil {
|
||||
s.logger.Error("完成详细报告生成失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("完成详细报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存报告
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存详细报告失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("保存详细报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("详细报告生成成功",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.String("report_type", reportType))
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GetReport 获取报告
|
||||
func (s *StatisticsReportServiceImpl) GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error) {
|
||||
if reportID == "" {
|
||||
return nil, fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询报告失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// GetReportsByUser 获取用户的报告列表
|
||||
func (s *StatisticsReportServiceImpl) GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error) {
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
reports, err := s.reportRepo.FindByUser(ctx, userID, limit, offset)
|
||||
if err != nil {
|
||||
s.logger.Error("查询用户报告失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("查询用户报告失败: %w", err)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// DeleteReport 删除报告
|
||||
func (s *StatisticsReportServiceImpl) DeleteReport(ctx context.Context, reportID string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
err := s.reportRepo.Delete(ctx, reportID)
|
||||
if err != nil {
|
||||
s.logger.Error("删除报告失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("删除报告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告删除成功", zap.String("report_id", reportID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartReportGeneration 开始报告生成
|
||||
func (s *StatisticsReportServiceImpl) StartReportGeneration(ctx context.Context, reportID, generatedBy string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
if generatedBy == "" {
|
||||
return fmt.Errorf("生成者ID不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
err = report.StartGeneration(generatedBy)
|
||||
if err != nil {
|
||||
s.logger.Error("开始报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("开始报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存报告状态失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存报告状态失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告生成开始",
|
||||
zap.String("report_id", reportID),
|
||||
zap.String("generated_by", generatedBy))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteReportGeneration 完成报告生成
|
||||
func (s *StatisticsReportServiceImpl) CompleteReportGeneration(ctx context.Context, reportID string, content string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
if content == "" {
|
||||
return fmt.Errorf("报告内容不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
err = report.CompleteGeneration(content)
|
||||
if err != nil {
|
||||
s.logger.Error("完成报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("完成报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存报告内容失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告生成完成", zap.String("report_id", reportID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailReportGeneration 报告生成失败
|
||||
func (s *StatisticsReportServiceImpl) FailReportGeneration(ctx context.Context, reportID string, reason string) error {
|
||||
if reportID == "" {
|
||||
return fmt.Errorf("报告ID不能为空")
|
||||
}
|
||||
|
||||
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询报告失败: %w", err)
|
||||
}
|
||||
|
||||
err = report.FailGeneration(reason)
|
||||
if err != nil {
|
||||
s.logger.Error("标记报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("标记报告生成失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存报告状态失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存报告状态失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("报告生成失败",
|
||||
zap.String("report_id", reportID),
|
||||
zap.String("reason", reason))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupExpiredReports 清理过期报告
|
||||
func (s *StatisticsReportServiceImpl) CleanupExpiredReports(ctx context.Context) error {
|
||||
s.logger.Info("开始清理过期报告")
|
||||
|
||||
// 获取所有已完成的报告
|
||||
reports, err := s.reportRepo.FindByStatus(ctx, "completed")
|
||||
if err != nil {
|
||||
s.logger.Error("查询已完成报告失败", zap.Error(err))
|
||||
return fmt.Errorf("查询已完成报告失败: %w", err)
|
||||
}
|
||||
|
||||
var deletedCount int
|
||||
for _, report := range reports {
|
||||
if report.IsExpired() {
|
||||
err = report.MarkAsExpired()
|
||||
if err != nil {
|
||||
s.logger.Error("标记报告过期失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.reportRepo.Save(ctx, report)
|
||||
if err != nil {
|
||||
s.logger.Error("保存过期报告状态失败",
|
||||
zap.String("report_id", report.ID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
deletedCount++
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("过期报告清理完成", zap.Int("deleted_count", deletedCount))
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSummaryContent 生成汇总报告内容
|
||||
func (s *StatisticsReportServiceImpl) generateSummaryContent(ctx context.Context, startDate, endDate time.Time) (string, error) {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// API调用统计
|
||||
apiCallsTotal, err := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算API调用总数失败", zap.Error(err))
|
||||
}
|
||||
apiCallsSuccess, err := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算API调用成功数失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 用户统计
|
||||
usersTotal, err := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算用户总数失败", zap.Error(err))
|
||||
}
|
||||
usersCertified, err := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算认证用户数失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 财务统计
|
||||
financeTotal, err := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Warn("计算财务总额失败", zap.Error(err))
|
||||
}
|
||||
|
||||
content["api_calls"] = map[string]interface{}{
|
||||
"total": apiCallsTotal,
|
||||
"success": apiCallsSuccess,
|
||||
"rate": s.calculateRate(apiCallsSuccess, apiCallsTotal),
|
||||
}
|
||||
|
||||
content["users"] = map[string]interface{}{
|
||||
"total": usersTotal,
|
||||
"certified": usersCertified,
|
||||
"rate": s.calculateRate(usersCertified, usersTotal),
|
||||
}
|
||||
|
||||
content["finance"] = map[string]interface{}{
|
||||
"total_amount": financeTotal,
|
||||
}
|
||||
|
||||
content["period"] = map[string]interface{}{
|
||||
"start_date": startDate.Format("2006-01-02"),
|
||||
"end_date": endDate.Format("2006-01-02"),
|
||||
}
|
||||
|
||||
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
// 转换为JSON字符串
|
||||
jsonContent, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
return string(jsonContent), nil
|
||||
}
|
||||
|
||||
// generateDetailedContent 生成详细报告内容
|
||||
func (s *StatisticsReportServiceImpl) generateDetailedContent(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (string, error) {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 根据报告类型生成不同的内容
|
||||
switch reportType {
|
||||
case "api_calls":
|
||||
content = s.generateApiCallsDetailedContent(ctx, startDate, endDate, filters)
|
||||
case "users":
|
||||
content = s.generateUsersDetailedContent(ctx, startDate, endDate, filters)
|
||||
case "finance":
|
||||
content = s.generateFinanceDetailedContent(ctx, startDate, endDate, filters)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的报告类型: %s", reportType)
|
||||
}
|
||||
|
||||
content["report_type"] = reportType
|
||||
content["period"] = map[string]interface{}{
|
||||
"start_date": startDate.Format("2006-01-02"),
|
||||
"end_date": endDate.Format("2006-01-02"),
|
||||
}
|
||||
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
// 转换为JSON字符串
|
||||
jsonContent, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化报告内容失败: %w", err)
|
||||
}
|
||||
|
||||
return string(jsonContent), nil
|
||||
}
|
||||
|
||||
// generateApiCallsDetailedContent 生成API调用详细内容
|
||||
func (s *StatisticsReportServiceImpl) generateApiCallsDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 获取API调用统计数据
|
||||
totalCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
|
||||
successCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
|
||||
failedCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "failed_count", startDate, endDate)
|
||||
avgResponseTime, _ := s.calcService.CalculateAverage(ctx, "api_calls", "response_time", startDate, endDate)
|
||||
|
||||
content["total_calls"] = totalCalls
|
||||
content["success_calls"] = successCalls
|
||||
content["failed_calls"] = failedCalls
|
||||
content["success_rate"] = s.calculateRate(successCalls, totalCalls)
|
||||
content["avg_response_time"] = avgResponseTime
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// generateUsersDetailedContent 生成用户详细内容
|
||||
func (s *StatisticsReportServiceImpl) generateUsersDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 获取用户统计数据
|
||||
totalUsers, _ := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
|
||||
certifiedUsers, _ := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
|
||||
activeUsers, _ := s.calcService.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
|
||||
|
||||
content["total_users"] = totalUsers
|
||||
content["certified_users"] = certifiedUsers
|
||||
content["active_users"] = activeUsers
|
||||
content["certification_rate"] = s.calculateRate(certifiedUsers, totalUsers)
|
||||
content["retention_rate"] = s.calculateRate(activeUsers, totalUsers)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// generateFinanceDetailedContent 生成财务详细内容
|
||||
func (s *StatisticsReportServiceImpl) generateFinanceDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
|
||||
content := make(map[string]interface{})
|
||||
|
||||
// 获取财务统计数据
|
||||
totalAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
|
||||
rechargeAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "recharge_amount", startDate, endDate)
|
||||
deductAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "deduct_amount", startDate, endDate)
|
||||
|
||||
content["total_amount"] = totalAmount
|
||||
content["recharge_amount"] = rechargeAmount
|
||||
content["deduct_amount"] = deductAmount
|
||||
content["net_amount"] = rechargeAmount - deductAmount
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// calculateRate 计算比率
|
||||
func (s *StatisticsReportServiceImpl) calculateRate(numerator, denominator float64) float64 {
|
||||
if denominator == 0 {
|
||||
return 0
|
||||
}
|
||||
return (numerator / denominator) * 100
|
||||
}
|
||||
|
||||
// getRoleDisplayName 获取角色显示名称
|
||||
func (s *StatisticsReportServiceImpl) getRoleDisplayName(role string) string {
|
||||
roleNames := map[string]string{
|
||||
"admin": "管理员",
|
||||
"user": "用户",
|
||||
"manager": "经理",
|
||||
"analyst": "分析师",
|
||||
}
|
||||
if name, exists := roleNames[role]; exists {
|
||||
return name
|
||||
}
|
||||
return role
|
||||
}
|
||||
|
||||
// getPeriodDisplayName 获取周期显示名称
|
||||
func (s *StatisticsReportServiceImpl) getPeriodDisplayName(period string) string {
|
||||
periodNames := map[string]string{
|
||||
"today": "今日",
|
||||
"week": "本周",
|
||||
"month": "本月",
|
||||
"quarter": "本季度",
|
||||
"year": "本年",
|
||||
}
|
||||
if name, exists := periodNames[period]; exists {
|
||||
return name
|
||||
}
|
||||
return period
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/repositories/queries"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
@@ -27,6 +28,7 @@ type UserRepository interface {
|
||||
|
||||
// 关联查询
|
||||
GetByIDWithEnterpriseInfo(ctx context.Context, id string) (entities.User, error)
|
||||
BatchGetByIDsWithEnterpriseInfo(ctx context.Context, ids []string) ([]*entities.User, error)
|
||||
|
||||
// 企业信息查询
|
||||
ExistsByUnifiedSocialCode(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error)
|
||||
@@ -46,6 +48,17 @@ type UserRepository interface {
|
||||
// 统计信息
|
||||
GetStats(ctx context.Context) (*UserStats, error)
|
||||
GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemUserStats(ctx context.Context) (*UserStats, error)
|
||||
GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*UserStats, error)
|
||||
GetSystemDailyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 排行榜查询方法
|
||||
GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
GetUserCallRankingByConsumption(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
GetRechargeRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// SMSCodeRepository 短信验证码仓储接口
|
||||
|
||||
Reference in New Issue
Block a user