package routes import ( "database/sql" "encoding/json" "fmt" "log/slog" "net/http" "os" "path/filepath" "regexp" "strconv" "strings" "github.com/google/uuid" "github.com/gorilla/mux" ) type ProductImageItem struct { ID int64 `json:"id"` FileName string `json:"file_name"` 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"` } 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 s } func uniqueNonEmpty(items ...string) []string { out := make([]string, 0, len(items)) seen := make(map[string]struct{}, len(items)) for _, it := range items { v := normalizeDimParam(it) if v == "" { continue } if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} out = append(out, v) } return out } func buildNameLikePatterns(token string) []string { t := strings.ToUpper(strings.TrimSpace(token)) if t == "" { return nil } return []string{ "% " + t + " %", "%-" + t + "-%", "%-" + t + "_%", "%_" + t + "_%", "%(" + t + ")%", t + " %", } } func resolveDimvalFromFileNameToken(pg *sql.DB, column, token string) string { patterns := buildNameLikePatterns(token) if len(patterns) == 0 { return "" } query := fmt.Sprintf(` SELECT x.dimv FROM ( SELECT COALESCE(%s::text, '') AS dimv, COUNT(*) AS cnt FROM dfblob WHERE src_table='mmitem' AND typ='img' AND COALESCE(%s::text, '') <> '' AND ( UPPER(COALESCE(file_name,'')) LIKE $1 OR UPPER(COALESCE(file_name,'')) LIKE $2 OR UPPER(COALESCE(file_name,'')) LIKE $3 OR UPPER(COALESCE(file_name,'')) LIKE $4 OR UPPER(COALESCE(file_name,'')) LIKE $5 OR UPPER(COALESCE(file_name,'')) LIKE $6 ) GROUP BY COALESCE(%s::text, '') ) x ORDER BY x.cnt DESC, x.dimv LIMIT 1 `, column, column, column) var v string if err := pg.QueryRow(query, patterns[0], patterns[1], patterns[2], patterns[3], patterns[4], patterns[5], ).Scan(&v); err != nil { return "" } return normalizeDimParam(v) } func extractImageUUID(storagePath, fileName string) string { if m := uuidPattern.FindString(storagePath); m != "" { return strings.ToLower(m) } if m := uuidPattern.FindString(fileName); m != "" { return strings.ToLower(m) } return "" } // 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")) dim1 := strings.TrimSpace(r.URL.Query().Get("dim1")) if dim1 == "" { dim1 = strings.TrimSpace(r.URL.Query().Get("color")) } dim3 := strings.TrimSpace(r.URL.Query().Get("dim3")) if dim3 == "" { dim3 = strings.TrimSpace(r.URL.Query().Get("yaka")) } 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 == "" { 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 } runQuery := func(dim1Filter, dim3Filter string) ([]ProductImageItem, error) { query := ` SELECT 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(sort_order,999999), zlins_dttm DESC, id DESC` rows, err := pg.Query(query, args...) if err != nil { return nil, err } defer rows.Close() items := make([]ProductImageItem, 0, 16) for rows.Next() { var it ProductImageItem 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) } if err := rows.Err(); err != nil { return nil, err } return items, nil } // Rule: // dim1!=0 && dim3!=0 => dimval1=dim1 AND dimval3=dim3 // dim1!=0 && dim3==0 => dimval1=dim1 // dim1==0 && dim3==0 => generic photos // // Frontend'den yanlis dim id gelebildigi icin: // 1) once *_id ile deneriz // 2) sonuc yoksa kod degeriyle fallback deneriz. resolvedDim1ID := normalizeDimParam(dim1ID) if resolvedDim1ID == "" && normalizeDimParam(dim1) != "" { resolvedDim1ID = resolveDimvalFromFileNameToken(pg, "dimval1", dim1) } resolvedDim3ID := normalizeDimParam(dim3ID) if resolvedDim3ID == "" && normalizeDimParam(dim3) != "" { resolvedDim3ID = resolveDimvalFromFileNameToken(pg, "dimval3", dim3) } dim1Candidates := uniqueNonEmpty(resolvedDim1ID, dim1ID, dim1) if len(dim1Candidates) == 0 { dim1Candidates = []string{""} } dim3Candidates := uniqueNonEmpty(resolvedDim3ID, dim3ID, dim3) items := make([]ProductImageItem, 0, 16) selectedDim1 := "" selectedDim3 := "" var queryErr error for _, d1 := range dim1Candidates { localDim3Candidates := []string{""} if d1 != "" { if len(dim3Candidates) > 0 { localDim3Candidates = append([]string{}, dim3Candidates...) localDim3Candidates = append(localDim3Candidates, "") } } for _, d3 := range localDim3Candidates { var runErr error items, runErr = runQuery(d1, d3) if runErr != nil { queryErr = runErr continue } if len(items) > 0 { selectedDim1 = d1 selectedDim3 = d3 break } if selectedDim1 == "" && selectedDim3 == "" { selectedDim1 = d1 selectedDim3 = d3 } } if len(items) > 0 { break } } if queryErr != nil && len(items) == 0 { slog.Error("product_images.list.query_failed", "req_id", reqID, "code", code, "dim1", dim1, "dim1_id", dim1ID, "dim3", dim3, "dim3_id", dim3ID, "err", queryErr.Error(), ) http.Error(w, "Gorsel sorgu hatasi: "+queryErr.Error(), http.StatusInternalServerError) return } slog.Info("product_images.list.ok", "req_id", reqID, "code", code, "dim1", dim1, "dim1_id", dim1ID, "resolved_dim1_id", resolvedDim1ID, "dim3", dim3, "dim3_id", dim3ID, "resolved_dim3_id", resolvedDim3ID, "selected_dim1", selectedDim1, "selected_dim3", selectedDim3, "count", len(items), ) w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(items) } } // 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 { http.Error(w, "Gecersiz gorsel id", http.StatusBadRequest) return } var ( fileName string storagePath string storedInDB bool binData []byte ) err = pg.QueryRow(` SELECT 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 { http.NotFound(w, r) return } http.Error(w, "Gorsel okunamadi: "+err.Error(), http.StatusInternalServerError) return } 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, _ := resolveStoragePath(storagePath) if resolved == "" { http.NotFound(w, r) return } w.Header().Set("Cache-Control", "public, max-age=3600") http.ServeFile(w, r, resolved) } } 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 { raw = rest[i:] } } 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, "/"))), filepath.FromSlash(filepath.Clean(raw)), relUploads, filepath.Join(".", relUploads), filepath.Join("..", relUploads), filepath.Join("..", "..", relUploads), } if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" { candidates = append(candidates, filepath.Join(root, raw), filepath.Join(root, relUploads), filepath.Join(root, "uploads", raw), ) } for _, p := range candidates { if p == "" { continue } if st, err := os.Stat(p); err == nil && !st.IsDir() { return p, candidates } } return "", candidates }