初始化项目

This commit is contained in:
2025-12-06 15:24:30 +08:00
parent 88a2753211
commit fd08b8925f
59 changed files with 2456 additions and 447 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

View File

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