diff --git a/svc/product_series_auto_scheduler.go b/svc/product_series_auto_scheduler.go index 01b7a2e..cd44224 100644 --- a/svc/product_series_auto_scheduler.go +++ b/svc/product_series_auto_scheduler.go @@ -109,6 +109,15 @@ func productSeriesFallbackLogEnabled() bool { return raw == "1" || raw == "true" || raw == "on" || raw == "yes" } +func productSeriesFallbackOverrideExisting() bool { + // Default off: do not override existing (possibly manual) assignments. + raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_SERIES_FALLBACK_OVERRIDE_EXISTING"))) + if raw == "" { + return false + } + return raw == "1" || raw == "true" || raw == "on" || raw == "yes" +} + func productSeriesFallbackCode() string { code := strings.TrimSpace(os.Getenv("PRODUCT_SERIES_FALLBACK_SERIES_CODE")) if code == "" { @@ -505,9 +514,10 @@ func productSeriesApplyVariant(ctx context.Context, pg *sql.DB, v productSeriesA return 0, 1, err } if fallbackID > 0 { - // Only apply fallback when the variant has no assignments yet. - var exists int - checkErr := pg.QueryRowContext(ctx, ` + if !productSeriesFallbackOverrideExisting() { + // Only apply fallback when the variant has no assignments yet. + var exists int + checkErr := pg.QueryRowContext(ctx, ` SELECT 1 FROM zbggseri WHERE mmitem_id=$1 @@ -515,18 +525,23 @@ WHERE mmitem_id=$1 AND (($3::bigint IS NULL AND dim3 IS NULL) OR dim3=$3::bigint) LIMIT 1 `, mmitemID, dim1ID, nullableInt64ForAuto(dim3ID)).Scan(&exists) - if checkErr == nil { - if productSeriesFallbackLogEnabled() { - log.Printf("[ProductSeriesFallback] already_exists product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID) + if checkErr == nil { + if productSeriesFallbackLogEnabled() { + log.Printf("[ProductSeriesFallback] already_exists product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID) + } + // keep existing manual/previous assignment; nothing to do + return 0, 0, nil + } + if checkErr != nil && checkErr != sql.ErrNoRows { + return 0, 1, checkErr } - // keep existing manual/previous assignment; nothing to do - return 0, 0, nil - } - if checkErr != nil && checkErr != sql.ErrNoRows { - return 0, 1, checkErr } if productSeriesFallbackLogEnabled() { - log.Printf("[ProductSeriesFallback] apply product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID) + if productSeriesFallbackOverrideExisting() { + log.Printf("[ProductSeriesFallback] override_apply product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID) + } else { + log.Printf("[ProductSeriesFallback] apply product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID) + } } // Use the fallback series as the single selected rule. selected = []productSeriesAutoRule{{SeriesID: fallbackID}} @@ -834,14 +849,6 @@ func productSeriesResolveDimTokenID(ctx context.Context, pg *sql.DB, column stri return 0, false, nil } - // dimval3 tokens like "001" can map to different dim ids per product in this installation. - // Prefer per-mmitem inference from dfblob (src_id filter) to avoid global mk_dim_token_map mismatches. - if column == "dimval3" && mmitemID > 0 { - if inferred, ok := productSeriesInferDimIDFromImages(pg, mmitemID, column, tok); ok { - return inferred, true, nil - } - } - var id int64 err := pg.QueryRowContext(ctx, `SELECT dim_id FROM mk_dim_token_map WHERE dim_column=$1 AND token=$2`, column, tok).Scan(&id) if err == nil { @@ -851,8 +858,9 @@ func productSeriesResolveDimTokenID(ctx context.Context, pg *sql.DB, column stri return 0, false, err } - // Fallback: infer from dfblob filenames. For dimval3 do not persist globally. - if mmitemID > 0 { + // Fallback: infer from dfblob filenames. + // For dimval3, prefer token map as the source of truth when present; use image inference only when missing. + if column == "dimval3" && mmitemID > 0 { if inferred, ok := productSeriesInferDimIDFromImages(pg, mmitemID, column, tok); ok { return inferred, true, nil } diff --git a/svc/routes/product_series.go b/svc/routes/product_series.go index b6da31c..76eab0e 100644 --- a/svc/routes/product_series.go +++ b/svc/routes/product_series.go @@ -361,11 +361,11 @@ LIMIT 1 row.MmitemID = mmitemByCode[row.ProductCode] row.Dim1ID = dim1ByToken[row.ColorCode] if row.Dim3Code != "" { - // dimval3 tokens can be ambiguous globally; prefer per-mmitem inference. - if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 { + // dimval3 can be ambiguous, but if mk_dim_token_map has a row we treat it as source of truth. + if v := dim3ByToken[row.Dim3Code]; v > 0 { + row.Dim3ID = v + } else if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 { row.Dim3ID = inferred - } else { - row.Dim3ID = dim3ByToken[row.Dim3Code] } } row.MappingReady = row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)