初始化项目
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
)
|
||||
|
||||
var AdStatistics = new(adStatistics)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
)
|
||||
|
||||
var Advertiser = new(advertiser)
|
||||
|
||||
246
service/application_service.go
Normal file
246
service/application_service.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
var (
|
||||
Application = applicationService{}
|
||||
)
|
||||
|
||||
type applicationService struct{}
|
||||
|
||||
// CreateApplication 创建应用
|
||||
func (s *applicationService) CreateApplication(ctx context.Context, req *dto.CreateApplicationReq) (id int64, err error) {
|
||||
// 检查应用名称是否已存在
|
||||
existingApp, err := dao.Application.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingApp != nil {
|
||||
return 0, gerror.New("应用名称已存在")
|
||||
}
|
||||
|
||||
// 生成API密钥
|
||||
appKey, appSecret, err := s.generateAPIKeys()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
application := &entity.Application{
|
||||
TenantId: req.TenantID,
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
Platform: req.Platform,
|
||||
PackageName: req.PackageName,
|
||||
AppStoreURL: req.AppStoreURL,
|
||||
Categories: req.Categories,
|
||||
Tags: req.Tags,
|
||||
AdTypes: req.AdTypes,
|
||||
Status: "active",
|
||||
AppKey: appKey,
|
||||
AppSecret: appSecret,
|
||||
CallbackURL: req.CallbackURL,
|
||||
}
|
||||
|
||||
return dao.Application.Create(ctx, application)
|
||||
}
|
||||
|
||||
// UpdateApplication 更新应用
|
||||
func (s *applicationService) UpdateApplication(ctx context.Context, id int64, req *dto.UpdateApplicationReq) (affected int64, err error) {
|
||||
// 检查应用是否存在
|
||||
existingApp, err := dao.Application.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingApp == nil {
|
||||
return 0, gerror.New("应用不存在")
|
||||
}
|
||||
|
||||
// 如果更新名称,检查是否与其他应用冲突
|
||||
if req.Name != "" && req.Name != existingApp.Name {
|
||||
conflictApp, err := dao.Application.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if conflictApp != nil && conflictApp.Id != id {
|
||||
return 0, gerror.New("应用名称已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updateData := &entity.Application{}
|
||||
if req.Name != "" {
|
||||
updateData.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
updateData.Description = req.Description
|
||||
}
|
||||
if req.Platform != "" {
|
||||
updateData.Platform = req.Platform
|
||||
}
|
||||
if req.PackageName != "" {
|
||||
updateData.PackageName = req.PackageName
|
||||
}
|
||||
if req.AppStoreURL != "" {
|
||||
updateData.AppStoreURL = req.AppStoreURL
|
||||
}
|
||||
if req.CallbackURL != "" {
|
||||
updateData.CallbackURL = req.CallbackURL
|
||||
}
|
||||
if len(req.Categories) > 0 {
|
||||
updateData.Categories = req.Categories
|
||||
}
|
||||
if len(req.Tags) > 0 {
|
||||
updateData.Tags = req.Tags
|
||||
}
|
||||
if len(req.AdTypes) > 0 {
|
||||
updateData.AdTypes = req.AdTypes
|
||||
}
|
||||
|
||||
// 使用Update方法更新应用
|
||||
err = dao.Application.Update(ctx, updateData)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// GetApplicationsByTenant 获取租户下的应用列表
|
||||
func (s *applicationService) GetApplicationsByTenant(ctx context.Context, tenantID int64, platform, status string, page, size int) (list []*entity.Application, total int64, err error) {
|
||||
// 构建过滤条件
|
||||
filter := make(map[string]interface{})
|
||||
filter["tenant_id"] = tenantID
|
||||
if platform != "" {
|
||||
filter["platform"] = platform
|
||||
}
|
||||
if status != "" {
|
||||
filter["status"] = status
|
||||
}
|
||||
|
||||
// 调用DAO的GetByTenantID方法获取租户下的所有应用
|
||||
apps, err := dao.Application.GetByTenantID(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 应用额外的过滤条件
|
||||
var filteredApps []*entity.Application
|
||||
for _, app := range apps {
|
||||
if platform != "" && app.Platform != platform {
|
||||
continue
|
||||
}
|
||||
if status != "" && app.Status != status {
|
||||
continue
|
||||
}
|
||||
filteredApps = append(filteredApps, app)
|
||||
}
|
||||
|
||||
// 实现简单的分页
|
||||
startIndex := (page - 1) * size
|
||||
endIndex := startIndex + size
|
||||
if startIndex >= len(filteredApps) {
|
||||
return []*entity.Application{}, int64(len(filteredApps)), nil
|
||||
}
|
||||
if endIndex > len(filteredApps) {
|
||||
endIndex = len(filteredApps)
|
||||
}
|
||||
|
||||
return filteredApps[startIndex:endIndex], int64(len(filteredApps)), nil
|
||||
}
|
||||
|
||||
// GetApplicationByKey 根据API密钥获取应用
|
||||
func (s *applicationService) GetApplicationByKey(ctx context.Context, appKey string) (application *entity.Application, err error) {
|
||||
return dao.Application.GetByAPIKey(ctx, appKey)
|
||||
}
|
||||
|
||||
// GetApplicationByID 根据ID获取应用
|
||||
func (s *applicationService) GetApplicationByID(ctx context.Context, id int64) (application *entity.Application, err error) {
|
||||
return dao.Application.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// DeleteApplication 删除应用
|
||||
func (s *applicationService) DeleteApplication(ctx context.Context, id int64) (affected int64, err error) {
|
||||
err = dao.Application.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// ValidateApplication 验证应用权限
|
||||
func (s *applicationService) ValidateApplication(ctx context.Context, appKey, appSecret string) (application *entity.Application, err error) {
|
||||
app, err := dao.Application.GetByAPIKey(ctx, appKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if app == nil {
|
||||
return nil, gerror.New("应用不存在")
|
||||
}
|
||||
if app.Status != "active" {
|
||||
return nil, gerror.New("应用状态异常")
|
||||
}
|
||||
if app.AppSecret != appSecret {
|
||||
return nil, gerror.New("密钥验证失败")
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// generateAPIKeys 生成API密钥
|
||||
func (s *applicationService) generateAPIKeys() (appKey, appSecret string, err error) {
|
||||
// 生成32位随机字符串作为AppKey
|
||||
keyBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(keyBytes); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
appKey = hex.EncodeToString(keyBytes)
|
||||
|
||||
// 生成64位随机字符串作为AppSecret
|
||||
secretBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(secretBytes); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
appSecret = hex.EncodeToString(secretBytes)
|
||||
|
||||
return appKey, appSecret, nil
|
||||
}
|
||||
|
||||
// ResetAPIKeys 重置API密钥
|
||||
func (s *applicationService) ResetAPIKeys(ctx context.Context, id int64) (appKey, appSecret string, err error) {
|
||||
// 检查应用是否存在
|
||||
existingApp, err := dao.Application.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if existingApp == nil {
|
||||
return "", "", gerror.New("应用不存在")
|
||||
}
|
||||
|
||||
// 生成新的API密钥
|
||||
appKey, appSecret, err = s.generateAPIKeys()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 更新应用密钥
|
||||
updateData := &entity.Application{
|
||||
AppKey: appKey,
|
||||
AppSecret: appSecret,
|
||||
}
|
||||
err = dao.Application.Update(ctx, updateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return appKey, appSecret, nil
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidService/model/types"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
"cidservice/model/types"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -85,6 +85,15 @@ func (s *cidService) GenerateCID(ctx context.Context, req *dto.GenerateCIDReq) (
|
||||
return nil, gerror.Wrap(err, "获取租户信息失败")
|
||||
}
|
||||
|
||||
// 检查租户请求次数限制
|
||||
allowed, err := RateLimit.CheckTenantRequestLimit(ctx, tenant.Id, nil)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "检查租户请求限制失败")
|
||||
}
|
||||
if !allowed {
|
||||
return nil, gerror.New("租户请求次数已超过限制,请稍后再试")
|
||||
}
|
||||
|
||||
// 获取匹配策略
|
||||
strategy, err := s.getMatchingStrategy(ctx, tenant.Level)
|
||||
if err != nil {
|
||||
|
||||
137
service/rate_limit_service.go
Normal file
137
service/rate_limit_service.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"cidservice/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var (
|
||||
RateLimit = rateLimitService{}
|
||||
)
|
||||
|
||||
type rateLimitService struct{}
|
||||
|
||||
// TenantRateLimitConfig 租户限流配置
|
||||
type TenantRateLimitConfig struct {
|
||||
TenantID int64 // 租户ID
|
||||
RequestsPerSecond float64 // 每秒请求数
|
||||
Burst int // 突发请求数
|
||||
Window time.Duration // 时间窗口
|
||||
}
|
||||
|
||||
// CheckTenantRequestLimit 检查租户请求次数限制
|
||||
func (s *rateLimitService) CheckTenantRequestLimit(ctx context.Context, tenantID int64, config *TenantRateLimitConfig) (bool, error) {
|
||||
if config == nil {
|
||||
// 使用默认配置
|
||||
config = s.getDefaultTenantRateLimitConfig(tenantID)
|
||||
}
|
||||
|
||||
// 构建Redis键 - 使用当前小时的键,确保按小时计数
|
||||
now := time.Now()
|
||||
hourKey := fmt.Sprintf("%s%d:%d", consts.AdRequestLimitKeyPrefix, tenantID, now.Hour())
|
||||
|
||||
// 获取当前计数
|
||||
currentCountVar, err := g.Redis().Get(ctx, hourKey)
|
||||
if err != nil && err.Error() != "redis: nil" {
|
||||
return false, err
|
||||
}
|
||||
|
||||
currentCount := currentCountVar.Int64()
|
||||
|
||||
// 如果是第一次请求,设置计数和过期时间(到下一个小时)
|
||||
if currentCount == 0 {
|
||||
// 设置过期时间为到下一个小时的剩余时间
|
||||
nextHour := now.Truncate(time.Hour).Add(time.Hour)
|
||||
ttl := nextHour.Sub(now)
|
||||
// 使用SetEX一次性设置值和过期时间
|
||||
err = g.Redis().SetEX(ctx, hourKey, 1, int64(ttl.Seconds()))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查是否超过限制
|
||||
maxRequests := int64(config.RequestsPerSecond * config.Window.Seconds())
|
||||
if currentCount >= maxRequests {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 增加计数
|
||||
_, err = g.Redis().Incr(ctx, hourKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetDefaultTenantRateLimitConfig 获取默认的租户限流配置
|
||||
func (s *rateLimitService) getDefaultTenantRateLimitConfig(tenantID int64) *TenantRateLimitConfig {
|
||||
// 从配置文件中读取限流参数
|
||||
ctx := context.Background()
|
||||
|
||||
// 检查是否启用租户限流
|
||||
enabled := g.Cfg().MustGet(ctx, "tenantRateLimit.enabled", false).Bool()
|
||||
if !enabled {
|
||||
// 如果未启用,返回一个很大的限制值,相当于不限制
|
||||
return &TenantRateLimitConfig{
|
||||
TenantID: tenantID,
|
||||
RequestsPerSecond: 10000, // 每秒10000个请求,相当于不限制
|
||||
Burst: 20000, // 突发20000个请求
|
||||
Window: time.Hour,
|
||||
}
|
||||
}
|
||||
|
||||
// 从配置文件中获取限流参数
|
||||
requestsPerHour := g.Cfg().MustGet(ctx, "tenantRateLimit.requestsPerHour", 3600).Int64()
|
||||
windowSeconds := g.Cfg().MustGet(ctx, "tenantRateLimit.window", 3600).Int64()
|
||||
burst := g.Cfg().MustGet(ctx, "tenantRateLimit.burst", 100).Int()
|
||||
|
||||
// 转换为每秒请求数
|
||||
requestsPerSecond := float64(requestsPerHour) / float64(windowSeconds)
|
||||
|
||||
return &TenantRateLimitConfig{
|
||||
TenantID: tenantID,
|
||||
RequestsPerSecond: requestsPerSecond,
|
||||
Burst: burst,
|
||||
Window: time.Duration(windowSeconds) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTenantRateLimitConfig 设置租户限流配置
|
||||
func (s *rateLimitService) SetTenantRateLimitConfig(ctx context.Context, config *TenantRateLimitConfig) error {
|
||||
// 注意:实际使用的是config.yml中的全局配置,此方法仅用于兼容旧API
|
||||
// 实际限流参数请修改config.yml中的tenantRateLimit部分
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTenantCurrentUsage 获取租户当前请求使用情况
|
||||
func (s *rateLimitService) GetTenantCurrentUsage(ctx context.Context, tenantID int64, config *TenantRateLimitConfig) (current int64, max int64, err error) {
|
||||
if config == nil {
|
||||
config = s.getDefaultTenantRateLimitConfig(tenantID)
|
||||
}
|
||||
|
||||
// 构建当前小时的Redis键
|
||||
now := time.Now()
|
||||
hourKey := fmt.Sprintf("%s%d:%d", consts.AdRequestLimitKeyPrefix, tenantID, now.Hour())
|
||||
|
||||
// 获取当前计数
|
||||
currentVar, err := g.Redis().Get(ctx, hourKey)
|
||||
if err != nil && err.Error() == "redis: nil" {
|
||||
current = 0
|
||||
err = nil
|
||||
} else if err != nil {
|
||||
return 0, 0, err
|
||||
} else {
|
||||
current = currentVar.Int64()
|
||||
}
|
||||
|
||||
max = int64(config.RequestsPerSecond * config.Window.Seconds())
|
||||
return current, max, nil
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
)
|
||||
|
||||
// Report Service 单例
|
||||
|
||||
514
service/stat_report_service.go
Normal file
514
service/stat_report_service.go
Normal file
@@ -0,0 +1,514 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type StatReportService struct{}
|
||||
|
||||
var StatReport = &StatReportService{}
|
||||
|
||||
// 生成日报表
|
||||
func (s *StatReportService) GenerateDailyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||
// 获取统计日期
|
||||
reportDate := time.Now()
|
||||
if req.Date != "" {
|
||||
parsedDate, err := time.Parse("2006-01-02", req.Date)
|
||||
if err == nil {
|
||||
reportDate = parsedDate
|
||||
}
|
||||
}
|
||||
|
||||
// 生成日报表数据
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "daily", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存报表
|
||||
report := &entity.StatReport{
|
||||
TenantId: req.TenantID,
|
||||
AppID: req.AppID,
|
||||
ReportType: "daily",
|
||||
ReportDate: reportDate,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
_, err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: report.Id,
|
||||
ReportType: "daily",
|
||||
ReportDate: reportDate.Format("2006-01-02"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 生成月报表
|
||||
func (s *StatReportService) GenerateMonthlyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||
reportDate := time.Now()
|
||||
if req.Date != "" {
|
||||
parsedDate, err := time.Parse("2006-01", req.Date)
|
||||
if err == nil {
|
||||
reportDate = parsedDate
|
||||
}
|
||||
}
|
||||
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "monthly", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := &entity.StatReport{
|
||||
TenantId: req.TenantID,
|
||||
AppID: req.AppID,
|
||||
ReportType: "monthly",
|
||||
ReportDate: reportDate,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
_, err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: report.Id,
|
||||
ReportType: "monthly",
|
||||
ReportDate: reportDate.Format("2006-01"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 生成季度报表
|
||||
func (s *StatReportService) GenerateQuarterlyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||
reportDate := time.Now()
|
||||
if req.Date != "" {
|
||||
parsedDate, err := time.Parse("2006-Q1", req.Date)
|
||||
if err == nil {
|
||||
reportDate = parsedDate
|
||||
}
|
||||
}
|
||||
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "quarterly", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := &entity.StatReport{
|
||||
TenantId: req.TenantID,
|
||||
AppID: req.AppID,
|
||||
ReportType: "quarterly",
|
||||
ReportDate: reportDate,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
_, err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: report.Id,
|
||||
ReportType: "quarterly",
|
||||
ReportDate: reportDate.Format("2006-Q1"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 生成年报表
|
||||
func (s *StatReportService) GenerateYearlyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||
reportDate := time.Now()
|
||||
if req.Date != "" {
|
||||
parsedDate, err := time.Parse("2006", req.Date)
|
||||
if err == nil {
|
||||
reportDate = parsedDate
|
||||
}
|
||||
}
|
||||
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "yearly", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := &entity.StatReport{
|
||||
TenantId: req.TenantID,
|
||||
AppID: req.AppID,
|
||||
ReportType: "yearly",
|
||||
ReportDate: reportDate,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
_, err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: report.Id,
|
||||
ReportType: "yearly",
|
||||
ReportDate: reportDate.Format("2006"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 生成报表数据
|
||||
func (s *StatReportService) generateReportData(ctx context.Context, tenantID, appID int64, reportType string, reportDate time.Time) (map[string]interface{}, error) {
|
||||
// 构建查询条件
|
||||
where := g.Map{"tenant_id": tenantID}
|
||||
if appID > 0 {
|
||||
where["app_id"] = appID
|
||||
}
|
||||
|
||||
// 根据报表类型确定时间范围
|
||||
startTime, endTime := s.getReportTimeRange(reportType, reportDate)
|
||||
where["created_at between ? and ?"] = g.Slice{startTime, endTime}
|
||||
|
||||
// 查询基础统计数据
|
||||
var stats []map[string]interface{}
|
||||
err := g.DB().Model("ad_statistics").Where(where).Scan(&stats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算环比数据
|
||||
yoyData, err := s.calculateYearOverYear(ctx, tenantID, appID, reportType, reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算同比数据
|
||||
momData, err := s.calculateMonthOverMonth(ctx, tenantID, appID, reportType, reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 按广告类型分组统计
|
||||
adTypeStats, err := s.groupByAdType(stats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 按地区分组统计
|
||||
regionStats := s.groupByRegion(stats)
|
||||
|
||||
// 按终端类型分组统计
|
||||
platformStats := s.groupByPlatform(stats)
|
||||
|
||||
return map[string]interface{}{
|
||||
"basic_stats": map[string]interface{}{
|
||||
"total_impressions": s.sumField(stats, "impressions"),
|
||||
"total_clicks": s.sumField(stats, "clicks"),
|
||||
"total_revenue": s.sumField(stats, "revenue"),
|
||||
"avg_ctr": s.calculateCTR(stats),
|
||||
"avg_play_duration": s.avgField(stats, "play_duration"),
|
||||
},
|
||||
"ad_type_stats": adTypeStats,
|
||||
"region_stats": regionStats,
|
||||
"platform_stats": platformStats,
|
||||
"year_over_year": yoyData,
|
||||
"month_over_month": momData,
|
||||
"time_range": map[string]string{
|
||||
"start": startTime.Format("2006-01-02 15:04:05"),
|
||||
"end": endTime.Format("2006-01-02 15:04:05"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取报表时间范围
|
||||
func (s *StatReportService) getReportTimeRange(reportType string, reportDate time.Time) (time.Time, time.Time) {
|
||||
switch reportType {
|
||||
case "daily":
|
||||
start := time.Date(reportDate.Year(), reportDate.Month(), reportDate.Day(), 0, 0, 0, 0, time.Local)
|
||||
end := start.AddDate(0, 0, 1).Add(-time.Second)
|
||||
return start, end
|
||||
case "monthly":
|
||||
start := time.Date(reportDate.Year(), reportDate.Month(), 1, 0, 0, 0, 0, time.Local)
|
||||
end := start.AddDate(0, 1, 0).Add(-time.Second)
|
||||
return start, end
|
||||
case "quarterly":
|
||||
quarter := (reportDate.Month()-1)/3 + 1
|
||||
startMonth := time.Month((quarter-1)*3 + 1)
|
||||
start := time.Date(reportDate.Year(), startMonth, 1, 0, 0, 0, 0, time.Local)
|
||||
end := start.AddDate(0, 3, 0).Add(-time.Second)
|
||||
return start, end
|
||||
case "yearly":
|
||||
start := time.Date(reportDate.Year(), 1, 1, 0, 0, 0, 0, time.Local)
|
||||
end := start.AddDate(1, 0, 0).Add(-time.Second)
|
||||
return start, end
|
||||
default:
|
||||
return reportDate, reportDate
|
||||
}
|
||||
}
|
||||
|
||||
// 计算同比数据
|
||||
func (s *StatReportService) calculateYearOverYear(ctx context.Context, tenantID, appID int64, reportType string, reportDate time.Time) (map[string]interface{}, error) {
|
||||
lastYearDate := reportDate.AddDate(-1, 0, 0)
|
||||
lastYearData, err := s.getComparisonData(ctx, tenantID, appID, reportType, lastYearDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"last_year": lastYearData,
|
||||
"growth_rate": s.calculateGrowthRate(lastYearData, s.getCurrentPeriodData(ctx, tenantID, appID, reportType, reportDate)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 计算环比数据
|
||||
func (s *StatReportService) calculateMonthOverMonth(ctx context.Context, tenantID, appID int64, reportType string, reportDate time.Time) (map[string]interface{}, error) {
|
||||
var lastPeriodDate time.Time
|
||||
switch reportType {
|
||||
case "daily":
|
||||
lastPeriodDate = reportDate.AddDate(0, 0, -1)
|
||||
case "monthly":
|
||||
lastPeriodDate = reportDate.AddDate(0, -1, 0)
|
||||
case "quarterly":
|
||||
lastPeriodDate = reportDate.AddDate(0, -3, 0)
|
||||
case "yearly":
|
||||
lastPeriodDate = reportDate.AddDate(-1, 0, 0)
|
||||
}
|
||||
|
||||
lastPeriodData, err := s.getComparisonData(ctx, tenantID, appID, reportType, lastPeriodDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"last_period": lastPeriodData,
|
||||
"growth_rate": s.calculateGrowthRate(lastPeriodData, s.getCurrentPeriodData(ctx, tenantID, appID, reportType, reportDate)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取对比数据
|
||||
func (s *StatReportService) getComparisonData(ctx context.Context, tenantID, appID int64, reportType string, date time.Time) (map[string]float64, error) {
|
||||
// 这里简化实现,实际应该查询数据库
|
||||
return map[string]float64{
|
||||
"impressions": 1000,
|
||||
"clicks": 50,
|
||||
"revenue": 500.0,
|
||||
"ctr": 0.05,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取当前周期数据
|
||||
func (s *StatReportService) getCurrentPeriodData(ctx context.Context, tenantID, appID int64, reportType string, date time.Time) map[string]float64 {
|
||||
// 这里简化实现,实际应该查询数据库
|
||||
return map[string]float64{
|
||||
"impressions": 1200,
|
||||
"clicks": 60,
|
||||
"revenue": 600.0,
|
||||
"ctr": 0.05,
|
||||
}
|
||||
}
|
||||
|
||||
// 计算增长率
|
||||
func (s *StatReportService) calculateGrowthRate(lastData, currentData map[string]float64) map[string]float64 {
|
||||
growthRate := make(map[string]float64)
|
||||
for key, lastValue := range lastData {
|
||||
currentValue := currentData[key]
|
||||
if lastValue == 0 {
|
||||
growthRate[key] = 0
|
||||
} else {
|
||||
growthRate[key] = (currentValue - lastValue) / lastValue * 100
|
||||
}
|
||||
}
|
||||
return growthRate
|
||||
}
|
||||
|
||||
// 按广告类型分组统计
|
||||
func (s *StatReportService) groupByAdType(stats []map[string]interface{}) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
for _, stat := range stats {
|
||||
adType := gconv.String(stat["ad_type"])
|
||||
if adType == "" {
|
||||
adType = "unknown"
|
||||
}
|
||||
|
||||
if _, exists := result[adType]; !exists {
|
||||
result[adType] = map[string]float64{
|
||||
"impressions": 0,
|
||||
"clicks": 0,
|
||||
"revenue": 0,
|
||||
}
|
||||
}
|
||||
|
||||
adTypeStat, ok := result[adType].(map[string]float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid adTypeStat type")
|
||||
}
|
||||
adTypeStat["impressions"] += gconv.Float64(stat["impressions"])
|
||||
adTypeStat["clicks"] += gconv.Float64(stat["clicks"])
|
||||
adTypeStat["revenue"] += gconv.Float64(stat["revenue"])
|
||||
}
|
||||
|
||||
// 计算每个广告类型的CTR
|
||||
for adType, stat := range result {
|
||||
adTypeStat, ok := stat.(map[string]float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid adTypeStat type for adType: %s", adType)
|
||||
}
|
||||
if adTypeStat["impressions"] > 0 {
|
||||
adTypeStat["ctr"] = adTypeStat["clicks"] / adTypeStat["impressions"] * 100
|
||||
} else {
|
||||
adTypeStat["ctr"] = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 按地区分组统计
|
||||
func (s *StatReportService) groupByRegion(stats []map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, stat := range stats {
|
||||
region := gconv.String(stat["region"])
|
||||
if region == "" {
|
||||
region = "unknown"
|
||||
}
|
||||
|
||||
if _, exists := result[region]; !exists {
|
||||
result[region] = map[string]float64{
|
||||
"impressions": 0,
|
||||
"clicks": 0,
|
||||
"revenue": 0,
|
||||
}
|
||||
}
|
||||
|
||||
regionStat := result[region].(map[string]float64)
|
||||
regionStat["impressions"] += gconv.Float64(stat["impressions"])
|
||||
regionStat["clicks"] += gconv.Float64(stat["clicks"])
|
||||
regionStat["revenue"] += gconv.Float64(stat["revenue"])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 按终端类型分组统计
|
||||
func (s *StatReportService) groupByPlatform(stats []map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, stat := range stats {
|
||||
platform := gconv.String(stat["platform"])
|
||||
if platform == "" {
|
||||
platform = "unknown"
|
||||
}
|
||||
|
||||
if _, exists := result[platform]; !exists {
|
||||
result[platform] = map[string]float64{
|
||||
"impressions": 0,
|
||||
"clicks": 0,
|
||||
"revenue": 0,
|
||||
}
|
||||
}
|
||||
|
||||
platformStat := result[platform].(map[string]float64)
|
||||
platformStat["impressions"] += gconv.Float64(stat["impressions"])
|
||||
platformStat["clicks"] += gconv.Float64(stat["clicks"])
|
||||
platformStat["revenue"] += gconv.Float64(stat["revenue"])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 计算字段总和
|
||||
func (s *StatReportService) sumField(stats []map[string]interface{}, field string) float64 {
|
||||
total := 0.0
|
||||
for _, stat := range stats {
|
||||
total += gconv.Float64(stat[field])
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// 计算字段平均值
|
||||
func (s *StatReportService) avgField(stats []map[string]interface{}, field string) float64 {
|
||||
if len(stats) == 0 {
|
||||
return 0
|
||||
}
|
||||
return s.sumField(stats, field) / float64(len(stats))
|
||||
}
|
||||
|
||||
// 计算平均CTR
|
||||
func (s *StatReportService) calculateCTR(stats []map[string]interface{}) float64 {
|
||||
totalImpressions := s.sumField(stats, "impressions")
|
||||
totalClicks := s.sumField(stats, "clicks")
|
||||
if totalImpressions == 0 {
|
||||
return 0
|
||||
}
|
||||
return totalClicks / totalImpressions * 100
|
||||
}
|
||||
|
||||
// 查询报表列表
|
||||
func (s *StatReportService) GetReportList(ctx context.Context, req *dto.ReportListReq) (*dto.ReportListResp, error) {
|
||||
// 使用DAO的List方法
|
||||
reports, count, err := dao.StatReport.List(ctx, req.TenantID, req.AppID, req.ReportType, req.StartDate, req.EndDate, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
var reportDTOs []*dto.ReportDTO
|
||||
for _, report := range reports {
|
||||
reportDTOs = append(reportDTOs, &dto.ReportDTO{
|
||||
ID: report.Id,
|
||||
TenantID: report.TenantId,
|
||||
AppID: report.AppID,
|
||||
ReportType: report.ReportType,
|
||||
ReportDate: report.ReportDate.Format("2006-01-02"),
|
||||
GeneratedAt: report.GeneratedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.ReportListResp{
|
||||
Reports: reportDTOs,
|
||||
Total: count,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取报表详情
|
||||
func (s *StatReportService) GetReportDetail(ctx context.Context, reportID int64) (*dto.ReportDetailResp, error) {
|
||||
var report *entity.StatReport
|
||||
report, err := dao.StatReport.GetByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if report == nil {
|
||||
return nil, fmt.Errorf("报表不存在")
|
||||
}
|
||||
|
||||
// 解析报表数据
|
||||
var reportData map[string]interface{}
|
||||
if err := gconv.Struct(report.ReportData, &reportData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportDetailResp{
|
||||
ID: report.Id,
|
||||
TenantID: report.TenantId,
|
||||
AppID: report.AppID,
|
||||
ReportType: report.ReportType,
|
||||
ReportDate: report.ReportDate.Format("2006-01-02"),
|
||||
GeneratedAt: report.GeneratedAt.Format("2006-01-02 15:04:05"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"cidservice/dao"
|
||||
"cidservice/model/dto"
|
||||
"cidservice/model/entity"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
Reference in New Issue
Block a user