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

View File

@@ -271,6 +271,12 @@ type IVYZ5E3FReq struct {
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}
type IVYZ7F3AReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}
type YYSY4F2EReq struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,validIDCard"`

View File

@@ -1,11 +1,7 @@
package entities
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
"github.com/google/uuid"
@@ -56,7 +52,7 @@ type ApiCall struct {
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
UserId *string `gorm:"type:varchar(36);index" json:"user_id,omitempty"`
ProductId *string `gorm:"type:varchar(64);index" json:"product_id,omitempty"`
TransactionId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id"`
TransactionId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id"`
ClientIp string `gorm:"type:varchar(64);not null;index" json:"client_ip"`
RequestParams string `gorm:"type:text" json:"request_params"`
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
@@ -145,40 +141,9 @@ func (a *ApiCall) Validate() error {
return nil
}
// 全局计数器,用于确保TransactionID的唯一性
var (
transactionCounter int64
counterMutex sync.Mutex
)
// GenerateTransactionID 生成16位数的交易单号
// GenerateTransactionID 生成UUID格式的交易单号
func GenerateTransactionID() string {
// 使用互斥锁确保计数器的线程安全
counterMutex.Lock()
transactionCounter++
currentCounter := transactionCounter
counterMutex.Unlock()
// 获取当前时间戳(微秒精度)
timestamp := time.Now().UnixMicro()
// 组合时间戳和计数器,确保唯一性
combined := fmt.Sprintf("%d%06d", timestamp, currentCounter%1000000)
// 如果长度超出16位截断如果不够填充随机字符
if len(combined) >= 16 {
return combined[:16]
}
// 如果长度不够,使用随机字节填充
if len(combined) < 16 {
randomBytes := make([]byte, 8)
rand.Read(randomBytes)
randomHex := hex.EncodeToString(randomBytes)
combined += randomHex[:16-len(combined)]
}
return combined
return uuid.New().String()
}
// TableName 指定数据库表名

View File

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

View File

@@ -2,6 +2,7 @@ package repositories
import (
"context"
"time"
"tyapi-server/internal/domains/api/entities"
"tyapi-server/internal/shared/interfaces"
)
@@ -27,6 +28,20 @@ type ApiCallRepository interface {
// 新增根据TransactionID查询
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
// 统计相关方法
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
// 管理端根据条件筛选所有API调用记录包含产品名称
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error)
// 系统级别统计方法
GetSystemTotalCalls(ctx context.Context) (int64, error)
GetSystemCallsByDateRange(ctx context.Context, startDate, endDate time.Time) (int64, error)
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
// API受欢迎程度排行榜
GetApiPopularityRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
}

View File

@@ -149,6 +149,7 @@ func registerAllProcessors(combService *comb.CombService) {
"IVYZ2A8B": ivyz.ProcessIVYZ2A8BRequest,
"IVYZ7C9D": ivyz.ProcessIVYZ7C9DRequest,
"IVYZ5E3F": ivyz.ProcessIVYZ5E3FRequest,
"IVYZ7F3A": ivyz.ProcessIVYZ7F3ARequest,
// COMB系列处理器
"COMB298Y": comb.ProcessCOMB298YRequest,

View File

@@ -2,6 +2,7 @@ package services
import (
"context"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/api/entities"
repo "tyapi-server/internal/domains/api/repositories"
)
@@ -20,14 +21,15 @@ type ApiUserAggregateService interface {
type ApiUserAggregateServiceImpl struct {
repo repo.ApiUserRepository
cfg *config.Config
}
func NewApiUserAggregateService(repo repo.ApiUserRepository) ApiUserAggregateService {
return &ApiUserAggregateServiceImpl{repo: repo}
func NewApiUserAggregateService(repo repo.ApiUserRepository, cfg *config.Config) ApiUserAggregateService {
return &ApiUserAggregateServiceImpl{repo: repo, cfg: cfg}
}
func (s *ApiUserAggregateServiceImpl) CreateApiUser(ctx context.Context, apiUserId string) error {
apiUser, err := entities.NewApiUser(apiUserId)
apiUser, err := entities.NewApiUser(apiUserId, s.cfg.Wallet.BalanceAlert.DefaultEnabled, s.cfg.Wallet.BalanceAlert.DefaultThreshold)
if err != nil {
return err
}

View File

@@ -0,0 +1,56 @@
package ivyz
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessIVYZ7F3ARequest IVYZ7F3A API处理方法 - 身份二要素认证(ZCI004)
func ProcessIVYZ7F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.IVYZ7F3AReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI004", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -35,7 +35,6 @@ type Article struct {
IsFeatured bool `gorm:"default:false" json:"is_featured" comment:"是否推荐"`
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
TaskID string `gorm:"type:varchar(100)" json:"task_id" comment:"定时任务ID"`
// 统计信息
ViewCount int `gorm:"default:0" json:"view_count" comment:"阅读量"`
@@ -120,7 +119,7 @@ func (a *Article) Publish() error {
}
// SchedulePublish 定时发布文章
func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error {
func (a *Article) SchedulePublish(scheduledTime time.Time) error {
if a.Status == ArticleStatusPublished {
return NewValidationError("文章已经是发布状态")
}
@@ -131,13 +130,12 @@ func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error
a.Status = ArticleStatusDraft // 保持草稿状态,等待定时发布
a.ScheduledAt = &scheduledTime
a.TaskID = taskID
return nil
}
// UpdateSchedulePublish 更新定时发布时间
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string) error {
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time) error {
if a.Status == ArticleStatusPublished {
return NewValidationError("文章已经是发布状态")
}
@@ -147,7 +145,6 @@ func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string)
}
a.ScheduledAt = &scheduledTime
a.TaskID = taskID
return nil
}
@@ -159,7 +156,6 @@ func (a *Article) CancelSchedulePublish() error {
}
a.ScheduledAt = nil
a.TaskID = ""
return nil
}

View File

@@ -23,7 +23,7 @@ type Wallet struct {
// 钱包状态 - 钱包的基本状态信息
IsActive bool `gorm:"default:true" json:"is_active" comment:"钱包是否激活"`
Balance decimal.Decimal `gorm:"type:decimal(20,8);default:0" json:"balance" comment:"钱包余额(精确到8位小数)"`
Version int64 `gorm:"version" json:"version" comment:"乐观锁版本号"`
Version int64 `gorm:"default:0" json:"version" comment:"乐观锁版本号"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`

View File

@@ -15,7 +15,7 @@ type WalletTransaction struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"交易记录唯一标识"`
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"扣款用户ID"`
ApiCallID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"api_call_id" comment:"关联API调用ID"`
TransactionID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id" comment:"交易ID"`
TransactionID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id" comment:"交易ID"`
ProductID string `gorm:"type:varchar(64);not null;index" json:"product_id" comment:"产品ID"`
// 扣款信息

View File

@@ -2,6 +2,7 @@ package repositories
import (
"context"
"time"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/shared/interfaces"
@@ -20,4 +21,16 @@ type RechargeRecordRepository interface {
// 管理员查询方法
List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
// 统计相关方法
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
// 系统级别统计方法
GetSystemTotalAmount(ctx context.Context) (float64, error)
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
}

View File

@@ -4,6 +4,8 @@ import (
"context"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/shared/interfaces"
"github.com/shopspring/decimal"
)
// FinanceStats 财务统计信息
@@ -25,7 +27,9 @@ type WalletRepository interface {
GetByUserID(ctx context.Context, userID string) (*entities.Wallet, error)
// 乐观锁更新(自动重试)
UpdateBalanceWithVersion(ctx context.Context, walletID string, newBalance string, oldVersion int64) (bool, error)
UpdateBalanceWithVersion(ctx context.Context, walletID string, amount decimal.Decimal, operation string) (bool, error)
// 乐观锁更新通过用户ID直接更新避免重复查询
UpdateBalanceByUserID(ctx context.Context, userID string, amount decimal.Decimal, operation string) (bool, error)
// 状态操作
ActivateWallet(ctx context.Context, walletID string) error

View File

@@ -2,6 +2,7 @@ package repositories
import (
"context"
"time"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/shared/interfaces"
)
@@ -26,6 +27,22 @@ type WalletTransactionRepository interface {
// 新增:统计用户钱包交易次数
CountByUserId(ctx context.Context, userId string) (int64, error)
// 统计相关方法
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
// 管理端:根据条件筛选所有钱包交易记录(包含产品名称)
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error)
// 管理端:导出钱包交易记录(包含产品名称和企业信息)
ExportWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}) ([]*entities.WalletTransaction, error)
// 系统级别统计方法
GetSystemTotalAmount(ctx context.Context) (float64, error)
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
}

View File

@@ -0,0 +1,186 @@
package services
import (
"context"
"fmt"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/api/entities"
api_repositories "tyapi-server/internal/domains/api/repositories"
user_repositories "tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/infrastructure/external/sms"
)
// BalanceAlertService 余额预警服务接口
type BalanceAlertService interface {
CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error
}
// BalanceAlertServiceImpl 余额预警服务实现
type BalanceAlertServiceImpl struct {
apiUserRepo api_repositories.ApiUserRepository
userRepo user_repositories.UserRepository
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository
smsService *sms.AliSMSService
config *config.Config
logger *zap.Logger
}
// NewBalanceAlertService 创建余额预警服务
func NewBalanceAlertService(
apiUserRepo api_repositories.ApiUserRepository,
userRepo user_repositories.UserRepository,
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
smsService *sms.AliSMSService,
config *config.Config,
logger *zap.Logger,
) BalanceAlertService {
return &BalanceAlertServiceImpl{
apiUserRepo: apiUserRepo,
userRepo: userRepo,
enterpriseInfoRepo: enterpriseInfoRepo,
smsService: smsService,
config: config,
logger: logger,
}
}
// CheckAndSendAlert 检查余额并发送预警
func (s *BalanceAlertServiceImpl) CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error {
// 1. 获取API用户信息
apiUser, err := s.apiUserRepo.FindByUserId(ctx, userID)
if err != nil {
s.logger.Error("获取API用户信息失败",
zap.String("user_id", userID),
zap.Error(err))
return fmt.Errorf("获取API用户信息失败: %w", err)
}
if apiUser == nil {
s.logger.Debug("API用户不存在跳过余额预警检查", zap.String("user_id", userID))
return nil
}
// 2. 兼容性处理如果API用户没有配置预警信息从用户表获取并更新
needUpdate := false
if apiUser.AlertPhone == "" {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
s.logger.Error("获取用户信息失败",
zap.String("user_id", userID),
zap.Error(err))
return fmt.Errorf("获取用户信息失败: %w", err)
}
if user.Phone != "" {
apiUser.AlertPhone = user.Phone
needUpdate = true
}
}
// 3. 兼容性处理如果API用户没有配置预警阈值使用默认值
if apiUser.BalanceAlertThreshold == 0 {
apiUser.BalanceAlertThreshold = s.config.Wallet.BalanceAlert.DefaultThreshold
needUpdate = true
}
// 4. 如果需要更新API用户信息保存到数据库
if needUpdate {
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
s.logger.Error("更新API用户预警配置失败",
zap.String("user_id", userID),
zap.Error(err))
// 不返回错误,继续执行预警检查
}
}
balanceFloat, _ := balance.Float64()
// 5. 检查是否需要发送欠费预警(不受冷却期限制)
if apiUser.ShouldSendArrearsAlert(balanceFloat) {
if err := s.sendArrearsAlert(ctx, apiUser, balanceFloat); err != nil {
s.logger.Error("发送欠费预警失败",
zap.String("user_id", userID),
zap.Error(err))
return err
}
// 欠费预警不受冷却期限制不需要更新LastArrearsAlert时间
return nil
}
// 6. 检查是否需要发送低余额预警
if apiUser.ShouldSendLowBalanceAlert(balanceFloat) {
if err := s.sendLowBalanceAlert(ctx, apiUser, balanceFloat); err != nil {
s.logger.Error("发送低余额预警失败",
zap.String("user_id", userID),
zap.Error(err))
return err
}
// 标记预警已发送
apiUser.MarkLowBalanceAlertSent()
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
s.logger.Error("更新API用户预警时间失败",
zap.String("user_id", userID),
zap.Error(err))
}
}
return nil
}
// sendArrearsAlert 发送欠费预警
func (s *BalanceAlertServiceImpl) sendArrearsAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
// 直接从企业信息表获取企业名称
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
if err != nil {
s.logger.Error("获取企业信息失败",
zap.String("user_id", apiUser.UserId),
zap.Error(err))
// 如果获取企业信息失败,使用默认名称
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", "天远数据用户")
}
// 获取企业名称,如果没有则使用默认名称
enterpriseName := "天远数据用户"
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
enterpriseName = enterpriseInfo.CompanyName
}
s.logger.Info("发送欠费预警短信",
zap.String("user_id", apiUser.UserId),
zap.String("phone", apiUser.AlertPhone),
zap.Float64("balance", balance),
zap.String("enterprise_name", enterpriseName))
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", enterpriseName)
}
// sendLowBalanceAlert 发送低余额预警
func (s *BalanceAlertServiceImpl) sendLowBalanceAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
// 直接从企业信息表获取企业名称
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
if err != nil {
s.logger.Error("获取企业信息失败",
zap.String("user_id", apiUser.UserId),
zap.Error(err))
// 如果获取企业信息失败,使用默认名称
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", "天远数据用户")
}
// 获取企业名称,如果没有则使用默认名称
enterpriseName := "天远数据用户"
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
enterpriseName = enterpriseInfo.CompanyName
}
s.logger.Info("发送低余额预警短信",
zap.String("user_id", apiUser.UserId),
zap.String("phone", apiUser.AlertPhone),
zap.Float64("balance", balance),
zap.Float64("threshold", apiUser.BalanceAlertThreshold),
zap.String("enterprise_name", enterpriseName))
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", enterpriseName)
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/shopspring/decimal"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/finance/entities"
@@ -25,21 +26,27 @@ type WalletAggregateService interface {
// WalletAggregateServiceImpl 钱包聚合服务实现
type WalletAggregateServiceImpl struct {
db *gorm.DB
walletRepo repositories.WalletRepository
transactionRepo repositories.WalletTransactionRepository
balanceAlertSvc BalanceAlertService
logger *zap.Logger
cfg *config.Config
}
func NewWalletAggregateService(
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))
}
}

View File

@@ -0,0 +1,434 @@
package entities
import (
"errors"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// StatisticsDashboard 仪表板配置实体
// 用于存储仪表板的配置信息
type StatisticsDashboard struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"仪表板唯一标识"`
Name string `gorm:"type:varchar(100);not null" json:"name" comment:"仪表板名称"`
Description string `gorm:"type:text" json:"description" comment:"仪表板描述"`
UserRole string `gorm:"type:varchar(20);not null;index" json:"user_role" comment:"用户角色"`
IsDefault bool `gorm:"default:false" json:"is_default" comment:"是否为默认仪表板"`
IsActive bool `gorm:"default:true" json:"is_active" comment:"是否激活"`
// 仪表板配置
Layout string `gorm:"type:json" json:"layout" comment:"布局配置"`
Widgets string `gorm:"type:json" json:"widgets" comment:"组件配置"`
Settings string `gorm:"type:json" json:"settings" comment:"设置配置"`
RefreshInterval int `gorm:"default:300" json:"refresh_interval" comment:"刷新间隔(秒)"`
// 权限和访问控制
CreatedBy string `gorm:"type:varchar(36);not null" json:"created_by" comment:"创建者ID"`
AccessLevel string `gorm:"type:varchar(20);default:'private'" json:"access_level" comment:"访问级别"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
// 领域事件 (不持久化)
domainEvents []interface{} `gorm:"-" json:"-"`
}
// TableName 指定数据库表名
func (StatisticsDashboard) TableName() string {
return "statistics_dashboards"
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (s *StatisticsDashboard) BeforeCreate(tx *gorm.DB) error {
if s.ID == "" {
s.ID = uuid.New().String()
}
return nil
}
// 实现 Entity 接口 - 提供统一的实体管理接口
// GetID 获取实体唯一标识
func (s *StatisticsDashboard) GetID() string {
return s.ID
}
// GetCreatedAt 获取创建时间
func (s *StatisticsDashboard) GetCreatedAt() time.Time {
return s.CreatedAt
}
// GetUpdatedAt 获取更新时间
func (s *StatisticsDashboard) GetUpdatedAt() time.Time {
return s.UpdatedAt
}
// Validate 验证仪表板配置信息
// 检查仪表板必填字段是否完整,确保数据的有效性
func (s *StatisticsDashboard) Validate() error {
if s.Name == "" {
return NewValidationError("仪表板名称不能为空")
}
if s.UserRole == "" {
return NewValidationError("用户角色不能为空")
}
if s.CreatedBy == "" {
return NewValidationError("创建者ID不能为空")
}
// 验证用户角色
if !s.IsValidUserRole() {
return NewValidationError("无效的用户角色")
}
// 验证访问级别
if !s.IsValidAccessLevel() {
return NewValidationError("无效的访问级别")
}
// 验证刷新间隔
if s.RefreshInterval < 30 {
return NewValidationError("刷新间隔不能少于30秒")
}
return nil
}
// IsValidUserRole 检查用户角色是否有效
func (s *StatisticsDashboard) IsValidUserRole() bool {
validRoles := []string{
"admin", // 管理员
"user", // 普通用户
"manager", // 经理
"analyst", // 分析师
}
for _, validRole := range validRoles {
if s.UserRole == validRole {
return true
}
}
return false
}
// IsValidAccessLevel 检查访问级别是否有效
func (s *StatisticsDashboard) IsValidAccessLevel() bool {
validLevels := []string{
"private", // 私有
"public", // 公开
"shared", // 共享
}
for _, validLevel := range validLevels {
if s.AccessLevel == validLevel {
return true
}
}
return false
}
// GetUserRoleName 获取用户角色的中文名称
func (s *StatisticsDashboard) GetUserRoleName() string {
roleNames := map[string]string{
"admin": "管理员",
"user": "普通用户",
"manager": "经理",
"analyst": "分析师",
}
if name, exists := roleNames[s.UserRole]; exists {
return name
}
return s.UserRole
}
// GetAccessLevelName 获取访问级别的中文名称
func (s *StatisticsDashboard) GetAccessLevelName() string {
levelNames := map[string]string{
"private": "私有",
"public": "公开",
"shared": "共享",
}
if name, exists := levelNames[s.AccessLevel]; exists {
return name
}
return s.AccessLevel
}
// NewStatisticsDashboard 工厂方法 - 创建仪表板配置
func NewStatisticsDashboard(name, description, userRole, createdBy string) (*StatisticsDashboard, error) {
if name == "" {
return nil, errors.New("仪表板名称不能为空")
}
if userRole == "" {
return nil, errors.New("用户角色不能为空")
}
if createdBy == "" {
return nil, errors.New("创建者ID不能为空")
}
dashboard := &StatisticsDashboard{
Name: name,
Description: description,
UserRole: userRole,
CreatedBy: createdBy,
IsDefault: false,
IsActive: true,
AccessLevel: "private",
RefreshInterval: 300, // 默认5分钟
domainEvents: make([]interface{}, 0),
}
// 验证仪表板
if err := dashboard.Validate(); err != nil {
return nil, err
}
// 添加领域事件
dashboard.addDomainEvent(&StatisticsDashboardCreatedEvent{
DashboardID: dashboard.ID,
Name: name,
UserRole: userRole,
CreatedBy: createdBy,
CreatedAt: time.Now(),
})
return dashboard, nil
}
// SetAsDefault 设置为默认仪表板
func (s *StatisticsDashboard) SetAsDefault() error {
if !s.IsActive {
return NewValidationError("只有激活状态的仪表板才能设置为默认")
}
s.IsDefault = true
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardSetAsDefaultEvent{
DashboardID: s.ID,
SetAt: time.Now(),
})
return nil
}
// RemoveAsDefault 取消默认状态
func (s *StatisticsDashboard) RemoveAsDefault() error {
if !s.IsDefault {
return NewValidationError("当前仪表板不是默认仪表板")
}
s.IsDefault = false
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardRemovedAsDefaultEvent{
DashboardID: s.ID,
RemovedAt: time.Now(),
})
return nil
}
// Activate 激活仪表板
func (s *StatisticsDashboard) Activate() error {
if s.IsActive {
return NewValidationError("仪表板已经是激活状态")
}
s.IsActive = true
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardActivatedEvent{
DashboardID: s.ID,
ActivatedAt: time.Now(),
})
return nil
}
// Deactivate 停用仪表板
func (s *StatisticsDashboard) Deactivate() error {
if !s.IsActive {
return NewValidationError("仪表板已经是停用状态")
}
s.IsActive = false
// 如果是默认仪表板,需要先取消默认状态
if s.IsDefault {
s.IsDefault = false
}
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardDeactivatedEvent{
DashboardID: s.ID,
DeactivatedAt: time.Now(),
})
return nil
}
// UpdateLayout 更新布局配置
func (s *StatisticsDashboard) UpdateLayout(layout string) error {
if layout == "" {
return NewValidationError("布局配置不能为空")
}
s.Layout = layout
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardLayoutUpdatedEvent{
DashboardID: s.ID,
UpdatedAt: time.Now(),
})
return nil
}
// UpdateWidgets 更新组件配置
func (s *StatisticsDashboard) UpdateWidgets(widgets string) error {
if widgets == "" {
return NewValidationError("组件配置不能为空")
}
s.Widgets = widgets
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardWidgetsUpdatedEvent{
DashboardID: s.ID,
UpdatedAt: time.Now(),
})
return nil
}
// UpdateSettings 更新设置配置
func (s *StatisticsDashboard) UpdateSettings(settings string) error {
s.Settings = settings
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardSettingsUpdatedEvent{
DashboardID: s.ID,
UpdatedAt: time.Now(),
})
return nil
}
// UpdateRefreshInterval 更新刷新间隔
func (s *StatisticsDashboard) UpdateRefreshInterval(interval int) error {
if interval < 30 {
return NewValidationError("刷新间隔不能少于30秒")
}
oldInterval := s.RefreshInterval
s.RefreshInterval = interval
// 添加领域事件
s.addDomainEvent(&StatisticsDashboardRefreshIntervalUpdatedEvent{
DashboardID: s.ID,
OldInterval: oldInterval,
NewInterval: interval,
UpdatedAt: time.Now(),
})
return nil
}
// CanBeModified 检查仪表板是否可以被修改
func (s *StatisticsDashboard) CanBeModified() bool {
return s.IsActive
}
// CanBeDeleted 检查仪表板是否可以被删除
func (s *StatisticsDashboard) CanBeDeleted() bool {
return !s.IsDefault && s.IsActive
}
// ================ 领域事件管理 ================
// addDomainEvent 添加领域事件
func (s *StatisticsDashboard) addDomainEvent(event interface{}) {
if s.domainEvents == nil {
s.domainEvents = make([]interface{}, 0)
}
s.domainEvents = append(s.domainEvents, event)
}
// GetDomainEvents 获取领域事件
func (s *StatisticsDashboard) GetDomainEvents() []interface{} {
return s.domainEvents
}
// ClearDomainEvents 清除领域事件
func (s *StatisticsDashboard) ClearDomainEvents() {
s.domainEvents = make([]interface{}, 0)
}
// ================ 领域事件定义 ================
// StatisticsDashboardCreatedEvent 仪表板创建事件
type StatisticsDashboardCreatedEvent struct {
DashboardID string `json:"dashboard_id"`
Name string `json:"name"`
UserRole string `json:"user_role"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
// StatisticsDashboardSetAsDefaultEvent 仪表板设置为默认事件
type StatisticsDashboardSetAsDefaultEvent struct {
DashboardID string `json:"dashboard_id"`
SetAt time.Time `json:"set_at"`
}
// StatisticsDashboardRemovedAsDefaultEvent 仪表板取消默认事件
type StatisticsDashboardRemovedAsDefaultEvent struct {
DashboardID string `json:"dashboard_id"`
RemovedAt time.Time `json:"removed_at"`
}
// StatisticsDashboardActivatedEvent 仪表板激活事件
type StatisticsDashboardActivatedEvent struct {
DashboardID string `json:"dashboard_id"`
ActivatedAt time.Time `json:"activated_at"`
}
// StatisticsDashboardDeactivatedEvent 仪表板停用事件
type StatisticsDashboardDeactivatedEvent struct {
DashboardID string `json:"dashboard_id"`
DeactivatedAt time.Time `json:"deactivated_at"`
}
// StatisticsDashboardLayoutUpdatedEvent 仪表板布局更新事件
type StatisticsDashboardLayoutUpdatedEvent struct {
DashboardID string `json:"dashboard_id"`
UpdatedAt time.Time `json:"updated_at"`
}
// StatisticsDashboardWidgetsUpdatedEvent 仪表板组件更新事件
type StatisticsDashboardWidgetsUpdatedEvent struct {
DashboardID string `json:"dashboard_id"`
UpdatedAt time.Time `json:"updated_at"`
}
// StatisticsDashboardSettingsUpdatedEvent 仪表板设置更新事件
type StatisticsDashboardSettingsUpdatedEvent struct {
DashboardID string `json:"dashboard_id"`
UpdatedAt time.Time `json:"updated_at"`
}
// StatisticsDashboardRefreshIntervalUpdatedEvent 仪表板刷新间隔更新事件
type StatisticsDashboardRefreshIntervalUpdatedEvent struct {
DashboardID string `json:"dashboard_id"`
OldInterval int `json:"old_interval"`
NewInterval int `json:"new_interval"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@@ -0,0 +1,244 @@
package entities
import (
"errors"
"fmt"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// StatisticsMetric 统计指标实体
// 用于存储各种统计指标数据,支持多维度统计
type StatisticsMetric struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"统计指标唯一标识"`
MetricType string `gorm:"type:varchar(50);not null;index" json:"metric_type" comment:"指标类型"`
MetricName string `gorm:"type:varchar(100);not null" json:"metric_name" comment:"指标名称"`
Dimension string `gorm:"type:varchar(50)" json:"dimension" comment:"统计维度"`
Value float64 `gorm:"type:decimal(20,4);not null" json:"value" comment:"指标值"`
Metadata string `gorm:"type:json" json:"metadata" comment:"额外维度信息"`
Date time.Time `gorm:"type:date;index" json:"date" comment:"统计日期"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
// 领域事件 (不持久化)
domainEvents []interface{} `gorm:"-" json:"-"`
}
// TableName 指定数据库表名
func (StatisticsMetric) TableName() string {
return "statistics_metrics"
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (s *StatisticsMetric) BeforeCreate(tx *gorm.DB) error {
if s.ID == "" {
s.ID = uuid.New().String()
}
return nil
}
// 实现 Entity 接口 - 提供统一的实体管理接口
// GetID 获取实体唯一标识
func (s *StatisticsMetric) GetID() string {
return s.ID
}
// GetCreatedAt 获取创建时间
func (s *StatisticsMetric) GetCreatedAt() time.Time {
return s.CreatedAt
}
// GetUpdatedAt 获取更新时间
func (s *StatisticsMetric) GetUpdatedAt() time.Time {
return s.UpdatedAt
}
// Validate 验证统计指标信息
// 检查统计指标必填字段是否完整,确保数据的有效性
func (s *StatisticsMetric) Validate() error {
if s.MetricType == "" {
return NewValidationError("指标类型不能为空")
}
if s.MetricName == "" {
return NewValidationError("指标名称不能为空")
}
if s.Value < 0 {
return NewValidationError("指标值不能为负数")
}
if s.Date.IsZero() {
return NewValidationError("统计日期不能为空")
}
// 验证指标类型
if !s.IsValidMetricType() {
return NewValidationError("无效的指标类型")
}
return nil
}
// IsValidMetricType 检查指标类型是否有效
func (s *StatisticsMetric) IsValidMetricType() bool {
validTypes := []string{
"api_calls", // API调用统计
"users", // 用户统计
"finance", // 财务统计
"products", // 产品统计
"certification", // 认证统计
}
for _, validType := range validTypes {
if s.MetricType == validType {
return true
}
}
return false
}
// GetMetricTypeName 获取指标类型的中文名称
func (s *StatisticsMetric) GetMetricTypeName() string {
typeNames := map[string]string{
"api_calls": "API调用统计",
"users": "用户统计",
"finance": "财务统计",
"products": "产品统计",
"certification": "认证统计",
}
if name, exists := typeNames[s.MetricType]; exists {
return name
}
return s.MetricType
}
// GetFormattedValue 获取格式化的指标值
func (s *StatisticsMetric) GetFormattedValue() string {
// 根据指标类型格式化数值
switch s.MetricType {
case "api_calls", "users":
return fmt.Sprintf("%.0f", s.Value)
case "finance":
return fmt.Sprintf("%.2f", s.Value)
default:
return fmt.Sprintf("%.4f", s.Value)
}
}
// NewStatisticsMetric 工厂方法 - 创建统计指标
func NewStatisticsMetric(metricType, metricName, dimension string, value float64, date time.Time) (*StatisticsMetric, error) {
if metricType == "" {
return nil, errors.New("指标类型不能为空")
}
if metricName == "" {
return nil, errors.New("指标名称不能为空")
}
if value < 0 {
return nil, errors.New("指标值不能为负数")
}
if date.IsZero() {
return nil, errors.New("统计日期不能为空")
}
metric := &StatisticsMetric{
MetricType: metricType,
MetricName: metricName,
Dimension: dimension,
Value: value,
Date: date,
domainEvents: make([]interface{}, 0),
}
// 验证指标
if err := metric.Validate(); err != nil {
return nil, err
}
// 添加领域事件
metric.addDomainEvent(&StatisticsMetricCreatedEvent{
MetricID: metric.ID,
MetricType: metricType,
MetricName: metricName,
Value: value,
CreatedAt: time.Now(),
})
return metric, nil
}
// UpdateValue 更新指标值
func (s *StatisticsMetric) UpdateValue(newValue float64) error {
if newValue < 0 {
return NewValidationError("指标值不能为负数")
}
oldValue := s.Value
s.Value = newValue
// 添加领域事件
s.addDomainEvent(&StatisticsMetricUpdatedEvent{
MetricID: s.ID,
OldValue: oldValue,
NewValue: newValue,
UpdatedAt: time.Now(),
})
return nil
}
// ================ 领域事件管理 ================
// addDomainEvent 添加领域事件
func (s *StatisticsMetric) addDomainEvent(event interface{}) {
if s.domainEvents == nil {
s.domainEvents = make([]interface{}, 0)
}
s.domainEvents = append(s.domainEvents, event)
}
// GetDomainEvents 获取领域事件
func (s *StatisticsMetric) GetDomainEvents() []interface{} {
return s.domainEvents
}
// ClearDomainEvents 清除领域事件
func (s *StatisticsMetric) ClearDomainEvents() {
s.domainEvents = make([]interface{}, 0)
}
// ================ 领域事件定义 ================
// StatisticsMetricCreatedEvent 统计指标创建事件
type StatisticsMetricCreatedEvent struct {
MetricID string `json:"metric_id"`
MetricType string `json:"metric_type"`
MetricName string `json:"metric_name"`
Value float64 `json:"value"`
CreatedAt time.Time `json:"created_at"`
}
// StatisticsMetricUpdatedEvent 统计指标更新事件
type StatisticsMetricUpdatedEvent struct {
MetricID string `json:"metric_id"`
OldValue float64 `json:"old_value"`
NewValue float64 `json:"new_value"`
UpdatedAt time.Time `json:"updated_at"`
}
// ValidationError 验证错误
type ValidationError struct {
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
func NewValidationError(message string) *ValidationError {
return &ValidationError{Message: message}
}

View File

@@ -0,0 +1,343 @@
package entities
import (
"errors"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// StatisticsReport 统计报告实体
// 用于存储生成的统计报告数据
type StatisticsReport struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"报告唯一标识"`
ReportType string `gorm:"type:varchar(50);not null;index" json:"report_type" comment:"报告类型"`
Title string `gorm:"type:varchar(200);not null" json:"title" comment:"报告标题"`
Content string `gorm:"type:json" json:"content" comment:"报告内容"`
Period string `gorm:"type:varchar(20)" json:"period" comment:"统计周期"`
UserRole string `gorm:"type:varchar(20)" json:"user_role" comment:"用户角色"`
Status string `gorm:"type:varchar(20);default:'draft'" json:"status" comment:"报告状态"`
// 报告元数据
GeneratedBy string `gorm:"type:varchar(36)" json:"generated_by" comment:"生成者ID"`
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
// 领域事件 (不持久化)
domainEvents []interface{} `gorm:"-" json:"-"`
}
// TableName 指定数据库表名
func (StatisticsReport) TableName() string {
return "statistics_reports"
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (s *StatisticsReport) BeforeCreate(tx *gorm.DB) error {
if s.ID == "" {
s.ID = uuid.New().String()
}
return nil
}
// 实现 Entity 接口 - 提供统一的实体管理接口
// GetID 获取实体唯一标识
func (s *StatisticsReport) GetID() string {
return s.ID
}
// GetCreatedAt 获取创建时间
func (s *StatisticsReport) GetCreatedAt() time.Time {
return s.CreatedAt
}
// GetUpdatedAt 获取更新时间
func (s *StatisticsReport) GetUpdatedAt() time.Time {
return s.UpdatedAt
}
// Validate 验证统计报告信息
// 检查统计报告必填字段是否完整,确保数据的有效性
func (s *StatisticsReport) Validate() error {
if s.ReportType == "" {
return NewValidationError("报告类型不能为空")
}
if s.Title == "" {
return NewValidationError("报告标题不能为空")
}
if s.Period == "" {
return NewValidationError("统计周期不能为空")
}
// 验证报告类型
if !s.IsValidReportType() {
return NewValidationError("无效的报告类型")
}
// 验证报告状态
if !s.IsValidStatus() {
return NewValidationError("无效的报告状态")
}
return nil
}
// IsValidReportType 检查报告类型是否有效
func (s *StatisticsReport) IsValidReportType() bool {
validTypes := []string{
"dashboard", // 仪表板报告
"summary", // 汇总报告
"detailed", // 详细报告
"custom", // 自定义报告
}
for _, validType := range validTypes {
if s.ReportType == validType {
return true
}
}
return false
}
// IsValidStatus 检查报告状态是否有效
func (s *StatisticsReport) IsValidStatus() bool {
validStatuses := []string{
"draft", // 草稿
"generating", // 生成中
"completed", // 已完成
"failed", // 生成失败
"expired", // 已过期
}
for _, validStatus := range validStatuses {
if s.Status == validStatus {
return true
}
}
return false
}
// GetReportTypeName 获取报告类型的中文名称
func (s *StatisticsReport) GetReportTypeName() string {
typeNames := map[string]string{
"dashboard": "仪表板报告",
"summary": "汇总报告",
"detailed": "详细报告",
"custom": "自定义报告",
}
if name, exists := typeNames[s.ReportType]; exists {
return name
}
return s.ReportType
}
// GetStatusName 获取报告状态的中文名称
func (s *StatisticsReport) GetStatusName() string {
statusNames := map[string]string{
"draft": "草稿",
"generating": "生成中",
"completed": "已完成",
"failed": "生成失败",
"expired": "已过期",
}
if name, exists := statusNames[s.Status]; exists {
return name
}
return s.Status
}
// NewStatisticsReport 工厂方法 - 创建统计报告
func NewStatisticsReport(reportType, title, period, userRole string) (*StatisticsReport, error) {
if reportType == "" {
return nil, errors.New("报告类型不能为空")
}
if title == "" {
return nil, errors.New("报告标题不能为空")
}
if period == "" {
return nil, errors.New("统计周期不能为空")
}
report := &StatisticsReport{
ReportType: reportType,
Title: title,
Period: period,
UserRole: userRole,
Status: "draft",
domainEvents: make([]interface{}, 0),
}
// 验证报告
if err := report.Validate(); err != nil {
return nil, err
}
// 添加领域事件
report.addDomainEvent(&StatisticsReportCreatedEvent{
ReportID: report.ID,
ReportType: reportType,
Title: title,
Period: period,
CreatedAt: time.Now(),
})
return report, nil
}
// StartGeneration 开始生成报告
func (s *StatisticsReport) StartGeneration(generatedBy string) error {
if s.Status != "draft" {
return NewValidationError("只有草稿状态的报告才能开始生成")
}
s.Status = "generating"
s.GeneratedBy = generatedBy
now := time.Now()
s.GeneratedAt = &now
// 添加领域事件
s.addDomainEvent(&StatisticsReportGenerationStartedEvent{
ReportID: s.ID,
GeneratedBy: generatedBy,
StartedAt: now,
})
return nil
}
// CompleteGeneration 完成报告生成
func (s *StatisticsReport) CompleteGeneration(content string) error {
if s.Status != "generating" {
return NewValidationError("只有生成中状态的报告才能完成生成")
}
s.Status = "completed"
s.Content = content
// 设置过期时间默认7天
expiresAt := time.Now().Add(7 * 24 * time.Hour)
s.ExpiresAt = &expiresAt
// 添加领域事件
s.addDomainEvent(&StatisticsReportCompletedEvent{
ReportID: s.ID,
CompletedAt: time.Now(),
})
return nil
}
// FailGeneration 报告生成失败
func (s *StatisticsReport) FailGeneration(reason string) error {
if s.Status != "generating" {
return NewValidationError("只有生成中状态的报告才能标记为失败")
}
s.Status = "failed"
// 添加领域事件
s.addDomainEvent(&StatisticsReportFailedEvent{
ReportID: s.ID,
Reason: reason,
FailedAt: time.Now(),
})
return nil
}
// IsExpired 检查报告是否已过期
func (s *StatisticsReport) IsExpired() bool {
if s.ExpiresAt == nil {
return false
}
return time.Now().After(*s.ExpiresAt)
}
// MarkAsExpired 标记报告为过期
func (s *StatisticsReport) MarkAsExpired() error {
if s.Status != "completed" {
return NewValidationError("只有已完成状态的报告才能标记为过期")
}
s.Status = "expired"
// 添加领域事件
s.addDomainEvent(&StatisticsReportExpiredEvent{
ReportID: s.ID,
ExpiredAt: time.Now(),
})
return nil
}
// CanBeRegenerated 检查报告是否可以重新生成
func (s *StatisticsReport) CanBeRegenerated() bool {
return s.Status == "failed" || s.Status == "expired"
}
// ================ 领域事件管理 ================
// addDomainEvent 添加领域事件
func (s *StatisticsReport) addDomainEvent(event interface{}) {
if s.domainEvents == nil {
s.domainEvents = make([]interface{}, 0)
}
s.domainEvents = append(s.domainEvents, event)
}
// GetDomainEvents 获取领域事件
func (s *StatisticsReport) GetDomainEvents() []interface{} {
return s.domainEvents
}
// ClearDomainEvents 清除领域事件
func (s *StatisticsReport) ClearDomainEvents() {
s.domainEvents = make([]interface{}, 0)
}
// ================ 领域事件定义 ================
// StatisticsReportCreatedEvent 统计报告创建事件
type StatisticsReportCreatedEvent struct {
ReportID string `json:"report_id"`
ReportType string `json:"report_type"`
Title string `json:"title"`
Period string `json:"period"`
CreatedAt time.Time `json:"created_at"`
}
// StatisticsReportGenerationStartedEvent 统计报告生成开始事件
type StatisticsReportGenerationStartedEvent struct {
ReportID string `json:"report_id"`
GeneratedBy string `json:"generated_by"`
StartedAt time.Time `json:"started_at"`
}
// StatisticsReportCompletedEvent 统计报告完成事件
type StatisticsReportCompletedEvent struct {
ReportID string `json:"report_id"`
CompletedAt time.Time `json:"completed_at"`
}
// StatisticsReportFailedEvent 统计报告失败事件
type StatisticsReportFailedEvent struct {
ReportID string `json:"report_id"`
Reason string `json:"reason"`
FailedAt time.Time `json:"failed_at"`
}
// StatisticsReportExpiredEvent 统计报告过期事件
type StatisticsReportExpiredEvent struct {
ReportID string `json:"report_id"`
ExpiredAt time.Time `json:"expired_at"`
}

View File

@@ -0,0 +1,572 @@
package events
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
// StatisticsEventType 统计事件类型
type StatisticsEventType string
const (
// 指标相关事件
MetricCreatedEventType StatisticsEventType = "statistics.metric.created"
MetricUpdatedEventType StatisticsEventType = "statistics.metric.updated"
MetricAggregatedEventType StatisticsEventType = "statistics.metric.aggregated"
// 报告相关事件
ReportCreatedEventType StatisticsEventType = "statistics.report.created"
ReportGenerationStartedEventType StatisticsEventType = "statistics.report.generation_started"
ReportCompletedEventType StatisticsEventType = "statistics.report.completed"
ReportFailedEventType StatisticsEventType = "statistics.report.failed"
ReportExpiredEventType StatisticsEventType = "statistics.report.expired"
// 仪表板相关事件
DashboardCreatedEventType StatisticsEventType = "statistics.dashboard.created"
DashboardUpdatedEventType StatisticsEventType = "statistics.dashboard.updated"
DashboardActivatedEventType StatisticsEventType = "statistics.dashboard.activated"
DashboardDeactivatedEventType StatisticsEventType = "statistics.dashboard.deactivated"
)
// BaseStatisticsEvent 统计事件基础结构
type BaseStatisticsEvent struct {
ID string `json:"id"`
Type string `json:"type"`
Version string `json:"version"`
Timestamp time.Time `json:"timestamp"`
Source string `json:"source"`
AggregateID string `json:"aggregate_id"`
AggregateType string `json:"aggregate_type"`
Metadata map[string]interface{} `json:"metadata"`
Payload interface{} `json:"payload"`
// DDD特有字段
DomainVersion string `json:"domain_version"`
CausationID string `json:"causation_id"`
CorrelationID string `json:"correlation_id"`
}
// 实现 Event 接口
func (e *BaseStatisticsEvent) GetID() string {
return e.ID
}
func (e *BaseStatisticsEvent) GetType() string {
return e.Type
}
func (e *BaseStatisticsEvent) GetVersion() string {
return e.Version
}
func (e *BaseStatisticsEvent) GetTimestamp() time.Time {
return e.Timestamp
}
func (e *BaseStatisticsEvent) GetPayload() interface{} {
return e.Payload
}
func (e *BaseStatisticsEvent) GetMetadata() map[string]interface{} {
return e.Metadata
}
func (e *BaseStatisticsEvent) GetSource() string {
return e.Source
}
func (e *BaseStatisticsEvent) GetAggregateID() string {
return e.AggregateID
}
func (e *BaseStatisticsEvent) GetAggregateType() string {
return e.AggregateType
}
func (e *BaseStatisticsEvent) GetDomainVersion() string {
return e.DomainVersion
}
func (e *BaseStatisticsEvent) GetCausationID() string {
return e.CausationID
}
func (e *BaseStatisticsEvent) GetCorrelationID() string {
return e.CorrelationID
}
func (e *BaseStatisticsEvent) Marshal() ([]byte, error) {
return json.Marshal(e)
}
func (e *BaseStatisticsEvent) Unmarshal(data []byte) error {
return json.Unmarshal(data, e)
}
// ================ 指标相关事件 ================
// MetricCreatedEvent 指标创建事件
type MetricCreatedEvent struct {
*BaseStatisticsEvent
MetricID string `json:"metric_id"`
MetricType string `json:"metric_type"`
MetricName string `json:"metric_name"`
Value float64 `json:"value"`
Dimension string `json:"dimension"`
Date time.Time `json:"date"`
}
func NewMetricCreatedEvent(metricID, metricType, metricName, dimension string, value float64, date time.Time, correlationID string) *MetricCreatedEvent {
return &MetricCreatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(MetricCreatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: metricID,
AggregateType: "StatisticsMetric",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"metric_id": metricID,
"metric_type": metricType,
"metric_name": metricName,
"dimension": dimension,
},
},
MetricID: metricID,
MetricType: metricType,
MetricName: metricName,
Value: value,
Dimension: dimension,
Date: date,
}
}
func (e *MetricCreatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"metric_id": e.MetricID,
"metric_type": e.MetricType,
"metric_name": e.MetricName,
"value": e.Value,
"dimension": e.Dimension,
"date": e.Date,
}
}
// MetricUpdatedEvent 指标更新事件
type MetricUpdatedEvent struct {
*BaseStatisticsEvent
MetricID string `json:"metric_id"`
OldValue float64 `json:"old_value"`
NewValue float64 `json:"new_value"`
UpdatedAt time.Time `json:"updated_at"`
}
func NewMetricUpdatedEvent(metricID string, oldValue, newValue float64, correlationID string) *MetricUpdatedEvent {
return &MetricUpdatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(MetricUpdatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: metricID,
AggregateType: "StatisticsMetric",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"metric_id": metricID,
},
},
MetricID: metricID,
OldValue: oldValue,
NewValue: newValue,
UpdatedAt: time.Now(),
}
}
func (e *MetricUpdatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"metric_id": e.MetricID,
"old_value": e.OldValue,
"new_value": e.NewValue,
"updated_at": e.UpdatedAt,
}
}
// MetricAggregatedEvent 指标聚合事件
type MetricAggregatedEvent struct {
*BaseStatisticsEvent
MetricType string `json:"metric_type"`
Dimension string `json:"dimension"`
AggregatedAt time.Time `json:"aggregated_at"`
RecordCount int `json:"record_count"`
TotalValue float64 `json:"total_value"`
}
func NewMetricAggregatedEvent(metricType, dimension string, recordCount int, totalValue float64, correlationID string) *MetricAggregatedEvent {
return &MetricAggregatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(MetricAggregatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: uuid.New().String(),
AggregateType: "StatisticsMetric",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"metric_type": metricType,
"dimension": dimension,
},
},
MetricType: metricType,
Dimension: dimension,
AggregatedAt: time.Now(),
RecordCount: recordCount,
TotalValue: totalValue,
}
}
func (e *MetricAggregatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"metric_type": e.MetricType,
"dimension": e.Dimension,
"aggregated_at": e.AggregatedAt,
"record_count": e.RecordCount,
"total_value": e.TotalValue,
}
}
// ================ 报告相关事件 ================
// ReportCreatedEvent 报告创建事件
type ReportCreatedEvent struct {
*BaseStatisticsEvent
ReportID string `json:"report_id"`
ReportType string `json:"report_type"`
Title string `json:"title"`
Period string `json:"period"`
UserRole string `json:"user_role"`
}
func NewReportCreatedEvent(reportID, reportType, title, period, userRole, correlationID string) *ReportCreatedEvent {
return &ReportCreatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(ReportCreatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: reportID,
AggregateType: "StatisticsReport",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"report_id": reportID,
"report_type": reportType,
"user_role": userRole,
},
},
ReportID: reportID,
ReportType: reportType,
Title: title,
Period: period,
UserRole: userRole,
}
}
func (e *ReportCreatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"report_id": e.ReportID,
"report_type": e.ReportType,
"title": e.Title,
"period": e.Period,
"user_role": e.UserRole,
}
}
// ReportGenerationStartedEvent 报告生成开始事件
type ReportGenerationStartedEvent struct {
*BaseStatisticsEvent
ReportID string `json:"report_id"`
GeneratedBy string `json:"generated_by"`
StartedAt time.Time `json:"started_at"`
}
func NewReportGenerationStartedEvent(reportID, generatedBy, correlationID string) *ReportGenerationStartedEvent {
return &ReportGenerationStartedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(ReportGenerationStartedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: reportID,
AggregateType: "StatisticsReport",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"report_id": reportID,
"generated_by": generatedBy,
},
},
ReportID: reportID,
GeneratedBy: generatedBy,
StartedAt: time.Now(),
}
}
func (e *ReportGenerationStartedEvent) GetPayload() interface{} {
return map[string]interface{}{
"report_id": e.ReportID,
"generated_by": e.GeneratedBy,
"started_at": e.StartedAt,
}
}
// ReportCompletedEvent 报告完成事件
type ReportCompletedEvent struct {
*BaseStatisticsEvent
ReportID string `json:"report_id"`
CompletedAt time.Time `json:"completed_at"`
ContentSize int `json:"content_size"`
}
func NewReportCompletedEvent(reportID string, contentSize int, correlationID string) *ReportCompletedEvent {
return &ReportCompletedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(ReportCompletedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: reportID,
AggregateType: "StatisticsReport",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"report_id": reportID,
},
},
ReportID: reportID,
CompletedAt: time.Now(),
ContentSize: contentSize,
}
}
func (e *ReportCompletedEvent) GetPayload() interface{} {
return map[string]interface{}{
"report_id": e.ReportID,
"completed_at": e.CompletedAt,
"content_size": e.ContentSize,
}
}
// ReportFailedEvent 报告失败事件
type ReportFailedEvent struct {
*BaseStatisticsEvent
ReportID string `json:"report_id"`
Reason string `json:"reason"`
FailedAt time.Time `json:"failed_at"`
}
func NewReportFailedEvent(reportID, reason, correlationID string) *ReportFailedEvent {
return &ReportFailedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(ReportFailedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: reportID,
AggregateType: "StatisticsReport",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"report_id": reportID,
},
},
ReportID: reportID,
Reason: reason,
FailedAt: time.Now(),
}
}
func (e *ReportFailedEvent) GetPayload() interface{} {
return map[string]interface{}{
"report_id": e.ReportID,
"reason": e.Reason,
"failed_at": e.FailedAt,
}
}
// ================ 仪表板相关事件 ================
// DashboardCreatedEvent 仪表板创建事件
type DashboardCreatedEvent struct {
*BaseStatisticsEvent
DashboardID string `json:"dashboard_id"`
Name string `json:"name"`
UserRole string `json:"user_role"`
CreatedBy string `json:"created_by"`
}
func NewDashboardCreatedEvent(dashboardID, name, userRole, createdBy, correlationID string) *DashboardCreatedEvent {
return &DashboardCreatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(DashboardCreatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: dashboardID,
AggregateType: "StatisticsDashboard",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"dashboard_id": dashboardID,
"user_role": userRole,
"created_by": createdBy,
},
},
DashboardID: dashboardID,
Name: name,
UserRole: userRole,
CreatedBy: createdBy,
}
}
func (e *DashboardCreatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"dashboard_id": e.DashboardID,
"name": e.Name,
"user_role": e.UserRole,
"created_by": e.CreatedBy,
}
}
// DashboardUpdatedEvent 仪表板更新事件
type DashboardUpdatedEvent struct {
*BaseStatisticsEvent
DashboardID string `json:"dashboard_id"`
UpdatedBy string `json:"updated_by"`
UpdatedAt time.Time `json:"updated_at"`
Changes map[string]interface{} `json:"changes"`
}
func NewDashboardUpdatedEvent(dashboardID, updatedBy string, changes map[string]interface{}, correlationID string) *DashboardUpdatedEvent {
return &DashboardUpdatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(DashboardUpdatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: dashboardID,
AggregateType: "StatisticsDashboard",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"dashboard_id": dashboardID,
"updated_by": updatedBy,
},
},
DashboardID: dashboardID,
UpdatedBy: updatedBy,
UpdatedAt: time.Now(),
Changes: changes,
}
}
func (e *DashboardUpdatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"dashboard_id": e.DashboardID,
"updated_by": e.UpdatedBy,
"updated_at": e.UpdatedAt,
"changes": e.Changes,
}
}
// DashboardActivatedEvent 仪表板激活事件
type DashboardActivatedEvent struct {
*BaseStatisticsEvent
DashboardID string `json:"dashboard_id"`
ActivatedBy string `json:"activated_by"`
ActivatedAt time.Time `json:"activated_at"`
}
func NewDashboardActivatedEvent(dashboardID, activatedBy, correlationID string) *DashboardActivatedEvent {
return &DashboardActivatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(DashboardActivatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: dashboardID,
AggregateType: "StatisticsDashboard",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"dashboard_id": dashboardID,
"activated_by": activatedBy,
},
},
DashboardID: dashboardID,
ActivatedBy: activatedBy,
ActivatedAt: time.Now(),
}
}
func (e *DashboardActivatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"dashboard_id": e.DashboardID,
"activated_by": e.ActivatedBy,
"activated_at": e.ActivatedAt,
}
}
// DashboardDeactivatedEvent 仪表板停用事件
type DashboardDeactivatedEvent struct {
*BaseStatisticsEvent
DashboardID string `json:"dashboard_id"`
DeactivatedBy string `json:"deactivated_by"`
DeactivatedAt time.Time `json:"deactivated_at"`
}
func NewDashboardDeactivatedEvent(dashboardID, deactivatedBy, correlationID string) *DashboardDeactivatedEvent {
return &DashboardDeactivatedEvent{
BaseStatisticsEvent: &BaseStatisticsEvent{
ID: uuid.New().String(),
Type: string(DashboardDeactivatedEventType),
Version: "1.0",
Timestamp: time.Now(),
Source: "statistics-service",
AggregateID: dashboardID,
AggregateType: "StatisticsDashboard",
DomainVersion: "1.0",
CorrelationID: correlationID,
Metadata: map[string]interface{}{
"dashboard_id": dashboardID,
"deactivated_by": deactivatedBy,
},
},
DashboardID: dashboardID,
DeactivatedBy: deactivatedBy,
DeactivatedAt: time.Now(),
}
}
func (e *DashboardDeactivatedEvent) GetPayload() interface{} {
return map[string]interface{}{
"dashboard_id": e.DashboardID,
"deactivated_by": e.DeactivatedBy,
"deactivated_at": e.DeactivatedAt,
}
}

View File

@@ -0,0 +1,301 @@
package queries
import (
"fmt"
"time"
)
// StatisticsQuery 统计查询对象
type StatisticsQuery struct {
// 基础查询条件
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
MetricName string `json:"metric_name" form:"metric_name"` // 指标名称
Dimension string `json:"dimension" form:"dimension"` // 统计维度
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
// 分页参数
Limit int `json:"limit" form:"limit"` // 限制数量
Offset int `json:"offset" form:"offset"` // 偏移量
// 排序参数
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
// 过滤条件
MinValue float64 `json:"min_value" form:"min_value"` // 最小值
MaxValue float64 `json:"max_value" form:"max_value"` // 最大值
// 聚合参数
AggregateBy string `json:"aggregate_by" form:"aggregate_by"` // 聚合维度 (hour/day/week/month)
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
}
// StatisticsReportQuery 统计报告查询对象
type StatisticsReportQuery struct {
// 基础查询条件
ReportType string `json:"report_type" form:"report_type"` // 报告类型
UserRole string `json:"user_role" form:"user_role"` // 用户角色
Status string `json:"status" form:"status"` // 报告状态
Period string `json:"period" form:"period"` // 统计周期
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
// 分页参数
Limit int `json:"limit" form:"limit"` // 限制数量
Offset int `json:"offset" form:"offset"` // 偏移量
// 排序参数
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
// 过滤条件
GeneratedBy string `json:"generated_by" form:"generated_by"` // 生成者ID
AccessLevel string `json:"access_level" form:"access_level"` // 访问级别
}
// StatisticsDashboardQuery 统计仪表板查询对象
type StatisticsDashboardQuery struct {
// 基础查询条件
UserRole string `json:"user_role" form:"user_role"` // 用户角色
IsDefault *bool `json:"is_default" form:"is_default"` // 是否默认
IsActive *bool `json:"is_active" form:"is_active"` // 是否激活
AccessLevel string `json:"access_level" form:"access_level"` // 访问级别
// 分页参数
Limit int `json:"limit" form:"limit"` // 限制数量
Offset int `json:"offset" form:"offset"` // 偏移量
// 排序参数
SortBy string `json:"sort_by" form:"sort_by"` // 排序字段
SortOrder string `json:"sort_order" form:"sort_order"` // 排序顺序 (asc/desc)
// 过滤条件
CreatedBy string `json:"created_by" form:"created_by"` // 创建者ID
Name string `json:"name" form:"name"` // 仪表板名称
}
// RealtimeStatisticsQuery 实时统计查询对象
type RealtimeStatisticsQuery struct {
// 查询条件
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
TimeRange string `json:"time_range" form:"time_range"` // 时间范围 (last_hour/last_day/last_week)
// 过滤条件
Dimension string `json:"dimension" form:"dimension"` // 统计维度
}
// HistoricalStatisticsQuery 历史统计查询对象
type HistoricalStatisticsQuery struct {
// 查询条件
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
Period string `json:"period" form:"period"` // 统计周期
// 分页参数
Limit int `json:"limit" form:"limit"` // 限制数量
Offset int `json:"offset" form:"offset"` // 偏移量
// 聚合参数
AggregateBy string `json:"aggregate_by" form:"aggregate_by"` // 聚合维度
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
// 过滤条件
Dimension string `json:"dimension" form:"dimension"` // 统计维度
MinValue float64 `json:"min_value" form:"min_value"` // 最小值
MaxValue float64 `json:"max_value" form:"max_value"` // 最大值
}
// DashboardDataQuery 仪表板数据查询对象
type DashboardDataQuery struct {
// 查询条件
UserRole string `json:"user_role" form:"user_role"` // 用户角色
Period string `json:"period" form:"period"` // 统计周期
// 时间范围
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
// 过滤条件
MetricTypes []string `json:"metric_types" form:"metric_types"` // 指标类型列表
Dimensions []string `json:"dimensions" form:"dimensions"` // 统计维度列表
}
// ReportGenerationQuery 报告生成查询对象
type ReportGenerationQuery struct {
// 报告配置
ReportType string `json:"report_type" form:"report_type"` // 报告类型
Title string `json:"title" form:"title"` // 报告标题
Period string `json:"period" form:"period"` // 统计周期
UserRole string `json:"user_role" form:"user_role"` // 用户角色
// 时间范围
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
// 过滤条件
Filters map[string]interface{} `json:"filters" form:"filters"` // 过滤条件
// 生成配置
GeneratedBy string `json:"generated_by" form:"generated_by"` // 生成者ID
Format string `json:"format" form:"format"` // 输出格式 (json/pdf/excel)
}
// ExportQuery 导出查询对象
type ExportQuery struct {
// 导出配置
Format string `json:"format" form:"format"` // 导出格式 (excel/csv/pdf)
MetricType string `json:"metric_type" form:"metric_type"` // 指标类型
// 时间范围
StartDate time.Time `json:"start_date" form:"start_date"` // 开始日期
EndDate time.Time `json:"end_date" form:"end_date"` // 结束日期
// 过滤条件
Dimension string `json:"dimension" form:"dimension"` // 统计维度
GroupBy string `json:"group_by" form:"group_by"` // 分组维度
// 导出配置
IncludeCharts bool `json:"include_charts" form:"include_charts"` // 是否包含图表
Columns []string `json:"columns" form:"columns"` // 导出列
}
// Validate 验证统计查询对象
func (q *StatisticsQuery) Validate() error {
if q.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if q.StartDate.IsZero() || q.EndDate.IsZero() {
return fmt.Errorf("开始日期和结束日期不能为空")
}
if q.StartDate.After(q.EndDate) {
return fmt.Errorf("开始日期不能晚于结束日期")
}
if q.Limit <= 0 {
q.Limit = 20 // 默认限制
}
if q.Limit > 1000 {
q.Limit = 1000 // 最大限制
}
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
q.SortOrder = "desc" // 默认降序
}
return nil
}
// Validate 验证统计报告查询对象
func (q *StatisticsReportQuery) Validate() error {
if q.Limit <= 0 {
q.Limit = 20 // 默认限制
}
if q.Limit > 1000 {
q.Limit = 1000 // 最大限制
}
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
q.SortOrder = "desc" // 默认降序
}
return nil
}
// Validate 验证统计仪表板查询对象
func (q *StatisticsDashboardQuery) Validate() error {
if q.Limit <= 0 {
q.Limit = 20 // 默认限制
}
if q.Limit > 1000 {
q.Limit = 1000 // 最大限制
}
if q.SortOrder != "" && q.SortOrder != "asc" && q.SortOrder != "desc" {
q.SortOrder = "desc" // 默认降序
}
return nil
}
// Validate 验证实时统计查询对象
func (q *RealtimeStatisticsQuery) Validate() error {
if q.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if q.TimeRange == "" {
q.TimeRange = "last_hour" // 默认最近1小时
}
validTimeRanges := []string{"last_hour", "last_day", "last_week"}
for _, validRange := range validTimeRanges {
if q.TimeRange == validRange {
return nil
}
}
return fmt.Errorf("无效的时间范围: %s", q.TimeRange)
}
// Validate 验证历史统计查询对象
func (q *HistoricalStatisticsQuery) Validate() error {
if q.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if q.StartDate.IsZero() || q.EndDate.IsZero() {
return fmt.Errorf("开始日期和结束日期不能为空")
}
if q.StartDate.After(q.EndDate) {
return fmt.Errorf("开始日期不能晚于结束日期")
}
if q.Limit <= 0 {
q.Limit = 20 // 默认限制
}
if q.Limit > 1000 {
q.Limit = 1000 // 最大限制
}
return nil
}
// Validate 验证仪表板数据查询对象
func (q *DashboardDataQuery) Validate() error {
if q.UserRole == "" {
return fmt.Errorf("用户角色不能为空")
}
if q.Period == "" {
q.Period = "today" // 默认今天
}
return nil
}
// Validate 验证报告生成查询对象
func (q *ReportGenerationQuery) Validate() error {
if q.ReportType == "" {
return fmt.Errorf("报告类型不能为空")
}
if q.Title == "" {
return fmt.Errorf("报告标题不能为空")
}
if q.Period == "" {
return fmt.Errorf("统计周期不能为空")
}
if q.UserRole == "" {
return fmt.Errorf("用户角色不能为空")
}
return nil
}
// Validate 验证导出查询对象
func (q *ExportQuery) Validate() error {
if q.Format == "" {
return fmt.Errorf("导出格式不能为空")
}
if q.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if q.StartDate.IsZero() || q.EndDate.IsZero() {
return fmt.Errorf("开始日期和结束日期不能为空")
}
if q.StartDate.After(q.EndDate) {
return fmt.Errorf("开始日期不能晚于结束日期")
}
validFormats := []string{"excel", "csv", "pdf"}
for _, validFormat := range validFormats {
if q.Format == validFormat {
return nil
}
}
return fmt.Errorf("无效的导出格式: %s", q.Format)
}

View File

@@ -0,0 +1,107 @@
package repositories
import (
"context"
"time"
"tyapi-server/internal/domains/statistics/entities"
)
// StatisticsRepository 统计指标仓储接口
type StatisticsRepository interface {
// 基础CRUD操作
Save(ctx context.Context, metric *entities.StatisticsMetric) error
FindByID(ctx context.Context, id string) (*entities.StatisticsMetric, error)
FindByType(ctx context.Context, metricType string, limit, offset int) ([]*entities.StatisticsMetric, error)
Update(ctx context.Context, metric *entities.StatisticsMetric) error
Delete(ctx context.Context, id string) error
// 按类型和日期范围查询
FindByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
FindByTypeDimensionAndDateRange(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
FindByTypeNameAndDateRange(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
// 聚合查询
GetAggregatedMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) (map[string]float64, error)
GetMetricsByDimension(ctx context.Context, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
// 统计查询
CountByType(ctx context.Context, metricType string) (int64, error)
CountByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) (int64, error)
// 批量操作
BatchSave(ctx context.Context, metrics []*entities.StatisticsMetric) error
BatchDelete(ctx context.Context, ids []string) error
// 清理操作
DeleteByDateRange(ctx context.Context, startDate, endDate time.Time) error
DeleteByTypeAndDateRange(ctx context.Context, metricType string, startDate, endDate time.Time) error
}
// StatisticsReportRepository 统计报告仓储接口
type StatisticsReportRepository interface {
// 基础CRUD操作
Save(ctx context.Context, report *entities.StatisticsReport) error
FindByID(ctx context.Context, id string) (*entities.StatisticsReport, error)
FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error)
FindByStatus(ctx context.Context, status string) ([]*entities.StatisticsReport, error)
Update(ctx context.Context, report *entities.StatisticsReport) error
Delete(ctx context.Context, id string) error
// 按类型查询
FindByType(ctx context.Context, reportType string, limit, offset int) ([]*entities.StatisticsReport, error)
FindByTypeAndPeriod(ctx context.Context, reportType, period string, limit, offset int) ([]*entities.StatisticsReport, error)
// 按日期范围查询
FindByDateRange(ctx context.Context, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error)
FindByUserAndDateRange(ctx context.Context, userID string, startDate, endDate time.Time, limit, offset int) ([]*entities.StatisticsReport, error)
// 统计查询
CountByUser(ctx context.Context, userID string) (int64, error)
CountByType(ctx context.Context, reportType string) (int64, error)
CountByStatus(ctx context.Context, status string) (int64, error)
// 批量操作
BatchSave(ctx context.Context, reports []*entities.StatisticsReport) error
BatchDelete(ctx context.Context, ids []string) error
// 清理操作
DeleteExpiredReports(ctx context.Context, expiredBefore time.Time) error
DeleteByStatus(ctx context.Context, status string) error
}
// StatisticsDashboardRepository 统计仪表板仓储接口
type StatisticsDashboardRepository interface {
// 基础CRUD操作
Save(ctx context.Context, dashboard *entities.StatisticsDashboard) error
FindByID(ctx context.Context, id string) (*entities.StatisticsDashboard, error)
FindByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsDashboard, error)
FindByUserRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
Update(ctx context.Context, dashboard *entities.StatisticsDashboard) error
Delete(ctx context.Context, id string) error
// 按角色查询
FindByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
FindDefaultByRole(ctx context.Context, userRole string) (*entities.StatisticsDashboard, error)
FindActiveByRole(ctx context.Context, userRole string, limit, offset int) ([]*entities.StatisticsDashboard, error)
// 按状态查询
FindByStatus(ctx context.Context, isActive bool, limit, offset int) ([]*entities.StatisticsDashboard, error)
FindByAccessLevel(ctx context.Context, accessLevel string, limit, offset int) ([]*entities.StatisticsDashboard, error)
// 统计查询
CountByUser(ctx context.Context, userID string) (int64, error)
CountByRole(ctx context.Context, userRole string) (int64, error)
CountByStatus(ctx context.Context, isActive bool) (int64, error)
// 批量操作
BatchSave(ctx context.Context, dashboards []*entities.StatisticsDashboard) error
BatchDelete(ctx context.Context, ids []string) error
// 特殊操作
SetDefaultDashboard(ctx context.Context, dashboardID string) error
RemoveDefaultDashboard(ctx context.Context, userRole string) error
ActivateDashboard(ctx context.Context, dashboardID string) error
DeactivateDashboard(ctx context.Context, dashboardID string) error
}

View File

@@ -0,0 +1,388 @@
package services
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"tyapi-server/internal/domains/statistics/entities"
"tyapi-server/internal/domains/statistics/repositories"
)
// StatisticsAggregateService 统计聚合服务接口
// 负责统计数据的聚合和计算
type StatisticsAggregateService interface {
// 实时统计
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error)
// 历史统计聚合
AggregateHourlyMetrics(ctx context.Context, date time.Time) error
AggregateDailyMetrics(ctx context.Context, date time.Time) error
AggregateWeeklyMetrics(ctx context.Context, date time.Time) error
AggregateMonthlyMetrics(ctx context.Context, date time.Time) error
// 统计查询
GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error)
// 统计计算
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
}
// StatisticsAggregateServiceImpl 统计聚合服务实现
type StatisticsAggregateServiceImpl struct {
metricRepo repositories.StatisticsRepository
logger *zap.Logger
}
// NewStatisticsAggregateService 创建统计聚合服务
func NewStatisticsAggregateService(
metricRepo repositories.StatisticsRepository,
logger *zap.Logger,
) StatisticsAggregateService {
return &StatisticsAggregateServiceImpl{
metricRepo: metricRepo,
logger: logger,
}
}
// UpdateRealtimeMetric 更新实时统计指标
func (s *StatisticsAggregateServiceImpl) UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error {
if metricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if metricName == "" {
return fmt.Errorf("指标名称不能为空")
}
// 创建或更新实时指标
metric, err := entities.NewStatisticsMetric(metricType, metricName, "realtime", value, time.Now())
if err != nil {
s.logger.Error("创建统计指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return fmt.Errorf("创建统计指标失败: %w", err)
}
// 保存到数据库
err = s.metricRepo.Save(ctx, metric)
if err != nil {
s.logger.Error("保存统计指标失败",
zap.String("metric_id", metric.ID),
zap.Error(err))
return fmt.Errorf("保存统计指标失败: %w", err)
}
s.logger.Info("实时统计指标更新成功",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("value", value))
return nil
}
// GetRealtimeMetrics 获取实时统计指标
func (s *StatisticsAggregateServiceImpl) GetRealtimeMetrics(ctx context.Context, metricType string) (map[string]float64, error) {
if metricType == "" {
return nil, fmt.Errorf("指标类型不能为空")
}
// 获取今天的实时指标
today := time.Now().Truncate(24 * time.Hour)
tomorrow := today.Add(24 * time.Hour)
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, today, tomorrow)
if err != nil {
s.logger.Error("查询实时统计指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return nil, fmt.Errorf("查询实时统计指标失败: %w", err)
}
// 转换为map格式
result := make(map[string]float64)
for _, metric := range metrics {
if metric.Dimension == "realtime" {
result[metric.MetricName] = metric.Value
}
}
return result, nil
}
// AggregateHourlyMetrics 聚合小时级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateHourlyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合小时级统计指标", zap.Time("date", date))
// 获取指定小时的所有实时指标
startTime := date.Truncate(time.Hour)
endTime := startTime.Add(time.Hour)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "hourly")
if err != nil {
s.logger.Error("聚合小时级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合小时级指标失败: %w", err)
}
}
s.logger.Info("小时级统计指标聚合完成", zap.Time("date", date))
return nil
}
// AggregateDailyMetrics 聚合日级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateDailyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合日级统计指标", zap.Time("date", date))
// 获取指定日期的所有小时级指标
startTime := date.Truncate(24 * time.Hour)
endTime := startTime.Add(24 * time.Hour)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "daily")
if err != nil {
s.logger.Error("聚合日级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合日级指标失败: %w", err)
}
}
s.logger.Info("日级统计指标聚合完成", zap.Time("date", date))
return nil
}
// AggregateWeeklyMetrics 聚合周级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateWeeklyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合周级统计指标", zap.Time("date", date))
// 获取指定周的所有日级指标
startTime := date.Truncate(24 * time.Hour)
endTime := startTime.Add(7 * 24 * time.Hour)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "weekly")
if err != nil {
s.logger.Error("聚合周级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合周级指标失败: %w", err)
}
}
s.logger.Info("周级统计指标聚合完成", zap.Time("date", date))
return nil
}
// AggregateMonthlyMetrics 聚合月级统计指标
func (s *StatisticsAggregateServiceImpl) AggregateMonthlyMetrics(ctx context.Context, date time.Time) error {
s.logger.Info("开始聚合月级统计指标", zap.Time("date", date))
// 获取指定月的所有日级指标
startTime := date.Truncate(24 * time.Hour)
endTime := startTime.AddDate(0, 1, 0)
// 聚合不同类型的指标
metricTypes := []string{"api_calls", "users", "finance", "products", "certification"}
for _, metricType := range metricTypes {
err := s.aggregateMetricsByType(ctx, metricType, startTime, endTime, "monthly")
if err != nil {
s.logger.Error("聚合月级指标失败",
zap.String("metric_type", metricType),
zap.Error(err))
return fmt.Errorf("聚合月级指标失败: %w", err)
}
}
s.logger.Info("月级统计指标聚合完成", zap.Time("date", date))
return nil
}
// GetMetricsByType 根据类型获取统计指标
func (s *StatisticsAggregateServiceImpl) GetMetricsByType(ctx context.Context, metricType string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
if metricType == "" {
return nil, fmt.Errorf("指标类型不能为空")
}
metrics, err := s.metricRepo.FindByTypeAndDateRange(ctx, metricType, startDate, endDate)
if err != nil {
s.logger.Error("查询统计指标失败",
zap.String("metric_type", metricType),
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Error(err))
return nil, fmt.Errorf("查询统计指标失败: %w", err)
}
return metrics, nil
}
// GetMetricsByDimension 根据维度获取统计指标
func (s *StatisticsAggregateServiceImpl) GetMetricsByDimension(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) ([]*entities.StatisticsMetric, error) {
if metricType == "" {
return nil, fmt.Errorf("指标类型不能为空")
}
if dimension == "" {
return nil, fmt.Errorf("统计维度不能为空")
}
metrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, dimension, startDate, endDate)
if err != nil {
s.logger.Error("查询统计指标失败",
zap.String("metric_type", metricType),
zap.String("dimension", dimension),
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Error(err))
return nil, fmt.Errorf("查询统计指标失败: %w", err)
}
return metrics, nil
}
// CalculateGrowthRate 计算增长率
func (s *StatisticsAggregateServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
// 获取当前周期的指标值
currentMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("查询当前周期指标失败: %w", err)
}
// 获取上一周期的指标值
previousMetrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("查询上一周期指标失败: %w", err)
}
// 计算总值
var currentValue, previousValue float64
for _, metric := range currentMetrics {
currentValue += metric.Value
}
for _, metric := range previousMetrics {
previousValue += metric.Value
}
// 计算增长率
if previousValue == 0 {
if currentValue > 0 {
return 100, nil // 从0增长到正数增长率为100%
}
return 0, nil // 都是0增长率为0%
}
growthRate := ((currentValue - previousValue) / previousValue) * 100
return growthRate, nil
}
// CalculateTrend 计算趋势
func (s *StatisticsAggregateServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
if metricType == "" || metricName == "" {
return "", fmt.Errorf("指标类型和名称不能为空")
}
// 获取时间范围内的指标
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
return "", fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < 2 {
return "insufficient_data", nil // 数据不足
}
// 按时间排序
sortMetricsByDate(metrics)
// 计算趋势
firstValue := metrics[0].Value
lastValue := metrics[len(metrics)-1].Value
if lastValue > firstValue {
return "increasing", nil // 上升趋势
} else if lastValue < firstValue {
return "decreasing", nil // 下降趋势
} else {
return "stable", nil // 稳定趋势
}
}
// aggregateMetricsByType 按类型聚合指标
func (s *StatisticsAggregateServiceImpl) aggregateMetricsByType(ctx context.Context, metricType string, startTime, endTime time.Time, dimension string) error {
// 获取源数据(实时或小时级数据)
sourceDimension := "realtime"
if dimension == "daily" {
sourceDimension = "hourly"
} else if dimension == "weekly" || dimension == "monthly" {
sourceDimension = "daily"
}
// 查询源数据
sourceMetrics, err := s.metricRepo.FindByTypeDimensionAndDateRange(ctx, metricType, sourceDimension, startTime, endTime)
if err != nil {
return fmt.Errorf("查询源数据失败: %w", err)
}
// 按指标名称分组聚合
metricGroups := make(map[string][]*entities.StatisticsMetric)
for _, metric := range sourceMetrics {
metricGroups[metric.MetricName] = append(metricGroups[metric.MetricName], metric)
}
// 聚合每个指标
for metricName, metrics := range metricGroups {
var totalValue float64
for _, metric := range metrics {
totalValue += metric.Value
}
// 创建聚合后的指标
aggregatedMetric, err := entities.NewStatisticsMetric(metricType, metricName, dimension, totalValue, startTime)
if err != nil {
return fmt.Errorf("创建聚合指标失败: %w", err)
}
// 保存聚合指标
err = s.metricRepo.Save(ctx, aggregatedMetric)
if err != nil {
return fmt.Errorf("保存聚合指标失败: %w", err)
}
}
return nil
}
// sortMetricsByDate 按日期排序指标
func sortMetricsByDate(metrics []*entities.StatisticsMetric) {
// 简单的冒泡排序
n := len(metrics)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if metrics[j].Date.After(metrics[j+1].Date) {
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
}
}
}
}

View File

@@ -0,0 +1,510 @@
package services
import (
"context"
"fmt"
"math"
"time"
"go.uber.org/zap"
"tyapi-server/internal/domains/statistics/entities"
"tyapi-server/internal/domains/statistics/repositories"
)
// StatisticsCalculationService 统计计算服务接口
// 负责各种统计计算和分析
type StatisticsCalculationService interface {
// 基础统计计算
CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error)
// 高级统计计算
CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error)
CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error)
CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error)
// 业务指标计算
CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error)
// 时间序列分析
CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error)
CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error)
}
// StatisticsCalculationServiceImpl 统计计算服务实现
type StatisticsCalculationServiceImpl struct {
metricRepo repositories.StatisticsRepository
logger *zap.Logger
}
// NewStatisticsCalculationService 创建统计计算服务
func NewStatisticsCalculationService(
metricRepo repositories.StatisticsRepository,
logger *zap.Logger,
) StatisticsCalculationService {
return &StatisticsCalculationServiceImpl{
metricRepo: metricRepo,
logger: logger,
}
}
// CalculateTotal 计算总值
func (s *StatisticsCalculationServiceImpl) CalculateTotal(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
var total float64
for _, metric := range metrics {
total += metric.Value
}
s.logger.Info("计算总值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("total", total))
return total, nil
}
// CalculateAverage 计算平均值
func (s *StatisticsCalculationServiceImpl) CalculateAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) == 0 {
return 0, nil
}
var total float64
for _, metric := range metrics {
total += metric.Value
}
average := total / float64(len(metrics))
s.logger.Info("计算平均值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("average", average))
return average, nil
}
// CalculateMax 计算最大值
func (s *StatisticsCalculationServiceImpl) CalculateMax(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) == 0 {
return 0, nil
}
max := metrics[0].Value
for _, metric := range metrics {
if metric.Value > max {
max = metric.Value
}
}
s.logger.Info("计算最大值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("max", max))
return max, nil
}
// CalculateMin 计算最小值
func (s *StatisticsCalculationServiceImpl) CalculateMin(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return 0, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) == 0 {
return 0, nil
}
min := metrics[0].Value
for _, metric := range metrics {
if metric.Value < min {
min = metric.Value
}
}
s.logger.Info("计算最小值完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("min", min))
return min, nil
}
// CalculateGrowthRate 计算增长率
func (s *StatisticsCalculationServiceImpl) CalculateGrowthRate(ctx context.Context, metricType, metricName string, currentPeriod, previousPeriod time.Time) (float64, error) {
if metricType == "" || metricName == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
// 获取当前周期的总值
currentTotal, err := s.CalculateTotal(ctx, metricType, metricName, currentPeriod, currentPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("计算当前周期总值失败: %w", err)
}
// 获取上一周期的总值
previousTotal, err := s.CalculateTotal(ctx, metricType, metricName, previousPeriod, previousPeriod.Add(24*time.Hour))
if err != nil {
return 0, fmt.Errorf("计算上一周期总值失败: %w", err)
}
// 计算增长率
if previousTotal == 0 {
if currentTotal > 0 {
return 100, nil // 从0增长到正数增长率为100%
}
return 0, nil // 都是0增长率为0%
}
growthRate := ((currentTotal - previousTotal) / previousTotal) * 100
s.logger.Info("计算增长率完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Float64("growth_rate", growthRate))
return growthRate, nil
}
// CalculateTrend 计算趋势
func (s *StatisticsCalculationServiceImpl) CalculateTrend(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (string, error) {
if metricType == "" || metricName == "" {
return "", fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return "", fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < 2 {
return "insufficient_data", nil // 数据不足
}
// 按时间排序
sortMetricsByDateCalc(metrics)
// 计算趋势
firstValue := metrics[0].Value
lastValue := metrics[len(metrics)-1].Value
var trend string
if lastValue > firstValue {
trend = "increasing" // 上升趋势
} else if lastValue < firstValue {
trend = "decreasing" // 下降趋势
} else {
trend = "stable" // 稳定趋势
}
s.logger.Info("计算趋势完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.String("trend", trend))
return trend, nil
}
// CalculateCorrelation 计算相关性
func (s *StatisticsCalculationServiceImpl) CalculateCorrelation(ctx context.Context, metricType1, metricName1, metricType2, metricName2 string, startDate, endDate time.Time) (float64, error) {
if metricType1 == "" || metricName1 == "" || metricType2 == "" || metricName2 == "" {
return 0, fmt.Errorf("指标类型和名称不能为空")
}
// 获取两个指标的数据
metrics1, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType1, metricName1, startDate, endDate)
if err != nil {
return 0, fmt.Errorf("查询指标1失败: %w", err)
}
metrics2, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType2, metricName2, startDate, endDate)
if err != nil {
return 0, fmt.Errorf("查询指标2失败: %w", err)
}
if len(metrics1) != len(metrics2) || len(metrics1) < 2 {
return 0, fmt.Errorf("数据点数量不足或不对称")
}
// 计算皮尔逊相关系数
correlation := s.calculatePearsonCorrelation(metrics1, metrics2)
s.logger.Info("计算相关性完成",
zap.String("metric1", metricType1+"."+metricName1),
zap.String("metric2", metricType2+"."+metricName2),
zap.Float64("correlation", correlation))
return correlation, nil
}
// CalculateSuccessRate 计算成功率
func (s *StatisticsCalculationServiceImpl) CalculateSuccessRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
// 获取成功调用次数
successTotal, err := s.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算成功调用次数失败: %w", err)
}
// 获取总调用次数
totalCalls, err := s.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算总调用次数失败: %w", err)
}
if totalCalls == 0 {
return 0, nil
}
successRate := (successTotal / totalCalls) * 100
s.logger.Info("计算成功率完成",
zap.Float64("success_rate", successRate))
return successRate, nil
}
// CalculateConversionRate 计算转化率
func (s *StatisticsCalculationServiceImpl) CalculateConversionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
// 获取认证用户数
certifiedUsers, err := s.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算认证用户数失败: %w", err)
}
// 获取总用户数
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算总用户数失败: %w", err)
}
if totalUsers == 0 {
return 0, nil
}
conversionRate := (certifiedUsers / totalUsers) * 100
s.logger.Info("计算转化率完成",
zap.Float64("conversion_rate", conversionRate))
return conversionRate, nil
}
// CalculateRetentionRate 计算留存率
func (s *StatisticsCalculationServiceImpl) CalculateRetentionRate(ctx context.Context, startDate, endDate time.Time) (float64, error) {
// 获取活跃用户数
activeUsers, err := s.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算活跃用户数失败: %w", err)
}
// 获取总用户数
totalUsers, err := s.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
if err != nil {
return 0, fmt.Errorf("计算总用户数失败: %w", err)
}
if totalUsers == 0 {
return 0, nil
}
retentionRate := (activeUsers / totalUsers) * 100
s.logger.Info("计算留存率完成",
zap.Float64("retention_rate", retentionRate))
return retentionRate, nil
}
// CalculateMovingAverage 计算移动平均
func (s *StatisticsCalculationServiceImpl) CalculateMovingAverage(ctx context.Context, metricType, metricName string, startDate, endDate time.Time, windowSize int) ([]float64, error) {
if metricType == "" || metricName == "" {
return nil, fmt.Errorf("指标类型和名称不能为空")
}
if windowSize <= 0 {
return nil, fmt.Errorf("窗口大小必须大于0")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return nil, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < windowSize {
return nil, fmt.Errorf("数据点数量不足")
}
// 按时间排序
sortMetricsByDateCalc(metrics)
// 计算移动平均
var movingAverages []float64
for i := windowSize - 1; i < len(metrics); i++ {
var sum float64
for j := i - windowSize + 1; j <= i; j++ {
sum += metrics[j].Value
}
average := sum / float64(windowSize)
movingAverages = append(movingAverages, average)
}
s.logger.Info("计算移动平均完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Int("window_size", windowSize),
zap.Int("result_count", len(movingAverages)))
return movingAverages, nil
}
// CalculateSeasonality 计算季节性
func (s *StatisticsCalculationServiceImpl) CalculateSeasonality(ctx context.Context, metricType, metricName string, startDate, endDate time.Time) (map[string]float64, error) {
if metricType == "" || metricName == "" {
return nil, fmt.Errorf("指标类型和名称不能为空")
}
metrics, err := s.metricRepo.FindByTypeNameAndDateRange(ctx, metricType, metricName, startDate, endDate)
if err != nil {
s.logger.Error("查询指标失败",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Error(err))
return nil, fmt.Errorf("查询指标失败: %w", err)
}
if len(metrics) < 7 {
return nil, fmt.Errorf("数据点数量不足至少需要7个数据点")
}
// 按星期几分组
weeklyAverages := make(map[string][]float64)
for _, metric := range metrics {
weekday := metric.Date.Weekday().String()
weeklyAverages[weekday] = append(weeklyAverages[weekday], metric.Value)
}
// 计算每个星期几的平均值
seasonality := make(map[string]float64)
for weekday, values := range weeklyAverages {
var sum float64
for _, value := range values {
sum += value
}
seasonality[weekday] = sum / float64(len(values))
}
s.logger.Info("计算季节性完成",
zap.String("metric_type", metricType),
zap.String("metric_name", metricName),
zap.Int("weekday_count", len(seasonality)))
return seasonality, nil
}
// calculatePearsonCorrelation 计算皮尔逊相关系数
func (s *StatisticsCalculationServiceImpl) calculatePearsonCorrelation(metrics1, metrics2 []*entities.StatisticsMetric) float64 {
n := len(metrics1)
if n < 2 {
return 0
}
// 计算均值
var sum1, sum2 float64
for i := 0; i < n; i++ {
sum1 += metrics1[i].Value
sum2 += metrics2[i].Value
}
mean1 := sum1 / float64(n)
mean2 := sum2 / float64(n)
// 计算协方差和方差
var numerator, denominator1, denominator2 float64
for i := 0; i < n; i++ {
diff1 := metrics1[i].Value - mean1
diff2 := metrics2[i].Value - mean2
numerator += diff1 * diff2
denominator1 += diff1 * diff1
denominator2 += diff2 * diff2
}
// 计算相关系数
if denominator1 == 0 || denominator2 == 0 {
return 0
}
correlation := numerator / math.Sqrt(denominator1*denominator2)
return correlation
}
// sortMetricsByDateCalc 按日期排序指标
func sortMetricsByDateCalc(metrics []*entities.StatisticsMetric) {
// 简单的冒泡排序
n := len(metrics)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if metrics[j].Date.After(metrics[j+1].Date) {
metrics[j], metrics[j+1] = metrics[j+1], metrics[j]
}
}
}
}

View File

@@ -0,0 +1,582 @@
package services
import (
"context"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
"tyapi-server/internal/domains/statistics/entities"
"tyapi-server/internal/domains/statistics/repositories"
)
// StatisticsReportService 报告生成服务接口
// 负责统计报告的生成和管理
type StatisticsReportService interface {
// 报告生成
GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error)
GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error)
GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error)
// 报告管理
GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error)
GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error)
DeleteReport(ctx context.Context, reportID string) error
// 报告状态管理
StartReportGeneration(ctx context.Context, reportID, generatedBy string) error
CompleteReportGeneration(ctx context.Context, reportID string, content string) error
FailReportGeneration(ctx context.Context, reportID string, reason string) error
// 报告清理
CleanupExpiredReports(ctx context.Context) error
}
// StatisticsReportServiceImpl 报告生成服务实现
type StatisticsReportServiceImpl struct {
reportRepo repositories.StatisticsReportRepository
metricRepo repositories.StatisticsRepository
calcService StatisticsCalculationService
logger *zap.Logger
}
// NewStatisticsReportService 创建报告生成服务
func NewStatisticsReportService(
reportRepo repositories.StatisticsReportRepository,
metricRepo repositories.StatisticsRepository,
calcService StatisticsCalculationService,
logger *zap.Logger,
) StatisticsReportService {
return &StatisticsReportServiceImpl{
reportRepo: reportRepo,
metricRepo: metricRepo,
calcService: calcService,
logger: logger,
}
}
// GenerateDashboardReport 生成仪表板报告
func (s *StatisticsReportServiceImpl) GenerateDashboardReport(ctx context.Context, userRole string, period string) (*entities.StatisticsReport, error) {
if userRole == "" {
return nil, fmt.Errorf("用户角色不能为空")
}
if period == "" {
return nil, fmt.Errorf("统计周期不能为空")
}
// 创建报告实体
title := fmt.Sprintf("%s仪表板报告 - %s", s.getRoleDisplayName(userRole), s.getPeriodDisplayName(period))
report, err := entities.NewStatisticsReport("dashboard", title, period, userRole)
if err != nil {
s.logger.Error("创建仪表板报告失败",
zap.String("user_role", userRole),
zap.String("period", period),
zap.Error(err))
return nil, fmt.Errorf("创建仪表板报告失败: %w", err)
}
// 保存报告
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存仪表板报告失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("保存仪表板报告失败: %w", err)
}
s.logger.Info("仪表板报告创建成功",
zap.String("report_id", report.ID),
zap.String("user_role", userRole),
zap.String("period", period))
return report, nil
}
// GenerateSummaryReport 生成汇总报告
func (s *StatisticsReportServiceImpl) GenerateSummaryReport(ctx context.Context, period string, startDate, endDate time.Time) (*entities.StatisticsReport, error) {
if period == "" {
return nil, fmt.Errorf("统计周期不能为空")
}
// 创建报告实体
title := fmt.Sprintf("汇总报告 - %s (%s 至 %s)",
s.getPeriodDisplayName(period),
startDate.Format("2006-01-02"),
endDate.Format("2006-01-02"))
report, err := entities.NewStatisticsReport("summary", title, period, "admin")
if err != nil {
s.logger.Error("创建汇总报告失败",
zap.String("period", period),
zap.Error(err))
return nil, fmt.Errorf("创建汇总报告失败: %w", err)
}
// 生成报告内容
content, err := s.generateSummaryContent(ctx, startDate, endDate)
if err != nil {
s.logger.Error("生成汇总报告内容失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("生成汇总报告内容失败: %w", err)
}
// 完成报告生成
err = report.CompleteGeneration(content)
if err != nil {
s.logger.Error("完成汇总报告生成失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("完成汇总报告生成失败: %w", err)
}
// 保存报告
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存汇总报告失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("保存汇总报告失败: %w", err)
}
s.logger.Info("汇总报告生成成功",
zap.String("report_id", report.ID),
zap.String("period", period))
return report, nil
}
// GenerateDetailedReport 生成详细报告
func (s *StatisticsReportServiceImpl) GenerateDetailedReport(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (*entities.StatisticsReport, error) {
if reportType == "" {
return nil, fmt.Errorf("报告类型不能为空")
}
// 创建报告实体
title := fmt.Sprintf("详细报告 - %s (%s 至 %s)",
reportType,
startDate.Format("2006-01-02"),
endDate.Format("2006-01-02"))
report, err := entities.NewStatisticsReport("detailed", title, "custom", "admin")
if err != nil {
s.logger.Error("创建详细报告失败",
zap.String("report_type", reportType),
zap.Error(err))
return nil, fmt.Errorf("创建详细报告失败: %w", err)
}
// 生成报告内容
content, err := s.generateDetailedContent(ctx, reportType, startDate, endDate, filters)
if err != nil {
s.logger.Error("生成详细报告内容失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("生成详细报告内容失败: %w", err)
}
// 完成报告生成
err = report.CompleteGeneration(content)
if err != nil {
s.logger.Error("完成详细报告生成失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("完成详细报告生成失败: %w", err)
}
// 保存报告
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存详细报告失败",
zap.String("report_id", report.ID),
zap.Error(err))
return nil, fmt.Errorf("保存详细报告失败: %w", err)
}
s.logger.Info("详细报告生成成功",
zap.String("report_id", report.ID),
zap.String("report_type", reportType))
return report, nil
}
// GetReport 获取报告
func (s *StatisticsReportServiceImpl) GetReport(ctx context.Context, reportID string) (*entities.StatisticsReport, error) {
if reportID == "" {
return nil, fmt.Errorf("报告ID不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
s.logger.Error("查询报告失败",
zap.String("report_id", reportID),
zap.Error(err))
return nil, fmt.Errorf("查询报告失败: %w", err)
}
return report, nil
}
// GetReportsByUser 获取用户的报告列表
func (s *StatisticsReportServiceImpl) GetReportsByUser(ctx context.Context, userID string, limit, offset int) ([]*entities.StatisticsReport, error) {
if userID == "" {
return nil, fmt.Errorf("用户ID不能为空")
}
reports, err := s.reportRepo.FindByUser(ctx, userID, limit, offset)
if err != nil {
s.logger.Error("查询用户报告失败",
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("查询用户报告失败: %w", err)
}
return reports, nil
}
// DeleteReport 删除报告
func (s *StatisticsReportServiceImpl) DeleteReport(ctx context.Context, reportID string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
err := s.reportRepo.Delete(ctx, reportID)
if err != nil {
s.logger.Error("删除报告失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("删除报告失败: %w", err)
}
s.logger.Info("报告删除成功", zap.String("report_id", reportID))
return nil
}
// StartReportGeneration 开始报告生成
func (s *StatisticsReportServiceImpl) StartReportGeneration(ctx context.Context, reportID, generatedBy string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
if generatedBy == "" {
return fmt.Errorf("生成者ID不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return fmt.Errorf("查询报告失败: %w", err)
}
err = report.StartGeneration(generatedBy)
if err != nil {
s.logger.Error("开始报告生成失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("开始报告生成失败: %w", err)
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存报告状态失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("保存报告状态失败: %w", err)
}
s.logger.Info("报告生成开始",
zap.String("report_id", reportID),
zap.String("generated_by", generatedBy))
return nil
}
// CompleteReportGeneration 完成报告生成
func (s *StatisticsReportServiceImpl) CompleteReportGeneration(ctx context.Context, reportID string, content string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
if content == "" {
return fmt.Errorf("报告内容不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return fmt.Errorf("查询报告失败: %w", err)
}
err = report.CompleteGeneration(content)
if err != nil {
s.logger.Error("完成报告生成失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("完成报告生成失败: %w", err)
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存报告内容失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("保存报告内容失败: %w", err)
}
s.logger.Info("报告生成完成", zap.String("report_id", reportID))
return nil
}
// FailReportGeneration 报告生成失败
func (s *StatisticsReportServiceImpl) FailReportGeneration(ctx context.Context, reportID string, reason string) error {
if reportID == "" {
return fmt.Errorf("报告ID不能为空")
}
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return fmt.Errorf("查询报告失败: %w", err)
}
err = report.FailGeneration(reason)
if err != nil {
s.logger.Error("标记报告生成失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("标记报告生成失败: %w", err)
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存报告状态失败",
zap.String("report_id", reportID),
zap.Error(err))
return fmt.Errorf("保存报告状态失败: %w", err)
}
s.logger.Info("报告生成失败",
zap.String("report_id", reportID),
zap.String("reason", reason))
return nil
}
// CleanupExpiredReports 清理过期报告
func (s *StatisticsReportServiceImpl) CleanupExpiredReports(ctx context.Context) error {
s.logger.Info("开始清理过期报告")
// 获取所有已完成的报告
reports, err := s.reportRepo.FindByStatus(ctx, "completed")
if err != nil {
s.logger.Error("查询已完成报告失败", zap.Error(err))
return fmt.Errorf("查询已完成报告失败: %w", err)
}
var deletedCount int
for _, report := range reports {
if report.IsExpired() {
err = report.MarkAsExpired()
if err != nil {
s.logger.Error("标记报告过期失败",
zap.String("report_id", report.ID),
zap.Error(err))
continue
}
err = s.reportRepo.Save(ctx, report)
if err != nil {
s.logger.Error("保存过期报告状态失败",
zap.String("report_id", report.ID),
zap.Error(err))
continue
}
deletedCount++
}
}
s.logger.Info("过期报告清理完成", zap.Int("deleted_count", deletedCount))
return nil
}
// generateSummaryContent 生成汇总报告内容
func (s *StatisticsReportServiceImpl) generateSummaryContent(ctx context.Context, startDate, endDate time.Time) (string, error) {
content := make(map[string]interface{})
// API调用统计
apiCallsTotal, err := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算API调用总数失败", zap.Error(err))
}
apiCallsSuccess, err := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算API调用成功数失败", zap.Error(err))
}
// 用户统计
usersTotal, err := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算用户总数失败", zap.Error(err))
}
usersCertified, err := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
if err != nil {
s.logger.Warn("计算认证用户数失败", zap.Error(err))
}
// 财务统计
financeTotal, err := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
if err != nil {
s.logger.Warn("计算财务总额失败", zap.Error(err))
}
content["api_calls"] = map[string]interface{}{
"total": apiCallsTotal,
"success": apiCallsSuccess,
"rate": s.calculateRate(apiCallsSuccess, apiCallsTotal),
}
content["users"] = map[string]interface{}{
"total": usersTotal,
"certified": usersCertified,
"rate": s.calculateRate(usersCertified, usersTotal),
}
content["finance"] = map[string]interface{}{
"total_amount": financeTotal,
}
content["period"] = map[string]interface{}{
"start_date": startDate.Format("2006-01-02"),
"end_date": endDate.Format("2006-01-02"),
}
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
// 转换为JSON字符串
jsonContent, err := json.Marshal(content)
if err != nil {
return "", fmt.Errorf("序列化报告内容失败: %w", err)
}
return string(jsonContent), nil
}
// generateDetailedContent 生成详细报告内容
func (s *StatisticsReportServiceImpl) generateDetailedContent(ctx context.Context, reportType string, startDate, endDate time.Time, filters map[string]interface{}) (string, error) {
content := make(map[string]interface{})
// 根据报告类型生成不同的内容
switch reportType {
case "api_calls":
content = s.generateApiCallsDetailedContent(ctx, startDate, endDate, filters)
case "users":
content = s.generateUsersDetailedContent(ctx, startDate, endDate, filters)
case "finance":
content = s.generateFinanceDetailedContent(ctx, startDate, endDate, filters)
default:
return "", fmt.Errorf("不支持的报告类型: %s", reportType)
}
content["report_type"] = reportType
content["period"] = map[string]interface{}{
"start_date": startDate.Format("2006-01-02"),
"end_date": endDate.Format("2006-01-02"),
}
content["generated_at"] = time.Now().Format("2006-01-02 15:04:05")
// 转换为JSON字符串
jsonContent, err := json.Marshal(content)
if err != nil {
return "", fmt.Errorf("序列化报告内容失败: %w", err)
}
return string(jsonContent), nil
}
// generateApiCallsDetailedContent 生成API调用详细内容
func (s *StatisticsReportServiceImpl) generateApiCallsDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
content := make(map[string]interface{})
// 获取API调用统计数据
totalCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "total_count", startDate, endDate)
successCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "success_count", startDate, endDate)
failedCalls, _ := s.calcService.CalculateTotal(ctx, "api_calls", "failed_count", startDate, endDate)
avgResponseTime, _ := s.calcService.CalculateAverage(ctx, "api_calls", "response_time", startDate, endDate)
content["total_calls"] = totalCalls
content["success_calls"] = successCalls
content["failed_calls"] = failedCalls
content["success_rate"] = s.calculateRate(successCalls, totalCalls)
content["avg_response_time"] = avgResponseTime
return content
}
// generateUsersDetailedContent 生成用户详细内容
func (s *StatisticsReportServiceImpl) generateUsersDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
content := make(map[string]interface{})
// 获取用户统计数据
totalUsers, _ := s.calcService.CalculateTotal(ctx, "users", "total_count", startDate, endDate)
certifiedUsers, _ := s.calcService.CalculateTotal(ctx, "users", "certified_count", startDate, endDate)
activeUsers, _ := s.calcService.CalculateTotal(ctx, "users", "active_count", startDate, endDate)
content["total_users"] = totalUsers
content["certified_users"] = certifiedUsers
content["active_users"] = activeUsers
content["certification_rate"] = s.calculateRate(certifiedUsers, totalUsers)
content["retention_rate"] = s.calculateRate(activeUsers, totalUsers)
return content
}
// generateFinanceDetailedContent 生成财务详细内容
func (s *StatisticsReportServiceImpl) generateFinanceDetailedContent(ctx context.Context, startDate, endDate time.Time, filters map[string]interface{}) map[string]interface{} {
content := make(map[string]interface{})
// 获取财务统计数据
totalAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "total_amount", startDate, endDate)
rechargeAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "recharge_amount", startDate, endDate)
deductAmount, _ := s.calcService.CalculateTotal(ctx, "finance", "deduct_amount", startDate, endDate)
content["total_amount"] = totalAmount
content["recharge_amount"] = rechargeAmount
content["deduct_amount"] = deductAmount
content["net_amount"] = rechargeAmount - deductAmount
return content
}
// calculateRate 计算比率
func (s *StatisticsReportServiceImpl) calculateRate(numerator, denominator float64) float64 {
if denominator == 0 {
return 0
}
return (numerator / denominator) * 100
}
// getRoleDisplayName 获取角色显示名称
func (s *StatisticsReportServiceImpl) getRoleDisplayName(role string) string {
roleNames := map[string]string{
"admin": "管理员",
"user": "用户",
"manager": "经理",
"analyst": "分析师",
}
if name, exists := roleNames[role]; exists {
return name
}
return role
}
// getPeriodDisplayName 获取周期显示名称
func (s *StatisticsReportServiceImpl) getPeriodDisplayName(period string) string {
periodNames := map[string]string{
"today": "今日",
"week": "本周",
"month": "本月",
"quarter": "本季度",
"year": "本年",
}
if name, exists := periodNames[period]; exists {
return name
}
return period
}

View File

@@ -2,6 +2,7 @@ package repositories
import (
"context"
"time"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/domains/user/repositories/queries"
"tyapi-server/internal/shared/interfaces"
@@ -27,6 +28,7 @@ type UserRepository interface {
// 关联查询
GetByIDWithEnterpriseInfo(ctx context.Context, id string) (entities.User, error)
BatchGetByIDsWithEnterpriseInfo(ctx context.Context, ids []string) ([]*entities.User, error)
// 企业信息查询
ExistsByUnifiedSocialCode(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error)
@@ -46,6 +48,17 @@ type UserRepository interface {
// 统计信息
GetStats(ctx context.Context) (*UserStats, error)
GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error)
// 系统级别统计方法
GetSystemUserStats(ctx context.Context) (*UserStats, error)
GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*UserStats, error)
GetSystemDailyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
// 排行榜查询方法
GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
GetUserCallRankingByConsumption(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
GetRechargeRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
}
// SMSCodeRepository 短信验证码仓储接口