Улучшения управления ценами и конфигурациями
- Добавлено отображение последней полученной цены в окне настройки цены - Добавлен функционал переименования конфигураций (PATCH /api/configs/:uuid/rename) - Изменён формат имени файла при экспорте: "YYYY-MM-DD NAME SPEC.ext" - Исправлена сортировка компонентов: перенесена на сервер для корректной работы с пагинацией - Добавлен расчёт popularity_score на основе котировок из lot_log - Исправлена потеря настроек (метод, период, коэффициент) при пересчёте цен Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -123,17 +124,53 @@ func (h *ConfigurationHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
type RenameConfigRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *ConfigurationHandler) Rename(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
var req RenameConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
config, err := h.configService.Rename(uuid, userID, req.Name)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if err == services.ErrConfigNotFound {
|
||||
status = http.StatusNotFound
|
||||
} else if err == services.ErrConfigForbidden {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, config)
|
||||
}
|
||||
|
||||
func (h *ConfigurationHandler) ExportJSON(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
config, err := h.configService.GetByUUID(uuid, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.configService.ExportJSON(uuid, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Disposition", "attachment; filename=config.json")
|
||||
filename := fmt.Sprintf("%s %s SPEC.json", config.CreatedAt.Format("2006-01-02"), config.Name)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||
c.Data(http.StatusOK, "application/json", data)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ func (h *ExportHandler) ExportCSV(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%s_%s.csv", req.Name, time.Now().Format("20060102"))
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
filename := fmt.Sprintf("%s %s SPEC.csv", time.Now().Format("2006-01-02"), req.Name)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||
c.Data(http.StatusOK, "text/csv; charset=utf-8", csvData)
|
||||
}
|
||||
|
||||
@@ -101,8 +101,8 @@ func (h *ExportHandler) ExportConfigCSV(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%s_%s.csv", config.Name, config.CreatedAt.Format("20060102"))
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
filename := fmt.Sprintf("%s %s SPEC.csv", config.CreatedAt.Format("2006-01-02"), config.Name)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||
c.Data(http.StatusOK, "text/csv; charset=utf-8", csvData)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,10 @@ func (h *PricingHandler) ListComponents(c *gin.Context) {
|
||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
||||
|
||||
filter := repository.ComponentFilter{
|
||||
Category: c.Query("category"),
|
||||
Search: c.Query("search"),
|
||||
Category: c.Query("category"),
|
||||
Search: c.Query("search"),
|
||||
SortField: c.Query("sort"),
|
||||
SortDir: c.Query("dir"),
|
||||
}
|
||||
|
||||
if page < 1 {
|
||||
@@ -203,7 +205,6 @@ func (h *PricingHandler) recalculateSinglePrice(lotName string) {
|
||||
}
|
||||
|
||||
periodDays := comp.PricePeriodDays
|
||||
usedAllTime := false
|
||||
var result struct {
|
||||
Price *float64
|
||||
}
|
||||
@@ -213,13 +214,10 @@ func (h *PricingHandler) recalculateSinglePrice(lotName string) {
|
||||
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)
|
||||
|
||||
// If no prices found in period, fall back to all time
|
||||
// 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 result.Price != nil && *result.Price > 0 {
|
||||
usedAllTime = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := `SELECT AVG(price) as price FROM lot_log WHERE lot = ?`
|
||||
@@ -236,19 +234,13 @@ func (h *PricingHandler) recalculateSinglePrice(lotName string) {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"current_price": finalPrice,
|
||||
"price_updated_at": now,
|
||||
}
|
||||
|
||||
// If we fell back to all time, update the period setting
|
||||
if usedAllTime {
|
||||
updates["price_period_days"] = 0
|
||||
}
|
||||
|
||||
// Only update price, preserve all user settings
|
||||
h.db.Model(&models.LotMetadata{}).
|
||||
Where("lot_name = ?", lotName).
|
||||
Updates(updates)
|
||||
Updates(map[string]interface{}{
|
||||
"current_price": finalPrice,
|
||||
"price_updated_at": now,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
||||
@@ -339,22 +331,20 @@ func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine which price to use
|
||||
// Determine which price to use based on component settings
|
||||
var finalPrice float64
|
||||
usedAllTime := false
|
||||
periodDays := comp.PricePeriodDays
|
||||
|
||||
if periodDays <= 0 {
|
||||
// Already using all time
|
||||
// Use all time
|
||||
finalPrice = *priceData.AvgPrice
|
||||
} 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
|
||||
// Fall back to all time if no data in period, but keep user's period setting
|
||||
finalPrice = *priceData.AvgPrice
|
||||
usedAllTime = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,15 +353,12 @@ func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
||||
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,
|
||||
}
|
||||
|
||||
if usedAllTime {
|
||||
updates["price_period_days"] = 0
|
||||
}
|
||||
|
||||
err := tx.Model(&models.LotMetadata{}).
|
||||
Where("lot_name = ?", comp.LotName).
|
||||
Updates(updates).Error
|
||||
@@ -400,6 +387,9 @@ func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
// Update popularity scores
|
||||
h.statsRepo.UpdatePopularityScores()
|
||||
|
||||
// Send completion
|
||||
c.SSEvent("progress", gin.H{
|
||||
"current": updated + skipped + manual + errors,
|
||||
@@ -514,6 +504,13 @@ func (h *PricingHandler) PreviewPrice(c *gin.Context) {
|
||||
var quoteCount int64
|
||||
h.db.Model(&models.LotLog{}).Where("lot = ?", req.LotName).Count("eCount)
|
||||
|
||||
// Get last received price
|
||||
var lastPrice struct {
|
||||
Price *float64
|
||||
Date *time.Time
|
||||
}
|
||||
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
|
||||
if req.PeriodDays > 0 {
|
||||
@@ -542,5 +539,7 @@ func (h *PricingHandler) PreviewPrice(c *gin.Context) {
|
||||
"new_price": newPrice,
|
||||
"quote_count": quoteCount,
|
||||
"manual_price": comp.ManualPrice,
|
||||
"last_price": lastPrice.Price,
|
||||
"last_price_date": lastPrice.Date,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user