初始化项目
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
server:
|
server:
|
||||||
address : ":3004"
|
address : ":3001"
|
||||||
name: "cidService"
|
name: "cidService"
|
||||||
jwt:
|
jwt:
|
||||||
secret: "abcdefghijklmnopqrstuvwxyz"
|
secret: "abcdefghijklmnopqrstuvwxyz"
|
||||||
@@ -40,8 +40,3 @@ rabbitMQ:
|
|||||||
password: guest # 默认密码
|
password: guest # 默认密码
|
||||||
jaeger: #链路追踪
|
jaeger: #链路追踪
|
||||||
addr: 192.168.3.200:4318
|
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)
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"cidservice/model/dto"
|
"cidservice/model/dto"
|
||||||
"cidservice/service"
|
"cidservice/service"
|
||||||
@@ -18,6 +19,8 @@ func (c *statReport) GenerateReport(ctx context.Context, req *dto.ReportGenerate
|
|||||||
switch req.ReportType {
|
switch req.ReportType {
|
||||||
case "daily":
|
case "daily":
|
||||||
resp, err = service.StatReport.GenerateDailyReport(ctx, req)
|
resp, err = service.StatReport.GenerateDailyReport(ctx, req)
|
||||||
|
case "weekly":
|
||||||
|
resp, err = service.StatReport.GenerateWeeklyReport(ctx, req)
|
||||||
case "monthly":
|
case "monthly":
|
||||||
resp, err = service.StatReport.GenerateMonthlyReport(ctx, req)
|
resp, err = service.StatReport.GenerateMonthlyReport(ctx, req)
|
||||||
case "quarterly":
|
case "quarterly":
|
||||||
@@ -25,7 +28,7 @@ func (c *statReport) GenerateReport(ctx context.Context, req *dto.ReportGenerate
|
|||||||
case "yearly":
|
case "yearly":
|
||||||
resp, err = service.StatReport.GenerateYearlyReport(ctx, req)
|
resp, err = service.StatReport.GenerateYearlyReport(ctx, req)
|
||||||
default:
|
default:
|
||||||
return nil, err
|
return nil, fmt.Errorf("不支持的报表类型: %s", req.ReportType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -66,11 +66,8 @@ func (d *advertisement) Update(ctx context.Context, req *dto.UpdateAdvertisement
|
|||||||
if !g.IsEmpty(req.MaterialUrl) {
|
if !g.IsEmpty(req.MaterialUrl) {
|
||||||
updateFields["materialUrl"] = req.MaterialUrl
|
updateFields["materialUrl"] = req.MaterialUrl
|
||||||
}
|
}
|
||||||
if !g.IsEmpty(req.LinkUrl) {
|
if !g.IsEmpty(req.TargetUrl) {
|
||||||
updateFields["linkUrl"] = req.LinkUrl
|
updateFields["targetUrl"] = req.TargetUrl
|
||||||
}
|
|
||||||
if !g.IsEmpty(req.LandingPageUrl) {
|
|
||||||
updateFields["landingPageUrl"] = req.LandingPageUrl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投放设置
|
// 投放设置
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
16
main.go
16
main.go
@@ -2,6 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"cidservice/controller"
|
"cidservice/controller"
|
||||||
|
"cidservice/service"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitee.com/red-future---jilin-g/common/http"
|
"gitee.com/red-future---jilin-g/common/http"
|
||||||
"gitee.com/red-future---jilin-g/common/jaeger"
|
"gitee.com/red-future---jilin-g/common/jaeger"
|
||||||
@@ -13,13 +16,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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{}{
|
http.RouteRegister([]interface{}{
|
||||||
controller.Advertisement,
|
controller.Advertisement,
|
||||||
controller.Advertiser,
|
controller.Advertiser,
|
||||||
controller.AdPosition,
|
controller.AdPosition,
|
||||||
controller.AdStatistics,
|
controller.AdStatistics,
|
||||||
controller.Report,
|
|
||||||
controller.RateLimit,
|
controller.RateLimit,
|
||||||
controller.Application,
|
controller.Application,
|
||||||
controller.StatReport,
|
controller.StatReport,
|
||||||
|
|||||||
@@ -12,15 +12,14 @@ type AddAdvertisementReq struct {
|
|||||||
g.Meta `path:"/add" method:"post" tags:"广告管理" summary:"添加广告" dc:"添加新的广告"`
|
g.Meta `path:"/add" method:"post" tags:"广告管理" summary:"添加广告" dc:"添加新的广告"`
|
||||||
|
|
||||||
// 广告基本信息
|
// 广告基本信息
|
||||||
Title string `json:"title" v:"required"` // 广告标题
|
Title string `json:"title" v:"required"` // 广告标题
|
||||||
Description string `json:"description"` // 广告描述
|
Description string `json:"description"` // 广告描述
|
||||||
AdvertiserId string `json:"advertiserId" v:"required"` // 广告主ID
|
AdvertiserId string `json:"advertiserId" v:"required"` // 广告主ID
|
||||||
AdPositionId string `json:"adPositionId" v:"required"` // 广告位ID
|
AdPositionId string `json:"adPositionId" v:"required"` // 广告位ID
|
||||||
AdType string `json:"adType" v:"required"` // 广告类型:图片、视频、文字等
|
AdType string `json:"adType" v:"required"` // 广告类型:图片、视频、文字等
|
||||||
AdFormat string `json:"adFormat" v:"required"` // 广告格式
|
AdFormat string `json:"adFormat" v:"required"` // 广告格式
|
||||||
MaterialUrl string `json:"materialUrl" v:"required"` // 广告素材URL
|
MaterialUrl string `json:"materialUrl" v:"required"` // 广告素材URL
|
||||||
LinkUrl string `json:"linkUrl"` // 点击跳转链接
|
TargetUrl string `json:"targetUrl" v:"required"` // 目标链接
|
||||||
LandingPageUrl string `json:"landingPageUrl"` // 落地页URL
|
|
||||||
|
|
||||||
// 投放设置
|
// 投放设置
|
||||||
StartDate int64 `json:"startDate" v:"required"` // 开始投放时间
|
StartDate int64 `json:"startDate" v:"required"` // 开始投放时间
|
||||||
@@ -45,15 +44,14 @@ type UpdateAdvertisementReq struct {
|
|||||||
Id string `json:"id" v:"required"` // ID
|
Id string `json:"id" v:"required"` // ID
|
||||||
|
|
||||||
// 广告基本信息
|
// 广告基本信息
|
||||||
Title string `json:"title"` // 广告标题
|
Title string `json:"title"` // 广告标题
|
||||||
Description string `json:"description"` // 广告描述
|
Description string `json:"description"` // 广告描述
|
||||||
AdvertiserId string `json:"advertiserId"` // 广告主ID
|
AdvertiserId string `json:"advertiserId"` // 广告主ID
|
||||||
AdPositionId string `json:"adPositionId"` // 广告位ID
|
AdPositionId string `json:"adPositionId"` // 广告位ID
|
||||||
AdType string `json:"adType"` // 广告类型:图片、视频、文字等
|
AdType string `json:"adType"` // 广告类型:图片、视频、文字等
|
||||||
AdFormat string `json:"adFormat"` // 广告格式
|
AdFormat string `json:"adFormat"` // 广告格式
|
||||||
MaterialUrl string `json:"materialUrl"` // 广告素材URL
|
MaterialUrl string `json:"materialUrl"` // 广告素材URL
|
||||||
LinkUrl string `json:"linkUrl"` // 点击跳转链接
|
TargetUrl string `json:"targetUrl"` // 目标链接
|
||||||
LandingPageUrl string `json:"landingPageUrl"` // 落地页URL
|
|
||||||
|
|
||||||
// 投放设置
|
// 投放设置
|
||||||
StartDate *int64 `json:"startDate"` // 开始投放时间
|
StartDate *int64 `json:"startDate"` // 开始投放时间
|
||||||
@@ -67,8 +65,8 @@ type UpdateAdvertisementReq struct {
|
|||||||
Targeting *entity.Targeting `json:"targeting"` // 定向条件
|
Targeting *entity.Targeting `json:"targeting"` // 定向条件
|
||||||
|
|
||||||
// 状态信息
|
// 状态信息
|
||||||
Status *string `json:"status"` // 广告状态:待审核、已审核、已拒绝、投放中、已暂停、已结束
|
Status *string `json:"status"` // 广告状态:待审核、审核中、已通过、已拒绝、投放中、已暂停、已结束
|
||||||
AuditStatus *string `json:"auditStatus"` // 审核状态
|
AuditStatus *string `json:"auditStatus"` // 审核状态:通过、拒绝
|
||||||
AuditReason *string `json:"auditReason"` // 审核不通过原因
|
AuditReason *string `json:"auditReason"` // 审核不通过原因
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ type ReportGenerateReq struct {
|
|||||||
g.Meta `path:"/generateReport" method:"post"`
|
g.Meta `path:"/generateReport" method:"post"`
|
||||||
TenantID int64 `json:"tenant_id" v:"required"`
|
TenantID int64 `json:"tenant_id" v:"required"`
|
||||||
AppID int64 `json:"app_id"`
|
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)
|
Date string `json:"date"` // 格式: 2024-01-01 (daily), 2024-01 (monthly), 2024-Q1 (quarterly), 2024 (yearly)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,14 @@ type Advertisement struct {
|
|||||||
do.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted
|
do.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted
|
||||||
|
|
||||||
// 广告基本信息
|
// 广告基本信息
|
||||||
Title string `bson:"title" json:"title"` // 广告标题
|
Title string `bson:"title" json:"title"` // 广告标题
|
||||||
Description string `bson:"description" json:"description"` // 广告描述
|
Description string `bson:"description" json:"description"` // 广告描述
|
||||||
AdvertiserId string `bson:"advertiserId" json:"advertiserId"` // 广告主ID
|
AdvertiserId string `bson:"advertiserId" json:"advertiserId"` // 广告主ID
|
||||||
AdPositionId string `bson:"adPositionId" json:"adPositionId"` // 广告位ID
|
AdPositionId string `bson:"adPositionId" json:"adPositionId"` // 广告位ID
|
||||||
AdType string `bson:"adType" json:"adType"` // 广告类型:图片、视频、文字等
|
AdType string `bson:"adType" json:"adType"` // 广告类型:图片、视频、文字等
|
||||||
AdFormat string `bson:"adFormat" json:"adFormat"` // 广告格式
|
AdFormat string `bson:"adFormat" json:"adFormat"` // 广告格式
|
||||||
MaterialUrl string `bson:"materialUrl" json:"materialUrl"` // 广告素材URL
|
MaterialUrl string `bson:"materialUrl" json:"materialUrl"` // 广告素材URL
|
||||||
LinkUrl string `bson:"linkUrl" json:"linkUrl"` // 点击跳转链接
|
TargetUrl string `bson:"targetUrl" json:"targetUrl"` // 目标链接(点击跳转或落地页)
|
||||||
LandingPageUrl string `bson:"landingPageUrl" json:"landingPageUrl"` // 落地页URL
|
|
||||||
|
|
||||||
// 投放设置
|
// 投放设置
|
||||||
StartDate int64 `bson:"startDate" json:"startDate"` // 开始投放时间
|
StartDate int64 `bson:"startDate" json:"startDate"` // 开始投放时间
|
||||||
@@ -33,21 +32,16 @@ type Advertisement struct {
|
|||||||
Targeting *Targeting `bson:"targeting" json:"targeting"` // 定向条件
|
Targeting *Targeting `bson:"targeting" json:"targeting"` // 定向条件
|
||||||
|
|
||||||
// 状态信息
|
// 状态信息
|
||||||
Status string `bson:"status" json:"status"` // 广告状态:待审核、已审核、已拒绝、投放中、已暂停、已结束
|
Status string `bson:"status" json:"status"` // 广告状态:待审核、审核中、已通过、已拒绝、投放中、已暂停、已结束
|
||||||
AuditStatus string `bson:"auditStatus" json:"auditStatus"` // 审核状态
|
|
||||||
AuditReason string `bson:"auditReason" json:"auditReason"` // 审核不通过原因
|
AuditReason string `bson:"auditReason" json:"auditReason"` // 审核不通过原因
|
||||||
AuditTime int64 `bson:"auditTime" json:"auditTime"` // 审核时间
|
AuditTime int64 `bson:"auditTime" json:"auditTime"` // 审核时间
|
||||||
AuditBy string `bson:"auditBy" json:"auditBy"` // 审核人
|
AuditBy string `bson:"auditBy" json:"auditBy"` // 审核人
|
||||||
|
|
||||||
// 统计信息
|
// 基础统计信息(比率字段通过计算得到,不持久化存储)
|
||||||
Impressions int64 `bson:"impressions" json:"impressions"` // 展示次数
|
Impressions int64 `bson:"impressions" json:"impressions"` // 展示次数
|
||||||
Clicks int64 `bson:"clicks" json:"clicks"` // 点击次数
|
Clicks int64 `bson:"clicks" json:"clicks"` // 点击次数
|
||||||
Conversions int64 `bson:"conversions" json:"conversions"` // 转化次数
|
Conversions int64 `bson:"conversions" json:"conversions"` // 转化次数
|
||||||
Cost int64 `bson:"cost" json:"cost"` // 消耗(分)
|
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"` // 单次点击成本
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Targeting 广告定向条件
|
// Targeting 广告定向条件
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ type Advertiser struct {
|
|||||||
ExpireDate int64 `bson:"expireDate" json:"expireDate"` // 到期日期
|
ExpireDate int64 `bson:"expireDate" json:"expireDate"` // 到期日期
|
||||||
|
|
||||||
// 状态信息
|
// 状态信息
|
||||||
Status string `bson:"status" json:"status"` // 广告主状态:待审核、已审核、已拒绝、已冻结
|
Status string `bson:"status" json:"status"` // 广告主状态:待审核、审核中、已通过、已拒绝、已冻结
|
||||||
AuditStatus string `bson:"auditStatus" json:"auditStatus"` // 审核状态
|
|
||||||
AuditReason string `bson:"auditReason" json:"auditReason"` // 审核不通过原因
|
AuditReason string `bson:"auditReason" json:"auditReason"` // 审核不通过原因
|
||||||
AuditTime int64 `bson:"auditTime" json:"auditTime"` // 审核时间
|
AuditTime int64 `bson:"auditTime" json:"auditTime"` // 审核时间
|
||||||
AuditBy string `bson:"auditBy" json:"auditBy"` // 审核人
|
AuditBy string `bson:"auditBy" json:"auditBy"` // 审核人
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type StatReport struct {
|
|||||||
IsDeleted bool `json:"isDeleted"` // 是否删除
|
IsDeleted bool `json:"isDeleted"` // 是否删除
|
||||||
|
|
||||||
// 报表基本信息
|
// 报表基本信息
|
||||||
AppID int64 `json:"appId"` // 应用ID
|
AppID int64 `json:"appId"` // 应用ID (0表示所有应用)
|
||||||
ReportType string `json:"reportType"` // 报表类型:daily, weekly, monthly, quarterly, yearly
|
ReportType string `json:"reportType"` // 报表类型:daily, weekly, monthly, quarterly, yearly
|
||||||
ReportDate time.Time `json:"reportDate"` // 报表日期
|
ReportDate time.Time `json:"reportDate"` // 报表日期
|
||||||
GeneratedAt time.Time `json:"generatedAt"` // 生成时间
|
GeneratedAt time.Time `json:"generatedAt"` // 生成时间
|
||||||
|
|||||||
@@ -111,10 +111,9 @@ func (s *adPosition) MatchAd(ctx context.Context, positionCode string, userInfo
|
|||||||
// 返回匹配的广告
|
// 返回匹配的广告
|
||||||
// 这里返回第一个广告作为示例
|
// 这里返回第一个广告作为示例
|
||||||
ad = &entity.Advertisement{
|
ad = &entity.Advertisement{
|
||||||
Title: "示例广告",
|
Title: "示例广告",
|
||||||
MaterialUrl: "https://example.com/ad.jpg",
|
MaterialUrl: "https://example.com/ad.jpg",
|
||||||
LinkUrl: "https://example.com",
|
TargetUrl: "https://example.com",
|
||||||
LandingPageUrl: "https://example.com/landing",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -29,17 +29,12 @@ func (s *advertisement) Add(ctx context.Context, req *dto.AddAdvertisementReq) (
|
|||||||
|
|
||||||
// 设置初始状态
|
// 设置初始状态
|
||||||
advertisement.Status = "待审核"
|
advertisement.Status = "待审核"
|
||||||
advertisement.AuditStatus = "待审核"
|
|
||||||
|
|
||||||
// 初始化统计字段
|
// 初始化统计字段
|
||||||
advertisement.Impressions = 0
|
advertisement.Impressions = 0
|
||||||
advertisement.Clicks = 0
|
advertisement.Clicks = 0
|
||||||
advertisement.Conversions = 0
|
advertisement.Conversions = 0
|
||||||
advertisement.Cost = 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 {
|
if err = dao.Advertisement.Insert(ctx, advertisement); err != nil {
|
||||||
return
|
return
|
||||||
@@ -133,10 +128,6 @@ func (s *advertisement) UpdateAdStatistics(ctx context.Context, id string, impre
|
|||||||
"clicks": totalClicks,
|
"clicks": totalClicks,
|
||||||
"conversions": totalConversions,
|
"conversions": totalConversions,
|
||||||
"cost": totalCost,
|
"cost": totalCost,
|
||||||
"ctr": ctr,
|
|
||||||
"cvr": cvr,
|
|
||||||
"cpm": cpm,
|
|
||||||
"cpc": cpc,
|
|
||||||
"updatedAt": time.Now(),
|
"updatedAt": time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ func (s *advertiser) Add(ctx context.Context, req *dto.AddAdvertiserReq) (res *d
|
|||||||
|
|
||||||
// 设置初始状态
|
// 设置初始状态
|
||||||
advertiser.Status = "待审核"
|
advertiser.Status = "待审核"
|
||||||
advertiser.AuditStatus = "待审核"
|
|
||||||
|
|
||||||
if err = dao.Advertiser.Insert(ctx, advertiser); err != nil {
|
if err = dao.Advertiser.Insert(ctx, advertiser); err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
634
service/stat_report_scheduler.go
Normal file
634
service/stat_report_scheduler.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ type StatReportService struct{}
|
|||||||
|
|
||||||
var StatReport = &StatReportService{}
|
var StatReport = &StatReportService{}
|
||||||
|
|
||||||
// 生成日报表
|
// GenerateDailyReport 生成日报表(现在只用于手动触发,定时任务会自动生成)
|
||||||
func (s *StatReportService) GenerateDailyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
func (s *StatReportService) GenerateDailyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||||
// 获取统计日期
|
// 获取统计日期
|
||||||
reportDate := time.Now()
|
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)
|
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "daily", reportDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -58,7 +75,7 @@ func (s *StatReportService) GenerateDailyReport(ctx context.Context, req *dto.Re
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成月报表
|
// GenerateMonthlyReport 生成月报表(现在优先使用预生成的报表)
|
||||||
func (s *StatReportService) GenerateMonthlyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
func (s *StatReportService) GenerateMonthlyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||||
reportDate := time.Now()
|
reportDate := time.Now()
|
||||||
if req.Date != "" {
|
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)
|
reportData, err := s.generateReportData(ctx, req.TenantID, req.AppID, "monthly", reportDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -96,6 +129,61 @@ func (s *StatReportService) GenerateMonthlyReport(ctx context.Context, req *dto.
|
|||||||
}, nil
|
}, 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) {
|
func (s *StatReportService) GenerateQuarterlyReport(ctx context.Context, req *dto.ReportGenerateReq) (*dto.ReportGenerateResp, error) {
|
||||||
reportDate := time.Now()
|
reportDate := time.Now()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user