|
|
|
@ -1,16 +1,27 @@ |
|
|
|
package cc.hiver.mall.quartz; |
|
|
|
|
|
|
|
import cc.hiver.core.common.redis.RedisTemplateHelper; |
|
|
|
import cc.hiver.core.common.utils.SnowFlakeUtil; |
|
|
|
import cc.hiver.mall.entity.MallOrderGroup; |
|
|
|
import cc.hiver.mall.entity.ProductGroupBuyPrice; |
|
|
|
import cc.hiver.mall.entity.Shop; |
|
|
|
import cc.hiver.mall.planet.service.PlanetAdventurePkService; |
|
|
|
import cc.hiver.mall.pojo.dto.ShopCacheDTO; |
|
|
|
import cc.hiver.mall.pojo.vo.ProductPageVO; |
|
|
|
import cc.hiver.mall.service.mybatis.MallOrderGroupService; |
|
|
|
import cn.hutool.json.JSONArray; |
|
|
|
import cn.hutool.json.JSONObject; |
|
|
|
import cn.hutool.json.JSONUtil; |
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.apache.commons.lang3.StringUtils; |
|
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
|
import org.springframework.scheduling.annotation.Scheduled; |
|
|
|
import org.springframework.stereotype.Component; |
|
|
|
|
|
|
|
import java.util.Date; |
|
|
|
import java.util.List; |
|
|
|
import java.math.BigDecimal; |
|
|
|
import java.util.*; |
|
|
|
import java.util.concurrent.ThreadLocalRandom; |
|
|
|
|
|
|
|
/** |
|
|
|
* 拼团超时自动失败定时任务 |
|
|
|
@ -20,17 +31,31 @@ import java.util.List; |
|
|
|
@Component |
|
|
|
public class GroupBuyExpireTask { |
|
|
|
|
|
|
|
private static final String MOCK_REGION_ID = "2097949297042657280"; |
|
|
|
private static final String SHOP_CACHE_PREFIX = "SHOP_CACHE:"; |
|
|
|
private static final String SHOP_PRODUCTS_PREFIX = "SHOP_PRODUCTS:"; |
|
|
|
private static final String SHOP_GROUP_ORDERS_PREFIX = "SHOP_GROUP_ORDERS:"; |
|
|
|
private static final int MOCK_GROUP_SHOP_COUNT = 20; |
|
|
|
private static final int MOCK_GROUP_TARGET_MEMBERS = 2; |
|
|
|
private static final int SALE_SHOP_COUNT = 40; |
|
|
|
private static final int SALE_INCREMENT_TOTAL = 100; |
|
|
|
private static final long MOCK_GROUP_EXPIRE_MILLIS = 60 * 60 * 1000L; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private MallOrderGroupService mallOrderGroupService; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private PlanetAdventurePkService planetAdventurePkService; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private RedisTemplateHelper redisTemplateHelper; |
|
|
|
|
|
|
|
/** |
|
|
|
* 每5分钟执行一次 |
|
|
|
*/ |
|
|
|
@Scheduled(cron = "0 0/5 * * * ?") |
|
|
|
public void expireOverdueGroups() { |
|
|
|
refreshMockGroupOrderCache(); |
|
|
|
log.info("[拼团过期任务] 开始扫描超时拼团..."); |
|
|
|
try { |
|
|
|
LambdaQueryWrapper<MallOrderGroup> qw = new LambdaQueryWrapper<>(); |
|
|
|
@ -59,6 +84,40 @@ public class GroupBuyExpireTask { |
|
|
|
expireOverduePkRooms(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 每30分钟随机增加店铺列表和商品缓存销量,仅用于展示缓存假数据。 |
|
|
|
*/ |
|
|
|
@Scheduled(cron = "0 0/30 * * * ?") |
|
|
|
public void increaseMockShopSales() { |
|
|
|
try { |
|
|
|
Map<Object, Object> shopCache = redisTemplateHelper.hGetAll(buildShopCacheKey(MOCK_REGION_ID)); |
|
|
|
if (shopCache == null || shopCache.isEmpty()) { |
|
|
|
log.info("[模拟销量任务] 区域 {} 无店铺缓存", MOCK_REGION_ID); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
List<Map.Entry<Object, Object>> selectedShops = selectCachedShops(shopCache, SALE_SHOP_COUNT, true); |
|
|
|
if (selectedShops.isEmpty()) { |
|
|
|
log.info("[模拟销量任务] 区域 {} 无可用店铺缓存", MOCK_REGION_ID); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
List<Integer> increments = randomPositiveParts(SALE_INCREMENT_TOTAL, selectedShops.size()); |
|
|
|
int updatedCount = 0; |
|
|
|
int actualIncrement = 0; |
|
|
|
for (int i = 0; i < selectedShops.size(); i++) { |
|
|
|
if (increaseShopSaleCache(selectedShops.get(i), increments.get(i))) { |
|
|
|
updatedCount++; |
|
|
|
actualIncrement += increments.get(i); |
|
|
|
} |
|
|
|
} |
|
|
|
log.info("[模拟销量任务] 区域 {} 已为 {} 个商家累计增加 {} 销量", |
|
|
|
MOCK_REGION_ID, updatedCount, actualIncrement); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("[模拟销量任务] 执行异常: {}", e.getMessage(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void expireOverduePkRooms() { |
|
|
|
try { |
|
|
|
int settled = planetAdventurePkService.autoSettleOverdueRooms(); |
|
|
|
@ -67,4 +126,259 @@ public class GroupBuyExpireTask { |
|
|
|
log.error("[拼团过期任务] 星球擂台超时结算异常: {}", e.getMessage(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void refreshMockGroupOrderCache() { |
|
|
|
try { |
|
|
|
Map<Object, Object> shopCache = redisTemplateHelper.hGetAll(buildShopCacheKey(MOCK_REGION_ID)); |
|
|
|
if (shopCache == null || shopCache.isEmpty()) { |
|
|
|
log.info("[模拟待成团任务] 区域 {} 无店铺缓存", MOCK_REGION_ID); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
clearRegionShopGroupOrderCache(shopCache); |
|
|
|
List<Map.Entry<Object, Object>> selectedShops = selectCachedShops(shopCache, shopCache.size(), false); |
|
|
|
int mockCount = 0; |
|
|
|
for (Map.Entry<Object, Object> shopEntry : selectedShops) { |
|
|
|
if (mockCount >= MOCK_GROUP_SHOP_COUNT) { |
|
|
|
break; |
|
|
|
} |
|
|
|
ShopCacheDTO cacheDTO = parseShopCacheDTO(shopEntry.getValue()); |
|
|
|
Shop shop = cacheDTO == null ? null : cacheDTO.getShop(); |
|
|
|
String shopId = getShopId(shopEntry); |
|
|
|
if (shop == null || StringUtils.isBlank(shopId)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
List<ProductPageVO> products = loadVisibleGroupBuyProducts(shopId); |
|
|
|
if (products.isEmpty()) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
ProductPageVO product = products.get(ThreadLocalRandom.current().nextInt(products.size())); |
|
|
|
MallOrderGroup mockGroup = buildMockGroup(shopId, shop.getShopName(), product); |
|
|
|
String json = JSONUtil.toJsonStr(mockGroup); |
|
|
|
redisTemplateHelper.hPut(buildShopGroupOrdersKey(shopId), mockGroup.getId(), json); |
|
|
|
mockCount++; |
|
|
|
} |
|
|
|
log.info("[模拟待成团任务] 区域 {} 已刷新 {} 个店铺待成团缓存", MOCK_REGION_ID, mockCount); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("[模拟待成团任务] 执行异常: {}", e.getMessage(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void clearRegionShopGroupOrderCache(Map<Object, Object> shopCache) { |
|
|
|
List<String> keys = new ArrayList<>(); |
|
|
|
for (Object shopIdObj : shopCache.keySet()) { |
|
|
|
String shopId = shopIdObj == null ? null : shopIdObj.toString(); |
|
|
|
if (StringUtils.isNotBlank(shopId)) { |
|
|
|
keys.add(buildShopGroupOrdersKey(shopId)); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!keys.isEmpty()) { |
|
|
|
redisTemplateHelper.delete(keys); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private List<Map.Entry<Object, Object>> selectCachedShops(Map<Object, Object> shopCache, int limit, boolean requireProducts) { |
|
|
|
List<Map.Entry<Object, Object>> entries = new ArrayList<>(shopCache.entrySet()); |
|
|
|
Collections.shuffle(entries); |
|
|
|
|
|
|
|
List<Map.Entry<Object, Object>> selected = new ArrayList<>(); |
|
|
|
for (Map.Entry<Object, Object> entry : entries) { |
|
|
|
if (selected.size() >= limit) { |
|
|
|
break; |
|
|
|
} |
|
|
|
String shopId = getShopId(entry); |
|
|
|
ShopCacheDTO cacheDTO = parseShopCacheDTO(entry.getValue()); |
|
|
|
if (StringUtils.isNotBlank(shopId) |
|
|
|
&& cacheDTO != null |
|
|
|
&& cacheDTO.getShop() != null |
|
|
|
&& (!requireProducts || hasVisibleProductCache(shopId))) { |
|
|
|
selected.add(entry); |
|
|
|
} |
|
|
|
} |
|
|
|
return selected; |
|
|
|
} |
|
|
|
|
|
|
|
private boolean increaseShopSaleCache(Map.Entry<Object, Object> shopEntry, int increment) { |
|
|
|
String shopId = getShopId(shopEntry); |
|
|
|
ShopCacheDTO cacheDTO = parseShopCacheDTO(shopEntry.getValue()); |
|
|
|
if (StringUtils.isBlank(shopId) || cacheDTO == null || cacheDTO.getShop() == null || increment <= 0) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
Shop shop = cacheDTO.getShop(); |
|
|
|
int oldSaleCount = shop.getSaleCount() == null ? 0 : shop.getSaleCount(); |
|
|
|
if (!increaseProductTailWarnCache(shopId, increment)) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
shop.setSaleCount(oldSaleCount + increment); |
|
|
|
redisTemplateHelper.hPut(buildShopCacheKey(MOCK_REGION_ID), shopId, JSONUtil.toJsonStr(cacheDTO)); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
private boolean increaseProductTailWarnCache(String shopId, int totalIncrement) { |
|
|
|
String productsKey = SHOP_PRODUCTS_PREFIX + shopId; |
|
|
|
String productsJson = redisTemplateHelper.get(productsKey); |
|
|
|
if (StringUtils.isBlank(productsJson) || totalIncrement <= 0) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
JSONArray productArray = JSONUtil.parseArray(productsJson); |
|
|
|
if (productArray.isEmpty()) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
List<Integer> productIndexes = new ArrayList<>(); |
|
|
|
for (int i = 0; i < productArray.size(); i++) { |
|
|
|
JSONObject productObj = productArray.getJSONObject(i); |
|
|
|
if (productObj != null && Integer.valueOf(1).equals(productObj.getInt("delFlag", 1))) { |
|
|
|
productIndexes.add(i); |
|
|
|
} |
|
|
|
} |
|
|
|
if (productIndexes.isEmpty()) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
Collections.shuffle(productIndexes); |
|
|
|
int productCount = Math.min(productIndexes.size(), totalIncrement); |
|
|
|
List<Integer> productIncrements = randomPositiveParts(totalIncrement, productCount); |
|
|
|
for (int i = 0; i < productCount; i++) { |
|
|
|
JSONObject productObj = productArray.getJSONObject(productIndexes.get(i)); |
|
|
|
int oldTailWarn = productObj.getInt("tailWarn", 0); |
|
|
|
productObj.set("tailWarn", oldTailWarn + productIncrements.get(i)); |
|
|
|
} |
|
|
|
redisTemplateHelper.set(productsKey, productArray.toString()); |
|
|
|
return true; |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[模拟销量任务] 更新商品销量缓存失败 shopId={}: {}", shopId, e.getMessage()); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private boolean hasVisibleProductCache(String shopId) { |
|
|
|
return !loadVisibleProducts(shopId).isEmpty(); |
|
|
|
} |
|
|
|
|
|
|
|
private List<ProductPageVO> loadVisibleProducts(String shopId) { |
|
|
|
String productsJson = redisTemplateHelper.get(SHOP_PRODUCTS_PREFIX + shopId); |
|
|
|
if (StringUtils.isBlank(productsJson)) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
List<ProductPageVO> products = JSONUtil.toList(productsJson, ProductPageVO.class); |
|
|
|
if (products == null || products.isEmpty()) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
List<ProductPageVO> visibleProducts = new ArrayList<>(); |
|
|
|
for (ProductPageVO product : products) { |
|
|
|
if (product != null |
|
|
|
&& StringUtils.isNotBlank(product.getId()) |
|
|
|
&& StringUtils.isNotBlank(product.getProductName()) |
|
|
|
&& Integer.valueOf(1).equals(product.getDelFlag())) { |
|
|
|
visibleProducts.add(product); |
|
|
|
} |
|
|
|
} |
|
|
|
return visibleProducts; |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[模拟待成团任务] 读取商品缓存失败 shopId={}: {}", shopId, e.getMessage()); |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private MallOrderGroup buildMockGroup(String shopId, String shopName, ProductPageVO product) { |
|
|
|
String orderId = SnowFlakeUtil.nextId().toString(); |
|
|
|
MallOrderGroup group = new MallOrderGroup(); |
|
|
|
group.setId(SnowFlakeUtil.nextId().toString()); |
|
|
|
group.setShopId(shopId); |
|
|
|
group.setShopName(shopName); |
|
|
|
group.setRegionId(MOCK_REGION_ID); |
|
|
|
group.setHeadUserId("2092548901814013952"); |
|
|
|
group.setHeadOrderId(orderId); |
|
|
|
group.setGroupOrderIds(orderId); |
|
|
|
group.setTargetMembers(MOCK_GROUP_TARGET_MEMBERS); |
|
|
|
group.setCurrentMembers(1); |
|
|
|
group.setIsFace(0); |
|
|
|
group.setStatus(0); |
|
|
|
group.setGroupPrice(getTwoMemberGroupPrice(product)); |
|
|
|
group.setWorkerCommission(new BigDecimal("0.01")); |
|
|
|
group.setTotalDeliveryFee(BigDecimal.ZERO); |
|
|
|
group.setCreateTime(new Date()); |
|
|
|
group.setExpireTime(new Date(System.currentTimeMillis() + MOCK_GROUP_EXPIRE_MILLIS)); |
|
|
|
group.setProductName(product.getProductName()); |
|
|
|
group.setProductPicture(product.getProductPicture()); |
|
|
|
group.setProductId(product.getId()); |
|
|
|
return group; |
|
|
|
} |
|
|
|
|
|
|
|
private List<ProductPageVO> loadVisibleGroupBuyProducts(String shopId) { |
|
|
|
List<ProductPageVO> products = loadVisibleProducts(shopId); |
|
|
|
if (products.isEmpty()) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
|
|
|
|
List<ProductPageVO> groupBuyProducts = new ArrayList<>(); |
|
|
|
for (ProductPageVO product : products) { |
|
|
|
if (getTwoMemberGroupPrice(product) != null) { |
|
|
|
groupBuyProducts.add(product); |
|
|
|
} |
|
|
|
} |
|
|
|
return groupBuyProducts; |
|
|
|
} |
|
|
|
|
|
|
|
private BigDecimal getTwoMemberGroupPrice(ProductPageVO product) { |
|
|
|
if (product != null && product.getProductGroupBuyPrices() != null) { |
|
|
|
for (ProductGroupBuyPrice price : product.getProductGroupBuyPrices()) { |
|
|
|
if (price != null |
|
|
|
&& Integer.valueOf(MOCK_GROUP_TARGET_MEMBERS).equals(price.getGroupCount()) |
|
|
|
&& price.getGroupPrice() != null) { |
|
|
|
return price.getGroupPrice(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private List<Integer> randomPositiveParts(int total, int count) { |
|
|
|
if (total <= 0 || count <= 0) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
int actualCount = Math.min(total, count); |
|
|
|
List<Integer> parts = new ArrayList<>(actualCount); |
|
|
|
for (int i = 0; i < actualCount; i++) { |
|
|
|
parts.add(1); |
|
|
|
} |
|
|
|
for (int remaining = total - actualCount; remaining > 0; remaining--) { |
|
|
|
int index = ThreadLocalRandom.current().nextInt(actualCount); |
|
|
|
parts.set(index, parts.get(index) + 1); |
|
|
|
} |
|
|
|
Collections.shuffle(parts); |
|
|
|
return parts; |
|
|
|
} |
|
|
|
|
|
|
|
private ShopCacheDTO parseShopCacheDTO(Object value) { |
|
|
|
if (value == null) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
try { |
|
|
|
return JSONUtil.toBean(value.toString(), ShopCacheDTO.class); |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[模拟缓存任务] 店铺缓存解析失败: {}", e.getMessage()); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private String getShopId(Map.Entry<Object, Object> shopEntry) { |
|
|
|
return shopEntry == null || shopEntry.getKey() == null ? null : shopEntry.getKey().toString(); |
|
|
|
} |
|
|
|
|
|
|
|
private String buildShopCacheKey(String regionId) { |
|
|
|
return SHOP_CACHE_PREFIX + regionId; |
|
|
|
} |
|
|
|
|
|
|
|
private String buildShopGroupOrdersKey(String shopId) { |
|
|
|
return SHOP_GROUP_ORDERS_PREFIX + shopId; |
|
|
|
} |
|
|
|
} |
|
|
|
|