603 lines
17 KiB
Go
603 lines
17 KiB
Go
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{}
|
||
|
||
// 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, req.TenantID, "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,
|
||
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{
|
||
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
|
||
}
|
||
|
||
// 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, req.TenantID, "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,
|
||
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{
|
||
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
|
||
}
|
||
|
||
// 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, req.TenantID, "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,
|
||
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{
|
||
TenantId: req.TenantID,
|
||
AppID: req.AppID,
|
||
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,
|
||
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
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|