Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -21,139 +22,38 @@ type ProductImageItem struct {
|
|||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
Storage string `json:"storage_path"`
|
Storage string `json:"storage_path"`
|
||||||
ContentURL string `json:"content_url"`
|
ContentURL string `json:"content_url"`
|
||||||
|
UUID string `json:"uuid,omitempty"`
|
||||||
|
ThumbURL string `json:"thumb_url,omitempty"`
|
||||||
|
FullURL string `json:"full_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenizeImageFileName(fileName string) []string {
|
var uuidPattern = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
|
||||||
up := strings.ToUpper(strings.TrimSpace(fileName))
|
|
||||||
if up == "" {
|
func normalizeDimParam(v string) string {
|
||||||
return nil
|
s := strings.TrimSpace(v)
|
||||||
|
if s == "" || s == "0" {
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
return strings.FieldsFunc(up, func(r rune) bool {
|
return s
|
||||||
isUpper := r >= 'A' && r <= 'Z'
|
|
||||||
isDigit := r >= '0' && r <= '9'
|
|
||||||
if isUpper || isDigit || r == '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAllDigits(s string) bool {
|
func extractImageUUID(storagePath, fileName string) string {
|
||||||
if s == "" {
|
if m := uuidPattern.FindString(storagePath); m != "" {
|
||||||
return false
|
return strings.ToLower(m)
|
||||||
}
|
|
||||||
for _, r := range s {
|
|
||||||
if r < '0' || r > '9' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageFileHasDim3Pattern(fileName string) bool {
|
|
||||||
tokens := tokenizeImageFileName(fileName)
|
|
||||||
for _, t := range tokens {
|
|
||||||
parts := strings.SplitN(t, "_", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(parts[0]) == 3 && isAllDigits(parts[0]) && isAllDigits(parts[1]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstThreeDigitToken(tokens []string) string {
|
|
||||||
for _, t := range tokens {
|
|
||||||
base := t
|
|
||||||
if idx := strings.Index(base, "_"); idx >= 0 {
|
|
||||||
base = base[:idx]
|
|
||||||
}
|
|
||||||
if len(base) == 3 && isAllDigits(base) {
|
|
||||||
return base
|
|
||||||
}
|
}
|
||||||
|
if m := uuidPattern.FindString(fileName); m != "" {
|
||||||
|
return strings.ToLower(m)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func filePrimaryMatchesDim1(fileName, dim1 string) bool {
|
|
||||||
dim1 = strings.ToUpper(strings.TrimSpace(dim1))
|
|
||||||
if dim1 == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
tokens := tokenizeImageFileName(fileName)
|
|
||||||
if len(tokens) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
primary := firstThreeDigitToken(tokens)
|
|
||||||
if primary == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return primary == dim1
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageFileMatches(fileName, dim1, dim3 string) bool {
|
|
||||||
dim1 = strings.ToUpper(strings.TrimSpace(dim1))
|
|
||||||
dim3 = strings.ToUpper(strings.TrimSpace(dim3))
|
|
||||||
if dim1 == "" && dim3 == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens := tokenizeImageFileName(fileName)
|
|
||||||
if len(tokens) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
matchesToken := func(token, target string) bool {
|
|
||||||
if token == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// "002" filtresi, dosya adindaki "002_1" gibi varyantlari da yakalamali.
|
|
||||||
if len(target) == 3 && isAllDigits(target) && strings.HasPrefix(token, target+"_") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
hasToken := func(target string) bool {
|
|
||||||
if target == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, t := range tokens {
|
|
||||||
if matchesToken(t, target) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// dim1 filtresi varsa, dosya adindaki primary renk token'i farkliysa eslesme sayma.
|
|
||||||
// Ornek: "017--002_1" dosyasi dim1=002 icin degil, primary=017 oldugu icin dislanmali.
|
|
||||||
if dim1 != "" {
|
|
||||||
primary := firstThreeDigitToken(tokens)
|
|
||||||
if primary != "" && primary != dim1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasToken(dim1) && hasToken(dim3)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// LIST PRODUCT IMAGES
|
|
||||||
//
|
|
||||||
|
|
||||||
// GET /api/product-images?code=...&dim1=...&dim3=...
|
// GET /api/product-images?code=...&dim1=...&dim3=...
|
||||||
func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
|
func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
|
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
|
||||||
if reqID == "" {
|
if reqID == "" {
|
||||||
reqID = uuid.NewString()
|
reqID = uuid.NewString()
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("X-Request-ID", reqID)
|
w.Header().Set("X-Request-ID", reqID)
|
||||||
|
|
||||||
code := strings.TrimSpace(r.URL.Query().Get("code"))
|
code := strings.TrimSpace(r.URL.Query().Get("code"))
|
||||||
@@ -161,10 +61,6 @@ func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
if dim1 == "" {
|
if dim1 == "" {
|
||||||
dim1 = strings.TrimSpace(r.URL.Query().Get("color"))
|
dim1 = strings.TrimSpace(r.URL.Query().Get("color"))
|
||||||
}
|
}
|
||||||
dim1ID := strings.TrimSpace(r.URL.Query().Get("dim1_id"))
|
|
||||||
if dim1ID == "" {
|
|
||||||
dim1ID = strings.TrimSpace(r.URL.Query().Get("itemdim1"))
|
|
||||||
}
|
|
||||||
dim3 := strings.TrimSpace(r.URL.Query().Get("dim3"))
|
dim3 := strings.TrimSpace(r.URL.Query().Get("dim3"))
|
||||||
if dim3 == "" {
|
if dim3 == "" {
|
||||||
dim3 = strings.TrimSpace(r.URL.Query().Get("yaka"))
|
dim3 = strings.TrimSpace(r.URL.Query().Get("yaka"))
|
||||||
@@ -172,62 +68,93 @@ func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
if dim3 == "" {
|
if dim3 == "" {
|
||||||
dim3 = strings.TrimSpace(r.URL.Query().Get("renk2"))
|
dim3 = strings.TrimSpace(r.URL.Query().Get("renk2"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dim1ID := strings.TrimSpace(r.URL.Query().Get("dim1_id"))
|
||||||
|
if dim1ID == "" {
|
||||||
|
dim1ID = strings.TrimSpace(r.URL.Query().Get("itemdim1"))
|
||||||
|
}
|
||||||
dim3ID := strings.TrimSpace(r.URL.Query().Get("dim3_id"))
|
dim3ID := strings.TrimSpace(r.URL.Query().Get("dim3_id"))
|
||||||
if dim3ID == "" {
|
if dim3ID == "" {
|
||||||
dim3ID = strings.TrimSpace(r.URL.Query().Get("itemdim3"))
|
dim3ID = strings.TrimSpace(r.URL.Query().Get("itemdim3"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if code == "" {
|
if code == "" {
|
||||||
|
|
||||||
slog.Warn("product_images.list.bad_request",
|
|
||||||
"req_id", reqID,
|
|
||||||
"path", r.URL.Path,
|
|
||||||
"query", r.URL.RawQuery,
|
|
||||||
"reason", "missing_code",
|
|
||||||
)
|
|
||||||
|
|
||||||
http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest)
|
http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rule: code -> mmitem.id
|
||||||
|
var mmItemID int64
|
||||||
|
err := pg.QueryRow(`
|
||||||
|
SELECT id
|
||||||
|
FROM mmitem
|
||||||
|
WHERE UPPER(REPLACE(COALESCE(code,''), ' ', '')) = UPPER(REPLACE(COALESCE($1,''), ' ', ''))
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT 1
|
||||||
|
`, code).Scan(&mmItemID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
err = pg.QueryRow(`
|
||||||
|
SELECT id
|
||||||
|
FROM mmitem
|
||||||
|
WHERE UPPER(REPLACE(REGEXP_REPLACE(COALESCE(code,''), '^.*-', ''), ' ', '')) =
|
||||||
|
UPPER(REPLACE(REGEXP_REPLACE(COALESCE($1,''), '^.*-', ''), ' ', ''))
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT 1
|
||||||
|
`, code).Scan(&mmItemID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_ = json.NewEncoder(w).Encode([]ProductImageItem{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Gorsel sorgu hatasi: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule:
|
||||||
|
// dim1!=0 && dim3!=0 => dimval1=dim1 AND dimval3=dim3
|
||||||
|
// dim1!=0 && dim3==0 => dimval1=dim1
|
||||||
|
// dim1==0 && dim3==0 => generic photos
|
||||||
|
dim1Filter := normalizeDimParam(dim1ID)
|
||||||
|
if dim1Filter == "" {
|
||||||
|
dim1Filter = normalizeDimParam(dim1)
|
||||||
|
}
|
||||||
|
dim3Filter := normalizeDimParam(dim3ID)
|
||||||
|
if dim3Filter == "" {
|
||||||
|
dim3Filter = normalizeDimParam(dim3)
|
||||||
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
b.id,
|
id,
|
||||||
b.file_name,
|
COALESCE(file_name,'') AS file_name,
|
||||||
COALESCE(b.file_size,0) AS file_size,
|
COALESCE(file_size,0) AS file_size,
|
||||||
COALESCE(b.storage_path,'') AS storage_path,
|
COALESCE(storage_path,'') AS storage_path
|
||||||
UPPER(COALESCE(b.dimval1::text,'')) AS dimval1,
|
FROM dfblob
|
||||||
UPPER(COALESCE(b.dimval2::text,'')) AS dimval2,
|
WHERE typ='img'
|
||||||
UPPER(COALESCE(b.dimval3::text,'')) AS dimval3
|
AND src_table='mmitem'
|
||||||
FROM dfblob b
|
AND src_id=$1`
|
||||||
JOIN mmitem i
|
args := []interface{}{mmItemID}
|
||||||
ON i.id = b.src_id
|
argPos := 2
|
||||||
WHERE b.typ = 'img'
|
if dim1Filter != "" {
|
||||||
AND b.src_table = 'mmitem'
|
query += fmt.Sprintf(" AND COALESCE(dimval1::text,'') = $%d", argPos)
|
||||||
AND (
|
args = append(args, dim1Filter)
|
||||||
-- exact code match (spaces ignored)
|
argPos++
|
||||||
UPPER(REPLACE(COALESCE(i.code,''), ' ', '')) = UPPER(REPLACE(COALESCE($1,''), ' ', ''))
|
if dim3Filter != "" {
|
||||||
-- core-code fallback only when there is no exact item code for the input
|
query += fmt.Sprintf(" AND COALESCE(dimval3::text,'') = $%d", argPos)
|
||||||
OR (
|
args = append(args, dim3Filter)
|
||||||
NOT EXISTS (
|
argPos++
|
||||||
SELECT 1
|
}
|
||||||
FROM mmitem ix
|
}
|
||||||
WHERE UPPER(REPLACE(COALESCE(ix.code,''), ' ', '')) = UPPER(REPLACE(COALESCE($1,''), ' ', ''))
|
query += `
|
||||||
)
|
|
||||||
AND UPPER(REPLACE(REGEXP_REPLACE(COALESCE(i.code,''), '^.*-', ''), ' ', '')) =
|
|
||||||
UPPER(REPLACE(REGEXP_REPLACE(COALESCE($1,''), '^.*-', ''), ' ', ''))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
COALESCE(b.sort_order,999999),
|
COALESCE(sort_order,999999),
|
||||||
b.zlins_dttm DESC,
|
zlins_dttm DESC,
|
||||||
b.id DESC
|
id DESC`
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := pg.Query(query, code)
|
|
||||||
|
|
||||||
|
rows, err := pg.Query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
slog.Error("product_images.list.query_failed",
|
slog.Error("product_images.list.query_failed",
|
||||||
"req_id", reqID,
|
"req_id", reqID,
|
||||||
"code", code,
|
"code", code,
|
||||||
@@ -237,142 +164,24 @@ ORDER BY
|
|||||||
"dim3_id", dim3ID,
|
"dim3_id", dim3ID,
|
||||||
"err", err.Error(),
|
"err", err.Error(),
|
||||||
)
|
)
|
||||||
|
|
||||||
http.Error(w, "Gorsel sorgu hatasi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Gorsel sorgu hatasi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
items := make([]ProductImageItem, 0, 16)
|
items := make([]ProductImageItem, 0, 16)
|
||||||
rowDim1ByID := make(map[int64]string, 16)
|
|
||||||
matchedByDim := make([]ProductImageItem, 0, 16)
|
|
||||||
matchedByName := make([]ProductImageItem, 0, 16)
|
|
||||||
matchedByNameDim1Only := make([]ProductImageItem, 0, 16)
|
|
||||||
dim1Upper := strings.ToUpper(strings.TrimSpace(dim1))
|
|
||||||
dim1IDUpper := strings.ToUpper(strings.TrimSpace(dim1ID))
|
|
||||||
dim3Upper := strings.ToUpper(strings.TrimSpace(dim3))
|
|
||||||
dim3IDUpper := strings.ToUpper(strings.TrimSpace(dim3ID))
|
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
|
||||||
var it ProductImageItem
|
var it ProductImageItem
|
||||||
var rowDim1, rowDim2, rowDim3 string
|
if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil {
|
||||||
|
|
||||||
if err := rows.Scan(
|
|
||||||
&it.ID,
|
|
||||||
&it.FileName,
|
|
||||||
&it.FileSize,
|
|
||||||
&it.Storage,
|
|
||||||
&rowDim1,
|
|
||||||
&rowDim2,
|
|
||||||
&rowDim3,
|
|
||||||
); err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
||||||
|
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
|
||||||
|
it.UUID = u
|
||||||
|
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
|
||||||
|
it.FullURL = "/uploads/image/" + u + ".jpg"
|
||||||
|
}
|
||||||
items = append(items, it)
|
items = append(items, it)
|
||||||
rowDim1ByID[it.ID] = strings.TrimSpace(rowDim1)
|
|
||||||
|
|
||||||
dimMatched := true
|
|
||||||
if dim1IDUpper != "" {
|
|
||||||
dimMatched = dimMatched && (rowDim1 == dim1IDUpper)
|
|
||||||
} else if dim1Upper != "" {
|
|
||||||
// Bazı eski kayıtlarda dimval1 gerçek renk kodu yerine numeric id tutulmuş olabilir.
|
|
||||||
// Bu yüzden dimval karşılaştırması yardımcı; asıl fallback file_name token eşleşmesidir.
|
|
||||||
dimMatched = dimMatched && (rowDim1 == dim1Upper)
|
|
||||||
}
|
|
||||||
if dim3IDUpper != "" {
|
|
||||||
dimMatched = dimMatched && (rowDim3 == dim3IDUpper || rowDim2 == dim3IDUpper)
|
|
||||||
} else if dim3Upper != "" {
|
|
||||||
dimMatched = dimMatched && (rowDim3 == dim3Upper || rowDim2 == dim3Upper)
|
|
||||||
}
|
|
||||||
if dimMatched {
|
|
||||||
matchedByDim = append(matchedByDim, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if imageFileMatches(it.FileName, dim1Upper, dim3Upper) {
|
|
||||||
matchedByName = append(matchedByName, it)
|
|
||||||
}
|
|
||||||
if dim1Upper != "" && imageFileMatches(it.FileName, dim1Upper, "") {
|
|
||||||
matchedByNameDim1Only = append(matchedByNameDim1Only, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dim1Upper != "" || dim1IDUpper != "" || dim3Upper != "" || dim3IDUpper != "" {
|
|
||||||
if dim3Upper != "" || dim3IDUpper != "" {
|
|
||||||
// dim3 verildiginde kesin varyant listesi oncelikli.
|
|
||||||
if dim3Upper != "" && len(matchedByName) > 0 {
|
|
||||||
items = matchedByName
|
|
||||||
} else if len(matchedByDim) > 0 {
|
|
||||||
items = matchedByDim
|
|
||||||
} else if dim3Upper != "" && len(matchedByNameDim1Only) > 0 {
|
|
||||||
// dim3 pattern'i olmayan legacy tek-renk isimlerde dim1-only fallback.
|
|
||||||
hasDim3Pattern := false
|
|
||||||
for _, it := range matchedByNameDim1Only {
|
|
||||||
if imageFileHasDim3Pattern(it.FileName) {
|
|
||||||
hasDim3Pattern = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasDim3Pattern {
|
|
||||||
targetDimval1 := make(map[string]struct{}, 4)
|
|
||||||
for _, it := range matchedByNameDim1Only {
|
|
||||||
if dv := rowDim1ByID[it.ID]; dv != "" {
|
|
||||||
targetDimval1[dv] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clustered := make([]ProductImageItem, 0, len(items))
|
|
||||||
for _, it := range items {
|
|
||||||
if _, ok := targetDimval1[rowDim1ByID[it.ID]]; ok {
|
|
||||||
clustered = append(clustered, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = clustered
|
|
||||||
} else {
|
|
||||||
items = []ProductImageItem{}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items = []ProductImageItem{}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if dim1IDUpper != "" && len(matchedByDim) > 0 {
|
|
||||||
items = matchedByDim
|
|
||||||
} else {
|
|
||||||
targetDimval1 := make(map[string]struct{}, 4)
|
|
||||||
for _, it := range matchedByName {
|
|
||||||
if dv := rowDim1ByID[it.ID]; dv != "" {
|
|
||||||
targetDimval1[dv] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(targetDimval1) == 0 {
|
|
||||||
for _, it := range matchedByNameDim1Only {
|
|
||||||
if dv := rowDim1ByID[it.ID]; dv != "" {
|
|
||||||
targetDimval1[dv] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(targetDimval1) > 0 {
|
|
||||||
clustered := make([]ProductImageItem, 0, len(items))
|
|
||||||
for _, it := range items {
|
|
||||||
if _, ok := targetDimval1[rowDim1ByID[it.ID]]; ok && filePrimaryMatchesDim1(it.FileName, dim1Upper) {
|
|
||||||
clustered = append(clustered, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = clustered
|
|
||||||
} else if len(matchedByDim) > 0 {
|
|
||||||
items = matchedByDim
|
|
||||||
} else if len(matchedByName) > 0 {
|
|
||||||
items = matchedByName
|
|
||||||
} else if len(matchedByNameDim1Only) > 0 {
|
|
||||||
items = matchedByNameDim1Only
|
|
||||||
} else {
|
|
||||||
items = []ProductImageItem{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("product_images.list.ok",
|
slog.Info("product_images.list.ok",
|
||||||
@@ -390,35 +199,18 @@ ORDER BY
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// GET IMAGE CONTENT
|
|
||||||
//
|
|
||||||
|
|
||||||
// GET /api/product-images/{id}/content
|
// GET /api/product-images/{id}/content
|
||||||
func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc {
|
func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
|
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
|
||||||
if reqID == "" {
|
if reqID == "" {
|
||||||
reqID = uuid.NewString()
|
reqID = uuid.NewString()
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("X-Request-ID", reqID)
|
w.Header().Set("X-Request-ID", reqID)
|
||||||
|
|
||||||
idStr := mux.Vars(r)["id"]
|
idStr := mux.Vars(r)["id"]
|
||||||
|
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
|
|
||||||
slog.Warn("product_images.content.bad_request",
|
|
||||||
"req_id", reqID,
|
|
||||||
"id_raw", idStr,
|
|
||||||
"path", r.URL.Path,
|
|
||||||
"reason", "invalid_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
http.Error(w, "Gecersiz gorsel id", http.StatusBadRequest)
|
http.Error(w, "Gecersiz gorsel id", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -440,81 +232,43 @@ FROM dfblob
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND typ = 'img'
|
AND typ = 'img'
|
||||||
`, id).Scan(&fileName, &storagePath, &storedInDB, &binData)
|
`, id).Scan(&fileName, &storagePath, &storedInDB, &binData)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
|
||||||
slog.Warn("product_images.content.not_found_row",
|
|
||||||
"req_id", reqID,
|
|
||||||
"id", id,
|
|
||||||
)
|
|
||||||
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Error("product_images.content.query_failed",
|
|
||||||
"req_id", reqID,
|
|
||||||
"id", id,
|
|
||||||
"err", err.Error(),
|
|
||||||
)
|
|
||||||
|
|
||||||
http.Error(w, "Gorsel okunamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Gorsel okunamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB içinde binary saklıysa
|
|
||||||
if storedInDB && len(binData) > 0 {
|
if storedInDB && len(binData) > 0 {
|
||||||
|
|
||||||
w.Header().Set("Content-Type", http.DetectContentType(binData))
|
w.Header().Set("Content-Type", http.DetectContentType(binData))
|
||||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||||
|
|
||||||
_, _ = w.Write(binData)
|
_, _ = w.Write(binData)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved, tried := resolveStoragePath(storagePath)
|
resolved, _ := resolveStoragePath(storagePath)
|
||||||
|
|
||||||
if resolved == "" {
|
if resolved == "" {
|
||||||
|
|
||||||
slog.Warn("product_images.content.file_not_found",
|
|
||||||
"req_id", reqID,
|
|
||||||
"id", id,
|
|
||||||
"file_name", fileName,
|
|
||||||
"storage_path", storagePath,
|
|
||||||
"tried", tried,
|
|
||||||
)
|
|
||||||
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||||
|
|
||||||
http.ServeFile(w, r, resolved)
|
http.ServeFile(w, r, resolved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// FILE PATH RESOLVER
|
|
||||||
//
|
|
||||||
|
|
||||||
func resolveStoragePath(storagePath string) (string, []string) {
|
func resolveStoragePath(storagePath string) (string, []string) {
|
||||||
|
|
||||||
raw := strings.TrimSpace(storagePath)
|
raw := strings.TrimSpace(storagePath)
|
||||||
|
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if i := strings.Index(raw, "?"); i >= 0 {
|
if i := strings.Index(raw, "?"); i >= 0 {
|
||||||
raw = raw[:i]
|
raw = raw[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
raw = strings.ReplaceAll(raw, "\\", "/")
|
raw = strings.ReplaceAll(raw, "\\", "/")
|
||||||
|
|
||||||
if scheme := strings.Index(raw, "://"); scheme >= 0 {
|
if scheme := strings.Index(raw, "://"); scheme >= 0 {
|
||||||
rest := raw[scheme+3:]
|
rest := raw[scheme+3:]
|
||||||
if i := strings.Index(rest, "/"); i >= 0 {
|
if i := strings.Index(rest, "/"); i >= 0 {
|
||||||
@@ -525,11 +279,9 @@ func resolveStoragePath(storagePath string) (string, []string) {
|
|||||||
raw = strings.TrimPrefix(raw, "./")
|
raw = strings.TrimPrefix(raw, "./")
|
||||||
raw = strings.TrimPrefix(raw, "/")
|
raw = strings.TrimPrefix(raw, "/")
|
||||||
raw = strings.TrimPrefix(raw, "uploads/")
|
raw = strings.TrimPrefix(raw, "uploads/")
|
||||||
|
|
||||||
raw = filepath.ToSlash(filepath.Clean(raw))
|
raw = filepath.ToSlash(filepath.Clean(raw))
|
||||||
|
|
||||||
relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
|
relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
|
||||||
|
|
||||||
candidates := []string{
|
candidates := []string{
|
||||||
filepath.Clean(storagePath),
|
filepath.Clean(storagePath),
|
||||||
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
|
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
|
||||||
@@ -541,7 +293,6 @@ func resolveStoragePath(storagePath string) (string, []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
|
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
|
||||||
|
|
||||||
candidates = append(candidates,
|
candidates = append(candidates,
|
||||||
filepath.Join(root, raw),
|
filepath.Join(root, raw),
|
||||||
filepath.Join(root, relUploads),
|
filepath.Join(root, relUploads),
|
||||||
@@ -550,11 +301,9 @@ func resolveStoragePath(storagePath string) (string, []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range candidates {
|
for _, p := range candidates {
|
||||||
|
|
||||||
if p == "" {
|
if p == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||||||
return p, candidates
|
return p, candidates
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,14 +486,7 @@ function resolvePhotoDim3(item, secondColorDisplay = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolvePhotoDim1ID(item) {
|
function resolvePhotoDim1ID(item) {
|
||||||
const candidates = [
|
const candidates = [item?.ItemDim1Code, item?.itemDim1Code, item?.ITEMDIM1CODE, item?.Beden]
|
||||||
item?.ItemDim1Code,
|
|
||||||
item?.itemDim1Code,
|
|
||||||
item?.ITEMDIM1CODE,
|
|
||||||
item?.Beden,
|
|
||||||
item?.RenkID,
|
|
||||||
item?.Renk_Id
|
|
||||||
]
|
|
||||||
for (const value of candidates) {
|
for (const value of candidates) {
|
||||||
const s = String(value || '').trim()
|
const s = String(value || '').trim()
|
||||||
if (/^\d+$/.test(s)) return s
|
if (/^\d+$/.test(s)) return s
|
||||||
@@ -502,12 +495,7 @@ function resolvePhotoDim1ID(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolvePhotoDim3ID(item) {
|
function resolvePhotoDim3ID(item) {
|
||||||
const candidates = [
|
const candidates = [item?.ItemDim3Code, item?.itemDim3Code, item?.ITEMDIM3CODE, item?.Renk2]
|
||||||
item?.ItemDim3Code,
|
|
||||||
item?.itemDim3Code,
|
|
||||||
item?.ITEMDIM3CODE,
|
|
||||||
item?.Renk2
|
|
||||||
]
|
|
||||||
for (const value of candidates) {
|
for (const value of candidates) {
|
||||||
const s = String(value || '').trim()
|
const s = String(value || '').trim()
|
||||||
if (/^\d+$/.test(s)) return s
|
if (/^\d+$/.test(s)) return s
|
||||||
@@ -553,7 +541,7 @@ function normalizeUploadsPath(storagePath) {
|
|||||||
|
|
||||||
function resolveProductImageUrl(item) {
|
function resolveProductImageUrl(item) {
|
||||||
if (!item || typeof item !== 'object') {
|
if (!item || typeof item !== 'object') {
|
||||||
return { contentUrl: '', publicUrl: '' }
|
return { contentUrl: '', publicUrl: '', thumbUrl: '', fullUrl: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentUrl = ''
|
let contentUrl = ''
|
||||||
@@ -577,11 +565,16 @@ function resolveProductImageUrl(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { contentUrl, publicUrl }
|
const thumbUrl = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||||
|
const fullUrl = String(item.full_url || item.fullUrl || '').trim()
|
||||||
|
|
||||||
|
return { contentUrl, publicUrl, thumbUrl, fullUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveProductImageUrlForCarousel(item) {
|
async function resolveProductImageUrlForCarousel(item) {
|
||||||
const resolved = resolveProductImageUrl(item)
|
const resolved = resolveProductImageUrl(item)
|
||||||
|
const fullUrl = String(resolved.fullUrl || '').trim()
|
||||||
|
if (fullUrl) return fullUrl
|
||||||
const contentUrl = String(resolved.contentUrl || '').trim()
|
const contentUrl = String(resolved.contentUrl || '').trim()
|
||||||
if (contentUrl) {
|
if (contentUrl) {
|
||||||
try {
|
try {
|
||||||
@@ -597,7 +590,7 @@ async function resolveProductImageUrlForCarousel(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const publicUrl = String(resolved.publicUrl || '').trim()
|
const publicUrl = String(resolved.publicUrl || '').trim()
|
||||||
return String(publicUrl || contentUrl || '').trim()
|
return String(publicUrl || fullUrl || contentUrl || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
||||||
@@ -670,7 +663,7 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
|
|||||||
|
|
||||||
const resolved = resolveProductImageUrl(first)
|
const resolved = resolveProductImageUrl(first)
|
||||||
|
|
||||||
productImageCache.value[key] = resolved.contentUrl || resolved.publicUrl || ''
|
productImageCache.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.contentUrl || resolved.publicUrl || ''
|
||||||
productImageFallbackByKey.value[key] = resolved.contentUrl || ''
|
productImageFallbackByKey.value[key] = resolved.contentUrl || ''
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err })
|
console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err })
|
||||||
|
|||||||
@@ -479,14 +479,7 @@ function resolvePhotoDim3(item, secondColorDisplay = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolvePhotoDim1ID(item) {
|
function resolvePhotoDim1ID(item) {
|
||||||
const candidates = [
|
const candidates = [item?.ItemDim1Code, item?.itemDim1Code, item?.ITEMDIM1CODE, item?.Beden]
|
||||||
item?.ItemDim1Code,
|
|
||||||
item?.itemDim1Code,
|
|
||||||
item?.ITEMDIM1CODE,
|
|
||||||
item?.Beden,
|
|
||||||
item?.RenkID,
|
|
||||||
item?.Renk_Id
|
|
||||||
]
|
|
||||||
for (const value of candidates) {
|
for (const value of candidates) {
|
||||||
const s = String(value || '').trim()
|
const s = String(value || '').trim()
|
||||||
if (/^\d+$/.test(s)) return s
|
if (/^\d+$/.test(s)) return s
|
||||||
@@ -495,12 +488,7 @@ function resolvePhotoDim1ID(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolvePhotoDim3ID(item) {
|
function resolvePhotoDim3ID(item) {
|
||||||
const candidates = [
|
const candidates = [item?.ItemDim3Code, item?.itemDim3Code, item?.ITEMDIM3CODE, item?.Renk2]
|
||||||
item?.ItemDim3Code,
|
|
||||||
item?.itemDim3Code,
|
|
||||||
item?.ITEMDIM3CODE,
|
|
||||||
item?.Renk2
|
|
||||||
]
|
|
||||||
for (const value of candidates) {
|
for (const value of candidates) {
|
||||||
const s = String(value || '').trim()
|
const s = String(value || '').trim()
|
||||||
if (/^\d+$/.test(s)) return s
|
if (/^\d+$/.test(s)) return s
|
||||||
@@ -542,7 +530,7 @@ function normalizeUploadsPath(storagePath) {
|
|||||||
|
|
||||||
function resolveProductImageUrl(item) {
|
function resolveProductImageUrl(item) {
|
||||||
if (!item || typeof item !== 'object') {
|
if (!item || typeof item !== 'object') {
|
||||||
return { contentUrl: '', publicUrl: '' }
|
return { contentUrl: '', publicUrl: '', thumbUrl: '', fullUrl: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentUrl = ''
|
let contentUrl = ''
|
||||||
@@ -564,11 +552,16 @@ function resolveProductImageUrl(item) {
|
|||||||
if (fileName) publicUrl = `/uploads/image/${fileName}`
|
if (fileName) publicUrl = `/uploads/image/${fileName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return { contentUrl, publicUrl }
|
const thumbUrl = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||||
|
const fullUrl = String(item.full_url || item.fullUrl || '').trim()
|
||||||
|
|
||||||
|
return { contentUrl, publicUrl, thumbUrl, fullUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveProductImageUrlForCarousel(item) {
|
async function resolveProductImageUrlForCarousel(item) {
|
||||||
const resolved = resolveProductImageUrl(item)
|
const resolved = resolveProductImageUrl(item)
|
||||||
|
const fullUrl = String(resolved.fullUrl || '').trim()
|
||||||
|
if (fullUrl) return fullUrl
|
||||||
const contentUrl = String(resolved.contentUrl || '').trim()
|
const contentUrl = String(resolved.contentUrl || '').trim()
|
||||||
if (contentUrl) {
|
if (contentUrl) {
|
||||||
try {
|
try {
|
||||||
@@ -584,7 +577,7 @@ async function resolveProductImageUrlForCarousel(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const publicUrl = String(resolved.publicUrl || '').trim()
|
const publicUrl = String(resolved.publicUrl || '').trim()
|
||||||
return String(publicUrl || contentUrl || '').trim()
|
return String(publicUrl || fullUrl || contentUrl || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
|
||||||
@@ -654,7 +647,7 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
|
|||||||
const first = list[0] || null
|
const first = list[0] || null
|
||||||
|
|
||||||
const resolved = resolveProductImageUrl(first)
|
const resolved = resolveProductImageUrl(first)
|
||||||
productImageCache.value[key] = resolved.contentUrl || resolved.publicUrl || ''
|
productImageCache.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.contentUrl || resolved.publicUrl || ''
|
||||||
productImageFallbackByKey.value[key] = resolved.contentUrl || ''
|
productImageFallbackByKey.value[key] = resolved.contentUrl || ''
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
|
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
|
||||||
|
|||||||
Reference in New Issue
Block a user