Use admin price-refresh logic for pricelist recalculation
This commit is contained in:
@@ -110,6 +110,10 @@ func (r *ComponentRepository) Update(component *models.LotMetadata) error {
|
||||
return r.db.Save(component).Error
|
||||
}
|
||||
|
||||
func (r *ComponentRepository) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
func (r *ComponentRepository) Create(component *models.LotMetadata) error {
|
||||
return r.db.Create(component).Error
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
package pricing
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/config"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
componentRepo *repository.ComponentRepository
|
||||
priceRepo *repository.PriceRepository
|
||||
config config.PricingConfig
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type RecalculateProgress struct {
|
||||
Current int
|
||||
Total int
|
||||
LotName string
|
||||
Updated int
|
||||
Errors int
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@@ -19,10 +30,16 @@ func NewService(
|
||||
priceRepo *repository.PriceRepository,
|
||||
cfg config.PricingConfig,
|
||||
) *Service {
|
||||
var db *gorm.DB
|
||||
if componentRepo != nil {
|
||||
db = componentRepo.DB()
|
||||
}
|
||||
|
||||
return &Service{
|
||||
componentRepo: componentRepo,
|
||||
priceRepo: priceRepo,
|
||||
config: cfg,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,27 +196,183 @@ type PriceStats struct {
|
||||
|
||||
// RecalculateAllPrices recalculates prices for all components
|
||||
func (s *Service) RecalculateAllPrices() (updated int, errors int) {
|
||||
// Get all components
|
||||
filter := repository.ComponentFilter{}
|
||||
offset := 0
|
||||
limit := 100
|
||||
|
||||
for {
|
||||
components, _, err := s.componentRepo.List(filter, offset, limit)
|
||||
if err != nil || len(components) == 0 {
|
||||
break
|
||||
return s.RecalculateAllPricesWithProgress(nil)
|
||||
}
|
||||
|
||||
// RecalculateAllPricesWithProgress recalculates prices and reports progress.
|
||||
func (s *Service) RecalculateAllPricesWithProgress(onProgress func(RecalculateProgress)) (updated int, errors int) {
|
||||
if s.db == nil {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Logic mirrors "Обновить цены" in admin pricing.
|
||||
var components []models.LotMetadata
|
||||
if err := s.db.Find(&components).Error; err != nil {
|
||||
return 0, len(components)
|
||||
}
|
||||
total := len(components)
|
||||
|
||||
var allLotNames []string
|
||||
_ = s.db.Model(&models.LotMetadata{}).Pluck("lot_name", &allLotNames).Error
|
||||
|
||||
type lotDate struct {
|
||||
Lot string
|
||||
Date time.Time
|
||||
}
|
||||
var latestDates []lotDate
|
||||
_ = s.db.Raw(`SELECT lot, MAX(date) as date FROM lot_log GROUP BY lot`).Scan(&latestDates).Error
|
||||
lotLatestDate := make(map[string]time.Time, len(latestDates))
|
||||
for _, ld := range latestDates {
|
||||
lotLatestDate[ld.Lot] = ld.Date
|
||||
}
|
||||
|
||||
var skipped, manual, unchanged int
|
||||
now := time.Now()
|
||||
current := 0
|
||||
|
||||
for _, comp := range components {
|
||||
if err := s.UpdateComponentPrice(comp.LotName); err != nil {
|
||||
current++
|
||||
reportProgress := func() {
|
||||
if onProgress != nil && (current%10 == 0 || current == total) {
|
||||
onProgress(RecalculateProgress{
|
||||
Current: current,
|
||||
Total: total,
|
||||
LotName: comp.LotName,
|
||||
Updated: updated,
|
||||
Errors: errors,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if comp.ManualPrice != nil && *comp.ManualPrice > 0 {
|
||||
manual++
|
||||
reportProgress()
|
||||
continue
|
||||
}
|
||||
|
||||
method := comp.PriceMethod
|
||||
if method == "" {
|
||||
method = models.PriceMethodMedian
|
||||
}
|
||||
|
||||
var sourceLots []string
|
||||
if comp.MetaPrices != "" {
|
||||
sourceLots = expandMetaPricesWithCache(comp.MetaPrices, comp.LotName, allLotNames)
|
||||
} else {
|
||||
sourceLots = []string{comp.LotName}
|
||||
}
|
||||
if len(sourceLots) == 0 {
|
||||
skipped++
|
||||
reportProgress()
|
||||
continue
|
||||
}
|
||||
|
||||
if comp.PriceUpdatedAt != nil {
|
||||
hasNewData := false
|
||||
for _, lot := range sourceLots {
|
||||
if latestDate, ok := lotLatestDate[lot]; ok && latestDate.After(*comp.PriceUpdatedAt) {
|
||||
hasNewData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasNewData {
|
||||
unchanged++
|
||||
reportProgress()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var prices []float64
|
||||
if comp.PricePeriodDays > 0 {
|
||||
_ = s.db.Raw(
|
||||
`SELECT price FROM lot_log WHERE lot IN ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY) ORDER BY price`,
|
||||
sourceLots, comp.PricePeriodDays,
|
||||
).Pluck("price", &prices).Error
|
||||
} else {
|
||||
_ = s.db.Raw(
|
||||
`SELECT price FROM lot_log WHERE lot IN ? ORDER BY price`,
|
||||
sourceLots,
|
||||
).Pluck("price", &prices).Error
|
||||
}
|
||||
|
||||
if len(prices) == 0 && comp.PricePeriodDays > 0 {
|
||||
_ = s.db.Raw(`SELECT price FROM lot_log WHERE lot IN ? ORDER BY price`, sourceLots).Pluck("price", &prices).Error
|
||||
}
|
||||
if len(prices) == 0 {
|
||||
skipped++
|
||||
reportProgress()
|
||||
continue
|
||||
}
|
||||
|
||||
var basePrice float64
|
||||
switch method {
|
||||
case models.PriceMethodAverage:
|
||||
basePrice = CalculateAverage(prices)
|
||||
default:
|
||||
basePrice = CalculateMedian(prices)
|
||||
}
|
||||
if basePrice <= 0 {
|
||||
skipped++
|
||||
reportProgress()
|
||||
continue
|
||||
}
|
||||
|
||||
finalPrice := basePrice
|
||||
if comp.PriceCoefficient != 0 {
|
||||
finalPrice = finalPrice * (1 + comp.PriceCoefficient/100)
|
||||
}
|
||||
|
||||
if err := s.db.Model(&models.LotMetadata{}).
|
||||
Where("lot_name = ?", comp.LotName).
|
||||
Updates(map[string]interface{}{
|
||||
"current_price": finalPrice,
|
||||
"price_updated_at": now,
|
||||
}).Error; err != nil {
|
||||
errors++
|
||||
} else {
|
||||
updated++
|
||||
}
|
||||
|
||||
reportProgress()
|
||||
}
|
||||
|
||||
offset += limit
|
||||
if onProgress != nil && total == 0 {
|
||||
onProgress(RecalculateProgress{
|
||||
Current: 0,
|
||||
Total: 0,
|
||||
LotName: "",
|
||||
Updated: updated,
|
||||
Errors: errors,
|
||||
})
|
||||
}
|
||||
|
||||
return updated, errors
|
||||
}
|
||||
|
||||
func expandMetaPricesWithCache(metaPrices, excludeLot string, allLotNames []string) []string {
|
||||
sources := strings.Split(metaPrices, ",")
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, source := range sources {
|
||||
source = strings.TrimSpace(source)
|
||||
if source == "" || source == excludeLot {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(source, "*") {
|
||||
prefix := strings.TrimSuffix(source, "*")
|
||||
for _, lot := range allLotNames {
|
||||
if strings.HasPrefix(lot, prefix) && lot != excludeLot && !seen[lot] {
|
||||
result = append(result, lot)
|
||||
seen[lot] = true
|
||||
}
|
||||
}
|
||||
} else if !seen[source] {
|
||||
result = append(result, source)
|
||||
seen[source] = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user