mh_goadmin_server/app/admin/models/erp_market.go

636 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package models
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
orm "go-admin/common/global"
"go-admin/logger"
"go-admin/tools"
"go-admin/tools/config"
"math"
"sort"
"strconv"
"strings"
"time"
)
// DailyBusinessSummary 每日经营数据汇总表
type DailyBusinessSummary struct {
Model
Date time.Time `json:"date"` // 汇总日期
StoreID uint `json:"store_id"` // 门店ID
SalesAmount float64 `json:"sales_amount"` // 销售额
PromotionFee float64 `json:"promotion_fee"` // 推广费
SalesProfit float64 `json:"sales_profit"` // 销售毛利
StaffProfit float64 `json:"staff_profit"` // 员工毛利
Count int32 `json:"count"` // 销售数量
}
type CreateBusinessSummaryRequest struct {
Date string `json:"date" binding:"required"` // 格式yyyy-MM-dd
StoreID uint `json:"store_id" binding:"required"` // 门店id
SalesAmount float64 `json:"sales_amount"` // 销售额
PromotionFee float64 `json:"promotion_fee"` // 推广费
SalesProfit float64 `json:"sales_profit"` // 销售毛利
StaffProfit float64 `json:"staff_profit"` // 员工毛利
Count int32 `json:"count"` // 销售数量
}
type BusinessSummaryListRequest struct {
StoreId []uint `json:"store_id"` // 可选复选门店ID
StartTime string `json:"start_time"` // 开始时间
EndTime string `json:"end_time"` // 结束时间
PageIndex int `json:"page_index"` // 当前页码
PageSize int `json:"page_size"` // 每页条数
SortType string `json:"sort_type"` // 排序类型desc 降序、asc 升序
}
type BusinessSummaryItem struct {
Date string `json:"date"` // 时间,如:"2023-12-25"
SalesAmount float64 `json:"sales_amount"` // 销售额
PromotionFee float64 `json:"promotion_fee"` // 推广费
SalesProfit float64 `json:"sales_profit"` // 销售毛利
StaffProfit float64 `json:"staff_profit"` // 员工毛利
Count int32 `json:"count"` // 销售数量
}
type BusinessSummaryListResp struct {
List []BusinessSummaryItem `json:"list"`
Total int `json:"total"`
PageIndex int `json:"pageIndex"`
PageSize int `json:"pageSize"`
TotalSalesAmount float64 `json:"total_sales_amount"` // 总销售额
TotalPromotionFee float64 `json:"total_promotion_fee"` // 总推广费
TotalSalesProfit float64 `json:"total_sales_profit"` // 总销售毛利
TotalStaffProfit float64 `json:"total_staff_profit"` // 总员工毛利
TotalCount int64 `json:"total_count"` // 总销售数量
}
// MarketStoreSalesDataReq 门店销售对比入参
type MarketStoreSalesDataReq struct {
StoreId []uint32 `json:"store_id"` // 门店ID
StartTime string `json:"start_time"` // 开始时间
EndTime string `json:"end_time"` // 结束时间
PageIndex int `json:"pageIndex"` // 页码
PageSize int `json:"pageSize"` // 页面条数
IsExport uint32 `json:"is_export"` // 1-导出
SortType string `json:"sort_type"` // 排序类型desc 降序、asc 升序
}
// MarketStoreSalesDataResp 门店销售对比出参
type MarketStoreSalesDataResp struct {
List []MarketStoreSalesData `json:"list"`
Total int `json:"total"` // 总条数
PageIndex int `json:"pageIndex"` // 页码
PageSize int `json:"pageSize"` // 每页展示条数
ExportUrl string `json:"export_url"`
TotalSalesAmount float64 `json:"total_sales_amount"` // 总销售额
TotalPromotionFee float64 `json:"total_promotion_fee"` // 总推广费
TotalSalesProfit float64 `json:"total_sales_profit"` // 总销售毛利
TotalStaffProfit float64 `json:"total_staff_profit"` // 总员工毛利
TotalCount int64 `json:"total_count"` // 总销售数量
}
// MarketStoreSalesData 门店销售数据
type MarketStoreSalesData struct {
StoreId uint32 `json:"store_id"` // 门店id
StoreName string `json:"store_name"` // 门店名称
SalesAmount float64 `json:"sales_amount"` // 销售额
PromotionFee float64 `json:"promotion_fee"` // 推广费
SalesProfit float64 `json:"sales_profit"` // 销售毛利
StaffProfit float64 `json:"staff_profit"` // 员工毛利
Count int64 `json:"count"` // 销售数量
}
// ParseToDate 解析多种格式的时间字符串,仅保留日期部分(年月日)
func ParseToDate(s string) (time.Time, error) {
formats := []string{
"2006-01-02", // 标准日期格式
time.RFC3339, // 带时区时间戳 2025-05-20T00:00:00+08:00
"2006-01-02 15:04:05", // 常见完整格式
}
for _, layout := range formats {
if t, err := time.Parse(layout, s); err == nil {
// 格式化为 YYYY-MM-DD 再转换,去除时间部分
return time.Parse("2006-01-02", t.Format("2006-01-02"))
}
}
return time.Time{}, fmt.Errorf("无法解析日期格式: %s", s)
}
func QueryListBusinessSummary(req *BusinessSummaryListRequest, c *gin.Context) (*BusinessSummaryListResp, error) {
page := req.PageIndex - 1
if page < 0 {
page = 0
}
if req.PageSize == 0 {
req.PageSize = 10
}
resp := &BusinessSummaryListResp{
PageIndex: req.PageIndex,
PageSize: req.PageSize,
}
startDate, err1 := ParseToDate(req.StartTime)
endDate, err2 := ParseToDate(req.EndTime)
if err1 != nil || err2 != nil {
return nil, errors.New("日期格式错误")
}
if endDate.Before(startDate) {
return nil, errors.New("结束时间早于开始时间")
}
// 生成所有日期用于补0和分页
var allDates []string
for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) {
allDates = append(allDates, d.Format("2006-01-02"))
}
// 根据排序方式进行调整
if strings.ToLower(req.SortType) == "desc" {
// 倒序排列
for i, j := 0, len(allDates)-1; i < j; i, j = i+1, j-1 {
allDates[i], allDates[j] = allDates[j], allDates[i]
}
}
totalDays := len(allDates)
// 日期分页
start := page * req.PageSize
end := start + req.PageSize
if start > totalDays {
start = totalDays
}
if end > totalDays {
end = totalDays
}
pagedDates := allDates[start:end]
// 查询聚合数据
type DailyAggregatedData struct {
Date string `json:"summary_date"`
SalesAmount float64 `json:"sales_amount"`
PromotionFee float64 `json:"promotion_fee"`
SalesProfit float64 `json:"sales_profit"`
StaffProfit float64 `json:"staff_profit"`
Count int32 `json:"count"`
}
var dataList []DailyAggregatedData
qs := orm.Eloquent.Table("daily_business_summary")
es := orm.Eloquent.Table("daily_business_summary")
if req.StartTime != "" && req.EndTime != "" {
startTime, err := time.Parse(QueryTimeFormat, req.StartTime)
if err != nil {
logger.Error("startTime parse err")
}
endTime, err := time.Parse(QueryTimeFormat, req.EndTime)
if err != nil {
logger.Error("endTime parse err")
}
qs = qs.Where("date BETWEEN ? AND ?", startTime, endTime)
es = es.Where("date BETWEEN ? AND ?", startTime, endTime)
}
// 非管理员才判断所属门店
if !(tools.GetRoleName(c) == "admin" || tools.GetRoleName(c) == "系统管理员") {
sysUser, err := GetSysUserByCtx(c)
if err != nil {
return nil, errors.New("操作失败:" + err.Error())
}
// 返回sysUser未过期的门店id列表
storeList := GetValidStoreIDs(sysUser.StoreData)
if len(storeList) > 0 {
if len(storeList) == 1 {
qs = qs.Where("store_id = ?", storeList[0])
es = es.Where("store_id = ?", storeList[0])
} else {
qs = qs.Where("store_id IN (?)", storeList)
es = es.Where("store_id IN (?)", storeList)
}
} else {
return nil, errors.New("用户未绑定门店")
}
}
if len(req.StoreId) > 0 {
qs = qs.Where("store_id IN ?", req.StoreId)
es = es.Where("store_id IN ?", req.StoreId)
}
var err error
if req.SortType == "asc" {
err = qs.Select("DATE_FORMAT(date, '%Y-%m-%d') AS date, " +
"SUM(promotion_fee) AS promotion_fee, " +
"SUM(sales_amount) AS sales_amount, " +
"SUM(sales_profit) AS sales_profit, " +
"SUM(staff_profit) AS staff_profit, " +
"SUM(count) AS count").
Group("date").
Order("date ASC").
Find(&dataList).Error
} else {
err = qs.Select("DATE_FORMAT(date, '%Y-%m-%d') AS date, " +
"SUM(promotion_fee) AS promotion_fee, " +
"SUM(sales_amount) AS sales_amount, " +
"SUM(sales_profit) AS sales_profit, " +
"SUM(staff_profit) AS staff_profit, " +
"SUM(count) AS count").
Group("date").
Order("date DESC").
Find(&dataList).Error
}
if err != nil {
return nil, err
}
// 查询汇总数据
var summary struct {
TotalSalesAmount float64
TotalPromotionFee float64
TotalSalesProfit float64
TotalStaffProfit float64
TotalCount int64
}
err = es.Select("SUM(sales_amount) AS total_sales_amount, " +
"SUM(promotion_fee) AS total_promotion_fee, " +
"SUM(sales_profit) AS total_sales_profit, " +
"SUM(staff_profit) AS total_staff_profit, " +
"SUM(count) AS total_count").
Find(&summary).Error
if err != nil {
return nil, err
}
// 聚合结果映射date → 汇总值
resultMap := make(map[string]DailyAggregatedData)
for _, d := range dataList {
resultMap[d.Date] = d
}
// 构造分页结果(补零)
var result []BusinessSummaryItem
for _, dateStr := range pagedDates {
if d, ok := resultMap[dateStr]; ok {
result = append(result, BusinessSummaryItem{
Date: dateStr,
SalesAmount: d.SalesAmount,
PromotionFee: d.PromotionFee,
SalesProfit: d.SalesProfit,
StaffProfit: d.StaffProfit,
Count: d.Count,
})
} else {
// 补0
result = append(result, BusinessSummaryItem{
Date: dateStr,
SalesAmount: 0,
PromotionFee: 0,
SalesProfit: 0,
StaffProfit: 0,
Count: 0,
})
}
}
switch req.SortType {
case "asc":
sort.Slice(result, func(i, j int) bool {
return result[i].Date < result[j].Date
})
case "desc":
sort.Slice(result, func(i, j int) bool {
return result[i].Date > result[j].Date
})
}
resp.List = result
resp.Total = totalDays
resp.PageIndex = req.PageIndex
resp.PageSize = req.PageSize
resp.TotalSalesAmount = math.Round(summary.TotalSalesAmount*100) / 100
resp.TotalPromotionFee = math.Round(summary.TotalPromotionFee*100) / 100
resp.TotalSalesProfit = math.Round(summary.TotalSalesProfit*100) / 100
resp.TotalStaffProfit = math.Round(summary.TotalStaffProfit*100) / 100
resp.TotalCount = summary.TotalCount
return resp, nil
}
func QueryDailyBusinessSummaryData(req *MarketStoreSalesDataReq, c *gin.Context) (*MarketStoreSalesDataResp, error) {
page := req.PageIndex - 1
if page < 0 {
page = 0
}
if req.PageSize == 0 {
req.PageSize = 10
}
resp := &MarketStoreSalesDataResp{
PageIndex: req.PageIndex,
PageSize: req.PageSize,
}
var storeManageDataList []MarketStoreSalesData
// 查询原始数据
qs := orm.Eloquent.Table("daily_business_summary")
var storeList []uint32
// 非管理员才判断所属门店
if !(tools.GetRoleName(c) == "admin" || tools.GetRoleName(c) == "系统管理员") {
sysUser, err := GetSysUserByCtx(c)
if err != nil {
return nil, errors.New("操作失败:" + err.Error())
}
// 返回sysUser未过期的门店id列表
storeList = GetValidStoreIDs(sysUser.StoreData)
if len(storeList) > 0 {
if len(storeList) == 1 {
qs = qs.Where("store_id = ?", storeList[0])
} else {
qs = qs.Where("store_id IN (?)", storeList)
}
} else {
return nil, errors.New("用户未绑定门店")
}
} else {
var allStoreList []Store
err := orm.Eloquent.Table("store").Where("is_online = 1 and cooperative_business_id = 1").
Find(&allStoreList).Error
if err != nil {
return nil, err
}
for _, v := range allStoreList {
storeList = append(storeList, v.ID)
}
}
// 限定门店
if len(req.StoreId) > 0 {
qs = qs.Where("store_id IN ?", req.StoreId)
}
// 限定时间
if req.StartTime != "" && req.EndTime != "" {
qs = qs.Where("date BETWEEN ? AND ?", req.StartTime, req.EndTime)
}
// 查询全部数据(用于聚合、分页)
var allData []DailyBusinessSummary
err := qs.Order("date DESC").Find(&allData).Error
if err != nil {
logger.Error("QueryDailyBusinessSummaryData err:", logger.Field("err", err))
return nil, err
}
// 查询汇总数据
var summary struct {
TotalSalesAmount float64
TotalPromotionFee float64
TotalSalesProfit float64
TotalStaffProfit float64
TotalCount int64
}
err = qs.Select("SUM(sales_amount) AS total_sales_amount, " +
"SUM(promotion_fee) AS total_promotion_fee, " +
"SUM(sales_profit) AS total_sales_profit, " +
"SUM(staff_profit) AS total_staff_profit, " +
"SUM(count) AS total_count").
Find(&summary).Error
if err != nil {
logger.Error("QueryStoreManageData summary err:", logger.Field("err", err))
return nil, err
}
// 查询分页数据 按 store_id 进行汇总
var storeData []struct {
StoreId uint32 `json:"store_id"`
SalesAmount float64 `json:"sales_amount"`
PromotionFee float64 `json:"promotion_fee"`
SalesProfit float64 `json:"sales_profit"`
StaffProfit float64 `json:"staff_profit"`
Count int64 `json:"count"`
}
err = qs.Select("store_id, " +
"SUM(promotion_fee) AS promotion_fee, " +
"SUM(sales_amount) AS sales_amount, " +
"SUM(sales_profit) AS sales_profit, " +
"SUM(staff_profit) AS staff_profit, " +
"SUM(count) AS count").
Group("store_id").
Order("store_id").
Find(&storeData).Error
if err != nil {
logger.Error("QueryStoreManageData err:", logger.Field("err", err))
return nil, err
}
storeMap, err := GetAllStoreData()
if err != nil {
return nil, err
}
// 组合数据
for _, storeId := range storeList {
flag := false
for _, v := range storeData {
if v.StoreId == storeId {
flag = true
storeManageDataList = append(storeManageDataList, MarketStoreSalesData{
StoreId: v.StoreId,
StoreName: storeMap[v.StoreId].Name,
SalesAmount: math.Round(v.SalesAmount*100) / 100,
PromotionFee: math.Round(v.PromotionFee*100) / 100,
SalesProfit: math.Round(v.SalesProfit*100) / 100,
StaffProfit: math.Round(v.StaffProfit*100) / 100,
Count: v.Count,
})
} else {
continue
}
}
if !flag {
storeManageDataList = append(storeManageDataList, MarketStoreSalesData{
StoreId: storeId,
StoreName: storeMap[storeId].Name,
SalesAmount: 0,
PromotionFee: 0,
SalesProfit: 0,
StaffProfit: 0,
Count: 0,
})
}
}
// 汇总数据赋值给响应
resp.TotalSalesAmount = math.Round(summary.TotalSalesAmount*100) / 100
resp.TotalPromotionFee = math.Round(summary.TotalPromotionFee*100) / 100
resp.TotalSalesProfit = math.Round(summary.TotalSalesProfit*100) / 100
resp.TotalStaffProfit = math.Round(summary.TotalStaffProfit*100) / 100
resp.TotalCount = summary.TotalCount
// 是否导出
if req.IsExport == 1 {
filePath, err := marketStoreSalesDataExport(storeManageDataList, summary, c)
if err != nil {
logger.Error("storeSalesDataExport err:", logger.Field("err", err))
return nil, err
}
resp.ExportUrl = filePath
} else {
// 分页
startIndex := page * req.PageSize
endIndex := startIndex + req.PageSize
if endIndex > len(storeManageDataList) {
endIndex = len(storeManageDataList)
}
resp.List = storeManageDataList[startIndex:endIndex]
resp.Total = len(storeManageDataList)
}
return resp, nil
}
// marketStoreSalesDataExport 导出门店经营数据
func marketStoreSalesDataExport(list []MarketStoreSalesData, summary struct {
TotalSalesAmount float64
TotalPromotionFee float64
TotalSalesProfit float64
TotalStaffProfit float64
TotalCount int64
}, c *gin.Context) (string, error) {
file := excelize.NewFile()
fSheet := "Sheet1"
url := config.ExportConfig.Url
fileName := time.Now().Format(TimeFormat) + "门店经营数据" + ".xlsx"
fmt.Println("url fileName:", url+fileName)
// 判断是否有入销售毛利、员工毛利的权限
flag1, _ := checkRoleMenu(c, SalesProfitMenu)
flag2, _ := checkRoleMenu(c, StaffProfitMenu)
fmt.Println("flag1 is:", flag1)
fmt.Println("flag2 is:", flag2)
logger.Info("flag1 is:", logger.Field("flag1", flag1))
logger.Info("flag2 is:", logger.Field("flag2", flag2))
nEndCount := 0
title := []interface{}{"店铺名称", "销售额", "推广费"}
if flag1 { // 销售毛利
title = append(title, "销售毛利")
nEndCount += 1
}
if flag2 { // 员工毛利
title = append(title, "员工毛利")
nEndCount += 1
}
title = append(title, "销售数量")
for i, _ := range title {
cell, _ := excelize.CoordinatesToCellName(1+i, 1)
err := file.SetCellValue(fSheet, cell, title[i])
if err != nil {
logger.Errorf("file set value err:", err)
}
}
var row []interface{}
nExcelStartRow := 0
for rowId := 0; rowId < len(list); rowId++ {
row = []interface{}{
list[rowId].StoreName,
list[rowId].SalesAmount,
list[rowId].PromotionFee,
}
// 控制是否导出
if flag1 { // 销售毛利
row = append(row, list[rowId].SalesProfit)
}
if flag2 { // 员工毛利
row = append(row, list[rowId].StaffProfit)
}
row = append(row, list[rowId].Count)
for j, _ := range row {
cell, _ := excelize.CoordinatesToCellName(1+j, nExcelStartRow+2)
err := file.SetCellValue(fSheet, cell, row[j])
if err != nil {
logger.Error("file set value err:", logger.Field("err", err))
}
}
nExcelStartRow++
}
totalData := "汇总 记录数:" + strconv.FormatInt(int64(len(list)), 10)
end := []interface{}{totalData, summary.TotalSalesAmount, summary.TotalPromotionFee}
if flag1 { // 销售毛利
end = append(end, summary.TotalSalesProfit)
}
if flag2 { // 员工毛利
end = append(end, summary.TotalStaffProfit)
}
end = append(end, summary.TotalCount)
for i, _ := range end {
cell, _ := excelize.CoordinatesToCellName(1+i, nExcelStartRow+2)
err := file.SetCellValue(fSheet, cell, end[i])
if err != nil {
logger.Error("file set value err:", logger.Field("err", err))
}
}
// 设置所有单元格的样式: 居中、加边框
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("Sheet1", 1, 20)
// 设置单元格大小
file.SetColWidth("Sheet1", "A", "A", 30)
file.SetColWidth("Sheet1", "B", "B", 18)
file.SetColWidth("Sheet1", "C", "C", 18)
file.SetColWidth("Sheet1", "D", "D", 18)
file.SetColWidth("Sheet1", "E", "E", 18)
file.SetColWidth("Sheet1", "F", "F", 18)
var endRow string
switch nEndCount {
case 1:
endRow = fmt.Sprintf("E"+"%d", nExcelStartRow+2)
case 2:
endRow = fmt.Sprintf("F"+"%d", nExcelStartRow+2)
default:
endRow = fmt.Sprintf("D"+"%d", nExcelStartRow+2)
}
// 应用样式到整个表格
_ = file.SetCellStyle("Sheet1", "A1", endRow, style)
fmt.Println("save fileName:", config.ExportConfig.Path+fileName)
if err := file.SaveAs(config.ExportConfig.Path + fileName); err != nil {
fmt.Println(err)
}
return url + fileName, nil
}