RiderDeliveryAcceptanceService.java 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package com.ruoyi.app.order;
  2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  3. import com.ruoyi.common.core.domain.entity.SysDictData;
  4. import com.ruoyi.common.exception.ServiceException;
  5. import com.ruoyi.common.utils.DictUtils;
  6. import com.ruoyi.common.utils.MessageUtils;
  7. import com.ruoyi.system.domain.InfoUser;
  8. import com.ruoyi.system.domain.PosOrder;
  9. import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
  10. import com.ruoyi.system.mapper.OrderCheckoutMapper;
  11. import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
  12. import com.ruoyi.system.service.IInfoUserService;
  13. import com.ruoyi.system.service.IPosOrderService;
  14. import org.slf4j.Logger;
  15. import org.slf4j.LoggerFactory;
  16. import org.springframework.stereotype.Service;
  17. import java.util.List;
  18. import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.ACCEPTED;
  19. import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.PICKED_UP;
  20. /**
  21. * 骑手接单统一策略:在骑手级锁内检查账号资格、配送类型、进行中任务容量和急送独占。
  22. * 调用方必须先取得 {@link RiderDeliveryLockService} 的同一骑手锁,避免并发请求同时绕过容量检查。
  23. */
  24. @Service
  25. public class RiderDeliveryAcceptanceService {
  26. static final String FOOD_LIMIT_DICT = "sys_rider_food_active_order_limit";
  27. static final String FLASH_LIMIT_DICT = "sys_rider_flash_active_order_limit";
  28. static final String TOTAL_LIMIT_DICT = "sys_rider_total_active_order_limit";
  29. private static final String URGENT = "URGENT";
  30. private static final int DEFAULT_LIMIT = 1;
  31. private static final int MAX_LIMIT = 20;
  32. private static final Logger log = LoggerFactory.getLogger(RiderDeliveryAcceptanceService.class);
  33. private final IInfoUserService riders;
  34. private final IPosOrderService foodOrders;
  35. private final FlashDeliveryOrderMapper flashOrders;
  36. private final OrderCheckoutMapper checkout;
  37. public RiderDeliveryAcceptanceService(IInfoUserService riders,
  38. IPosOrderService foodOrders,
  39. FlashDeliveryOrderMapper flashOrders) {
  40. this(riders, foodOrders, flashOrders, null);
  41. }
  42. @org.springframework.beans.factory.annotation.Autowired
  43. public RiderDeliveryAcceptanceService(IInfoUserService riders,
  44. IPosOrderService foodOrders,
  45. FlashDeliveryOrderMapper flashOrders,
  46. OrderCheckoutMapper checkout) {
  47. this.riders = riders;
  48. this.foodOrders = foodOrders;
  49. this.flashOrders = flashOrders;
  50. this.checkout = checkout;
  51. }
  52. /** 校验骑手能否再承接一笔外卖;进行中外卖和跨类型总量均受字典上限控制。 */
  53. public void assertCanAcceptFood(Long riderId) {
  54. requireEligibleRider(riderId, "FOOD");
  55. ActiveTasks active = activeTasks(riderId);
  56. if (active.urgentFlash() > 0) throw exclusiveConflict();
  57. if (active.food() >= configuredLimit(FOOD_LIMIT_DICT)) {
  58. throw new ServiceException(MessageUtils.message("rider.accept.food.limit.reached"));
  59. }
  60. assertTotalCapacity(active);
  61. }
  62. /** 校验骑手能否承接闪送;加急闪送保持跨外卖、闪送的一对一独占。 */
  63. public void assertCanAcceptFlash(Long riderId, String deliveryType) {
  64. requireEligibleRider(riderId, "FLASH");
  65. ActiveTasks active = activeTasks(riderId);
  66. if (URGENT.equals(deliveryType)) {
  67. if (active.food() > 0 || active.flash() > 0) throw exclusiveConflict();
  68. return;
  69. }
  70. if (active.urgentFlash() > 0) throw exclusiveConflict();
  71. if (active.flash() >= configuredLimit(FLASH_LIMIT_DICT)) {
  72. throw new ServiceException(MessageUtils.message("rider.accept.flash.limit.reached"));
  73. }
  74. assertTotalCapacity(active);
  75. }
  76. /**
  77. * 统一账号资格口径。空配送类型仍沿用 InfoUser 的历史兼容规则,但停用、未审核和离线账号均不得接单。
  78. */
  79. private InfoUser requireEligibleRider(Long riderId, String deliveryType) {
  80. InfoUser rider = riderId == null ? null : riders.getById(riderId);
  81. if (rider == null || !"2".equals(rider.getUserType())) {
  82. throw new ServiceException(MessageUtils.message("rider.operation.role.required"));
  83. }
  84. if (!"1".equals(rider.getAuditStatus())) {
  85. throw new ServiceException(MessageUtils.message("no.user.state.no.audit"));
  86. }
  87. if (!"0".equals(rider.getStatus())) {
  88. throw new ServiceException(MessageUtils.message("no.user.stop"));
  89. }
  90. if (!"0".equals(rider.getOffline())) {
  91. throw new ServiceException(MessageUtils.message("rider.accept.offline"));
  92. }
  93. if (!rider.supportsDeliveryType(deliveryType)) {
  94. String key = "FOOD".equals(deliveryType)
  95. ? "no.user.delivery.type.not.enabled"
  96. : "flash.delivery.rider.type.not.enabled";
  97. throw new ServiceException(MessageUtils.message(key));
  98. }
  99. return rider;
  100. }
  101. /** 一次读取两类进行中任务,所有容量判断使用同一数据库快照口径。 */
  102. private ActiveTasks activeTasks(Long riderId) {
  103. QueryWrapper<PosOrder> foodQuery = new QueryWrapper<PosOrder>()
  104. .eq("qs_id", riderId)
  105. .in("delivery_status", 1, 2)
  106. .in("state", 0, 1, 2)
  107. .eq("after_sale_status", 0);
  108. QueryWrapper<FlashDeliveryOrder> flashQuery = new QueryWrapper<FlashDeliveryOrder>()
  109. .eq("rider_id", riderId)
  110. .in("status", ACCEPTED, PICKED_UP);
  111. QueryWrapper<FlashDeliveryOrder> urgentQuery = new QueryWrapper<FlashDeliveryOrder>()
  112. .eq("rider_id", riderId)
  113. .in("status", ACCEPTED, PICKED_UP)
  114. .eq("delivery_type", URGENT);
  115. long rawFood = safe(foodOrders.count(foodQuery));
  116. long duplicateGroupMembers = 0;
  117. if (checkout != null && rawFood > 1) {
  118. try {
  119. duplicateGroupMembers = safe(checkout.countActiveGroupDuplicateMembers(riderId));
  120. } catch (RuntimeException unavailable) {
  121. // 036 迁移未执行或组表暂不可用时,保留普通订单的原始计数;只会更保守,不会放宽容量。
  122. log.warn("Multi-store rider group count unavailable, keep raw food order count, riderId={}", riderId, unavailable);
  123. }
  124. }
  125. return new ActiveTasks(rawFood - Math.min(rawFood, duplicateGroupMembers),
  126. safe(flashOrders.selectCount(flashQuery)), safe(flashOrders.selectCount(urgentQuery)));
  127. }
  128. private void assertTotalCapacity(ActiveTasks active) {
  129. if (active.food() + active.flash() >= configuredLimit(TOTAL_LIMIT_DICT)) {
  130. throw new ServiceException(MessageUtils.message("rider.accept.total.limit.reached"));
  131. }
  132. }
  133. /**
  134. * 容量字典只接受 1 至 20 的整数。缺失、空值、越界、解析或缓存异常一律保守回退为 1,绝不放宽为不限单。
  135. */
  136. private int configuredLimit(String dictType) {
  137. List<SysDictData> values;
  138. try {
  139. values = DictUtils.getDictCache(dictType);
  140. } catch (RuntimeException exception) {
  141. log.warn("Failed to read rider acceptance limit, fallback to 1, dictType={}", dictType, exception);
  142. return DEFAULT_LIMIT;
  143. }
  144. if (values == null || values.isEmpty()) return DEFAULT_LIMIT;
  145. String raw = values.get(0).getDictValue();
  146. if (raw == null || !raw.trim().matches("[0-9]+")) return DEFAULT_LIMIT;
  147. try {
  148. int value = Integer.parseInt(raw.trim());
  149. return value >= 1 && value <= MAX_LIMIT ? value : DEFAULT_LIMIT;
  150. } catch (NumberFormatException ignored) {
  151. return DEFAULT_LIMIT;
  152. }
  153. }
  154. private long safe(Long value) {
  155. return value == null ? 0L : value;
  156. }
  157. private ServiceException exclusiveConflict() {
  158. return new ServiceException(MessageUtils.message("flash.delivery.rider.exclusive.conflict"));
  159. }
  160. private record ActiveTasks(long food, long flash, long urgentFlash) { }
  161. }