fix meilisearch
This commit is contained in:
@@ -585,6 +585,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/meili-search/settings": {
|
||||
"get": {
|
||||
"tags": ["Search"],
|
||||
"summary": "Get MeiliSearch settings",
|
||||
"description": "Returns MeiliSearch configuration and settings. Requires authentication.",
|
||||
"operationId": "getMeiliSearchSettings",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Settings retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/public/auth/me": {
|
||||
"get": {
|
||||
"tags": ["Auth"],
|
||||
@@ -1191,17 +1226,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/menu/get-routes": {
|
||||
"/api/v1/public/menu/get-routes": {
|
||||
"get": {
|
||||
"tags": ["Menu"],
|
||||
"summary": "Get routes",
|
||||
"description": "Returns the routing structure for the current language. Requires authentication.",
|
||||
"description": "Returns the routing structure for the current language.",
|
||||
"operationId": "getRoutes",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Routes retrieved successfully",
|
||||
@@ -1222,16 +1252,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
app/delivery/web/api/public/routing.go
Normal file
45
app/delivery/web/api/public/routing.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/menuService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type RoutingHandler struct {
|
||||
menuService *menuService.MenuService
|
||||
}
|
||||
|
||||
func NewRoutingHandler() *RoutingHandler {
|
||||
menuService := menuService.New()
|
||||
return &RoutingHandler{
|
||||
menuService: menuService,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthHandlerRoutes registers all auth routes
|
||||
func RoutingHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewRoutingHandler()
|
||||
|
||||
r.Get("/get-routes", handler.GetRouting)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *RoutingHandler) GetRouting(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
menu, err := h.menuService.GetRoutes(lang_id)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
@@ -24,7 +24,6 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewMenuHandler()
|
||||
|
||||
r.Get("/get-menu", handler.GetMenu)
|
||||
r.Get("/get-routes", handler.GetRouting)
|
||||
r.Get("/get-top-menu", handler.GetTopMenu)
|
||||
|
||||
return r
|
||||
@@ -45,21 +44,6 @@ func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
||||
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetRouting(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
menu, err := h.menuService.GetRoutes(lang_id)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
if !ok {
|
||||
|
||||
@@ -89,6 +89,9 @@ func (s *Server) Setup() error {
|
||||
auth := s.public.Group("/auth")
|
||||
public.AuthHandlerRoutes(auth)
|
||||
|
||||
menuRouting := s.public.Group("/menu")
|
||||
public.RoutingHandlerRoutes(menuRouting)
|
||||
|
||||
// product translation routes (restricted)
|
||||
productTranslation := s.restricted.Group("/product-translation")
|
||||
restricted.ProductTranslationHandlerRoutes(productTranslation)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// ProductDescription contains all the information visible on webpage, in given language.
|
||||
type ProductDescription struct {
|
||||
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
|
||||
@@ -28,20 +30,30 @@ type ProductRow struct {
|
||||
}
|
||||
|
||||
type MeiliSearchProduct struct {
|
||||
ProductID uint `gorm:"column:id_product"`
|
||||
Name string `gorm:"column:name"`
|
||||
Description string `gorm:"column:description"`
|
||||
DescriptionShort string `gorm:"column:description_short"`
|
||||
Usage string `gorm:"column:used_for"`
|
||||
EAN13 string `gorm:"column:ean13"`
|
||||
Reference string `gorm:"column:reference"`
|
||||
Width float64 `gorm:"column:width"`
|
||||
Height float64 `gorm:"column:height"`
|
||||
Depth float64 `gorm:"column:depth"`
|
||||
Weight float64 `gorm:"column:weight"`
|
||||
CategoryID uint `gorm:"column:id_category"`
|
||||
CategoryName string `gorm:"column:category_name"`
|
||||
Variations uint `gorm:"column:variations"`
|
||||
CategoryIDs []uint `gorm:"-"` // All category IDs including children for filtering
|
||||
CoverImage string `gorm:"-"` // Cover image URL (not indexed)
|
||||
ProductID uint `gorm:"column:id_product" json:"product_id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Active uint8 `gorm:"column:active" json:"active"`
|
||||
Description string `gorm:"column:description" json:"description"`
|
||||
DescriptionShort string `gorm:"column:description_short" json:"description_short"`
|
||||
Usage string `gorm:"column:used_for" json:"usage"`
|
||||
EAN13 string `gorm:"column:ean13" json:"ean13"`
|
||||
Reference string `gorm:"column:reference" json:"reference"`
|
||||
Price float64 `gorm:"column:price" json:"price"`
|
||||
CategoryID uint `gorm:"column:id_category" json:"category_id"`
|
||||
CategoryName string `gorm:"column:category_name" json:"category_name"`
|
||||
Variations uint `gorm:"column:variations" json:"variations"`
|
||||
|
||||
// JSON fields stored as raw, converted to string for search
|
||||
Attributes json.RawMessage `gorm:"column:attributes" json:"attributes"`
|
||||
Features json.RawMessage `gorm:"column:features" json:"features"`
|
||||
AttributeFilters json.RawMessage `gorm:"column:attribute_filters" json:"attribute_filters"`
|
||||
|
||||
// String versions for searchable text (populated during indexing)
|
||||
FeaturesStr string `json:"features_str"`
|
||||
AttributesStr string `json:"attributes_str"`
|
||||
AttributeFiltersStr string `json:"attribute_filters_str"`
|
||||
|
||||
CategoryIDs json.RawMessage `gorm:"column:category_ids" json:"category_ids"` // All category IDs including children for filtering
|
||||
IDImage uint `gorm:"column:id_image" json:"id_image"` // Cover image ID (not indexed)
|
||||
CoverImage string `json:"cover_image"` // Cover image URL (populated during indexing)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
package model
|
||||
|
||||
type Route struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"type:varchar(255);not null;unique"`
|
||||
Path *string `gorm:"type:varchar(255);default:null"`
|
||||
Component string `gorm:"type:varchar(255);not null;comment:path to component file"`
|
||||
Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"`
|
||||
Meta *string `gorm:"type:longtext;default:'{}'"`
|
||||
IsActive *bool `gorm:"type:tinyint;default:1"`
|
||||
SortOrder *int `gorm:"type:int;default:0"`
|
||||
|
||||
ParentID *uint `gorm:"index"`
|
||||
Parent *Route `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:SET NULL;foreignKey:ParentID"`
|
||||
|
||||
Children []Route `gorm:"foreignKey:ParentID"`
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"type:varchar(255);not null;unique" json:"name"`
|
||||
Path *string `gorm:"type:varchar(255);default:null" json:"path,omitempty"`
|
||||
Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"`
|
||||
Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"`
|
||||
Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"`
|
||||
SortOrder *int `gorm:"type:int;default:0" json:"sort_order,omitempty"`
|
||||
}
|
||||
|
||||
func (Route) TableName() string {
|
||||
|
||||
@@ -6,7 +6,7 @@ type B2BTopMenu struct {
|
||||
MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"`
|
||||
Label json.RawMessage `gorm:"column:label;type:longtext;not null;default:'{}'" json:"label"`
|
||||
ParentID *int `gorm:"column:parent_id;index:FK_b2b_top_menu_parent_id" json:"parent_id,omitempty"`
|
||||
Params string `gorm:"column:params;type:longtext;not null;default:'{}'" json:"params"`
|
||||
Params json.RawMessage `gorm:"column:params;type:longtext;not null;default:'{}'" json:"params"`
|
||||
Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"`
|
||||
Position int `gorm:"column:position;not null;default:1" json:"position"`
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type UIProductDescriptionRepo interface {
|
||||
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
|
||||
CreateIfDoesNotExist(productID uint, productLangID uint) error
|
||||
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
|
||||
GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error)
|
||||
CreateIfDoesNotExist(productID uint, productid_lang uint) error
|
||||
UpdateFields(productID uint, productid_lang uint, updates map[string]string) error
|
||||
GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error)
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ func New() UIProductDescriptionRepo {
|
||||
}
|
||||
|
||||
// We assume that any user has access to all product descriptions
|
||||
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error) {
|
||||
var ProductDescription model.ProductDescription
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product_lang").
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productid_lang).
|
||||
First(&ProductDescription).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database error: %w", err)
|
||||
@@ -37,16 +37,16 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLa
|
||||
}
|
||||
|
||||
// If it doesn't exist, returns an error.
|
||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
|
||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_lang uint) error {
|
||||
record := model.ProductDescription{
|
||||
ProductID: productID,
|
||||
ShopID: constdata.SHOP_ID,
|
||||
LangID: productLangID,
|
||||
LangID: productid_lang,
|
||||
}
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product_lang").
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productid_lang).
|
||||
FirstOrCreate(&record).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
@@ -55,7 +55,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLan
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error {
|
||||
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productid_lang uint, updates map[string]string) error {
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint
|
||||
|
||||
err := db.DB.
|
||||
Table("ps_product_lang").
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productid_lang).
|
||||
Updates(updatesIface).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
@@ -79,96 +79,123 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint
|
||||
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) {
|
||||
var products []model.MeiliSearchProduct
|
||||
|
||||
err := db.DB.Debug().
|
||||
// Select(`
|
||||
// ps.id_product AS id_product,
|
||||
// pl.name AS name,
|
||||
// ps.price AS price,
|
||||
// pl.description AS description,
|
||||
// pl.description_short AS description_short,
|
||||
// pl.usage AS used_for,
|
||||
// p.ean13 AS ean13,
|
||||
// p.reference AS reference,
|
||||
// p.width AS width,
|
||||
// p.height AS height,
|
||||
// p.depth AS depth,
|
||||
// p.weight AS weight,
|
||||
// ps.id_category_default AS id_category,
|
||||
// cl.name AS category_name,
|
||||
// COUNT(DISTINCT pas.id_product_attribute) AS variations
|
||||
// `).
|
||||
// Table("ps_product_shop AS ps").
|
||||
// Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||
// Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product").
|
||||
// Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||
// Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID).
|
||||
// Where("ps.id_shop = ? AND ps.active = 1", constdata.SHOP_ID).
|
||||
// Group("ps.id_product").
|
||||
|
||||
Select(`
|
||||
ps.id_product AS id_product,
|
||||
pl.name AS name,
|
||||
pl.description AS description,
|
||||
pl.description_short AS description_short,
|
||||
pl.usage AS used_for,
|
||||
p.ean13 AS ean13,
|
||||
p.reference AS reference,
|
||||
query := db.Get().Debug().Raw(`
|
||||
WITH products_page AS (
|
||||
SELECT ps.id_product, ps.price
|
||||
FROM ps_product_shop ps
|
||||
WHERE ps.id_shop = ? AND ps.active = 1
|
||||
),
|
||||
variation_attributes AS (
|
||||
SELECT pas.id_product, pagl.public_name AS attribute_name,
|
||||
JSON_ARRAYAGG(DISTINCT pal.name) AS attribute_values
|
||||
FROM ps_product_attribute_shop pas
|
||||
JOIN ps_product_attribute_combination ppac
|
||||
ON ppac.id_product_attribute = pas.id_product_attribute
|
||||
JOIN ps_attribute_lang pal
|
||||
ON pal.id_attribute = ppac.id_attribute AND pal.id_lang = ?
|
||||
JOIN ps_attribute pa
|
||||
ON pa.id_attribute = ppac.id_attribute
|
||||
JOIN ps_attribute_group pag
|
||||
ON pag.id_attribute_group = pa.id_attribute_group
|
||||
JOIN ps_attribute_group_lang pagl
|
||||
ON pagl.id_attribute_group = pag.id_attribute_group AND pagl.id_lang = ?
|
||||
WHERE pas.id_shop = ?
|
||||
GROUP BY pas.id_product, pagl.public_name
|
||||
),
|
||||
variations AS (
|
||||
SELECT id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes
|
||||
FROM variation_attributes
|
||||
GROUP BY id_product
|
||||
),
|
||||
variation_attribute_filters AS (
|
||||
SELECT pas.id_product,
|
||||
JSON_ARRAYAGG(
|
||||
DISTINCT CONCAT(
|
||||
LOWER(REPLACE(CAST(pagl.public_name AS CHAR) COLLATE utf8mb4_unicode_ci, ' ', '_')),
|
||||
':',
|
||||
LOWER(REPLACE(CAST(pal.name AS CHAR) COLLATE utf8mb4_unicode_ci, ' ', '_'))
|
||||
)
|
||||
) AS attribute_filters
|
||||
FROM ps_product_attribute_shop pas
|
||||
JOIN ps_product_attribute_combination ppac
|
||||
ON ppac.id_product_attribute = pas.id_product_attribute
|
||||
JOIN ps_attribute_lang pal
|
||||
ON pal.id_attribute = ppac.id_attribute AND pal.id_lang = ?
|
||||
JOIN ps_attribute pa
|
||||
ON pa.id_attribute = ppac.id_attribute
|
||||
JOIN ps_attribute_group pag
|
||||
ON pag.id_attribute_group = pa.id_attribute_group
|
||||
JOIN ps_attribute_group_lang pagl
|
||||
ON pagl.id_attribute_group = pag.id_attribute_group AND pagl.id_lang = ?
|
||||
WHERE pas.id_shop = ?
|
||||
GROUP BY pas.id_product
|
||||
),
|
||||
features AS (
|
||||
SELECT pfp.id_product, JSON_OBJECTAGG(pfl.name, pfvl.value) AS features
|
||||
FROM ps_feature_product pfp
|
||||
JOIN ps_feature_lang pfl
|
||||
ON pfl.id_feature = pfp.id_feature AND pfl.id_lang = ?
|
||||
JOIN ps_feature_value_lang pfvl
|
||||
ON pfvl.id_feature_value = pfp.id_feature_value AND pfvl.id_lang = ?
|
||||
GROUP BY pfp.id_product
|
||||
),
|
||||
images AS (
|
||||
SELECT id_product, id_image
|
||||
FROM ps_image_shop
|
||||
WHERE id_shop = ? AND cover = 1
|
||||
),
|
||||
categories AS (
|
||||
SELECT id_product, JSON_ARRAYAGG(id_category) AS category_ids
|
||||
FROM ps_category_product
|
||||
GROUP BY id_product
|
||||
)
|
||||
SELECT pp.id_product,
|
||||
pl.name,
|
||||
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description,
|
||||
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description_short,
|
||||
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.usage, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS used_for,
|
||||
p.ean13,
|
||||
p.reference,
|
||||
pp.price,
|
||||
ps.id_category_default AS id_category,
|
||||
cl.name AS category_name,
|
||||
cl.link_rewrite as link_rewrite,
|
||||
COUNT(DISTINCT pas.id_product_attribute) AS variations,
|
||||
pis.id_image AS id_image,
|
||||
GROUP_CONCAT(DISTINCT pcp.id_category) AS category_ids
|
||||
`).
|
||||
Table("ps_product_shop AS ps").
|
||||
Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||
Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product").
|
||||
Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||
Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID).
|
||||
Joins("JOIN ps_image_shop AS pis ON pis.id_product = ps.id_product AND pis.cover = 1").
|
||||
Joins("JOIN ps_category_product AS pcp ON pcp.id_product = ps.id_product").
|
||||
Where("ps.id_shop = ? AND ps.active = 1", constdata.SHOP_ID).
|
||||
Group("pcp.id_product, ps.id_product").
|
||||
Scan(&products).Error
|
||||
if err != nil {
|
||||
cl.link_rewrite,
|
||||
COALESCE(vary.attributes, JSON_OBJECT()) AS attributes,
|
||||
COALESCE(vaf.attribute_filters, JSON_ARRAY()) AS attribute_filters,
|
||||
COALESCE(feat.features, JSON_OBJECT()) AS features,
|
||||
img.id_image,
|
||||
cat.category_ids,
|
||||
(SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = pp.id_product AND pas2.id_shop = ?) AS variations
|
||||
FROM products_page pp
|
||||
JOIN ps_product_shop ps ON ps.id_product = pp.id_product
|
||||
JOIN ps_product_lang pl
|
||||
ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?
|
||||
JOIN ps_product p ON p.id_product = ps.id_product
|
||||
JOIN ps_category_lang cl
|
||||
ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?
|
||||
LEFT JOIN variations vary ON vary.id_product = ps.id_product
|
||||
LEFT JOIN variation_attribute_filters vaf ON vaf.id_product = ps.id_product
|
||||
LEFT JOIN features feat ON feat.id_product = ps.id_product
|
||||
LEFT JOIN images img ON img.id_product = ps.id_product
|
||||
LEFT JOIN categories cat ON cat.id_product = ps.id_product
|
||||
ORDER BY ps.id_product
|
||||
`,
|
||||
constdata.SHOP_ID, // products_page
|
||||
id_lang, id_lang, // variation_attributes pal.id_lang, pagl.id_lang
|
||||
constdata.SHOP_ID, // variation_attributes pas.id_shop
|
||||
id_lang, id_lang, // variation_attribute_filters pal.id_lang, pagl.id_lang
|
||||
constdata.SHOP_ID, // variation_attribute_filters pas.id_shop
|
||||
id_lang, id_lang, // features pfl.id_lang, pfvl.id_lang
|
||||
constdata.SHOP_ID, // images id_shop
|
||||
constdata.SHOP_ID, // variation count subquery
|
||||
constdata.SHOP_ID, // ps_product_lang pl.id_shop
|
||||
id_lang, // ps_product_lang pl.id_lang
|
||||
constdata.SHOP_ID, // ps_category_lang cl.id_shop
|
||||
id_lang, // ps_category_lang cl.id_lang
|
||||
)
|
||||
if err := query.Scan(&products).Error; err != nil {
|
||||
return products, fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
|
||||
// Get all category IDs for each product (including child categories)
|
||||
for i := range products {
|
||||
var categoryIDs []uint
|
||||
// Find all parent categories and their children using nested set
|
||||
err := db.DB.
|
||||
Table("ps_category AS c").
|
||||
Select("c.id_category").
|
||||
Joins("JOIN ps_category_product AS cp ON cp.id_category = c.id_category").
|
||||
Joins("JOIN ps_category AS parent ON c.nleft >= parent.nleft AND c.nright <= parent.nright AND parent.id_category = ?", products[i].CategoryID).
|
||||
Where("cp.id_product = ?", products[i].ProductID).
|
||||
Group("c.id_category").
|
||||
Pluck("c.id_category", &categoryIDs).Error
|
||||
if err != nil {
|
||||
continue // Skip if error, use default category
|
||||
}
|
||||
if len(categoryIDs) > 0 {
|
||||
products[i].CategoryIDs = categoryIDs
|
||||
} else {
|
||||
products[i].CategoryIDs = []uint{products[i].CategoryID}
|
||||
}
|
||||
|
||||
// Get cover image for the product
|
||||
var imageID int
|
||||
err = db.DB.
|
||||
Table("ps_image AS i").
|
||||
Select("i.id_image").
|
||||
Joins("LEFT JOIN ps_image_shop AS iss ON iss.id_image = i.id_image AND iss.id_shop = ?", constdata.SHOP_ID).
|
||||
Where("i.id_product = ? AND (i.cover = 1 OR i.cover IS TRUE)", products[i].ProductID).
|
||||
Order("i.position ASC").
|
||||
Limit(1).
|
||||
Pluck("i.id_image", &imageID).Error
|
||||
if err == nil && imageID > 0 {
|
||||
products[i].CoverImage = fmt.Sprintf("%d/%d.jpg", products[i].ProductID, imageID)
|
||||
}
|
||||
}
|
||||
|
||||
return products, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package routesrepo
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||
)
|
||||
|
||||
type UIRoutesRepo interface {
|
||||
@@ -18,7 +19,7 @@ func New() UIRoutesRepo {
|
||||
|
||||
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
|
||||
routes := []model.Route{}
|
||||
err := db.DB.Find(&routes).Error
|
||||
err := db.DB.Find(&routes, model.Route{Active: nullable.GetNil(true)}).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package meiliService
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -31,81 +30,145 @@ func New() *MeiliService {
|
||||
}
|
||||
}
|
||||
|
||||
func getIndexName(id_lang uint) string {
|
||||
return fmt.Sprintf("shop_%d_lang_%d", constdata.SHOP_ID, id_lang)
|
||||
}
|
||||
|
||||
// ==================================== FOR TESTING ONLY ====================================
|
||||
func (s *MeiliService) CreateIndex(id_lang uint) error {
|
||||
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||
indexName := getIndexName(id_lang)
|
||||
|
||||
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
|
||||
for i := 0; i < len(products); i++ {
|
||||
products[i].Description = cleanHTML(products[i].Description)
|
||||
products[i].DescriptionShort = cleanHTML(products[i].DescriptionShort)
|
||||
products[i].Usage = cleanHTML(products[i].Usage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get products: %w", err)
|
||||
}
|
||||
|
||||
primaryKey := "ProductID"
|
||||
if len(products) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process products: prepare for indexing
|
||||
for i := range products {
|
||||
// Convert JSON fields to searchable strings
|
||||
if len(products[i].Features) > 0 {
|
||||
products[i].FeaturesStr = string(products[i].Features)
|
||||
}
|
||||
if len(products[i].Attributes) > 0 {
|
||||
products[i].AttributesStr = string(products[i].Attributes)
|
||||
}
|
||||
if len(products[i].AttributeFilters) > 0 {
|
||||
products[i].AttributeFiltersStr = string(products[i].AttributeFilters)
|
||||
}
|
||||
|
||||
// Build cover image URL from image ID
|
||||
if products[i].IDImage > 0 {
|
||||
products[i].CoverImage = config.Get().Image.ImagePrefix + fmt.Sprintf("/%d", products[i].IDImage)
|
||||
}
|
||||
}
|
||||
|
||||
// Add documents to index
|
||||
primaryKey := "product_id"
|
||||
docOptions := &meilisearch.DocumentOptions{
|
||||
PrimaryKey: &primaryKey,
|
||||
SkipCreation: false,
|
||||
}
|
||||
|
||||
task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||
return fmt.Errorf("failed to add documents: %w", err)
|
||||
}
|
||||
finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||
|
||||
finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for task: %w", err)
|
||||
}
|
||||
if finishedTask.Status == "failed" {
|
||||
return fmt.Errorf("task failed: %v", finishedTask.Error)
|
||||
}
|
||||
|
||||
// Configure filterable attributes
|
||||
filterableAttributes := []interface{}{
|
||||
"CategoryID",
|
||||
"CategoryIDs",
|
||||
"product_id",
|
||||
"category_id",
|
||||
"category_ids",
|
||||
"active",
|
||||
"attribute_filters",
|
||||
"features",
|
||||
}
|
||||
task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||
return fmt.Errorf("failed to update filterable attributes: %w", err)
|
||||
}
|
||||
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for filterable task: %w", err)
|
||||
}
|
||||
finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||
|
||||
// Configure sortable attributes
|
||||
sortableAttributes := []string{
|
||||
"price",
|
||||
"name",
|
||||
"product_id",
|
||||
"category_ids",
|
||||
}
|
||||
task, err = s.meiliClient.Index(indexName).UpdateSortableAttributes(&sortableAttributes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update sortable attributes: %w", err)
|
||||
}
|
||||
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for sortable task: %w", err)
|
||||
}
|
||||
|
||||
// Configure displayed attributes
|
||||
displayedAttributes := []string{
|
||||
"ProductID",
|
||||
"Name",
|
||||
"EAN13",
|
||||
"Reference",
|
||||
"Variations",
|
||||
"CoverImage",
|
||||
"product_id",
|
||||
"name",
|
||||
"ean13",
|
||||
"reference",
|
||||
"variations",
|
||||
"id_image",
|
||||
"price",
|
||||
"category_name",
|
||||
"category_ids",
|
||||
"attribute_filters",
|
||||
}
|
||||
task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||
return fmt.Errorf("failed to update displayed attributes: %w", err)
|
||||
}
|
||||
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for displayed task: %w", err)
|
||||
}
|
||||
finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||
|
||||
// Configure searchable attributes
|
||||
searchableAttributes := []string{
|
||||
"Name",
|
||||
"DescriptionShort",
|
||||
"Reference",
|
||||
"EAN13",
|
||||
"CategoryName",
|
||||
"Description",
|
||||
"Usage",
|
||||
"name",
|
||||
"description",
|
||||
"description_short",
|
||||
"usage",
|
||||
"features_str",
|
||||
"attributes_str",
|
||||
"reference",
|
||||
"ean13",
|
||||
"category_name",
|
||||
}
|
||||
task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||
return fmt.Errorf("failed to update searchable attributes: %w", err)
|
||||
}
|
||||
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for searchable task: %w", err)
|
||||
}
|
||||
finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================================== FOR TESTING ONLY ====================================
|
||||
func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) {
|
||||
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||
indexName := getIndexName(id_lang)
|
||||
|
||||
searchReq := &meilisearch.SearchRequest{
|
||||
Limit: 4,
|
||||
@@ -128,7 +191,7 @@ func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) {
|
||||
|
||||
// Search performs a full-text search on the specified index
|
||||
func (s *MeiliService) Search(id_lang uint, query string, id_category uint) (meilisearch.SearchResponse, error) {
|
||||
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||
indexName := getIndexName(id_lang)
|
||||
|
||||
filter_query := "Active = 1"
|
||||
if id_category != 0 {
|
||||
@@ -165,7 +228,7 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
|
||||
|
||||
// GetIndexSettings retrieves the current settings for a specific index
|
||||
func (s *MeiliService) GetIndexSettings(id_lang uint) (map[string]interface{}, error) {
|
||||
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||
indexName := getIndexName(id_lang)
|
||||
|
||||
index := s.meiliClient.Index(indexName)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user