1、营收分析、用户留存记录、交易流水记录等接口增加excel导出功能;

2、订单列表、交易流水记录导出excel时增加时间限制;
This commit is contained in:
chenlin 2025-02-07 11:55:45 +08:00
parent 962802d54f
commit 401eaaaaba
6 changed files with 403 additions and 19 deletions

View File

@ -46,6 +46,15 @@ func (e MiGuDeployService) TransactionList(c *gin.Context) {
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
@ -93,7 +102,11 @@ func (e MiGuDeployService) TransactionList(c *gin.Context) {
}
var transactionLogs []models.MgTransactionLog
err = qs.Order("created_at desc").Offset(pageNum * req.PageSize).Limit(req.PageSize).Find(&transactionLogs).Error
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, "查询失败")
@ -103,6 +116,16 @@ func (e MiGuDeployService) TransactionList(c *gin.Context) {
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, "")
}
@ -268,6 +291,15 @@ func (e MiGuDeployService) OrderList(c *gin.Context) {
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 {
@ -1419,6 +1451,16 @@ func (e MiGuDeployService) UserRetentionList(c *gin.Context) {
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, "成功")
}
@ -2039,21 +2081,32 @@ func (e MiGuDeployService) CalculateRevenueAnalysis(c *gin.Context) {
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`).
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
if err != nil {
response.Error(c, http.StatusInternalServerError, err, "查询失败")
return
}
// 以下代码是按非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
//}
// 查询每个月的上个月未退订用户数
for i := 0; i < len(result); i++ {
@ -2073,7 +2126,9 @@ func (e MiGuDeployService) CalculateRevenueAnalysis(c *gin.Context) {
return
}
result[i].ValidUsersCount += totalValidUsers + totalUnsubscribedUsers
result[i].RetainedUsersCount += totalValidUsers + totalUnsubscribedUsers
result[i].TotalValidUsersCount = result[i].RetainedUsersCount + result[i].ValidUsersCount
//// 获取当前记录的月份
//currentMonth := result[i].Month
@ -2102,7 +2157,7 @@ func (e MiGuDeployService) CalculateRevenueAnalysis(c *gin.Context) {
totalValidUsers := 0
for _, data := range result {
totalNewUserCount += data.NewUserCount
totalValidUsers += data.ValidUsersCount
totalValidUsers += data.TotalValidUsersCount
}
// 返回结果
@ -2111,6 +2166,17 @@ func (e MiGuDeployService) CalculateRevenueAnalysis(c *gin.Context) {
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, "查询成功")
}

View File

@ -3,6 +3,7 @@ package models
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/xuri/excelize/v2"
@ -344,6 +345,7 @@ type TransactionListReq struct {
EndTime string `json:"end_time"` // 结束时间
PageNum int `json:"page_num"` // 页码
PageSize int `json:"page_size"` // 每页条数
IsExport uint32 `json:"is_export"` // 1-导出
}
// TransactionListResp 查询交易流水记录-出参
@ -628,13 +630,16 @@ type RetentionMonthsReq struct {
EndTime string `json:"end_time"` // 查询结束时间
ProductID int `json:"product_id"` // 产品ID
Channel string `json:"channel"` // 渠道名称
IsExport uint32 `json:"is_export"` // 1-导出
}
// MonthlyRetention 表示每月留存数据
type MonthlyRetention struct {
Month string `json:"month"` // 年月
NewUserCount int `json:"new_user_count"` // 新用户数
ValidUsersCount int `json:"valid_users_count"` // 当月有效用户数
Month string `json:"month"` // 年月
NewUserCount int `json:"new_user_count"` // 新用户数
ValidUsersCount int `json:"valid_users_count"` // 当月新增有效用户数
RetainedUsersCount int `json:"retained_users_count"` // 历史推广用户本月留存数
TotalValidUsersCount int `json:"total_valid_users_count"` // 总有效用户数
}
// RetentionMonthsResp 表示响应参数结构体
@ -1781,3 +1786,272 @@ func ExportHourSummaryToExcel(data []MgHourSummary, sumData TotalHourSummary, db
return url, nil
}
// ExportRevenueAnalysisToExcel 营收分析数据导出excel
func ExportRevenueAnalysisToExcel(data RetentionMonthsResp, db *gorm.DB) (string, error) {
// 创建一个新的Excel文件
file := excelize.NewFile()
sheet := "Sheet1"
// 设置标题栏
titles := []string{"年月", "新用户数", "当月新增有效用户数", "历史推广用户本月留存数", "总有效用户数"}
for i, title := range titles {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
file.SetCellValue(sheet, cell, title)
}
// 设置所有单元格的样式: 居中、加边框
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
"border":[{"type":"left","color":"000000","style":1},
{"type":"top","color":"000000","style":1},
{"type":"right","color":"000000","style":1},
{"type":"bottom","color":"000000","style":1}]}`)
// 设置单元格高度
file.SetRowHeight(sheet, 1, 20)
// 设置列宽
file.SetColWidth(sheet, "A", "A", 15)
file.SetColWidth(sheet, "B", "B", 15)
file.SetColWidth(sheet, "C", "C", 20)
file.SetColWidth(sheet, "D", "D", 20)
file.SetColWidth(sheet, "E", "E", 18)
// 填充数据
for i, record := range data.MonthlyRetentionData {
row := i + 2
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.Month)
file.SetCellValue(sheet, "B"+strconv.Itoa(row), record.NewUserCount)
file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.ValidUsersCount)
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.RetainedUsersCount)
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.TotalValidUsersCount)
}
// 填充合计行数据
file.SetCellValue(sheet, "A"+strconv.Itoa(len(data.MonthlyRetentionData)+2), "合计:")
file.SetCellValue(sheet, "B"+strconv.Itoa(len(data.MonthlyRetentionData)+2), data.TotalNewUsers)
file.SetCellValue(sheet, "C"+strconv.Itoa(len(data.MonthlyRetentionData)+2), "--")
file.SetCellValue(sheet, "D"+strconv.Itoa(len(data.MonthlyRetentionData)+2), "--")
file.SetCellValue(sheet, "E"+strconv.Itoa(len(data.MonthlyRetentionData)+2), data.TotalValidUsers)
endRow := fmt.Sprintf("E%d", len(data.MonthlyRetentionData)+2)
// 应用样式到整个表格
_ = file.SetCellStyle(sheet, "A1", endRow, style)
// 从配置文件读取保存路径和URL前缀
fileName := time.Now().Format("20060102150405") + "_营收分析.xlsx"
url := MiGuExportUrl + fileName
// 保存Excel文件
if err := file.SaveAs(ExportFile + fileName); err != nil {
logger.Errorf("Failed to save Excel file: %v", err)
return "", err
}
return url, nil
}
// CheckDateRange 检查时间间隔是否超过 n 天
func CheckDateRange(start, end string, nDay int) error {
if start == "" || end == "" {
return errors.New("导出失败,时间不能为空")
}
// 解析时间,假设时间格式为 "2006-01-02"
startTime, err := time.Parse(MiGuTimeFormat, start)
if err != nil {
return errors.New("开始时间格式错误")
}
endTime, err := time.Parse(MiGuTimeFormat, end)
if err != nil {
return errors.New("结束时间格式错误")
}
// 计算时间间隔
if endTime.Sub(startTime).Hours() > float64(nDay*24) {
return errors.New(fmt.Sprintf("导出时间间隔不能超过 %d 天", nDay))
}
return nil
}
// ExportUserRetentionToExcel 用户留存记录导出excel
func ExportUserRetentionToExcel(data []MgUserRetention, db *gorm.DB) (string, error) {
// 创建一个新的Excel文件
file := excelize.NewFile()
sheet := "Sheet1"
lastMonthFirstDay := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local) // 当前月1号
lastTwoMonthFirstDay := lastMonthFirstDay.AddDate(0, -1, 0) // 上个月1号
lastMonthCountTitle := lastMonthFirstDay.Format("2006-01-02") + "留存数"
lastMonthRateTitle := lastMonthFirstDay.Format("2006-01-02") + "留存率"
lastTwoMonthCountTitle := lastTwoMonthFirstDay.Format("2006-01-02") + "留存数"
lastTwoMonthRateTitle := lastTwoMonthFirstDay.Format("2006-01-02") + "留存率"
// 设置标题栏
titles := []string{"留存月份", "渠道", "产品", "新增数", "留存数", "留存率", lastTwoMonthCountTitle, lastTwoMonthRateTitle,
lastMonthCountTitle, lastMonthRateTitle}
for i, title := range titles {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
file.SetCellValue(sheet, cell, title)
}
// 设置所有单元格的样式: 居中、加边框
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
"border":[{"type":"left","color":"000000","style":1},
{"type":"top","color":"000000","style":1},
{"type":"right","color":"000000","style":1},
{"type":"bottom","color":"000000","style":1}]}`)
// 设置单元格高度
file.SetRowHeight(sheet, 1, 20)
// 设置列宽
file.SetColWidth(sheet, "A", "A", 15)
file.SetColWidth(sheet, "B", "B", 15)
file.SetColWidth(sheet, "C", "C", 18)
file.SetColWidth(sheet, "D", "D", 15)
file.SetColWidth(sheet, "E", "E", 15)
file.SetColWidth(sheet, "F", "F", 15)
file.SetColWidth(sheet, "G", "G", 18)
file.SetColWidth(sheet, "H", "H", 18)
file.SetColWidth(sheet, "I", "I", 18)
file.SetColWidth(sheet, "J", "J", 18)
// 创建一个产品ID到名称的映射
productMap := make(map[int64]string)
for _, order := range data {
if _, exists := productMap[order.ProductID]; !exists {
var product MgProduct
// 查询产品信息
if err := db.First(&product, order.ProductID).Error; err == nil {
productMap[order.ProductID] = product.Name
} else {
productMap[order.ProductID] = "未知产品"
}
}
}
// 填充数据
for i, record := range data {
row := i + 2
productName := productMap[record.ProductID] // 获取产品名称
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.RetentionMonth)
file.SetCellValue(sheet, "B"+strconv.Itoa(row), record.ChannelCode)
file.SetCellValue(sheet, "C"+strconv.Itoa(row), productName)
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.NewUserCount)
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.RetainedUserCount)
file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.RetentionRate)
file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.LastTwoMonthRetentionCount)
file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.LastTwoMonthRetentionRate)
file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.LastMonthRetentionCount)
file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.LastMonthRetentionRate)
}
endRow := fmt.Sprintf("J%d", len(data)+1)
// 应用样式到整个表格
_ = file.SetCellStyle(sheet, "A1", endRow, style)
// 从配置文件读取保存路径和URL前缀
fileName := time.Now().Format("20060102150405") + "_用户留存记录.xlsx"
url := MiGuExportUrl + fileName
// 保存Excel文件
if err := file.SaveAs(ExportFile + fileName); err != nil {
logger.Errorf("Failed to save Excel file: %v", err)
return "", err
}
return url, nil
}
// ExportTransactionToExcel 交易流水记录导出excel
func ExportTransactionToExcel(data []MgTransactionLog, db *gorm.DB) (string, error) {
// 创建一个新的Excel文件
file := excelize.NewFile()
sheet := "Sheet1"
// 设置标题栏
titles := []string{"调用时间", "产品", "渠道", "手机号", "平台订单号", "外部平台订单号", "渠道订单号", "结果", "原因",
"验证码", "订单时间"}
for i, title := range titles {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
file.SetCellValue(sheet, cell, title)
}
// 设置所有单元格的样式: 居中、加边框
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
"border":[{"type":"left","color":"000000","style":1},
{"type":"top","color":"000000","style":1},
{"type":"right","color":"000000","style":1},
{"type":"bottom","color":"000000","style":1}]}`)
// 设置单元格高度
file.SetRowHeight(sheet, 1, 20)
// 设置列宽
file.SetColWidth(sheet, "A", "A", 18)
file.SetColWidth(sheet, "B", "B", 18)
file.SetColWidth(sheet, "C", "C", 15)
file.SetColWidth(sheet, "D", "D", 15)
file.SetColWidth(sheet, "E", "E", 20)
file.SetColWidth(sheet, "F", "F", 20)
file.SetColWidth(sheet, "G", "G", 28)
file.SetColWidth(sheet, "I", "I", 28)
file.SetColWidth(sheet, "K", "K", 18)
// 创建一个产品ID到名称的映射
productMap := make(map[int64]string)
for _, order := range data {
if _, exists := productMap[order.ProductID]; !exists {
var product MgProduct
// 查询产品信息
if err := db.First(&product, order.ProductID).Error; err == nil {
productMap[order.ProductID] = product.Name
} else {
productMap[order.ProductID] = "未知产品"
}
}
}
// 填充数据
for i, record := range data {
row := i + 2
var orderTime string
if record.OrderTime == nil {
orderTime = ""
} else {
orderTime = record.OrderTime.Format(MiGuTimeFormat)
}
productName := productMap[record.ProductID] // 获取产品名称
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.CreatedAt.Format(MiGuTimeFormat))
file.SetCellValue(sheet, "B"+strconv.Itoa(row), productName)
file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.ChannelCode)
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.PhoneNumber)
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.OutTradeNo)
file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.LinkId)
file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.ChannelTradeNo)
file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.Result)
file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.Reason)
file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.VerificationCode)
file.SetCellValue(sheet, "K"+strconv.Itoa(row), orderTime)
}
endRow := fmt.Sprintf("K%d", len(data)+2)
// 应用样式到整个表格
_ = file.SetCellStyle(sheet, "A1", endRow, style)
// 从配置文件读取保存路径和URL前缀
fileName := time.Now().Format("20060102150405") + "_交易流水记录.xlsx"
url := MiGuExportUrl + fileName
// 保存Excel文件
if err := file.SaveAs(ExportFile + fileName); err != nil {
logger.Errorf("Failed to save Excel file: %v", err)
return "", err
}
return url, nil
}

View File

@ -30,7 +30,7 @@ func registerMiGuControlManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.Gi
api.POST("user_day_retention/list", apiMiGu.UserDayRetentionList) // 用户留存记录(按天)
api.POST("sys_channel/list", apiMiGu.SysChannelList) // 查询系统所有渠道编码
api.POST("home/data", apiMiGu.HomepageDataSummary) // 查询首页汇总数据
api.POST("home/revenue_analysis", apiMiGu.CalculateRevenueAnalysis) // 查询不同日期的留存月份
api.POST("home/revenue_analysis", apiMiGu.CalculateRevenueAnalysis) // 营收分析
api.POST("historical_summary/list", apiMiGu.HistoricalSummaryListOld) // 历史汇总查询
//api.POST("order/import", apiMiGu.ImportExcelToMgOrderHandler) // 通过excel导入订单数据

View File

@ -5408,8 +5408,16 @@ const docTemplateadmin = `{
"description": "新用户数",
"type": "integer"
},
"retained_users_count": {
"description": "历史推广用户本月留存数",
"type": "integer"
},
"total_valid_users_count": {
"description": "总有效用户数",
"type": "integer"
},
"valid_users_count": {
"description": "当月有效用户数",
"description": "当月新增有效用户数",
"type": "integer"
}
}
@ -5969,6 +5977,10 @@ const docTemplateadmin = `{
"description": "渠道号",
"type": "string"
},
"is_export": {
"description": "1-导出",
"type": "integer"
},
"only_first_day": {
"description": "是否只查询每个月1号的数据",
"type": "boolean"
@ -6018,6 +6030,10 @@ const docTemplateadmin = `{
"description": "渠道号",
"type": "string"
},
"is_export": {
"description": "1-导出",
"type": "integer"
},
"page_num": {
"description": "页码",
"type": "integer"

View File

@ -5400,8 +5400,16 @@
"description": "新用户数",
"type": "integer"
},
"retained_users_count": {
"description": "历史推广用户本月留存数",
"type": "integer"
},
"total_valid_users_count": {
"description": "总有效用户数",
"type": "integer"
},
"valid_users_count": {
"description": "当月有效用户数",
"description": "当月新增有效用户数",
"type": "integer"
}
}
@ -5961,6 +5969,10 @@
"description": "渠道号",
"type": "string"
},
"is_export": {
"description": "1-导出",
"type": "integer"
},
"only_first_day": {
"description": "是否只查询每个月1号的数据",
"type": "boolean"
@ -6010,6 +6022,10 @@
"description": "渠道号",
"type": "string"
},
"is_export": {
"description": "1-导出",
"type": "integer"
},
"page_num": {
"description": "页码",
"type": "integer"

View File

@ -1402,8 +1402,14 @@ definitions:
new_user_count:
description: 新用户数
type: integer
retained_users_count:
description: 历史推广用户本月留存数
type: integer
total_valid_users_count:
description: 总有效用户数
type: integer
valid_users_count:
description: 当月有效用户数
description: 当月新增有效用户数
type: integer
type: object
models.OrderListReq:
@ -1789,6 +1795,9 @@ definitions:
channel:
description: 渠道号
type: string
is_export:
description: 1-导出
type: integer
only_first_day:
description: 是否只查询每个月1号的数据
type: boolean
@ -1827,6 +1836,9 @@ definitions:
channel:
description: 渠道号
type: string
is_export:
description: 1-导出
type: integer
page_num:
description: 页码
type: integer