|
|
|
@ -79,6 +79,7 @@ import java.util.stream.Collectors; |
|
|
|
public class ShopController { |
|
|
|
private static final String GROUP_BUY_DEFAULT_PAGE_CACHE_PREFIX = "GROUP_BUY_DEFAULT_SHOP_PAGE:"; |
|
|
|
private static final long GROUP_BUY_DEFAULT_PAGE_CACHE_SECONDS = 30L; |
|
|
|
private static final long SHOP_LIST_SLOW_THRESHOLD_MS = 10L; |
|
|
|
private static final DateTimeFormatter BUSINESS_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); |
|
|
|
private static final String PRINTING_METHOD_BLUETOOTH = "bluetooth"; |
|
|
|
private static final String PRINTING_METHOD_CLOUD = "cloud"; |
|
|
|
@ -458,7 +459,7 @@ public class ShopController { |
|
|
|
@ApiOperation("多条件分页获取公司列表") |
|
|
|
public Result<Page<Shop>> getByCondition(Shop shop, |
|
|
|
PageVo pageVo) { |
|
|
|
long queryStartTime = System.currentTimeMillis(); |
|
|
|
ShopListStageTimer timer = new ShopListStageTimer(); |
|
|
|
String regionId = shop.getRegionId(); |
|
|
|
boolean adminQuery = CharSequenceUtil.isNotBlank(shop.getShopIcon()); |
|
|
|
boolean hasKeyword = hasSearchKeyword(shop); |
|
|
|
@ -466,17 +467,23 @@ public class ShopController { |
|
|
|
&& !hasKeyword |
|
|
|
&& CharSequenceUtil.isBlank(shop.getShopName()); |
|
|
|
String defaultPageCacheKey = buildDefaultGroupBuyPageCacheKey(shop, pageVo, adminQuery); |
|
|
|
timer.note("pageCacheKey", defaultPageCacheKey == null ? "null" : defaultPageCacheKey); |
|
|
|
if (CharSequenceUtil.isNotBlank(defaultPageCacheKey)) { |
|
|
|
String cacheJson = redisTemplateHelper.get(defaultPageCacheKey); |
|
|
|
timer.mark("pageCacheGet"); |
|
|
|
timer.note("pageCacheBytes", cacheJson == null ? 0 : cacheJson.length()); |
|
|
|
if (CharSequenceUtil.isNotBlank(cacheJson)) { |
|
|
|
try { |
|
|
|
ShopListPageCache cache = JSONUtil.toBean(cacheJson, ShopListPageCache.class); |
|
|
|
timer.mark("pageCacheParse"); |
|
|
|
if (cache != null && cache.getContent() != null) { |
|
|
|
if (containsUserHiddenShop(cache.getContent())) { |
|
|
|
redisTemplateHelper.delete(defaultPageCacheKey); |
|
|
|
} else { |
|
|
|
fillBaseDeliveryFee(cache.getContent(), regionId); |
|
|
|
timer.mark("baseFee"); |
|
|
|
Page<Shop> pageResult = new PageImpl<>(cache.getContent(), PageUtil.initPage(pageVo), cache.getTotalElements()); |
|
|
|
logShopListQuery("pageCache", timer, cache.getTotalElements(), cache.getContent().size(), regionId); |
|
|
|
return new ResultUtil<Page<Shop>>().setData(pageResult); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -488,10 +495,14 @@ public class ShopController { |
|
|
|
} |
|
|
|
if (useRegionCache) { |
|
|
|
List<Object> values = redisTemplateHelper.hValues("SHOP_CACHE:" + regionId); |
|
|
|
timer.mark("hvals"); |
|
|
|
if (values != null && !values.isEmpty()) { |
|
|
|
List<Shop> allShops = new ArrayList<>(); |
|
|
|
long hvalsBytes = 0L; |
|
|
|
for (Object v : values) { |
|
|
|
ShopCacheDTO dto = parseShopCacheBase(v.toString()); |
|
|
|
String shopCacheJson = v.toString(); |
|
|
|
hvalsBytes += shopCacheJson.length(); |
|
|
|
ShopCacheDTO dto = parseShopCacheBase(shopCacheJson); |
|
|
|
Shop s = dto.getShop(); |
|
|
|
if (s == null) { |
|
|
|
continue; |
|
|
|
@ -552,50 +563,18 @@ public class ShopController { |
|
|
|
} |
|
|
|
allShops.add(s); |
|
|
|
} |
|
|
|
timer.mark("parseShops"); |
|
|
|
timer.note("hvalsShops", values.size()); |
|
|
|
timer.note("hvalsBytes", hvalsBytes); |
|
|
|
|
|
|
|
String sort = pageVo.getSort(); |
|
|
|
String order = pageVo.getOrder(); |
|
|
|
if (CharSequenceUtil.isNotBlank(sort)) { |
|
|
|
allShops.sort((s1, s2) -> { |
|
|
|
int compareResult = 0; |
|
|
|
if ("saleCount".equals(sort)) { |
|
|
|
Integer count1 = s1.getSaleCount(); |
|
|
|
Integer count2 = s2.getSaleCount(); |
|
|
|
Integer c1 = count1 == null ? 0 : count1; |
|
|
|
Integer c2 = count2 == null ? 0 : count2; |
|
|
|
compareResult = c1.compareTo(c2); |
|
|
|
} else if ("shopScore".equals(sort)) { |
|
|
|
BigDecimal score1 = s1.getShopScore() == null ? BigDecimal.ZERO : s1.getShopScore(); |
|
|
|
BigDecimal score2 = s2.getShopScore() == null ? BigDecimal.ZERO : s2.getShopScore(); |
|
|
|
compareResult = score1.compareTo(score2); |
|
|
|
} else if ("shoprank".equals(sort)) { |
|
|
|
Integer shoprank1 = s1.getShoprank() == null ? 0 : s1.getShoprank(); |
|
|
|
Integer shoprank2 = s2.getShoprank() == null ? 0 : s2.getShoprank(); |
|
|
|
compareResult = shoprank1.compareTo(shoprank2); |
|
|
|
} |
|
|
|
if ("desc".equalsIgnoreCase(order)) { |
|
|
|
compareResult = -compareResult; |
|
|
|
} |
|
|
|
return compareResult; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 稳定排序:用户端优先展示营业中的商家,后台仍将禁用商家置底。
|
|
|
|
if (adminQuery) { |
|
|
|
allShops.sort((s1, s2) -> { |
|
|
|
boolean disabled1 = ShopConstant.SHOP_STATUS_LOCK.equals(s1.getStatus()); |
|
|
|
boolean disabled2 = ShopConstant.SHOP_STATUS_LOCK.equals(s2.getStatus()); |
|
|
|
if (disabled1 != disabled2) { |
|
|
|
return disabled1 ? 1 : -1; |
|
|
|
} |
|
|
|
Integer status1 = s1.getStatus() == null ? ShopConstant.SHOP_STATUS_LOCK : s1.getStatus(); |
|
|
|
Integer status2 = s2.getStatus() == null ? ShopConstant.SHOP_STATUS_LOCK : s2.getStatus(); |
|
|
|
return status1.compareTo(status2); |
|
|
|
}); |
|
|
|
applyAdminShopListSort(allShops, pageVo); |
|
|
|
} else { |
|
|
|
applyUserShopListSort(allShops, pageVo); |
|
|
|
String currentBusinessTime = LocalTime.now().format(BUSINESS_TIME_FORMATTER); |
|
|
|
allShops.sort((s1, s2) -> compareUserShopPriority(s1, s2, currentBusinessTime)); |
|
|
|
} |
|
|
|
timer.mark("sort"); |
|
|
|
|
|
|
|
int pageNumber = pageVo.getPageNumber() > 0 ? pageVo.getPageNumber() : 1; |
|
|
|
int pageSize = pageVo.getPageSize() > 0 ? pageVo.getPageSize() : 10; |
|
|
|
@ -615,21 +594,27 @@ public class ShopController { |
|
|
|
productKeys.add("SHOP_PRODUCTS:" + s.getId()); |
|
|
|
} |
|
|
|
List<String> productsJsonList = redisTemplateHelper.multiGet(productKeys); |
|
|
|
timer.mark("mgetProducts"); |
|
|
|
long productBytes = 0L; |
|
|
|
int productCount = 0; |
|
|
|
if (productsJsonList != null && productsJsonList.size() == pageList.size()) { |
|
|
|
for (int i = 0; i < pageList.size(); i++) { |
|
|
|
String productsJson = productsJsonList.get(i); |
|
|
|
if (CharSequenceUtil.isNotBlank(productsJson)) { |
|
|
|
productBytes += productsJson.length(); |
|
|
|
List<ProductPageVO> productsReturn = new ArrayList<>(); |
|
|
|
List<ProductPageVO> products = JSONUtil.toList(productsJson, ProductPageVO.class); |
|
|
|
productCount += products == null ? 0 : products.size(); |
|
|
|
if (hasKeyword) { |
|
|
|
for (ProductPageVO p : products) { |
|
|
|
if (p.getDelFlag() == 1 && p.getProductName() != null && p.getProductName().contains(shop.getKeyWord())) { |
|
|
|
if (isSearchMatchedProduct(p, shop.getKeyWord())) { |
|
|
|
p.setIsPush(1); |
|
|
|
productsReturn.add(p); |
|
|
|
} |
|
|
|
} |
|
|
|
}else{ |
|
|
|
for (ProductPageVO p : products) { |
|
|
|
if (p.getDelFlag() == 1 && p.getIsPush() == 1) { |
|
|
|
if (Integer.valueOf(1).equals(p.getDelFlag()) && Integer.valueOf(1).equals(p.getIsPush())) { |
|
|
|
productsReturn.add(p); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -640,28 +625,40 @@ public class ShopController { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
timer.mark("parseProducts"); |
|
|
|
timer.note("productBytes", productBytes); |
|
|
|
timer.note("productCount", productCount); |
|
|
|
} |
|
|
|
fillGroupDeliveryTime(pageList, regionId); |
|
|
|
timer.mark("deliveryTime"); |
|
|
|
fillBaseDeliveryFee(pageList, regionId); |
|
|
|
timer.mark("baseFee"); |
|
|
|
|
|
|
|
Page<Shop> pageResult = new PageImpl<>(pageList, PageUtil.initPage(pageVo), allShops.size()); |
|
|
|
cacheDefaultGroupBuyPage(defaultPageCacheKey, pageList, allShops.size()); |
|
|
|
logSlowShopListQuery("redis", queryStartTime, allShops.size(), pageList.size(), regionId); |
|
|
|
timer.mark("pageCacheWrite"); |
|
|
|
logShopListQuery("redis", timer, allShops.size(), pageList.size(), regionId); |
|
|
|
return new ResultUtil<Page<Shop>>().setData(pageResult); |
|
|
|
} |
|
|
|
} |
|
|
|
Page<Shop> page; |
|
|
|
if (adminQuery) { |
|
|
|
page = shopService.findByCondition(shop, PageUtil.initPage(pageVo)); |
|
|
|
timer.mark("dbQuery"); |
|
|
|
fillShopTakeawayAndProducts(page.getContent(), shop, false); |
|
|
|
timer.mark("dbFill"); |
|
|
|
} else { |
|
|
|
// 用户端:全量查询 → 挂 takeaway → 按多时段营业状态排序 → 再分页(与缓存路径一致)
|
|
|
|
Page<Shop> allPage = shopService.findByCondition(shop, Pageable.unpaged()); |
|
|
|
List<Shop> allShops = new ArrayList<>(allPage.getContent()); |
|
|
|
timer.mark("dbQuery"); |
|
|
|
timer.note("dbShops", allShops.size()); |
|
|
|
fillShopTakeawayOnly(allShops); |
|
|
|
timer.mark("dbTakeaway"); |
|
|
|
applyUserShopListSort(allShops, pageVo); |
|
|
|
String currentBusinessTime = LocalTime.now().format(BUSINESS_TIME_FORMATTER); |
|
|
|
allShops.sort((s1, s2) -> compareUserShopPriority(s1, s2, currentBusinessTime)); |
|
|
|
timer.mark("sort"); |
|
|
|
|
|
|
|
int pageNumber = pageVo.getPageNumber() > 0 ? pageVo.getPageNumber() : 1; |
|
|
|
int pageSize = pageVo.getPageSize() > 0 ? pageVo.getPageSize() : 10; |
|
|
|
@ -671,15 +668,85 @@ public class ShopController { |
|
|
|
? new ArrayList<>() |
|
|
|
: new ArrayList<>(allShops.subList(fromIndex, toIndex)); |
|
|
|
fillShopProducts(pageList, shop); |
|
|
|
timer.mark("dbProducts"); |
|
|
|
fillGroupDeliveryTime(pageList, shop.getRegionId()); |
|
|
|
timer.mark("deliveryTime"); |
|
|
|
fillBaseDeliveryFee(pageList, shop.getRegionId()); |
|
|
|
timer.mark("baseFee"); |
|
|
|
page = new PageImpl<>(pageList, PageUtil.initPage(pageVo), allShops.size()); |
|
|
|
} |
|
|
|
cacheDefaultGroupBuyPage(defaultPageCacheKey, page.getContent(), page.getTotalElements()); |
|
|
|
logSlowShopListQuery("db", queryStartTime, page.getTotalElements(), page.getContent().size(), shop.getRegionId()); |
|
|
|
timer.mark("pageCacheWrite"); |
|
|
|
logShopListQuery("db", timer, page.getTotalElements(), page.getContent().size(), shop.getRegionId()); |
|
|
|
return new ResultUtil<Page<Shop>>().setData(page); |
|
|
|
} |
|
|
|
|
|
|
|
private void applyAdminShopListSort(List<Shop> allShops, PageVo pageVo) { |
|
|
|
if (allShops == null || allShops.isEmpty()) { |
|
|
|
return; |
|
|
|
} |
|
|
|
String shoprankOrder = getShoprankOrder(pageVo); |
|
|
|
String sort = pageVo == null ? null : pageVo.getSort(); |
|
|
|
String order = pageVo == null ? null : pageVo.getOrder(); |
|
|
|
allShops.sort((s1, s2) -> { |
|
|
|
int compareResult = Integer.compare(getAdminShopStatusOrder(s1), getAdminShopStatusOrder(s2)); |
|
|
|
if (compareResult != 0) { |
|
|
|
return compareResult; |
|
|
|
} |
|
|
|
compareResult = compareShoprank(s1, s2, shoprankOrder); |
|
|
|
if (compareResult != 0 || CharSequenceUtil.isBlank(sort) |
|
|
|
|| "status".equals(sort) || "shoprank".equals(sort)) { |
|
|
|
return compareResult; |
|
|
|
} |
|
|
|
compareResult = compareShopBySortField(s1, s2, sort); |
|
|
|
if ("desc".equalsIgnoreCase(order)) { |
|
|
|
compareResult = -compareResult; |
|
|
|
} |
|
|
|
return compareResult; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
private int getAdminShopStatusOrder(Shop shop) { |
|
|
|
Integer status = shop == null ? null : shop.getStatus(); |
|
|
|
if (ShopConstant.SHOP_STATUS_NORMAL.equals(status)) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
return ShopConstant.SHOP_STATUS_LOCK.equals(status) ? 2 : 1; |
|
|
|
} |
|
|
|
|
|
|
|
private String getShoprankOrder(PageVo pageVo) { |
|
|
|
if (pageVo == null) { |
|
|
|
return "desc"; |
|
|
|
} |
|
|
|
if ("shoprank".equals(pageVo.getSortOrder())) { |
|
|
|
return CharSequenceUtil.isBlank(pageVo.getOrderOrder()) ? "desc" : pageVo.getOrderOrder(); |
|
|
|
} |
|
|
|
if ("shoprank".equals(pageVo.getSort())) { |
|
|
|
return CharSequenceUtil.isBlank(pageVo.getOrder()) ? "desc" : pageVo.getOrder(); |
|
|
|
} |
|
|
|
return "desc"; |
|
|
|
} |
|
|
|
|
|
|
|
private int compareShoprank(Shop s1, Shop s2, String order) { |
|
|
|
Integer shoprank1 = s1.getShoprank() == null ? 0 : s1.getShoprank(); |
|
|
|
Integer shoprank2 = s2.getShoprank() == null ? 0 : s2.getShoprank(); |
|
|
|
int compareResult = shoprank1.compareTo(shoprank2); |
|
|
|
return "asc".equalsIgnoreCase(order) ? compareResult : -compareResult; |
|
|
|
} |
|
|
|
|
|
|
|
private int compareShopBySortField(Shop s1, Shop s2, String sort) { |
|
|
|
if ("saleCount".equals(sort)) { |
|
|
|
Integer c1 = s1.getSaleCount() == null ? 0 : s1.getSaleCount(); |
|
|
|
Integer c2 = s2.getSaleCount() == null ? 0 : s2.getSaleCount(); |
|
|
|
return c1.compareTo(c2); |
|
|
|
} else if ("shopScore".equals(sort)) { |
|
|
|
BigDecimal score1 = s1.getShopScore() == null ? BigDecimal.ZERO : s1.getShopScore(); |
|
|
|
BigDecimal score2 = s2.getShopScore() == null ? BigDecimal.ZERO : s2.getShopScore(); |
|
|
|
return score1.compareTo(score2); |
|
|
|
} |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
private void applyUserShopListSort(List<Shop> allShops, PageVo pageVo) { |
|
|
|
if (allShops == null || allShops.isEmpty() || pageVo == null) { |
|
|
|
return; |
|
|
|
@ -747,6 +814,9 @@ public class ShopController { |
|
|
|
Map<String, List<ProductPageVO>> productsByShopId = new HashMap<>(); |
|
|
|
if (productList != null && productList.getRecords() != null) { |
|
|
|
productList.getRecords().forEach(productPageVO -> { |
|
|
|
if (CharSequenceUtil.isNotBlank(query.getKeyWord())) { |
|
|
|
productPageVO.setIsPush(1); |
|
|
|
} |
|
|
|
productsByShopId.computeIfAbsent(productPageVO.getShopId(), key -> new ArrayList<>()).add(productPageVO); |
|
|
|
}); |
|
|
|
} |
|
|
|
@ -755,6 +825,14 @@ public class ShopController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private boolean isSearchMatchedProduct(ProductPageVO product, String keyword) { |
|
|
|
return product != null |
|
|
|
&& Integer.valueOf(1).equals(product.getDelFlag()) |
|
|
|
&& CharSequenceUtil.isNotBlank(product.getProductName()) |
|
|
|
&& CharSequenceUtil.isNotBlank(keyword) |
|
|
|
&& product.getProductName().contains(keyword); |
|
|
|
} |
|
|
|
|
|
|
|
private void fillShopTakeawayAndProducts(List<Shop> shops, Shop query) { |
|
|
|
fillShopTakeawayAndProducts(shops, query, true); |
|
|
|
} |
|
|
|
@ -851,11 +929,42 @@ public class ShopController { |
|
|
|
return ShopBusinessHourUtil.getBusinessTimeOrder(takeaway, currentTime); |
|
|
|
} |
|
|
|
|
|
|
|
private void logSlowShopListQuery(String source, long startTime, long totalElements, int pageSize, String regionId) { |
|
|
|
long cost = System.currentTimeMillis() - startTime; |
|
|
|
if (cost > 300) { |
|
|
|
log.warn("app/shop/getByCondition slow source={}, cost={}ms, regionId={}, total={}, pageSize={}", |
|
|
|
source, cost, regionId, totalElements, pageSize); |
|
|
|
private void logShopListQuery(String source, ShopListStageTimer timer, long totalElements, int pageSize, String regionId) { |
|
|
|
long cost = timer.totalMs(); |
|
|
|
if (cost > SHOP_LIST_SLOW_THRESHOLD_MS) { |
|
|
|
log.warn("app/shop/getByCondition slow source={}, cost={}ms, regionId={}, total={}, pageSize={}, stages=[{}]", |
|
|
|
source, cost, regionId, totalElements, pageSize, timer.detail()); |
|
|
|
} else if (log.isDebugEnabled()) { |
|
|
|
log.debug("app/shop/getByCondition source={}, cost={}ms, regionId={}, total={}, pageSize={}, stages=[{}]", |
|
|
|
source, cost, regionId, totalElements, pageSize, timer.detail()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 分段计时:mark 记录距上一个 mark 的耗时,note 记录传输字节数等辅助指标, |
|
|
|
* 用于定位 getByCondition 的耗时分布(Redis 取数 / JSON 解析 / 排序 / DB)。 |
|
|
|
*/ |
|
|
|
private static final class ShopListStageTimer { |
|
|
|
private final long startNano = System.nanoTime(); |
|
|
|
private final StringBuilder detail = new StringBuilder(); |
|
|
|
private long lastNano = startNano; |
|
|
|
|
|
|
|
void mark(String stage) { |
|
|
|
long now = System.nanoTime(); |
|
|
|
detail.append(stage).append('=').append((now - lastNano) / 1_000_000L).append("ms "); |
|
|
|
lastNano = now; |
|
|
|
} |
|
|
|
|
|
|
|
void note(String name, Object value) { |
|
|
|
detail.append(name).append('=').append(value).append(' '); |
|
|
|
} |
|
|
|
|
|
|
|
long totalMs() { |
|
|
|
return (System.nanoTime() - startNano) / 1_000_000L; |
|
|
|
} |
|
|
|
|
|
|
|
String detail() { |
|
|
|
return detail.toString().trim(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -1032,7 +1141,7 @@ public class ShopController { |
|
|
|
if (shop.getStoreFlag() != null) { |
|
|
|
oldShop.setStoreFlag(shop.getStoreFlag()); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 新增字段处理
|
|
|
|
if (shop.getShopImages() != null) { |
|
|
|
oldShop.setShopImages(shop.getShopImages()); |
|
|
|
|