From 0f9b7549ecec5447a8c801ec793c98dadfe2c0b0 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Tue, 23 Jun 2026 17:17:44 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/product_series_auto_scheduler.go | 140 +++++++++++++++++++++++++-- svc/routes/product_series.go | 106 +++++++++++++++++++- 2 files changed, 234 insertions(+), 12 deletions(-) diff --git a/svc/product_series_auto_scheduler.go b/svc/product_series_auto_scheduler.go index a847ec4..22dda6c 100644 --- a/svc/product_series_auto_scheduler.go +++ b/svc/product_series_auto_scheduler.go @@ -621,27 +621,147 @@ func productSeriesResolvePGVariant(ctx context.Context, pg *sql.DB, productCode, } return 0, 0, sql.NullInt64{}, false, err } - var dim1ID int64 - if err := pg.QueryRowContext(ctx, `SELECT dim_id FROM mk_dim_token_map WHERE dim_column='dimval1' AND token=$1`, strings.TrimSpace(colorCode)).Scan(&dim1ID); err != nil { - if err == sql.ErrNoRows { - return 0, 0, sql.NullInt64{}, false, nil - } + dim1ID, ok, err := productSeriesResolveDimTokenID(ctx, pg, "dimval1", colorCode, mmitemID) + if err != nil { return 0, 0, sql.NullInt64{}, false, err } + if !ok || dim1ID <= 0 { + return 0, 0, sql.NullInt64{}, false, nil + } var dim3ID sql.NullInt64 if strings.TrimSpace(dim3Code) != "" { - var id int64 - if err := pg.QueryRowContext(ctx, `SELECT dim_id FROM mk_dim_token_map WHERE dim_column='dimval3' AND token=$1`, strings.TrimSpace(dim3Code)).Scan(&id); err != nil { - if err == sql.ErrNoRows { - return 0, 0, sql.NullInt64{}, false, nil - } + id, ok, err := productSeriesResolveDimTokenID(ctx, pg, "dimval3", dim3Code, mmitemID) + if err != nil { return 0, 0, sql.NullInt64{}, false, err } + if !ok || id <= 0 { + return 0, 0, sql.NullInt64{}, false, nil + } dim3ID = sql.NullInt64{Int64: id, Valid: true} } return mmitemID, dim1ID, dim3ID, true, nil } +func productSeriesResolveDimTokenID(ctx context.Context, pg *sql.DB, column string, token string, mmitemID int64) (int64, bool, error) { + tok := strings.ToUpper(strings.TrimSpace(token)) + if tok == "" || tok == "0" { + 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 { + return id, id > 0, nil + } + if err != sql.ErrNoRows { + return 0, false, err + } + + // Fallback: infer from dfblob filenames. For dimval3 do not persist globally. + if mmitemID > 0 { + if inferred, ok := productSeriesInferDimIDFromImages(pg, mmitemID, column, tok); ok { + return inferred, true, nil + } + } + v := productSeriesResolveDimvalFromFileNameToken(pg, column, tok, 0) + if v == "" { + return 0, false, nil + } + parsed, perr := strconv.ParseInt(v, 10, 64) + if perr != nil || parsed <= 0 { + return 0, false, nil + } + if column == "dimval1" { + // Persist only for dimval1 where tokens are globally stable. + _, _ = pg.ExecContext(ctx, ` +INSERT INTO mk_dim_token_map (dim_column, token, dim_id, updated_at) +VALUES ($1,$2,$3,now()) +ON CONFLICT (dim_column, token) +DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at +`, column, tok, parsed) + } + return parsed, true, nil +} + +func productSeriesBuildNameLikePatterns(token string) []string { + t := strings.ToUpper(strings.TrimSpace(token)) + if t == "" { + return nil + } + return []string{ + "% " + t + " %", + "%-" + t + "-%", + "%-" + t + "_%", + "%_" + t + "_%", + "%(" + t + ")%", + t + " %", + } +} + +func productSeriesResolveDimvalFromFileNameToken(pg *sql.DB, column, token string, mmitemID int64) string { + patterns := productSeriesBuildNameLikePatterns(token) + if len(patterns) == 0 { + return "" + } + srcFilter := "" + args := []any{patterns[0], patterns[1], patterns[2], patterns[3], patterns[4], patterns[5]} + if mmitemID > 0 { + srcFilter = " AND src_id=$7" + args = append(args, mmitemID) + } + 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, '') <> '' + %s + 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, srcFilter, column) + var v string + if err := pg.QueryRow(query, args...).Scan(&v); err != nil { + return "" + } + v = strings.TrimSpace(v) + if v == "" || v == "0" { + return "" + } + return v +} + +func productSeriesInferDimIDFromImages(pg *sql.DB, mmitemID int64, column, token string) (int64, bool) { + v := productSeriesResolveDimvalFromFileNameToken(pg, column, token, mmitemID) + if v == "" { + return 0, false + } + id, err := strconv.ParseInt(v, 10, 64) + if err != nil || id <= 0 { + return 0, false + } + return id, true +} + func productSeriesClaimQueue(ctx context.Context, tx *sql.Tx, limit int) ([]productSeriesQueueItem, error) { rows, err := tx.QueryContext(ctx, ` WITH picked AS ( diff --git a/svc/routes/product_series.go b/svc/routes/product_series.go index bd2adeb..76b920c 100644 --- a/svc/routes/product_series.go +++ b/svc/routes/product_series.go @@ -297,12 +297,76 @@ func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc { dim3ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval3", setToSortedSlice(dim3Set)) existing, _ := loadProductSeriesAssignments(ctx, pg, codes) + // Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans. + inferCache := map[string]int64{} + inferDim3ForMmitem := func(mmitemID int64, token string) int64 { + mmitemID = int64(mmitemID) + tok := strings.ToUpper(normalizeDimParam(token)) + if mmitemID <= 0 || tok == "" { + return 0 + } + key := fmt.Sprintf("%d|%s", mmitemID, tok) + if v, ok := inferCache[key]; ok { + return v + } + // Use the same pattern approach as resolveDimvalFromFileNameToken, but scoped to src_id. + patterns := buildNameLikePatterns(tok) + if len(patterns) == 0 { + inferCache[key] = 0 + return 0 + } + var dimv string + err := pg.QueryRowContext(ctx, ` +SELECT x.dimv +FROM ( + SELECT COALESCE(dimval3::text, '') AS dimv, COUNT(*) AS cnt + FROM dfblob + WHERE src_table='mmitem' + AND typ='img' + AND src_id=$7 + AND COALESCE(dimval3::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(dimval3::text, '') +) x +ORDER BY x.cnt DESC, x.dimv +LIMIT 1 +`, patterns[0], patterns[1], patterns[2], patterns[3], patterns[4], patterns[5], mmitemID).Scan(&dimv) + if err != nil { + inferCache[key] = 0 + return 0 + } + dimv = strings.TrimSpace(dimv) + if dimv == "" || dimv == "0" { + inferCache[key] = 0 + return 0 + } + id, err := strconv.ParseInt(dimv, 10, 64) + if err != nil || id <= 0 { + inferCache[key] = 0 + return 0 + } + inferCache[key] = id + return id + } + out := make([]productSeriesMappingRow, 0, len(grouped)) for _, row := range grouped { row.MmitemID = mmitemByCode[row.ProductCode] row.Dim1ID = dim1ByToken[row.ColorCode] if row.Dim3Code != "" { - row.Dim3ID = dim3ByToken[row.Dim3Code] + // dimval3 tokens can be ambiguous globally; prefer per-mmitem inference. + 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) if !row.MappingReady { @@ -515,7 +579,45 @@ WHERE dim_column=$1 AND token = ANY($2) } out[strings.TrimSpace(token)] = id } - return out, rows.Err() + if err := rows.Err(); err != nil { + return out, err + } + + // Best-effort fallback: infer missing token->dim_id from dfblob file_name patterns. + // NOTE: For dimval3, the same token can map to different dim ids per product in this + // installation, so we do NOT infer/persist globally here. Per-product inference is + // handled in the row loop (using mmitem_id) to avoid wrong matches. + for _, rawTok := range tokens { + tok := strings.ToUpper(normalizeDimParam(rawTok)) + if tok == "" { + continue + } + if _, ok := out[tok]; ok { + continue + } + if column == "dimval3" { + // avoid global inference for dimval3 + continue + } + v := resolveDimvalFromFileNameToken(pg, column, tok) + if v == "" { + continue + } + id, err := strconv.ParseInt(v, 10, 64) + if err != nil || id <= 0 { + continue + } + // Persist for future requests (best-effort). + _, _ = pg.ExecContext(ctx, ` +INSERT INTO mk_dim_token_map (dim_column, token, dim_id, updated_at) +VALUES ($1,$2,$3,now()) +ON CONFLICT (dim_column, token) +DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at +`, column, tok, id) + out[tok] = id + } + + return out, nil } func loadProductSeriesAssignments(ctx context.Context, pg *sql.DB, codes []string) (map[string][]int64, error) {