Files
bssapp/svc/routes/product_images.go
2026-03-13 16:48:11 +03:00

543 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"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"`
}
func tokenizeImageFileName(fileName string) []string {
up := strings.ToUpper(strings.TrimSpace(fileName))
if up == "" {
return nil
}
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
})
}
func isAllDigits(s string) bool {
if s == "" {
return false
}
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
}
}
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"))
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"))
}
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
}
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,''), '^.*-', ''), ' ', ''))
)
)
ORDER BY
COALESCE(b.sort_order,999999),
b.zlins_dttm DESC,
b.id DESC
`
rows, err := pg.Query(query, code)
if err != nil {
slog.Error("product_images.list.query_failed",
"req_id", reqID,
"code", code,
"dim1", dim1,
"dim3", dim3,
"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))
dim3Upper := strings.ToUpper(strings.TrimSpace(dim3))
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 {
continue
}
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
items = append(items, it)
rowDim1ByID[it.ID] = strings.TrimSpace(rowDim1)
dimMatched := true
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 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 != "" || dim3Upper != "" {
if dim3Upper != "" {
// dim3 verildiginde kesin varyant listesi oncelikli.
if len(matchedByName) > 0 {
items = matchedByName
} else if len(matchedByDim) > 0 {
items = matchedByDim
} else if 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 {
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",
"req_id", reqID,
"code", code,
"dim1", dim1,
"dim3", dim3,
"count", len(items),
)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(items)
}
}
//
// 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
}
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 {
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)
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 {
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
}