2025-12-06 15:24:30 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
2025-12-10 15:41:52 +08:00
|
|
|
|
"strconv"
|
2025-12-06 15:24:30 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-12-09 16:10:45 +08:00
|
|
|
|
"cid/dao"
|
|
|
|
|
|
"cid/model/dto"
|
|
|
|
|
|
"cid/model/entity"
|
2025-12-06 15:24:30 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
|
|
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type StatReportService struct{}
|
|
|
|
|
|
|
|
|
|
|
|
var StatReport = &StatReportService{}
|
|
|
|
|
|
|
2025-12-09 13:32:43 +08:00
|
|
|
|
// GenerateDailyReport 生成日报表(现在只用于手动触发,定时任务会自动生成)
|
2025-12-06 15:24:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 13:32:43 +08:00
|
|
|
|
// 检查是否已存在报表
|
2025-12-10 15:41:52 +08:00
|
|
|
|
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "daily", reportDate.Format("2006-01-02"))
|
2025-12-09 13:32:43 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
// 生成日报表数据
|
|
|
|
|
|
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "daily", reportDate)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存报表
|
|
|
|
|
|
report := &entity.StatReport{
|
|
|
|
|
|
TenantId: req.TenantID,
|
2025-12-10 15:41:52 +08:00
|
|
|
|
AppID: strconv.FormatInt(req.AppID, 10),
|
2025-12-06 15:24:30 +08:00
|
|
|
|
ReportType: "daily",
|
|
|
|
|
|
ReportDate: reportDate,
|
|
|
|
|
|
ReportData: gconv.String(reportData),
|
|
|
|
|
|
GeneratedAt: time.Now(),
|
|
|
|
|
|
Status: "completed",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
err = dao.StatReport.Create(ctx, report)
|
2025-12-06 15:24:30 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &dto.ReportGenerateResp{
|
|
|
|
|
|
ReportID: report.Id,
|
|
|
|
|
|
ReportType: "daily",
|
|
|
|
|
|
ReportDate: reportDate.Format("2006-01-02"),
|
|
|
|
|
|
Data: reportData,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 13:32:43 +08:00
|
|
|
|
// GenerateMonthlyReport 生成月报表(现在优先使用预生成的报表)
|
2025-12-06 15:24:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 13:32:43 +08:00
|
|
|
|
// 检查是否已存在报表
|
2025-12-10 15:41:52 +08:00
|
|
|
|
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "monthly", reportDate.Format("2006-01"))
|
2025-12-09 13:32:43 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "monthly", reportDate)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
report := &entity.StatReport{
|
|
|
|
|
|
TenantId: req.TenantID,
|
2025-12-10 15:41:52 +08:00
|
|
|
|
AppID: strconv.FormatInt(req.AppID, 10),
|
2025-12-06 15:24:30 +08:00
|
|
|
|
ReportType: "monthly",
|
|
|
|
|
|
ReportDate: reportDate,
|
|
|
|
|
|
ReportData: gconv.String(reportData),
|
|
|
|
|
|
GeneratedAt: time.Now(),
|
|
|
|
|
|
Status: "completed",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
err = dao.StatReport.Create(ctx, report)
|
2025-12-06 15:24:30 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &dto.ReportGenerateResp{
|
|
|
|
|
|
ReportID: report.Id,
|
|
|
|
|
|
ReportType: "monthly",
|
|
|
|
|
|
ReportDate: reportDate.Format("2006-01"),
|
|
|
|
|
|
Data: reportData,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 13:32:43 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已存在报表
|
2025-12-10 15:41:52 +08:00
|
|
|
|
existingReport, err := dao.StatReport.GetByTenantAndDate(ctx, strconv.FormatInt(req.TenantID, 10), "weekly", reportDate.Format("2006-W01"))
|
2025-12-09 13:32:43 +08:00
|
|
|
|
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,
|
2025-12-10 15:41:52 +08:00
|
|
|
|
AppID: strconv.FormatInt(req.AppID, 10),
|
2025-12-09 13:32:43 +08:00
|
|
|
|
ReportType: "weekly",
|
|
|
|
|
|
ReportDate: reportDate,
|
|
|
|
|
|
ReportData: gconv.String(reportData),
|
|
|
|
|
|
GeneratedAt: time.Now(),
|
|
|
|
|
|
Status: "completed",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
err = dao.StatReport.Create(ctx, report)
|
2025-12-09 13:32:43 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &dto.ReportGenerateResp{
|
|
|
|
|
|
ReportID: report.Id,
|
|
|
|
|
|
ReportType: "weekly",
|
|
|
|
|
|
ReportDate: reportDate.Format("2006-W01"),
|
|
|
|
|
|
Data: reportData,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
// 生成季度报表
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
// 检查是否已存在报表
|
|
|
|
|
|
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,
|
|
|
|
|
|
ReportType: "quarterly",
|
|
|
|
|
|
ReportDate: reportDate.Format("2006-Q1"),
|
|
|
|
|
|
Data: reportData,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "quarterly", reportDate)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
report := &entity.StatReport{
|
|
|
|
|
|
TenantId: req.TenantID,
|
2025-12-10 15:41:52 +08:00
|
|
|
|
AppID: strconv.FormatInt(req.AppID, 10),
|
2025-12-06 15:24:30 +08:00
|
|
|
|
ReportType: "quarterly",
|
|
|
|
|
|
ReportDate: reportDate,
|
|
|
|
|
|
ReportData: gconv.String(reportData),
|
|
|
|
|
|
GeneratedAt: time.Now(),
|
|
|
|
|
|
Status: "completed",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
err = dao.StatReport.Create(ctx, report)
|
2025-12-06 15:24:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
// 检查是否已存在报表
|
|
|
|
|
|
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,
|
|
|
|
|
|
ReportType: "yearly",
|
|
|
|
|
|
ReportDate: reportDate.Format("2006"),
|
|
|
|
|
|
Data: reportData,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "yearly", reportDate)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
report := &entity.StatReport{
|
|
|
|
|
|
TenantId: req.TenantID,
|
2025-12-10 15:41:52 +08:00
|
|
|
|
AppID: strconv.FormatInt(req.AppID, 10),
|
2025-12-06 15:24:30 +08:00
|
|
|
|
ReportType: "yearly",
|
|
|
|
|
|
ReportDate: reportDate,
|
|
|
|
|
|
ReportData: gconv.String(reportData),
|
|
|
|
|
|
GeneratedAt: time.Now(),
|
|
|
|
|
|
Status: "completed",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
err = dao.StatReport.Create(ctx, report)
|
2025-12-06 15:24:30 +08:00
|
|
|
|
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}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询基础统计数据
|
2025-12-10 15:41:52 +08:00
|
|
|
|
// 这里简化实现,实际应该使用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,
|
|
|
|
|
|
},
|
2025-12-06 15:24:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算环比数据
|
|
|
|
|
|
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方法
|
2025-12-10 15:41:52 +08:00
|
|
|
|
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)
|
2025-12-06 15:24:30 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为DTO
|
|
|
|
|
|
var reportDTOs []*dto.ReportDTO
|
|
|
|
|
|
for _, report := range reports {
|
2025-12-10 15:41:52 +08:00
|
|
|
|
appID, _ := strconv.ParseInt(report.AppID, 10, 64)
|
|
|
|
|
|
id, _ := strconv.ParseInt(report.Id, 10, 64)
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
reportDTOs = append(reportDTOs, &dto.ReportDTO{
|
2025-12-10 15:41:52 +08:00
|
|
|
|
ID: id,
|
2025-12-06 15:24:30 +08:00
|
|
|
|
TenantID: report.TenantId,
|
2025-12-10 15:41:52 +08:00
|
|
|
|
AppID: appID,
|
2025-12-06 15:24:30 +08:00
|
|
|
|
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
|
2025-12-10 15:41:52 +08:00
|
|
|
|
report, err := dao.StatReport.GetByID(ctx, strconv.FormatInt(reportID, 10))
|
2025-12-06 15:24:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 15:41:52 +08:00
|
|
|
|
appID, _ := strconv.ParseInt(report.AppID, 10, 64)
|
|
|
|
|
|
id, _ := strconv.ParseInt(report.Id, 10, 64)
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
return &dto.ReportDetailResp{
|
2025-12-10 15:41:52 +08:00
|
|
|
|
ID: id,
|
2025-12-06 15:24:30 +08:00
|
|
|
|
TenantID: report.TenantId,
|
2025-12-10 15:41:52 +08:00
|
|
|
|
AppID: appID,
|
2025-12-06 15:24:30 +08:00
|
|
|
|
ReportType: report.ReportType,
|
|
|
|
|
|
ReportDate: report.ReportDate.Format("2006-01-02"),
|
|
|
|
|
|
GeneratedAt: report.GeneratedAt.Format("2006-01-02 15:04:05"),
|
|
|
|
|
|
Data: reportData,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|