FlashDeliveryApplicationService.java 108 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860
  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.RiderDeliveryAcceptanceService;
  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 com.ruoyi.system.service.PaymentMethodGateService;
  23. import org.springframework.beans.BeanUtils;
  24. import org.springframework.dao.DataIntegrityViolationException;
  25. import org.springframework.transaction.support.TransactionSynchronizationManager;
  26. import org.springframework.stereotype.Service;
  27. import org.springframework.transaction.annotation.Transactional;
  28. import java.net.URI;
  29. import java.math.BigDecimal;
  30. import java.math.RoundingMode;
  31. import java.security.SecureRandom;
  32. import java.time.Instant;
  33. import java.time.ZoneId;
  34. import java.time.format.DateTimeFormatter;
  35. import java.util.*;
  36. import java.util.concurrent.atomic.AtomicLong;
  37. import java.util.function.Function;
  38. import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
  39. /**
  40. * 闪送业务编排服务。
  41. *
  42. * <p>负责报价、订单创建、抢单、履约状态流转、平台介入和审计日志。
  43. * 订单身份只接受 Controller 从 token 解析后的用户 ID,客户端传入的金额、距离或操作人均不可信。</p>
  44. */
  45. @Service
  46. public class FlashDeliveryApplicationService {
  47. private static final List<String> SERVICE_TYPES = List.of("HELP_SEND", "HELP_PICKUP");
  48. private static final Set<String> DELIVERY_TYPES = Set.of("NORMAL", "URGENT");
  49. private static final Set<String> PACKAGE_TYPES = Set.of("DOCUMENT", "GIFT", "CLOTHING", "BEAUTY",
  50. "DAILY_NECESSITIES", "FOOD_INGREDIENTS", "ELECTRONICS", "SMALL_APPLIANCE", "OTHER");
  51. private static final Set<String> WEIGHT_RANGES = Set.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
  52. "OVER_10_TO_15_KG", "OVER_15_TO_20_KG");
  53. private static final Set<String> DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
  54. /** 支付方式取值与主订单系统同域:4=现金、6=线下转账(027 线下转账支付)。 */
  55. private static final String PAY_TYPE_CASH = "4";
  56. /** 线下转账 payType=6:送达后向寄件人展示骑手启用卡,线下转账结清。 */
  57. private static final String PAY_TYPE_OFFLINE_TRANSFER = "6";
  58. private static final String PAY_TYPE_TRANSFER = "6";
  59. /** 自动取消原因使用稳定代码,供不同端按各自语言展示而不是保存单一语言文本。 */
  60. private static final String SCHEDULE_EXPIRED_REASON = "SCHEDULE_EXPIRED";
  61. private static final DateTimeFormatter PRICING_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
  62. private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
  63. private static final int MAX_DISTANCE_METERS = 40_000;
  64. private static final BigDecimal MAX_PRICING_DISTANCE = new BigDecimal("999999.99");
  65. private static final long SCHEDULE_SLOT_MILLIS = 30L * 60L * 1000L;
  66. private static final long MAX_SCHEDULE_DELAY_MILLIS = 3L * 24L * 60L * 60L * 1000L;
  67. private static final SecureRandom PIN_RANDOM = new SecureRandom();
  68. /** 订单号同毫秒递增序列(01-99 循环):同一毫秒内的并发由原子递增区分,避免同号。 */
  69. private static final AtomicLong ORDER_NO_SEQ = new AtomicLong();
  70. private final FlashDeliveryOrderMapper orderMapper;
  71. private final FlashDeliveryOrderStopMapper stopMapper;
  72. private final FlashDeliveryTargetAdjustmentMapper adjustmentMapper;
  73. private final FlashDeliveryPricingMapper pricingMapper;
  74. private final FlashDeliveryOrderImageMapper imageMapper;
  75. private final FlashDeliveryOrderLogMapper logMapper;
  76. private final InfoUserMapper userMapper;
  77. private final com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper;
  78. private final FlashDeliveryRouteService routeService;
  79. private final FlashDeliveryPricingCalculator pricingCalculator;
  80. private final RiderDeliveryLockService riderDeliveryLockService;
  81. private final RiderDeliveryAcceptanceService riderDeliveryAcceptanceService;
  82. /** 031 支付闸门:闪送下单、报价展示和骑手待抢列表使用同一可用性规则。 */
  83. private final PaymentMethodGateService paymentMethodGateService;
  84. private final FlashDeliveryNotificationService notificationService;
  85. public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
  86. FlashDeliveryPricingMapper pricingMapper,
  87. FlashDeliveryOrderImageMapper imageMapper,
  88. FlashDeliveryOrderLogMapper logMapper,
  89. InfoUserMapper userMapper,
  90. com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper,
  91. FlashDeliveryRouteService routeService,
  92. FlashDeliveryPricingCalculator pricingCalculator,
  93. RiderDeliveryLockService riderDeliveryLockService,
  94. RiderDeliveryAcceptanceService riderDeliveryAcceptanceService,
  95. FlashDeliveryOrderStopMapper stopMapper,
  96. FlashDeliveryTargetAdjustmentMapper adjustmentMapper,
  97. PaymentMethodGateService paymentMethodGateService,
  98. FlashDeliveryNotificationService notificationService) {
  99. this.orderMapper = orderMapper;
  100. this.stopMapper = stopMapper;
  101. this.adjustmentMapper = adjustmentMapper;
  102. this.pricingMapper = pricingMapper;
  103. this.imageMapper = imageMapper;
  104. this.logMapper = logMapper;
  105. this.userMapper = userMapper;
  106. this.bankCardMapper = bankCardMapper;
  107. this.routeService = routeService;
  108. this.pricingCalculator = pricingCalculator;
  109. this.riderDeliveryLockService = riderDeliveryLockService;
  110. this.riderDeliveryAcceptanceService = riderDeliveryAcceptanceService;
  111. this.paymentMethodGateService = paymentMethodGateService;
  112. this.notificationService = notificationService;
  113. }
  114. /** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
  115. public FlashDeliveryHomeView home() {
  116. FlashDeliveryHomeView view = new FlashDeliveryHomeView();
  117. view.setPickupStopLimit(pickupStopLimit());
  118. view.setShowMultiPickup(dictionaryEnabled("flash_show_multi_pickup"));
  119. view.setShowUrgentOption(dictionaryEnabled("flash_show_urgent_option"));
  120. view.setServiceTypes(SERVICE_TYPES);
  121. view.setDeliveryTypes(List.of("NORMAL", "URGENT"));
  122. FlashDeliveryPricing pricing = findPricingAtOrNull(pricingTargetTime(new Date()), FlashDeliveryVehicleType.MOTORCYCLE);
  123. view.setServices(pricing == null ? List.of() : SERVICE_TYPES.stream()
  124. .map(serviceType -> serviceView(pricing, serviceType)).toList());
  125. view.setPayMethods(paymentMethodGateService.listAvailable(
  126. PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
  127. return view;
  128. }
  129. /** 实时报价:校验请求后按服务端路线距离和命中时段的运价计算费用(立即单取当前时段、预约单取预约开始时刻),不写库。 */
  130. public FlashDeliveryQuoteView quote(FlashDeliveryQuoteRequest request) {
  131. // 报价只使用服务端路线距离和当前启用的计价配置,避免客户端篡改金额。
  132. QuoteContext context = calculateQuote(request);
  133. return quoteView(request, context);
  134. }
  135. /** 将报价上下文组装为对外报价视图,含运价快照字段与费用明细。 */
  136. private FlashDeliveryQuoteView quoteView(FlashDeliveryQuoteRequest request, QuoteContext context) {
  137. FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
  138. view.setServiceType(request.getServiceType());
  139. view.setDeliveryType(request.getDeliveryType());
  140. view.setVehicleType(request.getVehicleType());
  141. view.setDeliveryMode(normalizeDeliveryMode(request.getDeliveryMode()));
  142. view.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
  143. view.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
  144. view.setPricingId(context.pricing().getId());
  145. view.setStartTime(context.pricing().getStartTime());
  146. view.setEndTime(context.pricing().getEndTime());
  147. view.setDistanceMeters(context.route().distanceMeters());
  148. view.setDistanceSource(context.route().source());
  149. view.setEstimatedDurationSeconds(context.route().durationSeconds());
  150. view.setStartingDistance(context.pricing().getStartingDistance());
  151. view.setStartingFare(context.pricing().getStartingFare());
  152. view.setDistance(context.pricing().getDistance());
  153. view.setFreight(context.pricing().getFreight());
  154. view.setUrgentRate(context.pricing().getUrgentRate());
  155. view.setMinimumUrgentFee(context.pricing().getMinimumUrgentFee());
  156. applyBreakdown(view, context.breakdown());
  157. view.setCurrency("TWD");
  158. view.setPricingVersion(context.pricing().getConfigVersion());
  159. view.setPayMethods(paymentMethodGateService.listAvailable(
  160. PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
  161. return view;
  162. }
  163. /**
  164. * 幂等创建订单:clientRequestId 与 userId 构成唯一键,重复提交直接返回已有订单。
  165. * 创建前按最新条件重新报价,客户端回传报价不一致时抛出 FlashDeliveryQuoteChangedException;
  166. * 成功后保存计价快照、寄件图片和初始状态日志。
  167. */
  168. @Transactional
  169. public FlashDeliveryOrderDetailView create(Long userId, FlashDeliveryCreateRequest request) {
  170. requireUserId(userId);
  171. if (request == null || !hasText(request.getClientRequestId())
  172. || request.getClientRequestId().trim().length() > 64) {
  173. throw fail("flash.delivery.client.request.id.invalid");
  174. }
  175. String requestId = request.getClientRequestId().trim();
  176. // 用户 ID + clientRequestId 构成幂等键;重复请求直接返回第一次创建的订单。
  177. FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
  178. if (existing != null) return participantDetail(lockOrder(existing.getId()), true, false);
  179. if (request.getUserNote() != null && request.getUserNote().length() > 500) {
  180. throw fail("flash.delivery.note.too.long");
  181. }
  182. QuoteContext quote = calculateQuote(request);
  183. FlashDeliveryQuoteView latestQuote = quoteView(request, quote);
  184. if (!matchesQuotedPrice(request, latestQuote)) {
  185. throw new FlashDeliveryQuoteChangedException(latestQuote);
  186. }
  187. Date now = new Date();
  188. String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
  189. validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
  190. boolean pinRequired = request.getPinRequired() == null || request.getPinRequired();
  191. String payType = normalizePayType(request.getPayType());
  192. List<String> senderImages = validateProofUrls(request.getSenderImageUrls(), false);
  193. FlashDeliveryOrder order = new FlashDeliveryOrder();
  194. order.setOrderNo(generateOrderNo());
  195. order.setClientRequestId(requestId);
  196. order.setUserId(userId);
  197. order.setServiceType(request.getServiceType());
  198. order.setDeliveryType(request.getDeliveryType());
  199. order.setVehicleType(request.getVehicleType());
  200. order.setStatus(WAITING_ACCEPTANCE);
  201. copyPickupSummary(order, quote.pickups());
  202. order.setPickedUpStopCount(0);
  203. order.setIsDisplay("NOW".equals(deliveryMode));
  204. order.setDeliveryMode(deliveryMode);
  205. order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
  206. order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
  207. order.setPinRequired(pinRequired);
  208. order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
  209. copyAddress(request.getDelivery(), order, false);
  210. order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
  211. order.setDistanceMeters(quote.route().distanceMeters());
  212. order.setDistanceSource(quote.route().source());
  213. order.setEstimatedDurationSeconds(quote.route().durationSeconds());
  214. order.setAmount(quote.breakdown().getAmount());
  215. order.setCurrency("TWD");
  216. order.setPayType(payType);
  217. order.setPricingId(quote.pricing().getId());
  218. order.setPricingVersion(quote.pricing().getConfigVersion());
  219. order.setPricingStartTime(quote.pricing().getStartTime());
  220. order.setPricingEndTime(quote.pricing().getEndTime());
  221. order.setStartingDistance(quote.pricing().getStartingDistance());
  222. order.setStartingFare(quote.pricing().getStartingFare());
  223. order.setDistance(quote.pricing().getDistance());
  224. order.setFreight(quote.pricing().getFreight());
  225. // 保存计价快照,后台调价只影响新订单,不追溯修改历史订单金额。
  226. order.setDistanceFee(quote.breakdown().getDistanceFee());
  227. order.setBaseDeliveryFee(quote.breakdown().getBaseDeliveryFee());
  228. order.setUrgentRate(quote.pricing().getUrgentRate());
  229. order.setMinimumUrgentFee(quote.pricing().getMinimumUrgentFee());
  230. order.setUrgentFee(quote.breakdown().getUrgentFee());
  231. order.setTipAmount(quote.breakdown().getTipAmount());
  232. order.setUserNote(trimToNull(request.getUserNote()));
  233. order.setVersion(0);
  234. order.setCreateTime(now);
  235. order.setUpdateTime(now);
  236. try {
  237. orderMapper.insert(order);
  238. } catch (DataIntegrityViolationException duplicate) {
  239. // order_no 撞号兜底(时钟回拨/多实例等理论情形):唯一索引 uk_flash_order_no 拒绝后换号重插一次。
  240. if (isOrderNoConflict(duplicate)) {
  241. order.setId(null);
  242. order.setOrderNo(generateOrderNo());
  243. orderMapper.insert(order);
  244. } else {
  245. // 并发重复请求可能同时通过前置查询,最终由数据库唯一键完成幂等收口。
  246. FlashDeliveryOrder concurrent = orderMapper.selectOne(new QueryWrapper<FlashDeliveryOrder>()
  247. .eq("user_id", userId).eq("client_request_id", requestId).last("FOR UPDATE"));
  248. if (concurrent != null) return participantDetail(lockOrder(concurrent.getId()), true, false);
  249. throw duplicate;
  250. }
  251. }
  252. saveStops(order, quote.pickups(), request.getDelivery(), now);
  253. saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
  254. writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
  255. if (Boolean.TRUE.equals(order.getIsDisplay())) {
  256. notificationService.notifyAvailable(order);
  257. }
  258. return participantDetail(order, true, false);
  259. }
  260. /**
  261. * 闪送订单号:98 + 13 位毫秒时间戳 + 2 位同毫秒递增序列,共 17 位纯数字(2026-09-22 起)。
  262. * 与外卖订单(99 + 毫秒时间戳)风格同构,便于电话报单与按时间排查;
  263. * 同毫秒并发由 JVM 内原子递增序列区分,时钟回拨/多实例等理论撞号由
  264. * order_no 唯一索引(uk_flash_order_no)兜底,插入冲突换号重试。
  265. * 2026-09-22 之前的存量单为「FD + UUID 前 24 位大写」旧格式,保持不变。
  266. */
  267. static String generateOrderNo() {
  268. long seq = ORDER_NO_SEQ.updateAndGet(current -> (current + 1) % 100);
  269. return "98" + System.currentTimeMillis() + String.format(Locale.ROOT, "%02d", seq);
  270. }
  271. /** 判断唯一键冲突是否来自订单号(区别于 userId+clientRequestId 幂等键)。 */
  272. private static boolean isOrderNoConflict(DataIntegrityViolationException exception) {
  273. String message = exception.getMostSpecificCause().getMessage();
  274. return message != null && message.contains("uk_flash_order_no");
  275. }
  276. /** 分页查询当前用户参与的订单卡片;role=sender 按寄件人、receiver 按收件人过滤。 */
  277. public IPage<FlashDeliveryUserOrderListView> userOrders(Long userId, int pageNum, int pageSize, String role) {
  278. String selectedRole = hasText(role) ? role.trim().toLowerCase(Locale.ROOT) : "sender";
  279. if (!"sender".equals(selectedRole) && !"receiver".equals(selectedRole)) {
  280. throw fail("flash.delivery.tab.invalid");
  281. }
  282. String participantColumn = "receiver".equals(selectedRole) ? "receiver_user_id" : "user_id";
  283. QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
  284. .eq(participantColumn, userId).orderByDesc("create_time");
  285. return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), order -> {
  286. FlashDeliveryUserOrderListView view = userOrderListView(order);
  287. view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && canUserCancel(order));
  288. return view;
  289. });
  290. }
  291. /** 查询本人订单详情,仅寄件人和匹配到的收件人可见。 */
  292. public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
  293. return userDetail(userId, requireOrder(orderId));
  294. }
  295. /** 按订单编号查询本人订单详情;App 点击推送消息打开订单详情时使用。 */
  296. public FlashDeliveryOrderDetailView userDetailByOrderNo(Long userId, String orderNo) {
  297. return userDetail(userId, requireOrderByNo(orderNo));
  298. }
  299. private FlashDeliveryOrderDetailView userDetail(Long userId, FlashDeliveryOrder order) {
  300. if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
  301. FlashDeliveryOrderDetailView view = participantDetail(order, true, false);
  302. view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && canUserCancel(order));
  303. return view;
  304. }
  305. /** 地址修改先报价,用户确认前不改动订单。 */
  306. public FlashDeliveryQuoteView quoteAddress(Long userId, Long orderId,
  307. FlashDeliveryAddressChangeRequest request) {
  308. if (request == null) throw fail("flash.delivery.request.required");
  309. FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
  310. return calculateAddressQuote(order, request);
  311. }
  312. /** 确认修改待接单订单地址:重算报价并与回传值比对,按版本条件更新。 */
  313. @Transactional
  314. public FlashDeliveryOrderDetailView updateAddress(Long userId, Long orderId,
  315. FlashDeliveryAddressConfirmRequest request) {
  316. if (request == null) throw fail("flash.delivery.request.required");
  317. FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
  318. FlashDeliveryQuoteView quote = calculateAddressQuote(order, request);
  319. if (!Objects.equals(request.getQuotedDistanceMeters(), quote.getDistanceMeters())
  320. || !Objects.equals(request.getQuotedBaseDeliveryFee(), quote.getBaseDeliveryFee())
  321. || !Objects.equals(request.getQuotedDistanceFee(), quote.getDistanceFee())
  322. || !Objects.equals(request.getQuotedUrgentFee(), quote.getUrgentFee())
  323. || !Objects.equals(request.getQuotedAmount(), quote.getAmount())) {
  324. throw new FlashDeliveryQuoteChangedException(quote);
  325. }
  326. List<FlashDeliveryPickupRequest> pickups = editablePickups(order, request);
  327. copyPickupSummary(order, pickups);
  328. copyAddress(request.getDelivery(), order, false);
  329. order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
  330. order.setDistanceMeters(quote.getDistanceMeters());
  331. order.setDistanceSource(quote.getDistanceSource());
  332. order.setEstimatedDurationSeconds(quote.getEstimatedDurationSeconds());
  333. // 改址报价携带原运价快照,金额与完整站点同事务保存,不引入最新运价。
  334. order.setPricingId(quote.getPricingId());
  335. order.setPricingVersion(quote.getPricingVersion());
  336. order.setPricingStartTime(quote.getStartTime());
  337. order.setPricingEndTime(quote.getEndTime());
  338. order.setStartingDistance(quote.getStartingDistance());
  339. order.setStartingFare(quote.getStartingFare());
  340. order.setDistance(quote.getDistance());
  341. order.setFreight(quote.getFreight());
  342. order.setUrgentRate(quote.getUrgentRate());
  343. order.setMinimumUrgentFee(quote.getMinimumUrgentFee());
  344. order.setDistanceFee(quote.getDistanceFee());
  345. order.setBaseDeliveryFee(quote.getBaseDeliveryFee());
  346. order.setUrgentFee(quote.getUrgentFee());
  347. order.setAmount(quote.getAmount());
  348. Date now = new Date();
  349. order.setUpdateTime(now);
  350. // 即使上面的查询仍显示待接单,骑手也可能已接走,最终以条件 UPDATE 为准。
  351. if (orderMapper.updateWaitingAddress(order, request.getOrderVersion()) != 1) {
  352. throw fail("flash.delivery.state.changed");
  353. }
  354. // 条件更新成功后持有订单行锁至提交,抢单不会读到仅修改一半的地址。
  355. stopMapper.delete(new QueryWrapper<FlashDeliveryOrderStop>().eq("order_id", orderId));
  356. saveStops(order, pickups, request.getDelivery(), now);
  357. order.setVersion(order.getVersion() + 1);
  358. writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "ADDRESS_UPDATED", now);
  359. return participantDetail(order, true, false);
  360. }
  361. /** 待接单订单追加小费:订单总额与小费同步增加,版本条件防止并发重复加款。 */
  362. @Transactional
  363. public FlashDeliveryOrderDetailView addTip(Long userId, Long orderId, FlashDeliveryTipAddRequest request) {
  364. if (request == null) throw fail("flash.delivery.request.required");
  365. FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
  366. if (request.getAdditionalTipAmount() == null || request.getAdditionalTipAmount().signum() <= 0) {
  367. throw fail("flash.delivery.tip.additional.invalid");
  368. }
  369. long tip;
  370. long amount;
  371. try {
  372. // 保留 JSON 原始数值,禁止 1.9 等小数在绑定时被截断为整数小费。
  373. long additionalTip = request.getAdditionalTipAmount().longValueExact();
  374. tip = Math.addExact(order.getTipAmount(), additionalTip);
  375. amount = Math.addExact(order.getAmount(), additionalTip);
  376. } catch (ArithmeticException exception) {
  377. throw fail("flash.delivery.tip.additional.invalid");
  378. }
  379. order.setTipAmount(tip);
  380. order.setAmount(amount);
  381. Date now = new Date();
  382. order.setUpdateTime(now);
  383. if (orderMapper.updateWaitingTip(order, request.getOrderVersion()) != 1) {
  384. throw fail("flash.delivery.state.changed");
  385. }
  386. order.setVersion(order.getVersion() + 1);
  387. writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "TIP_ADDED", now);
  388. return participantDetail(order, true, false);
  389. }
  390. /** 校验并加载可编辑订单:本人、待接单、未绑定骑手且版本一致。 */
  391. private FlashDeliveryOrder requireEditableOrder(Long userId, Long orderId, Integer version) {
  392. requireUserId(userId);
  393. if (orderId == null || orderId <= 0) throw fail("flash.delivery.order.not.found");
  394. // 写入路径先锁订单再读取站点,保持与抢单、取货和取消一致的锁顺序。
  395. FlashDeliveryOrder order = TransactionSynchronizationManager.isActualTransactionActive()
  396. ? lockOrder(orderId) : requireOrder(orderId);
  397. if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
  398. if (!WAITING_ACCEPTANCE.equals(order.getStatus()) || order.getRiderId() != null) {
  399. throw fail("flash.delivery.edit.not.allowed");
  400. }
  401. if (version == null || version < 0) throw fail("flash.delivery.order.version.invalid");
  402. if (!Objects.equals(version, order.getVersion())) throw fail("flash.delivery.state.changed");
  403. return order;
  404. }
  405. /** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
  406. private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
  407. FlashDeliveryAddressChangeRequest request) {
  408. List<FlashDeliveryPickupRequest> pickups = editablePickups(order, request);
  409. validatePickups(pickups, request.getDelivery());
  410. RouteDistance route = pickupRoute(pickups, request.getDelivery(), vehicleTypeOf(order));
  411. // FR-074/093:改址沿用下单快照,只重算整条路线,平台后续调价不影响此单。
  412. FlashDeliveryPricing pricing = new FlashDeliveryPricing();
  413. pricing.setId(order.getPricingId()); pricing.setConfigVersion(order.getPricingVersion());
  414. pricing.setStartTime(order.getPricingStartTime()); pricing.setEndTime(order.getPricingEndTime());
  415. pricing.setStartingDistance(order.getStartingDistance()); pricing.setStartingFare(order.getStartingFare());
  416. pricing.setDistance(order.getDistance()); pricing.setFreight(order.getFreight());
  417. pricing.setUrgentRate(order.getUrgentRate()); pricing.setMinimumUrgentFee(order.getMinimumUrgentFee());
  418. FlashDeliveryPriceBreakdown breakdown;
  419. try {
  420. breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
  421. order.getDeliveryType(), order.getTipAmount());
  422. } catch (ArithmeticException exception) {
  423. throw fail("flash.delivery.tip.invalid");
  424. }
  425. FlashDeliveryQuoteRequest quoteRequest = new FlashDeliveryQuoteRequest();
  426. quoteRequest.setServiceType(order.getServiceType());
  427. quoteRequest.setDeliveryType(order.getDeliveryType());
  428. quoteRequest.setVehicleType(vehicleTypeOf(order));
  429. quoteRequest.setDeliveryMode(order.getDeliveryMode());
  430. quoteRequest.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  431. quoteRequest.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  432. FlashDeliveryQuoteView view = quoteView(quoteRequest, new QuoteContext(pricing, route, breakdown, pickups));
  433. view.setOrderVersion(order.getVersion());
  434. return view;
  435. }
  436. /** 用户取消:仅待接单或已接单(实际取件前)允许。 */
  437. @Transactional
  438. public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
  439. FlashDeliveryOrder order = lockOrder(orderId);
  440. if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
  441. if (anyPicked(requireStops(order)) || !FlashDeliveryStateMachine.canUserCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
  442. String reason = requiredReason(request);
  443. Date now = new Date();
  444. if (orderMapper.cancel(orderId, order.getStatus(), "USER", userId, reason, now) != 1) {
  445. throw fail("flash.delivery.state.changed");
  446. }
  447. writeLog(orderId, order.getStatus(), CANCELLED, "USER", userId, reason, now);
  448. // 取消成功后通知接单骑手;未接单订单由通知服务内部跳过。
  449. notificationService.notifyCancelled(order);
  450. }
  451. /** 用户(寄件人或收件人)确认收货:已送达推进为已完成。 */
  452. @Transactional
  453. public void confirmReceipt(Long userId, Long orderId) {
  454. FlashDeliveryOrder order = lockOrder(orderId);
  455. if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
  456. if (!allPicked(requireStops(order))) throw fail("flash.delivery.complete.not.allowed");
  457. Date now = new Date();
  458. if (orderMapper.transitionByParticipant(orderId, userId, DELIVERED, COMPLETED, now) != 1) {
  459. throw fail("flash.delivery.complete.not.allowed");
  460. }
  461. writeLog(orderId, DELIVERED, COMPLETED, "USER", userId, null, now);
  462. }
  463. /** 寄件人对已完成订单评价骑手:星级 1-5 仅一次,条件更新原子防并发重复提交。 */
  464. @Transactional
  465. public void rateRider(Long userId, Long orderId, FlashDeliveryRatingRequest request) {
  466. if (request == null) throw fail("flash.delivery.request.required");
  467. Double stars = request.getStars();
  468. if (stars == null || stars < 1 || stars > 5) throw fail("flash.delivery.rating.stars.invalid");
  469. FlashDeliveryOrder order = requireOrder(orderId);
  470. if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
  471. if (!COMPLETED.equals(order.getStatus()) || order.getRiderId() == null) {
  472. throw fail("flash.delivery.rating.not.allowed");
  473. }
  474. if (order.getRiderStars() != null) throw fail("flash.delivery.rating.already");
  475. if (orderMapper.rateRider(orderId, userId, stars, new Date()) != 1) {
  476. throw fail("flash.delivery.rating.already");
  477. }
  478. }
  479. /**
  480. * 骑手任务列表:按页签查询(newTask 可抢任务、toPickup 待取件、delivering 配送中、
  481. * completed 已送达/已完成、cancelled 已取消)。newTask 页签额外按骑手坐标做距离过滤和就近排序,
  482. * 并返回附近任务数与最高订单金额;其余页签按订单归属骑手查询。
  483. */
  484. public FlashDeliveryRiderOrderPageView riderOrders(Long riderId, int pageNum, int pageSize,
  485. String tab, BigDecimal longitude, BigDecimal latitude) {
  486. InfoUser rider = requireRider(riderId);
  487. Set<String> acceptedPayTypes = acceptedRiderPayTypes(rider);
  488. if (!hasText(tab)) throw fail("flash.delivery.tab.invalid");
  489. String selectedTab = tab.trim();
  490. // lambda 无法捕获被重新赋值的参数,统一改用仅 newTask 携带坐标的有效变量。
  491. BigDecimal effectiveLongitude = "newTask".equals(selectedTab) ? longitude : null;
  492. BigDecimal effectiveLatitude = "newTask".equals(selectedTab) ? latitude : null;
  493. if ("newTask".equals(selectedTab)) {
  494. // 新任务派单按骑手选择的配送类型过滤;本人已接订单页签不受限制。
  495. if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
  496. validateRiderCoordinates(effectiveLongitude, effectiveLatitude);
  497. }
  498. Integer riderVehicleType = riderVehicleType(rider);
  499. QueryWrapper<FlashDeliveryOrder> query = riderOrdersQuery(riderId, riderVehicleType, selectedTab,
  500. effectiveLongitude, effectiveLatitude, true, acceptedPayTypes);
  501. IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
  502. FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
  503. result.setRecords(orders.getRecords().stream()
  504. .map(order -> riderOrderListView(order, effectiveLongitude, effectiveLatitude)).toList());
  505. result.setTotal(orders.getTotal());
  506. result.setCurrent(orders.getCurrent());
  507. result.setSize(orders.getSize());
  508. if ("newTask".equals(selectedTab)) {
  509. result.setNearbyTaskCount(orders.getTotal());
  510. QueryWrapper<FlashDeliveryOrder> maximumQuery = riderOrdersQuery(riderId, riderVehicleType, selectedTab,
  511. effectiveLongitude, effectiveLatitude, false, acceptedPayTypes)
  512. .select("MAX(amount)");
  513. result.setHighestOrderAmount(firstLong(orderMapper.selectObjs(maximumQuery)));
  514. }
  515. return result;
  516. }
  517. /** 骑手任务详情:抢单前返回脱敏视图(隐藏联系人、电话、坐标和 PIN);接单后仅中单骑手可见完整信息。 */
  518. public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
  519. return riderDetail(riderId, requireOrder(orderId));
  520. }
  521. /** 按订单编号查询骑手视角订单详情;脱敏与归属校验和数字 ID 查询一致。 */
  522. public FlashDeliveryOrderDetailView riderDetailByOrderNo(Long riderId, String orderNo) {
  523. return riderDetail(riderId, requireOrderByNo(orderNo));
  524. }
  525. private FlashDeliveryOrderDetailView riderDetail(Long riderId, FlashDeliveryOrder order) {
  526. InfoUser rider = requireRider(riderId);
  527. if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
  528. // 未开通闪送或车型不匹配时按订单不存在处理,避免泄露待抢任务信息。
  529. if (!Boolean.TRUE.equals(order.getIsDisplay()) || !isAvailableAt(order, new Date())
  530. || !rider.supportsDeliveryType("FLASH")
  531. || !isRiderVehicleCompatible(rider, order)) {
  532. throw fail("flash.delivery.order.not.found");
  533. }
  534. // 接单前显示取送文字地址,但隐藏联系人、电话、精确坐标、实际 PIN 和履约图片。
  535. return participantDetail(order, false, true);
  536. }
  537. if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
  538. return participantDetail(order, false, false);
  539. }
  540. /** 原子抢单:骑手锁内完成互斥校验与条件更新,同一订单仅一名骑手成功。 */
  541. @Transactional
  542. public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
  543. return riderDeliveryLockService.withLock(riderId, () -> {
  544. FlashDeliveryOrder target = requireOrder(orderId);
  545. InfoUser rider = requireRider(riderId);
  546. // 列表过滤不能替代锁内校验,防止骑手绕过列表直接提交不同车型订单。
  547. if (!isRiderVehicleCompatible(rider, target)) throw fail("flash.delivery.rider.vehicle.not.match");
  548. Date now = new Date();
  549. // 预约单必须仍处在取件窗口内;即使骑手持有订单 ID,也不能绕过列表限制提前或逾期接单。
  550. if (!Boolean.TRUE.equals(target.getIsDisplay()) || !isAvailableAt(target, now)) {
  551. throw fail("flash.delivery.schedule.not.open");
  552. }
  553. // 账号资格、闪送/总任务容量与急送独占统一在骑手锁内复核,列表结果不能替代接单校验。
  554. riderDeliveryAcceptanceService.assertCanAcceptFlash(riderId, target.getDeliveryType());
  555. // Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
  556. if (orderMapper.accept(orderId, riderId, now) != 1) {
  557. throw fail("flash.delivery.order.already.accepted");
  558. }
  559. writeLog(orderId, WAITING_ACCEPTANCE, ACCEPTED, "RIDER", riderId, null, now);
  560. notificationService.notifyAccepted(target);
  561. return participantDetail(requireOrder(orderId), false, false);
  562. });
  563. }
  564. /** 骑手逐站取件:保存本站凭证,仅在全部取货站完成后推进订单为已取件。 */
  565. @Transactional
  566. public void pickup(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
  567. pickupStation(riderId, orderId, request == null ? null : request.getStopId(), request);
  568. }
  569. /** 骑手送达:校验交付 PIN(启用时)并提交送达凭证,订单由已取件推进为已送达。 */
  570. @Transactional
  571. public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
  572. requireRider(riderId);
  573. FlashDeliveryOrder order = lockOrder(orderId);
  574. if (!Objects.equals(riderId, order.getRiderId()) || !PICKED_UP.equals(order.getStatus())) {
  575. throw fail("flash.delivery.transition.not.allowed");
  576. }
  577. List<FlashDeliveryOrderStop> rows = requireStops(order);
  578. if (!allPicked(rows)) throw fail("flash.delivery.transition.not.allowed");
  579. if (Boolean.TRUE.equals(order.getPinRequired())
  580. && (request == null || !Objects.equals(order.getDeliveryPinCode(), trimToNull(request.getPinCode())))) {
  581. throw fail("flash.delivery.pin.invalid");
  582. }
  583. FlashDeliveryOrderStop delivery = rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).findFirst().orElseThrow();
  584. if (request != null && request.getStopId() != null && !Objects.equals(request.getStopId(), delivery.getId())) throw fail("flash.delivery.stop.invalid");
  585. List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
  586. Date now = new Date();
  587. if (orderMapper.transitionByRider(orderId, riderId, PICKED_UP, DELIVERED, "delivered_at", now) != 1) {
  588. throw fail("flash.delivery.transition.not.allowed");
  589. }
  590. delivery.setStatus("DELIVERED"); delivery.setDeliveredAt(now);
  591. if (stopMapper.updateById(delivery) != 1) throw fail("flash.delivery.state.changed");
  592. saveStationImages(orderId, delivery.getId(), "DELIVERY", "RIDER", riderId, urls, now);
  593. writeLog(orderId, PICKED_UP, DELIVERED, "RIDER", riderId, null, now);
  594. // 送达成功后通知寄件用户;重读订单以携带最新状态。
  595. notificationService.notifyDelivered(requireOrder(orderId));
  596. }
  597. /** 平台运价配置列表,按时段开始时间排序。 */
  598. public List<FlashDeliveryPricing> adminPricing() {
  599. return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
  600. .orderByAsc("start_time", "id"));
  601. }
  602. /** 新增运价配置:校验参数合法性与时段不重叠后入库,版本号从 1 开始。 */
  603. @Transactional
  604. public FlashDeliveryPricing createPricing(Long adminId, FlashDeliveryPricingRequest request) {
  605. validatePricingRequest(request, false);
  606. ensureNoPricingOverlap(request, null);
  607. Date now = new Date();
  608. FlashDeliveryPricing pricing = pricingFrom(request);
  609. pricing.setConfigVersion(1);
  610. pricing.setUpdatedBy(adminId);
  611. pricing.setCreateTime(now);
  612. pricing.setUpdateTime(now);
  613. pricingMapper.insert(pricing);
  614. return pricing;
  615. }
  616. /** 修改运价配置:按 configVersion 乐观锁条件更新,防止管理员并发互相覆盖。 */
  617. @Transactional
  618. public FlashDeliveryPricing updatePricing(Long adminId, Long pricingId,
  619. FlashDeliveryPricingRequest request) {
  620. validatePricingRequest(request, true);
  621. FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
  622. if (preliminary == null) throw fail("flash.delivery.pricing.not.found");
  623. FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
  624. if (pricing == null) throw fail("flash.delivery.pricing.not.found");
  625. ensureNoPricingOverlap(request, pricingId);
  626. Date now = new Date();
  627. UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
  628. .eq("id", pricingId).eq("config_version", request.getConfigVersion())
  629. .set("vehicle_type", request.getVehicleType())
  630. .set("start_time", request.getStartTime())
  631. .set("end_time", request.getEndTime())
  632. .set("starting_distance", scaleDistance(request.getStartingDistance()))
  633. .set("starting_fare", request.getStartingFare())
  634. .set("distance", scaleDistance(request.getDistance()))
  635. .set("freight", request.getFreight())
  636. .set("urgent_rate", normalizeRate(request.getUrgentRate()))
  637. .set("minimum_urgent_fee", request.getMinimumUrgentFee())
  638. .set("updated_by", adminId)
  639. .set("update_time", now)
  640. .setSql("config_version = config_version + 1");
  641. // 版本条件防止两个管理员同时保存时后提交者静默覆盖先提交者。
  642. if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
  643. pricing.setVehicleType(request.getVehicleType());
  644. pricing.setStartTime(request.getStartTime());
  645. pricing.setEndTime(request.getEndTime());
  646. pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
  647. pricing.setStartingFare(request.getStartingFare());
  648. pricing.setDistance(scaleDistance(request.getDistance()));
  649. pricing.setFreight(request.getFreight());
  650. pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
  651. pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
  652. pricing.setConfigVersion(request.getConfigVersion() + 1);
  653. pricing.setUpdatedBy(adminId);
  654. pricing.setUpdateTime(now);
  655. return pricing;
  656. }
  657. /** 删除运价配置。 */
  658. @Transactional
  659. public void deletePricing(Long pricingId) {
  660. FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
  661. if (preliminary == null) {
  662. throw fail("flash.delivery.pricing.not.found");
  663. }
  664. FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
  665. if (pricing == null) throw fail("flash.delivery.pricing.not.found");
  666. if (pricingMapper.deleteById(pricingId) != 1) throw fail("flash.delivery.pricing.not.found");
  667. }
  668. /** 平台订单分页:支持状态、业务场景、订单号模糊、寄件用户、骑手条件过滤。 */
  669. public IPage<FlashDeliveryOrder> adminOrders(int pageNum, int pageSize, String status,
  670. String serviceType, String orderNo,
  671. Long userId, Long riderId) {
  672. validateOptionalServiceType(serviceType);
  673. QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>().orderByDesc("id");
  674. optionalEq(query, "status", status);
  675. optionalEq(query, "service_type", serviceType);
  676. if (hasText(orderNo)) query.like("order_no", orderNo.trim());
  677. if (userId != null) query.eq("user_id", userId);
  678. if (riderId != null) query.eq("rider_id", riderId);
  679. return orderMapper.selectPage(page(pageNum, pageSize), query);
  680. }
  681. /** 平台订单审计详情:返回完整订单实体、全部凭证图片和状态日志。 */
  682. public FlashDeliveryAdminOrderDetailView adminDetail(Long orderId) {
  683. FlashDeliveryOrder order = requireOrder(orderId);
  684. FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
  685. view.setOrder(order);
  686. List<FlashDeliveryOrderStop> stationRows = stops(orderId);
  687. view.setPickupStopCount(order.getPickupStopCount()); view.setPickedUpStopCount(order.getPickedUpStopCount());
  688. view.setCanUserCancel(canUserCancel(order)); view.setCurrentTargetStopId(targetStopId(order, stationRows));
  689. view.setTargetAdjustments(adjustmentMapper.selectList(new QueryWrapper<FlashDeliveryTargetAdjustment>()
  690. .eq("order_id", orderId).orderByAsc("create_time", "id")));
  691. view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
  692. .eq("order_id", orderId).orderByAsc("proof_type", "sort_order")));
  693. view.setStops(stopViews(stationRows, true, view.getImages()));
  694. view.setHandoffImageUrls(view.getImages().stream().filter(i -> "HANDOFF".equals(i.getProofType()))
  695. .map(FlashDeliveryOrderImage::getImageUrl).toList());
  696. view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
  697. .eq("order_id", orderId).orderByAsc("create_time", "id")));
  698. return view;
  699. }
  700. /** 平台取消:任何非终态订单可取消,记录操作人与原因,用于异常履约人工收口。 */
  701. @Transactional
  702. public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
  703. FlashDeliveryOrder order = lockOrder(orderId);
  704. if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
  705. String reason = requiredReason(request);
  706. List<FlashDeliveryOrderStop> rows = requireStops(order);
  707. if (request.getProblemStopId() == null || !hasText(request.getHandlingResult()) || tooLong(request.getHandlingResult(), 500)) {
  708. throw fail("flash.delivery.intervention.required");
  709. }
  710. requireStop(rows, request.getProblemStopId());
  711. boolean collected = anyPicked(rows);
  712. if ((collected && !hasText(request.getPickedGoodsDisposition())) || tooLong(request.getPickedGoodsDisposition(), 500)) {
  713. throw fail("flash.delivery.handoff.required");
  714. }
  715. List<String> proofs = validateProofUrls(request.getHandoffImageUrls(), collected);
  716. Date now = new Date();
  717. // 关闭异常订单前保存介入结果和物品去向;凭证写入失败时整笔事务回滚。
  718. UpdateWrapper<FlashDeliveryOrder> audit = new UpdateWrapper<FlashDeliveryOrder>().eq("id", orderId)
  719. .set("problem_stop_id", request.getProblemStopId()).set("handling_result", request.getHandlingResult().trim())
  720. .set("picked_goods_disposition", trimToNull(request.getPickedGoodsDisposition()));
  721. if (orderMapper.update(null, audit) != 1) throw fail("flash.delivery.state.changed");
  722. saveStationImages(orderId, request.getProblemStopId(), "HANDOFF", "ADMIN", adminId, proofs, now);
  723. if (orderMapper.cancel(orderId, order.getStatus(), "ADMIN", adminId, reason, now) != 1) throw fail("flash.delivery.state.changed");
  724. writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
  725. // 平台取消同样只通知接单骑手;寄件用户按规格不推送。
  726. notificationService.notifyCancelled(order);
  727. }
  728. /** 将已送达订单推进为已完成;平台手动与系统自动完成共用此入口,条件更新失败返回 false。 */
  729. @Transactional
  730. public boolean complete(Long operatorId, String operatorType, Long orderId) {
  731. FlashDeliveryOrder order = lockOrder(orderId);
  732. if (!DELIVERED.equals(order.getStatus()) || !allPicked(requireStops(order))) return false;
  733. Date now = new Date();
  734. if (orderMapper.transitionByStatus(orderId, DELIVERED, COMPLETED, "completed_at", now) != 1) return false;
  735. writeLog(orderId, DELIVERED, COMPLETED, operatorType, operatorId, null, now);
  736. return true;
  737. }
  738. /** 骑手确认实际收款:仅本人已送达订单可确认,条件更新保证重复请求不重复写日志。 */
  739. @Transactional
  740. public void confirmPayment(Long riderId, Long orderId) {
  741. requireRider(riderId);
  742. FlashDeliveryOrder order = requireOrder(orderId);
  743. if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
  744. if (!DELIVERED.equals(order.getStatus()) && !COMPLETED.equals(order.getStatus())) {
  745. throw fail("flash.delivery.payment.confirm.not.allowed");
  746. }
  747. if (Integer.valueOf(1).equals(order.getPaymentStatus())) return;
  748. Date now = new Date();
  749. if (orderMapper.update(null, new UpdateWrapper<FlashDeliveryOrder>()
  750. .eq("id", orderId).eq("payment_status", 0)
  751. .set("payment_status", 1).set("update_time", now)) == 1) {
  752. writeLog(orderId, order.getStatus(), order.getStatus(), "RIDER", riderId, "PAYMENT_CONFIRMED", now);
  753. }
  754. }
  755. /** 查询送达时间早于截止点且未完成的订单,供自动完成任务分批处理。 */
  756. public List<FlashDeliveryOrder> autoCompleteCandidates(Date deadline, int limit) {
  757. return orderMapper.selectAutoCompletable(deadline, Math.min(Math.max(limit, 1), 500));
  758. }
  759. /** 查询已经错过预约取件结束时间、但仍无人接单的订单,供 Quartz 任务分批处理。 */
  760. public List<FlashDeliveryOrder> expiredScheduledCandidates(Date now, int limit) {
  761. return orderMapper.selectExpiredScheduled(now, Math.min(Math.max(limit, 1), 500));
  762. }
  763. /** 查询已进入预约窗口但仍未向骑手开放的订单,供独立 Quartz 任务分批处理。 */
  764. public List<FlashDeliveryOrder> scheduledAvailabilityCandidates(Date now, int limit) {
  765. return orderMapper.selectScheduledAvailabilityCandidates(now, Math.min(Math.max(limit, 1), 500));
  766. }
  767. /** 原子开放预约订单;仅 false -> true 成功的一方触发一次骑手新单通知。 */
  768. @Transactional
  769. public boolean openScheduledOrderForRiders(Long orderId, Date now) {
  770. if (orderId == null || now == null) return false;
  771. FlashDeliveryOrder order = orderMapper.selectById(orderId);
  772. if (order == null || orderMapper.openScheduledForRiders(orderId, now) != 1) return false;
  773. order.setIsDisplay(true);
  774. order.setUpdateTime(now);
  775. notificationService.notifyAvailable(order);
  776. return true;
  777. }
  778. /** 原子取消过期预约单;条件更新失败表示订单已被抢、取消或状态已变化,无需重复写日志。 */
  779. @Transactional
  780. public boolean cancelExpiredScheduledOrder(Long orderId, Date now) {
  781. if (orderId == null || now == null || orderMapper.cancelExpiredScheduled(orderId, now) != 1) return false;
  782. writeLog(orderId, WAITING_ACCEPTANCE, CANCELLED, "SYSTEM", null, SCHEDULE_EXPIRED_REASON, now);
  783. return true;
  784. }
  785. /** 报价核心:校验枚举、物品、地址与预约窗后,按时段命中运价并计算路线距离,产出报价上下文。 */
  786. private QuoteContext calculateQuote(FlashDeliveryQuoteRequest request) {
  787. if (request == null) throw fail("flash.delivery.request.required");
  788. validateServiceType(request.getServiceType());
  789. validateDeliveryType(request.getDeliveryType());
  790. List<FlashDeliveryPickupRequest> pickups = normalizePickups(request);
  791. validatePickups(pickups, request.getDelivery());
  792. if (request.getTipAmount() == null || request.getTipAmount() < 0) throw fail("flash.delivery.tip.invalid");
  793. Date now = new Date();
  794. String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
  795. validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
  796. Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
  797. Integer selectedVehicle = requireVehicleType(request.getVehicleType());
  798. FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate), selectedVehicle);
  799. RouteDistance route = pickupRoute(pickups, request.getDelivery(), selectedVehicle);
  800. FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
  801. route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
  802. return new QuoteContext(pricing, route, breakdown, pickups);
  803. }
  804. /** 查询指定时刻命中的运价配置,未配置时直接报错。 */
  805. private FlashDeliveryPricing findPricingAt(String targetTime, Integer vehicleType) {
  806. FlashDeliveryPricing pricing = findPricingAtOrNull(targetTime, vehicleType);
  807. if (pricing == null) throw fail("flash.delivery.pricing.not.available");
  808. return pricing;
  809. }
  810. /** 查询命中运价;命中多条说明配置重叠,报错交由平台修正。 */
  811. private FlashDeliveryPricing findPricingAtOrNull(String targetTime, Integer vehicleType) {
  812. List<FlashDeliveryPricing> pricing = pricingMapper.selectAtTime(targetTime, vehicleType);
  813. if (pricing == null || pricing.isEmpty()) return null;
  814. if (pricing.size() > 1) throw fail("flash.delivery.pricing.overlap");
  815. return pricing.get(0);
  816. }
  817. /** 校验运价请求:时间格式与区间顺序、各金额取值范围及修改时的版本号。 */
  818. private void validatePricingRequest(FlashDeliveryPricingRequest request, boolean requireVersion) {
  819. if (request == null) throw fail("flash.delivery.pricing.invalid");
  820. BigDecimal startingDistance = normalizeDistance(request.getStartingDistance());
  821. BigDecimal distance = normalizeDistance(request.getDistance());
  822. BigDecimal urgentRate = normalizeRate(request.getUrgentRate());
  823. if (!FlashDeliveryVehicleType.isSupported(request.getVehicleType())
  824. || !validStartTime(request.getStartTime()) || !validEndTime(request.getEndTime())
  825. || toMinute(request.getStartTime()) >= toMinute(request.getEndTime())
  826. || startingDistance == null || startingDistance.signum() <= 0
  827. || startingDistance.compareTo(MAX_PRICING_DISTANCE) > 0
  828. || request.getStartingFare() == null || request.getStartingFare() <= 0
  829. || distance == null || distance.signum() <= 0
  830. || distance.compareTo(MAX_PRICING_DISTANCE) > 0
  831. || request.getFreight() == null || request.getFreight() <= 0
  832. || urgentRate == null || urgentRate.signum() < 0
  833. || urgentRate.compareTo(MAX_PRICING_DISTANCE) > 0
  834. || request.getMinimumUrgentFee() == null || request.getMinimumUrgentFee() < 0
  835. || requireVersion && (request.getConfigVersion() == null || request.getConfigVersion() <= 0)) {
  836. throw fail("flash.delivery.pricing.invalid");
  837. }
  838. }
  839. /** 确保新时段与既有运价不重叠,excludedId 用于修改时排除自身。 */
  840. private void ensureNoPricingOverlap(FlashDeliveryPricingRequest request, Long excludedId) {
  841. if (pricingMapper.countOverlapping(request.getStartTime(), request.getEndTime(), request.getVehicleType(), excludedId) > 0) {
  842. throw fail("flash.delivery.pricing.overlap");
  843. }
  844. }
  845. /** 将运价请求转换为实体,距离与比例统一保留两位小数。 */
  846. private FlashDeliveryPricing pricingFrom(FlashDeliveryPricingRequest request) {
  847. FlashDeliveryPricing pricing = new FlashDeliveryPricing();
  848. pricing.setVehicleType(request.getVehicleType());
  849. pricing.setStartTime(request.getStartTime());
  850. pricing.setEndTime(request.getEndTime());
  851. pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
  852. pricing.setStartingFare(request.getStartingFare());
  853. pricing.setDistance(scaleDistance(request.getDistance()));
  854. pricing.setFreight(request.getFreight());
  855. pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
  856. pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
  857. return pricing;
  858. }
  859. private boolean validStartTime(String value) {
  860. return value != null && value.matches("(?:[01]\\d|2[0-3]):[0-5]\\d");
  861. }
  862. private boolean validEndTime(String value) {
  863. return "24:00".equals(value) || validStartTime(value);
  864. }
  865. private int toMinute(String value) {
  866. if ("24:00".equals(value)) return 24 * 60;
  867. return Integer.parseInt(value.substring(0, 2)) * 60 + Integer.parseInt(value.substring(3));
  868. }
  869. private BigDecimal scaleDistance(BigDecimal value) {
  870. return normalizeDistance(value);
  871. }
  872. private BigDecimal normalizeDistance(BigDecimal value) {
  873. return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
  874. }
  875. private BigDecimal normalizeRate(BigDecimal value) {
  876. return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
  877. }
  878. /** 状态、图片和日志处于同一事务;任一图片写入失败会回滚本次业务操作。 */
  879. private void saveImages(Long orderId, String proofType, String operatorType,
  880. Long operatorId, List<String> urls, Date now) {
  881. for (int index = 0; index < urls.size(); index++) {
  882. FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
  883. image.setOrderId(orderId);
  884. image.setProofType(proofType);
  885. image.setImageUrl(urls.get(index));
  886. image.setSortOrder(index);
  887. image.setOperatorType(operatorType);
  888. image.setOperatorId(operatorId);
  889. image.setCreateTime(now);
  890. imageMapper.insert(image);
  891. }
  892. }
  893. /** 校验凭证图片 URL:必填时 1 至 9 张,每项为合法 URI 且不超过 1000 字符。 */
  894. private List<String> validateProofUrls(List<String> values, boolean required) {
  895. if (values == null || values.isEmpty()) {
  896. if (!required) return List.of();
  897. throw fail("flash.delivery.proof.required");
  898. }
  899. if (values.size() > 9) throw fail("flash.delivery.proof.too.many");
  900. List<String> urls = new ArrayList<>();
  901. for (String value : values) {
  902. try {
  903. String url = value == null ? null : value.trim();
  904. if (url == null || url.isEmpty() || url.length() > 1000) {
  905. throw new IllegalArgumentException();
  906. }
  907. URI.create(url);
  908. urls.add(url);
  909. } catch (IllegalArgumentException exception) {
  910. throw fail("flash.delivery.proof.url.invalid");
  911. }
  912. }
  913. return urls;
  914. }
  915. /** 骑手闪送平均分;尚无评价时按平台骑手展示惯例回退 4.5。 */
  916. private Double riderAverageStars(Long riderId) {
  917. Double average = orderMapper.selectRiderAverageStars(riderId);
  918. return average == null ? 4.5 : average;
  919. }
  920. /** 送达后线下转账单返回骑手启用卡三要素(户名/银行名/账号);无启用卡返回 null。 */
  921. private FlashDeliveryRiderBankCardView riderActiveCardView(Long riderId) {
  922. if (riderId == null) return null;
  923. return bankCardMapper.selectList(currentRead(new QueryWrapper<com.ruoyi.system.domain.InfoBankCard>()
  924. .eq("user_id", riderId).eq("is_active", 1))).stream().findFirst()
  925. .map(card -> new FlashDeliveryRiderBankCardView(card.getBankName(), card.getAccountNo(), card.getAccountName()))
  926. .orElse(null);
  927. }
  928. /** 组装用户或骑手视角详情;hideSensitiveFields 为 true 时按抢单前脱敏返回。 */
  929. private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
  930. boolean userView,
  931. boolean hideSensitiveFields) {
  932. FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
  933. if (!hideSensitiveFields) view.setOrderVersion(order.getVersion());
  934. List<FlashDeliveryOrderStop> stationRows = stops(order.getId());
  935. List<FlashDeliveryOrderImage> stationImages = hideSensitiveFields ? List.of() : imageMapper.selectList(currentRead(
  936. new QueryWrapper<FlashDeliveryOrderImage>().eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
  937. view.setStops(stopViews(stationRows, !hideSensitiveFields, stationImages));
  938. view.setCurrentTargetStopId(targetStopId(order, stationRows));
  939. if (!hideSensitiveFields) {
  940. view.setTargetAdjustments(adjustmentMapper.selectList(currentRead(new QueryWrapper<FlashDeliveryTargetAdjustment>()
  941. .eq("order_id", order.getId()).orderByAsc("create_time", "id"))));
  942. }
  943. if (userView) {
  944. view.setDeliveryPinCode(Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
  945. // 接单后直接返回骑手完整用户资料;star 填充该骑手的闪送平均分供前端展示。
  946. if (order.getRiderId() != null) {
  947. InfoUser rider = userMapper.selectInfoUserByUserId(order.getRiderId());
  948. if (rider != null) {
  949. rider.setStar(riderAverageStars(order.getRiderId()));
  950. }
  951. view.setRider(rider);
  952. }
  953. // 线下转账单送达/完成后返回骑手启用卡,供用户线下转账结清(仅寄件人视角)
  954. if (PAY_TYPE_OFFLINE_TRANSFER.equals(order.getPayType())
  955. && (DELIVERED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()))) {
  956. view.setRiderBankCard(riderActiveCardView(order.getRiderId()));
  957. }
  958. } else if (!hideSensitiveFields) {
  959. // 接单后仅中单骑手可见寄件用户完整资料;抢单前脱敏视图不返回用户身份信息。
  960. if (order.getUserId() != null) {
  961. view.setUser(userMapper.selectInfoUserByUserId(order.getUserId()));
  962. }
  963. }
  964. if (!hideSensitiveFields && order.getId() != null) {
  965. groupImageUrls(view, stationImages);
  966. }
  967. return view;
  968. }
  969. /** 订单实体转用户端订单卡片。 */
  970. private FlashDeliveryUserOrderListView userOrderListView(FlashDeliveryOrder order) {
  971. FlashDeliveryUserOrderListView view = new FlashDeliveryUserOrderListView();
  972. view.setId(order.getId());
  973. view.setOrderNo(order.getOrderNo());
  974. view.setServiceType(order.getServiceType());
  975. view.setDeliveryType(order.getDeliveryType());
  976. view.setVehicleType(vehicleTypeOf(order));
  977. view.setStatus(order.getStatus());
  978. view.setPickupStopCount(order.getPickupStopCount());
  979. view.setPickedUpStopCount(order.getPickedUpStopCount());
  980. view.setCanUserCancel(canUserCancel(order));
  981. view.setOrderVersion(order.getVersion());
  982. view.setPackageType(order.getPackageType());
  983. view.setQuantity(order.getQuantity());
  984. view.setWeightRange(order.getWeightRange());
  985. view.setDeliveryMode(order.getDeliveryMode());
  986. view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  987. view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  988. view.setPickupAddress(order.getPickupAddress());
  989. view.setPickupDetailAddress(order.getPickupAddressDetail());
  990. view.setDeliveryAddress(order.getDeliveryAddress());
  991. view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
  992. view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
  993. view.setBaseDeliveryFee(order.getBaseDeliveryFee());
  994. view.setUrgentFee(order.getUrgentFee());
  995. view.setTipAmount(order.getTipAmount());
  996. view.setAmount(order.getAmount());
  997. view.setCurrency(order.getCurrency());
  998. view.setPaymentStatus(order.getPaymentStatus());
  999. view.setDeliveredAt(order.getDeliveredAt());
  1000. view.setCreateTime(order.getCreateTime());
  1001. return view;
  1002. }
  1003. /** 订单实体转骑手端任务卡片,可附带骑手到取件点的直线距离。 */
  1004. private FlashDeliveryRiderOrderListView riderOrderListView(FlashDeliveryOrder order,
  1005. BigDecimal longitude, BigDecimal latitude) {
  1006. FlashDeliveryRiderOrderListView view = new FlashDeliveryRiderOrderListView();
  1007. view.setId(order.getId());
  1008. view.setOrderNo(order.getOrderNo());
  1009. view.setServiceType(order.getServiceType());
  1010. view.setDeliveryType(order.getDeliveryType());
  1011. view.setVehicleType(vehicleTypeOf(order));
  1012. view.setStatus(order.getStatus());
  1013. view.setPickupStopCount(order.getPickupStopCount());
  1014. view.setPickedUpStopCount(order.getPickedUpStopCount());
  1015. view.setCanUserCancel(canUserCancel(order));
  1016. view.setPackageType(order.getPackageType());
  1017. view.setQuantity(order.getQuantity());
  1018. view.setWeightRange(order.getWeightRange());
  1019. view.setSpecification(order.getSpecification());
  1020. view.setDeliveryMode(order.getDeliveryMode());
  1021. view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  1022. view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  1023. view.setPinRequired(order.getPinRequired());
  1024. view.setPickupAddress(order.getPickupAddress());
  1025. view.setPickupDetailAddress(order.getPickupAddressDetail());
  1026. view.setDeliveryAddress(order.getDeliveryAddress());
  1027. view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
  1028. view.setPayType(order.getPayType());
  1029. view.setPaymentStatus(order.getPaymentStatus());
  1030. view.setPickupDistanceMeters(distanceMeters(longitude, latitude,
  1031. order.getPickupLongitude(), order.getPickupLatitude()));
  1032. view.setDistanceMeters(order.getDistanceMeters());
  1033. view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
  1034. view.setBaseDeliveryFee(order.getBaseDeliveryFee());
  1035. view.setDistanceFee(order.getDistanceFee());
  1036. view.setUrgentFee(order.getUrgentFee());
  1037. view.setTipAmount(order.getTipAmount());
  1038. view.setAmount(order.getAmount());
  1039. view.setCurrency(order.getCurrency());
  1040. view.setCreateTime(order.getCreateTime());
  1041. return view;
  1042. }
  1043. /** 订单实体转详情基础视图。 */
  1044. private FlashDeliveryOrderDetailView detailOrderView(FlashDeliveryOrder order, boolean includeSensitiveFields) {
  1045. FlashDeliveryOrderDetailView view = new FlashDeliveryOrderDetailView();
  1046. populateOrderView(view, order, includeSensitiveFields);
  1047. view.setRiderStars(order.getRiderStars());
  1048. return view;
  1049. }
  1050. /** 按凭证类型把图片分组为寄件、取件、送达三组 URL。 */
  1051. private void groupImageUrls(FlashDeliveryOrderDetailView view, List<FlashDeliveryOrderImage> images) {
  1052. List<String> sender = new ArrayList<>();
  1053. List<String> pickup = new ArrayList<>();
  1054. List<String> delivery = new ArrayList<>();
  1055. for (FlashDeliveryOrderImage image : images) {
  1056. if ("SENDER".equals(image.getProofType())) sender.add(image.getImageUrl());
  1057. else if ("PICKUP".equals(image.getProofType())) pickup.add(image.getImageUrl());
  1058. else if ("DELIVERY".equals(image.getProofType())) delivery.add(image.getImageUrl());
  1059. }
  1060. view.setSenderImageUrls(sender);
  1061. view.setPickupImageUrls(pickup);
  1062. view.setDeliveryImageUrls(delivery);
  1063. }
  1064. /** 运价实体转首页服务摘要视图。 */
  1065. private FlashDeliveryServiceView serviceView(FlashDeliveryPricing pricing, String serviceType) {
  1066. FlashDeliveryServiceView view = new FlashDeliveryServiceView();
  1067. view.setServiceType(serviceType);
  1068. view.setPricingId(pricing.getId());
  1069. view.setStartTime(pricing.getStartTime());
  1070. view.setEndTime(pricing.getEndTime());
  1071. view.setStartingDistance(pricing.getStartingDistance());
  1072. view.setStartingFare(pricing.getStartingFare());
  1073. view.setDistance(pricing.getDistance());
  1074. view.setFreight(pricing.getFreight());
  1075. view.setUrgentRate(pricing.getUrgentRate());
  1076. view.setMinimumUrgentFee(pricing.getMinimumUrgentFee());
  1077. view.setPricingVersion(pricing.getConfigVersion());
  1078. view.setCurrency("TWD");
  1079. return view;
  1080. }
  1081. /** 将订单实体的公共字段填充到视图,includeSensitiveFields 控制是否包含敏感字段。 */
  1082. private void populateOrderView(FlashDeliveryOrderView view, FlashDeliveryOrder order,
  1083. boolean includeSensitiveFields) {
  1084. view.setId(order.getId());
  1085. view.setOrderNo(order.getOrderNo());
  1086. view.setServiceType(order.getServiceType());
  1087. view.setDeliveryType(order.getDeliveryType());
  1088. view.setVehicleType(vehicleTypeOf(order));
  1089. view.setStatus(order.getStatus());
  1090. view.setPickupStopCount(order.getPickupStopCount());
  1091. view.setPickedUpStopCount(order.getPickedUpStopCount());
  1092. view.setCanUserCancel(canUserCancel(order));
  1093. view.setPackageType(order.getPackageType());
  1094. view.setQuantity(order.getQuantity());
  1095. view.setWeightRange(order.getWeightRange());
  1096. view.setSpecification(order.getSpecification());
  1097. view.setDeliveryMode(order.getDeliveryMode());
  1098. view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
  1099. view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
  1100. view.setPinRequired(order.getPinRequired());
  1101. view.setPickup(addressView(order, true, includeSensitiveFields));
  1102. view.setDelivery(addressView(order, false, includeSensitiveFields));
  1103. view.setDistanceMeters(order.getDistanceMeters());
  1104. view.setDistanceSource(order.getDistanceSource());
  1105. view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
  1106. view.setBaseDeliveryFee(order.getBaseDeliveryFee());
  1107. view.setDistanceFee(order.getDistanceFee());
  1108. view.setUrgentFee(order.getUrgentFee());
  1109. view.setTipAmount(order.getTipAmount());
  1110. view.setAmount(order.getAmount());
  1111. view.setCurrency(order.getCurrency());
  1112. view.setPayType(order.getPayType());
  1113. view.setPaymentStatus(order.getPaymentStatus());
  1114. view.setUserNote(includeSensitiveFields ? order.getUserNote() : null);
  1115. view.setAcceptedAt(order.getAcceptedAt());
  1116. view.setPickedUpAt(order.getPickedUpAt());
  1117. view.setDeliveredAt(order.getDeliveredAt());
  1118. view.setCompletedAt(order.getCompletedAt());
  1119. view.setCancelledAt(order.getCancelledAt());
  1120. view.setCancelReason(order.getCancelReason());
  1121. view.setCreateTime(order.getCreateTime());
  1122. view.setUpdateTime(order.getUpdateTime());
  1123. }
  1124. /** 从订单实体提取取件或收件地址视图;脱敏时不返回联系人、电话和坐标。 */
  1125. private FlashDeliveryAddressView addressView(FlashDeliveryOrder order, boolean pickup,
  1126. boolean includeSensitiveFields) {
  1127. FlashDeliveryAddressView view = new FlashDeliveryAddressView();
  1128. if (pickup) {
  1129. view.setName(includeSensitiveFields ? order.getPickupName() : null);
  1130. view.setPhone(includeSensitiveFields ? order.getPickupPhone() : null);
  1131. view.setAddress(order.getPickupAddress()); view.setAddressDetail(order.getPickupAddressDetail());
  1132. view.setCity(order.getPickupCity()); view.setArea(order.getPickupArea());
  1133. view.setHandoffMethod(order.getPickupHandoffMethod());
  1134. view.setLongitude(includeSensitiveFields ? order.getPickupLongitude() : null);
  1135. view.setLatitude(includeSensitiveFields ? order.getPickupLatitude() : null);
  1136. } else {
  1137. view.setName(includeSensitiveFields ? order.getDeliveryName() : null);
  1138. view.setPhone(includeSensitiveFields ? order.getDeliveryPhone() : null);
  1139. view.setAddress(order.getDeliveryAddress()); view.setAddressDetail(order.getDeliveryAddressDetail());
  1140. view.setCity(order.getDeliveryCity()); view.setArea(order.getDeliveryArea());
  1141. view.setHandoffMethod(order.getDeliveryHandoffMethod());
  1142. view.setLongitude(includeSensitiveFields ? order.getDeliveryLongitude() : null);
  1143. view.setLatitude(includeSensitiveFields ? order.getDeliveryLatitude() : null);
  1144. }
  1145. return view;
  1146. }
  1147. /** 把费用明细写入报价视图。 */
  1148. private void applyBreakdown(FlashDeliveryQuoteView view, FlashDeliveryPriceBreakdown breakdown) {
  1149. view.setBillableDistance(breakdown.getBillableDistance());
  1150. view.setDistanceFee(breakdown.getDistanceFee());
  1151. view.setBaseDeliveryFee(breakdown.getBaseDeliveryFee());
  1152. view.setUrgentFee(breakdown.getUrgentFee());
  1153. view.setTipAmount(breakdown.getTipAmount());
  1154. view.setAmount(breakdown.getAmount());
  1155. }
  1156. /** 把请求地址复制为订单取件或收件快照字段。 */
  1157. private void copyAddress(FlashDeliveryAddressRequest source, FlashDeliveryOrder target, boolean pickup) {
  1158. if (pickup) {
  1159. target.setPickupName(source.getName().trim()); target.setPickupPhone(source.getPhone().trim());
  1160. target.setPickupAddress(source.getAddress().trim()); target.setPickupAddressDetail(trimToNull(source.getAddressDetail()));
  1161. target.setPickupCity(trimToNull(source.getCity())); target.setPickupArea(trimToNull(source.getArea()));
  1162. target.setPickupHandoffMethod(trimToNull(source.getHandoffMethod()));
  1163. target.setPickupLongitude(source.getLongitude()); target.setPickupLatitude(source.getLatitude());
  1164. } else {
  1165. target.setDeliveryName(source.getName().trim()); target.setDeliveryPhone(source.getPhone().trim());
  1166. target.setDeliveryAddress(source.getAddress().trim()); target.setDeliveryAddressDetail(trimToNull(source.getAddressDetail()));
  1167. target.setDeliveryCity(trimToNull(source.getCity())); target.setDeliveryArea(trimToNull(source.getArea()));
  1168. target.setDeliveryHandoffMethod(trimToNull(source.getHandoffMethod()));
  1169. target.setDeliveryLongitude(source.getLongitude()); target.setDeliveryLatitude(source.getLatitude());
  1170. }
  1171. }
  1172. /** 校验地址请求:必填项、长度限制和经纬度取值范围。 */
  1173. private void validateAddress(FlashDeliveryAddressRequest address) {
  1174. if (address == null || !hasText(address.getName()) || !hasText(address.getPhone())
  1175. || !hasText(address.getAddress()) || address.getLatitude() == null || address.getLongitude() == null
  1176. || address.getName().trim().length() > 64 || address.getPhone().trim().length() > 32
  1177. || address.getAddress().trim().length() > 255
  1178. || (address.getAddressDetail() != null && address.getAddressDetail().trim().length() > 255)
  1179. || tooLong(address.getCity(), 64) || tooLong(address.getArea(), 64)
  1180. || tooLong(address.getHandoffMethod(), 40)
  1181. || address.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0
  1182. || address.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0
  1183. || address.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0
  1184. || address.getLongitude().compareTo(BigDecimal.valueOf(180)) > 0) {
  1185. throw fail("flash.delivery.address.invalid");
  1186. }
  1187. }
  1188. /** 收件手机号归一化后唯一匹配普通用户作为收件人;匹配零个或多个视为无收件人。 */
  1189. private Long resolveReceiverUserId(String phone) {
  1190. if (!hasText(phone)) return null;
  1191. StringBuilder normalized = new StringBuilder(phone.length());
  1192. for (int index = 0; index < phone.length(); index++) {
  1193. char value = phone.charAt(index);
  1194. if (Character.isWhitespace(value) || value == '+' || value == '-'
  1195. || value == '(' || value == ')') {
  1196. continue;
  1197. }
  1198. if (value < '0' || value > '9') return null;
  1199. normalized.append(value);
  1200. }
  1201. if (normalized.isEmpty()) return null;
  1202. List<Long> matches = userMapper.selectOrdinaryUserIdsByNormalizedPhone(normalized.toString());
  1203. return matches != null && matches.size() == 1 ? matches.get(0) : null;
  1204. }
  1205. /** 判断用户是否为订单寄件人或匹配收件人。 */
  1206. private boolean isParticipant(Long userId, FlashDeliveryOrder order) {
  1207. return Objects.equals(userId, order.getUserId())
  1208. || Objects.equals(userId, order.getReceiverUserId());
  1209. }
  1210. /** 校验 token 用户为骑手(userType=2)并返回其资料。 */
  1211. private InfoUser requireRider(Long riderId) {
  1212. InfoUser rider = riderId == null ? null : userMapper.selectInfoUserByUserId(riderId);
  1213. if (rider == null || !"2".equals(rider.getUserType())) throw fail("flash.delivery.rider.required");
  1214. return rider;
  1215. }
  1216. /** 加载订单,不存在时统一按订单不存在报错。 */
  1217. private FlashDeliveryOrder requireOrder(Long orderId) {
  1218. FlashDeliveryOrder order = orderId == null ? null : orderMapper.selectById(orderId);
  1219. if (order == null) throw fail("flash.delivery.order.not.found");
  1220. return order;
  1221. }
  1222. /** 按订单编号加载订单;编号为空或不存在时按订单不存在处理。 */
  1223. private FlashDeliveryOrder requireOrderByNo(String orderNo) {
  1224. if (orderNo == null || orderNo.isBlank()) throw fail("flash.delivery.order.not.found");
  1225. FlashDeliveryOrder order = orderMapper.selectOne(new QueryWrapper<FlashDeliveryOrder>()
  1226. .eq("order_no", orderNo.trim()).last("limit 1"));
  1227. if (order == null) throw fail("flash.delivery.order.not.found");
  1228. return order;
  1229. }
  1230. /** 写入状态变更审计日志。 */
  1231. private void writeLog(Long orderId, String from, String to, String operatorType,
  1232. Long operatorId, String reason, Date now) {
  1233. FlashDeliveryOrderLog log = new FlashDeliveryOrderLog();
  1234. log.setOrderId(orderId); log.setFromStatus(from); log.setToStatus(to);
  1235. log.setOperatorType(operatorType); log.setOperatorId(operatorId); log.setReason(reason); log.setCreateTime(now);
  1236. logMapper.insert(log);
  1237. }
  1238. /** 校验并提取取消原因。 */
  1239. private String requiredReason(FlashDeliveryReasonRequest request) {
  1240. if (request == null || !hasText(request.getReason()) || request.getReason().trim().length() > 500) {
  1241. throw fail("flash.delivery.cancel.reason.required");
  1242. }
  1243. return request.getReason().trim();
  1244. }
  1245. private void validateServiceType(String value) {
  1246. if (!hasText(value) || !SERVICE_TYPES.contains(value)) throw fail("flash.delivery.service.type.invalid");
  1247. }
  1248. private void validateDeliveryType(String value) {
  1249. if (!hasText(value) || !DELIVERY_TYPES.contains(value)) {
  1250. throw fail("flash.delivery.delivery.type.invalid");
  1251. }
  1252. }
  1253. /** 支付方式由 031 闸门校验;闪送维度按平台开关判定,缺省现金。 */
  1254. private String normalizePayType(String payType) {
  1255. String value = hasText(payType) ? payType.trim() : PAY_TYPE_CASH;
  1256. paymentMethodGateService.assertUsable(value,
  1257. PaymentMethodGateService.GateScope.RIDER_FLASH, null, null);
  1258. return value;
  1259. }
  1260. /** 将时间转为台北时区 HH:mm,用于命中运价时段。 */
  1261. private String pricingTargetTime(Date date) {
  1262. return PRICING_TIME_FORMAT.format(Instant.ofEpochMilli(date.getTime())
  1263. .atZone(TAIPEI).toLocalTime());
  1264. }
  1265. /** 比对客户端回传报价与服务端最新报价是否完全一致。 */
  1266. private boolean matchesQuotedPrice(FlashDeliveryCreateRequest request, FlashDeliveryQuoteView latest) {
  1267. return Objects.equals(request.getPricingId(), latest.getPricingId())
  1268. && Objects.equals(request.getPricingVersion(), latest.getPricingVersion())
  1269. && Objects.equals(request.getQuotedBaseDeliveryFee(), latest.getBaseDeliveryFee())
  1270. && Objects.equals(request.getQuotedDistanceFee(), latest.getDistanceFee())
  1271. && Objects.equals(request.getQuotedUrgentFee(), latest.getUrgentFee())
  1272. && Objects.equals(request.getQuotedAmount(), latest.getAmount());
  1273. }
  1274. private void validateOptionalServiceType(String value) {
  1275. if (hasText(value)) validateServiceType(value);
  1276. }
  1277. private void requireUserId(Long userId) {
  1278. if (userId == null || userId <= 0) throw fail("flash.delivery.auth.required");
  1279. }
  1280. /** 构造分页对象并夹紧页码与每页数量(1 至 100)。 */
  1281. private Page<FlashDeliveryOrder> page(int pageNum, int pageSize) {
  1282. return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
  1283. }
  1284. /** 分页结果实体转换,保留分页元数据。 */
  1285. private <T> IPage<T> mapPage(IPage<FlashDeliveryOrder> source,
  1286. Function<FlashDeliveryOrder, T> mapper) {
  1287. Page<T> result = new Page<>(source.getCurrent(), source.getSize(), source.getTotal());
  1288. result.setRecords(source.getRecords().stream().map(mapper).toList());
  1289. return result;
  1290. }
  1291. /** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
  1292. private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, Integer riderVehicleType, String tab,
  1293. BigDecimal longitude, BigDecimal latitude,
  1294. boolean orderResults, Set<String> acceptedPayTypes) {
  1295. QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<>();
  1296. Date now = new Date();
  1297. switch (tab) {
  1298. case "newTask" -> {
  1299. query.eq("status", WAITING_ACCEPTANCE).isNull("rider_id").eq("is_display", true)
  1300. .eq("vehicle_type", riderVehicleType)
  1301. .and(item -> item.eq("delivery_mode", "NOW")
  1302. .or(nested -> nested.le("scheduled_pickup_start_at", now)
  1303. .gt("scheduled_pickup_end_at", now)));
  1304. // 同时满足车型和骑手已接受的支付方式,列表不能替代接单接口的资格复核。
  1305. if (acceptedPayTypes.isEmpty()) query.apply("1 = 0");
  1306. else query.in("pay_type", acceptedPayTypes);
  1307. if (longitude != null) {
  1308. Integer limitKm = newTaskDistanceLimit();
  1309. if (limitKm != null) {
  1310. query.apply("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point({0}, {1})) <= {2}",
  1311. longitude, latitude, limitKm.longValue() * 1000L);
  1312. }
  1313. if (orderResults) {
  1314. query.orderByAsc("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point(" + longitude
  1315. + ", " + latitude + "))");
  1316. }
  1317. } else if (orderResults) {
  1318. query.orderByDesc("create_time");
  1319. }
  1320. }
  1321. case "toPickup" -> {
  1322. query.eq("rider_id", riderId).eq("status", ACCEPTED);
  1323. if (orderResults) query.orderByAsc("create_time");
  1324. }
  1325. case "delivering" -> {
  1326. query.eq("rider_id", riderId).eq("status", PICKED_UP);
  1327. if (orderResults) query.orderByAsc("create_time");
  1328. }
  1329. case "completed" -> {
  1330. query.eq("rider_id", riderId).in("status", DELIVERED, COMPLETED);
  1331. if (orderResults) query.orderByDesc("create_time");
  1332. }
  1333. case "cancelled" -> {
  1334. query.eq("rider_id", riderId).eq("status", CANCELLED);
  1335. if (orderResults) query.orderByDesc("create_time");
  1336. }
  1337. default -> throw fail("flash.delivery.tab.invalid");
  1338. }
  1339. return query;
  1340. }
  1341. /** 骑手可抢支付方式:勾选(NULL=平台开放全部)∩ 平台开关 ∩ 就绪度,统一走闸门判定;现金单始终可见。 */
  1342. private Set<String> acceptedRiderPayTypes(InfoUser rider) {
  1343. Set<String> payTypes = new LinkedHashSet<>();
  1344. payTypes.add(PAY_TYPE_CASH);
  1345. for (PaymentMethodGateService.PayMethodOption option : paymentMethodGateService.listAvailable(
  1346. PaymentMethodGateService.GateScope.RIDER_FLASH, null, rider.getUserId())) {
  1347. if (option.isReady()) payTypes.addAll(PaymentMethodGateService.payTypesOf(option.getMethodCode()));
  1348. }
  1349. return payTypes;
  1350. }
  1351. /** 校验骑手坐标:经纬度必须成对出现且在合法范围。 */
  1352. private void validateRiderCoordinates(BigDecimal longitude, BigDecimal latitude) {
  1353. if ((longitude == null) != (latitude == null)
  1354. || longitude != null && (longitude.compareTo(BigDecimal.valueOf(-180)) < 0
  1355. || longitude.compareTo(BigDecimal.valueOf(180)) > 0
  1356. || latitude.compareTo(BigDecimal.valueOf(-90)) < 0
  1357. || latitude.compareTo(BigDecimal.valueOf(90)) > 0)) {
  1358. throw fail("flash.delivery.coordinates.invalid");
  1359. }
  1360. }
  1361. /** 读取字典 sys_qs_newtask_distance 的新任务距离上限(公里);缺失或非法时不限距离。 */
  1362. private Integer newTaskDistanceLimit() {
  1363. try {
  1364. List<SysDictData> values = DictUtils.getDictCache("sys_qs_newtask_distance");
  1365. if (values != null && !values.isEmpty()) {
  1366. int limit = Integer.parseInt(values.get(0).getDictValue());
  1367. return limit > 0 ? limit : null;
  1368. }
  1369. } catch (Exception ignored) {
  1370. // Keep the established rider-list behaviour: absent or malformed configuration disables distance filtering.
  1371. }
  1372. return null;
  1373. }
  1374. /** Haversine 球面直线距离(米),用于骑手列表展示距取件点的距离。 */
  1375. private Integer distanceMeters(BigDecimal longitude, BigDecimal latitude,
  1376. BigDecimal targetLongitude, BigDecimal targetLatitude) {
  1377. if (longitude == null || targetLongitude == null || targetLatitude == null) return null;
  1378. double latitudeRadians = Math.toRadians(latitude.doubleValue());
  1379. double targetLatitudeRadians = Math.toRadians(targetLatitude.doubleValue());
  1380. double latitudeDelta = targetLatitudeRadians - latitudeRadians;
  1381. double longitudeDelta = Math.toRadians(targetLongitude.doubleValue() - longitude.doubleValue());
  1382. double value = Math.sin(latitudeDelta / 2) * Math.sin(latitudeDelta / 2)
  1383. + Math.cos(latitudeRadians) * Math.cos(targetLatitudeRadians)
  1384. * Math.sin(longitudeDelta / 2) * Math.sin(longitudeDelta / 2);
  1385. return (int) Math.round(6_371_000D * 2D * Math.atan2(Math.sqrt(value), Math.sqrt(1D - value)));
  1386. }
  1387. /** 从 selectObjs 聚合结果中取第一个 Long 值。 */
  1388. private Long firstLong(List<Object> values) {
  1389. if (values == null || values.isEmpty() || values.get(0) == null) return null;
  1390. Object value = values.get(0);
  1391. if (value instanceof Number number) return number.longValue();
  1392. return Long.valueOf(String.valueOf(value));
  1393. }
  1394. /** 取件方式归一化:缺省按 NOW 处理,非法值报错。 */
  1395. private String normalizeDeliveryMode(String value) {
  1396. String mode = hasText(value) ? value.trim() : "NOW";
  1397. if (!DELIVERY_MODES.contains(mode)) throw fail("flash.delivery.mode.invalid");
  1398. return mode;
  1399. }
  1400. /** 校验预约窗:NOW 不得携带时间;SCHEDULED 必须是未来 3 天内恰好 30 分钟的窗。 */
  1401. private void validateSchedule(String mode, Date start, Date end, Date now) {
  1402. if ("NOW".equals(mode)) {
  1403. if (start != null || end != null) throw fail("flash.delivery.schedule.invalid");
  1404. return;
  1405. }
  1406. if (start == null || end == null || start.before(now)
  1407. || end.getTime() - start.getTime() != SCHEDULE_SLOT_MILLIS
  1408. || start.getTime() - now.getTime() > MAX_SCHEDULE_DELAY_MILLIS) {
  1409. throw fail("flash.delivery.schedule.invalid");
  1410. }
  1411. }
  1412. /** 判断订单当前是否开放抢单:立即单随时可抢,预约单仅在开始(含)至结束(不含)之间可抢。 */
  1413. private boolean isAvailableAt(FlashDeliveryOrder order, Date now) {
  1414. return "NOW".equals(order.getDeliveryMode())
  1415. || "SCHEDULED".equals(order.getDeliveryMode())
  1416. && order.getScheduledPickupStartAt() != null
  1417. && order.getScheduledPickupEndAt() != null
  1418. && !order.getScheduledPickupStartAt().after(now)
  1419. && order.getScheduledPickupEndAt().after(now);
  1420. }
  1421. private void optionalEq(QueryWrapper<FlashDeliveryOrder> query, String column, String value) {
  1422. if (hasText(value)) query.eq(column, value.trim());
  1423. }
  1424. private boolean hasText(String value) { return StringUtils.isNotEmpty(value) && !value.trim().isEmpty(); }
  1425. private String trimToNull(String value) { return hasText(value) ? value.trim() : null; }
  1426. private boolean tooLong(String value, int max) { return value != null && value.trim().length() > max; }
  1427. private boolean sameAddressText(FlashDeliveryAddressRequest left, FlashDeliveryAddressRequest right) {
  1428. return normalized(left.getAddress()).equals(normalized(right.getAddress()))
  1429. && normalized(left.getAddressDetail()).equals(normalized(right.getAddressDetail()));
  1430. }
  1431. private String normalized(String value) { return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); }
  1432. private ServiceException fail(String key) { return new ServiceException(MessageUtils.message(key)); }
  1433. /** 将旧单点输入适配为完整取货站;多点请求必须逐站申报物品。 */
  1434. private List<FlashDeliveryPickupRequest> normalizePickups(FlashDeliveryQuoteRequest request) {
  1435. if (request.getPickups() != null) {
  1436. if (request.getPickup() != null || request.getPackageType() != null || request.getQuantity() != null
  1437. || request.getWeightRange() != null || request.getSpecification() != null) {
  1438. throw fail("flash.delivery.pickup.input.conflict");
  1439. }
  1440. return request.getPickups();
  1441. }
  1442. FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
  1443. if (request.getPickup() == null) throw fail("flash.delivery.address.invalid");
  1444. BeanUtils.copyProperties(request.getPickup(), pickup);
  1445. pickup.setPackageType(request.getPackageType());
  1446. pickup.setQuantity(request.getQuantity());
  1447. pickup.setWeightRange(request.getWeightRange());
  1448. pickup.setSpecification(request.getSpecification());
  1449. return List.of(pickup);
  1450. }
  1451. /** 编辑提交完整站点;旧单点地址编辑保留该站原有物品信息。 */
  1452. private List<FlashDeliveryPickupRequest> editablePickups(FlashDeliveryOrder order,
  1453. FlashDeliveryAddressChangeRequest request) {
  1454. List<FlashDeliveryOrderStop> existing = requireStops(order);
  1455. if (request.getPickups() != null) {
  1456. if (request.getPickup() != null) throw fail("flash.delivery.pickup.input.conflict");
  1457. return request.getPickups();
  1458. }
  1459. List<FlashDeliveryOrderStop> pickups = pickupStops(existing);
  1460. if (pickups.size() != 1) throw fail("flash.delivery.pickups.required");
  1461. if (request.getPickup() == null) throw fail("flash.delivery.address.invalid");
  1462. FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
  1463. BeanUtils.copyProperties(pickups.get(0), pickup);
  1464. BeanUtils.copyProperties(request.getPickup(), pickup);
  1465. return List.of(pickup);
  1466. }
  1467. /** 地图调用前先校验有效点数、开关及每站地址和物品。 */
  1468. private void validatePickups(List<FlashDeliveryPickupRequest> pickups, FlashDeliveryAddressRequest delivery) {
  1469. if (pickups == null || pickups.isEmpty() || pickups.size() > pickupStopLimit()) {
  1470. throw fail("flash.delivery.pickup.limit.exceeded");
  1471. }
  1472. if (pickups.size() > 1 && !dictionaryEnabled("flash_show_multi_pickup")) {
  1473. throw fail("flash.delivery.multi.pickup.disabled");
  1474. }
  1475. validateAddress(delivery);
  1476. for (FlashDeliveryPickupRequest pickup : pickups) {
  1477. validateAddress(pickup);
  1478. if (!PACKAGE_TYPES.contains(Objects.toString(pickup.getPackageType(), ""))) {
  1479. throw fail("flash.delivery.package.type.invalid");
  1480. }
  1481. // 重量与规格仅供骑手判断物品情况,用户可不填写;填写重量时仍须使用受支持的区间。
  1482. if (pickup.getQuantity() == null || pickup.getQuantity() <= 0
  1483. || hasText(pickup.getWeightRange()) && !WEIGHT_RANGES.contains(pickup.getWeightRange())
  1484. || tooLong(pickup.getSpecification(), 255)) throw fail("flash.delivery.item.invalid");
  1485. if (sameAddressText(pickup, delivery)
  1486. || pickup.getLatitude().compareTo(delivery.getLatitude()) == 0
  1487. && pickup.getLongitude().compareTo(delivery.getLongitude()) == 0) {
  1488. throw fail("flash.delivery.address.same");
  1489. }
  1490. }
  1491. }
  1492. private RouteDistance pickupRoute(List<FlashDeliveryPickupRequest> pickups, FlashDeliveryAddressRequest delivery,
  1493. Integer vehicleType) {
  1494. List<GeoPoint> points = new ArrayList<>();
  1495. for (FlashDeliveryPickupRequest pickup : pickups) points.add(new GeoPoint(pickup.getLatitude(), pickup.getLongitude()));
  1496. points.add(new GeoPoint(delivery.getLatitude(), delivery.getLongitude()));
  1497. RouteDistance route = points.size() == 2
  1498. ? routeService.calculate(points.get(0), points.get(1), vehicleType)
  1499. : routeService.calculate(points, vehicleType);
  1500. if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
  1501. return route;
  1502. }
  1503. private Integer requireVehicleType(Integer vehicleType) {
  1504. if (!FlashDeliveryVehicleType.isSupported(vehicleType)) {
  1505. throw fail("flash.delivery.vehicle.type.invalid");
  1506. }
  1507. return vehicleType;
  1508. }
  1509. /** DDL 执行前存在的旧订单按用户确认的机车规则兼容。 */
  1510. private Integer vehicleTypeOf(FlashDeliveryOrder order) {
  1511. return FlashDeliveryVehicleType.isSupported(order.getVehicleType())
  1512. ? order.getVehicleType() : FlashDeliveryVehicleType.MOTORCYCLE;
  1513. }
  1514. /** 骑手资料复用既有 vehicleType;历史空值和非法值安全回落为机车。 */
  1515. private Integer riderVehicleType(InfoUser rider) {
  1516. try {
  1517. Integer value = Integer.valueOf(rider.getVehicleType());
  1518. return FlashDeliveryVehicleType.isSupported(value) ? value : FlashDeliveryVehicleType.MOTORCYCLE;
  1519. } catch (Exception ignored) {
  1520. return FlashDeliveryVehicleType.MOTORCYCLE;
  1521. }
  1522. }
  1523. private boolean isRiderVehicleCompatible(InfoUser rider, FlashDeliveryOrder order) {
  1524. return riderVehicleType(rider).equals(vehicleTypeOf(order));
  1525. }
  1526. /** 点数配置缺失、非整数或越界时,有效上限回退为1。 */
  1527. private int pickupStopLimit() {
  1528. String value = dictionaryValue("flash_pickup_stop_limit");
  1529. if (value == null || !value.matches("[0-9]+")) return 1;
  1530. try {
  1531. int limit = Integer.parseInt(value);
  1532. return limit >= 1 && limit <= 26 ? limit : 1;
  1533. } catch (NumberFormatException ignored) { return 1; }
  1534. }
  1535. private boolean dictionaryEnabled(String key) { return "1".equals(dictionaryValue(key)); }
  1536. private String dictionaryValue(String key) {
  1537. try {
  1538. List<SysDictData> values = DictUtils.getDictCache(key);
  1539. return values == null || values.isEmpty() ? null : values.get(0).getDictValue();
  1540. } catch (Exception ignored) { return null; }
  1541. }
  1542. /** 订单旧字段仅维护首站兼容摘要,不代表全部站点物品。 */
  1543. private void copyPickupSummary(FlashDeliveryOrder order, List<FlashDeliveryPickupRequest> pickups) {
  1544. FlashDeliveryPickupRequest first = pickups.get(0);
  1545. copyAddress(first, order, true);
  1546. order.setPackageType(first.getPackageType()); order.setQuantity(first.getQuantity());
  1547. order.setWeightRange(first.getWeightRange()); order.setSpecification(trimToNull(first.getSpecification()));
  1548. order.setPickupStopCount(pickups.size());
  1549. }
  1550. /** 调用方已创建订单或持有订单行锁,全部站点与订单在同一事务保存。 */
  1551. private void saveStops(FlashDeliveryOrder order, List<FlashDeliveryPickupRequest> pickups,
  1552. FlashDeliveryAddressRequest delivery, Date now) {
  1553. for (int i = 0; i < pickups.size(); i++) insertStop(order.getId(), pickups.get(i), "PICKUP", i, now);
  1554. insertStop(order.getId(), delivery, "DELIVERY", pickups.size(), now);
  1555. }
  1556. private void insertStop(Long orderId, FlashDeliveryAddressRequest address, String type, int index, Date now) {
  1557. FlashDeliveryOrderStop stop = new FlashDeliveryOrderStop();
  1558. BeanUtils.copyProperties(address, stop);
  1559. stop.setName(address.getName().trim()); stop.setPhone(address.getPhone().trim());
  1560. stop.setAddress(address.getAddress().trim()); stop.setAddressDetail(trimToNull(address.getAddressDetail()));
  1561. stop.setCity(trimToNull(address.getCity())); stop.setArea(trimToNull(address.getArea()));
  1562. stop.setHandoffMethod(trimToNull(address.getHandoffMethod())); stop.setSpecification(trimToNull(stop.getSpecification()));
  1563. stop.setOrderId(orderId); stop.setStopType(type); stop.setStopOrder(index);
  1564. stop.setStatus("PENDING"); stop.setCreateTime(now);
  1565. if (stopMapper.insert(stop) != 1) throw fail("flash.delivery.state.changed");
  1566. }
  1567. /** 事务内使用当前读,避免骑手资料查询建立的旧快照遮蔽锁等待期间的履约变化;GET 不加锁。 */
  1568. private <T> QueryWrapper<T> currentRead(QueryWrapper<T> query) {
  1569. if (TransactionSynchronizationManager.isActualTransactionActive()) {
  1570. query.last("FOR UPDATE");
  1571. }
  1572. return query;
  1573. }
  1574. private List<FlashDeliveryOrderStop> stops(Long orderId) {
  1575. return stopMapper.selectList(currentRead(new QueryWrapper<FlashDeliveryOrderStop>()
  1576. .eq("order_id", orderId).orderByAsc("stop_order", "id")));
  1577. }
  1578. /** 履约必须有真实站点记录,不从旧测试订单的摘要字段合成站点。 */
  1579. private List<FlashDeliveryOrderStop> requireStops(FlashDeliveryOrder order) {
  1580. List<FlashDeliveryOrderStop> rows = stops(order.getId());
  1581. if (rows == null || pickupStops(rows).isEmpty()
  1582. || !Objects.equals(order.getPickupStopCount(), pickupStops(rows).size())
  1583. || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != 1) {
  1584. throw fail("flash.delivery.stops.invalid");
  1585. }
  1586. return rows;
  1587. }
  1588. private List<FlashDeliveryOrderStop> pickupStops(List<FlashDeliveryOrderStop> rows) {
  1589. return rows.stream().filter(s -> "PICKUP".equals(s.getStopType())).toList();
  1590. }
  1591. private boolean anyPicked(List<FlashDeliveryOrderStop> rows) {
  1592. return rows.stream().anyMatch(s -> "PICKUP".equals(s.getStopType()) && "PICKED_UP".equals(s.getStatus()));
  1593. }
  1594. private boolean allPicked(List<FlashDeliveryOrderStop> rows) {
  1595. List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
  1596. return !pickups.isEmpty() && pickups.stream().allMatch(s -> "PICKED_UP".equals(s.getStatus()));
  1597. }
  1598. /** 选择的目标在刷新后保留;该站取货后回到原顺序最早的待取站点。 */
  1599. private Long targetStopId(FlashDeliveryOrder order, List<FlashDeliveryOrderStop> rows) {
  1600. if (CANCELLED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()) || DELIVERED.equals(order.getStatus())) return null;
  1601. if (order.getCurrentTargetStopId() != null && rows.stream().anyMatch(s ->
  1602. Objects.equals(s.getId(), order.getCurrentTargetStopId()) && "PICKUP".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))) {
  1603. return order.getCurrentTargetStopId();
  1604. }
  1605. return rows.stream().filter(s -> "PICKUP".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))
  1606. .map(FlashDeliveryOrderStop::getId).findFirst().orElseGet(() -> rows.stream()
  1607. .filter(s -> "DELIVERY".equals(s.getStopType())).map(FlashDeliveryOrderStop::getId).findFirst().orElse(null));
  1608. }
  1609. /** 事务内先锁订单并读取最新状态,再查询站点,统一取货、调整与取消的并发边界。 */
  1610. private FlashDeliveryOrder lockOrder(Long orderId) {
  1611. FlashDeliveryOrder order = orderId == null ? null : orderMapper.selectByIdForUpdate(orderId);
  1612. if (order == null) throw fail("flash.delivery.order.not.found");
  1613. return order;
  1614. }
  1615. private FlashDeliveryOrderStop requireStop(List<FlashDeliveryOrderStop> rows, Long stopId) {
  1616. return rows.stream().filter(s -> Objects.equals(s.getId(), stopId)).findFirst()
  1617. .orElseThrow(() -> fail("flash.delivery.stop.invalid"));
  1618. }
  1619. /** 选择前往目标只记录履约调整,不修改取件进度、金额或用户站点顺序。 */
  1620. @Transactional
  1621. public FlashDeliveryOrderDetailView selectTarget(Long riderId, Long orderId, Long stopId,
  1622. FlashDeliveryTargetRequest request) {
  1623. requireRider(riderId);
  1624. FlashDeliveryOrder order = lockOrder(orderId);
  1625. if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
  1626. if (request == null || !hasText(request.getClientRequestId()) || tooLong(request.getClientRequestId(), 64)) {
  1627. throw fail("flash.delivery.client.request.id.invalid");
  1628. }
  1629. String requestId = request.getClientRequestId().trim();
  1630. FlashDeliveryTargetAdjustment previous = adjustmentMapper.selectOne(new QueryWrapper<FlashDeliveryTargetAdjustment>()
  1631. .eq("order_id", orderId).eq("rider_id", riderId).eq("client_request_id", requestId).last("FOR UPDATE"));
  1632. if (previous != null) {
  1633. if (!Objects.equals(previous.getToStopId(), stopId)
  1634. || !Objects.equals(previous.getOrderVersion(), request.getOrderVersion())
  1635. || !Objects.equals(previous.getReason(), trimToNull(request.getReason()))
  1636. || !Objects.equals(previous.getConsentDeclared(), Boolean.TRUE.equals(request.getConsentDeclared()))) {
  1637. throw fail("flash.delivery.target.request.conflict");
  1638. }
  1639. // 重试返回当前进度,不把已过期的历史目标重新写回订单。
  1640. return participantDetail(order, false, false);
  1641. }
  1642. if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
  1643. if (request.getOrderVersion() == null || !Objects.equals(request.getOrderVersion(), order.getVersion())) {
  1644. throw fail("flash.delivery.state.changed");
  1645. }
  1646. List<FlashDeliveryOrderStop> rows = requireStops(order);
  1647. FlashDeliveryOrderStop target = requireStop(rows, stopId);
  1648. if (!"PICKUP".equals(target.getStopType()) || !"PENDING".equals(target.getStatus())) throw fail("flash.delivery.stop.invalid");
  1649. Long before = targetStopId(order, rows);
  1650. if (!Objects.equals(before, stopId) && (!hasText(request.getReason()) || tooLong(request.getReason(), 500)
  1651. || !Boolean.TRUE.equals(request.getConsentDeclared()))) throw fail("flash.delivery.target.consent.required");
  1652. if (tooLong(request.getReason(), 500)) throw fail("flash.delivery.target.consent.required");
  1653. Date now = new Date();
  1654. FlashDeliveryTargetAdjustment adjustment = new FlashDeliveryTargetAdjustment();
  1655. adjustment.setOrderId(orderId); adjustment.setFromStopId(before); adjustment.setToStopId(stopId);
  1656. adjustment.setRiderId(riderId); adjustment.setClientRequestId(requestId); adjustment.setOrderVersion(request.getOrderVersion());
  1657. adjustment.setReason(trimToNull(request.getReason())); adjustment.setConsentDeclared(Boolean.TRUE.equals(request.getConsentDeclared())); adjustment.setCreateTime(now);
  1658. if (adjustmentMapper.insert(adjustment) != 1) throw fail("flash.delivery.state.changed");
  1659. order.setCurrentTargetStopId(stopId); order.setUpdateTime(now);
  1660. if (orderMapper.updateFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
  1661. order.setVersion(order.getVersion() + 1);
  1662. return participantDetail(order, false, false);
  1663. }
  1664. /** 两种取货接口共用同一加锁流程,路径与请求体中的站点编号必须一致。 */
  1665. @Transactional
  1666. public void pickupAtStop(Long riderId, Long orderId, Long stopId, FlashDeliveryProofRequest request) {
  1667. if (request == null) throw fail("flash.delivery.request.required");
  1668. if (request.getStopId() != null && !Objects.equals(request.getStopId(), stopId)) throw fail("flash.delivery.stop.invalid");
  1669. pickupStation(riderId, orderId, stopId, request);
  1670. }
  1671. private void pickupStation(Long riderId, Long orderId, Long requestedStopId, FlashDeliveryProofRequest request) {
  1672. requireRider(riderId);
  1673. FlashDeliveryOrder order = lockOrder(orderId);
  1674. if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
  1675. List<FlashDeliveryOrderStop> rows = requireStops(order);
  1676. List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
  1677. Long stopId = requestedStopId;
  1678. if (stopId == null) {
  1679. if (pickups.size() != 1) throw fail("flash.delivery.stop.id.required");
  1680. stopId = pickups.get(0).getId();
  1681. }
  1682. FlashDeliveryOrderStop stop = requireStop(rows, stopId);
  1683. if (!"PICKUP".equals(stop.getStopType())) throw fail("flash.delivery.stop.invalid");
  1684. // 已确认站点的重试直接成功,不因当前目标或主状态已变化而重复写凭证。
  1685. if ("PICKED_UP".equals(stop.getStatus())) return;
  1686. if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
  1687. if (!Objects.equals(stopId, targetStopId(order, rows))) throw fail("flash.delivery.target.mismatch");
  1688. List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
  1689. Date now = new Date();
  1690. stop.setStatus("PICKED_UP"); stop.setPickedUpAt(now);
  1691. if (stopMapper.updateById(stop) != 1) throw fail("flash.delivery.state.changed");
  1692. saveStationImages(orderId, stopId, "PICKUP", "RIDER", riderId, urls, now);
  1693. order.setPickedUpStopCount((int) pickups.stream().filter(s -> "PICKED_UP".equals(s.getStatus())).count());
  1694. order.setCurrentTargetStopId(null); order.setUpdateTime(now);
  1695. if (allPicked(rows)) { order.setStatus(PICKED_UP); order.setPickedUpAt(now); }
  1696. if (orderMapper.updateFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
  1697. writeLog(orderId, ACCEPTED, order.getStatus(), "RIDER", riderId, "PICKUP_STOP:" + stopId, now);
  1698. // 全部取货站完成、订单推进为已取件时通知寄件用户。
  1699. if (PICKED_UP.equals(order.getStatus())) notificationService.notifyPickedUp(order);
  1700. }
  1701. private void saveStationImages(Long orderId, Long stopId, String proofType, String operatorType,
  1702. Long operatorId, List<String> urls, Date now) {
  1703. for (int index = 0; index < urls.size(); index++) {
  1704. FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
  1705. image.setOrderId(orderId); image.setStopId(stopId); image.setProofType(proofType);
  1706. image.setOperatorType(operatorType); image.setOperatorId(operatorId); image.setImageUrl(urls.get(index));
  1707. image.setSortOrder(index); image.setCreateTime(now);
  1708. if (imageMapper.insert(image) != 1) throw fail("flash.delivery.state.changed");
  1709. }
  1710. }
  1711. private List<FlashDeliveryStopView> stopViews(List<FlashDeliveryOrderStop> rows, boolean sensitive,
  1712. List<FlashDeliveryOrderImage> images) {
  1713. return rows.stream().map(stop -> {
  1714. FlashDeliveryStopView view = new FlashDeliveryStopView();
  1715. BeanUtils.copyProperties(stop, view);
  1716. if (sensitive) view.setImageUrls(images.stream().filter(i -> Objects.equals(i.getStopId(), stop.getId())
  1717. && Objects.equals(i.getProofType(), stop.getStopType()))
  1718. .map(FlashDeliveryOrderImage::getImageUrl).toList());
  1719. else { view.setName(null); view.setPhone(null); view.setLongitude(null); view.setLatitude(null); }
  1720. return view;
  1721. }).toList();
  1722. }
  1723. private boolean canUserCancel(FlashDeliveryOrder order) {
  1724. return FlashDeliveryStateMachine.canUserCancel(order.getStatus())
  1725. && Integer.valueOf(0).equals(order.getPickedUpStopCount());
  1726. }
  1727. /** 报价中间结果:命中的运价配置、路线距离和费用明细。 */
  1728. private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
  1729. FlashDeliveryPriceBreakdown breakdown, List<FlashDeliveryPickupRequest> pickups) { }
  1730. }