From 2ccbf71b6037a3f1bbc67a4638233ba2b2247b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Tue, 9 Dec 2025 13:32:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.yml | 9 +- controller/report_controller.go | 55 --- controller/stat_report_controller.go | 5 +- dao/advertisement_dao.go | 7 +- dao/report_dao.go | 171 -------- main.go | 16 +- model/dto/advertisement_dto.go | 38 +- model/dto/report_dto.go | 104 ----- model/dto/stat_report_dto.go | 2 +- model/entity/advertisement.go | 34 +- model/entity/advertiser.go | 3 +- model/entity/stat_report.go | 2 +- service/ad_position_service.go | 7 +- service/advertisement_service.go | 9 - service/advertiser_service.go | 1 - service/report_service.go | 183 -------- service/stat_report_scheduler.go | 634 +++++++++++++++++++++++++++ service/stat_report_service.go | 92 +++- test_config.go | 33 -- 19 files changed, 784 insertions(+), 621 deletions(-) delete mode 100644 controller/report_controller.go delete mode 100644 dao/report_dao.go delete mode 100644 model/dto/report_dto.go delete mode 100644 service/report_service.go create mode 100644 service/stat_report_scheduler.go delete mode 100644 test_config.go diff --git a/config.yml b/config.yml index 1ea07d4..0f82fa4 100644 --- a/config.yml +++ b/config.yml @@ -1,5 +1,5 @@ server: - address : ":3004" + address : ":3001" name: "cidService" jwt: secret: "abcdefghijklmnopqrstuvwxyz" @@ -39,9 +39,4 @@ rabbitMQ: username: guest # 默认用户名 password: guest # 默认密码 jaeger: #链路追踪 - addr: 192.168.3.200:4318 - -# RAGFlow 智能客服配置 -ragflow: - base_url: "http://192.168.3.200:9380" # RAGFlow 服务地址 - api_key: "ragflow-your-api-key-here" # RAGFlow API Key(登录 RAGFlow 管理界面 -> 设置 -> API Keys) \ No newline at end of file + addr: 192.168.3.200:4318 \ No newline at end of file diff --git a/controller/report_controller.go b/controller/report_controller.go deleted file mode 100644 index 8c58610..0000000 --- a/controller/report_controller.go +++ /dev/null @@ -1,55 +0,0 @@ -package controller - -import ( - "context" - - "gitee.com/red-future---jilin-g/common/http" - - "cidservice/model/dto" - "cidservice/service" -) - -type report struct{} - -var Report = new(report) - -// Create 创建报表 -func (c *report) Create(ctx context.Context, req *dto.CreateReportReq) (res *dto.CreateReportRes, err error) { - return service.Report.Create(ctx, req) -} - -// GetOne 获取报表详情 -func (c *report) GetOne(ctx context.Context, req *dto.GetReportReq) (res *dto.GetReportRes, err error) { - return service.Report.GetOne(ctx, req) -} - -// List 获取报表列表 -func (c *report) List(ctx context.Context, req *dto.ListReportReq) (res *dto.ListReportRes, err error) { - return service.Report.List(ctx, req) -} - -// Update 更新报表 -func (c *report) Update(ctx context.Context, req *dto.UpdateReportReq) (res *http.ResponseEmpty, err error) { - err = service.Report.Update(ctx, req) - res = &http.ResponseEmpty{} - return -} - -// Delete 删除报表 -func (c *report) Delete(ctx context.Context, req *dto.DeleteReportReq) (res *http.ResponseEmpty, err error) { - err = service.Report.Delete(ctx, req) - res = &http.ResponseEmpty{} - return -} - -// Download 下载报表 -func (c *report) Download(ctx context.Context, req *dto.DownloadReportReq) (res *dto.DownloadReportRes, err error) { - return service.Report.Download(ctx, req) -} - -// Generate 生成报表 -func (c *report) Generate(ctx context.Context, req *dto.GenerateReportReq) (res *http.ResponseEmpty, err error) { - err = service.Report.Generate(ctx, req) - res = &http.ResponseEmpty{} - return -} diff --git a/controller/stat_report_controller.go b/controller/stat_report_controller.go index fd7798c..4f2000f 100644 --- a/controller/stat_report_controller.go +++ b/controller/stat_report_controller.go @@ -2,6 +2,7 @@ package controller import ( "context" + "fmt" "cidservice/model/dto" "cidservice/service" @@ -18,6 +19,8 @@ func (c *statReport) GenerateReport(ctx context.Context, req *dto.ReportGenerate switch req.ReportType { case "daily": resp, err = service.StatReport.GenerateDailyReport(ctx, req) + case "weekly": + resp, err = service.StatReport.GenerateWeeklyReport(ctx, req) case "monthly": resp, err = service.StatReport.GenerateMonthlyReport(ctx, req) case "quarterly": @@ -25,7 +28,7 @@ func (c *statReport) GenerateReport(ctx context.Context, req *dto.ReportGenerate case "yearly": resp, err = service.StatReport.GenerateYearlyReport(ctx, req) default: - return nil, err + return nil, fmt.Errorf("不支持的报表类型: %s", req.ReportType) } if err != nil { diff --git a/dao/advertisement_dao.go b/dao/advertisement_dao.go index 148dd72..583c103 100644 --- a/dao/advertisement_dao.go +++ b/dao/advertisement_dao.go @@ -66,11 +66,8 @@ func (d *advertisement) Update(ctx context.Context, req *dto.UpdateAdvertisement if !g.IsEmpty(req.MaterialUrl) { updateFields["materialUrl"] = req.MaterialUrl } - if !g.IsEmpty(req.LinkUrl) { - updateFields["linkUrl"] = req.LinkUrl - } - if !g.IsEmpty(req.LandingPageUrl) { - updateFields["landingPageUrl"] = req.LandingPageUrl + if !g.IsEmpty(req.TargetUrl) { + updateFields["targetUrl"] = req.TargetUrl } // 投放设置 diff --git a/dao/report_dao.go b/dao/report_dao.go deleted file mode 100644 index e29767f..0000000 --- a/dao/report_dao.go +++ /dev/null @@ -1,171 +0,0 @@ -package dao - -import ( - "context" - - "cidservice/model/dto" - "cidservice/model/entity" - - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/util/gconv" - "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/mongo/options" - - "gitee.com/red-future---jilin-g/common/http" - "gitee.com/red-future---jilin-g/common/mongo" -) - -// Report DAO 单例 -var Report = &report{} - -type report struct{} - -// Insert 插入报表 -func (d *report) Insert(ctx context.Context, report *entity.AdReport) (err error) { - // 如果 ID 为空,生成一个新的 ObjectID - if report.Id.IsZero() { - report.Id = bson.NewObjectID() - } - _, err = mongo.Insert(ctx, []interface{}{report}, entity.AdReportCollection) - return -} - -// Update 更新报表 -func (d *report) Update(ctx context.Context, req *dto.UpdateReportReq) (err error) { - objectId, err := bson.ObjectIDFromHex(req.Id) - if err != nil { - return - } - filter := bson.M{"_id": objectId} - - // 构建动态更新字段 - updateFields := bson.M{} - - if !g.IsEmpty(req.ReportName) { - updateFields["reportName"] = req.ReportName - } - if !g.IsEmpty(req.ReportType) { - updateFields["reportType"] = req.ReportType - } - if !g.IsEmpty(req.ReportPeriod) { - updateFields["reportPeriod"] = req.ReportPeriod - } - if req.StartDate != nil { - updateFields["startDate"] = *req.StartDate - } - if req.EndDate != nil { - updateFields["endDate"] = *req.EndDate - } - if req.ReportConfig != nil { - updateFields["reportConfig"] = req.ReportConfig - } - if !g.IsEmpty(req.FileFormat) { - updateFields["fileFormat"] = req.FileFormat - } - if req.EmailRecipients != nil { - updateFields["emailRecipients"] = req.EmailRecipients - } - if !g.IsEmpty(req.Schedule) { - updateFields["schedule"] = req.Schedule - } - - if len(updateFields) > 0 { - update := bson.M{"$set": updateFields} - _, err = mongo.Update(ctx, filter, update, entity.AdReportCollection) - } - return -} - -// Delete 删除报表 -func (d *report) Delete(ctx context.Context, id string) (err error) { - objectId, err := bson.ObjectIDFromHex(id) - if err != nil { - return - } - filter := bson.M{"_id": objectId} - - _, err = mongo.Delete(ctx, filter, entity.AdReportCollection) - return -} - -// GetOne 获取单个报表 -func (d *report) GetOne(ctx context.Context, id string) (result *entity.AdReport, err error) { - objectId, err := bson.ObjectIDFromHex(id) - if err != nil { - return - } - filter := bson.M{"_id": objectId} - - result = &entity.AdReport{} - err = mongo.FindOne(ctx, filter, result, entity.AdReportCollection) - return -} - -// buildReportListFilter 构建报表列表查询的过滤条件 -func (d *report) buildReportListFilter(req *dto.ListReportReq) bson.M { - filter := bson.M{} - - if !g.IsEmpty(req.ReportName) { - filter["reportName"] = bson.M{"$regex": req.ReportName, "$options": "i"} - } - if !g.IsEmpty(req.ReportType) { - filter["reportType"] = req.ReportType - } - if !g.IsEmpty(req.Status) { - filter["status"] = req.Status - } - if !g.IsEmpty(req.Operator) { - filter["operator"] = req.Operator - } - - // 处理日期范围 - if len(req.DateRange) == 2 { - startTime := gconv.Int64(req.DateRange[0]) - endTime := gconv.Int64(req.DateRange[1]) - filter["createdAt"] = bson.M{ - "$gte": startTime, - "$lte": endTime, - } - } - - return filter -} - -// checkReportTotalCount 检查报表总数 -func (d *report) checkReportTotalCount(ctx context.Context, filter bson.M) (total int64, err error) { - total, err = mongo.Count(ctx, filter, entity.AdReportCollection) - return -} - -// List 获取报表列表 -func (d *report) List(ctx context.Context, req *dto.ListReportReq) (list []*entity.AdReport, total int64, err error) { - // 构建查询过滤条件 - filter := d.buildReportListFilter(req) - - // 检查总数 - total, err = d.checkReportTotalCount(ctx, filter) - if err != nil { - return - } - - // 分页参数处理 - pageNum := req.PageNum - if pageNum <= 0 { - pageNum = 1 - } - pageSize := req.PageSize - if pageSize <= 0 { - pageSize = http.PageSize - } - - limit := int64(pageSize) - skip := int64((pageNum - 1) * pageSize) - - // 排序处理 - sort := bson.M{"createdAt": -1} - - opts := options.Find().SetLimit(limit).SetSkip(skip).SetSort(sort) - - err = mongo.Find(ctx, filter, &list, entity.AdReportCollection, opts) - return -} diff --git a/main.go b/main.go index 0cc80a1..83d6c86 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,9 @@ package main import ( "cidservice/controller" + "cidservice/service" + "fmt" + "time" "gitee.com/red-future---jilin-g/common/http" "gitee.com/red-future---jilin-g/common/jaeger" @@ -13,13 +16,22 @@ import ( ) func main() { - defer jaeger.ShutDown(context.Background()) + ctx := context.Background() + defer jaeger.ShutDown(ctx) + + // 启动统计报表定时任务调度器 + go func() { + time.Sleep(5 * time.Second) // 等待数据库连接初始化完成 + if err := service.StatReportSchedulerInstance.StartScheduler(ctx); err != nil { + fmt.Printf("启动统计报表定时任务失败: %v\n", err) + } + }() + http.RouteRegister([]interface{}{ controller.Advertisement, controller.Advertiser, controller.AdPosition, controller.AdStatistics, - controller.Report, controller.RateLimit, controller.Application, controller.StatReport, diff --git a/model/dto/advertisement_dto.go b/model/dto/advertisement_dto.go index 9217c20..333ebfa 100644 --- a/model/dto/advertisement_dto.go +++ b/model/dto/advertisement_dto.go @@ -12,15 +12,14 @@ type AddAdvertisementReq struct { g.Meta `path:"/add" method:"post" tags:"广告管理" summary:"添加广告" dc:"添加新的广告"` // 广告基本信息 - Title string `json:"title" v:"required"` // 广告标题 - Description string `json:"description"` // 广告描述 - AdvertiserId string `json:"advertiserId" v:"required"` // 广告主ID - AdPositionId string `json:"adPositionId" v:"required"` // 广告位ID - AdType string `json:"adType" v:"required"` // 广告类型:图片、视频、文字等 - AdFormat string `json:"adFormat" v:"required"` // 广告格式 - MaterialUrl string `json:"materialUrl" v:"required"` // 广告素材URL - LinkUrl string `json:"linkUrl"` // 点击跳转链接 - LandingPageUrl string `json:"landingPageUrl"` // 落地页URL + Title string `json:"title" v:"required"` // 广告标题 + Description string `json:"description"` // 广告描述 + AdvertiserId string `json:"advertiserId" v:"required"` // 广告主ID + AdPositionId string `json:"adPositionId" v:"required"` // 广告位ID + AdType string `json:"adType" v:"required"` // 广告类型:图片、视频、文字等 + AdFormat string `json:"adFormat" v:"required"` // 广告格式 + MaterialUrl string `json:"materialUrl" v:"required"` // 广告素材URL + TargetUrl string `json:"targetUrl" v:"required"` // 目标链接 // 投放设置 StartDate int64 `json:"startDate" v:"required"` // 开始投放时间 @@ -45,15 +44,14 @@ type UpdateAdvertisementReq struct { Id string `json:"id" v:"required"` // ID // 广告基本信息 - Title string `json:"title"` // 广告标题 - Description string `json:"description"` // 广告描述 - AdvertiserId string `json:"advertiserId"` // 广告主ID - AdPositionId string `json:"adPositionId"` // 广告位ID - AdType string `json:"adType"` // 广告类型:图片、视频、文字等 - AdFormat string `json:"adFormat"` // 广告格式 - MaterialUrl string `json:"materialUrl"` // 广告素材URL - LinkUrl string `json:"linkUrl"` // 点击跳转链接 - LandingPageUrl string `json:"landingPageUrl"` // 落地页URL + Title string `json:"title"` // 广告标题 + Description string `json:"description"` // 广告描述 + AdvertiserId string `json:"advertiserId"` // 广告主ID + AdPositionId string `json:"adPositionId"` // 广告位ID + AdType string `json:"adType"` // 广告类型:图片、视频、文字等 + AdFormat string `json:"adFormat"` // 广告格式 + MaterialUrl string `json:"materialUrl"` // 广告素材URL + TargetUrl string `json:"targetUrl"` // 目标链接 // 投放设置 StartDate *int64 `json:"startDate"` // 开始投放时间 @@ -67,8 +65,8 @@ type UpdateAdvertisementReq struct { Targeting *entity.Targeting `json:"targeting"` // 定向条件 // 状态信息 - Status *string `json:"status"` // 广告状态:待审核、已审核、已拒绝、投放中、已暂停、已结束 - AuditStatus *string `json:"auditStatus"` // 审核状态 + Status *string `json:"status"` // 广告状态:待审核、审核中、已通过、已拒绝、投放中、已暂停、已结束 + AuditStatus *string `json:"auditStatus"` // 审核状态:通过、拒绝 AuditReason *string `json:"auditReason"` // 审核不通过原因 } diff --git a/model/dto/report_dto.go b/model/dto/report_dto.go deleted file mode 100644 index 1ea3045..0000000 --- a/model/dto/report_dto.go +++ /dev/null @@ -1,104 +0,0 @@ -package dto - -import ( - "cidservice/model/entity" - - "gitee.com/red-future---jilin-g/common/http" - "github.com/gogf/gf/v2/frame/g" -) - -// CreateReportReq 创建报表请求 -type CreateReportReq struct { - g.Meta `path:"/create" method:"post" tags:"广告报表" summary:"创建报表" dc:"创建新的广告报表"` - - // 报表信息 - ReportName string `json:"reportName" v:"required"` // 报表名称 - ReportType string `json:"reportType" v:"required"` // 报表类型:日报、周报、月报、自定义 - ReportPeriod string `json:"reportPeriod" v:"required"` // 报表周期 - StartDate int64 `json:"startDate" v:"required"` // 开始日期 - EndDate int64 `json:"endDate" v:"required"` // 结束日期 - ReportConfig map[string]interface{} `json:"reportConfig"` // 报表配置 - - // 其他信息 - FileFormat string `json:"fileFormat"` // 文件格式:CSV、Excel、PDF - EmailRecipients []string `json:"emailRecipients"` // 邮件接收人列表 - Schedule string `json:"schedule"` // 定时设置 -} - -type CreateReportRes struct { - Id string `json:"id"` -} - -// GetReportReq 获取报表详情请求 -type GetReportReq struct { - g.Meta `path:"/getOne" method:"get" tags:"广告报表" summary:"获取报表详情" dc:"根据ID获取单个报表详情"` - Id string `json:"id" v:"required"` // ID -} - -type GetReportRes struct { - *entity.AdReport -} - -// ListReportReq 获取报表列表请求 -type ListReportReq struct { - g.Meta `path:"/list" method:"get" tags:"广告报表" summary:"获取报表列表" dc:"分页查询报表列表,支持多条件筛选"` - http.Page - - ReportName string `json:"reportName"` // 报表名称模糊查询 - ReportType string `json:"reportType"` // 报表类型 - Status string `json:"status"` // 报表状态 - Operator string `json:"operator"` // 操作人 - DateRange []string `json:"dateRange"` // 创建时间范围 [start, end] -} - -type ListReportRes struct { - List []*entity.AdReport `json:"list"` - Total int `json:"total"` -} - -// UpdateReportReq 更新报表请求 -type UpdateReportReq struct { - g.Meta `path:"/update" method:"post" tags:"广告报表" summary:"更新报表" dc:"更新报表信息"` - - Id string `json:"id" v:"required"` // ID - - // 报表信息 - ReportName string `json:"reportName"` // 报表名称 - ReportType string `json:"reportType"` // 报表类型:日报、周报、月报、自定义 - ReportPeriod string `json:"reportPeriod"` // 报表周期 - StartDate *int64 `json:"startDate"` // 开始日期 - EndDate *int64 `json:"endDate"` // 结束日期 - ReportConfig map[string]interface{} `json:"reportConfig"` // 报表配置 - - // 其他信息 - FileFormat string `json:"fileFormat"` // 文件格式:CSV、Excel、PDF - EmailRecipients []string `json:"emailRecipients"` // 邮件接收人列表 - Schedule string `json:"schedule"` // 定时设置 -} - -// DeleteReportReq 删除报表请求 -type DeleteReportReq struct { - g.Meta `path:"/delete" method:"post" tags:"广告报表" summary:"删除报表" dc:"删除指定的报表"` - - Id string `json:"id" v:"required"` // 报表ID -} - -// DownloadReportReq 下载报表请求 -type DownloadReportReq struct { - g.Meta `path:"/download" method:"get" tags:"广告报表" summary:"下载报表" dc:"下载指定的报表文件"` - - Id string `json:"id" v:"required"` // 报表ID -} - -type DownloadReportRes struct { - DownloadUrl string `json:"downloadUrl"` // 下载链接 - FileSize int64 `json:"fileSize"` // 文件大小(字节) - FileFormat string `json:"fileFormat"` // 文件格式 -} - -// GenerateReportReq 生成报表请求 -type GenerateReportReq struct { - g.Meta `path:"/generate" method:"post" tags:"广告报表" summary:"生成报表" dc:"手动生成报表"` - - Id string `json:"id" v:"required"` // 报表ID -} diff --git a/model/dto/stat_report_dto.go b/model/dto/stat_report_dto.go index 0fb720f..53a30bf 100644 --- a/model/dto/stat_report_dto.go +++ b/model/dto/stat_report_dto.go @@ -7,7 +7,7 @@ type ReportGenerateReq struct { g.Meta `path:"/generateReport" method:"post"` TenantID int64 `json:"tenant_id" v:"required"` AppID int64 `json:"app_id"` - ReportType string `json:"report_type" v:"required|in:daily,monthly,quarterly,yearly"` + ReportType string `json:"report_type" v:"required|in:daily,weekly,monthly,quarterly,yearly"` Date string `json:"date"` // 格式: 2024-01-01 (daily), 2024-01 (monthly), 2024-Q1 (quarterly), 2024 (yearly) } diff --git a/model/entity/advertisement.go b/model/entity/advertisement.go index ae7aeaf..963a51d 100644 --- a/model/entity/advertisement.go +++ b/model/entity/advertisement.go @@ -11,15 +11,14 @@ type Advertisement struct { do.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted // 广告基本信息 - Title string `bson:"title" json:"title"` // 广告标题 - Description string `bson:"description" json:"description"` // 广告描述 - AdvertiserId string `bson:"advertiserId" json:"advertiserId"` // 广告主ID - AdPositionId string `bson:"adPositionId" json:"adPositionId"` // 广告位ID - AdType string `bson:"adType" json:"adType"` // 广告类型:图片、视频、文字等 - AdFormat string `bson:"adFormat" json:"adFormat"` // 广告格式 - MaterialUrl string `bson:"materialUrl" json:"materialUrl"` // 广告素材URL - LinkUrl string `bson:"linkUrl" json:"linkUrl"` // 点击跳转链接 - LandingPageUrl string `bson:"landingPageUrl" json:"landingPageUrl"` // 落地页URL + Title string `bson:"title" json:"title"` // 广告标题 + Description string `bson:"description" json:"description"` // 广告描述 + AdvertiserId string `bson:"advertiserId" json:"advertiserId"` // 广告主ID + AdPositionId string `bson:"adPositionId" json:"adPositionId"` // 广告位ID + AdType string `bson:"adType" json:"adType"` // 广告类型:图片、视频、文字等 + AdFormat string `bson:"adFormat" json:"adFormat"` // 广告格式 + MaterialUrl string `bson:"materialUrl" json:"materialUrl"` // 广告素材URL + TargetUrl string `bson:"targetUrl" json:"targetUrl"` // 目标链接(点击跳转或落地页) // 投放设置 StartDate int64 `bson:"startDate" json:"startDate"` // 开始投放时间 @@ -33,21 +32,16 @@ type Advertisement struct { Targeting *Targeting `bson:"targeting" json:"targeting"` // 定向条件 // 状态信息 - Status string `bson:"status" json:"status"` // 广告状态:待审核、已审核、已拒绝、投放中、已暂停、已结束 - AuditStatus string `bson:"auditStatus" json:"auditStatus"` // 审核状态 + Status string `bson:"status" json:"status"` // 广告状态:待审核、审核中、已通过、已拒绝、投放中、已暂停、已结束 AuditReason string `bson:"auditReason" json:"auditReason"` // 审核不通过原因 AuditTime int64 `bson:"auditTime" json:"auditTime"` // 审核时间 AuditBy string `bson:"auditBy" json:"auditBy"` // 审核人 - // 统计信息 - Impressions int64 `bson:"impressions" json:"impressions"` // 展示次数 - Clicks int64 `bson:"clicks" json:"clicks"` // 点击次数 - Conversions int64 `bson:"conversions" json:"conversions"` // 转化次数 - Cost int64 `bson:"cost" json:"cost"` // 消耗(分) - CTR float64 `bson:"ctr" json:"ctr"` // 点击率 - CVR float64 `bson:"cvr" json:"cvr"` // 转化率 - CPM int64 `bson:"cpm" json:"cpm"` // 千次展示成本 - CPC int64 `bson:"cpc" json:"cpc"` // 单次点击成本 + // 基础统计信息(比率字段通过计算得到,不持久化存储) + Impressions int64 `bson:"impressions" json:"impressions"` // 展示次数 + Clicks int64 `bson:"clicks" json:"clicks"` // 点击次数 + Conversions int64 `bson:"conversions" json:"conversions"` // 转化次数 + Cost int64 `bson:"cost" json:"cost"` // 消耗(分) } // Targeting 广告定向条件 diff --git a/model/entity/advertiser.go b/model/entity/advertiser.go index e7f390c..ee5e1e7 100644 --- a/model/entity/advertiser.go +++ b/model/entity/advertiser.go @@ -37,8 +37,7 @@ type Advertiser struct { ExpireDate int64 `bson:"expireDate" json:"expireDate"` // 到期日期 // 状态信息 - Status string `bson:"status" json:"status"` // 广告主状态:待审核、已审核、已拒绝、已冻结 - AuditStatus string `bson:"auditStatus" json:"auditStatus"` // 审核状态 + Status string `bson:"status" json:"status"` // 广告主状态:待审核、审核中、已通过、已拒绝、已冻结 AuditReason string `bson:"auditReason" json:"auditReason"` // 审核不通过原因 AuditTime int64 `bson:"auditTime" json:"auditTime"` // 审核时间 AuditBy string `bson:"auditBy" json:"auditBy"` // 审核人 diff --git a/model/entity/stat_report.go b/model/entity/stat_report.go index 1d06591..e5c76c1 100644 --- a/model/entity/stat_report.go +++ b/model/entity/stat_report.go @@ -15,7 +15,7 @@ type StatReport struct { IsDeleted bool `json:"isDeleted"` // 是否删除 // 报表基本信息 - AppID int64 `json:"appId"` // 应用ID + AppID int64 `json:"appId"` // 应用ID (0表示所有应用) ReportType string `json:"reportType"` // 报表类型:daily, weekly, monthly, quarterly, yearly ReportDate time.Time `json:"reportDate"` // 报表日期 GeneratedAt time.Time `json:"generatedAt"` // 生成时间 diff --git a/service/ad_position_service.go b/service/ad_position_service.go index fdd536a..81b2e65 100644 --- a/service/ad_position_service.go +++ b/service/ad_position_service.go @@ -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 diff --git a/service/advertisement_service.go b/service/advertisement_service.go index 0a7b7a1..c0d37ef 100644 --- a/service/advertisement_service.go +++ b/service/advertisement_service.go @@ -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(), } diff --git a/service/advertiser_service.go b/service/advertiser_service.go index 9221934..4c8da7c 100644 --- a/service/advertiser_service.go +++ b/service/advertiser_service.go @@ -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 diff --git a/service/report_service.go b/service/report_service.go deleted file mode 100644 index f9a9695..0000000 --- a/service/report_service.go +++ /dev/null @@ -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 -} diff --git a/service/stat_report_scheduler.go b/service/stat_report_scheduler.go new file mode 100644 index 0000000..bf78d5b --- /dev/null +++ b/service/stat_report_scheduler.go @@ -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) +} diff --git a/service/stat_report_service.go b/service/stat_report_service.go index 6dd2c7b..c7592e3 100644 --- a/service/stat_report_service.go +++ b/service/stat_report_service.go @@ -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() diff --git a/test_config.go b/test_config.go deleted file mode 100644 index 83e2e80..0000000 --- a/test_config.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "cidservice/service" -) - -func main() { - ctx := context.Background() - - // 检查租户请求次数限制 - allowed, err := service.RateLimit.CheckTenantRequestLimit(ctx, 1, nil) - if err != nil { - fmt.Printf("错误: %v\n", err) - return - } - - fmt.Printf("租户请求是否允许: %v\n", allowed) - - // 获取租户当前使用情况 - current, max, err := service.RateLimit.GetTenantCurrentUsage(ctx, 1, nil) - if err != nil { - fmt.Printf("错误: %v\n", err) - return - } - - fmt.Printf("租户请求使用情况:\n") - fmt.Printf(" 当前使用: %d\n", current) - fmt.Printf(" 最大允许: %d\n", max) - fmt.Printf(" 使用率: %.2f%%\n", float64(current)/float64(max)*100) -}