diff --git a/app/admin/apis/migumanage/migu_admin.go b/app/admin/apis/migumanage/migu_admin.go index 4866bcd..7514629 100644 --- a/app/admin/apis/migumanage/migu_admin.go +++ b/app/admin/apis/migumanage/migu_admin.go @@ -3150,3 +3150,107 @@ func (e MiGuDeployService) queryHistoricalDataByHour(startTime, endTime string, return paginatedData, summaryData, len(filteredData), nil } + +func (e MiGuDeployService) HistoricalSummaryListNewByCatch(c *gin.Context) { + fmt.Println("HistoricalSummaryList") + err := e.MakeContext(c).MakeOrm().Errors + if err != nil { + fmt.Println("MakeContext err:", err) + e.Logger.Error(err) + return + } + + // 请求参数绑定 + req := &models.HistoricalSummaryListReq{} + if c.ShouldBindJSON(req) != nil { + logger.Errorf("para err") + response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") + return + } + + // 分页处理 + if req.PageNum < 1 { + req.PageNum = 1 + } + if req.PageSize == 0 { + req.PageSize = 10 + } + + var summaries []models.MgHistoricalSummary + + today := time.Now().Format("2006-01-02") + + // 1. 先查缓存表(非今日) + var cacheQuery *gorm.DB + + // 如果 req.StartTime 和 req.EndTime 不为空,应用时间条件 + if req.StartTime != "" && req.EndTime != "" { + startDate := req.StartTime[:10] // 取前 10 位 YYYY-MM-DD + endDate := req.EndTime[:10] // 同上 + cacheQuery = e.Orm.Table("mg_historical_summary"). + Where("date >= ? AND date <= ?", startDate, endDate) + } else { + // 如果为空,则查询所有数据 + cacheQuery = e.Orm.Table("mg_historical_summary") + } + + if req.SkuCode != 0 { + cacheQuery = cacheQuery.Where("product_id = ?", req.SkuCode) + } + + if req.Channel != "" { + cacheQuery = cacheQuery.Where("channel_code = ?", req.Channel) + } + + var cacheData []models.MgHistoricalSummary + if err := cacheQuery.Find(&cacheData).Error; err != nil { + response.Error(c, http.StatusInternalServerError, err, "查询失败") + } + + summaries = append(summaries, cacheData...) + + // 2. 如果包含今日,查实时数据 + if req.EndTime >= today || req.EndTime == "" { + // 计算今天的开始时间和结束时间 + now := time.Now() + // 格式化时间为 string 类型,格式为 "YYYY-MM-DD" + start := now.Truncate(24 * time.Hour).Format("2006-01-02") // 今天的零点,字符串格式 + end := now.Truncate(24 * time.Hour).Add(24*time.Hour - time.Second).Format("2006-01-02") // 今天的23:59:59,字符串格式 + + realTimeData, err := models.CalculateDailySummaryFromRealtime(e.Orm, start, end, req.Channel, req.SkuCode) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "查询失败") + } + + for _, item := range realTimeData { + summaries = append(summaries, item) + } + } + + // 3. 排序:默认按日期倒序 + sort.SliceStable(summaries, func(i, j int) bool { + return summaries[i].Date > summaries[j].Date + }) + + // 4. 分页 + total := len(summaries) + start := (req.PageNum - 1) * req.PageSize + end := start + req.PageSize + if start > total { + start = total + } + if end > total { + end = total + } + + pageList := summaries[start:end] + + resp := &models.HistoricalSummaryListResp{ + List: pageList, + Count: total, + PageNum: req.PageNum, + PageSize: req.PageSize, + } + + e.OK(resp, "") +} diff --git a/app/admin/models/migu.go b/app/admin/models/migu.go index efa32b1..88bca9f 100644 --- a/app/admin/models/migu.go +++ b/app/admin/models/migu.go @@ -139,6 +139,8 @@ type MgHourSummary struct { // MgHistoricalSummary 历史汇总查询表对应的结构体 type MgHistoricalSummary struct { + Model + Date string `json:"date"` // 日期 ProductID int64 `json:"product_id"` // 产品ID ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码 @@ -2140,3 +2142,126 @@ func GetMonthlyEffectiveUserStats(db *gorm.DB, retentionMonth string, skuCode in return &stats, nil } + +func UpdateHistoricalSummaryCache() { + logger.Info("****** UpdateHistoricalSummaryCache start ******") + fmt.Println("****** UpdateHistoricalSummaryCache start ******") + if database.Db == nil { + logger.Error("Database connection is nil") + fmt.Println("Database connection is nil") + return + } + + startTime := "1970-01-01 00:00:00" + endTime := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + " 23:59:59" + + // 1. 查询某天的完整实时数据 + summaries, err := CalculateDailySummaryFromRealtime(database.Db, startTime, endTime, "", 0) + if err != nil { + log.Printf("calculateDailySummaryFromRealtime failed: %v", err) + return + } + + if len(summaries) == 0 { + return + } + + tx := database.Db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 2. 清空表中的所有数据 + if err = tx.Exec("TRUNCATE TABLE mg_historical_summary").Error; err != nil { + logger.Error("TRUNCATE TABLE mg_historical_summary error:", err) + fmt.Println("TRUNCATE TABLE mg_historical_summary error") + tx.Rollback() + return + } + + // 3. 插入新数据到缓存表 + if err = tx.Create(&summaries).Error; err != nil { + logger.Error("UpdateHistoricalSummaryCache Create error:", err) + fmt.Println("UpdateHistoricalSummaryCache Create error") + tx.Rollback() + return + } + + err = tx.Commit().Error + if err != nil { + logger.Error("UpdateHistoricalSummaryCache Commit error:", err) + fmt.Println("UpdateHistoricalSummaryCache Commit error") + return + } + + return +} + +func CalculateDailySummaryFromRealtime(db *gorm.DB, startTime, endTime, channelCode string, productID int) ([]MgHistoricalSummary, error) { + + var results []MgHistoricalSummary + + qs := db.Model(&MgOrder{}). + Select(` + DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date, + mg_order.product_id, + mg_order.channel_code, + IFNULL(submission_count.submission_count, 0) AS submission_count, + COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) AS new_user_unsub_within_hour, + COUNT(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 END) AS new_user_unsub_on_day, + COUNT(CASE WHEN mg_order.state = 2 AND TIMESTAMPDIFF(HOUR, mg_order.subscribe_time, mg_order.unsubscribe_time) <= 24 THEN 1 END) AS new_user_unsub_within_24h, + COUNT(*) AS new_user_count, + SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub, + CONCAT(ROUND(COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_within_hour_rate, + CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_on_day_rate, + CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 AND TIMESTAMPDIFF(HOUR, mg_order.subscribe_time, mg_order.unsubscribe_time) <= 24 THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_within_24h_rate, + CONCAT(ROUND(SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS total_new_user_unsub_rate, + IFNULL( + CONCAT( + LEAST( + ROUND(COUNT(*) * 100.0 / NULLIF(submission_count.submission_count, 0), 2), + 100.00 + ), + '%' + ), + '0.00%' + ) AS submission_success_rate + `). + // 使用 Joins 来替代 LeftJoin 进行左连接 + Joins(` + LEFT JOIN ( + SELECT + channel_code, + DATE(created_at) AS created_date, + COUNT(*) AS submission_count + FROM mg_transaction_log + WHERE verification_code != '' + GROUP BY channel_code, created_date + ) AS submission_count + ON submission_count.channel_code = mg_order.channel_code + AND submission_count.created_date = DATE(mg_order.subscribe_time) + `). + Where("mg_order.subscribe_time BETWEEN ? AND ?", startTime, endTime). + Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code"). + Order("mg_order.subscribe_time DESC") + + // 🔍 条件筛选:渠道 + if channelCode != "" { + qs = qs.Where("mg_order.channel_code = ?", channelCode) + } + + // 🔍 条件筛选:产品 + if productID != 0 { + qs = qs.Where("mg_order.product_id = ?", productID) + } + + err := qs.Find(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} diff --git a/app/admin/router/migu.go b/app/admin/router/migu.go index 53a9afa..6d74cc5 100644 --- a/app/admin/router/migu.go +++ b/app/admin/router/migu.go @@ -32,9 +32,11 @@ func registerMiGuControlManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.Gi api.POST("home/data", apiMiGu.HomepageDataSummary) // 查询首页汇总数据 api.POST("home/revenue_analysis", apiMiGu.CalculateRevenueAnalysis) // 营收分析 - api.POST("historical_summary/list", apiMiGu.HistoricalSummaryListOld) // 历史汇总查询 + //api.POST("historical_summary/list", apiMiGu.HistoricalSummaryListOld) // 历史汇总查询 //api.POST("order/import", apiMiGu.ImportExcelToMgOrderHandler) // 通过excel导入订单数据 //api.POST("order/import_update", apiMiGu.ImportExcelToMgOrderHandlerUpdate) // 通过excel导入订单退订数据 + + api.POST("historical_summary/list", apiMiGu.HistoricalSummaryListNewByCatch) // 历史汇总查询(缓存版本) } } diff --git a/cmd/api/server.go b/cmd/api/server.go index 6a08b86..f457694 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -100,8 +100,14 @@ func run() error { fmt.Println("err:", err) } } else { + // 每小时更新1次历史汇总数据 + err := s.Every(1).Hour().Do(models.UpdateHistoricalSummaryCache) + if err != nil { + fmt.Println("err:", err) + } + // 统一每天定时任务 - err := s.Every(1).Day().At("01:30").Do(func() { + err = s.Every(1).Day().At("01:30").Do(func() { if time.Now().Day() == 1 { // 每月1号的逻辑 fmt.Println("****每月1号 检测未退订 任务延迟到03:30执行****") diff --git a/config/settings.yml b/config/settings.yml index d406f59..bfb91d0 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -30,7 +30,7 @@ settings: # 数据库类型 mysql, sqlite3, postgres, sqlserver driver: mysql # 数据库连接字符串 mysql 缺省信息 charset=utf8&parseTime=True&loc=Local&timeout=1000ms - source: + source: migu_pro:RhDShkE4WabxXazb@tcp(112.33.14.191:3306)/migu_pro?charset=utf8&parseTime=True&loc=Local&timeout=1000ms gen: # 代码生成读取的数据库名称 dbname: dbname