Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 05:16:31 +03:00
parent 48bde09c2c
commit 27934bccdf
5 changed files with 486 additions and 162 deletions
+131 -29
View File
@@ -3465,11 +3465,13 @@ LIMIT $1
}
type ProductPerformanceGroupedRequest struct {
Mode string
GroupLevels []string
ExpandedKeys map[string]bool
Limit int
MainGroup string
Mode string
GroupLevels []string
ExpandedKeys map[string]bool
ExpandThroughLevel int
Limit int
MainGroup string
Filters map[string][]string
}
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
@@ -3480,6 +3482,9 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
if len(levels) == 0 {
levels = defaultProductPerformanceGroupLevels(req.Mode)
}
if req.ExpandThroughLevel > len(levels)-2 {
req.ExpandThroughLevel = len(levels) - 2
}
if out, ok, err := listProductPerformanceGroupedSQL(ctx, pg, req, levels); ok || err != nil {
return out, err
@@ -3489,14 +3494,16 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
if err != nil {
return nil, err
}
sourceRows = filterProductPerformanceGroupedRows(sourceRows, req.Filters)
out := make([]map[string]any, 0, len(sourceRows))
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys)
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
return out, nil
}
type productPerformanceSQLGroupFilter struct {
Field string
Value string
Field string
Value string
Values []string
}
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
@@ -3509,25 +3516,28 @@ func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req Produ
}
out := make([]map[string]any, 0, 512)
filters := productPerformanceGroupedBaseFilters(req)
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.Limit)
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
return out, true, err
}
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
filters := make([]productPerformanceSQLGroupFilter, 0, len(req.Filters)+1)
mainGroup := strings.TrimSpace(req.MainGroup)
if strings.TrimSpace(req.Mode) != "product_detail" || mainGroup == "" {
return nil
if strings.TrimSpace(req.Mode) == "product_detail" && mainGroup != "" {
filters = append(filters, productPerformanceSQLGroupFilter{Field: "urun_ana_grubu", Value: mainGroup})
}
return []productPerformanceSQLGroupFilter{{Field: "urun_ana_grubu", Value: mainGroup}}
for field, values := range req.Filters {
cleanValues := cleanProductPerformanceFilterValues(values)
if len(cleanValues) == 0 {
continue
}
filters = append(filters, productPerformanceSQLGroupFilter{Field: field, Values: cleanValues})
}
return filters
}
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, limit int) error {
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, expandThroughLevel int, limit int) error {
if level >= len(levels) {
rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit)
if err != nil {
return err
}
*out = append(*out, rows...)
return nil
}
@@ -3540,7 +3550,7 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out
value := stringFromMap(row, "group_value")
if shouldSkipProductPerformanceGroupValue(field, value) {
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel, parentKeys, nextFilters, expandedKeys, limit); err != nil {
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel, parentKeys, nextFilters, expandedKeys, expandThroughLevel, limit); err != nil {
return err
}
continue
@@ -3556,9 +3566,9 @@ func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out
row["__group"] = true
row[field] = value
*out = append(*out, row)
if expandedKeys[key] {
if expandedKeys[key] || level <= expandThroughLevel {
nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel+1, append(parentKeys, keyPart), nextFilters, expandedKeys, limit); err != nil {
if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel+1, append(parentKeys, keyPart), nextFilters, expandedKeys, expandThroughLevel, limit); err != nil {
return err
}
}
@@ -3889,7 +3899,9 @@ func productPerformanceSQLGroupExpr(field string) (string, bool) {
return "CASE WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE COALESCE(urun_ilk_grubu,'') END", true
case "askili_yan":
return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END", true
case "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu":
case "urun_ana_grubu":
return "COALESCE(NULLIF(btrim(COALESCE(urun_ana_grubu,'')), ''), NULLIF(btrim(COALESCE(urun_alt_grubu,'')), ''), NULLIF(btrim(COALESCE(kategori,'')), ''), '')", true
case "kategori", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu", "performance_bucket":
return field, true
case "color_yaka":
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
@@ -3920,12 +3932,93 @@ func productPerformanceSQLFilterWhere(filters []productPerformanceSQLGroupFilter
if !ok {
return "", nil, fmt.Errorf("unsupported product performance filter field: %s", filter.Field)
}
args = append(args, normalizeProductPerformanceGroupValue(filter.Value))
parts = append(parts, fmt.Sprintf("COALESCE(%s, '') = $%d", expr, len(args)))
values := filter.Values
if len(values) == 0 && strings.TrimSpace(filter.Value) != "" {
values = []string{filter.Value}
}
values = cleanProductPerformanceFilterValues(values)
if len(values) == 0 {
continue
}
args = append(args, pq.Array(values))
parts = append(parts, fmt.Sprintf("COALESCE(%s, '') = ANY($%d)", expr, len(args)))
}
if len(parts) == 0 {
return "", nil, nil
}
return "WHERE " + strings.Join(parts, " AND "), args, nil
}
func cleanProductPerformanceFilterValues(values []string) []string {
out := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
clean := normalizeProductPerformanceGroupValue(value)
if clean == "" || seen[clean] {
continue
}
seen[clean] = true
out = append(out, clean)
if len(out) >= 300 {
break
}
}
return out
}
func filterProductPerformanceGroupedRows(rows []map[string]any, filters map[string][]string) []map[string]any {
if len(filters) == 0 || len(rows) == 0 {
return rows
}
cleanFilters := map[string]map[string]bool{}
for field, values := range filters {
cleanValues := cleanProductPerformanceFilterValues(values)
if len(cleanValues) == 0 {
continue
}
set := map[string]bool{}
for _, value := range cleanValues {
set[value] = true
}
cleanFilters[field] = set
}
if len(cleanFilters) == 0 {
return rows
}
out := make([]map[string]any, 0, len(rows))
for _, row := range rows {
matches := true
for field, allowed := range cleanFilters {
if !allowed[productPerformanceGroupedFilterValue(row, field)] {
matches = false
break
}
}
if matches {
out = append(out, row)
}
}
return out
}
func productPerformanceGroupedFilterValue(row map[string]any, field string) string {
switch field {
case "urun_ana_grubu":
value := strings.TrimSpace(stringFromMap(row, "urun_ana_grubu"))
if value != "" {
return normalizeProductPerformanceGroupValue(value)
}
if value = strings.TrimSpace(stringFromMap(row, "urun_alt_grubu")); value != "" {
return normalizeProductPerformanceGroupValue(value)
}
return normalizeProductPerformanceGroupValue(stringFromMap(row, "kategori"))
case "market_key", "color_yaka", "urun_ilk_grubu", "askili_yan":
return normalizeProductPerformanceGroupValue(mapGroupValue(row, field))
default:
return normalizeProductPerformanceGroupValue(stringFromMap(row, field))
}
}
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
switch mode {
case "products":
@@ -3986,9 +4079,8 @@ func productPerformanceIdleSourceRows(rows []map[string]any) []map[string]any {
return out
}
func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map[string]any, levels []string, level int, visualLevel int, parentKeys []string, expandedKeys map[string]bool) {
func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map[string]any, levels []string, level int, visualLevel int, parentKeys []string, expandedKeys map[string]bool, expandThroughLevel int) {
if level >= len(levels) {
*out = append(*out, sourceRows...)
return
}
@@ -4010,14 +4102,14 @@ func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map
for _, value := range values {
groupRows := grouped[value]
if shouldSkipProductPerformanceGroupValue(field, value) {
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel, parentKeys, expandedKeys)
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel, parentKeys, expandedKeys, expandThroughLevel)
continue
}
keyPart := field + ":" + value
key := strings.Join(append(parentKeys, keyPart), "|")
*out = append(*out, makeProductPerformanceGroupedRow(key, visualLevel, field, value, groupRows))
if expandedKeys[key] {
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel+1, append(parentKeys, keyPart), expandedKeys)
if expandedKeys[key] || level <= expandThroughLevel {
appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel+1, append(parentKeys, keyPart), expandedKeys, expandThroughLevel)
}
}
}
@@ -4076,6 +4168,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
if key == "row_key" || key == "key" {
continue
}
if isProductPerformanceMarginField(key) {
continue
}
if key == "stock_qty" {
out[key] = distinctProductPerformanceVariantStockQty(rows)
continue
@@ -4093,6 +4188,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
for _, row := range rows {
for key := range row {
if isProductPerformanceMarginField(key) {
continue
}
if shouldAverageProductPerformanceField(key) {
out[key] = weightedAverageProductPerformanceRows(rows, key, productPerformanceMetricWeightField(key))
}
@@ -4103,6 +4201,10 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
return out
}
func isProductPerformanceMarginField(field string) bool {
return strings.HasPrefix(field, "gross_margin")
}
func distinctProductPerformanceVariantStockQty(rows []map[string]any) float64 {
seen := map[string]bool{}
total := 0.0
+16 -7
View File
@@ -141,18 +141,14 @@ func enrichProductImageItem(it *ProductImageItem) {
if it == nil {
return
}
if it.ID > 0 {
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"
}
if it.StoredInDB {
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
return
}
if resolved, _ := resolveStoragePath(it.Storage); resolved != "" {
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
}
}
// POST /api/product-images/batch
@@ -576,14 +572,25 @@ func resolveStoragePath(storagePath string) (string, []string) {
raw = filepath.ToSlash(filepath.Clean(raw))
relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
baseName := filepath.Base(raw)
relImage := filepath.FromSlash(filepath.Join("uploads", "image", baseName))
relImageThumb := filepath.FromSlash(filepath.Join("uploads", "image", "t300", baseName))
candidates := []string{
filepath.Clean(storagePath),
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
filepath.FromSlash(filepath.Clean(raw)),
relUploads,
relImage,
relImageThumb,
filepath.Join(".", relUploads),
filepath.Join(".", relImage),
filepath.Join(".", relImageThumb),
filepath.Join("..", relUploads),
filepath.Join("..", relImage),
filepath.Join("..", relImageThumb),
filepath.Join("..", "..", relUploads),
filepath.Join("..", "..", relImage),
filepath.Join("..", "..", relImageThumb),
}
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
@@ -591,6 +598,8 @@ func resolveStoragePath(storagePath string) (string, []string) {
filepath.Join(root, raw),
filepath.Join(root, relUploads),
filepath.Join(root, "uploads", raw),
filepath.Join(root, relImage),
filepath.Join(root, relImageThumb),
)
}
+28 -11
View File
@@ -273,7 +273,14 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
limit := intQuery(r, "limit", 50000)
expanded := map[string]bool{}
const maxExpandedGroupKeys = 260
filters := map[string][]string{}
expandThroughLevel := -1
if rawExpandLevel := strings.TrimSpace(r.URL.Query().Get("expand_through_level")); rawExpandLevel != "" {
if parsed, err := strconv.Atoi(rawExpandLevel); err == nil {
expandThroughLevel = parsed
}
}
const maxExpandedGroupKeys = 1000
addExpandedKey := func(key string) {
key = strings.TrimSpace(key)
if key != "" && len(expanded) < maxExpandedGroupKeys {
@@ -282,11 +289,13 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
}
if r.Method == http.MethodPost {
var body struct {
Mode string `json:"mode"`
GroupLevels []string `json:"group_levels"`
ExpandedKeys []string `json:"expanded_keys"`
Limit int `json:"limit"`
MainGroup string `json:"urun_ana_grubu"`
Mode string `json:"mode"`
GroupLevels []string `json:"group_levels"`
ExpandedKeys []string `json:"expanded_keys"`
ExpandThroughLevel *int `json:"expand_through_level"`
Limit int `json:"limit"`
MainGroup string `json:"urun_ana_grubu"`
Filters map[string][]string `json:"filters"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
@@ -307,6 +316,12 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
for _, key := range body.ExpandedKeys {
addExpandedKey(key)
}
if body.ExpandThroughLevel != nil {
expandThroughLevel = *body.ExpandThroughLevel
}
if len(body.Filters) > 0 {
filters = body.Filters
}
} else {
for _, key := range strings.Split(r.URL.Query().Get("expanded_keys"), ",") {
addExpandedKey(key)
@@ -314,11 +329,13 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
}
rows, err := queries.ListProductPerformanceGrouped(ctx, pg, queries.ProductPerformanceGroupedRequest{
Mode: mode,
GroupLevels: groupLevels,
ExpandedKeys: expanded,
Limit: limit,
MainGroup: mainGroup,
Mode: mode,
GroupLevels: groupLevels,
ExpandedKeys: expanded,
ExpandThroughLevel: expandThroughLevel,
Limit: limit,
MainGroup: mainGroup,
Filters: filters,
})
if err != nil {
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)