gomod引用
This commit is contained in:
@@ -28,13 +28,6 @@ func (s *adPosition) Add(ctx context.Context, req *dto.AddAdPositionReq) (res *d
|
||||
adPosition.UpdatedAt = now
|
||||
adPosition.IsDeleted = false
|
||||
|
||||
// 初始化统计字段
|
||||
adPosition.DailyImpressions = 0
|
||||
adPosition.DailyClicks = 0
|
||||
adPosition.DailyRevenue = 0
|
||||
adPosition.CTR = 0
|
||||
// eCPM字段是未导出的,无法直接设置
|
||||
|
||||
if err = dao.AdPosition.Insert(ctx, adPosition); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -121,62 +114,5 @@ func (s *adPosition) MatchAd(ctx context.Context, positionCode string, userInfo
|
||||
|
||||
// UpdateAdPositionStatistics 更新广告位统计
|
||||
func (s *adPosition) UpdateAdPositionStatistics(ctx context.Context, id string, impressions, clicks, revenue int64) (err error) {
|
||||
// 获取广告位信息
|
||||
adPosition, err := dao.AdPosition.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算统计数据
|
||||
totalImpressions := adPosition.DailyImpressions + impressions
|
||||
totalClicks := adPosition.DailyClicks + clicks
|
||||
totalRevenue := adPosition.DailyRevenue + revenue
|
||||
|
||||
// 计算比率
|
||||
ctr := 0.0
|
||||
if totalImpressions > 0 {
|
||||
ctr = float64(totalClicks) / float64(totalImpressions)
|
||||
}
|
||||
|
||||
ecpm := int64(0)
|
||||
if totalImpressions > 0 {
|
||||
ecpm = totalRevenue * 1000 / totalImpressions
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
stats := map[string]interface{}{
|
||||
"dailyImpressions": totalImpressions,
|
||||
"dailyClicks": totalClicks,
|
||||
"dailyRevenue": totalRevenue,
|
||||
"ctr": ctr,
|
||||
"ecpm": ecpm,
|
||||
"updatedAt": time.Now(),
|
||||
}
|
||||
|
||||
// 更新广告位统计
|
||||
err = dao.AdPosition.UpdateStatistics(ctx, id, stats)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 插入统计记录
|
||||
today := time.Now()
|
||||
todayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()).Unix()
|
||||
|
||||
statistics := &entity.AdStatistics{
|
||||
StatType: "adPosition",
|
||||
StatDimension: "day",
|
||||
TargetId: id,
|
||||
TargetName: adPosition.Name,
|
||||
StatDate: todayStart,
|
||||
Impressions: impressions,
|
||||
Clicks: clicks,
|
||||
Cost: 0, // 广告位不记录消耗,只记录收入
|
||||
Revenue: revenue,
|
||||
CTR: ctr,
|
||||
// eCPM字段是未导出的,无法直接设置
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, statistics)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,15 +36,18 @@ func (s *adSourceService) CreateAdSource(ctx context.Context, req *dto.CreateAdS
|
||||
}
|
||||
|
||||
adSource := &entity.AdSource{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Provider: req.Provider,
|
||||
Type: req.Type,
|
||||
APIEndpoint: req.APIEndpoint,
|
||||
Status: "active", // 默认状态
|
||||
Priority: 1, // 默认优先级
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Provider: req.Provider,
|
||||
Type: req.Type,
|
||||
APIConfig: entity.APIConfig{
|
||||
Endpoint: req.APIEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
adSource.Status = "active" // 默认状态
|
||||
|
||||
return dao.AdSource.Create(ctx, adSource)
|
||||
}
|
||||
|
||||
@@ -77,7 +80,7 @@ func (s *adSourceService) UpdateAdSource(ctx context.Context, id string, req *dt
|
||||
updateData.Name = req.Name
|
||||
}
|
||||
if req.APIEndpoint != "" {
|
||||
updateData.APIEndpoint = req.APIEndpoint
|
||||
updateData.APIConfig.Endpoint = req.APIEndpoint
|
||||
}
|
||||
|
||||
return dao.AdSource.UpdateFields(ctx, id, updateData)
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
)
|
||||
|
||||
var AdStatistics = new(adStatistics)
|
||||
|
||||
type adStatistics struct{}
|
||||
|
||||
// List 获取统计数据列表
|
||||
func (s *adStatistics) List(ctx context.Context, req *dto.GetAdStatisticsReq) (res *dto.GetAdStatisticsRes, err error) {
|
||||
list, total, err := dao.AdStatistics.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = &dto.GetAdStatisticsRes{
|
||||
Statistics: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetStatistics 获取统计数据
|
||||
func (s *adStatistics) GetStatistics(ctx context.Context, req *dto.GetAdStatisticsReq) (res *dto.GetAdStatisticsRes, err error) {
|
||||
list, total, err := dao.AdStatistics.GetStatistics(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = &dto.GetAdStatisticsRes{
|
||||
Statistics: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetDashboard 获取仪表盘数据
|
||||
func (s *adStatistics) GetDashboard(ctx context.Context, req *dto.GetDashboardReq) (res *dto.GetDashboardRes, err error) {
|
||||
// 构建统计查询请求
|
||||
statReq := &dto.GetAdStatisticsReq{
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
StatDimension: req.Dimension,
|
||||
}
|
||||
|
||||
// 获取所有统计数据
|
||||
stats, _, err := dao.AdStatistics.GetStatistics(ctx, statReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总览数据
|
||||
overview := s.calculateOverview(stats)
|
||||
|
||||
// 计算趋势数据
|
||||
trends := s.calculateTrends(stats, req.Dimension)
|
||||
|
||||
// 计算排行数据
|
||||
topAdvertisers := s.calculateTopAdvertisers(stats)
|
||||
topAds := s.calculateTopAds(stats)
|
||||
topPositions := s.calculateTopPositions(stats)
|
||||
|
||||
res = &dto.GetDashboardRes{
|
||||
Overview: overview,
|
||||
Trends: trends,
|
||||
TopAdvertisers: topAdvertisers,
|
||||
TopAds: topAds,
|
||||
TopPositions: topPositions,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateDailyStatistics 生成每日统计数据
|
||||
func (s *adStatistics) GenerateDailyStatistics(ctx context.Context, date int64) (err error) {
|
||||
// 转换日期
|
||||
t := time.Unix(date, 0)
|
||||
startOfDay := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
// 生成广告主统计数据
|
||||
err = s.generateAdvertiserStatistics(ctx, startOfDay.Unix(), endOfDay.Unix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成广告主统计数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成广告统计数据
|
||||
err = s.generateAdvertisementStatistics(ctx, startOfDay.Unix(), endOfDay.Unix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成广告统计数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成广告位统计数据
|
||||
err = s.generateAdPositionStatistics(ctx, startOfDay.Unix(), endOfDay.Unix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成广告位统计数据失败: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// generateAdvertiserStatistics 生成广告主统计数据
|
||||
func (s *adStatistics) generateAdvertiserStatistics(ctx context.Context, startDate int64, _ int64) (err error) {
|
||||
// 这里简化处理,实际项目中应该从日志表或实时数据中聚合
|
||||
advertiserStats := &entity.AdStatistics{
|
||||
StatType: "advertiser",
|
||||
StatDimension: "day",
|
||||
TargetId: "example_advertiser_id",
|
||||
TargetName: "示例广告主",
|
||||
StatDate: startDate,
|
||||
Impressions: 10000,
|
||||
Clicks: 500,
|
||||
Conversions: 50,
|
||||
Cost: 50000, // 500元,单位分
|
||||
CTR: 0.05, // 5%
|
||||
CVR: 0.1, // 10%
|
||||
CPM: 5000, // 50元/千次展示
|
||||
CPC: 100, // 1元/点击
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, advertiserStats)
|
||||
return
|
||||
}
|
||||
|
||||
// generateAdvertisementStatistics 生成广告统计数据
|
||||
func (s *adStatistics) generateAdvertisementStatistics(ctx context.Context, startDate int64, _ int64) (err error) {
|
||||
// 这里简化处理,实际项目中应该从日志表或实时数据中聚合
|
||||
adStats := &entity.AdStatistics{
|
||||
StatType: "advertisement",
|
||||
StatDimension: "day",
|
||||
TargetId: "example_ad_id",
|
||||
TargetName: "示例广告",
|
||||
StatDate: startDate,
|
||||
Impressions: 1000,
|
||||
Clicks: 50,
|
||||
Conversions: 5,
|
||||
Cost: 5000, // 50元,单位分
|
||||
CTR: 0.05, // 5%
|
||||
CVR: 0.1, // 10%
|
||||
CPM: 5000, // 50元/千次展示
|
||||
CPC: 100, // 1元/点击
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, adStats)
|
||||
return
|
||||
}
|
||||
|
||||
// generateAdPositionStatistics 生成广告位统计数据
|
||||
func (s *adStatistics) generateAdPositionStatistics(ctx context.Context, startDate int64, _ int64) (err error) {
|
||||
// 这里简化处理,实际项目中应该从日志表或实时数据中聚合
|
||||
positionStats := &entity.AdStatistics{
|
||||
StatType: "adPosition",
|
||||
StatDimension: "day",
|
||||
TargetId: "example_position_id",
|
||||
TargetName: "示例广告位",
|
||||
StatDate: startDate,
|
||||
Impressions: 5000,
|
||||
Clicks: 250,
|
||||
Revenue: 6000, // 60元,单位分
|
||||
CTR: 0.05, // 5%
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, positionStats)
|
||||
return
|
||||
}
|
||||
|
||||
// calculateOverview 计算总览数据
|
||||
func (s *adStatistics) calculateOverview(stats []*entity.AdStatistics) dto.OverviewData {
|
||||
var totalImpressions, totalClicks, totalCost, totalRevenue int64
|
||||
var totalCTR, totalCVR float64
|
||||
var count int
|
||||
|
||||
// 统计不同类型的数据
|
||||
advertiserCount := make(map[string]bool)
|
||||
adCount := make(map[string]bool)
|
||||
positionCount := make(map[string]bool)
|
||||
|
||||
for _, stat := range stats {
|
||||
totalImpressions += stat.Impressions
|
||||
totalClicks += stat.Clicks
|
||||
totalCost += stat.Cost
|
||||
totalRevenue += stat.Revenue
|
||||
totalCTR += stat.CTR
|
||||
totalCVR += stat.CVR
|
||||
count++
|
||||
|
||||
// 统计不同实体的数量
|
||||
if stat.StatType == "advertiser" {
|
||||
advertiserCount[stat.TargetId] = true
|
||||
} else if stat.StatType == "advertisement" {
|
||||
adCount[stat.TargetId] = true
|
||||
} else if stat.StatType == "adPosition" {
|
||||
positionCount[stat.TargetId] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 计算平均值
|
||||
averageCTR := 0.0
|
||||
averageCVR := 0.0
|
||||
if count > 0 {
|
||||
averageCTR = totalCTR / float64(count)
|
||||
averageCVR = totalCVR / float64(count)
|
||||
}
|
||||
|
||||
return dto.OverviewData{
|
||||
TotalAdvertisers: int64(len(advertiserCount)),
|
||||
TotalAds: int64(len(adCount)),
|
||||
TotalPositions: int64(len(positionCount)),
|
||||
TotalImpressions: totalImpressions,
|
||||
TotalClicks: totalClicks,
|
||||
TotalCost: totalCost,
|
||||
TotalRevenue: totalRevenue,
|
||||
AverageCTR: averageCTR,
|
||||
AverageCVR: averageCVR,
|
||||
}
|
||||
}
|
||||
|
||||
// calculateTrends 计算趋势数据
|
||||
func (s *adStatistics) calculateTrends(stats []*entity.AdStatistics, dimension string) []dto.TrendData {
|
||||
trends := make([]dto.TrendData, 0)
|
||||
|
||||
// 按日期分组统计数据
|
||||
dateMap := make(map[int64]*dto.TrendData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if _, exists := dateMap[stat.StatDate]; !exists {
|
||||
dateMap[stat.StatDate] = &dto.TrendData{
|
||||
Date: stat.StatDate,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
trend := dateMap[stat.StatDate]
|
||||
trend.Impressions += stat.Impressions
|
||||
trend.Clicks += stat.Clicks
|
||||
trend.Cost += stat.Cost
|
||||
trend.Revenue += stat.Revenue
|
||||
}
|
||||
|
||||
// 转换为切片并排序
|
||||
for _, trend := range dateMap {
|
||||
trends = append(trends, *trend)
|
||||
}
|
||||
|
||||
// 按日期排序
|
||||
sort.Slice(trends, func(i, j int) bool {
|
||||
return trends[i].Date < trends[j].Date
|
||||
})
|
||||
|
||||
return trends
|
||||
}
|
||||
|
||||
// calculateTopAdvertisers 计算广告主排行
|
||||
func (s *adStatistics) calculateTopAdvertisers(stats []*entity.AdStatistics) []dto.RankData {
|
||||
advertiserMap := make(map[string]*dto.RankData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if stat.StatType == "advertiser" {
|
||||
if _, exists := advertiserMap[stat.TargetId]; !exists {
|
||||
advertiserMap[stat.TargetId] = &dto.RankData{
|
||||
Id: stat.TargetId,
|
||||
Name: stat.TargetName,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
rank := advertiserMap[stat.TargetId]
|
||||
rank.Impressions += stat.Impressions
|
||||
rank.Clicks += stat.Clicks
|
||||
rank.Cost += stat.Cost
|
||||
rank.Revenue += stat.Revenue
|
||||
}
|
||||
}
|
||||
|
||||
return s.sortRankData(advertiserMap)
|
||||
}
|
||||
|
||||
// calculateTopAds 计算广告排行
|
||||
func (s *adStatistics) calculateTopAds(stats []*entity.AdStatistics) []dto.RankData {
|
||||
adMap := make(map[string]*dto.RankData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if stat.StatType == "advertisement" {
|
||||
if _, exists := adMap[stat.TargetId]; !exists {
|
||||
adMap[stat.TargetId] = &dto.RankData{
|
||||
Id: stat.TargetId,
|
||||
Name: stat.TargetName,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
rank := adMap[stat.TargetId]
|
||||
rank.Impressions += stat.Impressions
|
||||
rank.Clicks += stat.Clicks
|
||||
rank.Cost += stat.Cost
|
||||
rank.Revenue += stat.Revenue
|
||||
}
|
||||
}
|
||||
|
||||
return s.sortRankData(adMap)
|
||||
}
|
||||
|
||||
// calculateTopPositions 计算广告位排行
|
||||
func (s *adStatistics) calculateTopPositions(stats []*entity.AdStatistics) []dto.RankData {
|
||||
positionMap := make(map[string]*dto.RankData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if stat.StatType == "adPosition" {
|
||||
if _, exists := positionMap[stat.TargetId]; !exists {
|
||||
positionMap[stat.TargetId] = &dto.RankData{
|
||||
Id: stat.TargetId,
|
||||
Name: stat.TargetName,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
rank := positionMap[stat.TargetId]
|
||||
rank.Impressions += stat.Impressions
|
||||
rank.Clicks += stat.Clicks
|
||||
rank.Cost += stat.Cost
|
||||
rank.Revenue += stat.Revenue
|
||||
}
|
||||
}
|
||||
|
||||
return s.sortRankData(positionMap)
|
||||
}
|
||||
|
||||
// sortRankData 对排行数据进行排序
|
||||
func (s *adStatistics) sortRankData(dataMap map[string]*dto.RankData) []dto.RankData {
|
||||
rankList := make([]dto.RankData, 0, len(dataMap))
|
||||
|
||||
for _, rank := range dataMap {
|
||||
rankList = append(rankList, *rank)
|
||||
}
|
||||
|
||||
// 按收入降序排序
|
||||
sort.Slice(rankList, func(i, j int) bool {
|
||||
return rankList[i].Revenue > rankList[j].Revenue
|
||||
})
|
||||
|
||||
// 只返回前10名
|
||||
if len(rankList) > 10 {
|
||||
rankList = rankList[:10]
|
||||
}
|
||||
|
||||
return rankList
|
||||
}
|
||||
@@ -30,12 +30,6 @@ func (s *advertisement) Add(ctx context.Context, req *dto.AddAdvertisementReq) (
|
||||
// 设置初始状态
|
||||
advertisement.Status = "待审核"
|
||||
|
||||
// 初始化统计字段
|
||||
advertisement.Impressions = 0
|
||||
advertisement.Clicks = 0
|
||||
advertisement.Conversions = 0
|
||||
advertisement.Cost = 0
|
||||
|
||||
if err = dao.Advertisement.Insert(ctx, advertisement); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -89,75 +83,5 @@ func (s *advertisement) List(ctx context.Context, req *dto.ListAdvertisementReq)
|
||||
|
||||
// UpdateAdStatistics 更新广告统计
|
||||
func (s *advertisement) UpdateAdStatistics(ctx context.Context, id string, impressions, clicks, conversions int64, cost int64) (err error) {
|
||||
// 获取广告信息
|
||||
ad, err := dao.Advertisement.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算统计数据
|
||||
totalImpressions := ad.Impressions + impressions
|
||||
totalClicks := ad.Clicks + clicks
|
||||
totalConversions := ad.Conversions + conversions
|
||||
totalCost := ad.Cost + cost
|
||||
|
||||
// 计算比率
|
||||
ctr := 0.0
|
||||
if totalImpressions > 0 {
|
||||
ctr = float64(totalClicks) / float64(totalImpressions)
|
||||
}
|
||||
|
||||
cvr := 0.0
|
||||
if totalClicks > 0 {
|
||||
cvr = float64(totalConversions) / float64(totalClicks)
|
||||
}
|
||||
|
||||
cpm := int64(0)
|
||||
if totalImpressions > 0 {
|
||||
cpm = totalCost * 1000 / totalImpressions
|
||||
}
|
||||
|
||||
cpc := int64(0)
|
||||
if totalClicks > 0 {
|
||||
cpc = totalCost / totalClicks
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
stats := map[string]interface{}{
|
||||
"impressions": totalImpressions,
|
||||
"clicks": totalClicks,
|
||||
"conversions": totalConversions,
|
||||
"cost": totalCost,
|
||||
"updatedAt": time.Now(),
|
||||
}
|
||||
|
||||
// 更新广告统计
|
||||
err = dao.Advertisement.UpdateStatistics(ctx, id, stats)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 插入统计记录
|
||||
today := time.Now()
|
||||
todayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()).Unix()
|
||||
|
||||
statistics := &entity.AdStatistics{
|
||||
StatType: "advertisement",
|
||||
StatDimension: "day",
|
||||
TargetId: id,
|
||||
TargetName: ad.Title,
|
||||
StatDate: todayStart,
|
||||
Impressions: impressions,
|
||||
Clicks: clicks,
|
||||
Conversions: conversions,
|
||||
Cost: cost,
|
||||
CTR: ctr,
|
||||
CVR: cvr,
|
||||
CPM: cpm,
|
||||
CPC: cpc,
|
||||
// CreatedAt、UpdatedAt、IsDeleted字段是嵌入字段,无需单独设置
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, statistics)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -45,12 +45,14 @@ func (s *applicationService) CreateApplication(ctx context.Context, req *dto.Cre
|
||||
Categories: req.Categories,
|
||||
Tags: req.Tags,
|
||||
AdTypes: req.AdTypes,
|
||||
Status: "active",
|
||||
AppKey: appKey,
|
||||
AppSecret: appSecret,
|
||||
CallbackURL: req.CallbackURL,
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
application.Status = "active"
|
||||
|
||||
return dao.Application.Create(ctx, application)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,631 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cid/dao"
|
||||
"cid/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// StatReportScheduler 统计报表定时任务调度器
|
||||
type StatReportScheduler struct{}
|
||||
|
||||
var StatReportSchedulerInstance = &StatReportScheduler{}
|
||||
var schedulerLock sync.Mutex
|
||||
var isSchedulerRunning bool
|
||||
|
||||
// StartScheduler 启动定时任务调度器(分布式安全)
|
||||
func (s *StatReportScheduler) StartScheduler(ctx context.Context) error {
|
||||
schedulerLock.Lock()
|
||||
defer schedulerLock.Unlock()
|
||||
|
||||
// 检查是否已经有调度器在运行(分布式部署时避免重复执行)
|
||||
if isSchedulerRunning {
|
||||
g.Log().Info(ctx, "统计报表定时任务调度器已在运行")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试获取分布式锁
|
||||
if !s.acquireDistributedLock(ctx) {
|
||||
g.Log().Info(ctx, "其他节点正在运行统计报表定时任务,当前节点跳过")
|
||||
return nil
|
||||
}
|
||||
|
||||
isSchedulerRunning = true
|
||||
|
||||
// 启动锁续期任务
|
||||
go s.startLockRenewal(ctx)
|
||||
|
||||
// 启动日报表生成任务(每天凌晨3点执行)
|
||||
go s.startDailyReportScheduler(ctx)
|
||||
|
||||
// 启动月报表生成任务(每月1日凌晨4点执行)
|
||||
go s.startMonthlyReportScheduler(ctx)
|
||||
|
||||
// 启动季度报表生成任务(每季度第一天凌晨5点执行)
|
||||
go s.startQuarterlyReportScheduler(ctx)
|
||||
|
||||
// 启动年报表生成任务(每年1月1日凌晨6点执行)
|
||||
go s.startYearlyReportScheduler(ctx)
|
||||
|
||||
g.Log().Info(ctx, "统计报表定时任务调度器已启动")
|
||||
return nil
|
||||
}
|
||||
|
||||
// acquireDistributedLock 获取分布式锁(基于Redis)
|
||||
func (s *StatReportScheduler) acquireDistributedLock(ctx context.Context) bool {
|
||||
// 使用Redis实现分布式锁
|
||||
// 锁的有效期为1小时,避免死锁
|
||||
lockKey := "stat_report_scheduler_lock"
|
||||
lockValue := fmt.Sprintf("%d", time.Now().Unix())
|
||||
|
||||
// 尝试获取锁
|
||||
result, err := g.Redis().Do(ctx, "SET", lockKey, lockValue, "NX", "EX", 3600)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取分布式锁失败: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return result != nil
|
||||
}
|
||||
|
||||
// renewDistributedLock 续期分布式锁
|
||||
func (s *StatReportScheduler) renewDistributedLock(ctx context.Context) bool {
|
||||
lockKey := "stat_report_scheduler_lock"
|
||||
|
||||
// 检查锁是否存在
|
||||
exists, err := g.Redis().Do(ctx, "EXISTS", lockKey)
|
||||
if err != nil || exists == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查锁是否存在(EXISTS返回1表示存在,0表示不存在)
|
||||
existsInt := exists.Int64()
|
||||
if existsInt == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 续期锁,延长1小时
|
||||
_, err = g.Redis().Do(ctx, "EXPIRE", lockKey, 3600)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "续期分布式锁失败: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// startLockRenewal 启动锁续期任务
|
||||
func (s *StatReportScheduler) startLockRenewal(ctx context.Context) {
|
||||
ticker := time.NewTicker(30 * time.Minute) // 每30分钟续期一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !s.renewDistributedLock(ctx) {
|
||||
g.Log().Error(ctx, "锁续期失败,调度器将停止运行")
|
||||
// 锁丢失,停止调度器
|
||||
schedulerLock.Lock()
|
||||
isSchedulerRunning = false
|
||||
schedulerLock.Unlock()
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// acquireTaskLock 获取任务级分布式锁
|
||||
func (s *StatReportScheduler) acquireTaskLock(ctx context.Context, lockKey string) bool {
|
||||
lockValue := fmt.Sprintf("%d", time.Now().Unix())
|
||||
|
||||
// 尝试获取任务锁,有效期为2小时
|
||||
result, err := g.Redis().Do(ctx, "SET", lockKey, lockValue, "NX", "EX", 7200)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取任务锁失败: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return result != nil
|
||||
}
|
||||
|
||||
// releaseTaskLock 释放任务级分布式锁
|
||||
func (s *StatReportScheduler) releaseTaskLock(ctx context.Context, lockKey string) {
|
||||
_, err := g.Redis().Do(ctx, "DEL", lockKey)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "释放任务锁失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// startDailyReportScheduler 日报表定时任务
|
||||
func (s *StatReportScheduler) startDailyReportScheduler(ctx context.Context) {
|
||||
// 计算到凌晨3点的时间
|
||||
now := time.Now()
|
||||
next := time.Date(now.Year(), now.Month(), now.Day()+1, 3, 0, 0, 0, time.Local)
|
||||
duration := next.Sub(now)
|
||||
|
||||
// 等待到凌晨3点
|
||||
time.Sleep(duration)
|
||||
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
// 立即执行一次昨天的日报表生成
|
||||
go s.generateYesterdayDailyReport(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// 生成昨天的日报表
|
||||
s.generateYesterdayDailyReport(ctx)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startMonthlyReportScheduler 月报表定时任务
|
||||
func (s *StatReportScheduler) startMonthlyReportScheduler(ctx context.Context) {
|
||||
// 计算到下个月1日凌晨4点的时间
|
||||
now := time.Now()
|
||||
next := time.Date(now.Year(), now.Month()+1, 1, 4, 0, 0, 0, time.Local)
|
||||
duration := next.Sub(now)
|
||||
|
||||
// 等待到下个月1日凌晨4点
|
||||
time.Sleep(duration)
|
||||
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// 检查是否是每月1日,如果是则生成上个月的月报表
|
||||
if time.Now().Day() == 1 {
|
||||
go s.generateLastMonthReport(ctx)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startQuarterlyReportScheduler 季度报表定时任务
|
||||
func (s *StatReportScheduler) startQuarterlyReportScheduler(ctx context.Context) {
|
||||
// 计算到下个季度第一天凌晨5点的时间
|
||||
now := time.Now()
|
||||
nextQuarter := s.getNextQuarterFirstDay(now)
|
||||
next := time.Date(nextQuarter.Year(), nextQuarter.Month(), nextQuarter.Day(), 5, 0, 0, 0, time.Local)
|
||||
duration := next.Sub(now)
|
||||
|
||||
// 等待到下个季度第一天凌晨5点
|
||||
time.Sleep(duration)
|
||||
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// 检查是否是季度第一天,如果是则生成上个季度的季度报表
|
||||
if s.isQuarterFirstDay() {
|
||||
go s.generateLastQuarterReport(ctx)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startYearlyReportScheduler 年报表定时任务
|
||||
func (s *StatReportScheduler) startYearlyReportScheduler(ctx context.Context) {
|
||||
// 计算到明年1月1日凌晨6点的时间
|
||||
now := time.Now()
|
||||
next := time.Date(now.Year()+1, time.January, 1, 6, 0, 0, 0, time.Local)
|
||||
duration := next.Sub(now)
|
||||
|
||||
// 等待到明年1月1日凌晨6点
|
||||
time.Sleep(duration)
|
||||
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// 检查是否是1月1日,如果是则生成去年的年报表
|
||||
if time.Now().Month() == time.January && time.Now().Day() == 1 {
|
||||
go s.generateLastYearReport(ctx)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateYesterdayDailyReport 生成昨天的日报表
|
||||
func (s *StatReportScheduler) generateYesterdayDailyReport(ctx context.Context) error {
|
||||
yesterday := time.Now().AddDate(0, 0, -1)
|
||||
return s.generateDailyReportForDate(ctx, yesterday)
|
||||
}
|
||||
|
||||
// generateLastMonthReport 生成上个月的月报表
|
||||
func (s *StatReportScheduler) generateLastMonthReport(ctx context.Context) error {
|
||||
lastMonth := time.Now().AddDate(0, -1, 0)
|
||||
return s.generateMonthlyReportFromDaily(ctx, lastMonth)
|
||||
}
|
||||
|
||||
// generateLastQuarterReport 生成上个季度的季度报表
|
||||
func (s *StatReportScheduler) generateLastQuarterReport(ctx context.Context) error {
|
||||
lastQuarter := time.Now().AddDate(0, -3, 0)
|
||||
return s.generateQuarterlyReportFromMonthly(ctx, lastQuarter)
|
||||
}
|
||||
|
||||
// generateLastYearReport 生成去年的年报表
|
||||
func (s *StatReportScheduler) generateLastYearReport(ctx context.Context) error {
|
||||
lastYear := time.Now().AddDate(-1, 0, 0)
|
||||
return s.generateYearlyReportFromQuarterly(ctx, lastYear)
|
||||
}
|
||||
|
||||
// generateDailyReportForDate 为指定日期生成日报表
|
||||
func (s *StatReportScheduler) generateDailyReportForDate(ctx context.Context, date time.Time) error {
|
||||
// 获取日报表任务分布式锁
|
||||
dailyLockKey := fmt.Sprintf("daily_report_lock_%s", date.Format("2006-01-02"))
|
||||
if !s.acquireTaskLock(ctx, dailyLockKey) {
|
||||
g.Log().Info(ctx, "其他节点正在生成日报表,日期: %s", date.Format("2006-01-02"))
|
||||
return nil
|
||||
}
|
||||
defer s.releaseTaskLock(ctx, dailyLockKey)
|
||||
|
||||
// 获取所有租户
|
||||
tenants, err := s.getAllTenants(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tenantID := range tenants {
|
||||
// 检查是否已生成该日期的报表
|
||||
if s.isReportGenerated(ctx, tenantID, "daily", date.Format("2006-01-02")) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 生成日报表数据(从流水数据统计)
|
||||
reportData, err := s.generateReportDataFromRawData(ctx, tenantID, 0, "daily", date)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "生成租户%d日报表失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 保存日报表
|
||||
report := &entity.StatReport{
|
||||
AppID: "0", // 0表示所有应用
|
||||
ReportType: "daily",
|
||||
ReportDate: date,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "保存租户%d日报表失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "成功生成租户%d的日报表,日期: %s", tenantID, date.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateMonthlyReportFromDaily 从日报表生成月报表
|
||||
func (s *StatReportScheduler) generateMonthlyReportFromDaily(ctx context.Context, date time.Time) error {
|
||||
// 获取月报表任务分布式锁
|
||||
monthlyLockKey := fmt.Sprintf("monthly_report_lock_%s", date.Format("2006-01"))
|
||||
if !s.acquireTaskLock(ctx, monthlyLockKey) {
|
||||
g.Log().Info(ctx, "其他节点正在生成月报表,日期: %s", date.Format("2006-01"))
|
||||
return nil
|
||||
}
|
||||
defer s.releaseTaskLock(ctx, monthlyLockKey)
|
||||
|
||||
tenants, err := s.getAllTenants(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tenantID := range tenants {
|
||||
if s.isReportGenerated(ctx, tenantID, "monthly", date.Format("2006-01")) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取该月的所有日报表数据
|
||||
dailyReports, err := s.getDailyReportsForMonth(ctx, tenantID, date)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取租户%d月报数据失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 聚合日报表数据生成月报表
|
||||
reportData := s.aggregateDailyReportsToMonthly(dailyReports)
|
||||
|
||||
report := &entity.StatReport{
|
||||
AppID: "0",
|
||||
ReportType: "monthly",
|
||||
ReportDate: date,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "保存租户%d月报表失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "成功生成租户%d的月报表,日期: %s", tenantID, date.Format("2006-01"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateQuarterlyReportFromMonthly 从月报表生成季度报表
|
||||
func (s *StatReportScheduler) generateQuarterlyReportFromMonthly(ctx context.Context, date time.Time) error {
|
||||
// 获取季度报表任务分布式锁
|
||||
quarter := fmt.Sprintf("Q%d", (date.Month()-1)/3+1)
|
||||
quarterlyLockKey := fmt.Sprintf("quarterly_report_lock_%d-%s", date.Year(), quarter)
|
||||
if !s.acquireTaskLock(ctx, quarterlyLockKey) {
|
||||
g.Log().Info(ctx, "其他节点正在生成季度报表,日期: %d-%s", date.Year(), quarter)
|
||||
return nil
|
||||
}
|
||||
defer s.releaseTaskLock(ctx, quarterlyLockKey)
|
||||
|
||||
tenants, err := s.getAllTenants(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tenantID := range tenants {
|
||||
reportDate := fmt.Sprintf("%d-%s", date.Year(), quarter)
|
||||
|
||||
if s.isReportGenerated(ctx, tenantID, "quarterly", reportDate) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取该季度的所有月报表数据
|
||||
monthlyReports, err := s.getMonthlyReportsForQuarter(ctx, tenantID, date)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取租户%d季报数据失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 聚合月报表数据生成季度报表
|
||||
reportData := s.aggregateMonthlyReportsToQuarterly(monthlyReports)
|
||||
|
||||
report := &entity.StatReport{
|
||||
AppID: "0",
|
||||
ReportType: "quarterly",
|
||||
ReportDate: date,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "保存租户%d季度报表失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "成功生成租户%d的季度报表,日期: %s", tenantID, reportDate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateYearlyReportFromQuarterly 从季度报表生成年报表
|
||||
func (s *StatReportScheduler) generateYearlyReportFromQuarterly(ctx context.Context, date time.Time) error {
|
||||
// 获取年报表任务分布式锁
|
||||
yearlyLockKey := fmt.Sprintf("yearly_report_lock_%d", date.Year())
|
||||
if !s.acquireTaskLock(ctx, yearlyLockKey) {
|
||||
g.Log().Info(ctx, "其他节点正在生成年报表,日期: %d", date.Year())
|
||||
return nil
|
||||
}
|
||||
defer s.releaseTaskLock(ctx, yearlyLockKey)
|
||||
|
||||
tenants, err := s.getAllTenants(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tenantID := range tenants {
|
||||
reportDate := fmt.Sprintf("%d", date.Year())
|
||||
|
||||
if s.isReportGenerated(ctx, tenantID, "yearly", reportDate) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取该年度的所有季度报表数据
|
||||
quarterlyReports, err := s.getQuarterlyReportsForYear(ctx, tenantID, date)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取租户%d年报数据失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 聚合季度报表数据生成年报表
|
||||
reportData := s.aggregateQuarterlyReportsToYearly(quarterlyReports)
|
||||
|
||||
report := &entity.StatReport{
|
||||
AppID: "0",
|
||||
ReportType: "yearly",
|
||||
ReportDate: date,
|
||||
ReportData: gconv.String(reportData),
|
||||
GeneratedAt: time.Now(),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
err = dao.StatReport.Create(ctx, report)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "保存租户%d年报表失败: %v", tenantID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "成功生成租户%d的年报表,日期: %s", tenantID, reportDate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateReportDataFromRawData 从原始流水数据生成报表数据
|
||||
func (s *StatReportScheduler) generateReportDataFromRawData(ctx context.Context, tenantID, appID int64, reportType string, reportDate time.Time) (map[string]interface{}, error) {
|
||||
// 使用现有的报表生成逻辑
|
||||
return StatReport.generateReportData(ctx, tenantID, appID, reportType, reportDate)
|
||||
}
|
||||
|
||||
// getDailyReportsForMonth 获取某个月的所有日报表
|
||||
func (s *StatReportScheduler) getDailyReportsForMonth(ctx context.Context, tenantID int64, date time.Time) ([]map[string]interface{}, error) {
|
||||
startDate := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.Local)
|
||||
endDate := startDate.AddDate(0, 1, -1)
|
||||
|
||||
reports, _, err := dao.StatReport.List(ctx, strconv.FormatInt(tenantID, 10), "0", "daily", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), 1, 31)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dailyData []map[string]interface{}
|
||||
for _, report := range reports {
|
||||
var data map[string]interface{}
|
||||
if err := gconv.Struct(report.ReportData, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
dailyData = append(dailyData, data)
|
||||
}
|
||||
|
||||
return dailyData, nil
|
||||
}
|
||||
|
||||
// getMonthlyReportsForQuarter 获取某个季度的所有月报表
|
||||
func (s *StatReportScheduler) getMonthlyReportsForQuarter(ctx context.Context, tenantID int64, date time.Time) ([]map[string]interface{}, error) {
|
||||
quarterStartMonth := time.Month(((date.Month()-1)/3)*3 + 1)
|
||||
reports := make([]map[string]interface{}, 0)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
monthDate := time.Date(date.Year(), quarterStartMonth+time.Month(i), 1, 0, 0, 0, 0, time.Local)
|
||||
reportDate := monthDate.Format("2006-01")
|
||||
|
||||
report, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(tenantID, 10), "monthly", reportDate)
|
||||
if err != nil || report == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := gconv.Struct(report.ReportData, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
reports = append(reports, data)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// getQuarterlyReportsForYear 获取某年的所有季度报表
|
||||
func (s *StatReportScheduler) getQuarterlyReportsForYear(ctx context.Context, tenantID int64, date time.Time) ([]map[string]interface{}, error) {
|
||||
reports := make([]map[string]interface{}, 0)
|
||||
|
||||
for quarter := 1; quarter <= 4; quarter++ {
|
||||
reportDate := fmt.Sprintf("%d-Q%d", date.Year(), quarter)
|
||||
report, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(tenantID, 10), "quarterly", reportDate)
|
||||
if err != nil || report == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := gconv.Struct(report.ReportData, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
reports = append(reports, data)
|
||||
}
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// aggregateDailyReportsToMonthly 聚合日报表数据生成月报表
|
||||
func (s *StatReportScheduler) aggregateDailyReportsToMonthly(dailyReports []map[string]interface{}) map[string]interface{} {
|
||||
// 实现聚合逻辑,这里简化处理
|
||||
return map[string]interface{}{
|
||||
"type": "monthly",
|
||||
"data": dailyReports,
|
||||
"summary": "聚合后的月报数据",
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateMonthlyReportsToQuarterly 聚合月报表数据生成季度报表
|
||||
func (s *StatReportScheduler) aggregateMonthlyReportsToQuarterly(monthlyReports []map[string]interface{}) map[string]interface{} {
|
||||
// 实现聚合逻辑,这里简化处理
|
||||
return map[string]interface{}{
|
||||
"type": "quarterly",
|
||||
"data": monthlyReports,
|
||||
"summary": "聚合后的季报数据",
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateQuarterlyReportsToYearly 聚合季度报表数据生成年报表
|
||||
func (s *StatReportScheduler) aggregateQuarterlyReportsToYearly(quarterlyReports []map[string]interface{}) map[string]interface{} {
|
||||
// 实现聚合逻辑,这里简化处理
|
||||
return map[string]interface{}{
|
||||
"type": "yearly",
|
||||
"data": quarterlyReports,
|
||||
"summary": "聚合后的年报数据",
|
||||
}
|
||||
}
|
||||
|
||||
// getAllTenants 获取所有租户ID
|
||||
func (s *StatReportScheduler) getAllTenants(ctx context.Context) ([]int64, error) {
|
||||
// 这里应该从数据库查询所有租户ID
|
||||
// 暂时返回示例数据
|
||||
return []int64{1, 2, 3}, nil
|
||||
}
|
||||
|
||||
// isReportGenerated 检查报表是否已生成
|
||||
func (s *StatReportScheduler) isReportGenerated(ctx context.Context, tenantID int64, reportType, date string) bool {
|
||||
report, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(tenantID, 10), reportType, date)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return report != nil
|
||||
}
|
||||
|
||||
// isQuarterFirstDay 检查是否是季度第一天
|
||||
func (s *StatReportScheduler) isQuarterFirstDay() bool {
|
||||
now := time.Now()
|
||||
month := now.Month()
|
||||
day := now.Day()
|
||||
|
||||
// 季度第一天:1月1日、4月1日、7月1日、10月1日
|
||||
return (month == time.January && day == 1) ||
|
||||
(month == time.April && day == 1) ||
|
||||
(month == time.July && day == 1) ||
|
||||
(month == time.October && day == 1)
|
||||
}
|
||||
|
||||
// getNextQuarterFirstDay 获取下个季度第一天
|
||||
func (s *StatReportScheduler) getNextQuarterFirstDay(now time.Time) time.Time {
|
||||
currentQuarter := (now.Month()-1)/3 + 1
|
||||
nextQuarter := currentQuarter + 1
|
||||
if nextQuarter > 4 {
|
||||
nextQuarter = 1
|
||||
now = now.AddDate(1, 0, 0)
|
||||
}
|
||||
|
||||
nextQuarterMonth := time.Month((nextQuarter-1)*3 + 1)
|
||||
return time.Date(now.Year(), nextQuarterMonth, 1, 0, 0, 0, 0, time.Local)
|
||||
}
|
||||
@@ -1,657 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type StatReportService struct{}
|
||||
|
||||
var StatReport = &StatReportService{}
|
||||
|
||||
// GenerateDailyReport 生成日报表(现在只用于手动触发,定时任务会自动生成)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已存在报表
|
||||
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "daily", reportDate.Format("2006-01-02"))
|
||||
if err == nil && existingReport != nil {
|
||||
// 返回已存在的报表
|
||||
var reportData map[string]interface{}
|
||||
if err := gconv.Struct(existingReport.ReportData, &reportData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: existingReport.Id.Hex(),
|
||||
ReportType: "daily",
|
||||
ReportDate: reportDate.Format("2006-01-02"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 生成日报表数据
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "daily", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存报表
|
||||
report := &entity.StatReport{
|
||||
AppID: strconv.FormatInt(req.AppID, 10),
|
||||
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.Hex(),
|
||||
ReportType: "daily",
|
||||
ReportDate: reportDate.Format("2006-01-02"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateMonthlyReport 生成月报表(现在优先使用预生成的报表)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已存在报表
|
||||
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "monthly", reportDate.Format("2006-01"))
|
||||
if err == nil && existingReport != nil {
|
||||
var reportData map[string]interface{}
|
||||
if err := gconv.Struct(existingReport.ReportData, &reportData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: existingReport.Id.Hex(),
|
||||
ReportType: "monthly",
|
||||
ReportDate: reportDate.Format("2006-01"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "monthly", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := &entity.StatReport{
|
||||
AppID: strconv.FormatInt(req.AppID, 10),
|
||||
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.Hex(),
|
||||
ReportType: "monthly",
|
||||
ReportDate: reportDate.Format("2006-01"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateWeeklyReport 生成周报表(新增周报表支持)
|
||||
func (s *StatReportService) GenerateWeeklyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||
reportDate := time.Now()
|
||||
if req.Date != "" {
|
||||
// 周报表格式:2024-W01
|
||||
parsedDate, err := time.Parse("2006-W01", req.Date)
|
||||
if err == nil {
|
||||
reportDate = parsedDate
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已存在报表
|
||||
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "weekly", reportDate.Format("2006-W01"))
|
||||
if err == nil && existingReport != nil {
|
||||
var reportData map[string]interface{}
|
||||
if err := gconv.Struct(existingReport.ReportData, &reportData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: existingReport.Id.Hex(),
|
||||
ReportType: "weekly",
|
||||
ReportDate: reportDate.Format("2006-W01"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "weekly", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := &entity.StatReport{
|
||||
AppID: strconv.FormatInt(req.AppID, 10),
|
||||
ReportType: "weekly",
|
||||
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.Hex(),
|
||||
ReportType: "weekly",
|
||||
ReportDate: reportDate.Format("2006-W01"),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已存在报表
|
||||
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "quarterly", reportDate.Format("2006-Q1"))
|
||||
if err == nil && existingReport != nil {
|
||||
var reportData map[string]interface{}
|
||||
if err := gconv.Struct(existingReport.ReportData, &reportData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: existingReport.Id.Hex(),
|
||||
ReportType: "quarterly",
|
||||
ReportDate: reportDate.Format("2006-Q1"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "quarterly", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := &entity.StatReport{
|
||||
AppID: strconv.FormatInt(req.AppID, 10),
|
||||
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.Hex(),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已存在报表
|
||||
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "yearly", reportDate.Format("2006"))
|
||||
if err == nil && existingReport != nil {
|
||||
var reportData map[string]interface{}
|
||||
if err := gconv.Struct(existingReport.ReportData, &reportData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ReportGenerateResp{
|
||||
ReportID: existingReport.Id.Hex(),
|
||||
ReportType: "yearly",
|
||||
ReportDate: reportDate.Format("2006"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "yearly", reportDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := &entity.StatReport{
|
||||
AppID: strconv.FormatInt(req.AppID, 10),
|
||||
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.Hex(),
|
||||
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}
|
||||
|
||||
// 查询基础统计数据
|
||||
// 这里简化实现,实际应该使用mongo查询ad_statistics集合
|
||||
// 由于ad_statistics可能不存在或需要重构,这里返回模拟数据
|
||||
stats := []map[string]interface{}{
|
||||
{
|
||||
"impressions": 1200,
|
||||
"clicks": 60,
|
||||
"revenue": 600.0,
|
||||
"ad_type": "banner",
|
||||
"region": "北京",
|
||||
"platform": "web",
|
||||
"play_duration": 30.5,
|
||||
},
|
||||
}
|
||||
|
||||
// 计算环比数据
|
||||
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, strconv.FormatInt(req.TenantID, 10), strconv.FormatInt(req.AppID, 10), 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 {
|
||||
appID, _ := strconv.ParseInt(report.AppID, 10, 64)
|
||||
// 使用ObjectId的十六进制字符串作为ID,在DTO中保持为字符串
|
||||
idStr := report.Id.Hex()
|
||||
|
||||
// 将ObjectId的十六进制字符串转换为int64,如果失败则使用0
|
||||
var idInt64 int64
|
||||
if id, err := strconv.ParseInt(idStr, 16, 64); err == nil {
|
||||
idInt64 = id
|
||||
}
|
||||
|
||||
reportDTOs = append(reportDTOs, &dto.ReportDTO{
|
||||
ID: idInt64,
|
||||
TenantID: report.TenantId,
|
||||
AppID: 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, strconv.FormatInt(reportID, 10))
|
||||
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
|
||||
}
|
||||
|
||||
appID, _ := strconv.ParseInt(report.AppID, 10, 64)
|
||||
idStr := report.Id.Hex()
|
||||
|
||||
// 将ObjectId的十六进制字符串转换为int64,如果失败则使用0
|
||||
var idInt64 int64
|
||||
if id, err := strconv.ParseInt(idStr, 16, 64); err == nil {
|
||||
idInt64 = id
|
||||
}
|
||||
|
||||
return &dto.ReportDetailResp{
|
||||
ID: idInt64,
|
||||
TenantID: report.TenantId,
|
||||
AppID: appID,
|
||||
ReportType: report.ReportType,
|
||||
ReportDate: report.ReportDate.Format("2006-01-02"),
|
||||
GeneratedAt: report.GeneratedAt.Format("2006-01-02 15:04:05"),
|
||||
Data: reportData,
|
||||
}, nil
|
||||
}
|
||||
@@ -48,9 +48,11 @@ func (s *strategyService) CreateStrategy(ctx context.Context, req *dto.CreateStr
|
||||
SourceWeights: string(weightsJson),
|
||||
MaxAdsPerReq: req.MaxAdsPerReq,
|
||||
Priority: req.Priority,
|
||||
Status: req.Status,
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
strategy.Status = req.Status
|
||||
|
||||
_, err = dao.Strategy.Create(ctx, strategy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -101,9 +103,11 @@ func (s *strategyService) UpdateStrategy(ctx context.Context, req *dto.UpdateStr
|
||||
SourceWeights: string(weightsJson),
|
||||
MaxAdsPerReq: req.MaxAdsPerReq,
|
||||
Priority: req.Priority,
|
||||
Status: req.Status,
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
strategy.Status = req.Status
|
||||
|
||||
return dao.Strategy.Update(ctx, strategy)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user