Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-04-22 11:19:36 +03:00
parent e6ae925f1c
commit d2387bc221
10 changed files with 855 additions and 543 deletions

View File

@@ -6,35 +6,209 @@ import (
"context"
"database/sql"
"fmt"
"math"
"strconv"
"strings"
"time"
)
func GetProductPricingList(ctx context.Context, limit int, afterProductCode string) ([]models.ProductPricing, error) {
type ProductPricingFilters struct {
Search string
ProductCode []string
BrandGroup []string
AskiliYan []string
Kategori []string
UrunIlkGrubu []string
UrunAnaGrubu []string
UrunAltGrubu []string
Icerik []string
Karisim []string
Marka []string
}
type ProductPricingPage struct {
Rows []models.ProductPricing
TotalCount int
TotalPages int
Page int
Limit int
}
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters) (ProductPricingPage, error) {
result := ProductPricingPage{
Rows: []models.ProductPricing{},
TotalCount: 0,
TotalPages: 0,
Page: page,
Limit: limit,
}
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 500
}
afterProductCode = strings.TrimSpace(afterProductCode)
offset := (page - 1) * limit
paramIndex := 1
args := make([]any, 0, 64)
nextParam := func() string {
name := "@p" + strconv.Itoa(paramIndex)
paramIndex++
return name
}
whereParts := []string{
"ProductAtt42 IN ('SERI', 'AKSESUAR')",
"IsBlocked = 0",
"LEN(LTRIM(RTRIM(ProductCode))) = 13",
}
addInFilter := func(expr string, values []string) {
clean := make([]string, 0, len(values))
for _, v := range values {
v = strings.TrimSpace(v)
if v == "" {
continue
}
clean = append(clean, v)
}
if len(clean) == 0 {
return
}
ors := make([]string, 0, len(clean))
for _, v := range clean {
p := nextParam()
ors = append(ors, expr+" = "+p)
args = append(args, v)
}
whereParts = append(whereParts, "("+strings.Join(ors, " OR ")+")")
}
brandGroupExpr := `CASE ABS(CHECKSUM(LTRIM(RTRIM(ProductCode)))) % 3
WHEN 0 THEN 'MARKA GRUBU A'
WHEN 1 THEN 'MARKA GRUBU B'
ELSE 'MARKA GRUBU C'
END`
addInFilter("LTRIM(RTRIM(ProductCode))", filters.ProductCode)
addInFilter(brandGroupExpr, filters.BrandGroup)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '')", filters.AskiliYan)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '')", filters.Kategori)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '')", filters.UrunIlkGrubu)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '')", filters.UrunAnaGrubu)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '')", filters.UrunAltGrubu)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '')", filters.Icerik)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '')", filters.Karisim)
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '')", filters.Marka)
if q := strings.TrimSpace(filters.Search); q != "" {
p := nextParam()
args = append(args, "%"+q+"%")
whereParts = append(whereParts, "("+strings.Join([]string{
"LTRIM(RTRIM(ProductCode)) LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') LIKE " + p,
"COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') LIKE " + p,
}, " OR ")+")")
}
whereSQL := strings.Join(whereParts, " AND ")
countQuery := `
SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode)))
FROM ProductFilterWithDescription('TR')
WHERE ` + whereSQL + `;
`
var totalCount int
if err := db.MssqlDB.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil {
return result, err
}
result.TotalCount = totalCount
if totalCount == 0 {
result.TotalPages = 0
result.Page = 1
return result, nil
}
totalPages := int(math.Ceil(float64(totalCount) / float64(limit)))
if totalPages <= 0 {
totalPages = 1
}
if page > totalPages {
page = totalPages
offset = (page - 1) * limit
}
result.Page = page
result.Limit = limit
result.TotalPages = totalPages
// Stage 1: fetch only paged products first (fast path).
productQuery := `
SELECT TOP (` + strconv.Itoa(limit) + `)
LTRIM(RTRIM(ProductCode)) AS ProductCode,
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu,
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
FROM ProductFilterWithDescription('TR')
WHERE ProductAtt42 IN ('SERI', 'AKSESUAR')
AND IsBlocked = 0
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
AND (@p1 = '' OR LTRIM(RTRIM(ProductCode)) > @p1)
ORDER BY LTRIM(RTRIM(ProductCode));
IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes;
IF OBJECT_ID('tempdb..#stock_base') IS NOT NULL DROP TABLE #stock_base;
SELECT
f.ProductCode,
MAX(f.BrandGroupSec) AS BrandGroupSec,
MAX(f.AskiliYan) AS AskiliYan,
MAX(f.Kategori) AS Kategori,
MAX(f.UrunIlkGrubu) AS UrunIlkGrubu,
MAX(f.UrunAnaGrubu) AS UrunAnaGrubu,
MAX(f.UrunAltGrubu) AS UrunAltGrubu,
MAX(f.Icerik) AS Icerik,
MAX(f.Karisim) AS Karisim,
MAX(f.Marka) AS Marka
INTO #req_codes
FROM (
SELECT
LTRIM(RTRIM(ProductCode)) AS ProductCode,
` + brandGroupExpr + ` AS BrandGroupSec,
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu,
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
FROM ProductFilterWithDescription('TR')
WHERE ` + whereSQL + `
) f
GROUP BY f.ProductCode;
CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode);
SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
INTO #stock_base
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));
CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode);
SELECT
rc.ProductCode,
rc.BrandGroupSec,
rc.AskiliYan,
rc.Kategori,
rc.UrunIlkGrubu,
rc.UrunAnaGrubu,
rc.UrunAltGrubu,
rc.Icerik,
rc.Karisim,
rc.Marka
FROM #req_codes rc
LEFT JOIN #stock_base sb
ON sb.ItemCode = rc.ProductCode
ORDER BY
CAST(ROUND(ISNULL(sb.InventoryQty1, 0), 2) AS DECIMAL(18, 2)) DESC,
rc.ProductCode ASC
OFFSET ` + strconv.Itoa(offset) + ` ROWS
FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY;
`
var (
@@ -43,7 +217,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
)
for attempt := 1; attempt <= 3; attempt++ {
var err error
rows, err = db.MssqlDB.QueryContext(ctx, productQuery, afterProductCode)
rows, err = db.MssqlDB.QueryContext(ctx, productQuery, args...)
if err == nil {
rowsErr = nil
break
@@ -60,7 +234,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
}
}
if rowsErr != nil {
return nil, rowsErr
return result, rowsErr
}
defer rows.Close()
@@ -69,6 +243,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
var item models.ProductPricing
if err := rows.Scan(
&item.ProductCode,
&item.BrandGroupSec,
&item.AskiliYan,
&item.Kategori,
&item.UrunIlkGrubu,
@@ -78,15 +253,16 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
&item.Karisim,
&item.Marka,
); err != nil {
return nil, err
return result, err
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, err
return result, err
}
if len(out) == 0 {
return out, nil
result.Rows = out
return result, nil
}
// Stage 2: fetch metrics only for paged product codes.
@@ -134,6 +310,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
AND s.In_Qty1 > 0
AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP'
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',
@@ -214,7 +391,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
metricsRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...)
if err != nil {
return nil, fmt.Errorf("metrics query failed: %w", err)
return result, fmt.Errorf("metrics query failed: %w", err)
}
defer metricsRows.Close()
@@ -237,12 +414,12 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
&m.StockEntryDate,
&m.LastPricingDate,
); err != nil {
return nil, err
return result, err
}
metricsByCode[strings.TrimSpace(code)] = m
}
if err := metricsRows.Err(); err != nil {
return nil, err
return result, err
}
for i := range out {
@@ -254,7 +431,8 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
}
}
return out, nil
result.Rows = out
return result, nil
}
func isTransientMSSQLNetworkError(err error) bool {

View File

@@ -18,6 +18,7 @@ import (
"time"
"github.com/gorilla/mux"
"github.com/lib/pq"
"golang.org/x/crypto/bcrypt"
)
@@ -497,25 +498,65 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
// ROLES
for _, role := range payload.Roles {
_, _ = tx.Exec(queries.InsertUserRole, newID, role)
role = strings.TrimSpace(role)
if role == "" {
continue
}
if _, err := tx.Exec(queries.InsertUserRole, newID, role); err != nil {
log.Printf("USER ROLE INSERT ERROR user_id=%d role=%q err=%v", newID, role, err)
http.Error(w, "Rol eklenemedi", http.StatusInternalServerError)
return
}
}
// DEPARTMENTS
for _, d := range payload.Departments {
_, _ = tx.Exec(queries.InsertUserDepartment, newID, d.Code)
code := strings.TrimSpace(d.Code)
if code == "" {
continue
}
if _, err := tx.Exec(queries.InsertUserDepartment, newID, code); err != nil {
log.Printf("USER DEPARTMENT INSERT ERROR user_id=%d department=%q err=%v", newID, code, err)
http.Error(w, "Departman eklenemedi", http.StatusInternalServerError)
return
}
}
// PIYASALAR
for _, p := range payload.Piyasalar {
_, _ = tx.Exec(queries.InsertUserPiyasa, newID, p.Code)
code := strings.TrimSpace(p.Code)
if code == "" {
continue
}
if _, err := tx.Exec(queries.InsertUserPiyasa, newID, code); err != nil {
log.Printf("USER PIYASA INSERT ERROR user_id=%d piyasa=%q err=%v", newID, code, err)
http.Error(w, "Piyasa eklenemedi", http.StatusInternalServerError)
return
}
}
// NEBIM
for _, n := range payload.NebimUsers {
_, _ = tx.Exec(queries.InsertUserNebim, newID, n.Username)
username := strings.TrimSpace(n.Username)
if username == "" {
continue
}
if _, err := tx.Exec(queries.InsertUserNebim, newID, username); err != nil {
log.Printf("USER NEBIM INSERT ERROR user_id=%d username=%q err=%v", newID, username, err)
http.Error(w, "Nebim kullanıcısı eklenemedi", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(); err != nil {
if pe, ok := err.(*pq.Error); ok {
log.Printf(
"USER CREATE COMMIT ERROR user_id=%d code=%s detail=%s constraint=%s table=%s err=%v",
newID, pe.Code, pe.Detail, pe.Constraint, pe.Table, err,
)
} else {
log.Printf("USER CREATE COMMIT ERROR user_id=%d err=%v", newID, err)
}
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
return
}

View File

@@ -32,13 +32,31 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
limit := 500
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 10000 {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 500 {
limit = parsed
}
}
afterProductCode := strings.TrimSpace(r.URL.Query().Get("after_product_code"))
page := 1
if raw := strings.TrimSpace(r.URL.Query().Get("page")); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
page = parsed
}
}
filters := queries.ProductPricingFilters{
Search: strings.TrimSpace(r.URL.Query().Get("q")),
ProductCode: splitCSVParam(r.URL.Query().Get("product_code")),
BrandGroup: splitCSVParam(r.URL.Query().Get("brand_group_selection")),
AskiliYan: splitCSVParam(r.URL.Query().Get("askili_yan")),
Kategori: splitCSVParam(r.URL.Query().Get("kategori")),
UrunIlkGrubu: splitCSVParam(r.URL.Query().Get("urun_ilk_grubu")),
UrunAnaGrubu: splitCSVParam(r.URL.Query().Get("urun_ana_grubu")),
UrunAltGrubu: splitCSVParam(r.URL.Query().Get("urun_alt_grubu")),
Icerik: splitCSVParam(r.URL.Query().Get("icerik")),
Karisim: splitCSVParam(r.URL.Query().Get("karisim")),
Marka: splitCSVParam(r.URL.Query().Get("marka")),
}
rows, err := queries.GetProductPricingList(ctx, limit+1, afterProductCode)
pageResult, err := queries.GetProductPricingPage(ctx, page, limit, filters)
if err != nil {
if isPricingTimeoutLike(err, ctx.Err()) {
log.Printf(
@@ -63,38 +81,24 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
hasMore := len(rows) > limit
if hasMore {
rows = rows[:limit]
}
nextCursor := ""
if hasMore && len(rows) > 0 {
nextCursor = strings.TrimSpace(rows[len(rows)-1].ProductCode)
}
log.Printf(
"[ProductPricing] trace=%s success user=%s id=%d limit=%d after=%q count=%d has_more=%t next=%q duration_ms=%d",
"[ProductPricing] trace=%s success user=%s id=%d page=%d limit=%d count=%d total=%d total_pages=%d duration_ms=%d",
traceID,
claims.Username,
claims.ID,
pageResult.Page,
limit,
afterProductCode,
len(rows),
hasMore,
nextCursor,
len(pageResult.Rows),
pageResult.TotalCount,
pageResult.TotalPages,
time.Since(started).Milliseconds(),
)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if hasMore {
w.Header().Set("X-Has-More", "true")
} else {
w.Header().Set("X-Has-More", "false")
}
if nextCursor != "" {
w.Header().Set("X-Next-Cursor", nextCursor)
}
_ = json.NewEncoder(w).Encode(rows)
w.Header().Set("X-Total-Count", strconv.Itoa(pageResult.TotalCount))
w.Header().Set("X-Total-Pages", strconv.Itoa(pageResult.TotalPages))
w.Header().Set("X-Page", strconv.Itoa(pageResult.Page))
_ = json.NewEncoder(w).Encode(pageResult.Rows)
}
func buildPricingTraceID(r *http.Request) string {
@@ -124,3 +128,20 @@ func isPricingTimeoutLike(err error, ctxErr error) bool {
strings.Contains(e, "no connection could be made") ||
strings.Contains(e, "failed to respond")
}
func splitCSVParam(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
out = append(out, p)
}
return out
}