|  | @@ -6,7 +6,6 @@ import (
 | 
	
		
			
				|  |  |  	"fmt"
 | 
	
		
			
				|  |  |  	"kpt-pasture/model"
 | 
	
		
			
				|  |  |  	"kpt-pasture/util"
 | 
	
		
			
				|  |  | -	"log"
 | 
	
		
			
				|  |  |  	"net/http"
 | 
	
		
			
				|  |  |  	"sort"
 | 
	
		
			
				|  |  |  	"strings"
 | 
	
	
		
			
				|  | @@ -248,7 +247,7 @@ func (s *StoreEntry) BehaviorCurve(ctx context.Context, req *pasturePb.CowBehavi
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	data := model.NeckActiveHabitSlice(neckActiveHabitList).ToPB(req.CurveName)
 | 
	
		
			
				|  |  | -	q1, q3 := s.CowIQR(ctx, cowInfo, req.CurveName, dayRange, data.DateTimeList)
 | 
	
		
			
				|  |  | +	q1, q3 := s.CowIQR(cowInfo, req.CurveName, dayRange, data.DateTimeList)
 | 
	
		
			
				|  |  |  	data.IQR3 = q3
 | 
	
		
			
				|  |  |  	data.IQR1 = q1
 | 
	
		
			
				|  |  |  	eventMapList := s.EventTypeMap()
 | 
	
	
		
			
				|  | @@ -325,7 +324,7 @@ func (s *StoreEntry) BehaviorCurve(ctx context.Context, req *pasturePb.CowBehavi
 | 
	
		
			
				|  |  |  	}, nil
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -func (s *StoreEntry) CowIQR(ctx context.Context, cowInfo *model.Cow, curveName string, dayRange []string, dateTimeList []string) ([]int32, []int32) {
 | 
	
		
			
				|  |  | +func (s *StoreEntry) CowIQR(cowInfo *model.Cow, curveName string, dayRange []string, dateTimeList []string) ([]int32, []int32) {
 | 
	
		
			
				|  |  |  	q1, q3 := make([]int32, 0), make([]int32, 0)
 | 
	
		
			
				|  |  |  	if curveName == "" || curveName == "active" || curveName == "behavior" || len(dateTimeList) <= 0 {
 | 
	
		
			
				|  |  |  		return q1, q3
 | 
	
	
		
			
				|  | @@ -346,48 +345,47 @@ func (s *StoreEntry) CowIQR(ctx context.Context, cowInfo *model.Cow, curveName s
 | 
	
		
			
				|  |  |  		cowIds = append(cowIds, cow.Id)
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	/*// 行为曲线数据
 | 
	
		
			
				|  |  | -	neckActiveHabitList := make([]*model.NeckActiveHabit, 0)
 | 
	
		
			
				|  |  | -	if err := s.DB.Table(new(model.NeckActiveHabit).TableName()).
 | 
	
		
			
				|  |  | -		Select("rumina,intake,inactive,gasp,high,active,active_time").
 | 
	
		
			
				|  |  | -		Where("pasture_id = ?", cowInfo.PastureId).
 | 
	
		
			
				|  |  | -		Where("is_show = ?", pasturePb.IsShow_Ok).
 | 
	
		
			
				|  |  | -		Where("cow_id IN (?)", cowIds).
 | 
	
		
			
				|  |  | -		Where("heat_date BETWEEN ? AND ?", dayRange[0], dayRange[len(dayRange)-1]).
 | 
	
		
			
				|  |  | -		Find(&neckActiveHabitList).Error; err != nil {
 | 
	
		
			
				|  |  | -		return q1, q3
 | 
	
		
			
				|  |  | -	}*/
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  	neckActiveHabitList, err := s.GetNeckActiveHabitsConcurrent(cowInfo, cowIds, dayRange)
 | 
	
		
			
				|  |  |  	if err != nil {
 | 
	
		
			
				|  |  |  		return q1, q3
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +	neckActiveHabitMap := make(map[string][]*model.NeckActiveHabit)
 | 
	
		
			
				|  |  | +	for _, habit := range neckActiveHabitList {
 | 
	
		
			
				|  |  | +		activeTime := habit.ActiveTime[:13]
 | 
	
		
			
				|  |  | +		neckActiveHabitMap[activeTime] = append(neckActiveHabitMap[activeTime], habit)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  	penOriginal := make(map[string][]int32)
 | 
	
		
			
				|  |  |  	for _, dt := range dateTimeList {
 | 
	
		
			
				|  |  | -		var info bool
 | 
	
		
			
				|  |  | -		for _, habit := range neckActiveHabitList {
 | 
	
		
			
				|  |  | -			if !strings.Contains(habit.ActiveTime, dt) {
 | 
	
		
			
				|  |  | -				continue
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -			info = true
 | 
	
		
			
				|  |  | +		habits, ok := neckActiveHabitMap[dt]
 | 
	
		
			
				|  |  | +		if !ok {
 | 
	
		
			
				|  |  | +			penOriginal[dt] = append(penOriginal[dt], int32(0))
 | 
	
		
			
				|  |  | +			continue
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -			switch curveName {
 | 
	
		
			
				|  |  | -			case "rumina":
 | 
	
		
			
				|  |  | +		switch curveName {
 | 
	
		
			
				|  |  | +		case "rumina":
 | 
	
		
			
				|  |  | +			for _, habit := range habits {
 | 
	
		
			
				|  |  |  				penOriginal[dt] = append(penOriginal[dt], habit.Rumina)
 | 
	
		
			
				|  |  | -			case "intake":
 | 
	
		
			
				|  |  | -				penOriginal[dt] = append(penOriginal[dt], habit.Intake)
 | 
	
		
			
				|  |  | -			case "inactive":
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		case "intake":
 | 
	
		
			
				|  |  | +			for _, habit := range habits {
 | 
	
		
			
				|  |  | +				penOriginal[dt] = append(penOriginal[dt], habit.Inactive)
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		case "inactive":
 | 
	
		
			
				|  |  | +			for _, habit := range habits {
 | 
	
		
			
				|  |  |  				penOriginal[dt] = append(penOriginal[dt], habit.Inactive)
 | 
	
		
			
				|  |  | -			case "chew":
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		case "chew":
 | 
	
		
			
				|  |  | +			for _, habit := range habits {
 | 
	
		
			
				|  |  |  				penOriginal[dt] = append(penOriginal[dt], habit.Rumina+habit.Intake)
 | 
	
		
			
				|  |  | -			case "immobility":
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		case "immobility":
 | 
	
		
			
				|  |  | +			for _, habit := range habits {
 | 
	
		
			
				|  |  |  				penOriginal[dt] = append(penOriginal[dt], 120-habit.Active)
 | 
	
		
			
				|  |  |  			}
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  | -		if !info {
 | 
	
		
			
				|  |  | -			penOriginal[dt] = append(penOriginal[dt], int32(0))
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	if len(penOriginal) > 0 {
 | 
	
	
		
			
				|  | @@ -416,83 +414,40 @@ func (s *StoreEntry) CowIQR(ctx context.Context, cowInfo *model.Cow, curveName s
 | 
	
		
			
				|  |  |  	return q1, q3
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -func (s *StoreEntry) GetNeckActiveHabitsConcurrent2(cowInfo *model.Cow, cowIds []int64, dayRange []string) ([]*model.NeckActiveHabit, error) {
 | 
	
		
			
				|  |  | -	var neckActiveHabitList []*model.NeckActiveHabit
 | 
	
		
			
				|  |  | -	var batchSize = 100
 | 
	
		
			
				|  |  | -	var wg sync.WaitGroup
 | 
	
		
			
				|  |  | -	var mu sync.Mutex
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -	// 使用工作池控制并发数
 | 
	
		
			
				|  |  | -	workerCount := 5 // 并发数,可根据实际情况调整
 | 
	
		
			
				|  |  | -	sem := make(chan struct{}, workerCount)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -	for i := 0; i < len(cowIds); i += batchSize {
 | 
	
		
			
				|  |  | -		end := i + batchSize
 | 
	
		
			
				|  |  | -		if end > len(cowIds) {
 | 
	
		
			
				|  |  | -			end = len(cowIds)
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -		batch := cowIds[i:end]
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -		wg.Add(1)
 | 
	
		
			
				|  |  | -		go func(batch []int64) {
 | 
	
		
			
				|  |  | -			defer wg.Done()
 | 
	
		
			
				|  |  | -			sem <- struct{}{} // 获取令牌
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -			var batchResults []*model.NeckActiveHabit
 | 
	
		
			
				|  |  | -			err := s.DB.Table(new(model.NeckActiveHabit).TableName()).
 | 
	
		
			
				|  |  | -				Select("rumina,intake,inactive,gasp,high,active,active_time").
 | 
	
		
			
				|  |  | -				Where("pasture_id = ?", cowInfo.PastureId).
 | 
	
		
			
				|  |  | -				Where("is_show = ?", pasturePb.IsShow_Ok).
 | 
	
		
			
				|  |  | -				Where("cow_id IN (?)", batch).
 | 
	
		
			
				|  |  | -				Where("heat_date BETWEEN ? AND ?", dayRange[0], dayRange[len(dayRange)-1]).
 | 
	
		
			
				|  |  | -				Find(&batchResults).Error
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -			<-sem // 释放令牌
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -			if err != nil {
 | 
	
		
			
				|  |  | -				// 错误处理逻辑,可根据需要调整
 | 
	
		
			
				|  |  | -				log.Printf("batch query error: %v", err)
 | 
	
		
			
				|  |  | -				return
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -			mu.Lock()
 | 
	
		
			
				|  |  | -			neckActiveHabitList = append(neckActiveHabitList, batchResults...)
 | 
	
		
			
				|  |  | -			mu.Unlock()
 | 
	
		
			
				|  |  | -		}(batch)
 | 
	
		
			
				|  |  | -	}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -	wg.Wait()
 | 
	
		
			
				|  |  | -	return neckActiveHabitList, nil
 | 
	
		
			
				|  |  | -}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  func (s *StoreEntry) GetNeckActiveHabitsConcurrent(cowInfo *model.Cow, cowIds []int64, dayRange []string) ([]*model.NeckActiveHabit, error) {
 | 
	
		
			
				|  |  |  	const (
 | 
	
		
			
				|  |  |  		batchSize   = 100
 | 
	
		
			
				|  |  | -		workerCount = 5 // 并发数,可根据实际情况调整
 | 
	
		
			
				|  |  | +		workerCount = 5
 | 
	
		
			
				|  |  |  	)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	// 结果收集通道
 | 
	
		
			
				|  |  | -	resultsChan := make(chan []*model.NeckActiveHabit, len(cowIds)/batchSize+1)
 | 
	
		
			
				|  |  | -	errChan := make(chan error, 1)
 | 
	
		
			
				|  |  | +	resultsChan := make(chan []*model.NeckActiveHabit)
 | 
	
		
			
				|  |  | +	errChan := make(chan error, 1) // 缓冲 1,避免 goroutine 阻塞
 | 
	
		
			
				|  |  |  	doneChan := make(chan struct{})
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	// 启动结果收集goroutine
 | 
	
		
			
				|  |  | -	var neckActiveHabitList []*model.NeckActiveHabit
 | 
	
		
			
				|  |  | +	var (
 | 
	
		
			
				|  |  | +		neckActiveHabitList []*model.NeckActiveHabit
 | 
	
		
			
				|  |  | +		mu                  sync.Mutex
 | 
	
		
			
				|  |  | +	)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	// 结果收集 goroutine
 | 
	
		
			
				|  |  |  	go func() {
 | 
	
		
			
				|  |  |  		defer close(doneChan)
 | 
	
		
			
				|  |  |  		for batchResults := range resultsChan {
 | 
	
		
			
				|  |  | +			mu.Lock()
 | 
	
		
			
				|  |  |  			neckActiveHabitList = append(neckActiveHabitList, batchResults...)
 | 
	
		
			
				|  |  | +			mu.Unlock()
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	}()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	// 工作池
 | 
	
		
			
				|  |  |  	sem := make(chan struct{}, workerCount)
 | 
	
		
			
				|  |  | +	var wg sync.WaitGroup
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	// 分发任务
 | 
	
		
			
				|  |  |  	go func() {
 | 
	
		
			
				|  |  | -		defer close(resultsChan)
 | 
	
		
			
				|  |  | -		defer close(errChan)
 | 
	
		
			
				|  |  | +		defer func() {
 | 
	
		
			
				|  |  | +			wg.Wait()          // 等待所有 goroutine 完成
 | 
	
		
			
				|  |  | +			close(resultsChan) // 安全关闭
 | 
	
		
			
				|  |  | +		}()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  		for i := 0; i < len(cowIds); i += batchSize {
 | 
	
		
			
				|  |  |  			end := i + batchSize
 | 
	
	
		
			
				|  | @@ -501,10 +456,14 @@ func (s *StoreEntry) GetNeckActiveHabitsConcurrent(cowInfo *model.Cow, cowIds []
 | 
	
		
			
				|  |  |  			}
 | 
	
		
			
				|  |  |  			batch := cowIds[i:end]
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -			sem <- struct{}{} // 获取令牌
 | 
	
		
			
				|  |  | +			wg.Add(1)
 | 
	
		
			
				|  |  | +			sem <- struct{}{} // 获取令牌(可能阻塞,但不会死锁)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  			go func(batch []int64) {
 | 
	
		
			
				|  |  | -				defer func() { <-sem }() // 释放令牌
 | 
	
		
			
				|  |  | +				defer func() {
 | 
	
		
			
				|  |  | +					<-sem // 释放令牌
 | 
	
		
			
				|  |  | +					wg.Done()
 | 
	
		
			
				|  |  | +				}()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  				var batchResults []*model.NeckActiveHabit
 | 
	
		
			
				|  |  |  				if err := s.DB.Table(new(model.NeckActiveHabit).TableName()).
 | 
	
	
		
			
				|  | @@ -516,19 +475,16 @@ func (s *StoreEntry) GetNeckActiveHabitsConcurrent(cowInfo *model.Cow, cowIds []
 | 
	
		
			
				|  |  |  					Find(&batchResults).Error; err != nil {
 | 
	
		
			
				|  |  |  					select {
 | 
	
		
			
				|  |  |  					case errChan <- fmt.Errorf("batch query error: %v", err):
 | 
	
		
			
				|  |  | -					default: // 避免阻塞,如果已经有错误了
 | 
	
		
			
				|  |  | +					default:
 | 
	
		
			
				|  |  |  					}
 | 
	
		
			
				|  |  |  					return
 | 
	
		
			
				|  |  |  				}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -				resultsChan <- batchResults
 | 
	
		
			
				|  |  | +				if len(batchResults) > 0 {
 | 
	
		
			
				|  |  | +					resultsChan <- batchResults
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  |  			}(batch)
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -		// 等待所有worker完成
 | 
	
		
			
				|  |  | -		for i := 0; i < cap(sem); i++ {
 | 
	
		
			
				|  |  | -			sem <- struct{}{}
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  |  	}()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  	// 等待结果
 | 
	
	
		
			
				|  | @@ -539,6 +495,7 @@ func (s *StoreEntry) GetNeckActiveHabitsConcurrent(cowInfo *model.Cow, cowIds []
 | 
	
		
			
				|  |  |  		return neckActiveHabitList, nil
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  func (s *StoreEntry) CowGrowthCurve(ctx context.Context, req *pasturePb.CowGrowthCurveRequest) (*pasturePb.CowGrowthCurveResponse, error) {
 | 
	
		
			
				|  |  |  	userModel, err := s.GetUserModel(ctx)
 | 
	
		
			
				|  |  |  	if err != nil {
 |