mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-20 16:51:03 -05:00
finished cleaning up all this wild db heavy api calls.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package group.goforward.battlbuilder.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
||||
|
||||
import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO;
|
||||
|
||||
@Configuration
|
||||
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
|
||||
public class SpringDataWebConfig {
|
||||
}
|
||||
@@ -32,7 +32,23 @@ public class CatalogController {
|
||||
@RequestParam(required = false) String q,
|
||||
Pageable pageable
|
||||
) {
|
||||
return catalogQueryService.getOptions(platform, partRole, partRoles, brands, q, pageable);
|
||||
Pageable safe = sanitizeCatalogPageable(pageable);
|
||||
return catalogQueryService.getOptions(platform, partRole, partRoles, brands, q, safe);
|
||||
}
|
||||
|
||||
private Pageable sanitizeCatalogPageable(Pageable pageable) {
|
||||
int page = Math.max(0, pageable.getPageNumber());
|
||||
|
||||
// hard cap to keep UI snappy + protect DB
|
||||
int requested = pageable.getPageSize();
|
||||
int size = Math.min(Math.max(requested, 1), 48); // 48 max
|
||||
|
||||
// default sort if none provided
|
||||
Sort sort = pageable.getSort().isSorted()
|
||||
? pageable.getSort()
|
||||
: Sort.by(Sort.Direction.DESC, "updatedAt");
|
||||
|
||||
return PageRequest.of(page, size, sort);
|
||||
}
|
||||
|
||||
@PostMapping("/products/by-ids")
|
||||
|
||||
@@ -4,14 +4,14 @@ import group.goforward.battlbuilder.model.ImportStatus;
|
||||
import group.goforward.battlbuilder.model.Brand;
|
||||
import group.goforward.battlbuilder.model.Product;
|
||||
|
||||
import group.goforward.battlbuilder.repos.projections.CatalogRow;
|
||||
import group.goforward.battlbuilder.web.dto.catalog.CatalogOptionRow;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -21,7 +21,12 @@ public interface ProductRepository
|
||||
extends JpaRepository<Product, Integer>,
|
||||
JpaSpecificationExecutor<Product> {
|
||||
|
||||
@EntityGraph(attributePaths = {"brand"})
|
||||
List<Product> findByIdIn(Collection<Integer> ids);
|
||||
|
||||
@Override
|
||||
@EntityGraph(attributePaths = {"brand"})
|
||||
Page<Product> findAll(Specification<Product> spec, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Catalog mapping UI:
|
||||
@@ -533,4 +538,95 @@ ORDER BY productCount DESC
|
||||
limit :limit
|
||||
""", nativeQuery = true)
|
||||
List<Product> findProductsMissingCaliberGroup(@Param("limit") int limit);
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------
|
||||
// Deliver 1 Product and 1 Best Offer
|
||||
// -------------------------------------------------
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select
|
||||
p.id as id,
|
||||
p.name as name,
|
||||
p.platform as platform,
|
||||
p.part_role as partRole,
|
||||
coalesce(p.battl_image_url, p.main_image_url) as imageUrl,
|
||||
b.name as brand,
|
||||
|
||||
o.price as price,
|
||||
o.buy_url as buyUrl,
|
||||
o.in_stock as inStock
|
||||
from products p
|
||||
join brands b on b.id = p.brand_id
|
||||
left join lateral (
|
||||
select po.price, po.buy_url, po.in_stock
|
||||
from product_offers po
|
||||
where po.product_id = p.id
|
||||
order by
|
||||
case when po.in_stock then 0 else 1 end,
|
||||
po.price asc nulls last,
|
||||
po.last_seen_at desc
|
||||
limit 1
|
||||
) o on true
|
||||
where p.deleted_at is null
|
||||
and p.status = 'ACTIVE'
|
||||
and p.visibility = 'PUBLIC'
|
||||
and p.builder_eligible = true
|
||||
and (:platform is null or p.platform = :platform)
|
||||
and (
|
||||
(:partRole is null and (:partRoles is null or cardinality(:partRoles) = 0))
|
||||
or (:partRole is not null and p.part_role = :partRole)
|
||||
or (:partRoles is not null and p.part_role = any(:partRoles))
|
||||
)
|
||||
and (
|
||||
(:brands is null or cardinality(:brands) = 0)
|
||||
or b.name = any(:brands)
|
||||
)
|
||||
and (
|
||||
:q is null
|
||||
or lower(p.name) like lower(concat('%', :q, '%'))
|
||||
or lower(b.name) like lower(concat('%', :q, '%'))
|
||||
)
|
||||
""",
|
||||
countQuery = """
|
||||
select count(*)
|
||||
from products p
|
||||
join brands b on b.id = p.brand_id
|
||||
where p.deleted_at is null
|
||||
and p.status = 'ACTIVE'
|
||||
and p.visibility = 'PUBLIC'
|
||||
and p.builder_eligible = true
|
||||
and (:platform is null or p.platform = :platform)
|
||||
and (
|
||||
(:partRole is null and (:partRoles is null or cardinality(:partRoles) = 0))
|
||||
or (:partRole is not null and p.part_role = :partRole)
|
||||
or (:partRoles is not null and p.part_role = any(:partRoles))
|
||||
)
|
||||
and (
|
||||
(:brands is null or cardinality(:brands) = 0)
|
||||
or b.name = any(:brands)
|
||||
)
|
||||
and (
|
||||
:q is null
|
||||
or lower(p.name) like lower(concat('%', :q, '%'))
|
||||
or lower(b.name) like lower(concat('%', :q, '%'))
|
||||
)
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
Page<CatalogRow> searchCatalog(
|
||||
@Param("platform") String platform,
|
||||
@Param("partRole") String partRole,
|
||||
@Param("partRoles") String[] partRoles,
|
||||
@Param("brands") String[] brands,
|
||||
@Param("q") String q,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package group.goforward.battlbuilder.repos.projections;
|
||||
|
||||
public interface CatalogRow {
|
||||
Long getId();
|
||||
String getName();
|
||||
String getPlatform();
|
||||
String getPartRole();
|
||||
String getImageUrl(); // or mainImageUrl depending on your schema
|
||||
String getBrand();
|
||||
|
||||
Double getPrice();
|
||||
String getBuyUrl();
|
||||
Boolean getInStock();
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -93,7 +94,7 @@ public class CatalogQueryServiceImpl implements CatalogQueryService {
|
||||
|
||||
ids = ids.stream().filter(Objects::nonNull).distinct().toList();
|
||||
|
||||
List<Product> products = productRepository.findAllById(ids);
|
||||
List<Product> products = productRepository.findByIdIn(ids);
|
||||
if (products.isEmpty()) return List.of();
|
||||
|
||||
List<ProductOffer> offers = productOfferRepository.findByProduct_IdIn(ids);
|
||||
@@ -119,7 +120,7 @@ public class CatalogQueryServiceImpl implements CatalogQueryService {
|
||||
|
||||
private Map<Integer, ProductOffer> pickBestOffers(List<ProductOffer> offers) {
|
||||
Map<Integer, ProductOffer> best = new HashMap<>();
|
||||
if (offers == null) return best;
|
||||
if (offers == null || offers.isEmpty()) return best;
|
||||
|
||||
for (ProductOffer o : offers) {
|
||||
if (o == null || o.getProduct() == null || o.getProduct().getId() == null) continue;
|
||||
@@ -134,9 +135,40 @@ public class CatalogQueryServiceImpl implements CatalogQueryService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- ranking rules (in order) ----
|
||||
// 1) prefer in-stock
|
||||
boolean oStock = Boolean.TRUE.equals(o.getInStock());
|
||||
boolean cStock = Boolean.TRUE.equals(current.getInStock());
|
||||
if (oStock != cStock) {
|
||||
if (oStock) best.put(pid, o);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) prefer cheaper price
|
||||
BigDecimal currentPrice = current.getEffectivePrice();
|
||||
if (currentPrice == null || price.compareTo(currentPrice) < 0) {
|
||||
best.put(pid, o);
|
||||
continue;
|
||||
}
|
||||
if (price.compareTo(currentPrice) > 0) continue;
|
||||
|
||||
// 3) tie-break: most recently seen
|
||||
OffsetDateTime oSeen = o.getLastSeenAt();
|
||||
OffsetDateTime cSeen = current.getLastSeenAt();
|
||||
|
||||
if (oSeen != null && cSeen != null && oSeen.isAfter(cSeen)) {
|
||||
best.put(pid, o);
|
||||
continue;
|
||||
}
|
||||
if (oSeen != null && cSeen == null) {
|
||||
best.put(pid, o);
|
||||
}
|
||||
|
||||
// 4) tie-break: prefer offer with buyUrl
|
||||
String oUrl = o.getBuyUrl();
|
||||
String cUrl = current.getBuyUrl();
|
||||
if ((oUrl != null && !oUrl.isBlank()) && (cUrl == null || cUrl.isBlank())) {
|
||||
best.put(pid, o);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +181,8 @@ public class CatalogQueryServiceImpl implements CatalogQueryService {
|
||||
}
|
||||
|
||||
int page = pageable.getPageNumber();
|
||||
int size = pageable.getPageSize();
|
||||
int requested = pageable.getPageSize();
|
||||
int size = Math.min(Math.max(requested, 1), 48); // ✅ hard cap
|
||||
|
||||
// Default sort if none provided
|
||||
if (pageable.getSort() == null || pageable.getSort().isUnsorted()) {
|
||||
|
||||
Reference in New Issue
Block a user