21 changed files with 1101 additions and 50 deletions
@ -0,0 +1,535 @@ |
|||
package cc.hiver.mall.utils; |
|||
|
|||
import cc.hiver.core.common.redis.RedisTemplateHelper; |
|||
import cc.hiver.mall.entity.MallDeliveryOrder; |
|||
import cc.hiver.mall.pojo.dto.AreaRuleDTO; |
|||
import cc.hiver.mall.pojo.query.MallDeliveryOrderPageQuery; |
|||
import cn.hutool.json.JSONUtil; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.text.SimpleDateFormat; |
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 抢单大厅待完成订单缓存工具类 |
|||
* <p> |
|||
* 底层使用 Redis Hash 结构,保证按 regionId + orderId 维度的 O(1) 读写效率。 |
|||
* <pre> |
|||
* Key = WAIT_ORDERS:{regionId} |
|||
* Field = orderId |
|||
* Value = MallDeliveryOrder 的 JSON 序列化 |
|||
* </pre> |
|||
* <p> |
|||
* 使用场景:订单状态发生变化时(创建、更新、取消、完成等), |
|||
* 由业务方主动调用本工具类的方法维护缓存,以减少 pagebyworker 接口对数据库的查询压力。 |
|||
* |
|||
* @author system |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
public class WaitOrderCacheUtil { |
|||
|
|||
/** Redis Key 前缀 */ |
|||
private static final String KEY_PREFIX = "WAIT_ORDERS:"; |
|||
|
|||
@Autowired |
|||
private RedisTemplateHelper redisTemplateHelper; |
|||
|
|||
// ================================================================
|
|||
// Key 构建
|
|||
// ================================================================
|
|||
|
|||
private String buildKey(String regionId) { |
|||
return KEY_PREFIX + regionId; |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 存放(put)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 存放单个订单到缓存 |
|||
* |
|||
* @param regionId 区域ID |
|||
* @param orderVO 订单VO(必须包含有效的 id) |
|||
*/ |
|||
public void put(String regionId, MallDeliveryOrder orderVO) { |
|||
if (StringUtils.isBlank(regionId) || orderVO == null || StringUtils.isBlank(orderVO.getId())) { |
|||
log.info("UserPendingOrderCacheUtil.put 参数无效, userId={}, orderVO={}", regionId, orderVO); |
|||
return; |
|||
} |
|||
try { |
|||
String key = buildKey(regionId); |
|||
String json = JSONUtil.toJsonStr(orderVO); |
|||
redisTemplateHelper.hPut(key, orderVO.getId(), json); |
|||
log.info("缓存抢单大厅订单: regionId={}, orderId={}", regionId, orderVO.getId()); |
|||
} catch (Exception e) { |
|||
log.info("缓存抢单大厅订单失败: regionId={}, orderId={}", regionId, orderVO.getId(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 批量存放订单到缓存(通常用于缓存预热 / 首次加载) |
|||
* |
|||
* @param regionId 区域ID |
|||
* @param orders 订单列表 |
|||
*/ |
|||
public void putAll(String regionId, List<MallDeliveryOrder> orders) { |
|||
if (StringUtils.isBlank(regionId) || orders == null || orders.isEmpty()) { |
|||
return; |
|||
} |
|||
try { |
|||
String key = buildKey(regionId); |
|||
Map<String, String> map = new LinkedHashMap<>(orders.size()); |
|||
for (MallDeliveryOrder order : orders) { |
|||
if (order != null && StringUtils.isNotBlank(order.getId())) { |
|||
map.put(order.getId(), JSONUtil.toJsonStr(order)); |
|||
} |
|||
} |
|||
if (!map.isEmpty()) { |
|||
redisTemplateHelper.hPutAll(key, map); |
|||
log.info("批量缓存抢单大厅订单: regionId={}, count={}", regionId, map.size()); |
|||
} |
|||
} catch (Exception e) { |
|||
log.info("批量缓存抢单大厅订单失败: regionId={}", regionId, e); |
|||
} |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 删除(remove)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 根据 regionId 和 orderId 从缓存中删除指定订单 |
|||
* <p> |
|||
* 典型场景:订单成团、取消、退款成功后调用。 |
|||
* |
|||
* @param regionId 区域ID |
|||
* @param orderId 订单ID |
|||
*/ |
|||
public void remove(String regionId, String orderId) { |
|||
if (StringUtils.isBlank(regionId) || StringUtils.isBlank(orderId)) { |
|||
log.info("UserPendingOrderCacheUtil.remove 参数无效, regionId={}, orderId={}", regionId, orderId); |
|||
return; |
|||
} |
|||
try { |
|||
String key = buildKey(regionId); |
|||
redisTemplateHelper.hDelete(key, orderId); |
|||
log.info("删除订单缓存: regionId={}, orderId={}", regionId, orderId); |
|||
} catch (Exception e) { |
|||
log.info("删除订单缓存失败: regionId={}, orderId={}", regionId, orderId, e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除指定抢单大厅所有订单缓存 |
|||
* <p> |
|||
* 典型场景:需要强制刷新该抢单大厅缓存时调用。 |
|||
* |
|||
* @param regionId 区域ID |
|||
*/ |
|||
public void removeAll(String regionId) { |
|||
if (StringUtils.isBlank(regionId)) { |
|||
return; |
|||
} |
|||
try { |
|||
redisTemplateHelper.delete(buildKey(regionId)); |
|||
log.info("清除抢单大厅订单缓存: regionId={}", regionId); |
|||
} catch (Exception e) { |
|||
log.info("清除抢单大厅订单缓存失败: regionId={}", regionId, e); |
|||
} |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 更新(update)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 根据 regionId 和 orderId 更新缓存中的订单信息 |
|||
* <p> |
|||
* 如果更新后的订单状态已经不属于"待完成"(status ∈ {10}), |
|||
* 则自动从缓存中移除该订单,无需调用方额外判断。 |
|||
* |
|||
* @param regionId 区域ID |
|||
* @param orderVO 更新后的订单VO |
|||
*/ |
|||
public void update(String regionId, MallDeliveryOrder orderVO) { |
|||
if (StringUtils.isBlank(regionId) || orderVO == null || StringUtils.isBlank(orderVO.getId())) { |
|||
log.warn("UserPendingOrderCacheUtil.update 参数无效, regionId={}, orderVO={}", regionId, orderVO); |
|||
return; |
|||
} |
|||
try { |
|||
// 如果订单状态已不属于,直接删除
|
|||
/*if (!isTerminalStatus(orderVO.getStatus())) { |
|||
remove(regionId, orderVO.getId()); |
|||
log.debug("订单已终态,从缓存移除: regionId={}, orderId={}, status={}", |
|||
regionId, orderVO.getId(), orderVO.getStatus()); |
|||
return; |
|||
}*/ |
|||
// 覆盖更新
|
|||
put(regionId, orderVO); |
|||
log.info("更新抢单大厅缓存: regionId={}, orderId={}, status={}", |
|||
regionId, orderVO.getId(), orderVO.getStatus()); |
|||
} catch (Exception e) { |
|||
log.info("更新用户待完成订单缓存失败: regionId={}, orderId={}", regionId, orderVO.getId(), e); |
|||
} |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 查询(get)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 获取抢单大厅订单缓存(含分页、过滤、排序和各类型订单数统计) |
|||
* <p> |
|||
* 两种场景: |
|||
* 1. deliveryType==4 且 workerId 非空 → 查看指派单列表 + 其他类型订单数 |
|||
* 2. 其他情况(hallOnly=true)→ 查看抢单大厅某类型订单列表(分页) + 指派单数 + 各类型订单数 |
|||
* |
|||
* @param q 查询参数 |
|||
* @return 包含分页数据和统计数据的 Map,缓存不存在时返回 null(调用方据此回源查库) |
|||
*/ |
|||
public Map<String, Object> getAll(MallDeliveryOrderPageQuery q) { |
|||
// 1. 基础校验
|
|||
if (StringUtils.isBlank(q.getRegionId())) { |
|||
return null; |
|||
} |
|||
|
|||
try { |
|||
String key = buildKey(q.getRegionId()); |
|||
// 2. 获取 Hash 中的所有订单数据
|
|||
Map<Object, Object> entries = redisTemplateHelper.hGetAll(key); |
|||
|
|||
// 如果缓存中没有数据,返回 null 让调用方去查库
|
|||
if (entries == null || entries.isEmpty()) { |
|||
return null; |
|||
} |
|||
|
|||
// 3. 反序列化所有缓存订单
|
|||
List<MallDeliveryOrder> allCachedOrders = new ArrayList<>(entries.size()); |
|||
for (Object value : entries.values()) { |
|||
if (value != null) { |
|||
allCachedOrders.add(JSONUtil.toBean(value.toString(), MallDeliveryOrder.class)); |
|||
} |
|||
} |
|||
|
|||
Map<String, Object> result = new HashMap<>(); |
|||
|
|||
if (q.getDeliveryType() != null && q.getDeliveryType() == 4 |
|||
&& StringUtils.isNotBlank(q.getWorkerId())) { |
|||
// ====== 场景A: 查看指派单订单列表 ======
|
|||
// 筛选:workerId 匹配、status=0,不区分 deliveryType
|
|||
List<MallDeliveryOrder> zhipaiList = allCachedOrders.stream() |
|||
.filter(o -> q.getWorkerId().equals(o.getWorkerId())) |
|||
.filter(o -> o.getStatus() != null && o.getStatus() == 0) |
|||
.collect(Collectors.toList()); |
|||
|
|||
// 排序(复刻 selectPageVO 的 ORDER BY)
|
|||
zhipaiList.sort(buildComparator(q)); |
|||
|
|||
long total = zhipaiList.size(); |
|||
int limit = (int) Math.min(total, 100); |
|||
List<MallDeliveryOrder> records = new ArrayList<>(zhipaiList.subList(0, limit)); |
|||
|
|||
result.put("records", records); |
|||
result.put("total", total); |
|||
result.put("size", 100); |
|||
result.put("current", 1); |
|||
result.put("pages", 1); |
|||
result.put("zhipaiCount", total); |
|||
// 外卖、快递、跑腿待接单数
|
|||
result.put("orderCount", countOrdersByTypeInMemory(allCachedOrders)); |
|||
|
|||
} else { |
|||
// ====== 场景B: 查看抢单大厅订单列表(外卖/快递/跑腿)======
|
|||
|
|||
// 先计算指派单数量(workerId 匹配、status=0)
|
|||
if (StringUtils.isNotBlank(q.getWorkerId())) { |
|||
long zhipaiCount = allCachedOrders.stream() |
|||
.filter(o -> q.getWorkerId().equals(o.getWorkerId())) |
|||
.filter(o -> o.getStatus() != null && o.getStatus() == 0) |
|||
.count(); |
|||
result.put("zhipaiCount", zhipaiCount); |
|||
} |
|||
|
|||
// 过滤抢单大厅订单:workerId 为空 且 status=0
|
|||
List<MallDeliveryOrder> filteredList = allCachedOrders.stream() |
|||
.filter(o -> StringUtils.isBlank(o.getWorkerId())) |
|||
.filter(o -> o.getStatus() != null && o.getStatus() == 0) |
|||
.collect(Collectors.toList()); |
|||
|
|||
// 按 deliveryType 过滤
|
|||
if (q.getDeliveryType() != null) { |
|||
final Integer deliveryType = q.getDeliveryType(); |
|||
filteredList = filteredList.stream() |
|||
.filter(o -> deliveryType.equals(o.getDeliveryType())) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
// 按 getAreaId 过滤
|
|||
if (StringUtils.isNotBlank(q.getGetAreaId())) { |
|||
filteredList = filteredList.stream() |
|||
.filter(o -> q.getGetAreaId().equals(o.getGetAreaId())) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
// 按 putAreaId 过滤
|
|||
if (StringUtils.isNotBlank(q.getPutAreaId())) { |
|||
filteredList = filteredList.stream() |
|||
.filter(o -> q.getPutAreaId().equals(o.getPutAreaId())) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
// 按 shopId 过滤
|
|||
if (StringUtils.isNotBlank(q.getShopId())) { |
|||
filteredList = filteredList.stream() |
|||
.filter(o -> q.getShopId().equals(o.getShopId())) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
// 按 shopName 模糊过滤
|
|||
if (StringUtils.isNotBlank(q.getShopName())) { |
|||
final String shopNameKeyword = q.getShopName(); |
|||
filteredList = filteredList.stream() |
|||
.filter(o -> o.getShopName() != null && o.getShopName().contains(shopNameKeyword)) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
// 按日期范围过滤
|
|||
if (StringUtils.isNotBlank(q.getStartDate())) { |
|||
filteredList = filteredList.stream() |
|||
.filter(o -> o.getCreateTime() != null |
|||
&& formatDate(o.getCreateTime()).compareTo(q.getStartDate()) >= 0) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
if (StringUtils.isNotBlank(q.getEndDate())) { |
|||
filteredList = filteredList.stream() |
|||
.filter(o -> o.getCreateTime() != null |
|||
&& formatDate(o.getCreateTime()).compareTo(q.getEndDate()) <= 0) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
// 排序(复刻 selectPageVO 的 ORDER BY)
|
|||
filteredList.sort(buildComparator(q)); |
|||
|
|||
// 分页(抢单大厅每页20条)
|
|||
int pageSize = 20; |
|||
int pageNum = q.getPageNum(); |
|||
long total = filteredList.size(); |
|||
long pages = total > 0 ? (total + pageSize - 1) / pageSize : 0; |
|||
|
|||
// 循环分页(同 pageDelivery 逻辑:页码越界时循环回到前面)
|
|||
if (pages > 0 && pageNum > pages) { |
|||
pageNum = (int) (((pageNum - 1) % pages) + 1); |
|||
} |
|||
|
|||
List<MallDeliveryOrder> records; |
|||
if (total > 0) { |
|||
int offset = (pageNum - 1) * pageSize; |
|||
if (offset >= total) { |
|||
records = Collections.emptyList(); |
|||
} else { |
|||
int end = (int) Math.min(offset + pageSize, total); |
|||
records = new ArrayList<>(filteredList.subList(offset, end)); |
|||
} |
|||
} else { |
|||
records = Collections.emptyList(); |
|||
} |
|||
|
|||
result.put("records", records); |
|||
result.put("total", total); |
|||
result.put("size", pageSize); |
|||
result.put("current", pageNum); |
|||
result.put("pages", pages); |
|||
// 外卖、快递、跑腿待接单数
|
|||
result.put("orderCount", countOrdersByTypeInMemory(allCachedOrders)); |
|||
} |
|||
|
|||
return result; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("获取抢单大厅订单缓存失败: regionId={}", q.getRegionId(), e); |
|||
// 发生异常返回 null,强制回源查库,保证系统可用性
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取抢单大厅缓存单个订单 |
|||
* |
|||
* @param regionId 区域ID |
|||
* @param orderId 订单ID |
|||
* @return 缓存的订单VO,不存在时返回 null |
|||
*/ |
|||
public MallDeliveryOrder get(String regionId, String orderId) { |
|||
if (StringUtils.isBlank(regionId) || StringUtils.isBlank(orderId)) { |
|||
return null; |
|||
} |
|||
try { |
|||
String key = buildKey(regionId); |
|||
Object value = redisTemplateHelper.hGet(key, orderId); |
|||
if (value == null) { |
|||
return null; |
|||
} |
|||
return JSONUtil.toBean(value.toString(), MallDeliveryOrder.class); |
|||
} catch (Exception e) { |
|||
log.info("获取抢单大厅单个订单缓存失败: regionId={}, orderId={}", regionId, orderId, e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 判断该抢单大厅的缓存是否已存在 |
|||
* |
|||
* @param regionId 区域ID |
|||
* @return true 表示缓存存在 |
|||
*/ |
|||
public boolean exists(String regionId) { |
|||
if (StringUtils.isBlank(regionId)) { |
|||
return false; |
|||
} |
|||
Boolean hasKey = redisTemplateHelper.hasKey(buildKey(regionId)); |
|||
return hasKey != null && hasKey; |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 内部工具
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 构建排序比较器(复刻 selectPageVO 的 ORDER BY 逻辑) |
|||
* <pre> |
|||
* ORDER BY: |
|||
* 1. 如果 order='deliveryFee' → delivery_fee DESC |
|||
* 2. 如果有 waimaiData/kuaidiData → 区域匹配优先 DESC |
|||
* 3. must_finish_time IS NULL 排最后(ASC: NOT NULL=0, NULL=1) |
|||
* 4. must_finish_time ASC |
|||
* 5. delivery_fee DESC |
|||
* </pre> |
|||
*/ |
|||
private Comparator<MallDeliveryOrder> buildComparator(MallDeliveryOrderPageQuery q) { |
|||
List<Comparator<MallDeliveryOrder>> comparators = new ArrayList<>(); |
|||
|
|||
// 1. 按佣金降序(如果 order=deliveryFee)
|
|||
if ("deliveryFee".equals(q.getOrder())) { |
|||
comparators.add((a, b) -> { |
|||
BigDecimal feeA = a.getDeliveryFee() != null ? a.getDeliveryFee() : BigDecimal.ZERO; |
|||
BigDecimal feeB = b.getDeliveryFee() != null ? b.getDeliveryFee() : BigDecimal.ZERO; |
|||
return feeB.compareTo(feeA); |
|||
}); |
|||
} |
|||
|
|||
// 2. 区域匹配规则排序(匹配=1 不匹配=0,DESC)
|
|||
boolean hasWaimai = q.getWaimaiData() != null && !q.getWaimaiData().isEmpty(); |
|||
boolean hasKuaidi = q.getKuaidiData() != null && !q.getKuaidiData().isEmpty(); |
|||
if (hasWaimai || hasKuaidi) { |
|||
comparators.add((a, b) -> { |
|||
int scoreA = calcAreaMatchScore(a, q); |
|||
int scoreB = calcAreaMatchScore(b, q); |
|||
return Integer.compare(scoreB, scoreA); |
|||
}); |
|||
} |
|||
|
|||
// 3. must_finish_time IS NULL 排最后 (NULL→1, NOT NULL→0, ASC)
|
|||
comparators.add((a, b) -> { |
|||
int nullA = a.getMustFinishTime() == null ? 1 : 0; |
|||
int nullB = b.getMustFinishTime() == null ? 1 : 0; |
|||
return Integer.compare(nullA, nullB); |
|||
}); |
|||
|
|||
// 4. must_finish_time ASC
|
|||
comparators.add((a, b) -> { |
|||
if (a.getMustFinishTime() == null && b.getMustFinishTime() == null) return 0; |
|||
if (a.getMustFinishTime() == null) return 1; |
|||
if (b.getMustFinishTime() == null) return -1; |
|||
return a.getMustFinishTime().compareTo(b.getMustFinishTime()); |
|||
}); |
|||
|
|||
// 5. delivery_fee DESC
|
|||
comparators.add((a, b) -> { |
|||
BigDecimal feeA = a.getDeliveryFee() != null ? a.getDeliveryFee() : BigDecimal.ZERO; |
|||
BigDecimal feeB = b.getDeliveryFee() != null ? b.getDeliveryFee() : BigDecimal.ZERO; |
|||
return feeB.compareTo(feeA); |
|||
}); |
|||
|
|||
// 组合所有比较器
|
|||
return (a, b) -> { |
|||
for (Comparator<MallDeliveryOrder> c : comparators) { |
|||
int cmp = c.compare(a, b); |
|||
if (cmp != 0) return cmp; |
|||
} |
|||
return 0; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* 计算区域匹配得分(复刻 selectPageVO 中 CASE WHEN 逻辑) |
|||
* 外卖(deliveryType=1)匹配 waimaiData,快递(deliveryType=2)匹配 kuaidiData |
|||
*/ |
|||
private int calcAreaMatchScore(MallDeliveryOrder order, MallDeliveryOrderPageQuery q) { |
|||
if (order.getDeliveryType() == null) return 0; |
|||
// 外卖匹配规则
|
|||
if (order.getDeliveryType() == 1 && q.getWaimaiData() != null) { |
|||
for (AreaRuleDTO rule : q.getWaimaiData()) { |
|||
if (Objects.equals(order.getGetAreaId(), rule.getGetAreaId()) |
|||
&& Objects.equals(order.getPutAreaId(), rule.getPutAreaId())) { |
|||
return 1; |
|||
} |
|||
} |
|||
} |
|||
// 快递匹配规则
|
|||
if (order.getDeliveryType() == 2 && q.getKuaidiData() != null) { |
|||
for (AreaRuleDTO rule : q.getKuaidiData()) { |
|||
if (Objects.equals(order.getGetAreaId(), rule.getGetAreaId()) |
|||
&& Objects.equals(order.getPutAreaId(), rule.getPutAreaId())) { |
|||
return 1; |
|||
} |
|||
} |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
/** |
|||
* 在内存中统计各类型待接单数(复刻 countOrdersByType SQL 逻辑) |
|||
* 条件:worker_id IS NULL,status=0,按 delivery_type 分组计数 |
|||
*/ |
|||
private List<Map<String, Object>> countOrdersByTypeInMemory(List<MallDeliveryOrder> allOrders) { |
|||
Map<Integer, Long> countMap = allOrders.stream() |
|||
.filter(o -> StringUtils.isBlank(o.getWorkerId())) |
|||
.filter(o -> o.getStatus() != null && o.getStatus() == 0) |
|||
.filter(o -> o.getDeliveryType() != null) |
|||
.collect(Collectors.groupingBy(MallDeliveryOrder::getDeliveryType, Collectors.counting())); |
|||
|
|||
List<Map<String, Object>> result = new ArrayList<>(); |
|||
for (Map.Entry<Integer, Long> entry : countMap.entrySet()) { |
|||
Map<String, Object> m = new HashMap<>(); |
|||
m.put("deliveryType", entry.getKey()); |
|||
m.put("orderCount", entry.getValue()); |
|||
result.add(m); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 日期格式化为 yyyy-MM-dd(用于日期范围过滤比较) |
|||
*/ |
|||
private String formatDate(Date date) { |
|||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
|||
return sdf.format(date); |
|||
} |
|||
|
|||
/** |
|||
* 判断订单状态是否为终态(不再属于"") |
|||
*/ |
|||
/* private boolean isTerminalStatus(Integer status) { |
|||
if (status == null) { |
|||
return false; |
|||
} |
|||
return status == STATUS_DONE; |
|||
}*/ |
|||
} |
|||
@ -0,0 +1,324 @@ |
|||
package cc.hiver.mall.utils; |
|||
|
|||
import cc.hiver.core.common.redis.RedisTemplateHelper; |
|||
import cc.hiver.mall.entity.MallDeliveryOrder; |
|||
import cc.hiver.mall.pojo.query.MallDeliveryOrderPageQuery; |
|||
import cn.hutool.json.JSONUtil; |
|||
import com.baomidou.mybatisplus.core.metadata.IPage; |
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 配送员待完成订单缓存工具类 |
|||
* <p> |
|||
* 底层使用 Redis Hash 结构,保证按 workerId + orderId 维度的 O(1) 读写效率。 |
|||
* <pre> |
|||
* Key = WORKER_ORDERS:{workerId} |
|||
* Field = orderId |
|||
* Value = MallDeliveryOrder 的 JSON 序列化 |
|||
* </pre> |
|||
* <p> |
|||
* 使用场景:订单状态发生变化时(创建、更新、取消、完成等), |
|||
* 由业务方主动调用本工具类的方法维护缓存,以减少 pagebyworker 接口对数据库的查询压力。 |
|||
* |
|||
* @author system |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
public class WorkerOrderCacheUtil { |
|||
|
|||
/** Redis Key 前缀 */ |
|||
private static final String KEY_PREFIX = "WORKER_ORDERS:"; |
|||
|
|||
@Autowired |
|||
private RedisTemplateHelper redisTemplateHelper; |
|||
|
|||
// ================================================================
|
|||
// Key 构建
|
|||
// ================================================================
|
|||
|
|||
private String buildKey(String workerId) { |
|||
return KEY_PREFIX + workerId; |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 存放(put)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 存放单个订单到缓存 |
|||
* |
|||
* @param workerId 配送员ID |
|||
* @param orderVO 订单VO(必须包含有效的 id) |
|||
*/ |
|||
public void put(String workerId, MallDeliveryOrder orderVO) { |
|||
if (StringUtils.isBlank(workerId) || orderVO == null || StringUtils.isBlank(orderVO.getId())) { |
|||
log.info("UserPendingOrderCacheUtil.put 参数无效, userId={}, orderVO={}", workerId, orderVO); |
|||
return; |
|||
} |
|||
try { |
|||
String key = buildKey(workerId); |
|||
String json = JSONUtil.toJsonStr(orderVO); |
|||
redisTemplateHelper.hPut(key, orderVO.getId(), json); |
|||
log.info("缓存配送员订单: workerId={}, orderId={}", workerId, orderVO.getId()); |
|||
} catch (Exception e) { |
|||
log.info("缓存配送员订单失败: workerId={}, orderId={}", workerId, orderVO.getId(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 批量存放订单到缓存(通常用于缓存预热 / 首次加载) |
|||
* |
|||
* @param workerId 配送员ID |
|||
* @param orders 订单列表 |
|||
*/ |
|||
public void putAll(String workerId, List<MallDeliveryOrder> orders) { |
|||
if (StringUtils.isBlank(workerId) || orders == null || orders.isEmpty()) { |
|||
return; |
|||
} |
|||
try { |
|||
String key = buildKey(workerId); |
|||
Map<String, String> map = new java.util.LinkedHashMap<>(orders.size()); |
|||
for (MallDeliveryOrder order : orders) { |
|||
if (order != null && StringUtils.isNotBlank(order.getId())) { |
|||
map.put(order.getId(), JSONUtil.toJsonStr(order)); |
|||
} |
|||
} |
|||
if (!map.isEmpty()) { |
|||
redisTemplateHelper.hPutAll(key, map); |
|||
log.info("批量缓存配送员订单: workerId={}, count={}", workerId, map.size()); |
|||
} |
|||
} catch (Exception e) { |
|||
log.info("批量缓存配送员订单失败: workerId={}", workerId, e); |
|||
} |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 删除(remove)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 根据 workerId 和 orderId 从缓存中删除指定订单 |
|||
* <p> |
|||
* 典型场景:订单成团、取消、退款成功后调用。 |
|||
* |
|||
* @param workerId 配送员ID |
|||
* @param orderId 订单ID |
|||
*/ |
|||
public void remove(String workerId, String orderId) { |
|||
if (StringUtils.isBlank(workerId) || StringUtils.isBlank(orderId)) { |
|||
log.info("UserPendingOrderCacheUtil.remove 参数无效, workerId={}, orderId={}", workerId, orderId); |
|||
return; |
|||
} |
|||
try { |
|||
String key = buildKey(workerId); |
|||
redisTemplateHelper.hDelete(key, orderId); |
|||
log.info("删除订单缓存: workerId={}, orderId={}", workerId, orderId); |
|||
} catch (Exception e) { |
|||
log.info("删除订单缓存失败: workerId={}, orderId={}", workerId, orderId, e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除指定配送员所有订单缓存 |
|||
* <p> |
|||
* 典型场景:需要强制刷新该配送员缓存时调用。 |
|||
* |
|||
* @param workerId 配送员ID |
|||
*/ |
|||
public void removeAll(String workerId) { |
|||
if (StringUtils.isBlank(workerId)) { |
|||
return; |
|||
} |
|||
try { |
|||
redisTemplateHelper.delete(buildKey(workerId)); |
|||
log.info("清除配送员订单缓存: workerId={}", workerId); |
|||
} catch (Exception e) { |
|||
log.info("清除配送员订单缓存失败: workerId={}", workerId, e); |
|||
} |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 更新(update)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 根据 workerId 和 orderId 更新缓存中的订单信息 |
|||
* <p> |
|||
* 如果更新后的订单状态已经不属于"待完成"(status ∈ {10}), |
|||
* 则自动从缓存中移除该订单,无需调用方额外判断。 |
|||
* |
|||
* @param workerId 配送员ID |
|||
* @param orderVO 更新后的订单VO |
|||
*/ |
|||
public void update(String workerId, MallDeliveryOrder orderVO) { |
|||
if (StringUtils.isBlank(workerId) || orderVO == null || StringUtils.isBlank(orderVO.getId())) { |
|||
log.warn("UserPendingOrderCacheUtil.update 参数无效, workerId={}, orderVO={}", workerId, orderVO); |
|||
return; |
|||
} |
|||
try { |
|||
// 如果订单状态已不属于,直接删除
|
|||
/*if (!isTerminalStatus(orderVO.getStatus())) { |
|||
remove(workerId, orderVO.getId()); |
|||
log.debug("订单已终态,从缓存移除: workerId={}, orderId={}, status={}", |
|||
workerId, orderVO.getId(), orderVO.getStatus()); |
|||
return; |
|||
}*/ |
|||
// 覆盖更新
|
|||
put(workerId, orderVO); |
|||
log.info("更新配送员缓存: workerId={}, orderId={}, status={}", |
|||
workerId, orderVO.getId(), orderVO.getStatus()); |
|||
} catch (Exception e) { |
|||
log.info("更新用户待完成订单缓存失败: workerId={}, orderId={}", workerId, orderVO.getId(), e); |
|||
} |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 查询(get)
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 获取配送员订单缓存 |
|||
* |
|||
* @param q 配送员当前订单查询实体 deliveryType/pageNum/pageSize/regionId/status/workerId |
|||
* @return 缓存的订单列表,缓存不存在时返回 null(调用方可据此判断是否需要回源查库) |
|||
*/ |
|||
public IPage<MallDeliveryOrder> getAll(MallDeliveryOrderPageQuery q) { |
|||
// 1. 基础校验
|
|||
if (StringUtils.isBlank(q.getWorkerId())) { |
|||
return null; |
|||
} |
|||
|
|||
try { |
|||
String key = buildKey(q.getWorkerId()); |
|||
// 2. 获取 Hash 中的所有订单数据
|
|||
Map<Object, Object> entries = redisTemplateHelper.hGetAll(key); |
|||
|
|||
// 如果缓存中没有数据,返回 null 让调用方去查库
|
|||
if (entries == null || entries.isEmpty()) { |
|||
return null; |
|||
} |
|||
|
|||
// 3. 反序列化并转换为 List
|
|||
List<MallDeliveryOrder> allCachedOrders = new ArrayList<>(entries.size()); |
|||
for (Object value : entries.values()) { |
|||
if (value != null) { |
|||
// 假设 JSONUtil 是 Hutool 或类似工具类
|
|||
allCachedOrders.add(JSONUtil.toBean(value.toString(), MallDeliveryOrder.class)); |
|||
} |
|||
} |
|||
|
|||
// 4. 内存过滤 (根据 deliveryType, regionId, status)
|
|||
List<MallDeliveryOrder> filteredList = allCachedOrders.stream() |
|||
.filter(order -> { |
|||
// 状态过滤
|
|||
if (q.getStatus() != null && !Objects.equals(order.getStatus(), q.getStatus())) { |
|||
return false; |
|||
} |
|||
// 配送类型过滤
|
|||
if (q.getDeliveryType() != null && !Objects.equals(order.getDeliveryType(), q.getDeliveryType())) { |
|||
return false; |
|||
} |
|||
// 区域ID过滤
|
|||
if (StringUtils.isNotBlank(q.getRegionId()) && !Objects.equals(order.getRegionId(), q.getRegionId())) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}) |
|||
.collect(Collectors.toList()); |
|||
filteredList.sort(Comparator.comparing(MallDeliveryOrder::getMustFinishTime, |
|||
Comparator.nullsLast(Comparator.naturalOrder()))); |
|||
|
|||
// 5. 内存分页
|
|||
// 初始化分页对象
|
|||
Integer pageNum = q.getPageNum(); |
|||
Integer pageSize = q.getPageSize(); |
|||
|
|||
IPage<MallDeliveryOrder> page = new Page<>(pageNum, pageSize); |
|||
|
|||
// 计算总记录数
|
|||
long total = filteredList.size(); |
|||
page.setTotal(total); |
|||
|
|||
// 计算当前页的数据
|
|||
if (total > 0) { |
|||
long offset = (pageNum - 1) * pageSize; |
|||
// 防止越界
|
|||
if (offset >= total) { |
|||
page.setRecords(Collections.emptyList()); |
|||
} else { |
|||
int start = (int) Math.min(offset, total); |
|||
int end = (int) Math.min(offset + pageSize, total); |
|||
page.setRecords(filteredList.subList(start, end)); |
|||
} |
|||
} else { |
|||
page.setRecords(Collections.emptyList()); |
|||
} |
|||
|
|||
return page; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("获取配送员订单缓存失败: workerId={}", q.getWorkerId(), e); |
|||
// 发生异常返回 null,强制回源查库,保证系统可用性
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取配送员缓存单个订单 |
|||
* |
|||
* @param workerId 配送员ID |
|||
* @param orderId 订单ID |
|||
* @return 缓存的订单VO,不存在时返回 null |
|||
*/ |
|||
public MallDeliveryOrder get(String workerId, String orderId) { |
|||
if (StringUtils.isBlank(workerId) || StringUtils.isBlank(orderId)) { |
|||
return null; |
|||
} |
|||
try { |
|||
String key = buildKey(workerId); |
|||
Object value = redisTemplateHelper.hGet(key, orderId); |
|||
if (value == null) { |
|||
return null; |
|||
} |
|||
return JSONUtil.toBean(value.toString(), MallDeliveryOrder.class); |
|||
} catch (Exception e) { |
|||
log.info("获取配送员单个订单缓存失败: workerId={}, orderId={}", workerId, orderId, e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 判断该配送员的缓存是否已存在 |
|||
* |
|||
* @param workerId 配送员ID |
|||
* @return true 表示缓存存在 |
|||
*/ |
|||
public boolean exists(String workerId) { |
|||
if (StringUtils.isBlank(workerId)) { |
|||
return false; |
|||
} |
|||
Boolean hasKey = redisTemplateHelper.hasKey(buildKey(workerId)); |
|||
return hasKey != null && hasKey; |
|||
} |
|||
|
|||
// ================================================================
|
|||
// 内部工具
|
|||
// ================================================================
|
|||
|
|||
/** |
|||
* 判断订单状态是否为终态(不再属于"") |
|||
*/ |
|||
/* private boolean isTerminalStatus(Integer status) { |
|||
if (status == null) { |
|||
return false; |
|||
} |
|||
return status == STATUS_DONE; |
|||
}*/ |
|||
} |
|||
Loading…
Reference in new issue