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 }