446 lines
11 KiB
Go
446 lines
11 KiB
Go
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
|
|
}
|