Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-04-20 09:42:34 +03:00
parent a1f5c653c6
commit 7ef12df93a
4 changed files with 212 additions and 180 deletions

View File

@@ -5,6 +5,7 @@ import (
"bssapp-backend/models" "bssapp-backend/models"
"context" "context"
"database/sql" "database/sql"
"fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -16,16 +17,9 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
} }
afterProductCode = strings.TrimSpace(afterProductCode) afterProductCode = strings.TrimSpace(afterProductCode)
cursorFilter := "" // Stage 1: fetch only paged products first (fast path).
args := make([]any, 0, 1) productQuery := `
if afterProductCode != "" { SELECT TOP (` + strconv.Itoa(limit) + `)
cursorFilter = "WHERE bp.ProductCode > @p1"
args = append(args, afterProductCode)
}
query := `
WITH base_products AS (
SELECT
LTRIM(RTRIM(ProductCode)) AS ProductCode, LTRIM(RTRIM(ProductCode)) AS ProductCode,
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan, COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori, COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
@@ -39,153 +33,8 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
WHERE ProductAtt42 IN ('SERI', 'AKSESUAR') WHERE ProductAtt42 IN ('SERI', 'AKSESUAR')
AND IsBlocked = 0 AND IsBlocked = 0
AND LEN(LTRIM(RTRIM(ProductCode))) = 13 AND LEN(LTRIM(RTRIM(ProductCode))) = 13
), AND (@p1 = '' OR LTRIM(RTRIM(ProductCode)) > @p1)
paged_products AS ( ORDER BY LTRIM(RTRIM(ProductCode));
SELECT TOP (` + strconv.Itoa(limit) + `)
bp.ProductCode
FROM base_products bp
` + cursorFilter + `
ORDER BY bp.ProductCode
),
latest_base_price AS (
SELECT
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice,
CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate,
ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(b.ItemCode))
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
) AS rn
FROM prItemBasePrice b
WHERE b.ItemTypeCode = 1
AND b.BasePriceCode = 1
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
AND EXISTS (
SELECT 1
FROM paged_products pp
WHERE pp.ProductCode = LTRIM(RTRIM(b.ItemCode))
)
),
stock_entry_dates AS (
SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate
FROM trStock s WITH(NOLOCK)
WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
AND s.In_Qty1 > 0
AND LTRIM(RTRIM(s.WarehouseCode)) IN (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36'
)
AND EXISTS (
SELECT 1
FROM paged_products pp
WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode))
)
GROUP BY LTRIM(RTRIM(s.ItemCode))
),
stock_base AS (
SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
FROM trStock s WITH(NOLOCK)
WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
AND EXISTS (
SELECT 1
FROM paged_products pp
WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode))
)
GROUP BY LTRIM(RTRIM(s.ItemCode))
),
pick_base AS (
SELECT
LTRIM(RTRIM(p.ItemCode)) AS ItemCode,
SUM(p.Qty1) AS PickingQty1
FROM PickingStates p
WHERE p.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
AND EXISTS (
SELECT 1
FROM paged_products pp
WHERE pp.ProductCode = LTRIM(RTRIM(p.ItemCode))
)
GROUP BY LTRIM(RTRIM(p.ItemCode))
),
reserve_base AS (
SELECT
LTRIM(RTRIM(r.ItemCode)) AS ItemCode,
SUM(r.Qty1) AS ReserveQty1
FROM ReserveStates r
WHERE r.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
AND EXISTS (
SELECT 1
FROM paged_products pp
WHERE pp.ProductCode = LTRIM(RTRIM(r.ItemCode))
)
GROUP BY LTRIM(RTRIM(r.ItemCode))
),
disp_base AS (
SELECT
LTRIM(RTRIM(d.ItemCode)) AS ItemCode,
SUM(d.Qty1) AS DispOrderQty1
FROM DispOrderStates d
WHERE d.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
AND EXISTS (
SELECT 1
FROM paged_products pp
WHERE pp.ProductCode = LTRIM(RTRIM(d.ItemCode))
)
GROUP BY LTRIM(RTRIM(d.ItemCode))
),
stock_totals AS (
SELECT
pp.ProductCode AS ItemCode,
CAST(ROUND(
ISNULL(sb.InventoryQty1, 0)
- ISNULL(pb.PickingQty1, 0)
- ISNULL(rb.ReserveQty1, 0)
- ISNULL(db.DispOrderQty1, 0)
, 2) AS DECIMAL(18, 2)) AS StockQty
FROM paged_products pp
LEFT JOIN stock_base sb
ON sb.ItemCode = pp.ProductCode
LEFT JOIN pick_base pb
ON pb.ItemCode = pp.ProductCode
LEFT JOIN reserve_base rb
ON rb.ItemCode = pp.ProductCode
LEFT JOIN disp_base db
ON db.ItemCode = pp.ProductCode
)
SELECT
bp.ProductCode AS ProductCode,
COALESCE(lp.CostPrice, 0) AS CostPrice,
COALESCE(st.StockQty, 0) AS StockQty,
COALESCE(se.StockEntryDate, '') AS StockEntryDate,
COALESCE(lp.LastPricingDate, '') AS LastPricingDate,
bp.AskiliYan,
bp.Kategori,
bp.UrunIlkGrubu,
bp.UrunAnaGrubu,
bp.UrunAltGrubu,
bp.Icerik,
bp.Karisim,
bp.Marka
FROM paged_products pp
INNER JOIN base_products bp
ON bp.ProductCode = pp.ProductCode
LEFT JOIN latest_base_price lp
ON lp.ItemCode = bp.ProductCode
AND lp.rn = 1
LEFT JOIN stock_entry_dates se
ON se.ItemCode = bp.ProductCode
LEFT JOIN stock_totals st
ON st.ItemCode = bp.ProductCode
ORDER BY bp.ProductCode;
` `
var ( var (
@@ -194,7 +43,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
) )
for attempt := 1; attempt <= 3; attempt++ { for attempt := 1; attempt <= 3; attempt++ {
var err error var err error
rows, err = db.MssqlDB.QueryContext(ctx, query, args...) rows, err = db.MssqlDB.QueryContext(ctx, productQuery, afterProductCode)
if err == nil { if err == nil {
rowsErr = nil rowsErr = nil
break break
@@ -215,15 +64,11 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
} }
defer rows.Close() defer rows.Close()
var out []models.ProductPricing out := make([]models.ProductPricing, 0, limit)
for rows.Next() { for rows.Next() {
var item models.ProductPricing var item models.ProductPricing
if err := rows.Scan( if err := rows.Scan(
&item.ProductCode, &item.ProductCode,
&item.CostPrice,
&item.StockQty,
&item.StockEntryDate,
&item.LastPricingDate,
&item.AskiliYan, &item.AskiliYan,
&item.Kategori, &item.Kategori,
&item.UrunIlkGrubu, &item.UrunIlkGrubu,
@@ -237,6 +82,171 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
} }
out = append(out, item) out = append(out, item)
} }
if err := rows.Err(); err != nil {
return nil, err
}
if len(out) == 0 {
return out, nil
}
// Stage 2: fetch metrics only for paged product codes.
codes := make([]string, 0, len(out))
for _, item := range out {
codes = append(codes, strings.TrimSpace(item.ProductCode))
}
codesCSV := strings.Join(codes, ",")
metricsQuery := `
WITH req_codes AS (
SELECT DISTINCT LTRIM(RTRIM(value)) AS ProductCode
FROM STRING_SPLIT(@p1, ',')
WHERE LEN(LTRIM(RTRIM(value))) > 0
),
latest_base_price AS (
SELECT
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice,
CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate,
ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(b.ItemCode))
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
) AS rn
FROM prItemBasePrice b
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(b.ItemCode))
WHERE b.ItemTypeCode = 1
AND b.BasePriceCode = 1
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
),
stock_entry_dates AS (
SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate
FROM trStock s WITH(NOLOCK)
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode))
WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
AND s.In_Qty1 > 0
AND LTRIM(RTRIM(s.WarehouseCode)) IN (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36'
)
GROUP BY LTRIM(RTRIM(s.ItemCode))
),
stock_base AS (
SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
FROM trStock s WITH(NOLOCK)
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode))
WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
GROUP BY LTRIM(RTRIM(s.ItemCode))
),
pick_base AS (
SELECT
LTRIM(RTRIM(p.ItemCode)) AS ItemCode,
SUM(p.Qty1) AS PickingQty1
FROM PickingStates p
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode))
WHERE p.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
GROUP BY LTRIM(RTRIM(p.ItemCode))
),
reserve_base AS (
SELECT
LTRIM(RTRIM(r.ItemCode)) AS ItemCode,
SUM(r.Qty1) AS ReserveQty1
FROM ReserveStates r
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(r.ItemCode))
WHERE r.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
GROUP BY LTRIM(RTRIM(r.ItemCode))
),
disp_base AS (
SELECT
LTRIM(RTRIM(d.ItemCode)) AS ItemCode,
SUM(d.Qty1) AS DispOrderQty1
FROM DispOrderStates d
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(d.ItemCode))
WHERE d.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
GROUP BY LTRIM(RTRIM(d.ItemCode))
)
SELECT
rc.ProductCode,
COALESCE(lp.CostPrice, 0) AS CostPrice,
CAST(ROUND(
ISNULL(sb.InventoryQty1, 0)
- ISNULL(pb.PickingQty1, 0)
- ISNULL(rb.ReserveQty1, 0)
- ISNULL(db.DispOrderQty1, 0)
, 2) AS DECIMAL(18, 2)) AS StockQty,
COALESCE(se.StockEntryDate, '') AS StockEntryDate,
COALESCE(lp.LastPricingDate, '') AS LastPricingDate
FROM req_codes rc
LEFT JOIN latest_base_price lp
ON lp.ItemCode = rc.ProductCode
AND lp.rn = 1
LEFT JOIN stock_entry_dates se
ON se.ItemCode = rc.ProductCode
LEFT JOIN stock_base sb
ON sb.ItemCode = rc.ProductCode
LEFT JOIN pick_base pb
ON pb.ItemCode = rc.ProductCode
LEFT JOIN reserve_base rb
ON rb.ItemCode = rc.ProductCode
LEFT JOIN disp_base db
ON db.ItemCode = rc.ProductCode;
`
metricsRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, codesCSV)
if err != nil {
return nil, fmt.Errorf("metrics query failed: %w", err)
}
defer metricsRows.Close()
type metrics struct {
CostPrice float64
StockQty float64
StockEntryDate string
LastPricingDate string
}
metricsByCode := make(map[string]metrics, len(out))
for metricsRows.Next() {
var (
code string
m metrics
)
if err := metricsRows.Scan(
&code,
&m.CostPrice,
&m.StockQty,
&m.StockEntryDate,
&m.LastPricingDate,
); err != nil {
return nil, err
}
metricsByCode[strings.TrimSpace(code)] = m
}
if err := metricsRows.Err(); err != nil {
return nil, err
}
for i := range out {
if m, ok := metricsByCode[strings.TrimSpace(out[i].ProductCode)]; ok {
out[i].CostPrice = m.CostPrice
out[i].StockQty = m.StockQty
out[i].StockEntryDate = m.StockEntryDate
out[i].LastPricingDate = m.LastPricingDate
}
}
return out, nil return out, nil
} }

View File

@@ -26,7 +26,8 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
} }
log.Printf("[ProductPricing] trace=%s start user=%s id=%d", traceID, claims.Username, claims.ID) log.Printf("[ProductPricing] trace=%s start user=%s id=%d", traceID, claims.Username, claims.ID)
ctx, cancel := context.WithTimeout(r.Context(), 180*time.Second) // Cloudflare upstream timeout is lower than 180s; fail fast and return API 504 instead of CDN 524.
ctx, cancel := context.WithTimeout(r.Context(), 110*time.Second)
defer cancel() defer cancel()
limit := 500 limit := 500

View File

@@ -59,6 +59,7 @@ var (
reScriptLabelProp = regexp.MustCompile(`\blabel\s*:\s*['"]([^'"]{2,180})['"]`) reScriptLabelProp = regexp.MustCompile(`\blabel\s*:\s*['"]([^'"]{2,180})['"]`)
reScriptUIProp = regexp.MustCompile(`\b(?:label|message|title|placeholder|hint)\s*:\s*['"]([^'"]{2,180})['"]`) reScriptUIProp = regexp.MustCompile(`\b(?:label|message|title|placeholder|hint)\s*:\s*['"]([^'"]{2,180})['"]`)
reTemplateDynamic = regexp.MustCompile(`[{][{]|[}][}]`) reTemplateDynamic = regexp.MustCompile(`[{][{]|[}][}]`)
reCodeLikeText = regexp.MustCompile(`(?i)(\bconst\b|\blet\b|\bvar\b|\breturn\b|\bfunction\b|=>|\|\||&&|\?\?|//|/\*|\*/|\.trim\(|\.replace\(|\.map\(|\.filter\()`)
) )
var translationNoiseTokens = map[string]struct{}{ var translationNoiseTokens = map[string]struct{}{
@@ -1800,9 +1801,22 @@ func isCandidateText(s string) bool {
if strings.Contains(s, "/api/") { if strings.Contains(s, "/api/") {
return false return false
} }
if reCodeLikeText.MatchString(s) {
return false
}
if strings.ContainsAny(s, "{}[];`") { if strings.ContainsAny(s, "{}[];`") {
return false return false
} }
symbolCount := 0
for _, r := range s {
switch r {
case '(', ')', '=', ':', '/', '\\', '|', '&', '*', '<', '>', '_':
symbolCount++
}
}
if symbolCount >= 4 {
return false
}
return true return true
} }

View File

@@ -894,9 +894,16 @@ async function reloadData () {
console.info('[product-pricing][ui] reload:start', { console.info('[product-pricing][ui] reload:start', {
at: new Date(startedAt).toISOString() at: new Date(startedAt).toISOString()
}) })
try {
nextCursor.value = '' nextCursor.value = ''
await fetchChunk({ reset: true }) await fetchChunk({ reset: true })
await ensureEnoughVisibleRows(120, 6) await ensureEnoughVisibleRows(120, 6)
} catch (err) {
console.error('[product-pricing][ui] reload:error', {
duration_ms: Date.now() - startedAt,
message: String(err?.message || err || 'reload failed')
})
}
console.info('[product-pricing][ui] reload:done', { console.info('[product-pricing][ui] reload:done', {
duration_ms: Date.now() - startedAt, duration_ms: Date.now() - startedAt,
row_count: Array.isArray(store.rows) ? store.rows.length : 0, row_count: Array.isArray(store.rows) ? store.rows.length : 0,