migu_admin_server/app/admin/apis/migumanage/migu_admin.go
chenlin 2863174547 1、历史汇总列表排序优化(按时间排序);
2、历史汇总(按小时)优化,支持不同产品筛选;
3、用户留存记录(按天)增加当月最后1天留存数据展示;
2025-04-07 19:50:10 +08:00

3132 lines
96 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package migumanage
import (
"encoding/csv"
"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"
"go-admin/tools"
"gorm.io/gorm"
"io"
"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 {
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(*) 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.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 := 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("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay).
Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ?", lastMonthSecondDay, currentMonthFirstDay, currentMonthNextFirstDay).
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("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay).
Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ?", lastTwoMonthSecondDay, currentMonthFirstDay, currentMonthNextFirstDay).
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 ?", date+" 23:59:59", currentMonthFirstDay, currentMonthNextFirstDay).
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 ?", date+" 23:59:59", currentMonthFirstDay, currentMonthNextFirstDay).
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
var summaryData *models.MgHourSummary
for _, item := range data {
if item.Hour == "Total" {
summaryData = &item
} else {
filteredData = append(filteredData, item)
}
}
// 按小时降序排序
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
}