FlashDeliveryApplicationService.java 73 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298
  1. package com.ruoyi.app.flashdelivery.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  3. import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
  4. import com.baomidou.mybatisplus.core.metadata.IPage;
  5. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  6. import com.ruoyi.app.flashdelivery.dto.*;
  7. import com.ruoyi.app.flashdelivery.exception.FlashDeliveryQuoteChangedException;
  8. import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
  9. import com.ruoyi.app.flashdelivery.route.GeoPoint;
  10. import com.ruoyi.app.flashdelivery.route.RouteDistance;
  11. import com.ruoyi.app.order.RiderDeliveryExclusivityService;
  12. import com.ruoyi.app.order.RiderDeliveryLockService;
  13. import com.ruoyi.common.core.domain.entity.SysDictData;
  14. import com.ruoyi.common.exception.ServiceException;
  15. import com.ruoyi.common.utils.DictUtils;
  16. import com.ruoyi.common.utils.MessageUtils;
  17. import com.ruoyi.common.utils.StringUtils;
  18. import com.ruoyi.system.domain.InfoUser;
  19. import com.ruoyi.system.domain.flash.*;
  20. import com.ruoyi.system.mapper.InfoUserMapper;
  21. import com.ruoyi.system.mapper.flash.*;
  22. import org.springframework.dao.DataIntegrityViolationException;
  23. import org.springframework.stereotype.Service;
  24. import org.springframework.transaction.annotation.Transactional;
  25. import java.net.URI;
  26. import java.math.BigDecimal;
  27. import java.math.RoundingMode;
  28. import java.security.SecureRandom;
  29. import java.time.Instant;
  30. import java.time.ZoneId;
  31. import java.time.format.DateTimeFormatter;
  32. import java.util.*;
  33. import java.util.function.Function;
  34. import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
  35. /**
  36. * 闪送业务编排服务。
  37. *
  38. * <p>负责报价、订单创建、抢单、履约状态流转、平台介入和审计日志。
  39. * 订单身份只接受 Controller 从 token 解析后的用户 ID,客户端传入的金额、距离或操作人均不可信。</p>
  40. */
  41. @Service
  42. public class FlashDeliveryApplicationService {
  43. private static final List<String> SERVICE_TYPES = List.of("HELP_SEND", "HELP_PICKUP");
  44. private static final Set<String> DELIVERY_TYPES = Set.of("NORMAL", "URGENT");
  45. private static final Set<String> PACKAGE_TYPES = Set.of("DOCUMENT", "GIFT", "CLOTHING", "BEAUTY",
  46. "DAILY_NECESSITIES", "FOOD_INGREDIENTS", "ELECTRONICS", "SMALL_APPLIANCE", "OTHER");
  47. private static final Set<String> WEIGHT_RANGES = Set.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
  48. "OVER_10_TO_15_KG", "OVER_15_TO_20_KG");
  49. private static final Set<String> DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
  50. /** 支付方式取值与主订单系统同域:4=现金、6=线下转账(027 线下转账支付)。 */
  51. private static final String PAY_TYPE_CASH = "4";
  52. private static final String PAY_TYPE_TRANSFER = "6";
  53. private static final DateTimeFormatter PRICING_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
  54. private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
  55. private static final int MAX_DISTANCE_METERS = 40_000;
  56. private static final BigDecimal MAX_PRICING_DISTANCE = new BigDecimal("999999.99");
  57. private static final long SCHEDULE_SLOT_MILLIS = 30L * 60L * 1000L;
  58. private static final long MAX_SCHEDULE_DELAY_MILLIS = 3L * 24L * 60L * 60L * 1000L;
  59. private static final SecureRandom PIN_RANDOM = new SecureRandom();
  60. private final FlashDeliveryOrderMapper orderMapper;
  61. private final FlashDeliveryPricingMapper pricingMapper;
  62. private final FlashDeliveryOrderImageMapper imageMapper;
  63. private final FlashDeliveryOrderLogMapper logMapper;
  64. private final InfoUserMapper userMapper;
  65. private final FlashDeliveryRouteService routeService;
  66. private final FlashDeliveryPricingCalculator pricingCalculator;
  67. private final RiderDeliveryLockService riderDeliveryLockService;
  68. private final RiderDeliveryExclusivityService riderDeliveryExclusivityService;
  69. public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
  70. FlashDeliveryPricingMapper pricingMapper,
  71. FlashDeliveryOrderImageMapper imageMapper,
  72. FlashDeliveryOrderLogMapper logMapper,
  73. InfoUserMapper userMapper,
  74. FlashDeliveryRouteService routeService,
  75. FlashDeliveryPricingCalculator pricingCalculator,
  76. RiderDeliveryLockService riderDeliveryLockService,
  77. RiderDeliveryExclusivityService riderDeliveryExclusivityService) {
  78. this.orderMapper = orderMapper;
  79. this.pricingMapper = pricingMapper;
  80. this.imageMapper = imageMapper;
  81. this.logMapper = logMapper;
  82. this.userMapper = userMapper;
  83. this.routeService = routeService;
  84. this.pricingCalculator = pricingCalculator;
  85. this.riderDeliveryLockService = riderDeliveryLockService;
  86. this.riderDeliveryExclusivityService = riderDeliveryExclusivityService;
  87. }
  88. /** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
  89. public FlashDeliveryHomeView home() {
  90. FlashDeliveryHomeView view = new FlashDeliveryHomeView();
  91. view.setServiceTypes(SERVICE_TYPES);
  92. view.setDeliveryTypes(List.of("NORMAL", "URGENT"));
  93. FlashDeliveryPricing pricing = findPricingAtOrNull(pricingTargetTime(new Date()));
  94. view.setServices(pricing == null ? List.of() : SERVICE_TYPES.stream()
  95. .map(serviceType -> serviceView(pricing, serviceType)).toList());
  96. return view;
  97. }
  98. /** 实时报价:校验请求后按服务端路线距离和命中时段的运价计算费用(立即单取当前时段、预约单取预约开始时刻),不写库。 */
  99. public FlashDeliveryQuoteView quote(FlashDeliveryQuoteRequest request) {
  100. // 报价只使用服务端路线距离和当前启用的计价配置,避免客户端篡改金额。
  101. QuoteContext context = calculateQuote(request);
  102. return quoteView(request, context);
  103. }
  104. /** 将报价上下文组装为对外报价视图,含运价快照字段与费用明细。 */
  105. private FlashDeliveryQuoteView quoteView(FlashDeliveryQuoteRequest request, QuoteContext context) {
  106. FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
  107. view.setServiceType(request.getServiceType());
  108. view.setDeliveryType(request.getDeliveryType());
  109. view.setDeliveryMode(normalizeDeliveryMode(request.getDeliveryMode()));
  110. view.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
  111. view.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
  112. view.setPricingId(context.pricing().getId());
  113. view.setStartTime(context.pricing().getStartTime());
  114. view.setEndTime(context.pricing().getEndTime());
  115. view.setDistanceMeters(context.route().distanceMeters());
  116. view.setDistanceSource(context.route().source());
  117. view.setEstimatedDurationSeconds(context.route().durationSeconds());
  118. view.setStartingDistance(context.pricing().getStartingDistance());
  119. view.setStartingFare(context.pricing().getStartingFare());
  120. view.setDistance(context.pricing().getDistance());
  121. view.setFreight(context.pricing().getFreight());
  122. view.setUrgentRate(context.pricing().getUrgentRate());
  123. view.setMinimumUrgentFee(context.pricing().getMinimumUrgentFee());
  124. applyBreakdown(view, context.breakdown());
  125. view.setCurrency("TWD");
  126. view.setPricingVersion(context.pricing().getConfigVersion());
  127. return view;
  128. }
  129. /**
  130. * 幂等创建订单:clientRequestId 与 userId 构成唯一键,重复提交直接返回已有订单。
  131. * 创建前按最新条件重新报价,客户端回传报价不一致时抛出 FlashDeliveryQuoteChangedException;
  132. * 成功后保存计价快照、寄件图片和初始状态日志。
  133. */
  134. @Transactional
  135. public FlashDeliveryOrderDetailView create(Long userId, FlashDeliveryCreateRequest request) {
  136. requireUserId(userId);
  137. if (request == null || !hasText(request.getClientRequestId())
  138. || request.getClientRequestId().trim().length() > 64) {
  139. throw fail("flash.delivery.client.request.id.invalid");
  140. }
  141. String requestId = request.getClientRequestId().trim();
  142. // 用户 ID + clientRequestId 构成幂等键;重复请求直接返回第一次创建的订单。
  143. FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
  144. if (existing != null) return participantDetail(existing, true, false);
  145. if (request.getUserNote() != null && request.getUserNote().length() > 500) {
  146. throw fail("flash.delivery.note.too.long");
  147. }
  148. QuoteContext quote = calculateQuote(request);
  149. FlashDeliveryQuoteView latestQuote = quoteView(request, quote);
  150. if (!matchesQuotedPrice(request, latestQuote)) {
  151. throw new FlashDeliveryQuoteChangedException(latestQuote);
  152. }
  153. Date now = new Date();
  154. String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
  155. validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
  156. boolean pinRequired = request.getPinRequired() == null || request.getPinRequired();
  157. String payType = normalizePayType(request.getPayType());
  158. List<String> senderImages = validateProofUrls(request.getSenderImageUrls(), false);
  159. FlashDeliveryOrder order = new FlashDeliveryOrder();
  160. order.setOrderNo("FD" + UUID.randomUUID().toString().replace("-", "").substring(0, 24).toUpperCase(Locale.ROOT));
  161. order.setClientRequestId(requestId);
  162. order.setUserId(userId);
  163. order.setServiceType(request.getServiceType());
  164. order.setDeliveryType(request.getDeliveryType());
  165. order.setStatus(WAITING_ACCEPTANCE);
  166. order.setPackageType(request.getPackageType());
  167. order.setQuantity(request.getQuantity());
  168. order.setWeightRange(request.getWeightRange());
  169. order.setSpecification(trimToNull(request.getSpecification()));
  170. order.setDeliveryMode(deliveryMode);
  171. order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
  172. order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
  173. order.setPinRequired(pinRequired);
  174. order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
  175. copyAddress(request.getPickup(), order, true);
  176. copyAddress(request.getDelivery(), order, false);
  177. order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
  178. order.setDistanceMeters(quote.route().distanceMeters());
  179. order.setDistanceSource(quote.route().source());
  180. order.setEstimatedDurationSeconds(quote.route().durationSeconds());
  181. order.setAmount(quote.breakdown().getAmount());
  182. order.setCurrency("TWD");
  183. order.setPayType(payType);
  184. order.setPricingId(quote.pricing().getId());
  185. order.setPricingVersion(quote.pricing().getConfigVersion());
  186. order.setPricingStartTime(quote.pricing().getStartTime());
  187. order.setPricingEndTime(quote.pricing().getEndTime());
  188. order.setStartingDistance(quote.pricing().getStartingDistance());
  189. order.setStartingFare(quote.pricing().getStartingFare());
  190. order.setDistance(quote.pricing().getDistance());
  191. order.setFreight(quote.pricing().getFreight());
  192. // 保存计价快照,后台调价只影响新订单,不追溯修改历史订单金额。
  193. order.setDistanceFee(quote.breakdown().getDistanceFee());
  194. order.setBaseDeliveryFee(quote.breakdown().getBaseDeliveryFee());
  195. order.setUrgentRate(quote.pricing().getUrgentRate());
  196. order.setMinimumUrgentFee(quote.pricing().getMinimumUrgentFee());
  197. order.setUrgentFee(quote.breakdown().getUrgentFee());
  198. order.setTipAmount(quote.breakdown().getTipAmount());
  199. order.setUserNote(trimToNull(request.getUserNote()));
  200. order.setVersion(0);
  201. order.setCreateTime(now);
  202. order.setUpdateTime(now);
  203. try {
  204. orderMapper.insert(order);
  205. } catch (DataIntegrityViolationException duplicate) {
  206. // 并发重复请求可能同时通过前置查询,最终由数据库唯一键完成幂等收口。
  207. FlashDeliveryOrder concurrent = orderMapper.selectByUserRequestId(userId, requestId);
  208. if (concurrent != null) return participantDetail(concurrent, true, false);
  209. throw duplicate;
  210. }
  211. saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
  212. writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
  213. return participantDetail(order, true, false);
  214. }
  215. /** 分页查询当前用户参与的订单卡片;role=sender 按寄件人、receiver 按收件人过滤。 */
  216. public IPage<FlashDeliveryUserOrderListView> userOrders(Long userId, int pageNum, int pageSize, String role) {
  217. String selectedRole = hasText(role) ? role.trim().toLowerCase(Locale.ROOT) : "sender";
  218. if (!"sender".equals(selectedRole) && !"receiver".equals(selectedRole)) {
  219. throw fail("flash.delivery.tab.invalid");
  220. }
  221. String participantColumn = "receiver".equals(selectedRole) ? "receiver_user_id" : "user_id";
  222. QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
  223. .eq(participantColumn, userId).orderByDesc("create_time");
  224. return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), this::userOrderListView);
  225. }
  226. /** 查询本人订单详情,仅寄件人和匹配到的收件人可见。 */
  227. public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
  228. FlashDeliveryOrder order = requireOrder(orderId);
  229. if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
  230. return participantDetail(order, true, false);
  231. }
  232. /** 地址修改先报价,用户确认前不改动订单。 */
  233. public FlashDeliveryQuoteView quoteAddress(Long userId, Long orderId,
  234. FlashDeliveryAddressChangeRequest request) {
  235. if (request == null) throw fail("flash.delivery.request.required");
  236. FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
  237. return calculateAddressQuote(order, request);
  238. }
  239. /** 确认修改待接单订单地址:重算报价并与回传值比对,按版本条件更新。 */
  240. @Transactional
  241. public FlashDeliveryOrderDetailView updateAddress(Long userId, Long orderId,
  242. FlashDeliveryAddressConfirmRequest request) {
  243. if (request == null) throw fail("flash.delivery.request.required");
  244. FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
  245. FlashDeliveryQuoteView quote = calculateAddressQuote(order, request);
  246. if (!Objects.equals(request.getQuotedDistanceMeters(), quote.getDistanceMeters())
  247. || !Objects.equals(request.getQuotedBaseDeliveryFee(), quote.getBaseDeliveryFee())
  248. || !Objects.equals(request.getQuotedDistanceFee(), quote.getDistanceFee())
  249. || !Objects.equals(request.getQuotedUrgentFee(), quote.getUrgentFee())
  250. || !Objects.equals(request.getQuotedAmount(), quote.getAmount())) {
  251. throw new FlashDeliveryQuoteChangedException(quote);
  252. }
  253. copyAddress(request.getPickup(), order, true);
  254. copyAddress(request.getDelivery(), order, false);
  255. order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
  256. order.setDistanceMeters(quote.getDistanceMeters());
  257. order.setDistanceSource(quote.getDistanceSource());
  258. order.setEstimatedDurationSeconds(quote.getEstimatedDurationSeconds());
  259. // 运价快照与金额同事务刷新为本次计价依据(快照=金额的最后一次计算依据,而非最初依据)
  260. order.setPricingId(quote.getPricingId());
  261. order.setPricingVersion(quote.getPricingVersion());
  262. order.setPricingStartTime(quote.getStartTime());
  263. order.setPricingEndTime(quote.getEndTime());
  264. order.setStartingDistance(quote.getStartingDistance());
  265. order.setStartingFare(quote.getStartingFare());
  266. order.setDistance(quote.getDistance());
  267. order.setFreight(quote.getFreight());
  268. order.setUrgentRate(quote.getUrgentRate());
  269. order.setMinimumUrgentFee(quote.getMinimumUrgentFee());
  270. order.setDistanceFee(quote.getDistanceFee());
  271. order.setBaseDeliveryFee(quote.getBaseDeliveryFee());
  272. order.setUrgentFee(quote.getUrgentFee());
  273. order.setAmount(quote.getAmount());
  274. Date now = new Date();
  275. order.setUpdateTime(now);
  276. // 即使上面的查询仍显示待接单,骑手也可能已接走,最终以条件 UPDATE 为准。
  277. if (orderMapper.updateWaitingAddress(order, request.getOrderVersion()) != 1) {
  278. throw fail("flash.delivery.state.changed");
  279. }
  280. order.setVersion(order.getVersion() + 1);
  281. writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "ADDRESS_UPDATED", now);
  282. return participantDetail(order, true, false);
  283. }
  284. /** 待接单订单追加小费:订单总额与小费同步增加,版本条件防止并发重复加款。 */
  285. @Transactional
  286. public FlashDeliveryOrderDetailView addTip(Long userId, Long orderId, FlashDeliveryTipAddRequest request) {
  287. if (request == null) throw fail("flash.delivery.request.required");
  288. FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
  289. if (request.getAdditionalTipAmount() == null || request.getAdditionalTipAmount().signum() <= 0) {
  290. throw fail("flash.delivery.tip.additional.invalid");
  291. }
  292. long tip;
  293. long amount;
  294. try {
  295. // 保留 JSON 原始数值,禁止 1.9 等小数在绑定时被截断为整数小费。
  296. long additionalTip = request.getAdditionalTipAmount().longValueExact();
  297. tip = Math.addExact(order.getTipAmount(), additionalTip);
  298. amount = Math.addExact(order.getAmount(), additionalTip);
  299. } catch (ArithmeticException exception) {
  300. throw fail("flash.delivery.tip.additional.invalid");
  301. }
  302. order.setTipAmount(tip);
  303. order.setAmount(amount);
  304. Date now = new Date();
  305. order.setUpdateTime(now);
  306. if (orderMapper.updateWaitingTip(order, request.getOrderVersion()) != 1) {
  307. throw fail("flash.delivery.state.changed");
  308. }
  309. order.setVersion(order.getVersion() + 1);
  310. writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "TIP_ADDED", now);
  311. return participantDetail(order, true, false);
  312. }
  313. /** 校验并加载可编辑订单:本人、待接单、未绑定骑手且版本一致。 */
  314. private FlashDeliveryOrder requireEditableOrder(Long userId, Long orderId, Integer version) {
  315. requireUserId(userId);
  316. if (orderId == null || orderId <= 0) throw fail("flash.delivery.order.not.found");
  317. FlashDeliveryOrder order = requireOrder(orderId);
  318. if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
  319. if (!WAITING_ACCEPTANCE.equals(order.getStatus()) || order.getRiderId() != null) {
  320. throw fail("flash.delivery.edit.not.allowed");
  321. }
  322. if (version == null || version < 0) throw fail("flash.delivery.order.version.invalid");
  323. if (!Objects.equals(version, order.getVersion())) throw fail("flash.delivery.state.changed");
  324. return order;
  325. }
  326. /** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
  327. private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
  328. FlashDeliveryAddressChangeRequest request) {
  329. validateAddress(request.getPickup());
  330. validateAddress(request.getDelivery());
  331. if (sameAddressText(request.getPickup(), request.getDelivery())
  332. || request.getPickup().getLatitude().compareTo(request.getDelivery().getLatitude()) == 0
  333. && request.getPickup().getLongitude().compareTo(request.getDelivery().getLongitude()) == 0) {
  334. throw fail("flash.delivery.address.same");
  335. }
  336. RouteDistance route = routeService.calculate(
  337. new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
  338. new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
  339. if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
  340. // 待接单尚未成交,改址按"订单定价时点"的最新运价重算(预约单取预约取件时刻、
  341. // 立即单取当前),管理员调价对未接单的改址立即生效;时段语义保住,预约单不会用到错误时段的价格。
  342. Date pricingDate = "SCHEDULED".equals(order.getDeliveryMode())
  343. ? order.getScheduledPickupStartAt() : new Date();
  344. FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate));
  345. FlashDeliveryPriceBreakdown breakdown;
  346. try {
  347. breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
  348. order.getDeliveryType(), order.getTipAmount());
  349. } catch (ArithmeticException exception) {
  350. throw fail("flash.delivery.tip.invalid");
  351. }
  352. FlashDeliveryQuoteRequest quoteRequest = new FlashDeliveryQuoteRequest();
  353. quoteRequest.setServiceType(order.getServiceType());
  354. quoteRequest.setDeliveryType(order.getDeliveryType());
  355. quoteRequest.setDeliveryMode(order.getDeliveryMode());
  356. quoteRequest.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  357. quoteRequest.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  358. FlashDeliveryQuoteView view = quoteView(quoteRequest, new QuoteContext(pricing, route, breakdown));
  359. view.setOrderVersion(order.getVersion());
  360. return view;
  361. }
  362. /** 用户取消:仅待接单或已接单(实际取件前)允许。 */
  363. @Transactional
  364. public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
  365. FlashDeliveryOrder order = requireOrder(orderId);
  366. if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
  367. if (!FlashDeliveryStateMachine.canUserCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
  368. String reason = requiredReason(request);
  369. Date now = new Date();
  370. if (orderMapper.cancel(orderId, order.getStatus(), "USER", userId, reason, now) != 1) {
  371. throw fail("flash.delivery.state.changed");
  372. }
  373. writeLog(orderId, order.getStatus(), CANCELLED, "USER", userId, reason, now);
  374. }
  375. /** 用户(寄件人或收件人)确认收货:已送达推进为已完成。 */
  376. @Transactional
  377. public void confirmReceipt(Long userId, Long orderId) {
  378. FlashDeliveryOrder order = requireOrder(orderId);
  379. if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
  380. Date now = new Date();
  381. if (orderMapper.transitionByParticipant(orderId, userId, DELIVERED, COMPLETED, now) != 1) {
  382. throw fail("flash.delivery.complete.not.allowed");
  383. }
  384. writeLog(orderId, DELIVERED, COMPLETED, "USER", userId, null, now);
  385. }
  386. /**
  387. * 骑手任务列表:按页签查询(newTask 可抢任务、toPickup 待取件、delivering 配送中、
  388. * completed 已送达/已完成、cancelled 已取消)。newTask 页签额外按骑手坐标做距离过滤和就近排序,
  389. * 并返回附近任务数与最高订单金额;其余页签按订单归属骑手查询。
  390. */
  391. public FlashDeliveryRiderOrderPageView riderOrders(Long riderId, int pageNum, int pageSize,
  392. String tab, BigDecimal longitude, BigDecimal latitude) {
  393. InfoUser rider = requireRider(riderId);
  394. if (!hasText(tab)) throw fail("flash.delivery.tab.invalid");
  395. String selectedTab = tab.trim();
  396. // lambda 无法捕获被重新赋值的参数,统一改用仅 newTask 携带坐标的有效变量。
  397. BigDecimal effectiveLongitude = "newTask".equals(selectedTab) ? longitude : null;
  398. BigDecimal effectiveLatitude = "newTask".equals(selectedTab) ? latitude : null;
  399. if ("newTask".equals(selectedTab)) {
  400. // 新任务派单按骑手选择的配送类型过滤;本人已接订单页签不受限制。
  401. if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
  402. validateRiderCoordinates(effectiveLongitude, effectiveLatitude);
  403. }
  404. QueryWrapper<FlashDeliveryOrder> query = riderOrdersQuery(riderId, selectedTab,
  405. effectiveLongitude, effectiveLatitude, true);
  406. IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
  407. FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
  408. result.setRecords(orders.getRecords().stream()
  409. .map(order -> riderOrderListView(order, effectiveLongitude, effectiveLatitude)).toList());
  410. result.setTotal(orders.getTotal());
  411. result.setCurrent(orders.getCurrent());
  412. result.setSize(orders.getSize());
  413. if ("newTask".equals(selectedTab)) {
  414. result.setNearbyTaskCount(orders.getTotal());
  415. QueryWrapper<FlashDeliveryOrder> maximumQuery = riderOrdersQuery(riderId, selectedTab,
  416. effectiveLongitude, effectiveLatitude, false)
  417. .select("MAX(amount)");
  418. result.setHighestOrderAmount(firstLong(orderMapper.selectObjs(maximumQuery)));
  419. }
  420. return result;
  421. }
  422. /** 骑手任务详情:抢单前返回脱敏视图(隐藏联系人、电话、坐标和 PIN);接单后仅中单骑手可见完整信息。 */
  423. public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
  424. InfoUser rider = requireRider(riderId);
  425. FlashDeliveryOrder order = requireOrder(orderId);
  426. if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
  427. // 不兼容闪送的骑手按订单不存在处理,避免泄露待抢任务信息。
  428. if (!isAvailableAt(order, new Date()) || !rider.supportsDeliveryType("FLASH")) {
  429. throw fail("flash.delivery.order.not.found");
  430. }
  431. // 接单前显示取送文字地址,但隐藏联系人、电话、精确坐标、实际 PIN 和履约图片。
  432. return participantDetail(order, false, true);
  433. }
  434. if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
  435. return participantDetail(order, false, false);
  436. }
  437. /** 原子抢单:骑手锁内完成互斥校验与条件更新,同一订单仅一名骑手成功。 */
  438. @Transactional
  439. public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
  440. InfoUser rider = requireRider(riderId);
  441. // 抢单属于获取新任务,要求骑手承接闪送配送。
  442. if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
  443. return riderDeliveryLockService.withLock(riderId, () -> {
  444. FlashDeliveryOrder target = requireOrder(orderId);
  445. riderDeliveryExclusivityService.assertCanAcceptFlash(riderId, target.getDeliveryType());
  446. Date now = new Date();
  447. // Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
  448. if (orderMapper.accept(orderId, riderId, now) != 1) {
  449. throw fail("flash.delivery.order.already.accepted");
  450. }
  451. writeLog(orderId, WAITING_ACCEPTANCE, ACCEPTED, "RIDER", riderId, null, now);
  452. return participantDetail(requireOrder(orderId), false, false);
  453. });
  454. }
  455. /** 骑手取件:提交取件凭证,订单由已接单推进为已取件。 */
  456. @Transactional
  457. public void pickup(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
  458. transitionWithProof(riderId, orderId, request, ACCEPTED, PICKED_UP, "PICKUP", "picked_up_at");
  459. }
  460. /** 骑手送达:校验交付 PIN(启用时)并提交送达凭证,订单由已取件推进为已送达。 */
  461. @Transactional
  462. public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
  463. requireRider(riderId);
  464. FlashDeliveryOrder order = requireOrder(orderId);
  465. if (!Objects.equals(riderId, order.getRiderId()) || !PICKED_UP.equals(order.getStatus())) {
  466. throw fail("flash.delivery.transition.not.allowed");
  467. }
  468. if (Boolean.TRUE.equals(order.getPinRequired())
  469. && (request == null || !Objects.equals(order.getDeliveryPinCode(), trimToNull(request.getPinCode())))) {
  470. throw fail("flash.delivery.pin.invalid");
  471. }
  472. transitionWithProof(riderId, orderId, request, PICKED_UP, DELIVERED, "DELIVERY", "delivered_at");
  473. }
  474. /** 平台运价配置列表,按时段开始时间排序。 */
  475. public List<FlashDeliveryPricing> adminPricing() {
  476. return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
  477. .orderByAsc("start_time", "id"));
  478. }
  479. /** 新增运价配置:校验参数合法性与时段不重叠后入库,版本号从 1 开始。 */
  480. @Transactional
  481. public FlashDeliveryPricing createPricing(Long adminId, FlashDeliveryPricingRequest request) {
  482. validatePricingRequest(request, false);
  483. ensureNoPricingOverlap(request, null);
  484. Date now = new Date();
  485. FlashDeliveryPricing pricing = pricingFrom(request);
  486. pricing.setConfigVersion(1);
  487. pricing.setUpdatedBy(adminId);
  488. pricing.setCreateTime(now);
  489. pricing.setUpdateTime(now);
  490. pricingMapper.insert(pricing);
  491. return pricing;
  492. }
  493. /** 修改运价配置:按 configVersion 乐观锁条件更新,防止管理员并发互相覆盖。 */
  494. @Transactional
  495. public FlashDeliveryPricing updatePricing(Long adminId, Long pricingId,
  496. FlashDeliveryPricingRequest request) {
  497. validatePricingRequest(request, true);
  498. FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
  499. if (preliminary == null) throw fail("flash.delivery.pricing.not.found");
  500. FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
  501. if (pricing == null) throw fail("flash.delivery.pricing.not.found");
  502. ensureNoPricingOverlap(request, pricingId);
  503. Date now = new Date();
  504. UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
  505. .eq("id", pricingId).eq("config_version", request.getConfigVersion())
  506. .set("start_time", request.getStartTime())
  507. .set("end_time", request.getEndTime())
  508. .set("starting_distance", scaleDistance(request.getStartingDistance()))
  509. .set("starting_fare", request.getStartingFare())
  510. .set("distance", scaleDistance(request.getDistance()))
  511. .set("freight", request.getFreight())
  512. .set("urgent_rate", normalizeRate(request.getUrgentRate()))
  513. .set("minimum_urgent_fee", request.getMinimumUrgentFee())
  514. .set("updated_by", adminId)
  515. .set("update_time", now)
  516. .setSql("config_version = config_version + 1");
  517. // 版本条件防止两个管理员同时保存时后提交者静默覆盖先提交者。
  518. if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
  519. pricing.setStartTime(request.getStartTime());
  520. pricing.setEndTime(request.getEndTime());
  521. pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
  522. pricing.setStartingFare(request.getStartingFare());
  523. pricing.setDistance(scaleDistance(request.getDistance()));
  524. pricing.setFreight(request.getFreight());
  525. pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
  526. pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
  527. pricing.setConfigVersion(request.getConfigVersion() + 1);
  528. pricing.setUpdatedBy(adminId);
  529. pricing.setUpdateTime(now);
  530. return pricing;
  531. }
  532. /** 删除运价配置。 */
  533. @Transactional
  534. public void deletePricing(Long pricingId) {
  535. FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
  536. if (preliminary == null) {
  537. throw fail("flash.delivery.pricing.not.found");
  538. }
  539. FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
  540. if (pricing == null) throw fail("flash.delivery.pricing.not.found");
  541. if (pricingMapper.deleteById(pricingId) != 1) throw fail("flash.delivery.pricing.not.found");
  542. }
  543. /** 平台订单分页:支持状态、业务场景、订单号模糊、寄件用户、骑手条件过滤。 */
  544. public IPage<FlashDeliveryOrder> adminOrders(int pageNum, int pageSize, String status,
  545. String serviceType, String orderNo,
  546. Long userId, Long riderId) {
  547. validateOptionalServiceType(serviceType);
  548. QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>().orderByDesc("id");
  549. optionalEq(query, "status", status);
  550. optionalEq(query, "service_type", serviceType);
  551. if (hasText(orderNo)) query.like("order_no", orderNo.trim());
  552. if (userId != null) query.eq("user_id", userId);
  553. if (riderId != null) query.eq("rider_id", riderId);
  554. return orderMapper.selectPage(page(pageNum, pageSize), query);
  555. }
  556. /** 平台订单审计详情:返回完整订单实体、全部凭证图片和状态日志。 */
  557. public FlashDeliveryAdminOrderDetailView adminDetail(Long orderId) {
  558. FlashDeliveryOrder order = requireOrder(orderId);
  559. FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
  560. view.setOrder(order);
  561. view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
  562. .eq("order_id", orderId).orderByAsc("proof_type", "sort_order")));
  563. view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
  564. .eq("order_id", orderId).orderByAsc("create_time", "id")));
  565. return view;
  566. }
  567. /** 平台取消:任何非终态订单可取消,记录操作人与原因,用于异常履约人工收口。 */
  568. @Transactional
  569. public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
  570. FlashDeliveryOrder order = requireOrder(orderId);
  571. if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
  572. String reason = requiredReason(request);
  573. Date now = new Date();
  574. if (orderMapper.cancel(orderId, order.getStatus(), "ADMIN", adminId, reason, now) != 1) {
  575. throw fail("flash.delivery.state.changed");
  576. }
  577. writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
  578. }
  579. /** 将已送达订单推进为已完成;平台手动与系统自动完成共用此入口,条件更新失败返回 false。 */
  580. @Transactional
  581. public boolean complete(Long operatorId, String operatorType, Long orderId) {
  582. Date now = new Date();
  583. if (orderMapper.transitionByStatus(orderId, DELIVERED, COMPLETED, "completed_at", now) != 1) return false;
  584. writeLog(orderId, DELIVERED, COMPLETED, operatorType, operatorId, null, now);
  585. return true;
  586. }
  587. /** 查询送达时间早于截止点且未完成的订单,供自动完成任务分批处理。 */
  588. public List<FlashDeliveryOrder> autoCompleteCandidates(Date deadline, int limit) {
  589. return orderMapper.selectAutoCompletable(deadline, Math.min(Math.max(limit, 1), 500));
  590. }
  591. /** 报价核心:校验枚举、物品、地址与预约窗后,按时段命中运价并计算路线距离,产出报价上下文。 */
  592. private QuoteContext calculateQuote(FlashDeliveryQuoteRequest request) {
  593. if (request == null) throw fail("flash.delivery.request.required");
  594. validateServiceType(request.getServiceType());
  595. validateDeliveryType(request.getDeliveryType());
  596. if (!hasText(request.getPackageType()) || !PACKAGE_TYPES.contains(request.getPackageType())) {
  597. throw fail("flash.delivery.package.type.invalid");
  598. }
  599. validateItem(request);
  600. Date now = new Date();
  601. String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
  602. validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
  603. validateAddress(request.getPickup());
  604. validateAddress(request.getDelivery());
  605. if (sameAddressText(request.getPickup(), request.getDelivery())
  606. || request.getPickup().getLatitude().compareTo(request.getDelivery().getLatitude()) == 0
  607. && request.getPickup().getLongitude().compareTo(request.getDelivery().getLongitude()) == 0) {
  608. throw fail("flash.delivery.address.same");
  609. }
  610. Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
  611. FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate));
  612. // 路线服务优先使用 Google Routes;不可用时在服务内部降级为球面直线距离。
  613. RouteDistance route = routeService.calculate(
  614. new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
  615. new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
  616. if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
  617. FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
  618. route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
  619. return new QuoteContext(pricing, route, breakdown);
  620. }
  621. /** 查询指定时刻命中的运价配置,未配置时直接报错。 */
  622. private FlashDeliveryPricing findPricingAt(String targetTime) {
  623. FlashDeliveryPricing pricing = findPricingAtOrNull(targetTime);
  624. if (pricing == null) throw fail("flash.delivery.pricing.not.available");
  625. return pricing;
  626. }
  627. /** 查询命中运价;命中多条说明配置重叠,报错交由平台修正。 */
  628. private FlashDeliveryPricing findPricingAtOrNull(String targetTime) {
  629. List<FlashDeliveryPricing> pricing = pricingMapper.selectAtTime(targetTime);
  630. if (pricing == null || pricing.isEmpty()) return null;
  631. if (pricing.size() > 1) throw fail("flash.delivery.pricing.overlap");
  632. return pricing.get(0);
  633. }
  634. /** 校验运价请求:时间格式与区间顺序、各金额取值范围及修改时的版本号。 */
  635. private void validatePricingRequest(FlashDeliveryPricingRequest request, boolean requireVersion) {
  636. if (request == null) throw fail("flash.delivery.pricing.invalid");
  637. BigDecimal startingDistance = normalizeDistance(request.getStartingDistance());
  638. BigDecimal distance = normalizeDistance(request.getDistance());
  639. BigDecimal urgentRate = normalizeRate(request.getUrgentRate());
  640. if (!validStartTime(request.getStartTime()) || !validEndTime(request.getEndTime())
  641. || toMinute(request.getStartTime()) >= toMinute(request.getEndTime())
  642. || startingDistance == null || startingDistance.signum() <= 0
  643. || startingDistance.compareTo(MAX_PRICING_DISTANCE) > 0
  644. || request.getStartingFare() == null || request.getStartingFare() <= 0
  645. || distance == null || distance.signum() <= 0
  646. || distance.compareTo(MAX_PRICING_DISTANCE) > 0
  647. || request.getFreight() == null || request.getFreight() <= 0
  648. || urgentRate == null || urgentRate.signum() < 0
  649. || urgentRate.compareTo(MAX_PRICING_DISTANCE) > 0
  650. || request.getMinimumUrgentFee() == null || request.getMinimumUrgentFee() < 0
  651. || requireVersion && (request.getConfigVersion() == null || request.getConfigVersion() <= 0)) {
  652. throw fail("flash.delivery.pricing.invalid");
  653. }
  654. }
  655. /** 确保新时段与既有运价不重叠,excludedId 用于修改时排除自身。 */
  656. private void ensureNoPricingOverlap(FlashDeliveryPricingRequest request, Long excludedId) {
  657. if (pricingMapper.countOverlapping(request.getStartTime(), request.getEndTime(), excludedId) > 0) {
  658. throw fail("flash.delivery.pricing.overlap");
  659. }
  660. }
  661. /** 将运价请求转换为实体,距离与比例统一保留两位小数。 */
  662. private FlashDeliveryPricing pricingFrom(FlashDeliveryPricingRequest request) {
  663. FlashDeliveryPricing pricing = new FlashDeliveryPricing();
  664. pricing.setStartTime(request.getStartTime());
  665. pricing.setEndTime(request.getEndTime());
  666. pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
  667. pricing.setStartingFare(request.getStartingFare());
  668. pricing.setDistance(scaleDistance(request.getDistance()));
  669. pricing.setFreight(request.getFreight());
  670. pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
  671. pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
  672. return pricing;
  673. }
  674. private boolean validStartTime(String value) {
  675. return value != null && value.matches("(?:[01]\\d|2[0-3]):[0-5]\\d");
  676. }
  677. private boolean validEndTime(String value) {
  678. return "24:00".equals(value) || validStartTime(value);
  679. }
  680. private int toMinute(String value) {
  681. if ("24:00".equals(value)) return 24 * 60;
  682. return Integer.parseInt(value.substring(0, 2)) * 60 + Integer.parseInt(value.substring(3));
  683. }
  684. private BigDecimal scaleDistance(BigDecimal value) {
  685. return normalizeDistance(value);
  686. }
  687. private BigDecimal normalizeDistance(BigDecimal value) {
  688. return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
  689. }
  690. private BigDecimal normalizeRate(BigDecimal value) {
  691. return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
  692. }
  693. /** 骑手带凭证的状态推进:校验凭证后按骑手归属条件更新,再落图片与日志。 */
  694. private void transitionWithProof(Long riderId, Long orderId, FlashDeliveryProofRequest request,
  695. String expected, String next, String proofType, String timeColumn) {
  696. List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
  697. requireRider(riderId);
  698. Date now = new Date();
  699. if (orderMapper.transitionByRider(orderId, riderId, expected, next, timeColumn, now) != 1) {
  700. throw fail("flash.delivery.transition.not.allowed");
  701. }
  702. saveImages(orderId, proofType, "RIDER", riderId, urls, now);
  703. writeLog(orderId, expected, next, "RIDER", riderId, null, now);
  704. }
  705. /** 状态、图片和日志处于同一事务;任一图片写入失败会回滚本次业务操作。 */
  706. private void saveImages(Long orderId, String proofType, String operatorType,
  707. Long operatorId, List<String> urls, Date now) {
  708. for (int index = 0; index < urls.size(); index++) {
  709. FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
  710. image.setOrderId(orderId);
  711. image.setProofType(proofType);
  712. image.setImageUrl(urls.get(index));
  713. image.setSortOrder(index);
  714. image.setOperatorType(operatorType);
  715. image.setOperatorId(operatorId);
  716. image.setCreateTime(now);
  717. imageMapper.insert(image);
  718. }
  719. }
  720. /** 校验凭证图片 URL:必填时 1 至 9 张,每项为合法 URI 且不超过 1000 字符。 */
  721. private List<String> validateProofUrls(List<String> values, boolean required) {
  722. if (values == null || values.isEmpty()) {
  723. if (!required) return List.of();
  724. throw fail("flash.delivery.proof.required");
  725. }
  726. if (values.size() > 9) throw fail("flash.delivery.proof.too.many");
  727. List<String> urls = new ArrayList<>();
  728. for (String value : values) {
  729. try {
  730. String url = value == null ? null : value.trim();
  731. if (url == null || url.isEmpty() || url.length() > 1000) {
  732. throw new IllegalArgumentException();
  733. }
  734. URI.create(url);
  735. urls.add(url);
  736. } catch (IllegalArgumentException exception) {
  737. throw fail("flash.delivery.proof.url.invalid");
  738. }
  739. }
  740. return urls;
  741. }
  742. /** 组装用户或骑手视角详情;hideSensitiveFields 为 true 时按抢单前脱敏返回。 */
  743. private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
  744. boolean userView,
  745. boolean hideSensitiveFields) {
  746. FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
  747. if (userView) {
  748. view.setOrderVersion(order.getVersion());
  749. view.setDeliveryPinCode(Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
  750. view.setRider(riderSummary(order));
  751. }
  752. if (!hideSensitiveFields && order.getId() != null) {
  753. groupImageUrls(view, imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
  754. .eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
  755. }
  756. return view;
  757. }
  758. /** 订单实体转用户端订单卡片。 */
  759. private FlashDeliveryUserOrderListView userOrderListView(FlashDeliveryOrder order) {
  760. FlashDeliveryUserOrderListView view = new FlashDeliveryUserOrderListView();
  761. view.setId(order.getId());
  762. view.setOrderNo(order.getOrderNo());
  763. view.setServiceType(order.getServiceType());
  764. view.setDeliveryType(order.getDeliveryType());
  765. view.setStatus(order.getStatus());
  766. view.setOrderVersion(order.getVersion());
  767. view.setPackageType(order.getPackageType());
  768. view.setQuantity(order.getQuantity());
  769. view.setWeightRange(order.getWeightRange());
  770. view.setDeliveryMode(order.getDeliveryMode());
  771. view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  772. view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  773. view.setPickupAddress(order.getPickupAddress());
  774. view.setPickupDetailAddress(order.getPickupAddressDetail());
  775. view.setDeliveryAddress(order.getDeliveryAddress());
  776. view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
  777. view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
  778. view.setBaseDeliveryFee(order.getBaseDeliveryFee());
  779. view.setUrgentFee(order.getUrgentFee());
  780. view.setTipAmount(order.getTipAmount());
  781. view.setAmount(order.getAmount());
  782. view.setCurrency(order.getCurrency());
  783. view.setDeliveredAt(order.getDeliveredAt());
  784. view.setCreateTime(order.getCreateTime());
  785. return view;
  786. }
  787. /** 订单实体转骑手端任务卡片,可附带骑手到取件点的直线距离。 */
  788. private FlashDeliveryRiderOrderListView riderOrderListView(FlashDeliveryOrder order,
  789. BigDecimal longitude, BigDecimal latitude) {
  790. FlashDeliveryRiderOrderListView view = new FlashDeliveryRiderOrderListView();
  791. view.setId(order.getId());
  792. view.setOrderNo(order.getOrderNo());
  793. view.setServiceType(order.getServiceType());
  794. view.setDeliveryType(order.getDeliveryType());
  795. view.setStatus(order.getStatus());
  796. view.setPackageType(order.getPackageType());
  797. view.setQuantity(order.getQuantity());
  798. view.setWeightRange(order.getWeightRange());
  799. view.setSpecification(order.getSpecification());
  800. view.setDeliveryMode(order.getDeliveryMode());
  801. view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  802. view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  803. view.setPinRequired(order.getPinRequired());
  804. view.setPickupAddress(order.getPickupAddress());
  805. view.setPickupDetailAddress(order.getPickupAddressDetail());
  806. view.setDeliveryAddress(order.getDeliveryAddress());
  807. view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
  808. view.setPayType(order.getPayType());
  809. view.setPickupDistanceMeters(distanceMeters(longitude, latitude,
  810. order.getPickupLongitude(), order.getPickupLatitude()));
  811. view.setDistanceMeters(order.getDistanceMeters());
  812. view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
  813. view.setBaseDeliveryFee(order.getBaseDeliveryFee());
  814. view.setDistanceFee(order.getDistanceFee());
  815. view.setUrgentFee(order.getUrgentFee());
  816. view.setTipAmount(order.getTipAmount());
  817. view.setAmount(order.getAmount());
  818. view.setCurrency(order.getCurrency());
  819. view.setCreateTime(order.getCreateTime());
  820. return view;
  821. }
  822. /** 订单实体转详情基础视图。 */
  823. private FlashDeliveryOrderDetailView detailOrderView(FlashDeliveryOrder order, boolean includeSensitiveFields) {
  824. FlashDeliveryOrderDetailView view = new FlashDeliveryOrderDetailView();
  825. populateOrderView(view, order, includeSensitiveFields);
  826. return view;
  827. }
  828. /** 按凭证类型把图片分组为寄件、取件、送达三组 URL。 */
  829. private void groupImageUrls(FlashDeliveryOrderDetailView view, List<FlashDeliveryOrderImage> images) {
  830. List<String> sender = new ArrayList<>();
  831. List<String> pickup = new ArrayList<>();
  832. List<String> delivery = new ArrayList<>();
  833. for (FlashDeliveryOrderImage image : images) {
  834. if ("SENDER".equals(image.getProofType())) sender.add(image.getImageUrl());
  835. else if ("PICKUP".equals(image.getProofType())) pickup.add(image.getImageUrl());
  836. else if ("DELIVERY".equals(image.getProofType())) delivery.add(image.getImageUrl());
  837. }
  838. view.setSenderImageUrls(sender);
  839. view.setPickupImageUrls(pickup);
  840. view.setDeliveryImageUrls(delivery);
  841. }
  842. /** 运价实体转首页服务摘要视图。 */
  843. private FlashDeliveryServiceView serviceView(FlashDeliveryPricing pricing, String serviceType) {
  844. FlashDeliveryServiceView view = new FlashDeliveryServiceView();
  845. view.setServiceType(serviceType);
  846. view.setPricingId(pricing.getId());
  847. view.setStartTime(pricing.getStartTime());
  848. view.setEndTime(pricing.getEndTime());
  849. view.setStartingDistance(pricing.getStartingDistance());
  850. view.setStartingFare(pricing.getStartingFare());
  851. view.setDistance(pricing.getDistance());
  852. view.setFreight(pricing.getFreight());
  853. view.setUrgentRate(pricing.getUrgentRate());
  854. view.setMinimumUrgentFee(pricing.getMinimumUrgentFee());
  855. view.setPricingVersion(pricing.getConfigVersion());
  856. view.setCurrency("TWD");
  857. return view;
  858. }
  859. /** 将订单实体的公共字段填充到视图,includeSensitiveFields 控制是否包含敏感字段。 */
  860. private void populateOrderView(FlashDeliveryOrderView view, FlashDeliveryOrder order,
  861. boolean includeSensitiveFields) {
  862. view.setId(order.getId());
  863. view.setOrderNo(order.getOrderNo());
  864. view.setServiceType(order.getServiceType());
  865. view.setDeliveryType(order.getDeliveryType());
  866. view.setStatus(order.getStatus());
  867. view.setPackageType(order.getPackageType());
  868. view.setQuantity(order.getQuantity());
  869. view.setWeightRange(order.getWeightRange());
  870. view.setSpecification(order.getSpecification());
  871. view.setDeliveryMode(order.getDeliveryMode());
  872. view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  873. view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  874. view.setPinRequired(order.getPinRequired());
  875. view.setPickup(addressView(order, true, includeSensitiveFields));
  876. view.setDelivery(addressView(order, false, includeSensitiveFields));
  877. view.setDistanceMeters(order.getDistanceMeters());
  878. view.setDistanceSource(order.getDistanceSource());
  879. view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
  880. view.setBaseDeliveryFee(order.getBaseDeliveryFee());
  881. view.setDistanceFee(order.getDistanceFee());
  882. view.setUrgentFee(order.getUrgentFee());
  883. view.setTipAmount(order.getTipAmount());
  884. view.setAmount(order.getAmount());
  885. view.setCurrency(order.getCurrency());
  886. view.setPayType(order.getPayType());
  887. view.setUserNote(includeSensitiveFields ? order.getUserNote() : null);
  888. view.setAcceptedAt(order.getAcceptedAt());
  889. view.setPickedUpAt(order.getPickedUpAt());
  890. view.setDeliveredAt(order.getDeliveredAt());
  891. view.setCompletedAt(order.getCompletedAt());
  892. view.setCancelledAt(order.getCancelledAt());
  893. view.setCancelReason(order.getCancelReason());
  894. view.setCreateTime(order.getCreateTime());
  895. view.setUpdateTime(order.getUpdateTime());
  896. }
  897. /** 从订单实体提取取件或收件地址视图;脱敏时不返回联系人、电话和坐标。 */
  898. private FlashDeliveryAddressView addressView(FlashDeliveryOrder order, boolean pickup,
  899. boolean includeSensitiveFields) {
  900. FlashDeliveryAddressView view = new FlashDeliveryAddressView();
  901. if (pickup) {
  902. view.setName(includeSensitiveFields ? order.getPickupName() : null);
  903. view.setPhone(includeSensitiveFields ? order.getPickupPhone() : null);
  904. view.setAddress(order.getPickupAddress()); view.setAddressDetail(order.getPickupAddressDetail());
  905. view.setCity(order.getPickupCity()); view.setArea(order.getPickupArea());
  906. view.setHandoffMethod(order.getPickupHandoffMethod());
  907. view.setLongitude(includeSensitiveFields ? order.getPickupLongitude() : null);
  908. view.setLatitude(includeSensitiveFields ? order.getPickupLatitude() : null);
  909. } else {
  910. view.setName(includeSensitiveFields ? order.getDeliveryName() : null);
  911. view.setPhone(includeSensitiveFields ? order.getDeliveryPhone() : null);
  912. view.setAddress(order.getDeliveryAddress()); view.setAddressDetail(order.getDeliveryAddressDetail());
  913. view.setCity(order.getDeliveryCity()); view.setArea(order.getDeliveryArea());
  914. view.setHandoffMethod(order.getDeliveryHandoffMethod());
  915. view.setLongitude(includeSensitiveFields ? order.getDeliveryLongitude() : null);
  916. view.setLatitude(includeSensitiveFields ? order.getDeliveryLatitude() : null);
  917. }
  918. return view;
  919. }
  920. /** 组装骑手公开资料;仅已接单或已取件状态附带骑手实时位置。 */
  921. private FlashDeliveryRiderSummaryView riderSummary(FlashDeliveryOrder order) {
  922. if (order.getRiderId() == null) return null;
  923. InfoUser rider = userMapper.selectInfoUserByUserId(order.getRiderId());
  924. if (rider == null) return null;
  925. FlashDeliveryRiderSummaryView view = new FlashDeliveryRiderSummaryView();
  926. view.setName(hasText(rider.getNickName()) ? rider.getNickName() : rider.getUserName());
  927. view.setAvatar(rider.getAvatar());
  928. view.setRating(rider.getStar());
  929. view.setImUserId(rider.getImUserId() == null ? null : String.valueOf(rider.getImUserId()));
  930. if (ACCEPTED.equals(order.getStatus()) || PICKED_UP.equals(order.getStatus())) {
  931. view.setLongitude(rider.getLongitude());
  932. view.setLatitude(rider.getLatitude());
  933. }
  934. return view;
  935. }
  936. /** 把费用明细写入报价视图。 */
  937. private void applyBreakdown(FlashDeliveryQuoteView view, FlashDeliveryPriceBreakdown breakdown) {
  938. view.setBillableDistance(breakdown.getBillableDistance());
  939. view.setDistanceFee(breakdown.getDistanceFee());
  940. view.setBaseDeliveryFee(breakdown.getBaseDeliveryFee());
  941. view.setUrgentFee(breakdown.getUrgentFee());
  942. view.setTipAmount(breakdown.getTipAmount());
  943. view.setAmount(breakdown.getAmount());
  944. }
  945. /** 把请求地址复制为订单取件或收件快照字段。 */
  946. private void copyAddress(FlashDeliveryAddressRequest source, FlashDeliveryOrder target, boolean pickup) {
  947. if (pickup) {
  948. target.setPickupName(source.getName().trim()); target.setPickupPhone(source.getPhone().trim());
  949. target.setPickupAddress(source.getAddress().trim()); target.setPickupAddressDetail(trimToNull(source.getAddressDetail()));
  950. target.setPickupCity(trimToNull(source.getCity())); target.setPickupArea(trimToNull(source.getArea()));
  951. target.setPickupHandoffMethod(trimToNull(source.getHandoffMethod()));
  952. target.setPickupLongitude(source.getLongitude()); target.setPickupLatitude(source.getLatitude());
  953. } else {
  954. target.setDeliveryName(source.getName().trim()); target.setDeliveryPhone(source.getPhone().trim());
  955. target.setDeliveryAddress(source.getAddress().trim()); target.setDeliveryAddressDetail(trimToNull(source.getAddressDetail()));
  956. target.setDeliveryCity(trimToNull(source.getCity())); target.setDeliveryArea(trimToNull(source.getArea()));
  957. target.setDeliveryHandoffMethod(trimToNull(source.getHandoffMethod()));
  958. target.setDeliveryLongitude(source.getLongitude()); target.setDeliveryLatitude(source.getLatitude());
  959. }
  960. }
  961. /** 校验地址请求:必填项、长度限制和经纬度取值范围。 */
  962. private void validateAddress(FlashDeliveryAddressRequest address) {
  963. if (address == null || !hasText(address.getName()) || !hasText(address.getPhone())
  964. || !hasText(address.getAddress()) || address.getLatitude() == null || address.getLongitude() == null
  965. || address.getName().trim().length() > 64 || address.getPhone().trim().length() > 32
  966. || address.getAddress().trim().length() > 255
  967. || (address.getAddressDetail() != null && address.getAddressDetail().trim().length() > 255)
  968. || tooLong(address.getCity(), 64) || tooLong(address.getArea(), 64)
  969. || tooLong(address.getHandoffMethod(), 40)
  970. || address.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0
  971. || address.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0
  972. || address.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0
  973. || address.getLongitude().compareTo(BigDecimal.valueOf(180)) > 0) {
  974. throw fail("flash.delivery.address.invalid");
  975. }
  976. }
  977. /** 收件手机号归一化后唯一匹配普通用户作为收件人;匹配零个或多个视为无收件人。 */
  978. private Long resolveReceiverUserId(String phone) {
  979. if (!hasText(phone)) return null;
  980. StringBuilder normalized = new StringBuilder(phone.length());
  981. for (int index = 0; index < phone.length(); index++) {
  982. char value = phone.charAt(index);
  983. if (Character.isWhitespace(value) || value == '+' || value == '-'
  984. || value == '(' || value == ')') {
  985. continue;
  986. }
  987. if (value < '0' || value > '9') return null;
  988. normalized.append(value);
  989. }
  990. if (normalized.isEmpty()) return null;
  991. List<Long> matches = userMapper.selectOrdinaryUserIdsByNormalizedPhone(normalized.toString());
  992. return matches != null && matches.size() == 1 ? matches.get(0) : null;
  993. }
  994. /** 判断用户是否为订单寄件人或匹配收件人。 */
  995. private boolean isParticipant(Long userId, FlashDeliveryOrder order) {
  996. return Objects.equals(userId, order.getUserId())
  997. || Objects.equals(userId, order.getReceiverUserId());
  998. }
  999. /** 校验 token 用户为骑手(userType=2)并返回其资料。 */
  1000. private InfoUser requireRider(Long riderId) {
  1001. InfoUser rider = riderId == null ? null : userMapper.selectInfoUserByUserId(riderId);
  1002. if (rider == null || !"2".equals(rider.getUserType())) throw fail("flash.delivery.rider.required");
  1003. return rider;
  1004. }
  1005. /** 加载订单,不存在时统一按订单不存在报错。 */
  1006. private FlashDeliveryOrder requireOrder(Long orderId) {
  1007. FlashDeliveryOrder order = orderId == null ? null : orderMapper.selectById(orderId);
  1008. if (order == null) throw fail("flash.delivery.order.not.found");
  1009. return order;
  1010. }
  1011. /** 写入状态变更审计日志。 */
  1012. private void writeLog(Long orderId, String from, String to, String operatorType,
  1013. Long operatorId, String reason, Date now) {
  1014. FlashDeliveryOrderLog log = new FlashDeliveryOrderLog();
  1015. log.setOrderId(orderId); log.setFromStatus(from); log.setToStatus(to);
  1016. log.setOperatorType(operatorType); log.setOperatorId(operatorId); log.setReason(reason); log.setCreateTime(now);
  1017. logMapper.insert(log);
  1018. }
  1019. /** 校验并提取取消原因。 */
  1020. private String requiredReason(FlashDeliveryReasonRequest request) {
  1021. if (request == null || !hasText(request.getReason()) || request.getReason().trim().length() > 500) {
  1022. throw fail("flash.delivery.cancel.reason.required");
  1023. }
  1024. return request.getReason().trim();
  1025. }
  1026. private void validateServiceType(String value) {
  1027. if (!hasText(value) || !SERVICE_TYPES.contains(value)) throw fail("flash.delivery.service.type.invalid");
  1028. }
  1029. private void validateDeliveryType(String value) {
  1030. if (!hasText(value) || !DELIVERY_TYPES.contains(value)) {
  1031. throw fail("flash.delivery.delivery.type.invalid");
  1032. }
  1033. }
  1034. /** 校验物品信息:数量、重量范围、规格长度和小费。 */
  1035. private void validateItem(FlashDeliveryQuoteRequest request) {
  1036. if (request.getQuantity() == null || request.getQuantity() <= 0
  1037. || !hasText(request.getWeightRange()) || !WEIGHT_RANGES.contains(request.getWeightRange())
  1038. || tooLong(request.getSpecification(), 255)) {
  1039. throw fail("flash.delivery.item.invalid");
  1040. }
  1041. if (request.getTipAmount() == null || request.getTipAmount() < 0) {
  1042. throw fail("flash.delivery.tip.invalid");
  1043. }
  1044. }
  1045. /** 支付方式与主订单系统同域(4=现金、6=线下转账);闪送款项直达骑手,仅允许线下方式,缺省现金,下单后锁定。 */
  1046. private String normalizePayType(String payType) {
  1047. if (!hasText(payType)) return PAY_TYPE_CASH;
  1048. String trimmed = payType.trim();
  1049. return trimmed;
  1050. // if (PAY_TYPE_CASH.equals(trimmed) || PAY_TYPE_TRANSFER.equals(trimmed)) return trimmed;
  1051. // throw fail("flash.delivery.pay.type.invalid");
  1052. }
  1053. /** 将时间转为台北时区 HH:mm,用于命中运价时段。 */
  1054. private String pricingTargetTime(Date date) {
  1055. return PRICING_TIME_FORMAT.format(Instant.ofEpochMilli(date.getTime())
  1056. .atZone(TAIPEI).toLocalTime());
  1057. }
  1058. /** 比对客户端回传报价与服务端最新报价是否完全一致。 */
  1059. private boolean matchesQuotedPrice(FlashDeliveryCreateRequest request, FlashDeliveryQuoteView latest) {
  1060. return Objects.equals(request.getPricingId(), latest.getPricingId())
  1061. && Objects.equals(request.getPricingVersion(), latest.getPricingVersion())
  1062. && Objects.equals(request.getQuotedBaseDeliveryFee(), latest.getBaseDeliveryFee())
  1063. && Objects.equals(request.getQuotedDistanceFee(), latest.getDistanceFee())
  1064. && Objects.equals(request.getQuotedUrgentFee(), latest.getUrgentFee())
  1065. && Objects.equals(request.getQuotedAmount(), latest.getAmount());
  1066. }
  1067. private void validateOptionalServiceType(String value) {
  1068. if (hasText(value)) validateServiceType(value);
  1069. }
  1070. private void requireUserId(Long userId) {
  1071. if (userId == null || userId <= 0) throw fail("flash.delivery.auth.required");
  1072. }
  1073. /** 构造分页对象并夹紧页码与每页数量(1 至 100)。 */
  1074. private Page<FlashDeliveryOrder> page(int pageNum, int pageSize) {
  1075. return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
  1076. }
  1077. /** 分页结果实体转换,保留分页元数据。 */
  1078. private <T> IPage<T> mapPage(IPage<FlashDeliveryOrder> source,
  1079. Function<FlashDeliveryOrder, T> mapper) {
  1080. Page<T> result = new Page<>(source.getCurrent(), source.getSize(), source.getTotal());
  1081. result.setRecords(source.getRecords().stream().map(mapper).toList());
  1082. return result;
  1083. }
  1084. /** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
  1085. private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, String tab,
  1086. BigDecimal longitude, BigDecimal latitude,
  1087. boolean orderResults) {
  1088. QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<>();
  1089. Date now = new Date();
  1090. switch (tab) {
  1091. case "newTask" -> {
  1092. query.eq("status", WAITING_ACCEPTANCE).isNull("rider_id")
  1093. .and(item -> item.eq("delivery_mode", "NOW")
  1094. .or().le("scheduled_pickup_start_at", now));
  1095. if (longitude != null) {
  1096. Integer limitKm = newTaskDistanceLimit();
  1097. if (limitKm != null) {
  1098. query.apply("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point({0}, {1})) <= {2}",
  1099. longitude, latitude, limitKm.longValue() * 1000L);
  1100. }
  1101. if (orderResults) {
  1102. query.orderByAsc("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point(" + longitude
  1103. + ", " + latitude + "))");
  1104. }
  1105. } else if (orderResults) {
  1106. query.orderByDesc("create_time");
  1107. }
  1108. }
  1109. case "toPickup" -> {
  1110. query.eq("rider_id", riderId).eq("status", ACCEPTED);
  1111. if (orderResults) query.orderByAsc("create_time");
  1112. }
  1113. case "delivering" -> {
  1114. query.eq("rider_id", riderId).eq("status", PICKED_UP);
  1115. if (orderResults) query.orderByAsc("create_time");
  1116. }
  1117. case "completed" -> {
  1118. query.eq("rider_id", riderId).in("status", DELIVERED, COMPLETED);
  1119. if (orderResults) query.orderByDesc("create_time");
  1120. }
  1121. case "cancelled" -> {
  1122. query.eq("rider_id", riderId).eq("status", CANCELLED);
  1123. if (orderResults) query.orderByDesc("create_time");
  1124. }
  1125. default -> throw fail("flash.delivery.tab.invalid");
  1126. }
  1127. return query;
  1128. }
  1129. /** 校验骑手坐标:经纬度必须成对出现且在合法范围。 */
  1130. private void validateRiderCoordinates(BigDecimal longitude, BigDecimal latitude) {
  1131. if ((longitude == null) != (latitude == null)
  1132. || longitude != null && (longitude.compareTo(BigDecimal.valueOf(-180)) < 0
  1133. || longitude.compareTo(BigDecimal.valueOf(180)) > 0
  1134. || latitude.compareTo(BigDecimal.valueOf(-90)) < 0
  1135. || latitude.compareTo(BigDecimal.valueOf(90)) > 0)) {
  1136. throw fail("flash.delivery.coordinates.invalid");
  1137. }
  1138. }
  1139. /** 读取字典 sys_qs_newtask_distance 的新任务距离上限(公里);缺失或非法时不限距离。 */
  1140. private Integer newTaskDistanceLimit() {
  1141. try {
  1142. List<SysDictData> values = DictUtils.getDictCache("sys_qs_newtask_distance");
  1143. if (values != null && !values.isEmpty()) {
  1144. int limit = Integer.parseInt(values.get(0).getDictValue());
  1145. return limit > 0 ? limit : null;
  1146. }
  1147. } catch (Exception ignored) {
  1148. // Keep the established rider-list behaviour: absent or malformed configuration disables distance filtering.
  1149. }
  1150. return null;
  1151. }
  1152. /** Haversine 球面直线距离(米),用于骑手列表展示距取件点的距离。 */
  1153. private Integer distanceMeters(BigDecimal longitude, BigDecimal latitude,
  1154. BigDecimal targetLongitude, BigDecimal targetLatitude) {
  1155. if (longitude == null || targetLongitude == null || targetLatitude == null) return null;
  1156. double latitudeRadians = Math.toRadians(latitude.doubleValue());
  1157. double targetLatitudeRadians = Math.toRadians(targetLatitude.doubleValue());
  1158. double latitudeDelta = targetLatitudeRadians - latitudeRadians;
  1159. double longitudeDelta = Math.toRadians(targetLongitude.doubleValue() - longitude.doubleValue());
  1160. double value = Math.sin(latitudeDelta / 2) * Math.sin(latitudeDelta / 2)
  1161. + Math.cos(latitudeRadians) * Math.cos(targetLatitudeRadians)
  1162. * Math.sin(longitudeDelta / 2) * Math.sin(longitudeDelta / 2);
  1163. return (int) Math.round(6_371_000D * 2D * Math.atan2(Math.sqrt(value), Math.sqrt(1D - value)));
  1164. }
  1165. /** 从 selectObjs 聚合结果中取第一个 Long 值。 */
  1166. private Long firstLong(List<Object> values) {
  1167. if (values == null || values.isEmpty() || values.get(0) == null) return null;
  1168. Object value = values.get(0);
  1169. if (value instanceof Number number) return number.longValue();
  1170. return Long.valueOf(String.valueOf(value));
  1171. }
  1172. /** 取件方式归一化:缺省按 NOW 处理,非法值报错。 */
  1173. private String normalizeDeliveryMode(String value) {
  1174. String mode = hasText(value) ? value.trim() : "NOW";
  1175. if (!DELIVERY_MODES.contains(mode)) throw fail("flash.delivery.mode.invalid");
  1176. return mode;
  1177. }
  1178. /** 校验预约窗:NOW 不得携带时间;SCHEDULED 必须是未来 3 天内恰好 30 分钟的窗。 */
  1179. private void validateSchedule(String mode, Date start, Date end, Date now) {
  1180. if ("NOW".equals(mode)) {
  1181. if (start != null || end != null) throw fail("flash.delivery.schedule.invalid");
  1182. return;
  1183. }
  1184. if (start == null || end == null || start.before(now)
  1185. || end.getTime() - start.getTime() != SCHEDULE_SLOT_MILLIS
  1186. || start.getTime() - now.getTime() > MAX_SCHEDULE_DELAY_MILLIS) {
  1187. throw fail("flash.delivery.schedule.invalid");
  1188. }
  1189. }
  1190. /** 判断订单当前是否开放抢单:立即单随时可抢,预约单到点开放。 */
  1191. private boolean isAvailableAt(FlashDeliveryOrder order, Date now) {
  1192. return "NOW".equals(order.getDeliveryMode())
  1193. || "SCHEDULED".equals(order.getDeliveryMode())
  1194. && order.getScheduledPickupStartAt() != null
  1195. && !order.getScheduledPickupStartAt().after(now);
  1196. }
  1197. private void optionalEq(QueryWrapper<FlashDeliveryOrder> query, String column, String value) {
  1198. if (hasText(value)) query.eq(column, value.trim());
  1199. }
  1200. private boolean hasText(String value) { return StringUtils.isNotEmpty(value) && !value.trim().isEmpty(); }
  1201. private String trimToNull(String value) { return hasText(value) ? value.trim() : null; }
  1202. private boolean tooLong(String value, int max) { return value != null && value.trim().length() > max; }
  1203. private boolean sameAddressText(FlashDeliveryAddressRequest left, FlashDeliveryAddressRequest right) {
  1204. return normalized(left.getAddress()).equals(normalized(right.getAddress()))
  1205. && normalized(left.getAddressDetail()).equals(normalized(right.getAddressDetail()));
  1206. }
  1207. private String normalized(String value) { return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); }
  1208. private ServiceException fail(String key) { return new ServiceException(MessageUtils.message(key)); }
  1209. /** 报价中间结果:命中的运价配置、路线距离和费用明细。 */
  1210. private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
  1211. FlashDeliveryPriceBreakdown breakdown) { }
  1212. }