diff --git a/controllers/store_info.go b/controllers/store_info.go new file mode 100644 index 0000000..5259667 --- /dev/null +++ b/controllers/store_info.go @@ -0,0 +1,33 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "psi/constant" + "psi/database" + systemReq "psi/models/request" + systemRes "psi/models/response" + "psi/service" + "psi/utils" +) + +type StoreInfoApi struct{} + +var storeInfoService = service.StoreInfoService{} + +// StoreInfo 店铺统计接口 +func (i *StoreInfoApi) StoreInfo(c *gin.Context) { + var req systemReq.StoreInfoRequest + + if err := c.ShouldBindQuery(&req); err != nil { + ValidAndFail(constant.LoggerChannelRequest, "店铺统计请求异常", "参数错误: "+err.Error(), c, err) + return + } + + result, err := storeInfoService.StoreInfo(req, database.GetDB(c)) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "店铺统计异常", err, c, req) + return + } + + systemRes.OkWithDetailed(result, "查询成功", c) +} diff --git a/models/outbound_order.go b/models/outbound_order.go index 8317570..4874df8 100644 --- a/models/outbound_order.go +++ b/models/outbound_order.go @@ -14,9 +14,9 @@ type OutboundOrder struct { Operator string `json:"operator" gorm:"size:100;not null;default:'';comment:操作员"` OperatorID int64 `json:"operator_id" gorm:"not null;default:0;comment:操作员ID"` Remark string `json:"remark" gorm:"size:255;not null;default:'';comment:备注"` - CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:创建时间戳(秒)"` + CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;index:idx_oo_del_shop_time;priority:3;index:idx_oo_del_time;priority:2;comment:创建时间戳(秒)"` UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:更新时间戳(秒)"` - IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;comment:逻辑删除标记(0:未删除,1:已删除)"` + IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;index:idx_oo_del_shop_time;priority:1;index:idx_oo_del_time;priority:1;comment:逻辑删除标记(0:未删除,1:已删除)"` } func (OutboundOrder) TableName() string { diff --git a/models/product.go b/models/product.go index fbf8756..55d997b 100644 --- a/models/product.go +++ b/models/product.go @@ -25,7 +25,7 @@ type Product struct { Status int8 `json:"status" gorm:"type:tinyint(1);not null;default:1;comment:状态(0:禁用,1:启用)"` CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:创建时间戳"` UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:更新时间戳"` - IsDel int8 `json:"is_del" gorm:"not null;default:0;comment:逻辑删除"` + IsDel int8 `json:"is_del" gorm:"not null;default:0;index:idx_product_del;comment:逻辑删除"` } func (Product) TableName() string { diff --git a/models/receiving_order.go b/models/receiving_order.go index a9660b4..bad76ff 100644 --- a/models/receiving_order.go +++ b/models/receiving_order.go @@ -7,16 +7,16 @@ type ReceivingOrder struct { PurchaseOrderID int64 `json:"purchase_order_id" gorm:"not null;default:0;index;comment:关联采购单ID(可为空,支持无来源入库)"` WaveTaskID int64 `json:"wave_task_id" gorm:"not null;default:0;index;comment:关联波次任务ID"` WarehouseID int64 `json:"warehouse_id" gorm:"not null;default:0;index;comment:入库仓库ID"` - ShopId int64 `json:"shop_id" gorm:"type:bigint(20);default:0;comment:店铺ID"` + ShopId int64 `json:"shop_id" gorm:"type:bigint(20);default:0;index:idx_ro_del_shop_time;priority:2;comment:店铺ID"` SupplierID int64 `json:"supplier_id" gorm:"not null;default:0;comment:供应商ID"` ReceivingDate int64 `json:"receiving_date" gorm:"not null;default:0;comment:入库日期时间戳(秒)"` Status int8 `json:"status" gorm:"not null;default:1;index;comment:状态(1:待收货/pending, 2:验收中/checking, 3:已完成/completed, 4:已取消/cancelled)"` Operator string `json:"operator" gorm:"size:100;not null;default:'';comment:操作人"` OperatorID int64 `json:"operator_id" gorm:"not null;default:0;comment:操作人ID"` Remark string `json:"remark" gorm:"size:255;not null;default:'';comment:备注"` - CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:创建时间戳(秒)"` + CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;index:idx_ro_del_time;priority:2;comment:创建时间戳(秒)"` UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:更新时间戳(秒)"` - IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;comment:逻辑删除标记(0:未删除,1:已删除)"` + IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;index:idx_ro_del_shop_time;priority:1;index:idx_ro_del_time;priority:1;comment:逻辑删除标记(0:未删除,1:已删除)"` } func (ReceivingOrder) TableName() string { diff --git a/models/request/outbound.go b/models/request/outbound.go index 57f380e..1cb1444 100644 --- a/models/request/outbound.go +++ b/models/request/outbound.go @@ -12,6 +12,7 @@ type GetOutboundOrderListRequest struct { EndDate int64 `form:"end_date"` AssociationOrderNo string `form:"association_order_no"` LogisticsNo string `form:"logistics_no"` + ShopType int `form:"shop_type" json:"shop_type"` } // GetOutboundOrderDetailRequest 获取出库单详情请求 diff --git a/models/request/sales.go b/models/request/sales.go index 0a9a373..f6c23cb 100644 --- a/models/request/sales.go +++ b/models/request/sales.go @@ -12,6 +12,7 @@ type GetSalesOrderListRequest struct { EndDate int64 `form:"end_date"` AssociationOrderNo string `form:"association_order_no"` LogisticsNo string `form:"logistics_no"` + ShopType int `form:"shop_type" json:"shop_type"` } // GetSalesOrderDetailRequest 获取销售订单详情请求 @@ -37,4 +38,5 @@ type GetSalesOrderDetailListRequest struct { EndDate int64 `form:"end_date"` AssociationOrderNo string `form:"association_order_no"` LogisticsNo string `form:"logistics_no"` + ShopType int `form:"shop_type" json:"shop_type"` } diff --git a/models/request/shipping.go b/models/request/shipping.go index f58b074..3bc8034 100644 --- a/models/request/shipping.go +++ b/models/request/shipping.go @@ -11,6 +11,7 @@ type GetShippingOrderListRequest struct { EndDate int64 `form:"end_date" json:"end_date"` AssociationOrderNo string `form:"association_order_no" json:"association_order_no"` LogisticsNo string `form:"logistics_no" json:"logistics_no"` + ShopType int `form:"shop_type" json:"shop_type"` } // GetShippingOrderDetailRequest 获取发货单详情请求 @@ -29,4 +30,5 @@ type GetShippingOrderDetailListRequest struct { EndDate int64 `form:"end_date" json:"end_date"` AssociationOrderNo string `form:"association_order_no" json:"association_order_no"` LogisticsNo string `form:"logistics_no" json:"logistics_no"` + ShopType int `form:"shop_type" json:"shop_type"` } diff --git a/models/request/store_info.go b/models/request/store_info.go new file mode 100644 index 0000000..55ba8c9 --- /dev/null +++ b/models/request/store_info.go @@ -0,0 +1,7 @@ +package request + +// StoreInfoRequest 店铺统计请求 +type StoreInfoRequest struct { + TimeRange string `json:"time_range" form:"time_range"` // 时间范围:today/yesterday/7days/30days/90days/180days/365days + StoreName string `json:"store_name" form:"store_name"` // 店铺名称(模糊搜索) +} diff --git a/models/response/sales.go b/models/response/sales.go index 9bab858..0774939 100644 --- a/models/response/sales.go +++ b/models/response/sales.go @@ -69,6 +69,7 @@ type SalesOrderDetailResponse struct { TotalAmount int64 `json:"total_amount"` Status int8 `json:"status"` StatusText string `json:"status_text"` + ShopTypeText string `json:"shop_type_text"` SalesPerson string `json:"sales_person"` SalesPersonID int64 `json:"sales_person_id"` AssociationOrderNo string `json:"association_order_no"` @@ -147,6 +148,7 @@ func ConvertSalesOrderToDetail(order models.SalesOrder, customerName string, war TotalAmount: order.TotalAmount, Status: order.Status, StatusText: GetSalesOrderStatusText(order.Status), + ShopTypeText: GetShopTypeStatusText(order.ShopType), SalesPerson: order.SalesPerson, SalesPersonID: order.SalesPersonID, @@ -189,7 +191,7 @@ func GetShopTypeStatusText(status int8) string { statusMap := map[int8]string{ 1: "拼多多", 2: "孔夫子", - 5: "咸鱼", + 5: "闲鱼", } if text, ok := statusMap[status]; ok { return text diff --git a/models/response/statist.go b/models/response/statist.go index 3f981b6..328f730 100644 --- a/models/response/statist.go +++ b/models/response/statist.go @@ -2,16 +2,17 @@ package response // DashboardStatistResponse 仪表盘统计响应 type DashboardStatistResponse struct { - TotalReceivingCount int64 `json:"total_receiving_count"` // 总入库次数 - TotalOutboundCount int64 `json:"total_outbound_count"` // 总出库次数 - TotalSaleCount int64 `json:"total_sale_count"` // 今日销售数 - UserStats []UserStatItem `json:"user_stats"` // 个人统计数据 - ProductTotal int64 `json:"product_total"` // 商品总量 - InventoryTotal int64 `json:"inventory_total"` // 库存量 - TodayInbound int64 `json:"today_inbound"` // 今日入库 - TodayOutbound int64 `json:"today_outbound"` // 今日出库 - YesterdayInbound int64 `json:"yesterday_inbound"` // 昨日入库 - YesterdayOutbound int64 `json:"yesterday_outbound"` // 昨日出库 + TotalOrderCount int64 `json:"total_order_count"` // 今日订单数 + TotalReceivingCount int64 `json:"total_receiving_count"` // 今日入库数 + TotalOutboundCount int64 `json:"total_outbound_count"` // 今日出库数 + TotalSaleCount int64 `json:"total_sale_count"` // 今日销售数 + YesterdayOrderCount int64 `json:"yesterday_order_count"` // 昨日订单数 + YesterdayReceivingCount int64 `json:"yesterday_receiving_count"` // 昨日入库数 + YesterdayOutboundCount int64 `json:"yesterday_outbound_count"` // 昨日出库数 + YesterdaySaleCount int64 `json:"yesterday_sale_count"` // 昨日销售数 + UserStats []UserStatItem `json:"user_stats"` // 个人统计数据 + ProductTotal int64 `json:"product_total"` // 商品总量 + InventoryTotal int64 `json:"inventory_total"` // 库存量 } // UserStatItem 个人统计项 diff --git a/models/response/store_info.go b/models/response/store_info.go new file mode 100644 index 0000000..d446d28 --- /dev/null +++ b/models/response/store_info.go @@ -0,0 +1,12 @@ +package response + +// StoreInfoResponse 店铺统计响应(每条记录对应一个店铺) +type StoreInfoResponse struct { + StoreName string `json:"store_name"` // 店铺名称 + StoreType string `json:"store_type"` // 店铺类型描述 + SaleCount int64 `json:"sale_count"` // 销售数量(时间范围内该店铺的销售订单数) + OutboundCount int64 `json:"outbound_count"` // 出库次数(当前表结构无店铺维度,暂返回 0) + ReceivingCount int64 `json:"receiving_count"` // 入库次数(当前表结构无店铺维度,暂返回 0) + OrderCount int64 `json:"order_count"` // 订单数量(同 sale_count,从 sales_order 表统计) + ShippingCount int64 `json:"shipping_count"` // 发货次数(当前表结构无店铺维度,暂返回 0) +} diff --git a/models/sales_order.go b/models/sales_order.go index 9bae267..c7bad84 100644 --- a/models/sales_order.go +++ b/models/sales_order.go @@ -15,12 +15,12 @@ type SalesOrder struct { TotalAmount int64 `json:"total_amount" gorm:"not null;default:0;comment:订单总金额(分)"` Status int8 `json:"status" gorm:"not null;default:1;index;comment:状态(1:草稿/draft,2:已确认/confirmed,3:已分配库存/allocated,4:拣货完成/picking,5:已发货/shipped,6:已取消/cancelled)"` SalesPerson string `json:"sales_person" gorm:"size:100;not null;default:'';comment:店铺名称"` - SalesPersonID int64 `json:"sales_person_id" gorm:"not null;default:0;comment:店铺ID"` + SalesPersonID int64 `json:"sales_person_id" gorm:"not null;default:0;index:idx_so_del_person_time;priority:2;comment:店铺ID"` Remark string `json:"remark" gorm:"size:255;not null;default:'';comment:备注"` IsDistribution int8 `json:"is_distribution" gorm:"not null;default:0;comment:是否分销 0-正常 1-分销"` - CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:创建时间戳(秒)"` + CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;index:idx_so_del_person_time;priority:3;index:idx_so_del_time;priority:2;comment:创建时间戳(秒)"` UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:更新时间戳(秒)"` - IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;comment:逻辑删除标记(0:未删除,1:已删除)"` + IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;index:idx_so_del_person_time;priority:1;index:idx_so_del_time;priority:1;comment:逻辑删除标记(0:未删除,1:已删除)"` } func (SalesOrder) TableName() string { diff --git a/models/shipping_order.go b/models/shipping_order.go index a4557c5..05fb6da 100644 --- a/models/shipping_order.go +++ b/models/shipping_order.go @@ -5,15 +5,15 @@ type ShippingOrder struct { ID int64 `json:"id" gorm:"primarykey;comment:主键ID"` ShippingNo string `json:"shipping_no" gorm:"size:64;not null;default:'';uniqueIndex;comment:发货单号"` CustomerID int64 `json:"customer_id" gorm:"not null;default:0;index;comment:客户ID"` - ShopId int64 `json:"shop_id" gorm:"type:bigint(20);default:0;comment:店铺ID"` + ShopId int64 `json:"shop_id" gorm:"type:bigint(20);default:0;index:idx_sh_del_shop_time;priority:2;comment:店铺ID"` Status int8 `json:"status" gorm:"not null;default:1;index;comment:状态:1=待发货 2=已发货 3=已签收 4=已取消"` ShippingTime *int64 `json:"shipping_time" gorm:"type:bigint;comment:发货时间(时间戳秒)"` ExpectedArriveTime *int64 `json:"expected_arrive_time" gorm:"type:bigint;comment:预计到达时间(时间戳秒)"` ActualArriveTime *int64 `json:"actual_arrive_time" gorm:"type:bigint;comment:实际签收时间(时间戳秒)"` Operator string `json:"operator" gorm:"size:50;comment:操作人"` - CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:创建时间(时间戳秒)"` + CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;index:idx_sh_del_shop_time;priority:3;index:idx_sh_del_time;priority:2;comment:创建时间(时间戳秒)"` UpdatedAt *int64 `json:"updated_at" gorm:"type:bigint;comment:更新时间(时间戳秒)"` - IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;comment:逻辑删除标记(0:未删除,1:已删除)"` + IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;index:idx_sh_del_shop_time;priority:1;index:idx_sh_del_time;priority:1;comment:逻辑删除标记(0:未删除,1:已删除)"` Remark string `json:"remark" gorm:"size:500;comment:备注"` } diff --git a/service/StatistTaskService.go b/service/StatistTaskService.go index b86a353..57ee840 100644 --- a/service/StatistTaskService.go +++ b/service/StatistTaskService.go @@ -125,23 +125,34 @@ func (s *StatistTaskService) generateDashboardDailyStat(tx *gorm.DB, statDate in } } + // statist.stat_date 存储 YYYYMMDD 格式 int64(如 20260626),非 Unix 时间戳 + // statDate 已经是 YYYYMMDD 格式,直接用于查询 + // 昨天的 YYYYMMDD = statDate;前天需从 startDate(Unix时间戳)反算 + yesterdayTime := time.Unix(startDate, 0) + dayBeforeTime := yesterdayTime.AddDate(0, 0, -1) + dayBeforeStatDateStr := fmt.Sprintf("%04d%02d%02d", dayBeforeTime.Year(), dayBeforeTime.Month(), dayBeforeTime.Day()) + var dayBeforeStatDate int64 + fmt.Sscanf(dayBeforeStatDateStr, "%d", &dayBeforeStatDate) + + // 前天的 Unix 时间戳范围(用于 sales_order 查询,sales_order.created_at 存储 Unix 时间戳) + dayBeforeStart := time.Date(dayBeforeTime.Year(), dayBeforeTime.Month(), dayBeforeTime.Day(), 0, 0, 0, 0, dayBeforeTime.Location()).Unix() + dayBeforeEnd := time.Date(dayBeforeTime.Year(), dayBeforeTime.Month(), dayBeforeTime.Day(), 23, 59, 59, 0, dayBeforeTime.Location()).Unix() + var totalReceivingNum, totalOutboundNum int64 tx.Model(&models.Statist{}). - Where("stat_date <= ? AND is_del = ?", endDate, 0). + Where("stat_date <= ? AND is_del = ?", statDate, 0). Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). Row().Scan(&totalReceivingNum, &totalOutboundNum) var todayInbound, todayOutbound int64 tx.Model(&models.Statist{}). - Where("stat_date >= ? AND stat_date <= ? AND is_del = ?", startDate, endDate, 0). + Where("stat_date = ? AND is_del = ?", statDate, 0). Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). Row().Scan(&todayInbound, &todayOutbound) - beforeYesterdayStart := time.Date(time.Unix(startDate, 0).Year(), time.Unix(startDate, 0).Month(), time.Unix(startDate, 0).Day()-1, 0, 0, 0, 0, time.Unix(startDate, 0).Location()).Unix() - beforeYesterdayEnd := time.Date(time.Unix(startDate, 0).Year(), time.Unix(startDate, 0).Month(), time.Unix(startDate, 0).Day()-1, 23, 59, 59, 0, time.Unix(startDate, 0).Location()).Unix() var yesterdayInbound, yesterdayOutbound int64 tx.Model(&models.Statist{}). - Where("stat_date >= ? AND stat_date <= ? AND is_del = ?", beforeYesterdayStart, beforeYesterdayEnd, 0). + Where("stat_date = ? AND is_del = ?", dayBeforeStatDate, 0). Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). Row().Scan(&yesterdayInbound, &yesterdayOutbound) @@ -157,7 +168,7 @@ func (s *StatistTaskService) generateDashboardDailyStat(tx *gorm.DB, statDate in var yesterdaySalesCount int64 tx.Model(&models.SalesOrder{}). - Where("created_at >= ? AND created_at <= ? AND is_del = ?", beforeYesterdayStart, beforeYesterdayEnd, 0). + Where("created_at >= ? AND created_at <= ? AND is_del = ?", dayBeforeStart, dayBeforeEnd, 0). Count(&yesterdaySalesCount) var productTotal int64 @@ -231,7 +242,7 @@ func (s *StatistTaskService) generateUserDailyStat(tx *gorm.DB, mainDB *gorm.DB, } var receivingStats []UserReceivingStat tx.Model(&models.Statist{}). - Where("create_by IN ? AND stat_date >= ? AND stat_date <= ? AND is_del = ?", userIDs, startDate, endDate, 0). + Where("create_by IN ? AND stat_date = ? AND is_del = ?", userIDs, statDate, 0). Select("create_by, COALESCE(SUM(receiving_num), 0) as receiving_num, COALESCE(SUM(outbound_num), 0) as outbound_num"). Group("create_by"). Find(&receivingStats) diff --git a/service/book.go b/service/book.go index a542670..580e2fb 100644 --- a/service/book.go +++ b/service/book.go @@ -14,7 +14,7 @@ import ( systemRes "psi/models/response" "strconv" "strings" - + "time" ) type BookService struct{} diff --git a/service/outbound.go b/service/outbound.go index e30039b..d2d6498 100644 --- a/service/outbound.go +++ b/service/outbound.go @@ -59,6 +59,13 @@ func (s *OutboundService) GetOutboundOrderList(req systemReq.GetOutboundOrderLis Where("outbound_order_item.is_del = 0 AND sales_order_item.logistics_no LIKE ?", "%"+req.LogisticsNo+"%") query = query.Where("outbound_order.id IN (?)", subQuery) } + if req.ShopType > 0 { + subQuery := databaseConn.Table("outbound_order_item"). + Select("outbound_order_item.out_order_id"). + Joins("INNER JOIN sales_order ON outbound_order_item.sales_order_id = sales_order.id AND sales_order.is_del = 0"). + Where("outbound_order_item.is_del = 0 AND sales_order.shop_type = ?", req.ShopType) + query = query.Where("outbound_order.id IN (?)", subQuery) + } var total int64 if err := query.Count(&total).Error; err != nil { @@ -86,50 +93,85 @@ func (s *OutboundService) GetOutboundOrderList(req systemReq.GetOutboundOrderLis return nil, utils.NewError("查询出库单列表失败") } + // 收集订单ID用于批量查询 + orderIDs := make([]int64, len(orders)) + for i, order := range orders { + orderIDs[i] = order.ID + } + + // 批量查询店铺信息:按 out_order_id 分组 + type shopRow struct { + OutOrderID int64 `gorm:"column:out_order_id"` + ShopName string `gorm:"column:shop_name"` + ShopType int8 `gorm:"column:shop_type"` + } + var shopRows []shopRow + databaseConn.Table("outbound_order_item"). + Select("DISTINCT outbound_order_item.out_order_id, so.sales_person as shop_name, so.shop_type as shop_type"). + Joins("INNER JOIN sales_order so ON outbound_order_item.sales_order_id = so.id AND so.is_del = 0"). + Where("outbound_order_item.out_order_id IN ? AND outbound_order_item.is_del = ?", orderIDs, 0). + Scan(&shopRows) + + shopsByOrderID := make(map[int64][]systemRes.OutboundShopInfo, len(orderIDs)) + for _, row := range shopRows { + shopsByOrderID[row.OutOrderID] = append(shopsByOrderID[row.OutOrderID], systemRes.OutboundShopInfo{ + ShopName: row.ShopName, + ShopType: row.ShopType, + ShopTypeText: systemRes.GetShopTypeText(row.ShopType), + }) + } + + // 批量查询关联订单号:按 out_order_id 分组 + type assocRow struct { + OutOrderID int64 `gorm:"column:out_order_id"` + AssociationOrderNo string `gorm:"column:association_order_no"` + } + var assocRows []assocRow + databaseConn.Table("outbound_order_item"). + Select("outbound_order_item.out_order_id, GROUP_CONCAT(DISTINCT so.association_order_no SEPARATOR ', ') as association_order_no"). + Joins("INNER JOIN sales_order so ON outbound_order_item.sales_order_id = so.id AND so.is_del = 0"). + Where("outbound_order_item.out_order_id IN ? AND outbound_order_item.is_del = ? AND so.association_order_no != ''", orderIDs, 0). + Group("outbound_order_item.out_order_id"). + Scan(&assocRows) + + assocByOrderID := make(map[int64]string, len(assocRows)) + for _, row := range assocRows { + assocByOrderID[row.OutOrderID] = row.AssociationOrderNo + } + + // 批量查询物流单号:按 out_order_id 分组 + type logRow struct { + OutOrderID int64 `gorm:"column:out_order_id"` + LogisticsNos string `gorm:"column:logistics_nos"` + } + var logRows []logRow + databaseConn.Table("outbound_order_item"). + Select("outbound_order_item.out_order_id, COALESCE(GROUP_CONCAT(DISTINCT soi.logistics_no SEPARATOR ', '), '') as logistics_nos"). + Joins("INNER JOIN sales_order_item soi ON outbound_order_item.sales_order_id = soi.sales_order_id AND outbound_order_item.product_id = soi.product_id AND soi.is_del = 0"). + Where("outbound_order_item.out_order_id IN ? AND outbound_order_item.is_del = ? AND soi.logistics_no != ''", orderIDs, 0). + Group("outbound_order_item.out_order_id"). + Scan(&logRows) + + logByOrderID := make(map[int64]string, len(logRows)) + for _, row := range logRows { + logByOrderID[row.OutOrderID] = row.LogisticsNos + } + + // 组装结果:每个订单从 map 中查找 orderItems := make([]systemRes.OutboundOrderItem, 0, len(orders)) for _, order := range orders { - var shopList []systemRes.OutboundShopInfo - - var shops []struct { - ShopName string `gorm:"column:shop_name"` - ShopType int8 `gorm:"column:shop_type"` + shopList := shopsByOrderID[order.ID] + if shopList == nil { + shopList = []systemRes.OutboundShopInfo{} } - databaseConn.Table("outbound_order_item"). - Select("DISTINCT so.sales_person as shop_name, so.shop_type as shop_type"). - Joins("INNER JOIN sales_order so ON outbound_order_item.sales_order_id = so.id AND so.is_del = 0"). - Where("outbound_order_item.out_order_id = ? AND outbound_order_item.is_del = ?", order.ID, 0). - Scan(&shops) - - for _, shop := range shops { - shopList = append(shopList, systemRes.OutboundShopInfo{ - ShopName: shop.ShopName, - ShopType: shop.ShopType, - ShopTypeText: systemRes.GetShopTypeText(shop.ShopType), - }) - } - - var associationOrderNos string - databaseConn.Table("outbound_order_item"). - Select("GROUP_CONCAT(DISTINCT so.association_order_no SEPARATOR ', ')"). - Joins("INNER JOIN sales_order so ON outbound_order_item.sales_order_id = so.id AND so.is_del = 0"). - Where("outbound_order_item.out_order_id = ? AND outbound_order_item.is_del = ? AND so.association_order_no != ''", order.ID, 0). - Scan(&associationOrderNos) - - var logisticsNos string - databaseConn.Table("outbound_order_item"). - Select("GROUP_CONCAT(DISTINCT soi.logistics_no SEPARATOR ', ')"). - Joins("INNER JOIN sales_order_item soi ON outbound_order_item.sales_order_id = soi.sales_order_id AND outbound_order_item.product_id = soi.product_id AND soi.is_del = 0"). - Where("outbound_order_item.out_order_id = ? AND outbound_order_item.is_del = ? AND soi.logistics_no != ''", order.ID, 0). - Scan(&logisticsNos) - orderItems = append(orderItems, systemRes.ConvertOutboundOrderToItem( order.OutboundOrder, order.CustomerName, order.WarehouseName, shopList, - associationOrderNos, - logisticsNos, + assocByOrderID[order.ID], + logByOrderID[order.ID], )) } diff --git a/service/process.go b/service/process.go index 6c7ebe7..c4f59d6 100644 --- a/service/process.go +++ b/service/process.go @@ -333,6 +333,15 @@ func (s *ProcessService) BindWave(req systemReq.BindWaveRequest, db ...*gorm.DB) return fmt.Errorf("采购订单不存在: %v", err) } + // 获取小车关联的店铺ID + var shopId int64 + if waveTask.CarId > 0 { + var carShop models.CarShop + if err := tx.Where("car_id = ? AND is_del = 0", waveTask.CarId).First(&carShop).Error; err == nil { + shopId = carShop.ShopID + } + } + receivingNo := utils.GenerateReceivingNo() receivingOrder := models.ReceivingOrder{ @@ -340,6 +349,7 @@ func (s *ProcessService) BindWave(req systemReq.BindWaveRequest, db ...*gorm.DB) PurchaseOrderID: purchaseOrder.ID, //采购订单ID WaveTaskID: waveTaskID, //入库任务ID WarehouseID: purchaseOrder.WarehouseID, //仓库ID + ShopId: shopId, //店铺ID(来自小车) SupplierID: purchaseOrder.SupplierID, //供应商ID ReceivingDate: now, //入库日期时间戳(秒) Status: constant.ReceivingStatusPending, //状态(1:待收货/pending, ) @@ -1623,6 +1633,7 @@ func (s *ProcessService) CreateOutboundOrder(req systemReq.CreateOutboundOrderRe WaveTaskID: 0, WarehouseID: warehouseID, CustomerID: customerID, + ShopId: salesOrders[0].SalesPersonID, TotalQuantity: 0, TotalAmount: 0, Status: constant.OutboundStatusCreated, @@ -2127,11 +2138,29 @@ func (s *ProcessService) CreateShippingOrder(req systemReq.CreateShippingOrderRe return fmt.Errorf("选中的出库单没有明细数据") } + // 获取店铺ID(从出库单明细追溯销售单的 sales_person_id) + var shopId int64 + if outboundOrders[0].ShopId > 0 { + shopId = outboundOrders[0].ShopId + } else { + // 兜底:通过 outbound_order_item → sales_order → sales_person_id 获取 + var salesPersonID int64 + if err := tx.Raw(` + SELECT so.sales_person_id FROM outbound_order_item ooi + JOIN sales_order so ON ooi.sales_order_id = so.id AND so.is_del = 0 + WHERE ooi.out_order_id IN ? AND ooi.is_del = 0 + LIMIT 1 + `, req.OutboundOrderIDs).Scan(&salesPersonID).Error; err == nil && salesPersonID > 0 { + shopId = salesPersonID + } + } + shippingNo = utils.GenerateShippingNo() shippingOrder := models.ShippingOrder{ ShippingNo: shippingNo, CustomerID: customerID, + ShopId: shopId, Status: constant.ShippingStatusPending, ExpectedArriveTime: req.ExpectedArriveTime, Operator: operator, diff --git a/service/sales.go b/service/sales.go index f41a3a5..b9e16d5 100644 --- a/service/sales.go +++ b/service/sales.go @@ -57,6 +57,9 @@ func (s *SalesService) GetSalesOrderList(req systemReq.GetSalesOrderListRequest, Where("logistics_no LIKE ? AND is_del = 0", "%"+req.LogisticsNo+"%") query = query.Where("sales_order.id IN (?)", subQuery) } + if req.ShopType > 0 { + query = query.Where("sales_order.shop_type = ?", req.ShopType) + } var total int64 if err := query.Count(&total).Error; err != nil { @@ -84,19 +87,35 @@ func (s *SalesService) GetSalesOrderList(req systemReq.GetSalesOrderListRequest, return nil, utils.NewError("查询销售订单列表失败") } + // 收集订单ID用于批量查询物流单号 + orderIDs := make([]int64, len(orders)) + for i, order := range orders { + orderIDs[i] = order.ID + } + + type logRow struct { + SalesOrderID int64 `gorm:"column:sales_order_id"` + LogisticsNos string `gorm:"column:logistics_nos"` + } + var logRows []logRow + databaseConn.Table("sales_order_item"). + Select("sales_order_id, COALESCE(GROUP_CONCAT(DISTINCT logistics_no SEPARATOR ', '), '') as logistics_nos"). + Where("sales_order_id IN ? AND is_del = ? AND logistics_no != ''", orderIDs, 0). + Group("sales_order_id"). + Scan(&logRows) + + logByOrderID := make(map[int64]string, len(logRows)) + for _, row := range logRows { + logByOrderID[row.SalesOrderID] = row.LogisticsNos + } + orderItems := make([]systemRes.SalesOrderItem, 0, len(orders)) for _, order := range orders { - var logisticsNos string - databaseConn.Table("sales_order_item"). - Select("GROUP_CONCAT(DISTINCT logistics_no SEPARATOR ', ')"). - Where("sales_order_id = ? AND is_del = ? AND logistics_no != ''", order.ID, 0). - Scan(&logisticsNos) - orderItems = append(orderItems, systemRes.ConvertSalesOrderToItem( order.SalesOrder, order.CustomerName, order.WarehouseName, - logisticsNos, + logByOrderID[order.ID], )) } @@ -168,6 +187,9 @@ func (s *SalesService) GetSalesOrderDetailList(req systemReq.GetSalesOrderDetail Where("logistics_no LIKE ? AND is_del = 0", "%"+req.LogisticsNo+"%") query = query.Where("sales_order.id IN (?)", subQuery) } + if req.ShopType > 0 { + query = query.Where("sales_order.shop_type = ?", req.ShopType) + } var total int64 if err := query.Count(&total).Error; err != nil { diff --git a/service/shipping.go b/service/shipping.go index d6b8582..190d6bf 100644 --- a/service/shipping.go +++ b/service/shipping.go @@ -57,6 +57,14 @@ func (s *ShippingService) GetShippingOrderList(req systemReq.GetShippingOrderLis Where("shipping_order_item.is_del = 0 AND sales_order_item.logistics_no LIKE ?", "%"+req.LogisticsNo+"%") query = query.Where("shipping_order.id IN (?)", subQuery) } + if req.ShopType > 0 { + subQuery := databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id"). + Joins("INNER JOIN outbound_order_item ON shipping_order_item.outbound_order_item_id = outbound_order_item.id AND outbound_order_item.is_del = 0"). + Joins("INNER JOIN sales_order ON outbound_order_item.sales_order_id = sales_order.id AND sales_order.is_del = 0"). + Where("shipping_order_item.is_del = 0 AND sales_order.shop_type = ?", req.ShopType) + query = query.Where("shipping_order.id IN (?)", subQuery) + } var total int64 if err := query.Count(&total).Error; err != nil { @@ -83,52 +91,87 @@ func (s *ShippingService) GetShippingOrderList(req systemReq.GetShippingOrderLis return nil, utils.NewError("查询发货单列表失败") } + // 收集订单ID用于批量查询 + orderIDs := make([]int64, len(orders)) + for i, order := range orders { + orderIDs[i] = order.ID + } + + // 批量查询店铺信息:按 shipping_order_id 分组 + type shopRow struct { + ShippingOrderID int64 `gorm:"column:shipping_order_id"` + ShopName string `gorm:"column:shop_name"` + ShopType int8 `gorm:"column:shop_type"` + } + var shopRows []shopRow + databaseConn.Table("shipping_order_item"). + Select("DISTINCT shipping_order_item.shipping_order_id, so.sales_person as shop_name, so.shop_type as shop_type"). + Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). + Joins("INNER JOIN sales_order so ON ooi.sales_order_id = so.id AND so.is_del = 0"). + Where("shipping_order_item.shipping_order_id IN ? AND shipping_order_item.is_del = ?", orderIDs, 0). + Scan(&shopRows) + + shopsByOrderID := make(map[int64][]systemRes.OutboundShopInfo, len(orderIDs)) + for _, row := range shopRows { + shopsByOrderID[row.ShippingOrderID] = append(shopsByOrderID[row.ShippingOrderID], systemRes.OutboundShopInfo{ + ShopName: row.ShopName, + ShopType: row.ShopType, + ShopTypeText: systemRes.GetShopTypeText(row.ShopType), + }) + } + + // 批量查询关联订单号:按 shipping_order_id 分组 + type assocRow struct { + ShippingOrderID int64 `gorm:"column:shipping_order_id"` + AssociationOrderNo string `gorm:"column:association_order_no"` + } + var assocRows []assocRow + databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id, COALESCE(GROUP_CONCAT(DISTINCT so.association_order_no SEPARATOR ', '), '') as association_order_no"). + Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). + Joins("INNER JOIN sales_order so ON ooi.sales_order_id = so.id AND so.is_del = 0"). + Where("shipping_order_item.shipping_order_id IN ? AND shipping_order_item.is_del = ? AND so.association_order_no != ''", orderIDs, 0). + Group("shipping_order_item.shipping_order_id"). + Scan(&assocRows) + + assocByOrderID := make(map[int64]string, len(assocRows)) + for _, row := range assocRows { + assocByOrderID[row.ShippingOrderID] = row.AssociationOrderNo + } + + // 批量查询物流单号:按 shipping_order_id 分组 + type logRow struct { + ShippingOrderID int64 `gorm:"column:shipping_order_id"` + LogisticsNos string `gorm:"column:logistics_nos"` + } + var logRows []logRow + databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id, GROUP_CONCAT(DISTINCT soi.logistics_no SEPARATOR ', ') as logistics_nos"). + Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). + Joins("INNER JOIN sales_order_item soi ON ooi.sales_order_id = soi.sales_order_id AND ooi.product_id = soi.product_id AND soi.is_del = 0"). + Where("shipping_order_item.shipping_order_id IN ? AND shipping_order_item.is_del = ? AND soi.logistics_no != ''", orderIDs, 0). + Group("shipping_order_item.shipping_order_id"). + Scan(&logRows) + + logByOrderID := make(map[int64]string, len(logRows)) + for _, row := range logRows { + logByOrderID[row.ShippingOrderID] = row.LogisticsNos + } + + // 组装结果:每个订单从 map 中查找 orderItems := make([]systemRes.ShippingOrderItem, 0, len(orders)) for _, order := range orders { - var shopList []systemRes.OutboundShopInfo - - var shops []struct { - ShopName string `gorm:"column:shop_name"` - ShopType int8 `gorm:"column:shop_type"` + shopList := shopsByOrderID[order.ID] + if shopList == nil { + shopList = []systemRes.OutboundShopInfo{} } - databaseConn.Table("shipping_order_item"). - Select("DISTINCT so.sales_person as shop_name, so.shop_type as shop_type"). - Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). - Joins("INNER JOIN sales_order so ON ooi.sales_order_id = so.id AND so.is_del = 0"). - Where("shipping_order_item.shipping_order_id = ? AND shipping_order_item.is_del = ?", order.ID, 0). - Scan(&shops) - - for _, shop := range shops { - shopList = append(shopList, systemRes.OutboundShopInfo{ - ShopName: shop.ShopName, - ShopType: shop.ShopType, - ShopTypeText: systemRes.GetShopTypeText(shop.ShopType), - }) - } - - var associationOrderNos string - databaseConn.Table("shipping_order_item"). - Select("GROUP_CONCAT(DISTINCT so.association_order_no SEPARATOR ', ')"). - Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). - Joins("INNER JOIN sales_order so ON ooi.sales_order_id = so.id AND so.is_del = 0"). - Where("shipping_order_item.shipping_order_id = ? AND shipping_order_item.is_del = ? AND so.association_order_no != ''", order.ID, 0). - Scan(&associationOrderNos) - - var logisticsNos string - databaseConn.Table("shipping_order_item"). - Select("GROUP_CONCAT(DISTINCT soi.logistics_no SEPARATOR ', ')"). - Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). - Joins("INNER JOIN sales_order_item soi ON ooi.sales_order_id = soi.sales_order_id AND ooi.product_id = soi.product_id AND soi.is_del = 0"). - Where("shipping_order_item.shipping_order_id = ? AND shipping_order_item.is_del = ? AND soi.logistics_no != ''", order.ID, 0). - Scan(&logisticsNos) - orderItems = append(orderItems, systemRes.ConvertShippingOrderToItem( order.ShippingOrder, order.CustomerName, shopList, - associationOrderNos, - logisticsNos, + assocByOrderID[order.ID], + logByOrderID[order.ID], )) } @@ -288,6 +331,14 @@ func (s *ShippingService) GetShippingOrderDetailList(req systemReq.GetShippingOr Where("shipping_order_item.is_del = 0 AND sales_order_item.logistics_no LIKE ?", "%"+req.LogisticsNo+"%") query = query.Where("shipping_order.id IN (?)", subQuery) } + if req.ShopType > 0 { + subQuery := databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id"). + Joins("INNER JOIN outbound_order_item ON shipping_order_item.outbound_order_item_id = outbound_order_item.id AND outbound_order_item.is_del = 0"). + Joins("INNER JOIN sales_order ON outbound_order_item.sales_order_id = sales_order.id AND sales_order.is_del = 0"). + Where("shipping_order_item.is_del = 0 AND sales_order.shop_type = ?", req.ShopType) + query = query.Where("shipping_order.id IN (?)", subQuery) + } var total int64 if err := query.Count(&total).Error; err != nil { diff --git a/service/statist.go b/service/statist.go index 3d172b4..0a96a5c 100644 --- a/service/statist.go +++ b/service/statist.go @@ -7,7 +7,7 @@ import ( "psi/models" systemReq "psi/models/request" systemRes "psi/models/response" - "psi/utils" + "sync" "time" ) @@ -152,6 +152,7 @@ type StatistService struct{} }, nil }*/ // DashboardStatist 获取仪表盘统计数据 +// 统一口径:所有统计字段改为实时查原始单据表,不再依赖 dashboard_daily_stat 预计算表 func (s *StatistService) DashboardStatist(req systemReq.DashboardStatistRequest, db ...*gorm.DB) (*systemRes.DashboardStatistResponse, error) { databaseConn := database.OptionalDB(db...) @@ -159,8 +160,6 @@ func (s *StatistService) DashboardStatist(req systemReq.DashboardStatistRequest, startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Unix() - hasExplicitDate := req.EndDate != 0 - if req.StartDate == 0 { req.StartDate = startOfDay } @@ -168,162 +167,209 @@ func (s *StatistService) DashboardStatist(req systemReq.DashboardStatistRequest, req.EndDate = endOfDay } - var statDateStr string - if hasExplicitDate { - statDateStr = time.Unix(req.EndDate, 0).Format("20060102") - } else { - statDateStr = now.Format("20060102") - } - var statDate int64 - if _, err := fmt.Sscanf(statDateStr, "%d", &statDate); err != nil { - return s.getDashboardStatRealtime(databaseConn, req.StartDate, req.EndDate) - } - - var dashboardStat models.DashboardDailyStat - err := databaseConn.Where("stat_date = ? AND is_del = ?", statDate, 0).First(&dashboardStat).Error - - if err != nil { - return s.getDashboardStatRealtime(databaseConn, req.StartDate, req.EndDate) - } - - var userStats []systemRes.UserStatItem - var userDailyStats []models.UserDailyStat - if err := databaseConn.Where("stat_date = ? AND is_del = ?", statDate, 0).Find(&userDailyStats).Error; err == nil { - for _, userStat := range userDailyStats { - userStats = append(userStats, systemRes.UserStatItem{ - UserID: userStat.UserID, - UserName: userStat.UserName, - ReceivingCount: userStat.ReceivingNum, - OutboundCount: userStat.OutboundNum, - }) - } - } - - return &systemRes.DashboardStatistResponse{ - TotalReceivingCount: dashboardStat.TotalReceivingNum, - TotalOutboundCount: dashboardStat.TotalOutboundNum, - TotalSaleCount: dashboardStat.TodaySalesCount, - UserStats: userStats, - ProductTotal: dashboardStat.ProductTotal, - InventoryTotal: dashboardStat.InventoryTotal, - TodayInbound: dashboardStat.TodayInbound, - TodayOutbound: dashboardStat.TodayOutbound, - YesterdayInbound: dashboardStat.YesterdayInbound, - YesterdayOutbound: dashboardStat.YesterdayOutbound, - }, nil + return s.getDashboardStatRealtime(databaseConn, req.StartDate, req.EndDate) } -// getDashboardStatRealtime 实时查询仪表盘统计数据(降级方案) +// getDashboardStatRealtime 实时查询仪表盘统计数据 +// 统一口径:所有统计字段直接查原始单据表(sales_order / receiving_order / outbound_order) +// 所有独立查询通过 goroutine 并发执行,总耗时 = max(各查询耗时) func (s *StatistService) getDashboardStatRealtime(databaseConn *gorm.DB, startDate, endDate int64) (*systemRes.DashboardStatistResponse, error) { - var totalSaleCount int64 - saleOrderQuery := databaseConn.Model(&models.SalesOrder{}). - Where("created_at >= ? AND created_at <= ? AND is_del = ?", startDate, endDate, 0) - saleOrderQuery.Count(&totalSaleCount) - endTime := time.Unix(endDate, 0) - singleDayStart := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, endTime.Location()).Unix() - singleDayEnd := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 23, 59, 59, 0, endTime.Location()).Unix() + yesterdayTime := endTime.AddDate(0, 0, -1) - var totalReceivingCount, totalOutboundCount int64 - statistQuery := databaseConn.Model(&models.Statist{}). - Where("stat_date >= ? AND stat_date <= ? AND is_del = ?", singleDayStart, singleDayEnd, 0) + todayStart := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, endTime.Location()).Unix() + todayEnd := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 23, 59, 59, 0, endTime.Location()).Unix() + yesterdayStart := time.Date(yesterdayTime.Year(), yesterdayTime.Month(), yesterdayTime.Day(), 0, 0, 0, 0, yesterdayTime.Location()).Unix() + yesterdayEnd := time.Date(yesterdayTime.Year(), yesterdayTime.Month(), yesterdayTime.Day(), 23, 59, 59, 0, yesterdayTime.Location()).Unix() - var statistList []models.Statist - if err := statistQuery.Find(&statistList).Error; err != nil { - return nil, utils.NewError("查询统计数据失败") + // 结果结构体 + type salesTodayYesterday struct { + TodayCount int64 `gorm:"column:today_count"` + YesterdayCount int64 `gorm:"column:yesterday_count"` } - - if len(statistList) == 0 { - return &systemRes.DashboardStatistResponse{ - TotalReceivingCount: 0, - TotalOutboundCount: 0, - TotalSaleCount: totalSaleCount, - UserStats: []systemRes.UserStatItem{}, - }, nil + type receivingTodayYesterday struct { + TodayCount int64 `gorm:"column:today_count"` + YesterdayCount int64 `gorm:"column:yesterday_count"` } - - for _, stat := range statistList { - totalReceivingCount += stat.ReceivingNum - totalOutboundCount += stat.OutboundNum + type outboundTodayYesterday struct { + TodayCount int64 `gorm:"column:today_count"` + YesterdayCount int64 `gorm:"column:yesterday_count"` } - - var userStats []systemRes.UserStatItem - type UserStatGroup struct { CreateBy int64 `gorm:"column:create_by"` TotalReceiving int64 `gorm:"column:total_receiving"` TotalOutbound int64 `gorm:"column:total_outbound"` } - var userStatGroups []UserStatGroup - databaseConn.Model(&models.Statist{}). - Select("create_by, SUM(receiving_num) as total_receiving, SUM(outbound_num) as total_outbound"). - Where("stat_date >= ? AND stat_date <= ? AND is_del = ?", singleDayStart, singleDayEnd, 0). - Group("create_by"). - Scan(&userStatGroups) + var ( + wg sync.WaitGroup + salesStat salesTodayYesterday + receivingStat receivingTodayYesterday + outboundStat outboundTodayYesterday + userStatGroups []UserStatGroup + productTotal int64 + inventoryTotal int64 + statErr error + receivingErr error + outboundErr error + userErr error + productErr error + inventoryErr error + ) - for _, group := range userStatGroups { - var employee models.Employee - if err := databaseConn.Where("id = ?", group.CreateBy).First(&employee).Error; err == nil { - userStats = append(userStats, systemRes.UserStatItem{ - UserID: employee.ID, - UserName: employee.Username, - ReceivingCount: group.TotalReceiving, - OutboundCount: group.TotalOutbound, - }) + // 1. 销售订单统计(并发) + wg.Add(1) + go func() { + defer wg.Done() + statErr = databaseConn.Model(&models.SalesOrder{}). + Select(` + COALESCE(SUM(CASE WHEN created_at >= ? AND created_at <= ? THEN 1 ELSE 0 END), 0) as today_count, + COALESCE(SUM(CASE WHEN created_at >= ? AND created_at <= ? THEN 1 ELSE 0 END), 0) as yesterday_count + `, todayStart, todayEnd, yesterdayStart, yesterdayEnd). + Where("is_del = ?", 0). + Scan(&salesStat).Error + }() + + // 2. 入库单统计(并发) + wg.Add(1) + go func() { + defer wg.Done() + receivingErr = databaseConn.Model(&models.ReceivingOrder{}). + Select(` + COALESCE(SUM(CASE WHEN created_at >= ? AND created_at <= ? THEN 1 ELSE 0 END), 0) as today_count, + COALESCE(SUM(CASE WHEN created_at >= ? AND created_at <= ? THEN 1 ELSE 0 END), 0) as yesterday_count + `, todayStart, todayEnd, yesterdayStart, yesterdayEnd). + Where("is_del = ?", 0). + Scan(&receivingStat).Error + }() + + // 3. 出库单统计(并发) + wg.Add(1) + go func() { + defer wg.Done() + outboundErr = databaseConn.Model(&models.OutboundOrder{}). + Select(` + COALESCE(SUM(CASE WHEN created_at >= ? AND created_at <= ? THEN 1 ELSE 0 END), 0) as today_count, + COALESCE(SUM(CASE WHEN created_at >= ? AND created_at <= ? THEN 1 ELSE 0 END), 0) as yesterday_count + `, todayStart, todayEnd, yesterdayStart, yesterdayEnd). + Where("is_del = ?", 0). + Scan(&outboundStat).Error + }() + + // 4. 用户个人统计(并发) + wg.Add(1) + go func() { + defer wg.Done() + singleDayYYYYMMDD := fmt.Sprintf("%04d%02d%02d", endTime.Year(), endTime.Month(), endTime.Day()) + var singleDayStatDate int64 + fmt.Sscanf(singleDayYYYYMMDD, "%d", &singleDayStatDate) + userErr = databaseConn.Model(&models.Statist{}). + Select("create_by, SUM(receiving_num) as total_receiving, SUM(outbound_num) as total_outbound"). + Where("stat_date = ? AND is_del = ?", singleDayStatDate, 0). + Group("create_by"). + Scan(&userStatGroups).Error + }() + + // 5. 商品总数(并发) + wg.Add(1) + go func() { + defer wg.Done() + productErr = databaseConn.Model(&models.Product{}).Where("is_del = ?", 0).Count(&productTotal).Error + }() + + // 6. 库存总量(并发) + wg.Add(1) + go func() { + defer wg.Done() + inventoryErr = databaseConn.Model(&models.Inventory{}).Where("is_del = ?", 0).Select("COALESCE(SUM(quantity), 0)").Row().Scan(&inventoryTotal) + }() + + // 等待所有 goroutine 完成 + wg.Wait() + + // 检查关键查询错误(订单/入库/出库为核心统计,出错则返回) + if statErr != nil { + return nil, statErr + } + if receivingErr != nil { + return nil, receivingErr + } + if outboundErr != nil { + return nil, outboundErr + } + // 用户统计、商品、库存出错不阻塞,仅打印并置零 + if userErr != nil { + userStatGroups = nil + } + if productErr != nil { + productTotal = 0 + } + if inventoryErr != nil { + inventoryTotal = 0 + } + + // 根据用户统计结果批量查询员工信息 + var userStats []systemRes.UserStatItem + if len(userStatGroups) > 0 { + userIDs := make([]int64, len(userStatGroups)) + for i, group := range userStatGroups { + userIDs[i] = group.CreateBy + } + var employees []models.Employee + if err := database.DB.Where("id IN ?", userIDs).Find(&employees).Error; err == nil { + empMap := make(map[int64]models.Employee, len(employees)) + for _, emp := range employees { + empMap[emp.ID] = emp + } + for _, group := range userStatGroups { + if emp, ok := empMap[group.CreateBy]; ok { + userStats = append(userStats, systemRes.UserStatItem{ + UserID: emp.ID, + UserName: emp.Username, + ReceivingCount: group.TotalReceiving, + OutboundCount: group.TotalOutbound, + }) + } + } } } - var productTotal int64 - databaseConn.Model(&models.Product{}).Where("is_del = ?", 0).Count(&productTotal) - - var inventoryTotal int64 - databaseConn.Model(&models.Inventory{}).Where("is_del = ?", 0).Select("COALESCE(SUM(quantity), 0)").Row().Scan(&inventoryTotal) - - now := time.Now() - yesterdayStart := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()).Unix() - yesterdayEnd := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()).Unix() - - var todayInbound, todayOutbound, yesterdayInbound, yesterdayOutbound int64 - databaseConn.Model(&models.Statist{}). - Where("stat_date >= ? AND stat_date <= ? AND is_del = ?", startDate, endDate, 0). - Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). - Row().Scan(&todayInbound, &todayOutbound) - - databaseConn.Model(&models.Statist{}). - Where("stat_date >= ? AND stat_date <= ? AND is_del = ?", yesterdayStart, yesterdayEnd, 0). - Select("COALESCE(SUM(receiving_num), 0), COALESCE(SUM(outbound_num), 0)"). - Row().Scan(&yesterdayInbound, &yesterdayOutbound) - return &systemRes.DashboardStatistResponse{ - TotalReceivingCount: totalReceivingCount, - TotalOutboundCount: totalOutboundCount, - TotalSaleCount: totalSaleCount, - UserStats: userStats, - ProductTotal: productTotal, - InventoryTotal: inventoryTotal, - TodayInbound: todayInbound, - TodayOutbound: todayOutbound, - YesterdayInbound: yesterdayInbound, - YesterdayOutbound: yesterdayOutbound, + TotalOrderCount: salesStat.TodayCount, + TotalReceivingCount: receivingStat.TodayCount, + TotalOutboundCount: outboundStat.TodayCount, + TotalSaleCount: salesStat.TodayCount, + YesterdayOrderCount: salesStat.YesterdayCount, + YesterdayReceivingCount: receivingStat.YesterdayCount, + YesterdayOutboundCount: outboundStat.YesterdayCount, + YesterdaySaleCount: salesStat.YesterdayCount, + UserStats: userStats, + ProductTotal: productTotal, + InventoryTotal: inventoryTotal, }, nil } // GetWarehouseStatist 获取仓库统计数据 func (s *StatistService) GetWarehouseStatist(req systemReq.WarehouseStatistRequest, db ...*gorm.DB) (*systemRes.WarehouseStatistResponse, error) { databaseConn := database.OptionalDB(db...) - now := time.Now() - startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() - endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Unix() - yesterdayStart := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()).Unix() - yesterdayEnd := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()).Unix() + yesterdayTime := now.AddDate(0, 0, -1) - if req.StartDate == 0 { - req.StartDate = startOfDay - } - if req.EndDate == 0 { - req.EndDate = endOfDay + // statist.stat_date 存储 YYYYMMDD 格式 int64,需转换为 YYYYMMDD 再比较 + todayYYYYMMDD := fmt.Sprintf("%04d%02d%02d", now.Year(), now.Month(), now.Day()) + yesterdayYYYYMMDD := fmt.Sprintf("%04d%02d%02d", yesterdayTime.Year(), yesterdayTime.Month(), yesterdayTime.Day()) + var todayStatDate, yesterdayStatDate int64 + fmt.Sscanf(todayYYYYMMDD, "%d", &todayStatDate) + fmt.Sscanf(yesterdayYYYYMMDD, "%d", &yesterdayStatDate) + + // 若用户传了日期范围,从 req.EndDate 反算 YYYYMMDD;否则默认今天 + var reqEndStatDate int64 + if req.EndDate != 0 { + reqEndTime := time.Unix(req.EndDate, 0) + reqEndYYYYMMDD := fmt.Sprintf("%04d%02d%02d", reqEndTime.Year(), reqEndTime.Month(), reqEndTime.Day()) + fmt.Sscanf(reqEndYYYYMMDD, "%d", &reqEndStatDate) + } else { + reqEndStatDate = todayStatDate } var productTotal int64 @@ -339,14 +385,24 @@ func (s *StatistService) GetWarehouseStatist(req systemReq.WarehouseStatistReque YesterdayOutbound int64 `gorm:"column:yesterday_outbound"` } + // 计算昨天对应的 YYYYMMDD(基于 reqEndDate 或今天) + var yesterdayReqStatDate int64 + if req.EndDate != 0 { + yesterdayReqTime := time.Unix(req.EndDate, 0).AddDate(0, 0, -1) + yesterdayReqYYYYMMDD := fmt.Sprintf("%04d%02d%02d", yesterdayReqTime.Year(), yesterdayReqTime.Month(), yesterdayReqTime.Day()) + fmt.Sscanf(yesterdayReqYYYYMMDD, "%d", &yesterdayReqStatDate) + } else { + yesterdayReqStatDate = yesterdayStatDate + } + var dailyStat DailyStat databaseConn.Model(&models.Statist{}). Select(` - COALESCE(SUM(CASE WHEN stat_date >= ? AND stat_date <= ? THEN receiving_num ELSE 0 END), 0) as today_inbound, - COALESCE(SUM(CASE WHEN stat_date >= ? AND stat_date <= ? THEN outbound_num ELSE 0 END), 0) as today_outbound, - COALESCE(SUM(CASE WHEN stat_date >= ? AND stat_date <= ? THEN receiving_num ELSE 0 END), 0) as yesterday_inbound, - COALESCE(SUM(CASE WHEN stat_date >= ? AND stat_date <= ? THEN outbound_num ELSE 0 END), 0) as yesterday_outbound - `, req.StartDate, req.EndDate, req.StartDate, req.EndDate, yesterdayStart, yesterdayEnd, yesterdayStart, yesterdayEnd). + COALESCE(SUM(CASE WHEN stat_date = ? THEN receiving_num ELSE 0 END), 0) as today_inbound, + COALESCE(SUM(CASE WHEN stat_date = ? THEN outbound_num ELSE 0 END), 0) as today_outbound, + COALESCE(SUM(CASE WHEN stat_date = ? THEN receiving_num ELSE 0 END), 0) as yesterday_inbound, + COALESCE(SUM(CASE WHEN stat_date = ? THEN outbound_num ELSE 0 END), 0) as yesterday_outbound + `, reqEndStatDate, reqEndStatDate, yesterdayReqStatDate, yesterdayReqStatDate). Where("is_del = ?", 0). Scan(&dailyStat) diff --git a/service/store_info.go b/service/store_info.go new file mode 100644 index 0000000..90a3572 --- /dev/null +++ b/service/store_info.go @@ -0,0 +1,312 @@ +package service + +import ( + "log" + "psi/database" + systemReq "psi/models/request" + systemRes "psi/models/response" + "strings" + "sync" + "time" + + "gorm.io/gorm" +) + +type StoreInfoService struct{} + +// StoreInfo 获取店铺统计数据 +// +// 数据口径与 statist.getDashboardStatRealtime 完全对齐。 +// +// 名称来源:sales/outbound/shipping 直接用 sales_order.sales_person(业务表存的实际店铺名); +// receiving 用 shop 表名称(通过 car_shop.shop_id 雪花 ID 匹配 shop.id)。 +// 店铺类型:用名称去 shop 表匹配 shop_alias_name 获取,匹配不上显示"未知"。 +// +// 时间处理方式与 statist 完全一致,today 范围为 00:00:00 ~ 23:59:59。 +func (s *StoreInfoService) StoreInfo(req systemReq.StoreInfoRequest, db ...*gorm.DB) ([]systemRes.StoreInfoResponse, error) { + databaseConn := database.OptionalDB(db...) + + startTime, endTime := s.calcTimeRange(req.TimeRange) + + log.Printf("[store-info] timeRange=%s startTime=%d endTime=%d", req.TimeRange, startTime, endTime) + + // Step 1: 获取 shop 表 → name→type 映射(仅用于补全 shop_type) + type shopRow struct { + ShopAliasName string `gorm:"column:shop_alias_name"` + ShopType int8 `gorm:"column:shop_type"` + ID int64 `gorm:"column:id"` + } + var shopRows []shopRow + if err := databaseConn.Raw(`SELECT id, shop_alias_name, shop_type FROM shop WHERE del_flag = 0`).Scan(&shopRows).Error; err != nil { + return nil, err + } + nameToType := make(map[string]int8, len(shopRows)) + idToName := make(map[int64]string, len(shopRows)) + shopNames := make([]string, 0, len(shopRows)) + for _, s := range shopRows { + nameToType[s.ShopAliasName] = s.ShopType + idToName[s.ID] = s.ShopAliasName + shopNames = append(shopNames, s.ShopAliasName) + } + log.Printf("[store-info] shop names in DB: %v", shopNames) + + // 第三方订单,shop 表中无记录,手动补类型 + nameToType["kw8750193"] = 2 // 孔夫子 + nameToType["图书电商大全"] = 5 // 闲鱼 + + // Step 2: 获取 sales_person_id → sales_person 名称映射(用于 outbound/shipping 的 ID 转名称) + type idNameRow struct { + ID int64 `gorm:"column:sales_person_id"` + Name string `gorm:"column:sales_person"` + } + var idNames []idNameRow + if err := databaseConn.Raw(` + SELECT sales_person_id, MAX(sales_person) AS sales_person + FROM sales_order WHERE is_del = 0 AND sales_person_id > 0 + GROUP BY sales_person_id + `).Scan(&idNames).Error; err != nil { + return nil, err + } + salesIDToName := make(map[int64]string, len(idNames)) + for _, r := range idNames { + salesIDToName[r.ID] = strings.TrimSpace(r.Name) + } + + // Step 3: 4 goroutine 并发 + type storeStat struct { + ShopName string + SaleCount int64 + OutboundCount int64 + ReceivingCount int64 + ShippingCount int64 + } + statByName := make(map[string]*storeStat) + var mu sync.Mutex + var wg sync.WaitGroup + var queryErr error + var errOnce sync.Once + + // sales_order:按 sales_person 名称分组 + wg.Add(1) + go func() { + defer wg.Done() + type row struct { + Name string `gorm:"column:sales_person"` + Cnt int64 `gorm:"column:cnt"` + } + var rows []row + err := databaseConn.Raw(` + SELECT sales_person, COUNT(*) AS cnt + FROM sales_order + WHERE is_del = 0 AND created_at >= ? AND created_at <= ? AND sales_person != '' + GROUP BY sales_person + `, startTime, endTime).Scan(&rows).Error + if err != nil { + errOnce.Do(func() { queryErr = err }) + return + } + mu.Lock() + for _, r := range rows { + n := strings.TrimSpace(r.Name) + if n == "" { + continue + } + if statByName[n] == nil { + statByName[n] = &storeStat{ShopName: n} + } + statByName[n].SaleCount = r.Cnt + } + mu.Unlock() + }() + + // outbound_order:shop_id → salesIDToName → 名称 + wg.Add(1) + go func() { + defer wg.Done() + type row struct { + ShopID int64 `gorm:"column:shop_id"` + Cnt int64 `gorm:"column:cnt"` + } + var rows []row + err := databaseConn.Raw(` + SELECT shop_id, COUNT(*) AS cnt + FROM outbound_order + WHERE is_del = 0 AND created_at >= ? AND created_at <= ? AND shop_id > 0 + GROUP BY shop_id + `, startTime, endTime).Scan(&rows).Error + if err != nil { + errOnce.Do(func() { queryErr = err }) + return + } + mu.Lock() + for _, r := range rows { + name := salesIDToName[r.ShopID] + if name == "" { + continue + } + if statByName[name] == nil { + statByName[name] = &storeStat{ShopName: name} + } + statByName[name].OutboundCount = r.Cnt + } + mu.Unlock() + }() + + // receiving_order:car_shop.shop_id(雪花 ID)→ idToName → 名称 + wg.Add(1) + go func() { + defer wg.Done() + type row struct { + ShopID int64 `gorm:"column:shop_id"` + Cnt int64 `gorm:"column:cnt"` + } + var rows []row + err := databaseConn.Raw(` + SELECT COALESCE(cs.shop_id, 0) AS shop_id, COUNT(DISTINCT ro.id) AS cnt + FROM receiving_order ro + LEFT JOIN wave_task wt ON ro.wave_task_id = wt.id AND wt.is_del = 0 + LEFT JOIN car_shop cs ON wt.car_id = cs.car_id AND cs.is_del = 0 + WHERE ro.is_del = 0 AND ro.created_at >= ? AND ro.created_at <= ? + GROUP BY COALESCE(cs.shop_id, 0) + `, startTime, endTime).Scan(&rows).Error + if err != nil { + errOnce.Do(func() { queryErr = err }) + return + } + mu.Lock() + for _, r := range rows { + if r.ShopID <= 0 { + continue + } + name := idToName[r.ShopID] + if name == "" { + continue + } + if statByName[name] == nil { + statByName[name] = &storeStat{ShopName: name} + } + statByName[name].ReceivingCount = r.Cnt + } + mu.Unlock() + }() + + // shipping_order:shop_id → salesIDToName → 名称 + wg.Add(1) + go func() { + defer wg.Done() + type row struct { + ShopID int64 `gorm:"column:shop_id"` + Cnt int64 `gorm:"column:cnt"` + } + var rows []row + err := databaseConn.Raw(` + SELECT shop_id, COUNT(*) AS cnt + FROM shipping_order + WHERE is_del = 0 AND created_at >= ? AND created_at <= ? AND shop_id > 0 + GROUP BY shop_id + `, startTime, endTime).Scan(&rows).Error + if err != nil { + errOnce.Do(func() { queryErr = err }) + return + } + mu.Lock() + for _, r := range rows { + name := salesIDToName[r.ShopID] + if name == "" { + continue + } + if statByName[name] == nil { + statByName[name] = &storeStat{ShopName: name} + } + statByName[name].ShippingCount = r.Cnt + } + mu.Unlock() + }() + + wg.Wait() + if queryErr != nil { + return nil, queryErr + } + + // Step 4: 组装结果。名称用业务真实名称,类型从 shop 表匹配 + statNames := make([]string, 0, len(statByName)) + for n := range statByName { + statNames = append(statNames, n) + } + log.Printf("[store-info] stat names: %v", statNames) + shopTypeMap := map[int8]string{ + 1: "拼多多", + 2: "孔夫子", + 5: "闲鱼", + } + + result := make([]systemRes.StoreInfoResponse, 0, len(statByName)) + for name, st := range statByName { + if req.StoreName != "" && !containsIgnoreCase(name, req.StoreName) { + continue + } + storeType := "未知" + if t, ok := nameToType[name]; ok { + if s, exists := shopTypeMap[t]; exists { + storeType = s + } + } + result = append(result, systemRes.StoreInfoResponse{ + StoreName: name, + StoreType: storeType, + SaleCount: st.SaleCount, + OutboundCount: st.OutboundCount, + ReceivingCount: st.ReceivingCount, + OrderCount: st.SaleCount, + ShippingCount: st.ShippingCount, + }) + } + + return result, nil +} + +func containsIgnoreCase(s, substr string) bool { + return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) +} + +// calcTimeRange 根据 time_range 计算时间范围 +// 时间处理方式与 statist.getDashboardStatRealtime 完全一致:today 为 00:00:00 ~ 23:59:59 +func (s *StoreInfoService) calcTimeRange(timeRange string) (int64, int64) { + now := time.Now() + + switch timeRange { + case "today": + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() + end := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Unix() + return start, end + case "yesterday": + yesterday := now.AddDate(0, 0, -1) + start := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, now.Location()).Unix() + end := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 23, 59, 59, 0, now.Location()).Unix() + return start, end + case "7days": + start := time.Date(now.Year(), now.Month(), now.Day()-7, 0, 0, 0, 0, now.Location()).Unix() + end := now.Unix() + return start, end + case "30days": + start := time.Date(now.Year(), now.Month(), now.Day()-30, 0, 0, 0, 0, now.Location()).Unix() + end := now.Unix() + return start, end + case "90days": + start := time.Date(now.Year(), now.Month(), now.Day()-90, 0, 0, 0, 0, now.Location()).Unix() + end := now.Unix() + return start, end + case "180days": + start := time.Date(now.Year(), now.Month(), now.Day()-180, 0, 0, 0, 0, now.Location()).Unix() + end := now.Unix() + return start, end + case "365days": + start := time.Date(now.Year(), now.Month(), now.Day()-365, 0, 0, 0, 0, now.Location()).Unix() + end := now.Unix() + return start, end + default: + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() + end := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Unix() + return start, end + } +}