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