Исправления расчёта цен и добавление функционала своей цены
- Исправлен расчёт цен: теперь учитывается метод (медиана/среднее) и период для каждого компонента - Добавлены функции calculateMedian и calculateAverage - Исправлен PreviewPrice для корректного предпросмотра с учётом настроек - Сортировка по умолчанию изменена на популярность (desc) - Добавлен раздел "Своя цена" в конфигуратор: - Ввод целевой цены с пропорциональным пересчётом всех позиций - Отображение скидки в процентах - Таблица скорректированных цен - Экспорт CSV со скидкой Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -13,6 +14,31 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// calculateMedian returns the median of a sorted slice of prices
|
||||
func calculateMedian(prices []float64) float64 {
|
||||
if len(prices) == 0 {
|
||||
return 0
|
||||
}
|
||||
sort.Float64s(prices)
|
||||
n := len(prices)
|
||||
if n%2 == 0 {
|
||||
return (prices[n/2-1] + prices[n/2]) / 2
|
||||
}
|
||||
return prices[n/2]
|
||||
}
|
||||
|
||||
// calculateAverage returns the arithmetic mean of prices
|
||||
func calculateAverage(prices []float64) float64 {
|
||||
if len(prices) == 0 {
|
||||
return 0
|
||||
}
|
||||
var sum float64
|
||||
for _, p := range prices {
|
||||
sum += p
|
||||
}
|
||||
return sum / float64(len(prices))
|
||||
}
|
||||
|
||||
type PricingHandler struct {
|
||||
db *gorm.DB
|
||||
pricingService *pricing.Service
|
||||
@@ -205,30 +231,45 @@ func (h *PricingHandler) recalculateSinglePrice(lotName string) {
|
||||
}
|
||||
|
||||
periodDays := comp.PricePeriodDays
|
||||
var result struct {
|
||||
Price *float64
|
||||
method := comp.PriceMethod
|
||||
if method == "" {
|
||||
method = models.PriceMethodMedian
|
||||
}
|
||||
|
||||
// First try with configured period
|
||||
// Get prices based on period
|
||||
var prices []float64
|
||||
if periodDays > 0 {
|
||||
query := `SELECT AVG(price) as price FROM lot_log WHERE lot = ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY)`
|
||||
h.db.Raw(query, lotName, periodDays).Scan(&result)
|
||||
h.db.Raw(`SELECT price FROM lot_log WHERE lot = ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY) ORDER BY price`,
|
||||
lotName, periodDays).Pluck("price", &prices)
|
||||
|
||||
// If no prices found in period, fall back to all time but keep user's period setting
|
||||
if result.Price == nil || *result.Price <= 0 {
|
||||
query = `SELECT AVG(price) as price FROM lot_log WHERE lot = ?`
|
||||
h.db.Raw(query, lotName).Scan(&result)
|
||||
// If no prices in period, try all time
|
||||
if len(prices) == 0 {
|
||||
h.db.Raw(`SELECT price FROM lot_log WHERE lot = ? ORDER BY price`, lotName).Pluck("price", &prices)
|
||||
}
|
||||
} else {
|
||||
query := `SELECT AVG(price) as price FROM lot_log WHERE lot = ?`
|
||||
h.db.Raw(query, lotName).Scan(&result)
|
||||
h.db.Raw(`SELECT price FROM lot_log WHERE lot = ? ORDER BY price`, lotName).Pluck("price", &prices)
|
||||
}
|
||||
|
||||
if result.Price == nil || *result.Price <= 0 {
|
||||
if len(prices) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
finalPrice := *result.Price
|
||||
// Calculate price based on method
|
||||
var finalPrice float64
|
||||
switch method {
|
||||
case models.PriceMethodMedian:
|
||||
finalPrice = calculateMedian(prices)
|
||||
case models.PriceMethodAverage:
|
||||
finalPrice = calculateAverage(prices)
|
||||
default:
|
||||
finalPrice = calculateMedian(prices)
|
||||
}
|
||||
|
||||
if finalPrice <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply coefficient
|
||||
if comp.PriceCoefficient != 0 {
|
||||
finalPrice = finalPrice * (1 + comp.PriceCoefficient/100)
|
||||
}
|
||||
@@ -258,110 +299,96 @@ func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
||||
c.SSEvent("progress", gin.H{"current": 0, "total": total, "status": "starting"})
|
||||
c.Writer.Flush()
|
||||
|
||||
lotNames := make([]string, 0, len(components))
|
||||
for i := range components {
|
||||
lotNames = append(lotNames, components[i].LotName)
|
||||
}
|
||||
|
||||
// Batch query: Get average prices with date info for all lots
|
||||
// This query returns both all-time average and days since last quote
|
||||
type PriceResult struct {
|
||||
Lot string `gorm:"column:lot"`
|
||||
AvgPrice *float64 `gorm:"column:avg_price"`
|
||||
AvgPrice90 *float64 `gorm:"column:avg_price_90"`
|
||||
LastQuoteAge *int `gorm:"column:last_quote_age"`
|
||||
}
|
||||
var priceResults []PriceResult
|
||||
h.db.Raw(`
|
||||
SELECT
|
||||
lot,
|
||||
AVG(price) as avg_price,
|
||||
AVG(CASE WHEN date >= DATE_SUB(NOW(), INTERVAL 90 DAY) THEN price END) as avg_price_90,
|
||||
DATEDIFF(NOW(), MAX(date)) as last_quote_age
|
||||
FROM lot_log
|
||||
WHERE lot IN ?
|
||||
GROUP BY lot
|
||||
`, lotNames).Scan(&priceResults)
|
||||
|
||||
// Build price maps
|
||||
priceMap := make(map[string]PriceResult, len(priceResults))
|
||||
for _, p := range priceResults {
|
||||
priceMap[p.Lot] = p
|
||||
}
|
||||
|
||||
c.SSEvent("progress", gin.H{"current": 0, "total": total, "status": "processing", "updated": 0, "skipped": 0, "manual": 0, "errors": 0})
|
||||
c.Writer.Flush()
|
||||
|
||||
// Process components and prepare batch updates
|
||||
// Process components individually to respect their settings
|
||||
var updated, skipped, manual, errors int
|
||||
now := time.Now()
|
||||
batchSize := 200
|
||||
|
||||
for i := 0; i < len(components); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(components) {
|
||||
end = len(components)
|
||||
}
|
||||
batch := components[i:end]
|
||||
|
||||
// Start transaction for batch updates
|
||||
tx := h.db.Begin()
|
||||
|
||||
for _, comp := range batch {
|
||||
// If manual price is set, use it
|
||||
if comp.ManualPrice != nil && *comp.ManualPrice > 0 {
|
||||
err := tx.Model(&models.LotMetadata{}).
|
||||
Where("lot_name = ?", comp.LotName).
|
||||
Updates(map[string]interface{}{
|
||||
"current_price": *comp.ManualPrice,
|
||||
"price_updated_at": now,
|
||||
}).Error
|
||||
if err != nil {
|
||||
errors++
|
||||
} else {
|
||||
manual++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Get price data for this component
|
||||
priceData, hasPrices := priceMap[comp.LotName]
|
||||
if !hasPrices || priceData.AvgPrice == nil || *priceData.AvgPrice <= 0 {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine which price to use based on component settings
|
||||
var finalPrice float64
|
||||
periodDays := comp.PricePeriodDays
|
||||
|
||||
if periodDays <= 0 {
|
||||
// Use all time
|
||||
finalPrice = *priceData.AvgPrice
|
||||
for i, comp := range components {
|
||||
// If manual price is set, use it
|
||||
if comp.ManualPrice != nil && *comp.ManualPrice > 0 {
|
||||
err := h.db.Model(&models.LotMetadata{}).
|
||||
Where("lot_name = ?", comp.LotName).
|
||||
Updates(map[string]interface{}{
|
||||
"current_price": *comp.ManualPrice,
|
||||
"price_updated_at": now,
|
||||
}).Error
|
||||
if err != nil {
|
||||
errors++
|
||||
} else {
|
||||
// Try period price first (using 90-day as proxy)
|
||||
if priceData.AvgPrice90 != nil && *priceData.AvgPrice90 > 0 {
|
||||
finalPrice = *priceData.AvgPrice90
|
||||
} else {
|
||||
// Fall back to all time if no data in period, but keep user's period setting
|
||||
finalPrice = *priceData.AvgPrice
|
||||
}
|
||||
manual++
|
||||
}
|
||||
goto sendProgress
|
||||
}
|
||||
|
||||
// Calculate price based on component's individual settings
|
||||
{
|
||||
var basePrice *float64
|
||||
periodDays := comp.PricePeriodDays
|
||||
method := comp.PriceMethod
|
||||
if method == "" {
|
||||
method = models.PriceMethodMedian
|
||||
}
|
||||
|
||||
// Build query based on period
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if periodDays > 0 {
|
||||
query = `SELECT price FROM lot_log WHERE lot = ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY) ORDER BY price`
|
||||
args = []interface{}{comp.LotName, periodDays}
|
||||
} else {
|
||||
query = `SELECT price FROM lot_log WHERE lot = ? ORDER BY price`
|
||||
args = []interface{}{comp.LotName}
|
||||
}
|
||||
|
||||
var prices []float64
|
||||
h.db.Raw(query, args...).Pluck("price", &prices)
|
||||
|
||||
// If no prices in period, try all time
|
||||
if len(prices) == 0 && periodDays > 0 {
|
||||
h.db.Raw(`SELECT price FROM lot_log WHERE lot = ? ORDER BY price`, comp.LotName).Pluck("price", &prices)
|
||||
}
|
||||
|
||||
if len(prices) == 0 {
|
||||
skipped++
|
||||
goto sendProgress
|
||||
}
|
||||
|
||||
// Calculate price based on method
|
||||
switch method {
|
||||
case models.PriceMethodMedian:
|
||||
median := calculateMedian(prices)
|
||||
basePrice = &median
|
||||
case models.PriceMethodAverage:
|
||||
avg := calculateAverage(prices)
|
||||
basePrice = &avg
|
||||
default:
|
||||
median := calculateMedian(prices)
|
||||
basePrice = &median
|
||||
}
|
||||
|
||||
if basePrice == nil || *basePrice <= 0 {
|
||||
skipped++
|
||||
goto sendProgress
|
||||
}
|
||||
|
||||
finalPrice := *basePrice
|
||||
|
||||
// Apply coefficient
|
||||
if comp.PriceCoefficient != 0 {
|
||||
finalPrice = finalPrice * (1 + comp.PriceCoefficient/100)
|
||||
}
|
||||
|
||||
// Only update current_price and price_updated_at, preserve all other settings
|
||||
updates := map[string]interface{}{
|
||||
"current_price": finalPrice,
|
||||
"price_updated_at": now,
|
||||
}
|
||||
|
||||
err := tx.Model(&models.LotMetadata{}).
|
||||
// Update only price fields
|
||||
err := h.db.Model(&models.LotMetadata{}).
|
||||
Where("lot_name = ?", comp.LotName).
|
||||
Updates(updates).Error
|
||||
Updates(map[string]interface{}{
|
||||
"current_price": finalPrice,
|
||||
"price_updated_at": now,
|
||||
}).Error
|
||||
if err != nil {
|
||||
errors++
|
||||
} else {
|
||||
@@ -369,22 +396,20 @@ func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Commit batch
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
errors += len(batch)
|
||||
sendProgress:
|
||||
// Send progress update every 50 components
|
||||
if (i+1)%50 == 0 || i == len(components)-1 {
|
||||
c.SSEvent("progress", gin.H{
|
||||
"current": updated + skipped + manual + errors,
|
||||
"total": total,
|
||||
"updated": updated,
|
||||
"skipped": skipped,
|
||||
"manual": manual,
|
||||
"errors": errors,
|
||||
"status": "processing",
|
||||
})
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
// Send progress update
|
||||
c.SSEvent("progress", gin.H{
|
||||
"current": updated + skipped + manual + errors,
|
||||
"total": total,
|
||||
"updated": updated,
|
||||
"skipped": skipped,
|
||||
"manual": manual,
|
||||
"errors": errors,
|
||||
"status": "processing",
|
||||
})
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
// Update popularity scores
|
||||
@@ -494,11 +519,16 @@ func (h *PricingHandler) PreviewPrice(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get median for all time
|
||||
var medianAllTime struct {
|
||||
Price *float64
|
||||
// Get all prices for calculations
|
||||
var allPrices []float64
|
||||
h.db.Raw(`SELECT price FROM lot_log WHERE lot = ? ORDER BY price`, req.LotName).Pluck("price", &allPrices)
|
||||
|
||||
// Calculate median for all time
|
||||
var medianAllTime *float64
|
||||
if len(allPrices) > 0 {
|
||||
median := calculateMedian(allPrices)
|
||||
medianAllTime = &median
|
||||
}
|
||||
h.db.Raw(`SELECT AVG(price) as price FROM lot_log WHERE lot = ?`, req.LotName).Scan(&medianAllTime)
|
||||
|
||||
// Get quote count
|
||||
var quoteCount int64
|
||||
@@ -511,31 +541,43 @@ func (h *PricingHandler) PreviewPrice(c *gin.Context) {
|
||||
}
|
||||
h.db.Raw(`SELECT price, date FROM lot_log WHERE lot = ? ORDER BY date DESC, lot_log_id DESC LIMIT 1`, req.LotName).Scan(&lastPrice)
|
||||
|
||||
// Calculate new price based on parameters
|
||||
var basePrice *float64
|
||||
// Calculate new price based on parameters (method, period, coefficient)
|
||||
method := req.Method
|
||||
if method == "" {
|
||||
method = "median"
|
||||
}
|
||||
|
||||
var prices []float64
|
||||
if req.PeriodDays > 0 {
|
||||
var result struct {
|
||||
Price *float64
|
||||
h.db.Raw(`SELECT price FROM lot_log WHERE lot = ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY) ORDER BY price`,
|
||||
req.LotName, req.PeriodDays).Pluck("price", &prices)
|
||||
// Fall back to all time if no prices in period
|
||||
if len(prices) == 0 {
|
||||
prices = allPrices
|
||||
}
|
||||
h.db.Raw(`SELECT AVG(price) as price FROM lot_log WHERE lot = ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY)`, req.LotName, req.PeriodDays).Scan(&result)
|
||||
basePrice = result.Price
|
||||
} else {
|
||||
basePrice = medianAllTime.Price
|
||||
prices = allPrices
|
||||
}
|
||||
|
||||
var newPrice *float64
|
||||
if basePrice != nil && *basePrice > 0 {
|
||||
calculated := *basePrice
|
||||
if req.Coefficient != 0 {
|
||||
calculated = calculated * (1 + req.Coefficient/100)
|
||||
if len(prices) > 0 {
|
||||
var basePrice float64
|
||||
if method == "average" {
|
||||
basePrice = calculateAverage(prices)
|
||||
} else {
|
||||
basePrice = calculateMedian(prices)
|
||||
}
|
||||
newPrice = &calculated
|
||||
|
||||
if req.Coefficient != 0 {
|
||||
basePrice = basePrice * (1 + req.Coefficient/100)
|
||||
}
|
||||
newPrice = &basePrice
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"lot_name": req.LotName,
|
||||
"current_price": comp.CurrentPrice,
|
||||
"median_all_time": medianAllTime.Price,
|
||||
"median_all_time": medianAllTime,
|
||||
"new_price": newPrice,
|
||||
"quote_count": quoteCount,
|
||||
"manual_price": comp.ManualPrice,
|
||||
|
||||
Reference in New Issue
Block a user