初始化项目

This commit is contained in:
2025-12-09 13:32:43 +08:00
parent 46af4c6734
commit 2ccbf71b60
19 changed files with 784 additions and 621 deletions

View File

@@ -111,10 +111,9 @@ func (s *adPosition) MatchAd(ctx context.Context, positionCode string, userInfo
// 返回匹配的广告
// 这里返回第一个广告作为示例
ad = &entity.Advertisement{
Title: "示例广告",
MaterialUrl: "https://example.com/ad.jpg",
LinkUrl: "https://example.com",
LandingPageUrl: "https://example.com/landing",
Title: "示例广告",
MaterialUrl: "https://example.com/ad.jpg",
TargetUrl: "https://example.com",
}
return

View File

@@ -29,17 +29,12 @@ func (s *advertisement) Add(ctx context.Context, req *dto.AddAdvertisementReq) (
// 设置初始状态
advertisement.Status = "待审核"
advertisement.AuditStatus = "待审核"
// 初始化统计字段
advertisement.Impressions = 0
advertisement.Clicks = 0
advertisement.Conversions = 0
advertisement.Cost = 0
advertisement.CTR = 0
advertisement.CVR = 0
advertisement.CPM = 0
advertisement.CPC = 0
if err = dao.Advertisement.Insert(ctx, advertisement); err != nil {
return
@@ -133,10 +128,6 @@ func (s *advertisement) UpdateAdStatistics(ctx context.Context, id string, impre
"clicks": totalClicks,
"conversions": totalConversions,
"cost": totalCost,
"ctr": ctr,
"cvr": cvr,
"cpm": cpm,
"cpc": cpc,
"updatedAt": time.Now(),
}

View File

@@ -31,7 +31,6 @@ func (s *advertiser) Add(ctx context.Context, req *dto.AddAdvertiserReq) (res *d
// 设置初始状态
advertiser.Status = "待审核"
advertiser.AuditStatus = "待审核"
if err = dao.Advertiser.Insert(ctx, advertiser); err != nil {
return

View File

@@ -1,183 +0,0 @@
package service
import (
"context"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"cidservice/dao"
"cidservice/model/dto"
"cidservice/model/entity"
)
// Report Service 单例
var Report = new(report)
type report struct{}
// Create 创建报表
func (s *report) Create(ctx context.Context, req *dto.CreateReportReq) (res *dto.CreateReportRes, err error) {
data := &entity.AdReport{
ReportName: req.ReportName,
ReportType: req.ReportType,
ReportPeriod: req.ReportPeriod,
StartDate: req.StartDate,
EndDate: req.EndDate,
Status: "生成中",
GenerateTime: time.Now().Unix(),
FileFormat: req.FileFormat,
EmailRecipients: req.EmailRecipients,
Schedule: req.Schedule,
}
// 存储报表配置
if req.ReportConfig != nil {
data.ReportData = []entity.ReportItem{
{
Dimension: "config",
Data: req.ReportConfig,
},
}
}
if err = dao.Report.Insert(ctx, data); err != nil {
return nil, err
}
// 异步生成报表
go s.generateReport(data.Id.Hex(), req)
res = &dto.CreateReportRes{Id: data.Id.Hex()}
return
}
// generateReport 生成报表
func (s *report) generateReport(reportId string, req *dto.CreateReportReq) {
// 模拟生成报表
time.Sleep(5 * time.Second)
// 更新报表状态
ctx := context.Background()
updateReq := &dto.UpdateReportReq{
Id: reportId,
FileFormat: req.FileFormat,
EmailRecipients: req.EmailRecipients,
}
err := dao.Report.Update(ctx, updateReq)
if err != nil {
// 记录错误日志,这里简化处理
// 实际项目中应该使用日志框架
}
// 发送邮件通知(如果配置了邮件接收人)
if len(req.EmailRecipients) > 0 {
// 发送邮件
// 这里简化处理,实际项目中应该使用邮件服务
}
}
// GetOne 获取报表详情
func (s *report) GetOne(ctx context.Context, req *dto.GetReportReq) (res *dto.GetReportRes, err error) {
data, err := dao.Report.GetOne(ctx, req.Id)
if err != nil {
return nil, err
}
res = &dto.GetReportRes{
AdReport: data,
}
return
}
// List 获取报表列表
func (s *report) List(ctx context.Context, req *dto.ListReportReq) (res *dto.ListReportRes, err error) {
list, total, err := dao.Report.List(ctx, req)
if err != nil {
return nil, err
}
res = &dto.ListReportRes{
List: list,
Total: int(total),
}
return
}
// Update 更新报表
func (s *report) Update(ctx context.Context, req *dto.UpdateReportReq) (err error) {
return dao.Report.Update(ctx, req)
}
// Delete 删除报表
func (s *report) Delete(ctx context.Context, req *dto.DeleteReportReq) (err error) {
return dao.Report.Delete(ctx, req.Id)
}
// Download 下载报表
func (s *report) Download(ctx context.Context, req *dto.DownloadReportReq) (res *dto.DownloadReportRes, err error) {
data, err := dao.Report.GetOne(ctx, req.Id)
if err != nil {
return nil, err
}
// 检查报表状态
if data.Status != "已完成" {
return nil, gerror.New("报表尚未生成完成")
}
// 检查报表是否过期
if data.ExpiredTime > 0 && data.ExpiredTime < time.Now().Unix() {
return nil, gerror.New("报表已过期")
}
res = &dto.DownloadReportRes{
DownloadUrl: data.DownloadUrl,
FileSize: data.FileSize,
FileFormat: data.FileFormat,
}
return
}
// Generate 生成报表
func (s *report) Generate(ctx context.Context, req *dto.GenerateReportReq) (err error) {
data, err := dao.Report.GetOne(ctx, req.Id)
if err != nil {
return err
}
// 构建创建报表请求
createReq := &dto.CreateReportReq{
ReportName: data.ReportName,
ReportType: data.ReportType,
ReportPeriod: data.ReportPeriod,
StartDate: data.StartDate,
EndDate: data.EndDate,
FileFormat: data.FileFormat,
EmailRecipients: data.EmailRecipients,
Schedule: data.Schedule,
}
// 从ReportData中获取报表配置
if len(data.ReportData) > 0 {
for _, item := range data.ReportData {
if item.Dimension == "config" {
createReq.ReportConfig = item.Data
break
}
}
}
// 异步生成报表
go s.generateReport(req.Id, createReq)
// 更新状态
updateReq := &dto.UpdateReportReq{
Id: req.Id,
}
err = dao.Report.Update(ctx, updateReq)
return
}

View File

@@ -0,0 +1,634 @@
package service
import (
"context"
"fmt"
"sync"
"time"
"cidservice/dao"
"cidservice/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{
TenantId: tenantID,
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{
TenantId: tenantID,
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{
TenantId: tenantID,
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{
TenantId: tenantID,
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, tenantID, 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, tenantID, "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, tenantID, "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, tenantID, 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)
}

View File

@@ -17,7 +17,7 @@ type StatReportService struct{}
var StatReport = &StatReportService{}
// 生成日报表
// GenerateDailyReport 生成日报表(现在只用于手动触发,定时任务会自动生成)
func (s *StatReportService) GenerateDailyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
// 获取统计日期
reportDate := time.Now()
@@ -28,6 +28,23 @@ func (s *StatReportService) GenerateDailyReport(ctx context.Context, req *dto.Re
}
}
// 检查是否已存在报表
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 {
@@ -58,7 +75,7 @@ func (s *StatReportService) GenerateDailyReport(ctx context.Context, req *dto.Re
}, nil
}
// 生成月报表
// GenerateMonthlyReport 生成月报表(现在优先使用预生成的报表)
func (s *StatReportService) GenerateMonthlyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
reportDate := time.Now()
if req.Date != "" {
@@ -68,6 +85,22 @@ func (s *StatReportService) GenerateMonthlyReport(ctx context.Context, req *dto.
}
}
// 检查是否已存在报表
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
@@ -96,6 +129,61 @@ func (s *StatReportService) GenerateMonthlyReport(ctx context.Context, req *dto.
}, 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()