migu_admin_server/app/admin/apis/migumanage/migu_admin.go

3058 lines
94 KiB
Go
Raw Normal View History

2024-10-22 08:29:20 +00:00
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"
2024-10-22 08:29:20 +00:00
"net/http"
"sort"
"strconv"
2024-10-22 08:29:20 +00:00
"strings"
"sync"
2024-10-22 08:29:20 +00:00
"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
}
}
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
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
}
}
2024-10-22 08:29:20 +00:00
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)
2024-10-22 08:29:20 +00:00
}
}
// 开始时间和结束时间
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)
}
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
resp.List = orderList
resp.Count = int(count)
e.OK(resp, "")
}
func (e MiGuDeployService) HistoricalSummaryListOld(c *gin.Context) {
2024-10-22 08:29:20 +00:00
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")
2024-10-22 08:29:20 +00:00
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).
2024-10-22 08:29:20 +00:00
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")
2024-10-22 08:29:20 +00:00
// 添加过滤条件
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
}
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
// 返回结果
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
}
2024-10-22 08:29:20 +00:00
// 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
}
}
2024-10-22 08:29:20 +00:00
// 查询总记录数
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
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
// 返回数据
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{})
2024-10-22 08:29:20 +00:00
//// 获取开始和结束时间
//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)
//}
2024-10-22 08:29:20 +00:00
// 处理产品编号 (SkuCode) 过滤条件
if req.SkuCode != 0 {
2024-10-22 08:29:20 +00:00
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").
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
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)
}
}
2024-10-22 08:29:20 +00:00
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
}
2024-10-22 08:29:20 +00:00
// 返回结果
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").
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
}
// 计算下个月的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).
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表示退订
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).
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表示退订
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)
}
// 排序(按日期升序)
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
}
2024-10-22 08:29:20 +00:00
// 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)
// 添加产品和渠道的过滤条件
2024-10-23 05:51:56 +00:00
if req.ProductID != 0 {
qs = qs.Where("product_id = ?", req.ProductID)
2024-10-22 08:29:20 +00:00
}
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,
2024-10-22 08:29:20 +00:00
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) 过滤条件
2024-10-23 05:51:56 +00:00
if req.ProductID != 0 {
dailyQuery = dailyQuery.Where("product_id = ?", req.ProductID)
2024-10-22 08:29:20 +00:00
}
// 处理渠道名称 (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
}
}
}
2024-10-22 08:29:20 +00:00
resp.DailyDataList = dailyDataList
// 返回结果
e.OK(resp, "")
}
2024-10-23 05:51:56 +00:00
// 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{})
// 查询条件
2024-10-23 05:51:56 +00:00
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天来统计数据
2024-10-23 05:51:56 +00:00
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`).
2024-10-23 05:51:56 +00:00
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
//}
2024-10-23 05:51:56 +00:00
// 确保有数据
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)
if err != nil {
response.Error(c, http.StatusInternalServerError, err, "未退订用户查询失败")
return
}
// 查询当前月之前所有月份在本月2号之后退订的用户数
totalUnsubscribedUsers, err := GetTotalUnsubscribedUsers(e.Orm, tempCurrentMonth)
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)
}
// 计算总的新用户数和有效用户数
2024-10-23 05:51:56 +00:00
totalNewUserCount := 0
totalValidUsers := 0
for _, data := range result {
totalNewUserCount += data.NewUserCount
totalValidUsers += data.TotalValidUsersCount
2024-10-23 05:51:56 +00:00
}
// 返回结果
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
}
2024-10-23 05:51:56 +00:00
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) (int, error) {
var totalValidUsers int
err := db.Raw(`
SELECT
COUNT(*)
FROM mg_order
WHERE unsubscribe_time IS NULL
AND DATE_FORMAT(subscribe_time, '%Y-%m') < ?`, currentMonth).Scan(&totalValidUsers).Error
if err != nil {
return 0, err
}
return totalValidUsers, nil
}
// GetTotalUnsubscribedUsers 计算所有之前月份在本月2号之后退订的用户总数
func GetTotalUnsubscribedUsers(db *gorm.DB, currentMonth string) (int, error) {
var totalUnsubscribedUsers int
// 计算当前月份的 2 号 00:00:00
currentMonthFirstDay := currentMonth + "-02 00:00:00"
err := db.Raw(`
SELECT
COUNT(*)
FROM mg_order
WHERE unsubscribe_time >= ?
AND DATE_FORMAT(subscribe_time, '%Y-%m') < ?`,
currentMonthFirstDay, currentMonth).Scan(&totalUnsubscribedUsers).Error
if err != nil {
return 0, err
}
return totalUnsubscribedUsers, nil
}
2024-10-22 08:29:20 +00:00
// 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) < 4 {
// continue // 跳过数据不全的行
// }
//
// // 解析订阅时间
// subscribeTime, err := time.Parse("2006-01-02 15:04:05", row[3])
// 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)
//
// // 创建MgOrder对象
// order := models.MgOrderCopy{
// ProductID: 1,
// ChannelCode: "6015150807",
// OrderSerial: models.GetExcelOrderSerial(e.Orm, subscribeTime),
// SubscribeTime: &localTime,
// PhoneNumber: row[0],
// ChannelTradeNo: row[1],
// ExternalOrderID: row[2],
// State: 1,
// }
//
// 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) {
// 构建SQL查询字符串
sql := `
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 (
-- 第一个查询mg_order 数据
SELECT
HOUR(mg_order.created_at) AS hour,
product_id,
channel_code,
COUNT(*) AS new_user_count,
SUM(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 ELSE 0 END) AS new_user_unsub_within_hour,
SUM(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 ELSE 0 END) AS new_user_unsub_on_day,
SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub,
0 AS submission_count
FROM
mg_order
WHERE
mg_order.created_at BETWEEN ? AND ?
GROUP BY
HOUR(mg_order.created_at)
UNION ALL
-- 第二个查询mg_order 总计数据
SELECT
'Total' AS hour,
product_id,
channel_code,
COUNT(*) AS new_user_count,
SUM(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 ELSE 0 END) AS new_user_unsub_within_hour,
SUM(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 ELSE 0 END) AS new_user_unsub_on_day,
SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub,
0 AS submission_count
FROM
mg_order
WHERE
mg_order.created_at BETWEEN ? AND ?
UNION ALL
-- 第三个查询mg_transaction_log 数据
SELECT
HOUR(mg_transaction_log.created_at) AS hour,
product_id AS product_id,
channel_code AS channel_code,
0 AS new_user_count,
0 AS new_user_unsub_within_hour,
0 AS new_user_unsub_on_day,
0 AS total_new_user_unsub,
COUNT(*) AS submission_count
FROM
mg_transaction_log
WHERE
mg_transaction_log.created_at BETWEEN ? AND ?
AND verification_code != ''
GROUP BY
HOUR(mg_transaction_log.created_at)
UNION ALL
-- 第四个查询mg_transaction_log 总计数据
SELECT
'Total' AS hour,
product_id AS product_id,
channel_code AS channel_code,
0 AS new_user_count,
0 AS new_user_unsub_within_hour,
0 AS new_user_unsub_on_day,
0 AS total_new_user_unsub,
COUNT(*) AS submission_count
FROM
mg_transaction_log
WHERE
mg_transaction_log.created_at BETWEEN ? AND ?
AND verification_code != ''
) AS combined_data
GROUP BY hour
ORDER BY hour
) AS paginated_data;
`
// 执行查询
var data []models.MgHourSummary
err := e.Orm.Raw(sql, startTime, endTime, startTime, endTime, startTime, endTime, startTime, endTime).Scan(&data).Error
if err != nil {
return nil, nil, 0, err
}
// 剔除 "Total" 数据
var filteredData []models.MgHourSummary
var summaryData *models.MgHourSummary
for _, item := range data {
if item.Hour == "Total" {
summaryData = &item // 保存汇总数据
} else {
filteredData = append(filteredData, item) // 只保留小时数据
}
}
// 按小时排序(确保 hour 是数字,排序正确)
sort.Slice(filteredData, func(i, j int) bool {
// 将字符串类型的 hour 转换为整数进行比较
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 {
// 如果 i 的 hour 无法转换为数字,则认为它大于 j
return false
}
if errJ != nil {
// 如果 j 的 hour 无法转换为数字,则认为它大于 i
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 end > len(filteredData) {
end = len(filteredData)
}
if start > len(filteredData) {
start = 0
}
paginatedData := filteredData[start:end]
// 返回分页数据
return paginatedData, summaryData, len(filteredData), nil
}