316 lines
9.1 KiB
Go
316 lines
9.1 KiB
Go
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
|
||
SaleAmount 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"`
|
||
Amt int64 `gorm:"column:amt"`
|
||
}
|
||
var rows []row
|
||
err := databaseConn.Raw(`
|
||
SELECT sales_person, COUNT(*) AS cnt, COALESCE(SUM(total_amount), 0) AS amt
|
||
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
|
||
statByName[n].SaleAmount = r.Amt
|
||
}
|
||
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,
|
||
SaleAmount: st.SaleAmount,
|
||
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
|
||
}
|
||
}
|