package migumanage import ( "errors" "fmt" "github.com/gin-gonic/gin" "github.com/go-admin-team/go-admin-core/logger" "github.com/go-admin-team/go-admin-core/sdk/pkg/response" "go-admin/app/admin/models" "gorm.io/gorm" "net/http" "sort" "strconv" "strings" "sync" "time" ) // 以下是后台部分接口 // TransactionList 查询交易流水记录 // @Summary 查询交易流水记录 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.TransactionListReq true "查询交易流水记录" // @Success 200 {object} models.TransactionListResp // @Router /api/v1/admin/transaction/list [post] func (e MiGuDeployService) TransactionList(c *gin.Context) { fmt.Println("TransactionList") err := e.MakeContext(c).MakeOrm().Errors if err != nil { fmt.Println("MakeContext err:", err) e.Logger.Error(err) return } req := &models.TransactionListReq{} if c.ShouldBindJSON(req) != nil { logger.Errorf("para err") response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") return } resp := models.TransactionListResp{ PageNum: req.PageNum, } // 导出excel时检查时间范围 if req.IsExport == 1 { err = models.CheckDateRange(req.StartTime, req.EndTime, 7) if err != nil { response.Error(c, http.StatusBadRequest, err, err.Error()) return } } pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize if e.Orm == nil { fmt.Println("Orm is nil") } var count int64 qs := e.Orm.Model(&models.MgTransactionLog{}) // 手机号码 if req.Phone != "" { qs = qs.Where("phone_number = ?", req.Phone) } // 渠道号 if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } // 产品编号 if req.SkuCode != 0 { qs = qs.Where("product_id = ?", req.SkuCode) } // 交易结果 if req.Result != "" { if req.Result == "1" { // 成功 qs = qs.Where("result = ?", "00000") } else if req.Result == "2" { // 失败 qs = qs.Where("result != ?", "00000") } } // 开始时间和结束时间 if req.StartTime != "" && req.EndTime != "" { qs = qs.Where("created_at BETWEEN ? AND ?", req.StartTime, req.EndTime) } err = qs.Count(&count).Error if err != nil { logger.Errorf("count err:", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } var transactionLogs []models.MgTransactionLog if req.IsExport == 1 { err = qs.Order("created_at desc").Find(&transactionLogs).Error } else { err = qs.Order("created_at desc").Offset(pageNum * req.PageSize).Limit(req.PageSize).Find(&transactionLogs).Error } if err != nil { logger.Errorf("TransactionList err:%#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } resp.List = transactionLogs resp.Count = int(count) if req.IsExport == 1 { url, err := models.ExportTransactionToExcel(transactionLogs, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } response.OK(c, map[string]string{"export_url": url}, "导出成功") return } e.OK(resp, "") } // ProductList 查询权益产品 // @Summary 查询权益产品 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.ProductListReq true "查询权益产品" // @Success 200 {object} models.ProductListResp // @Router /api/v1/admin/product/list [post] func (e MiGuDeployService) ProductList(c *gin.Context) { fmt.Println("ProductList") err := e.MakeContext(c).MakeOrm().Errors if err != nil { fmt.Println("MakeContext err:", err) e.Logger.Error(err) return } req := &models.ProductListReq{} if c.ShouldBindJSON(req) != nil { logger.Errorf("para err") response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") return } resp := models.ProductListResp{ PageNum: req.PageNum, } pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize if e.Orm == nil { fmt.Println("Orm is nil") } var count int64 qs := e.Orm.Model(&models.MgProduct{}) err = qs.Count(&count).Error if err != nil { logger.Errorf("count err:", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } var productList []models.MgProduct err = qs.Order("id ").Offset(pageNum * req.PageSize).Limit(req.PageSize).Find(&productList).Error if err != nil { logger.Errorf("ProductList err:%#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } resp.List = productList e.OK(resp, "") } // ChannelList 查询渠道列表 // @Summary 查询渠道列表 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.ChannelListReq true "查询渠道列表" // @Success 200 {object} models.ChannelListResp // @Router /api/v1/admin/channel/list [post] func (e MiGuDeployService) ChannelList(c *gin.Context) { fmt.Println("ProductList") err := e.MakeContext(c).MakeOrm().Errors if err != nil { fmt.Println("MakeContext err:", err) e.Logger.Error(err) return } req := &models.ChannelListReq{} if c.ShouldBindJSON(req) != nil { logger.Errorf("para err") response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") return } resp := models.ChannelListResp{ PageNum: req.PageNum, } pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize if e.Orm == nil { fmt.Println("Orm is nil") } var count int64 qs := e.Orm.Model(&models.MgChannel{}) err = qs.Count(&count).Error if err != nil { logger.Errorf("count err:", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } var channelList []models.MgChannel err = qs.Order("id ").Offset(pageNum * req.PageSize).Limit(req.PageSize).Find(&channelList).Error if err != nil { logger.Errorf("ProductList err:%#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } resp.List = channelList e.OK(resp, "") } // OrderList 查询订单列表 // @Summary 查询订单列表 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.OrderListReq true "查询订单列表" // @Success 200 {object} models.OrderListResp // @Router /api/v1/admin/order/list [post] func (e MiGuDeployService) OrderList(c *gin.Context) { fmt.Println("OrderList") err := e.MakeContext(c).MakeOrm().Errors if err != nil { fmt.Println("MakeContext err:", err) e.Logger.Error(err) return } req := &models.OrderListReq{} if c.ShouldBindJSON(req) != nil { logger.Errorf("para err") response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") return } resp := models.OrderListResp{ PageNum: req.PageNum, } pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize if e.Orm == nil { fmt.Println("Orm is nil") } // 导出excel时检查时间范围 if req.IsExport == 1 { err = models.CheckDateRange(req.StartTime, req.EndTime, 31) if err != nil { response.Error(c, http.StatusBadRequest, err, err.Error()) return } } qs := e.Orm.Model(&models.MgOrder{}) // 产品编号 if req.SkuCode != 0 { qs = qs.Where("product_id = ?", req.SkuCode) } // 渠道号 if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } // 订单流水号 if req.OrderSerial != "" { qs = qs.Where("order_serial = ?", req.OrderSerial) } // 外部订单号 if req.OutTradeNo != "" { qs = qs.Where("external_order_id = ?", req.OutTradeNo) } // 渠道订单号 if req.ChannelTradeNo != "" { qs = qs.Where("channel_trade_no = ?", req.ChannelTradeNo) } // 手机号码 if req.Phone != "" { qs = qs.Where("phone_number = ?", req.Phone) } // SM4加密手机号 if req.SM4PhoneNumber != "" { qs = qs.Where("sm4_phone_number = ?", req.SM4PhoneNumber) } // 退订状态 if req.State != 0 { if req.State == 3 { // 1小时内退订 qs = qs.Where("is_one_hour_cancel = ?", 1) } else if req.State == 4 { // 查询退订状态为2,且退订时间与订购时间在24小时内 qs = qs.Where("state = ?", 2) qs = qs.Where("TIMESTAMPDIFF(HOUR, subscribe_time, unsubscribe_time) <= 24") } else { state := 1 // 订阅 if req.State == 1 { state = 2 // 退订 } qs = qs.Where("state = ?", state) } } // 开始时间和结束时间 if req.StartTime != "" && req.EndTime != "" { qs = qs.Where("subscribe_time BETWEEN ? AND ?", req.StartTime, req.EndTime) } // 退订时间不为空 if req.CancelStartTime != "" && req.CancelEndTime != "" { qs = qs.Where("unsubscribe_time BETWEEN ? AND ?", req.CancelStartTime, req.CancelEndTime) } var count int64 err = qs.Count(&count).Error if err != nil { logger.Errorf("count err:", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } var orderList []models.MgOrder if req.IsExport == 1 { err = qs.Order("created_at desc").Find(&orderList).Error } else { err = qs.Order("created_at desc").Offset(pageNum * req.PageSize).Limit(req.PageSize).Find(&orderList).Error } if err != nil { logger.Errorf("OrderList err:%#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 判断是否导出Excel if req.IsExport == 1 { // 调用导出Excel函数 url, err := models.ExportOrderListToExcel(orderList, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } // 返回导出文件的URL地址 response.OK(c, map[string]string{"export_url": url}, "导出成功") return } resp.List = orderList resp.Count = int(count) e.OK(resp, "") } func (e MiGuDeployService) HistoricalSummaryListOld(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 } resp := models.HistoricalSummaryListResp{ PageNum: req.PageNum, } // 分页处理 pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize if e.Orm == nil { fmt.Println("Orm is nil") } // 构建查询 var historicalSummaryList []models.MgHistoricalSummary var count int64 // 设置开始时间和结束时间 startTime := req.StartTime endTime := req.EndTime if startTime == "" { startTime = "1970-01-01 00:00:00" } if endTime == "" { endTime = time.Now().Format("2006-01-02 15:04:05") } // 使用左连接查询 //qs := e.Orm.Model(&models.MgOrder{}). // Select(`DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date, // mg_order.product_id, // mg_order.channel_code, // (SELECT COUNT(*) // FROM mg_transaction_log // WHERE verification_code != '' // AND channel_code = mg_order.channel_code // AND DATE(created_at) = DATE(mg_order.subscribe_time)) 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 THEN 1 END) AS new_user_unsub_on_day, // COUNT(*) AS new_user_count, // 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 THEN 1 END) * 100.0 / // NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_on_day_rate`). // Where("mg_order.subscribe_time >= ? AND mg_order.subscribe_time <= ?", startTime, endTime). // Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code"). // Order("mg_order.product_id, mg_order.channel_code, mg_order.subscribe_time") //qs := e.Orm.Model(&models.MgOrder{}). // Select(`DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date, // mg_order.product_id, // mg_order.channel_code, // (SELECT COUNT(*) // FROM mg_transaction_log // WHERE verification_code != '' // AND channel_code = mg_order.channel_code // AND DATE(created_at) = DATE(mg_order.subscribe_time)) 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 mg_order.unsubscribe_time >= ? AND mg_order.unsubscribe_time <= ? THEN 1 END) AS new_user_unsub_on_day, // COUNT(*) AS new_user_count, // 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 mg_order.unsubscribe_time >= ? AND mg_order.unsubscribe_time <= ? THEN 1 END) * 100.0 / // NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_on_day_rate, // IFNULL( // CONCAT( // LEAST( // ROUND(COUNT(*) * 100.0 / NULLIF( // (SELECT COUNT(*) // FROM mg_transaction_log // WHERE verification_code != '' // AND channel_code = mg_order.channel_code // AND DATE(created_at) = DATE(mg_order.subscribe_time)), 0), 2), // 100.00 // ), // '%' // ), // '0.00%' // ) AS submission_success_rate`, // startTime, endTime, startTime, endTime). // Where("mg_order.subscribe_time >= ? AND mg_order.subscribe_time <= ?", startTime, endTime). // Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code"). // Order("mg_order.product_id, mg_order.channel_code, mg_order.subscribe_time DESC") qs := e.Orm.Model(&models.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 req.SkuCode != 0 { qs = qs.Where("product_id = ?", req.SkuCode) } if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } // 查询总记录数 err = qs.Count(&count).Error if err != nil { logger.Errorf("count err: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 判断是否导出Excel if req.IsExport == 1 { // 执行查询 err = qs.Find(&historicalSummaryList).Error } else { // 执行查询 err = qs.Offset(pageNum * req.PageSize).Limit(req.PageSize).Find(&historicalSummaryList).Error } if err != nil { logger.Errorf("HistoricalSummaryList query err: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 判断是否导出Excel if req.IsExport == 1 { // 调用导出Excel函数 url, err := models.ExportHistoricalSummaryToExcel(historicalSummaryList, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } // 返回导出文件的URL地址 response.OK(c, map[string]string{"export_url": url}, "导出成功") return } // 返回结果 resp.List = historicalSummaryList resp.Count = int(count) e.OK(resp, "") } // HistoricalSummaryListNew 历史汇总查询 // @Summary 历史汇总查询 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.HistoricalSummaryListReq true "历史汇总查询" // @Success 200 {object} models.HistoricalSummaryListResp // @Router /api/v1/admin/historical_summary/list [post] func (e MiGuDeployService) HistoricalSummaryListNew(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 } resp := models.HistoricalSummaryListResp{ PageNum: req.PageNum, } // 分页处理 pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize // 获取日期范围 minDate, maxDate, nCount, err := e.getDateRange(req) if err != nil { response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") return } if nCount == 0 { e.OK(resp, "") return } var startTime, endTime string if req.IsExport == 1 { if startTime != "" && endTime != "" { startDate, err := time.Parse("2006-01-02", startTime) if err != nil { response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") return } endDate, err := time.Parse("2006-01-02", endTime) if err != nil { response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") return } startTime = startDate.Format("2006-01-02") + " 00:00:00" endTime = endDate.Format("2006-01-02") + " 23:59:59" } else { startTime = minDate + " 00:00:00" endTime = maxDate + " 23:59:59" } } else { // 处理分页日期范围 startTime, endTime, err = e.processTimeRange(req.StartTime, req.EndTime, minDate, maxDate, pageNum) if err != nil { response.Error(c, http.StatusBadRequest, err, "时间范围无效") return } } fmt.Println("Start Time:", startTime) fmt.Println("End Time:", endTime) // 查询数据 data, err := e.queryHistoricalSummary(startTime, endTime, req) if err != nil { response.Error(c, http.StatusInternalServerError, err, "查询失败") return } if req.IsExport != 1 { // 补充缺失的日期 dateList, err := e.getDateList(startTime, endTime) if err != nil { response.Error(c, http.StatusBadRequest, err, "日期格式无效") return } finalList := e.fillMissingDates(dateList, data) resp.List = finalList } else { resp.List = data } // 计算总条数 if req.StartTime != "" && req.EndTime != "" { // 解析日期 startDate, _ := time.Parse(models.MiGuTimeFormat, req.StartTime) endDate, _ := time.Parse(models.MiGuTimeFormat, req.EndTime) // 计算日期差 daysDiff := int(endDate.Sub(startDate).Hours() / 24) resp.Count = daysDiff + 1 } else { resp.Count = nCount } // 导出Excel if req.IsExport == 1 { url, err := models.ExportHistoricalSummaryToExcel(resp.List, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } response.OK(c, map[string]string{"export_url": url}, "导出成功") return } // 返回结果 e.OK(resp, "") } // 获取最早和最晚日期 func (e MiGuDeployService) getDateRange(req *models.HistoricalSummaryListReq) (string, string, int, error) { var result struct { MinDate string `json:"min_date"` MaxDate string `json:"max_date"` } qs := e.Orm.Model(&models.MgOrder{}). Select("MIN(DATE(mg_order.subscribe_time)) AS min_date, MAX(DATE(mg_order.subscribe_time)) AS max_date") if req.SkuCode != 0 { qs = qs.Where("product_id = ?", req.SkuCode) } if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } err := qs.Scan(&result).Error if err != nil { return "", "", 0, err } if result.MinDate == "" || result.MaxDate == "" { return "", "", 0, nil } // 解析日期 minDate, err := time.Parse("2006-01-02", result.MinDate) if err != nil { return "", "", 0, fmt.Errorf("failed to parse min_date: %v", err) } maxDate, err := time.Parse("2006-01-02", result.MaxDate) if err != nil { return "", "", 0, fmt.Errorf("failed to parse max_date: %v", err) } // 计算日期差 daysDiff := int(maxDate.Sub(minDate).Hours() / 24) if daysDiff != 0 { daysDiff += 1 } return result.MinDate, result.MaxDate, daysDiff, nil } // 处理分页日期范围 func (e MiGuDeployService) processTimeRange(startTime, endTime, minDate, maxDate string, pageNum int) (string, string, error) { var err error var startDate, endDate time.Time if startTime != "" && endTime != "" { startDate, err = time.Parse(models.MiGuTimeFormat, startTime) if err != nil { return "", "", fmt.Errorf("startTime 格式无效") } endDate, err = time.Parse(models.MiGuTimeFormat, endTime) if err != nil { return "", "", fmt.Errorf("endTime 格式无效") } } else { if minDate == "" || maxDate == "" { return "", "", fmt.Errorf("没有数据") } startDate, err = time.Parse("2006-01-02", minDate) if err != nil { return "", "", fmt.Errorf("minDate 格式无效") } endDate, err = time.Parse("2006-01-02", maxDate) if err != nil { return "", "", fmt.Errorf("maxDate 格式无效") } } pageStartDate := endDate.AddDate(0, 0, -(pageNum+1)*10+1) pageEndDate := pageStartDate.AddDate(0, 0, 9) if pageStartDate.Before(startDate) { pageStartDate = startDate } if pageEndDate.After(endDate) { pageEndDate = endDate } startTime = pageStartDate.Format("2006-01-02") + " 00:00:00" endTime = pageEndDate.Format("2006-01-02") + " 23:59:59" return startTime, endTime, nil } //// 查询历史数据 //func (e MiGuDeployService) queryHistoricalSummary(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHistoricalSummary, error) { // qs := e.Orm.Model(&models.MgOrder{}). // Select(`DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date, // mg_order.product_id, // mg_order.channel_code, // (SELECT COUNT(*) // FROM mg_transaction_log // WHERE verification_code != '' // AND channel_code = mg_order.channel_code // AND DATE(created_at) = DATE(mg_order.subscribe_time)) 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 mg_order.unsubscribe_time >= ? AND mg_order.unsubscribe_time <= ? THEN 1 END) AS new_user_unsub_on_day, // COUNT(*) AS new_user_count, // 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 mg_order.unsubscribe_time >= ? AND mg_order.unsubscribe_time <= ? THEN 1 END) * 100.0 / // NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_on_day_rate, // IFNULL( // CONCAT( // LEAST( // ROUND(COUNT(*) * 100.0 / NULLIF( // (SELECT COUNT(*) // FROM mg_transaction_log // WHERE verification_code != '' // AND channel_code = mg_order.channel_code // AND DATE(created_at) = DATE(mg_order.subscribe_time)), 0), 2), // 100.00 // ), // '%' // ), // '0.00%' // ) AS submission_success_rate`, // startTime, time.Now(), startTime, time.Now()). // Where("mg_order.subscribe_time >= ? AND mg_order.subscribe_time <= ?", startTime, endTime). // Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code"). // Order("mg_order.product_id, mg_order.channel_code, mg_order.subscribe_time DESC") // // if req.SkuCode != 0 { // qs = qs.Where("product_id = ?", req.SkuCode) // } // if req.Channel != "" { // qs = qs.Where("channel_code = ?", req.Channel) // } // // var err error // var data []models.MgHistoricalSummary // if req.IsExport == 1 { // err = qs.Find(&data).Error // } else { // err = qs.Limit(req.PageSize).Find(&data).Error // } // if err != nil { // return nil, err // } // // return data, nil //} //// 查询历史数据 //func (e MiGuDeployService) queryHistoricalSummary(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHistoricalSummary, error) { // qs := e.Orm.Model(&models.MgOrder{}). // Select(` // DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date, // mg_order.product_id, // mg_order.channel_code, // ( // SELECT COUNT(*) // FROM mg_transaction_log // WHERE verification_code != '' // AND channel_code = mg_order.channel_code // AND DATE(created_at) = DATE(mg_order.subscribe_time) // ) 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(*) 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(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( // (SELECT COUNT(*) // FROM mg_transaction_log // WHERE verification_code != '' // AND channel_code = mg_order.channel_code // AND DATE(created_at) = DATE(mg_order.subscribe_time)), 0), 2), // 100.00 // ), // '%' // ), // '0.00%' // ) AS submission_success_rate // `). // 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.product_id, mg_order.channel_code, mg_order.subscribe_time DESC") // // if req.SkuCode != 0 { // qs = qs.Where("product_id = ?", req.SkuCode) // } // if req.Channel != "" { // qs = qs.Where("channel_code = ?", req.Channel) // } // // var data []models.MgHistoricalSummary // var err error // if req.IsExport == 1 { // err = qs.Find(&data).Error // } else { // err = qs.Limit(req.PageSize).Find(&data).Error // } // if err != nil { // return nil, err // } // // return data, nil //} // 查询历史数据 func (e MiGuDeployService) queryHistoricalSummary(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHistoricalSummary, error) { qs := e.Orm.Model(&models.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(*) 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(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.product_id, mg_order.channel_code, mg_order.subscribe_time DESC") // 根据请求条件过滤 if req.SkuCode != 0 { qs = qs.Where("mg_order.product_id = ?", req.SkuCode) } if req.Channel != "" { qs = qs.Where("mg_order.channel_code = ?", req.Channel) } var data []models.MgHistoricalSummary var err error if req.IsExport == 1 { // 导出数据不限制分页 err = qs.Find(&data).Error } else { // 非导出时进行分页查询 err = qs.Limit(req.PageSize).Find(&data).Error } if err != nil { return nil, err } return data, nil } // 获取日期列表 func (e MiGuDeployService) getDateList(startTime, endTime string) ([]string, error) { var dateList []string currentDate, err := time.Parse("2006-01-02 15:04:05", startTime) if err != nil { return nil, fmt.Errorf("时间格式无效") } endDate, err := time.Parse("2006-01-02 15:04:05", endTime) if err != nil { return nil, fmt.Errorf("结束时间格式无效") } for currentDate.Before(endDate) { dateList = append(dateList, currentDate.Format("2006-01-02")) currentDate = currentDate.AddDate(0, 0, 1) } // 反转切片,确保日期是倒序的 for i := 0; i < len(dateList)/2; i++ { j := len(dateList) - i - 1 dateList[i], dateList[j] = dateList[j], dateList[i] } return dateList, nil } // 填充缺失日期 func (e MiGuDeployService) fillMissingDates(dateList []string, data []models.MgHistoricalSummary) []models.MgHistoricalSummary { dateMap := make(map[string]models.MgHistoricalSummary) for _, d := range data { dateMap[d.Date] = d } var result []models.MgHistoricalSummary for _, date := range dateList { if summary, ok := dateMap[date]; ok { result = append(result, summary) } else { result = append(result, models.MgHistoricalSummary{Date: date}) // 日期没有数据时,返回空数据 } } return result } // 获取日期列表 func (e MiGuDeployService) getDateListOnHome(startTime, endTime string) ([]string, error) { var dateList []string currentDate, err := time.Parse("2006-01-02 15:04:05", startTime) if err != nil { return nil, fmt.Errorf("时间格式无效") } endDate, err := time.Parse("2006-01-02 15:04:05", endTime) if err != nil { return nil, fmt.Errorf("结束时间格式无效") } for currentDate.Before(endDate) { dateList = append(dateList, currentDate.Format("2006-01-02")) currentDate = currentDate.AddDate(0, 0, 1) } return dateList, nil } // 填充缺失日期 func (e MiGuDeployService) fillMissingDatesOnHome(dateList []string, data []models.DailyData) []models.DailyData { dateMap := make(map[string]models.DailyData) for _, d := range data { dateMap[d.Date] = d } var result []models.DailyData for _, date := range dateList { if summary, ok := dateMap[date]; ok { result = append(result, summary) } else { result = append(result, models.DailyData{ Date: date, NewUserCount: 0, UnsubscribedUserCount: 0, UnsubscribedWithinOneHour: 0, UnsubscribeRate: "0.00%", UnsubscribeWithinOneHourRate: "0.00%", TotalCancelCount: 0, }) // 日期没有数据时,返回空数据 } } return result } // 获取最早和最晚日期 func (e MiGuDeployService) getDateRangeOnHome(req *models.HomepageDataSummaryReq) (time.Time, time.Time, int, error) { var minDate, maxDate time.Time var result struct { MinDate string `json:"min_date"` MaxDate string `json:"max_date"` } qs := e.Orm.Model(&models.MgOrder{}). Select("MIN(DATE(mg_order.subscribe_time)) AS min_date, MAX(DATE(mg_order.subscribe_time)) AS max_date") if req.ProductID != 0 { qs = qs.Where("product_id = ?", req.ProductID) } if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } err := qs.Scan(&result).Error if err != nil { return minDate, maxDate, 0, err } if result.MinDate == "" || result.MaxDate == "" { return minDate, maxDate, 0, err } // 解析日期 minDate, err = time.Parse("2006-01-02", result.MinDate) if err != nil { return minDate, maxDate, 0, fmt.Errorf("failed to parse min_date: %v", err) } maxDate, err = time.Parse("2006-01-02", result.MaxDate) if err != nil { return minDate, maxDate, 0, fmt.Errorf("failed to parse max_date: %v", err) } // 计算日期差 daysDiff := int(maxDate.Sub(minDate).Hours() / 24) if daysDiff != 0 { daysDiff += 1 } return minDate, maxDate, daysDiff, nil } // RealtimeSummaryList 当日实时汇总 // @Summary 当日实时汇总 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.RealtimeSummaryListReq true "当日实时汇总" // @Success 200 {object} models.RealtimeSummaryListResp // @Router /api/v1/admin/realtime_summary/list [post] func (e MiGuDeployService) RealtimeSummaryList(c *gin.Context) { fmt.Println("RealtimeSummaryList") err := e.MakeContext(c).MakeOrm().Errors if err != nil { fmt.Println("MakeContext err:", err) e.Logger.Error(err) return } req := &models.RealtimeSummaryListReq{} if c.ShouldBindJSON(req) != nil { logger.Errorf("para err") response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") return } resp := models.RealtimeSummaryListResp{ PageNum: req.PageNum, } // 分页处理 pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize if e.Orm == nil { fmt.Println("Orm is nil") } // 获取当天的起始时间和结束时间 today := time.Now().Format("2006-01-02") // 获取当天日期字符串 "yyyy-MM-dd" startDate := today + " 00:00:00" endDate := today + " 23:59:59" // 汇总查询,包括当日新增用户数 var count int64 var realtimeSummaryList []models.MgRealtimeSummary qs := e.Orm.Model(&models.MgOrder{}). Select(`product_id, channel_code, (SELECT COUNT(*) FROM mg_transaction_log WHERE verification_code != '' AND channel_code = mg_order.channel_code AND created_at >= ? AND created_at <= ?) 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 THEN 1 END) AS new_user_unsub_on_day, COUNT(CASE WHEN mg_order.created_at >= ? AND mg_order.created_at <= ? THEN 1 END) AS new_user_count, CONCAT(ROUND(COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) * 100.0 / NULLIF(COUNT(CASE WHEN mg_order.created_at >= ? AND mg_order.created_at <= ? THEN 1 END), 0), 2), '%') AS new_user_unsub_within_hour_rate, CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 THEN 1 END) * 100.0 / NULLIF(COUNT(CASE WHEN mg_order.created_at >= ? AND mg_order.created_at <= ? THEN 1 END), 0), 2), '%') AS new_user_unsub_on_day_rate`, startDate, endDate, startDate, endDate, startDate, endDate, startDate, endDate). Where("mg_order.created_at >= ? AND mg_order.created_at <= ?", startDate, endDate). Group("product_id, channel_code"). Order("product_id, channel_code"). Offset(pageNum * req.PageSize).Limit(req.PageSize) // 产品编号 if req.SkuCode != 0 { qs = qs.Where("product_id = ?", req.SkuCode) } // 渠道号 if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } // 获取数据 err = qs.Find(&realtimeSummaryList).Error if err != nil { logger.Errorf("RealtimeSummaryList query err: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } for i, value := range realtimeSummaryList { if value.SubmissionCount > 0 { // 避免除以零的错误 successRate := (float64(value.NewUserCount) / float64(value.SubmissionCount)) * 100 realtimeSummaryList[i].SubmissionSuccessRate = fmt.Sprintf("%.2f%%", successRate) // 格式化为百分比字符串 } else { realtimeSummaryList[i].SubmissionSuccessRate = "0.00%" // 提交数为 0 时,成功率为 0 } } // 查询总记录数 es := e.Orm.Model(&models.MgOrder{}). Select(`product_id, channel_code, (SELECT COUNT(*) FROM mg_transaction_log WHERE verification_code != '' AND channel_code = mg_order.channel_code AND created_at >= ? AND created_at <= ?) 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 THEN 1 END) AS new_user_unsub_on_day, COUNT(CASE WHEN mg_order.created_at >= ? AND mg_order.created_at <= ? THEN 1 END) AS new_user_count, CONCAT(ROUND(COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) * 100.0 / NULLIF(COUNT(CASE WHEN mg_order.created_at >= ? AND mg_order.created_at <= ? THEN 1 END), 0), 2), '%') AS new_user_unsub_within_hour_rate, CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 THEN 1 END) * 100.0 / NULLIF(COUNT(CASE WHEN mg_order.created_at >= ? AND mg_order.created_at <= ? THEN 1 END), 0), 2), '%') AS new_user_unsub_on_day_rate`, startDate, endDate, startDate, endDate, startDate, endDate, startDate, endDate). Where("mg_order.created_at >= ? AND mg_order.created_at <= ?", startDate, endDate). Group("product_id, channel_code"). Order("product_id, channel_code") err = es.Count(&count).Error if err != nil { logger.Errorf("count err: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 判断是否导出Excel if req.IsExport == 1 { // 调用导出Excel函数 url, err := models.ExportRealtimeSummaryToExcel(realtimeSummaryList, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } // 返回导出文件的URL地址 response.OK(c, map[string]string{"export_url": url}, "导出成功") return } // 返回数据 resp.List = realtimeSummaryList resp.Count = int(count) e.OK(resp, "") } // UserRetentionList 用户留存记录 // @Summary 用户留存记录 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.UserRetentionListReq true "用户留存记录" // @Success 200 {object} models.UserRetentionListResp // @Router /api/v1/admin/user_retention/list [post] func (e MiGuDeployService) UserRetentionList(c *gin.Context) { // 创建上下文和 ORM ctx := e.MakeContext(c) if err := ctx.MakeOrm().Errors; err != nil { e.Logger.Error(err) response.Error(c, http.StatusInternalServerError, err, "数据库错误") return } // 解析请求参数 req := &models.UserRetentionListReq{} if err := c.ShouldBindJSON(req); err != nil { response.Error(c, http.StatusBadRequest, err, "参数错误") return } resp := models.UserRetentionListResp{ PageNum: req.PageNum, } // 分页处理 pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize qs := e.Orm.Model(&models.MgOrder{}) //// 获取开始和结束时间 //retentionMonth := req.RetentionMonth // 例如 "2024-10" //if retentionMonth != "" { // //retentionMonth = time.Now().Format("2006-01") // // // 解析出年和月 // year, month, err := models.ParseYearMonth(retentionMonth) // if err != nil { // response.Error(c, http.StatusBadRequest, err, "日期格式错误") // return // } // // // 计算该月份的第一天和最后一天 // startDate := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) // 当月第一天 // endDate := startDate.AddDate(0, 1, 0).Add(-time.Second) // 当月最后一天的最后一毫秒 // qs = qs.Where("subscribe_time >= ? AND subscribe_time <= ?", startDate, endDate) //} // 处理产品编号 (SkuCode) 过滤条件 if req.SkuCode != 0 { qs = qs.Where("product_id = ?", req.SkuCode) } // 处理渠道名称 (Channel) 过滤条件 if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } // 定义返回的响应结构 var retentionList []models.MgUserRetention // 查询用户留存数据 err := qs. Select(`DATE_FORMAT(subscribe_time, '%Y-%m') AS retention_month, COUNT(phone_number) AS new_user_count, COUNT(CASE WHEN state = 1 THEN phone_number END) AS retained_user_count, channel_code, product_id, IFNULL(FORMAT(COUNT(CASE WHEN state = 1 THEN phone_number END) / NULLIF(COUNT(phone_number), 0) * 100, 2), 0) AS retention_rate`). Group("retention_month, channel_code, product_id"). Order("retention_month"). Find(&retentionList).Error if err != nil { e.Logger.Errorf("UserRetentionList query error: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 如果传递了月份参数,则在内存中过滤对应月份的数据 if req.RetentionMonth != "" { var filteredList []models.MgUserRetention for _, item := range retentionList { if item.RetentionMonth == req.RetentionMonth { filteredList = append(filteredList, item) } } retentionList = filteredList } for i := range retentionList { retentionList[i].RetentionRate += "%" } // 查询对应月份的最近2个月1号留存数据 // 现在遍历 retentionList,查询每个月份对应的最近1个月和最近2个月的留存数据 for i, item := range retentionList { // 获取当前记录的 RetentionMonth year, month, _ := models.ParseYearMonth(retentionList[i].RetentionMonth) // 计算当前月1号和上个月1号 currentYear := time.Now().Year() currentMonthFirstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) // 统计月的1号 currentMonthNextFirstDay := time.Date(year, month+1, 1, 0, 0, 0, 0, time.Local) // 统计月的次月1号 lastMonthFirstDay := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local) // 当前月1号 lastTwoMonthFirstDay := lastMonthFirstDay.AddDate(0, -1, 0) // 上个月1号 // 计算需要的时间变量:当月最后一天、上月2号、上月最后一天、上上月2号等 lastMonthSecondDay := lastMonthFirstDay.AddDate(0, 0, 1) // 上个月2号 lastTwoMonthSecondDay := lastTwoMonthFirstDay.AddDate(0, 0, 1) // 上上个月2号 // 获取最近1个月留存用户数 var lastMonthRetentionCount int64 if currentYear == year && month == time.Now().Month() { lastMonthRetentionCount = 0 } else { err = e.Orm.Model(&models.MgOrder{}). Where( e.Orm. Where("state = 1 AND created_at BETWEEN ? AND ?", currentMonthFirstDay, currentMonthNextFirstDay). Or("state = 2 AND unsubscribe_time > ? AND created_at BETWEEN ? AND ?", lastMonthSecondDay, currentMonthFirstDay, currentMonthNextFirstDay), ). Where("product_id = ? AND channel_code = ?", item.ProductID, item.ChannelCode). Count(&lastMonthRetentionCount).Error } // 获取最近2个月留存用户数 var lastTwoMonthRetentionCount int64 if currentYear == year && month == time.Now().Month() { lastTwoMonthRetentionCount = 0 } else if currentYear == year && month == time.Now().Month()-1 { lastTwoMonthRetentionCount = 0 } else { err = e.Orm.Model(&models.MgOrder{}). Where( e.Orm. Where("state = 1 AND created_at BETWEEN ? AND ?", currentMonthFirstDay, currentMonthNextFirstDay). Or("state = 2 AND unsubscribe_time > ? AND created_at BETWEEN ? AND ?", lastTwoMonthSecondDay, currentMonthFirstDay, currentMonthNextFirstDay), ). Where("product_id = ? AND channel_code = ?", item.ProductID, item.ChannelCode). Count(&lastTwoMonthRetentionCount).Error } // 设置最近1个月和最近2个月的留存数 retentionList[i].LastMonthDate = lastMonthFirstDay.Format("2006-01-02") retentionList[i].LastTwoMonthDate = lastTwoMonthFirstDay.Format("2006-01-02") retentionList[i].LastMonthRetentionCount = int(lastMonthRetentionCount) retentionList[i].LastTwoMonthRetentionCount = int(lastTwoMonthRetentionCount) // 计算最近1个月和最近2个月的留存率 if retentionList[i].NewUserCount > 0 { lastMonthRetentionRate := float64(lastMonthRetentionCount) / float64(retentionList[i].NewUserCount) * 100 lastTwoMonthRetentionRate := float64(lastTwoMonthRetentionCount) / float64(retentionList[i].NewUserCount) * 100 retentionList[i].LastMonthRetentionRate = fmt.Sprintf("%.2f%%", lastMonthRetentionRate) retentionList[i].LastTwoMonthRetentionRate = fmt.Sprintf("%.2f%%", lastTwoMonthRetentionRate) } } resp.List = retentionList resp.Count = len(retentionList) // Count should reflect the actual number of entries if req.IsExport == 1 { url, err := models.ExportUserRetentionToExcel(retentionList, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } response.OK(c, map[string]string{"export_url": url}, "导出成功") return } // 返回结果 response.OK(c, resp, "成功") } // UserDayRetentionList 用户留存记录(按天) // @Summary 用户留存记录(按天) // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.UserDayRetentionListReq true "用户留存记录(按天)" // @Success 200 {object} models.UserDayRetentionListResp // @Router /api/v1/admin/user_day_retention/list [post] func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) { // 创建上下文和 ORM ctx := e.MakeContext(c) if err := ctx.MakeOrm().Errors; err != nil { e.Logger.Error(err) response.Error(c, http.StatusInternalServerError, err, "数据库错误") return } // 解析请求参数 req := &models.UserDayRetentionListReq{} if err := c.ShouldBindJSON(req); err != nil { response.Error(c, http.StatusBadRequest, err, "参数错误") return } resp := models.UserDayRetentionListResp{ PageNum: req.PageNum, } // 分页处理 pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize // 获取新增用户数(在RetentionMonth的所有用户) var newUserCount int64 err := e.Orm.Table("mg_order"). Where("created_at >= ?", req.RetentionMonth+"-01 00:00:00"). Where("created_at < ?", req.RetentionMonth+"-31 23:59:59"). Where("product_id = ?", req.SkuCode). // 添加SkuCode条件 Where("channel_code = ?", req.Channel). // 添加Channel条件 Count(&newUserCount).Error if err != nil { e.Logger.Error(err) response.Error(c, http.StatusInternalServerError, err, "获取新增用户数失败") return } // 如果没有新增用户,则直接返回空结果 if newUserCount == 0 { resp.List = []models.MgUserDayRetention{} resp.Count = 0 e.OK(resp, "success") return } addData, err := models.GetMonthlyEffectiveUserStats(e.Orm, req.RetentionMonth, req.SkuCode, req.Channel) if err != nil { e.Logger.Error(err) response.Error(c, http.StatusInternalServerError, err, "获取当月留存数据失败") return } // 计算下个月的1号(留存查询从下个月1号开始) parsedMonth, err := time.Parse("2006-01", req.RetentionMonth) if err != nil { e.Logger.Error(err) response.Error(c, http.StatusBadRequest, err, "解析RetentionMonth失败") return } // 获取下个月的1号 nextMonth := parsedMonth.AddDate(0, 1, 0) // 加一个月 startDate := nextMonth.Format("2006-01-02") // 下个月1号 // 获取当前日期,作为退出条件 currentDate := time.Now().Format("2006-01-02") // 获取当前记录的 RetentionMonth year, month, _ := models.ParseYearMonth(req.RetentionMonth) // 计算当前月1号和上个月1号 currentMonthFirstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) // 统计月的1号 currentMonthNextFirstDay := time.Date(year, month+1, 1, 0, 0, 0, 0, time.Local) // 统计月的次月1号 // 初始化结果列表 var retentionData []models.MgUserDayRetention // 使用 goroutine 处理并发查询 var wg sync.WaitGroup // 创建一个 channel 来接收每一天的查询结果 //resultChan := make(chan models.MgUserDayRetention, 100) // 计算预计查询天数(或月份) var totalDays int if req.OnlyFirstDay { // 当前月的1号 now := time.Now() currentMonthFirst := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) // 计算从 nextMonth 到 currentMonthFirst 包含的1号数量 totalDays = countFirstDays(nextMonth, currentMonthFirst) } else { // 按天查询:计算两个日期之间的天数 startDateTime, _ := time.Parse("2006-01-02", startDate) currentTime, _ := time.Parse("2006-01-02", currentDate) totalDays = int(currentTime.Sub(startDateTime).Hours()/24) + 1 if totalDays <= 0 { totalDays = 1 } } resultChan := make(chan models.MgUserDayRetention, totalDays) // 控制并发数的最大值,避免数据库过载 sem := make(chan struct{}, 10) // 同时最多 10 个 goroutine 执行 // 根据 OnlyFirstDay 参数调整查询逻辑 if req.OnlyFirstDay { // 如果只查询每个月1号的数据 for { // 增加日期(开始日期自增一个月的1号) startDateTime, _ := time.Parse("2006-01-02", startDate) // 如果当前日期大于等于startDate,且是查询下一个月的1号,则退出 if startDateTime.After(time.Now()) { break } wg.Add(1) sem <- struct{}{} // 占一个信号,表示有一个 goroutine 正在执行 go func(date string) { defer wg.Done() defer func() { <-sem }() // 释放一个信号,表示该 goroutine 执行完毕 var retainedUserCount int64 var unsubOnDay int64 var localErr error // 使用局部变量 // 查询该天的留存用户数 localErr = e.Orm.Model(&models.MgOrder{}). Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay). Where("product_id = ?", req.SkuCode). // 添加SkuCode条件 Where("channel_code = ?", req.Channel). // 添加Channel条件 Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ? AND product_id = ? "+ "and channel_code = ?", date+" 23:59:59", currentMonthFirstDay, currentMonthNextFirstDay, req.SkuCode, req.Channel). Count(&retainedUserCount).Error // 查询该天的退订用户数 localErr = e.Orm.Table("mg_order"). Where("unsubscribe_time >= ?", date+" 00:00:00"). Where("unsubscribe_time <= ?", date+" 23:59:59"). Where("created_at >= ?", currentMonthFirstDay). Where("created_at < ?", currentMonthNextFirstDay). Where("state = 2"). // 状态为2表示退订 Where("product_id = ?", req.SkuCode). // 添加SkuCode条件 Where("channel_code = ?", req.Channel). // 添加Channel条件 Count(&unsubOnDay).Error if localErr != nil { e.Logger.Error(localErr) return } // 留存率计算:如果新增用户数为0,留存率为0 var retentionRate string if newUserCount > 0 { retentionRate = fmt.Sprintf("%.2f%%", float64(retainedUserCount)*100/float64(newUserCount)) } else { retentionRate = "0.00%" } // 将查询结果发送到 resultChan resultChan <- models.MgUserDayRetention{ Date: date, RetainedUserCount: int(retainedUserCount), RetentionRate: retentionRate, UserUnsubOnDay: int(unsubOnDay), } }(startDate) // 增加日期(开始日期自增一个月) startDate = startDateTime.AddDate(0, 1, 0).Format("2006-01-02") } } else { // 按天查询,直到留存数为 0 或查询到当前日期 for { // 增加日期(开始日期自增一天) startDateTime, _ := time.Parse("2006-01-02", startDate) if startDate > currentDate { break } wg.Add(1) sem <- struct{}{} // 占一个信号,表示有一个 goroutine 正在执行 go func(date string) { defer wg.Done() defer func() { <-sem }() // 释放一个信号,表示该 goroutine 执行完毕 var retainedUserCount int64 var unsubOnDay int64 var localErr error // 使用局部变量 // 查询该天的留存用户数 err = e.Orm.Model(&models.MgOrder{}). Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay). Where("product_id = ?", req.SkuCode). // 添加SkuCode条件 Where("channel_code = ?", req.Channel). // 添加Channel条件 Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ? AND product_id = ? "+ "and channel_code = ?", date+" 23:59:59", currentMonthFirstDay, currentMonthNextFirstDay, req.SkuCode, req.Channel). Count(&retainedUserCount).Error //retainedUserCount, localErr = getRetentionForDay(date, e.Orm, currentMonthFirstDay, currentMonthNextFirstDay) // 查询该天的退订用户数 localErr = e.Orm.Table("mg_order"). Where("unsubscribe_time >= ?", date+" 00:00:00"). Where("unsubscribe_time <= ?", date+" 23:59:59"). Where("created_at >= ?", currentMonthFirstDay). Where("created_at < ?", currentMonthNextFirstDay). Where("state = 2"). // 状态为2表示退订 Where("product_id = ?", req.SkuCode). // 添加SkuCode条件 Where("channel_code = ?", req.Channel). // 添加Channel条件 Count(&unsubOnDay).Error if localErr != nil { e.Logger.Error(localErr) return } // 留存率计算:如果新增用户数为0,留存率为0 var retentionRate string if newUserCount > 0 { retentionRate = fmt.Sprintf("%.2f%%", float64(retainedUserCount)*100/float64(newUserCount)) } else { retentionRate = "0.00%" } // 将查询结果发送到 resultChan resultChan <- models.MgUserDayRetention{ Date: date, RetainedUserCount: int(retainedUserCount), RetentionRate: retentionRate, UserUnsubOnDay: int(unsubOnDay), } }(startDate) // 增加日期(开始日期自增一天) startDate = startDateTime.AddDate(0, 0, 1).Format("2006-01-02") } } // 等待所有 goroutine 执行完毕 wg.Wait() close(resultChan) // 从 channel 中获取所有查询结果,并汇总 for result := range resultChan { retentionData = append(retentionData, result) } var addDayRetention models.MgUserDayRetention addDayRetention.Date = addData.Date addDayRetention.RetainedUserCount = addData.ValidUserCount addDayRetention.RetentionRate = addData.EffectiveRate addDayRetention.UserUnsubOnDay = int(addData.UnsubscribedToday) retentionData = append(retentionData, addDayRetention) // 排序(按日期升序) sort.SliceStable(retentionData, func(i, j int) bool { return retentionData[i].Date > retentionData[j].Date }) // 分页处理 totalRecords := len(retentionData) // 总记录数 startIdx := pageNum * req.PageSize endIdx := startIdx + req.PageSize if startIdx > totalRecords { resp.List = []models.MgUserDayRetention{} resp.Count = totalRecords e.OK(resp, "success") return } if endIdx > totalRecords { endIdx = totalRecords } // 返回分页后的数据 resp.List = retentionData[startIdx:endIdx] resp.Count = totalRecords e.OK(resp, "success") } // monthsBetween 返回 start 和 end 之间完整的月份数。 // 如果 end 的日期在 start 的日期之前(不满一个月),则不计入。 func monthsBetween(start, end time.Time) int { // 保证 start 在前,end 在后 if start.After(end) { start, end = end, start } y1, m1, d1 := start.Date() y2, m2, d2 := end.Date() // 计算年份和月份的差异 months := (y2-y1)*12 + int(m2-m1) // 如果 end 的日子小于 start 的日子,说明还没有满一个月 if d2 < d1 { months-- } return months } // countFirstDays 返回从 start 到 end(均为每月1号)之间包含的月份数。 // 例如:start = 2024-11-01, end = 2025-02-01,则返回 4(分别是 2024-11-01, 2024-12-01, 2025-01-01, 2025-02-01)。 func countFirstDays(start, end time.Time) int { // 如果 start 在 end 之后,返回 0 if start.After(end) { return 1 } y1, m1, _ := start.Date() y2, m2, _ := end.Date() // 计算月份差,注意此处 start 和 end 都应为当月1号 return (y2-y1)*12 + int(m2-m1) + 1 } // SysChannelList 查询系统渠道编码 // @Summary 查询系统渠道编码 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.SysChannelListReq true "查询系统渠道编码" // @Success 200 {object} models.SysChannelListResp // @Router /api/v1/admin/sys_channel/list [post] func (e MiGuDeployService) SysChannelList(c *gin.Context) { err := e.MakeContext(c).MakeOrm().Errors if err != nil { e.Logger.Error(err) response.Error(c, http.StatusInternalServerError, err, "数据库连接失败") return } req := &models.SysChannelListReq{} if c.ShouldBindJSON(req) != nil { e.Logger.Error("参数解析失败") response.Error(c, http.StatusBadRequest, errors.New("参数错误"), "参数错误") return } resp := models.SysChannelListResp{ PageNum: req.PageNum, PageSize: req.PageSize, } pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } var channels []models.ChannelData // 1. 查询mg_product表的ChannelCode var productChannelCodes []string var mgOrders []models.MgProduct err = e.Orm.Model(&models.MgProduct{}). Select("channel_api_id"). Find(&mgOrders).Error if err != nil { e.Logger.Error("查询产品渠道编码失败:", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 2. 拆分逗号分隔的ChannelCode,并去除空白 for _, order := range mgOrders { channelCodes := strings.Split(order.ChannelApiID, ",") for _, code := range channelCodes { code = strings.TrimSpace(code) // 去掉前后空白 if code != "" { productChannelCodes = append(productChannelCodes, code) } } } // 3. 查询mg_channel表的MainChannelCode和SubChannelCode var mgChannels []models.MgChannel err = e.Orm.Model(&models.MgChannel{}). Select("main_channel_code, sub_channel_code"). Find(&mgChannels).Error if err != nil { e.Logger.Error("查询渠道列表失败:", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 4. 构建所有组合,并剔除重复的渠道编码 channelSet := make(map[string]struct{}) // 添加产品表中的渠道编码 for _, code := range productChannelCodes { channelSet[code] = struct{}{} } // 添加渠道表中的主渠道和子渠道编码 for _, channel := range mgChannels { channelSet[channel.MainChannelCode] = struct{}{} channelSet[channel.SubChannelCode] = struct{}{} } // 5. 转换为ChannelData结构体并分页 for code := range channelSet { if code != "" { // 跳过空的channel_code channels = append(channels, models.ChannelData{ ChannelCode: code, }) } } // 总数 resp.Count = len(channels) // 分页 startIndex := pageNum * req.PageSize endIndex := startIndex + req.PageSize if startIndex > len(channels) { startIndex = len(channels) } if endIndex > len(channels) { endIndex = len(channels) } resp.List = channels[startIndex:endIndex] e.OK(resp, "") } // HomepageDataSummary 查询首页汇总数据 // @Summary 查询首页汇总数据 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.HomepageDataSummaryReq true "查询首页汇总数据" // @Success 200 {object} models.HomepageDataSummaryResp // @Router /api/v1/admin/home/data [post] func (e MiGuDeployService) HomepageDataSummary(c *gin.Context) { fmt.Println("HomepageDataSummary") err := e.MakeContext(c).MakeOrm().Errors if err != nil { fmt.Println("MakeContext err:", err) e.Logger.Error(err) return } // 请求参数绑定 req := &models.HomepageDataSummaryReq{} if c.ShouldBindJSON(req) != nil { logger.Errorf("para err") response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") return } resp := models.HomepageDataSummaryResp{} var summaryData models.SummaryData // 设置开始时间和结束时间 startTime := req.StartTime endTime := req.EndTime if startTime == "" { startTime = "1970-01-01 00:00:00" } if endTime == "" { endTime = time.Now().Format("2006-01-02 15:04:05") } // 查询汇总数据 qs := e.Orm.Model(&models.MgOrder{}). Select(`COUNT(*) AS total_user_count, COUNT(CASE WHEN state = 2 THEN 1 END) AS unsubscribed_user_count, COUNT(CASE WHEN is_one_hour_cancel = 1 THEN 1 END) AS one_hour_unsubscribed_user_count`). Where("subscribe_time >= ? AND subscribe_time <= ?", startTime, endTime) // 添加产品和渠道的过滤条件 if req.ProductID != 0 { qs = qs.Where("product_id = ?", req.ProductID) } if req.Channel != "" { qs = qs.Where("channel_code = ?", req.Channel) } err = qs.Find(&summaryData).Error if err != nil { logger.Errorf("HomepageDataSummary query err: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 计算留存用户数 summaryData.RetainedUserCount = summaryData.TotalUserCount - summaryData.UnsubscribedUserCount // 计算留存率 if summaryData.TotalUserCount > 0 { summaryData.RetentionRate = fmt.Sprintf("%.2f%%", float64(summaryData.RetainedUserCount)*100.0/float64(summaryData.TotalUserCount)) } else { summaryData.RetentionRate = "0.00%" } // 计算退订率 if summaryData.TotalUserCount > 0 { summaryData.UnsubscribeRate = fmt.Sprintf("%.2f%%", float64(summaryData.UnsubscribedUserCount)*100.0/float64(summaryData.TotalUserCount)) summaryData.OneHourUnsubscribeRate = fmt.Sprintf("%.2f%%", float64(summaryData.OneHourUnsubscribedUserCount)*100.0/float64(summaryData.TotalUserCount)) } else { summaryData.UnsubscribeRate = "0.00%" summaryData.OneHourUnsubscribeRate = "0.00%" } resp.Summary = summaryData // 查询每日数据 var dailyDataList []models.DailyData dailyQuery := e.Orm.Model(&models.MgOrder{}). Select(`DATE_FORMAT(subscribe_time, '%Y-%m-%d') AS date, COUNT(*) AS new_user_count, COUNT(CASE WHEN state = 2 THEN 1 END) AS unsubscribed_user_count, COUNT(CASE WHEN is_one_hour_cancel = 1 THEN 1 END) AS unsubscribed_within_one_hour`). Where("subscribe_time >= ? AND subscribe_time <= ?", startTime, endTime) // 处理产品编号 (SkuCode) 过滤条件 if req.ProductID != 0 { dailyQuery = dailyQuery.Where("product_id = ?", req.ProductID) } // 处理渠道名称 (Channel) 过滤条件 if req.Channel != "" { dailyQuery = dailyQuery.Where("channel_code = ?", req.Channel) } err = dailyQuery.Group("DATE(subscribe_time)"). Order("DATE(subscribe_time)"). Find(&dailyDataList).Error if err != nil { logger.Errorf("DailyData query err: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } // 计算每日退订率 for i := range dailyDataList { if dailyDataList[i].NewUserCount > 0 { dailyDataList[i].UnsubscribeRate = fmt.Sprintf("%.2f%%", float64(dailyDataList[i].UnsubscribedUserCount)*100.0/float64(dailyDataList[i].NewUserCount)) dailyDataList[i].UnsubscribeWithinOneHourRate = fmt.Sprintf("%.2f%%", float64(dailyDataList[i].UnsubscribedWithinOneHour)*100.0/float64(dailyDataList[i].NewUserCount)) } else { dailyDataList[i].UnsubscribeRate = "0.00%" dailyDataList[i].UnsubscribeWithinOneHourRate = "0.00%" } } // 获取日期范围 minDate, maxDate, nCount, err := e.getDateRangeOnHome(req) if err != nil { response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") return } if nCount == 0 { e.OK(resp, "") return } if req.StartTime != "" && req.EndTime != "" { startDate, err := time.Parse(models.MiGuTimeFormat, req.StartTime) if err != nil { response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") return } endDate, err := time.Parse(models.MiGuTimeFormat, req.EndTime) if err != nil { response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") return } if startDate.Before(minDate) { startDate = minDate } if endDate.After(maxDate) { endDate = maxDate } startTime = startDate.Format("2006-01-02") + " 00:00:00" endTime = endDate.Format("2006-01-02") + " 23:59:59" } else { startTime = minDate.Format("2006-01-02") + " 00:00:00" endTime = time.Now().Format(models.MiGuTimeFormat) } // 补充缺失的日期 dateList, err := e.getDateListOnHome(startTime, endTime) if err != nil { response.Error(c, http.StatusBadRequest, err, "日期格式无效") return } finalList := e.fillMissingDatesOnHome(dateList, dailyDataList) dailyDataList = finalList // 查询每日退订合计 // 增加TotalCancelCount字段的查询 var dailyCancelDataList []struct { Date string `json:"date"` // 日期 TotalCancelCount int64 `json:"total_cancel_count"` // 每日退订用户数合计 } cancelQuery := e.Orm.Model(&models.MgOrder{}). Select(`DATE_FORMAT(unsubscribe_time, '%Y-%m-%d') AS date, COUNT(*) AS total_cancel_count`). Where("state = 2 AND unsubscribe_time IS NOT NULL AND unsubscribe_time >= ? AND unsubscribe_time <= ?", startTime, endTime) // 处理产品编号 (SkuCode) 过滤条件 if req.ProductID != 0 { cancelQuery = cancelQuery.Where("product_id = ?", req.ProductID) } // 处理渠道名称 (Channel) 过滤条件 if req.Channel != "" { cancelQuery = cancelQuery.Where("channel_code = ?", req.Channel) } err = cancelQuery.Group("DATE(unsubscribe_time)"). Order("DATE(unsubscribe_time)"). Find(&dailyCancelDataList).Error if err != nil { logger.Errorf("DailyCancelData query err: %#v", err) response.Error(c, http.StatusInternalServerError, err, "查询失败") return } for i := range dailyDataList { for _, cancelData := range dailyCancelDataList { if dailyDataList[i].Date == cancelData.Date { dailyDataList[i].TotalCancelCount = cancelData.TotalCancelCount break } } } resp.DailyDataList = dailyDataList // 返回结果 e.OK(resp, "") } // CalculateRevenueAnalysis 营收分析 // @Summary 营收分析 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.RetentionMonthsReq true "营收分析" // @Success 200 {object} models.RetentionMonthsResp // @Router /api/v1/admin/home/revenue_analysis [post] func (e MiGuDeployService) CalculateRevenueAnalysis(c *gin.Context) { fmt.Println("CalculateRevenueAnalysis") err := e.MakeContext(c).MakeOrm().Errors if err != nil { fmt.Println("MakeContext err:", err) e.Logger.Error(err) return } // 绑定请求参数 req := &models.RetentionMonthsReq{} if err := c.ShouldBindJSON(req); err != nil { response.Error(c, http.StatusBadRequest, err, "参数错误") return } // 构建查询 query := e.Orm.Model(&models.MgOrder{}) // 查询条件 if req.StartTime != "" && req.EndTime != "" { query = query.Where("subscribe_time >= ? AND subscribe_time <= ?", req.StartTime, req.EndTime) } if req.ProductID != 0 { query = query.Where("product_id = ?", req.ProductID) } if req.Channel != "" { query = query.Where("channel_code = ?", req.Channel) } //// 获取每个月的新用户数和有效用户数,以下代码是按退订时间超过1天来统计数据 var result []models.MonthlyRetention err = query. Select(`DATE_FORMAT(subscribe_time, '%Y-%m') AS month, COUNT(DISTINCT phone_number) AS new_user_count, COUNT(DISTINCT CASE WHEN unsubscribe_time IS NULL OR TIMESTAMPDIFF(HOUR, subscribe_time, unsubscribe_time) > 24 THEN phone_number END) AS valid_users_count`). Group("month"). Order("month"). Find(&result).Error ////以下代码是按非1小时退订来统计数据 //err = query. // Select(`DATE_FORMAT(subscribe_time, '%Y-%m') AS month, // COUNT(DISTINCT phone_number) AS new_user_count, // COUNT(DISTINCT CASE // WHEN is_one_hour_cancel != 1 // THEN phone_number END) AS valid_users_count`). // Group("month"). // Order("month"). // Find(&result).Error //if err != nil { // response.Error(c, http.StatusInternalServerError, err, "查询失败") // return //} // 确保有数据 if len(result) <= 0 { resp := models.RetentionMonthsResp{} response.OK(c, resp, "查询成功") } earliestMonth := result[0].Month // 从查询结果中获取最早的月份 currentMonth := time.Now().Format("2006-01") // 获取当前月份 // 生成所有月份列表 allMonths := generateMonthRange(earliestMonth, currentMonth) // 用 map 记录已有数据 monthMap := make(map[string]models.MonthlyRetention) for _, data := range result { monthMap[data.Month] = data } // 确保所有月份都有数据 var completeResult []models.MonthlyRetention for _, month := range allMonths { if data, exists := monthMap[month]; exists { completeResult = append(completeResult, data) } else { // 补全没有数据的月份 completeResult = append(completeResult, models.MonthlyRetention{ Month: month, NewUserCount: 0, ValidUsersCount: 0, RetainedUsersCount: 0, TotalValidUsersCount: 0, }) } } result = completeResult // 更新最终结果 // 查询每个月的上个月未退订用户数 for i := 0; i < len(result); i++ { tempCurrentMonth := result[i].Month // 查询当前月之前所有月份未退订的用户数 totalValidUsers, err := GetTotalValidUsers(e.Orm, tempCurrentMonth, req.ProductID, req.Channel) if err != nil { response.Error(c, http.StatusInternalServerError, err, "未退订用户查询失败") return } // 查询当前月之前所有月份在本月2号之后退订的用户数 totalUnsubscribedUsers, err := GetTotalUnsubscribedUsers(e.Orm, tempCurrentMonth, req.ProductID, req.Channel) if err != nil { response.Error(c, http.StatusInternalServerError, err, "退订用户查询失败") return } result[i].RetainedUsersCount += totalValidUsers + totalUnsubscribedUsers result[i].TotalValidUsersCount = result[i].RetainedUsersCount + result[i].ValidUsersCount //// 获取当前记录的月份 //currentMonth := result[i].Month // //// 计算上个月的月份字符串 //currentMonthDate, _ := time.Parse("2006-01", currentMonth) //lastMonth := currentMonthDate.AddDate(0, -1, 0).Format("2006-01") // //// 查询上个月未退订的用户数 //var lastMonthValidUsersCount int64 //err = e.Orm.Model(&models.MgOrder{}). // Where("unsubscribe_time IS NULL"). // Where("DATE_FORMAT(subscribe_time, '%Y-%m') = ?", lastMonth). // Count(&lastMonthValidUsersCount).Error //if err != nil { // response.Error(c, http.StatusInternalServerError, err, "上个月数据查询失败") // return //} // //// 将上个月的未退订用户数累加到当前月的有效用户数 //result[i].ValidUsersCount += int(lastMonthValidUsersCount) } // 计算总的新用户数和有效用户数 totalNewUserCount := 0 totalValidUsers := 0 for _, data := range result { totalNewUserCount += data.NewUserCount totalValidUsers += data.TotalValidUsersCount } // 返回结果 resp := models.RetentionMonthsResp{ TotalNewUsers: totalNewUserCount, // 添加总新用户数 TotalValidUsers: totalValidUsers, // 添加有效用户数 MonthlyRetentionData: result, } if req.IsExport == 1 { url, err := models.ExportRevenueAnalysisToExcel(resp, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } response.OK(c, map[string]string{"export_url": url}, "导出成功") return } response.OK(c, resp, "查询成功") } // 生成从 start 到 end 之间的所有月份 func generateMonthRange(start, end string) []string { startTime, _ := time.Parse("2006-01", start) endTime, _ := time.Parse("2006-01", end) var months []string for t := startTime; !t.After(endTime); t = t.AddDate(0, 1, 0) { months = append(months, t.Format("2006-01")) } return months } // GetTotalValidUsers 计算所有之前月份的未退订用户总数 func GetTotalValidUsers(db *gorm.DB, currentMonth string, productID int, channel string) (int, error) { var totalValidUsers int // 基本查询条件 query := ` SELECT COUNT(*) FROM mg_order WHERE unsubscribe_time IS NULL AND DATE_FORMAT(subscribe_time, '%Y-%m') < ? ` // 动态添加 productID 和 channel 查询条件 var args []interface{} args = append(args, currentMonth) if productID != 0 { query += " AND product_id = ?" args = append(args, productID) } if channel != "" { query += " AND channel_code = ?" args = append(args, channel) } // 执行查询 err := db.Raw(query, args...).Scan(&totalValidUsers).Error if err != nil { return 0, err } return totalValidUsers, nil } // GetTotalUnsubscribedUsers 计算所有之前月份在本月2号之后退订的用户总数 func GetTotalUnsubscribedUsers(db *gorm.DB, currentMonth string, productID int, channel string) (int, error) { var totalUnsubscribedUsers int // 计算当前月份的 2 号 00:00:00 currentMonthFirstDay := currentMonth + "-02 00:00:00" // 基本查询条件 query := ` SELECT COUNT(*) FROM mg_order WHERE unsubscribe_time >= ? AND DATE_FORMAT(subscribe_time, '%Y-%m') < ? ` // 动态添加 productID 和 channel 查询条件 var args []interface{} args = append(args, currentMonthFirstDay, currentMonth) if productID != 0 { query += " AND product_id = ?" args = append(args, productID) } if channel != "" { query += " AND channel_code = ?" args = append(args, channel) } // 执行查询 err := db.Raw(query, args...).Scan(&totalUnsubscribedUsers).Error if err != nil { return 0, err } return totalUnsubscribedUsers, nil } // AddChannel 新增渠道 // @Summary 新增渠道 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.AddChannelReq true "新增渠道" // @Success 200 {object} models.AddChannelResp // @Router /api/v1/admin/channel/add [post] func (e MiGuDeployService) AddChannel(c *gin.Context) { fmt.Println("AddChannel called") err := e.MakeContext(c).MakeOrm().Errors if err != nil { e.Logger.Error(err) response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") return } req := &models.AddChannelReq{} if err := c.ShouldBindJSON(req); err != nil { e.Logger.Errorf("para err: %v", err) response.Error(c, http.StatusBadRequest, errors.New("参数错误"), "参数错误") return } // Validate required fields if req.ProductID == 0 || req.MainChannelCode == "" || req.SubscribeURL == "" { response.Error(c, http.StatusBadRequest, errors.New("必填字段缺失"), "必填字段缺失") return } channel := models.MgChannel{ ProductID: req.ProductID, MainChannelCode: req.MainChannelCode, SubChannelCode: req.SubChannelCode, SubscribeURL: req.SubscribeURL, UnsubscribeURL: req.UnsubscribeURL, Status: req.Status, Remarks: req.Remarks, } if err := e.Orm.Create(&channel).Error; err != nil { e.Logger.Errorf("create channel err: %v", err) response.Error(c, http.StatusInternalServerError, err, "创建失败") return } resp := models.AddChannelResp{ChannelID: channel.ID} e.OK(resp, "渠道创建成功") } //// UpdateChannel 更新渠道 //// @Summary 更新渠道 //// @Tags 2024-咪咕-管理后台 //// @Produce json //// @Accept json //// @Param request body models.UpdateChannelReq true "更新渠道请求" //// @Success 200 {object} models.UpdateChannelResp //// @Router /api/v1/admin/channel/update [post] //func (e MiGuDeployService) UpdateChannel(c *gin.Context) { // fmt.Println("UpdateChannel called") // err := e.MakeContext(c).MakeOrm().Errors // if err != nil { // e.Logger.Error(err) // response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") // return // } // // req := &models.UpdateChannelReq{} // if err := c.ShouldBindJSON(req); err != nil { // e.Logger.Errorf("para err: %v", err) // response.Error(c, http.StatusBadRequest, errors.New("参数错误"), "参数错误") // return // } // // // Validate required fields // if req.ID == 0 { // response.Error(c, http.StatusBadRequest, errors.New("必填字段缺失"), "必填字段缺失") // return // } // // channel := models.MgChannel{} // if err := e.Orm.First(&channel, req.ID).Error; err != nil { // e.Logger.Errorf("channel not found err: %v", err) // response.Error(c, http.StatusNotFound, err, "渠道未找到") // return // } // // // Update fields // if req.MainChannelCode != "" { // channel.MainChannelCode = req.MainChannelCode // } // if req.SubChannelCode != "" { // channel.SubChannelCode = req.SubChannelCode // } // if req.SubscribeURL != "" { // channel.SubscribeURL = req.SubscribeURL // } // if req.UnsubscribeURL != "" { // channel.UnsubscribeURL = req.UnsubscribeURL // } // if req.Status != 0 { // channel.Status = req.Status // } // channel.Remarks = req.Remarks // // if err := e.Orm.Save(&channel).Error; err != nil { // e.Logger.Errorf("update channel err: %v", err) // response.Error(c, http.StatusInternalServerError, err, "更新失败") // return // } // // e.OK(nil, "渠道更新成功") //} // //// DeleteChannel 删除渠道 //// @Summary 删除渠道 //// @Tags 2024-咪咕-管理后台 //// @Produce json //// @Accept json //// @Param request body models.DeleteChannelReq true "删除渠道请求" //// @Success 200 {object} models.DeleteChannelResp //// @Router /api/v1/admin/channel/delete [post] //func (e MiGuDeployService) DeleteChannel(c *gin.Context) { // fmt.Println("DeleteChannel called") // err := e.MakeContext(c).MakeOrm().Errors // if err != nil { // e.Logger.Error(err) // response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") // return // } // // req := &models.DeleteChannelReq{} // if err := c.ShouldBindJSON(req); err != nil { // e.Logger.Errorf("para err: %v", err) // response.Error(c, http.StatusBadRequest, errors.New("参数错误"), "参数错误") // return // } // // // Validate required fields // if req.ID == 0 { // response.Error(c, http.StatusBadRequest, errors.New("必填字段缺失"), "必填字段缺失") // return // } // // if err := e.Orm.Delete(&models.MgChannel{}, req.ID).Error; err != nil { // e.Logger.Errorf("delete channel err: %v", err) // response.Error(c, http.StatusInternalServerError, err, "删除失败") // return // } // // e.OK(nil, "渠道删除成功") //} // AddProduct 添加新产品 // @Summary 添加新产品 // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.AddProductReq true "新增产品" // @Success 200 {object} models.AddProductResp // @Router /api/v1/admin/product/add [post] func (e MiGuDeployService) AddProduct(c *gin.Context) { fmt.Println("AddProduct") err := e.MakeContext(c).MakeOrm().Errors if err != nil { e.Logger.Error(err) response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") return } req := &models.AddProductReq{} if err := c.ShouldBindJSON(req); err != nil { logger.Errorf("para err") response.Error(c, http.StatusBadRequest, errors.New("参数错误"), "参数错误") return } product := models.MgProduct{ Name: req.Name, UniqueCode: req.UniqueCode, SkuName: req.SkuName, BillingPointID: req.BillingPointID, ChannelCode: req.ChannelCode, ProductApiID: req.ProductApiID, ChannelApiID: req.ChannelApiID, OfficialPage: req.OfficialPage, } if err := e.Orm.Create(&product).Error; err != nil { logger.Errorf("AddProduct err:%#v", err) response.Error(c, http.StatusInternalServerError, err, "创建失败") return } e.OK(models.AddProductResp{ID: product.ID}, "创建成功") } //// UpdateProduct 修改产品 //// @Summary 修改产品 //// @Tags 2024-咪咕-管理后台 //// @Produce json //// @Accept json //// @Param request body models.UpdateProductReq true "修改产品" //// @Success 200 {object} models.UpdateProductResp //// @Router /api/v1/admin/product/update [post] //func (e MiGuDeployService) UpdateProduct(c *gin.Context) { // fmt.Println("UpdateProduct") // err := e.MakeContext(c).MakeOrm().Errors // if err != nil { // e.Logger.Error(err) // response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") // return // } // // req := &models.UpdateProductReq{} // if err := c.ShouldBindJSON(req); err != nil { // logger.Errorf("para err") // response.Error(c, http.StatusBadRequest, errors.New("参数错误"), "参数错误") // return // } // // product := models.MgProduct{} // if err := e.Orm.First(&product, req.ID).Error; err != nil { // logger.Errorf("Product not found: %#v", err) // response.Error(c, http.StatusNotFound, err, "产品未找到") // return // } // // // 更新产品信息 // product.Name = req.Name // product.UniqueCode = req.UniqueCode // product.SkuName = req.SkuName // product.BillingPointID = req.BillingPointID // product.ChannelCode = req.ChannelCode // product.ProductApiID = req.ProductApiID // product.ChannelApiID = req.ChannelApiID // product.OfficialPage = req.OfficialPage // // if err := e.Orm.Save(&product).Error; err != nil { // logger.Errorf("UpdateProduct err:%#v", err) // response.Error(c, http.StatusInternalServerError, err, "更新失败") // return // } // // e.OK(models.UpdateProductResp{ID: product.ID}, "更新成功") //} // //// DeleteProduct 删除产品 //// @Summary 删除产品 //// @Tags 2024-咪咕-管理后台 //// @Produce json //// @Accept json //// @Param request body models.DeleteProductReq true "删除产品" //// @Success 200 {object} nil //// @Router /api/v1/admin/product/delete [post] //func (e MiGuDeployService) DeleteProduct(c *gin.Context) { // fmt.Println("DeleteProduct") // err := e.MakeContext(c).MakeOrm().Errors // if err != nil { // e.Logger.Error(err) // response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") // return // } // // req := &models.DeleteProductReq{} // if err := c.ShouldBindJSON(req); err != nil { // logger.Errorf("para err") // response.Error(c, http.StatusBadRequest, errors.New("参数错误"), "参数错误") // return // } // // if err := e.Orm.Delete(&models.MgProduct{}, req.ID).Error; err != nil { // logger.Errorf("DeleteProduct err:%#v", err) // response.Error(c, http.StatusInternalServerError, err, "删除失败") // return // } // // e.OK("", "删除成功") //} //// ImportExcelToMgOrderHandler 处理Excel文件导入请求 //// @Summary 导入订单Excel文件 //// @Tags 2024-咪咕-管理后台 //// @Accept multipart/form-data //// @Produce json //// @Param file formData file true "Excel文件" //// @Success 200 {object} map[string]string{"message": "导入成功"} //// @Router /api/v1/admin/order/import [post] //func (e MiGuDeployService) ImportExcelToMgOrderHandler(c *gin.Context) { // err := e.MakeContext(c).MakeOrm().Errors // if err != nil { // e.Logger.Error(err) // response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") // return // } // // // 从请求中获取文件 // file, err := c.FormFile("file") // if err != nil { // c.JSON(http.StatusBadRequest, gin.H{"error": "无法读取文件"}) // return // } // // // 打开上传的文件 // fileStream, err := file.Open() // if err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "无法打开文件"}) // return // } // defer fileStream.Close() // // // 创建 CSV 阅读器 // reader := csv.NewReader(fileStream) // reader.LazyQuotes = true // // // 跳过 CSV 文件的标题行 // if _, err := reader.Read(); err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "无法读取CSV标题行"}) // return // } // // //nRow := 0 // // 逐行读取 CSV 并插入数据库 // for { // //nRow++ // row, err := reader.Read() // if err == io.EOF { // break // } // if err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "读取CSV文件失败"}) // return // } // // // 检查数据是否齐全 // if len(row) < 3 { // continue // 跳过数据不全的行 // } // // // 解析订阅时间 // subscribeTime, err := time.Parse("2006-01-02 15:04:05", row[1]) // if err != nil { // fmt.Printf("解析时间错误: %v\n", err) // continue // } // // //const cutoffTimeStr = "2024-10-18 18:58:00" // //cutoffTime, _ := time.Parse("2006-01-02 15:04:05", cutoffTimeStr) // //// 判断是否超过截止时间 // //if subscribeTime.After(cutoffTime) { // // fmt.Printf("跳过超过截止时间的记录: %v\n", subscribeTime) // // continue // //} // // // 将时间转换为 UTC+08:00 // // 将时间往前推8小时 // localTime := subscribeTime.Add(-8 * time.Hour) // // tempOrderNo := models.GetExcelOrderSerial(e.Orm, subscribeTime) // // // 创建MgOrder对象 // order := models.MgOrderCopy{ // ProductID: 2, // ChannelCode: "00211NV", // OrderSerial: tempOrderNo, // SubscribeTime: &localTime, // PhoneNumber: row[0], // ChannelTradeNo: tempOrderNo, // ExternalOrderID: tempOrderNo, // State: 1, // } // // if row[2] == "未包月" { // 1小时内退订 // order.IsOneHourCancel = 1 // order.State = 2 // unsubscribeTime := localTime.Add(30 * time.Minute) // order.UnsubscribeTime = &unsubscribeTime // } // // order.CreatedAt = localTime // order.UpdatedAt = localTime // order.SM4PhoneNumber, _ = tools.SM4Encrypt(models.SM4KEy, order.PhoneNumber) // // // 插入到数据库 // if err := e.Orm.Create(&order).Error; err != nil { // fmt.Printf("插入订单数据失败: %v\n", err) // continue // } // // fmt.Println("order is:", order) // //if nRow > 4 { // // break // //} // } // // // 返回成功消息 // c.JSON(http.StatusOK, gin.H{"message": "导入成功"}) //} // //// ImportExcelToMgOrderHandlerUpdate 处理Excel文件导入请求 //// @Summary 导入订单Excel退订文件 //// @Tags 2024-咪咕-管理后台 //// @Accept multipart/form-data //// @Produce json //// @Param file formData file true "Excel文件" //// @Success 200 {object} map[string]string{"message": "导入成功"} //// @Router /api/v1/admin/order/import_update [post] //func (e MiGuDeployService) ImportExcelToMgOrderHandlerUpdate(c *gin.Context) { // err := e.MakeContext(c).MakeOrm().Errors // if err != nil { // e.Logger.Error(err) // response.Error(c, http.StatusInternalServerError, err, "创建上下文失败") // return // } // // // 从请求中获取文件 // file, err := c.FormFile("file") // if err != nil { // c.JSON(http.StatusBadRequest, gin.H{"error": "无法读取文件"}) // return // } // // // 打开上传的文件 // fileStream, err := file.Open() // if err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "无法打开文件"}) // return // } // defer fileStream.Close() // // // 创建 CSV 阅读器 // reader := csv.NewReader(fileStream) // reader.LazyQuotes = true // // // 跳过 CSV 文件的标题行 // if _, err := reader.Read(); err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "无法读取CSV标题行"}) // return // } // // //nRow := 0 // // 逐行读取 CSV 并插入数据库 // for { // //nRow++ // row, err := reader.Read() // if err == io.EOF { // break // } // if err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "读取CSV文件失败"}) // return // } // // // 检查数据是否齐全 // if len(row) < 3 { // continue // 跳过数据不全的行 // } // // // 将时间往前推8小时 // //localTime := subscribeTime.Add(-8 * time.Hour) // // if !(row[0] != "" && len(row[0]) == 11) { // continue // } // // if row[0] == "15812800163" { // fmt.Println("found phone number: 15812800163") // break // } // // unsubscribeTime, _ := models.ConvertStringToTime(row[2]) // // err = e.Orm.Table("mg_order_copy").Where("phone_number = ?", row[0]).Updates(map[string]interface{}{ // "state": models.UnsubscribeOK, // "unsubscribe_time": unsubscribeTime, // "updated_at": unsubscribeTime, // }).Error // if err != nil { // fmt.Println("CheckOrderState update mg_order err:", err.Error()) // continue // } // //if nRow > 4 { // // break // //} // } // // // 返回成功消息 // c.JSON(http.StatusOK, gin.H{"message": "导入成功"}) //} // HourSummaryList 历史汇总(按小时) // @Summary 历史汇总(按小时) // @Tags 2024-咪咕-管理后台 // @Produce json // @Accept json // @Param request body models.HistoricalSummaryListReq true "历史汇总(按小时)" // @Success 200 {object} models.HourSummaryListResp // @Router /api/v1/admin/hour_summary/list [post] func (e MiGuDeployService) HourSummaryList(c *gin.Context) { fmt.Println("HourSummaryList") 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 } resp := models.HourSummaryListResp{ PageNum: req.PageNum, } // 分页处理 pageNum := req.PageNum - 1 if pageNum < 0 { pageNum = 0 } if req.PageSize == 0 { req.PageSize = 10 } resp.PageSize = req.PageSize // 设置开始时间和结束时间 startTime := req.StartTime endTime := req.EndTime if startTime == "" { startTime = "1970-01-01 00:00:00" } if endTime == "" { endTime = time.Now().Format("2006-01-02 15:04:05") } data, sumData, nCount, err := e.queryHistoricalDataByHour(startTime, endTime, req) if err != nil { response.Error(c, http.StatusInternalServerError, err, "查询失败") return } resp.List = data // 批量赋值操作 var totalData models.TotalHourSummary totalData.SubmissionCount = sumData.SubmissionCount totalData.NewUserCount = sumData.NewUserCount totalData.SubmissionSuccessRate = sumData.SubmissionSuccessRate totalData.NewUserUnsubWithinHour = sumData.NewUserUnsubWithinHour totalData.NewUserUnsubWithinHourRate = sumData.NewUserUnsubWithinHourRate totalData.NewUserUnsubOnDay = sumData.NewUserUnsubOnDay totalData.NewUserUnsubOnDayRate = sumData.NewUserUnsubOnDayRate totalData.TotalNewUserUnsub = sumData.TotalNewUserUnsub totalData.TotalNewUserUnsubRate = sumData.TotalNewUserUnsubRate if req.IsExport == 1 { url, err := models.ExportHourSummaryToExcel(data, totalData, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } response.OK(c, map[string]string{"export_url": url}, "导出成功") return } resp.SummaryData = &totalData resp.Count = nCount // 返回结果 e.OK(resp, "") } // 查询历史数据(按小时),并计算退订率和提交成功率 func (e MiGuDeployService) queryHistoricalDataByHour(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHourSummary, *models.MgHourSummary, int, error) { // 动态构建产品和渠道过滤条件 var filterConds string var args []interface{} args = append(args, startTime, endTime) if req.SkuCode > 0 { filterConds += " AND product_id = ?" args = append(args, req.SkuCode) } if req.Channel != "" { filterConds += " AND channel_code = ?" args = append(args, req.Channel) } // 拷贝参数用于不同的 UNION 查询 argsOrder := append([]interface{}{}, args...) argsOrderTotal := append([]interface{}{}, args...) argsLog := append([]interface{}{}, args...) argsLogTotal := append([]interface{}{}, args...) // 构建 SQL 查询 sql := fmt.Sprintf(` SELECT * FROM ( SELECT hour, product_id, channel_code, SUM(new_user_count) AS new_user_count, SUM(new_user_unsub_within_hour) AS new_user_unsub_within_hour, SUM(new_user_unsub_on_day) AS new_user_unsub_on_day, SUM(total_new_user_unsub) AS total_new_user_unsub, SUM(submission_count) AS submission_count, IF(SUM(submission_count) > 0, CONCAT(ROUND(SUM(new_user_count) / SUM(submission_count) * 100, 2), '%%'), '0.00%%' ) AS submission_success_rate, IF(SUM(new_user_count) > 0, CONCAT(ROUND(SUM(new_user_unsub_within_hour) / SUM(new_user_count) * 100, 2), '%%'), '0.00%%' ) AS new_user_unsub_within_hour_rate, IF(SUM(new_user_count) > 0, CONCAT(ROUND(SUM(new_user_unsub_on_day) / SUM(new_user_count) * 100, 2), '%%'), '0.00%%' ) AS new_user_unsub_on_day_rate, IF(SUM(new_user_count) > 0, CONCAT(ROUND(SUM(total_new_user_unsub) / SUM(new_user_count) * 100, 2), '%%'), '0.00%%' ) AS total_new_user_unsub_rate FROM ( SELECT HOUR(subscribe_time) AS hour, product_id, channel_code, COUNT(*) AS new_user_count, SUM(CASE WHEN is_one_hour_cancel = 1 THEN 1 ELSE 0 END) AS new_user_unsub_within_hour, SUM(CASE WHEN state = 2 AND DATE(unsubscribe_time) = DATE(subscribe_time) THEN 1 ELSE 0 END) AS new_user_unsub_on_day, SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub, 0 AS submission_count FROM mg_order WHERE subscribe_time BETWEEN ? AND ? %s GROUP BY HOUR(subscribe_time), channel_code, product_id -- 按小时和渠道分组 UNION ALL SELECT 'Total' AS hour, product_id, channel_code, COUNT(*) AS new_user_count, SUM(CASE WHEN is_one_hour_cancel = 1 THEN 1 ELSE 0 END), SUM(CASE WHEN state = 2 AND DATE(unsubscribe_time) = DATE(subscribe_time) THEN 1 ELSE 0 END), SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END), 0 FROM mg_order WHERE subscribe_time BETWEEN ? AND ? %s GROUP BY channel_code, product_id -- 按渠道和产品分组 UNION ALL SELECT HOUR(created_at) AS hour, product_id, channel_code, 0, 0, 0, 0, COUNT(*) AS submission_count FROM mg_transaction_log WHERE created_at BETWEEN ? AND ? AND verification_code != '' %s GROUP BY HOUR(created_at), channel_code -- 按小时和渠道分组 UNION ALL SELECT 'Total' AS hour, product_id, channel_code, 0, 0, 0, 0, COUNT(*) AS submission_count FROM mg_transaction_log WHERE created_at BETWEEN ? AND ? AND verification_code != '' %s GROUP BY channel_code, product_id -- 按渠道和产品分组 ) AS combined_data GROUP BY hour, channel_code -- 按小时和渠道分组 ORDER BY hour ) AS paginated_data `, filterConds, filterConds, filterConds, filterConds) // 整合参数 args = append(argsOrder, argsOrderTotal...) args = append(args, argsLog...) args = append(args, argsLogTotal...) // 查询数据 var data []models.MgHourSummary err := e.Orm.Raw(sql, args...).Scan(&data).Error if err != nil { return nil, nil, 0, err } // 拆分小时数据与汇总数据 var filteredData []models.MgHourSummary summaryData := &models.MgHourSummary{} for _, item := range data { if item.Hour == "Total" { // 累积汇总数据 summaryData.SubmissionCount += item.SubmissionCount summaryData.NewUserCount += item.NewUserCount summaryData.NewUserUnsubWithinHour += item.NewUserUnsubWithinHour summaryData.NewUserUnsubOnDay += item.NewUserUnsubOnDay summaryData.TotalNewUserUnsub += item.TotalNewUserUnsub } else { filteredData = append(filteredData, item) } } // 汇总数据比率重新计算 if summaryData.NewUserCount > 0 { summaryData.SubmissionSuccessRate = fmt.Sprintf("%.2f%%", float64(summaryData.NewUserCount)*100/float64(summaryData.SubmissionCount)) summaryData.NewUserUnsubWithinHourRate = fmt.Sprintf("%.2f%%", float64(summaryData.NewUserUnsubWithinHour)*100/float64(summaryData.NewUserCount)) summaryData.NewUserUnsubOnDayRate = fmt.Sprintf("%.2f%%", float64(summaryData.NewUserUnsubOnDay)*100/float64(summaryData.NewUserCount)) summaryData.TotalNewUserUnsubRate = fmt.Sprintf("%.2f%%", float64(summaryData.TotalNewUserUnsub)*100/float64(summaryData.NewUserCount)) } else { summaryData.SubmissionSuccessRate = "0.00%" summaryData.NewUserUnsubWithinHourRate = "0.00%" summaryData.NewUserUnsubOnDayRate = "0.00%" summaryData.TotalNewUserUnsubRate = "0.00%" } // 按小时降序排序 sort.Slice(filteredData, func(i, j int) bool { hourI, errI := strconv.Atoi(filteredData[i].Hour) hourJ, errJ := strconv.Atoi(filteredData[j].Hour) if errI != nil && errJ != nil { return filteredData[i].Hour > filteredData[j].Hour } if errI != nil { return false } if errJ != nil { return true } return hourI > hourJ }) // 导出模式:不分页 if req.IsExport == 1 { return filteredData, summaryData, 0, nil } // 分页处理 start := (req.PageNum - 1) * req.PageSize end := start + req.PageSize if start > len(filteredData) { start = 0 } if end > len(filteredData) { end = len(filteredData) } paginatedData := filteredData[start:end] 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 }) // 判断是否导出Excel if req.IsExport == 1 { // 调用导出Excel函数 url, err := models.ExportHistoricalSummaryToExcel(summaries, e.Orm) if err != nil { response.Error(c, http.StatusInternalServerError, err, "导出失败") return } // 返回导出文件的URL地址 response.OK(c, map[string]string{"export_url": url}, "导出成功") return } // 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, "") }