This commit is contained in:
Administrator 2026-06-27 17:14:23 +08:00
parent bb6d91cbd0
commit 4317136295
21 changed files with 830 additions and 247 deletions

33
controllers/store_info.go Normal file
View File

@ -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)
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 获取出库单详情请求

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"` // 店铺名称(模糊搜索)
}

View File

@ -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

View File

@ -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 个人统计项

View File

@ -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
}

View File

@ -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 {

View File

@ -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:备注"`
}

View File

@ -125,23 +125,34 @@ func (s *StatistTaskService) generateDashboardDailyStat(tx *gorm.DB, statDate in
}
}
// statist.stat_date 存储 YYYYMMDD 格式 int64如 20260626非 Unix 时间戳
// statDate 已经是 YYYYMMDD 格式,直接用于查询
// 昨天的 YYYYMMDD = statDate前天需从 startDateUnix时间戳反算
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)

View File

@ -14,7 +14,7 @@ import (
systemRes "psi/models/response"
"strconv"
"strings"
"time"
)
type BookService struct{}

View File

@ -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],
))
}

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

312
service/store_info.go Normal file
View File

@ -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_ordershop_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_ordercar_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_ordershop_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
}
}