diff --git a/svc/routes/product_images.go b/svc/routes/product_images.go index a7b7632..9f83f85 100644 --- a/svc/routes/product_images.go +++ b/svc/routes/product_images.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "strconv" "strings" @@ -21,139 +22,38 @@ type ProductImageItem struct { FileSize int64 `json:"file_size"` Storage string `json:"storage_path"` 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 { - up := strings.ToUpper(strings.TrimSpace(fileName)) - if up == "" { - return nil +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}`) + +func normalizeDimParam(v string) string { + s := strings.TrimSpace(v) + if s == "" || s == "0" { + return "" } - return strings.FieldsFunc(up, func(r rune) bool { - isUpper := r >= 'A' && r <= 'Z' - isDigit := r >= '0' && r <= '9' - if isUpper || isDigit || r == '_' { - return false - } - return true - }) + return s } -func isAllDigits(s string) bool { - if s == "" { - return false +func extractImageUUID(storagePath, fileName string) string { + if m := uuidPattern.FindString(storagePath); m != "" { + 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 "" } -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=... func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - reqID := strings.TrimSpace(r.Header.Get("X-Request-ID")) if reqID == "" { reqID = uuid.NewString() } - w.Header().Set("X-Request-ID", reqID) code := strings.TrimSpace(r.URL.Query().Get("code")) @@ -161,10 +61,6 @@ func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { if dim1 == "" { 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")) if dim3 == "" { dim3 = strings.TrimSpace(r.URL.Query().Get("yaka")) @@ -172,62 +68,93 @@ func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { if dim3 == "" { 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")) if dim3ID == "" { dim3ID = strings.TrimSpace(r.URL.Query().Get("itemdim3")) } 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) 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 := ` SELECT - b.id, - b.file_name, - COALESCE(b.file_size,0) AS file_size, - COALESCE(b.storage_path,'') AS storage_path, - UPPER(COALESCE(b.dimval1::text,'')) AS dimval1, - UPPER(COALESCE(b.dimval2::text,'')) AS dimval2, - UPPER(COALESCE(b.dimval3::text,'')) AS dimval3 -FROM dfblob b -JOIN mmitem i - ON i.id = b.src_id -WHERE b.typ = 'img' - AND b.src_table = 'mmitem' - AND ( - -- exact code match (spaces ignored) - UPPER(REPLACE(COALESCE(i.code,''), ' ', '')) = UPPER(REPLACE(COALESCE($1,''), ' ', '')) - -- core-code fallback only when there is no exact item code for the input - OR ( - NOT EXISTS ( - SELECT 1 - FROM mmitem ix - WHERE UPPER(REPLACE(COALESCE(ix.code,''), ' ', '')) = UPPER(REPLACE(COALESCE($1,''), ' ', '')) - ) - AND UPPER(REPLACE(REGEXP_REPLACE(COALESCE(i.code,''), '^.*-', ''), ' ', '')) = - UPPER(REPLACE(REGEXP_REPLACE(COALESCE($1,''), '^.*-', ''), ' ', '')) - ) - ) + id, + COALESCE(file_name,'') AS file_name, + COALESCE(file_size,0) AS file_size, + COALESCE(storage_path,'') AS storage_path +FROM dfblob +WHERE typ='img' + AND src_table='mmitem' + AND src_id=$1` + args := []interface{}{mmItemID} + argPos := 2 + if dim1Filter != "" { + query += fmt.Sprintf(" AND COALESCE(dimval1::text,'') = $%d", argPos) + args = append(args, dim1Filter) + argPos++ + if dim3Filter != "" { + query += fmt.Sprintf(" AND COALESCE(dimval3::text,'') = $%d", argPos) + args = append(args, dim3Filter) + argPos++ + } + } + query += ` ORDER BY - COALESCE(b.sort_order,999999), - b.zlins_dttm DESC, - b.id DESC -` - - rows, err := pg.Query(query, code) + COALESCE(sort_order,999999), + zlins_dttm DESC, + id DESC` + rows, err := pg.Query(query, args...) if err != nil { - slog.Error("product_images.list.query_failed", "req_id", reqID, "code", code, @@ -237,142 +164,24 @@ ORDER BY "dim3_id", dim3ID, "err", err.Error(), ) - http.Error(w, "Gorsel sorgu hatasi: "+err.Error(), http.StatusInternalServerError) return } - defer rows.Close() 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() { - var it ProductImageItem - var rowDim1, rowDim2, rowDim3 string - - if err := rows.Scan( - &it.ID, - &it.FileName, - &it.FileSize, - &it.Storage, - &rowDim1, - &rowDim2, - &rowDim3, - ); err != nil { + if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil { continue } - 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) - 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", @@ -390,35 +199,18 @@ ORDER BY } } -// -// GET IMAGE CONTENT -// - // GET /api/product-images/{id}/content func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - reqID := strings.TrimSpace(r.Header.Get("X-Request-ID")) if reqID == "" { reqID = uuid.NewString() } - w.Header().Set("X-Request-ID", reqID) idStr := mux.Vars(r)["id"] - id, err := strconv.ParseInt(idStr, 10, 64) - 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) return } @@ -432,89 +224,51 @@ func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc { err = pg.QueryRow(` SELECT - COALESCE(file_name,''), - COALESCE(storage_path,''), - COALESCE(stored_in_db,false), - bin + COALESCE(file_name,''), + COALESCE(storage_path,''), + COALESCE(stored_in_db,false), + bin FROM dfblob WHERE id = $1 AND typ = 'img' `, id).Scan(&fileName, &storagePath, &storedInDB, &binData) - if err != nil { - if err == sql.ErrNoRows { - - slog.Warn("product_images.content.not_found_row", - "req_id", reqID, - "id", id, - ) - http.NotFound(w, r) 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) return } - // DB içinde binary saklıysa if storedInDB && len(binData) > 0 { - w.Header().Set("Content-Type", http.DetectContentType(binData)) w.Header().Set("Cache-Control", "public, max-age=3600") - _, _ = w.Write(binData) - return } - resolved, tried := resolveStoragePath(storagePath) - + resolved, _ := resolveStoragePath(storagePath) 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) return } w.Header().Set("Cache-Control", "public, max-age=3600") - http.ServeFile(w, r, resolved) } } -// -// FILE PATH RESOLVER -// - func resolveStoragePath(storagePath string) (string, []string) { - raw := strings.TrimSpace(storagePath) - if raw == "" { return "", nil } - if i := strings.Index(raw, "?"); i >= 0 { raw = raw[:i] } raw = strings.ReplaceAll(raw, "\\", "/") - if scheme := strings.Index(raw, "://"); scheme >= 0 { rest := raw[scheme+3:] 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, "uploads/") - raw = filepath.ToSlash(filepath.Clean(raw)) relUploads := filepath.FromSlash(filepath.Join("uploads", raw)) - candidates := []string{ filepath.Clean(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 != "" { - candidates = append(candidates, filepath.Join(root, raw), filepath.Join(root, relUploads), @@ -550,11 +301,9 @@ func resolveStoragePath(storagePath string) (string, []string) { } for _, p := range candidates { - if p == "" { continue } - if st, err := os.Stat(p); err == nil && !st.IsDir() { return p, candidates } diff --git a/ui/src/pages/ProductStockByAttributes.vue b/ui/src/pages/ProductStockByAttributes.vue index 3c6beec..1ce4af1 100644 --- a/ui/src/pages/ProductStockByAttributes.vue +++ b/ui/src/pages/ProductStockByAttributes.vue @@ -486,14 +486,7 @@ function resolvePhotoDim3(item, secondColorDisplay = '') { } function resolvePhotoDim1ID(item) { - const candidates = [ - item?.ItemDim1Code, - item?.itemDim1Code, - item?.ITEMDIM1CODE, - item?.Beden, - item?.RenkID, - item?.Renk_Id - ] + const candidates = [item?.ItemDim1Code, item?.itemDim1Code, item?.ITEMDIM1CODE, item?.Beden] for (const value of candidates) { const s = String(value || '').trim() if (/^\d+$/.test(s)) return s @@ -502,12 +495,7 @@ function resolvePhotoDim1ID(item) { } function resolvePhotoDim3ID(item) { - const candidates = [ - item?.ItemDim3Code, - item?.itemDim3Code, - item?.ITEMDIM3CODE, - item?.Renk2 - ] + const candidates = [item?.ItemDim3Code, item?.itemDim3Code, item?.ITEMDIM3CODE, item?.Renk2] for (const value of candidates) { const s = String(value || '').trim() if (/^\d+$/.test(s)) return s @@ -553,7 +541,7 @@ function normalizeUploadsPath(storagePath) { function resolveProductImageUrl(item) { if (!item || typeof item !== 'object') { - return { contentUrl: '', publicUrl: '' } + return { contentUrl: '', publicUrl: '', thumbUrl: '', fullUrl: '' } } 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) { const resolved = resolveProductImageUrl(item) + const fullUrl = String(resolved.fullUrl || '').trim() + if (fullUrl) return fullUrl const contentUrl = String(resolved.contentUrl || '').trim() if (contentUrl) { try { @@ -597,7 +590,7 @@ async function resolveProductImageUrlForCarousel(item) { } } const publicUrl = String(resolved.publicUrl || '').trim() - return String(publicUrl || contentUrl || '').trim() + return String(publicUrl || fullUrl || contentUrl || '').trim() } function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') { @@ -670,7 +663,7 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di 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 || '' } catch (err) { console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err }) diff --git a/ui/src/pages/ProductStockQuery.vue b/ui/src/pages/ProductStockQuery.vue index 8474c06..71922f2 100644 --- a/ui/src/pages/ProductStockQuery.vue +++ b/ui/src/pages/ProductStockQuery.vue @@ -479,14 +479,7 @@ function resolvePhotoDim3(item, secondColorDisplay = '') { } function resolvePhotoDim1ID(item) { - const candidates = [ - item?.ItemDim1Code, - item?.itemDim1Code, - item?.ITEMDIM1CODE, - item?.Beden, - item?.RenkID, - item?.Renk_Id - ] + const candidates = [item?.ItemDim1Code, item?.itemDim1Code, item?.ITEMDIM1CODE, item?.Beden] for (const value of candidates) { const s = String(value || '').trim() if (/^\d+$/.test(s)) return s @@ -495,12 +488,7 @@ function resolvePhotoDim1ID(item) { } function resolvePhotoDim3ID(item) { - const candidates = [ - item?.ItemDim3Code, - item?.itemDim3Code, - item?.ITEMDIM3CODE, - item?.Renk2 - ] + const candidates = [item?.ItemDim3Code, item?.itemDim3Code, item?.ITEMDIM3CODE, item?.Renk2] for (const value of candidates) { const s = String(value || '').trim() if (/^\d+$/.test(s)) return s @@ -542,7 +530,7 @@ function normalizeUploadsPath(storagePath) { function resolveProductImageUrl(item) { if (!item || typeof item !== 'object') { - return { contentUrl: '', publicUrl: '' } + return { contentUrl: '', publicUrl: '', thumbUrl: '', fullUrl: '' } } let contentUrl = '' @@ -564,11 +552,16 @@ function resolveProductImageUrl(item) { 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) { const resolved = resolveProductImageUrl(item) + const fullUrl = String(resolved.fullUrl || '').trim() + if (fullUrl) return fullUrl const contentUrl = String(resolved.contentUrl || '').trim() if (contentUrl) { try { @@ -584,7 +577,7 @@ async function resolveProductImageUrlForCarousel(item) { } } const publicUrl = String(resolved.publicUrl || '').trim() - return String(publicUrl || contentUrl || '').trim() + return String(publicUrl || fullUrl || contentUrl || '').trim() } 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 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 || '' } catch (err) { console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })