FlashDeliveryApplicationService.java 99 KB

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