515 lines
14 KiB
Go
515 lines
14 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{}
|
|
|
|
// 生成日报表
|
|
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
|
|
}
|