| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405 |
- package com.ruoyi.app.flashdelivery.service;
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
- import com.baomidou.mybatisplus.core.metadata.IPage;
- import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
- import com.ruoyi.app.flashdelivery.dto.*;
- import com.ruoyi.app.flashdelivery.exception.FlashDeliveryQuoteChangedException;
- import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
- import com.ruoyi.app.flashdelivery.route.GeoPoint;
- import com.ruoyi.app.flashdelivery.route.RouteDistance;
- import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
- import com.ruoyi.app.order.RiderDeliveryLockService;
- import com.ruoyi.common.core.domain.entity.SysDictData;
- import com.ruoyi.common.exception.ServiceException;
- import com.ruoyi.common.utils.DictUtils;
- import com.ruoyi.common.utils.MessageUtils;
- import com.ruoyi.common.utils.StringUtils;
- import com.ruoyi.system.domain.InfoUser;
- import com.ruoyi.system.domain.UserBilling;
- import com.ruoyi.system.domain.flash.*;
- import com.ruoyi.system.mapper.InfoUserMapper;
- import com.ruoyi.system.mapper.UserBillingMapper;
- import com.ruoyi.system.mapper.flash.*;
- import com.ruoyi.system.service.PaymentMethodGateService;
- import org.springframework.beans.BeanUtils;
- import org.springframework.dao.DataIntegrityViolationException;
- import org.springframework.transaction.support.TransactionSynchronizationManager;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.net.URI;
- import java.math.BigDecimal;
- import java.math.RoundingMode;
- import java.security.SecureRandom;
- import java.time.Instant;
- import java.time.ZoneId;
- import java.time.format.DateTimeFormatter;
- import java.util.*;
- import java.util.concurrent.atomic.AtomicLong;
- import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
- /**
- * 闪送业务编排服务。
- *
- * <p>负责报价、订单创建、抢单、履约状态流转、平台介入和审计日志。
- * 订单身份只接受 Controller 从 token 解析后的用户 ID,客户端传入的金额、距离或操作人均不可信。</p>
- */
- @Service
- public class FlashDeliveryApplicationService {
- private static final List<String> SERVICE_TYPES = List.of("HELP_SEND", "HELP_PICKUP");
- private static final Set<String> DELIVERY_TYPES = Set.of("NORMAL", "URGENT");
- private static final Set<String> PACKAGE_TYPES = Set.of("DOCUMENT", "GIFT", "CLOTHING", "BEAUTY",
- "DAILY_NECESSITIES", "FOOD_INGREDIENTS", "ELECTRONICS", "SMALL_APPLIANCE", "OTHER");
- private static final Set<String> WEIGHT_RANGES = Set.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
- "OVER_10_TO_15_KG", "OVER_15_TO_20_KG");
- private static final Set<String> DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
- /** 支付方式取值与主订单系统同域:4=现金、6=线下转账(027 线下转账支付)。 */
- private static final String PAY_TYPE_CASH = "4";
- /** 线下转账 payType=6:送达后向寄件人展示骑手启用卡,线下转账结清。 */
- private static final String PAY_TYPE_OFFLINE_TRANSFER = "6";
- private static final String PAY_TYPE_TRANSFER = "6";
- /** 自动取消原因使用稳定代码,供不同端按各自语言展示而不是保存单一语言文本。 */
- private static final String SCHEDULE_EXPIRED_REASON = "SCHEDULE_EXPIRED";
- private static final DateTimeFormatter PRICING_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
- private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
- private static final int MAX_DISTANCE_METERS = 40_000;
- private static final BigDecimal MAX_PRICING_DISTANCE = new BigDecimal("999999.99");
- private static final long SCHEDULE_SLOT_MILLIS = 30L * 60L * 1000L;
- private static final long MAX_SCHEDULE_DELAY_MILLIS = 3L * 24L * 60L * 60L * 1000L;
- private static final SecureRandom PIN_RANDOM = new SecureRandom();
- /** 订单号同毫秒递增序列(01-99 循环):同一毫秒内的并发由原子递增区分,避免同号。 */
- private static final AtomicLong ORDER_NO_SEQ = new AtomicLong();
- private final FlashDeliveryOrderMapper orderMapper;
- private final FlashDeliveryOrderStopMapper stopMapper;
- private final FlashDeliveryTargetAdjustmentMapper adjustmentMapper;
- private final FlashDeliveryPricingMapper pricingMapper;
- private final FlashDeliveryOrderImageMapper imageMapper;
- private final FlashDeliveryOrderLogMapper logMapper;
- private final InfoUserMapper userMapper;
- private final com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper;
- private final FlashDeliveryRouteService routeService;
- private final FlashDeliveryPricingCalculator pricingCalculator;
- private final RiderDeliveryLockService riderDeliveryLockService;
- private final RiderDeliveryAcceptanceService riderDeliveryAcceptanceService;
- /** 031 支付闸门:闪送下单、报价展示和骑手待抢列表使用同一可用性规则。 */
- private final PaymentMethodGateService paymentMethodGateService;
- private final FlashDeliveryNotificationService notificationService;
- private final UserBillingMapper userBillingMapper;
- @org.springframework.beans.factory.annotation.Autowired
- private com.ruoyi.app.flashdelivery.payment.FlashLinePayFactService flashLinePayFactService;
- @org.springframework.beans.factory.annotation.Autowired
- private com.ruoyi.app.flashdelivery.payment.FlashLinePayService flashLinePayService;
- public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
- FlashDeliveryPricingMapper pricingMapper,
- FlashDeliveryOrderImageMapper imageMapper,
- FlashDeliveryOrderLogMapper logMapper,
- InfoUserMapper userMapper,
- com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper,
- FlashDeliveryRouteService routeService,
- FlashDeliveryPricingCalculator pricingCalculator,
- RiderDeliveryLockService riderDeliveryLockService,
- RiderDeliveryAcceptanceService riderDeliveryAcceptanceService,
- FlashDeliveryOrderStopMapper stopMapper,
- FlashDeliveryTargetAdjustmentMapper adjustmentMapper,
- PaymentMethodGateService paymentMethodGateService,
- FlashDeliveryNotificationService notificationService,
- UserBillingMapper userBillingMapper) {
- this.orderMapper = orderMapper;
- this.stopMapper = stopMapper;
- this.adjustmentMapper = adjustmentMapper;
- this.pricingMapper = pricingMapper;
- this.imageMapper = imageMapper;
- this.logMapper = logMapper;
- this.userMapper = userMapper;
- this.bankCardMapper = bankCardMapper;
- this.routeService = routeService;
- this.pricingCalculator = pricingCalculator;
- this.riderDeliveryLockService = riderDeliveryLockService;
- this.riderDeliveryAcceptanceService = riderDeliveryAcceptanceService;
- this.paymentMethodGateService = paymentMethodGateService;
- this.notificationService = notificationService;
- this.userBillingMapper = userBillingMapper;
- }
- /** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
- public FlashDeliveryHomeView home() {
- FlashDeliveryHomeView view = new FlashDeliveryHomeView();
- int pickupLimit = pickupStopLimit();
- int deliveryLimit = deliveryStopLimit();
- view.setPickupStopLimit(pickupLimit);
- view.setShowMultiPickup(pickupLimit > 1);
- view.setDeliveryStopLimit(deliveryLimit);
- view.setShowMultiDropoff(deliveryLimit > 1);
- view.setShowUrgentOption(true);
- view.setServiceTypes(SERVICE_TYPES);
- view.setDeliveryTypes(List.of("NORMAL", "URGENT"));
- FlashDeliveryPricing pricing = findPricingAtOrNull(pricingTargetTime(new Date()), FlashDeliveryVehicleType.MOTORCYCLE);
- view.setServices(pricing == null ? List.of() : SERVICE_TYPES.stream()
- .map(serviceType -> serviceView(pricing, serviceType)).toList());
- view.setPayMethods(paymentMethodGateService.listAvailable(
- PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
- return view;
- }
- /** 实时报价:校验请求后按服务端路线距离和命中时段的运价计算费用(立即单取当前时段、预约单取预约开始时刻),不写库。 */
- public FlashDeliveryQuoteView quote(FlashDeliveryQuoteRequest request) {
- // 报价只使用服务端路线距离和当前启用的计价配置,避免客户端篡改金额。
- QuoteContext context = calculateQuote(request);
- return quoteView(request, context);
- }
- /** 将报价上下文组装为对外报价视图,含运价快照字段与费用明细。 */
- private FlashDeliveryQuoteView quoteView(FlashDeliveryQuoteRequest request, QuoteContext context) {
- FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
- view.setServiceType(request.getServiceType());
- view.setDeliveryType(request.getDeliveryType());
- view.setVehicleType(request.getVehicleType());
- view.setDeliveryMode(normalizeDeliveryMode(request.getDeliveryMode()));
- view.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
- view.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
- view.setPricingId(context.pricing().getId());
- view.setStartTime(context.pricing().getStartTime());
- view.setEndTime(context.pricing().getEndTime());
- view.setDistanceMeters(context.route().distanceMeters());
- view.setDistanceSource(context.route().source());
- view.setEstimatedDurationSeconds(context.route().durationSeconds());
- view.setStartingDistance(context.pricing().getStartingDistance());
- view.setStartingFare(context.pricing().getStartingFare());
- view.setDistance(context.pricing().getDistance());
- view.setFreight(context.pricing().getFreight());
- view.setUrgentRate(context.pricing().getUrgentRate());
- view.setMinimumUrgentFee(context.pricing().getMinimumUrgentFee());
- applyBreakdown(view, context.breakdown());
- view.setCurrency("TWD");
- view.setPricingVersion(context.pricing().getConfigVersion());
- view.setPayMethods(paymentMethodGateService.listAvailable(
- PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
- return view;
- }
- /**
- * 幂等创建订单:clientRequestId 与 userId 构成唯一键,重复提交直接返回已有订单。
- * 创建前按最新条件重新报价,客户端回传报价不一致时抛出 FlashDeliveryQuoteChangedException;
- * 成功后保存计价快照、寄件图片和初始状态日志。
- */
- @Transactional
- public FlashDeliveryOrderDetailView create(Long userId, FlashDeliveryCreateRequest request) {
- requireUserId(userId);
- if (request == null || !hasText(request.getClientRequestId())
- || request.getClientRequestId().trim().length() > 64) {
- throw fail("flash.delivery.client.request.id.invalid");
- }
- String requestId = request.getClientRequestId().trim();
- // 用户 ID + clientRequestId 构成幂等键;重复请求直接返回第一次创建的订单。
- FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
- if (existing != null) return participantDetail(lockOrder(existing.getId()), true, false);
- if (request.getUserNote() != null && request.getUserNote().length() > 500) {
- throw fail("flash.delivery.note.too.long");
- }
- QuoteContext quote = calculateQuote(request);
- FlashDeliveryQuoteView latestQuote = quoteView(request, quote);
- if (!matchesQuotedPrice(request, latestQuote)) {
- throw new FlashDeliveryQuoteChangedException(latestQuote);
- }
- Date now = new Date();
- String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
- validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
- boolean pinRequired = request.getPinRequired() == null || request.getPinRequired();
- String payType = normalizePayType(request.getPayType());
- List<String> senderImages = validateProofUrls(request.getSenderImageUrls(), false);
- FlashDeliveryOrder order = new FlashDeliveryOrder();
- order.setOrderNo(generateOrderNo());
- order.setClientRequestId(requestId);
- order.setUserId(userId);
- order.setServiceType(request.getServiceType());
- order.setDeliveryType(request.getDeliveryType());
- order.setVehicleType(request.getVehicleType());
- order.setStatus(WAITING_ACCEPTANCE);
- copyPickupSummary(order, quote.pickups());
- order.setPickedUpStopCount(0);
- order.setIsDisplay("NOW".equals(deliveryMode));
- order.setDeliveryMode(deliveryMode);
- order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
- order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
- order.setPinRequired(pinRequired);
- order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
- copyAddress(request.getDelivery(), order, false);
- order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
- order.setDistanceMeters(quote.route().distanceMeters());
- order.setDistanceSource(quote.route().source());
- order.setEstimatedDurationSeconds(quote.route().durationSeconds());
- order.setAmount(quote.breakdown().getAmount());
- order.setCurrency("TWD");
- order.setPayType(payType);
- order.setPricingId(quote.pricing().getId());
- order.setPricingVersion(quote.pricing().getConfigVersion());
- order.setPricingStartTime(quote.pricing().getStartTime());
- order.setPricingEndTime(quote.pricing().getEndTime());
- order.setStartingDistance(quote.pricing().getStartingDistance());
- order.setStartingFare(quote.pricing().getStartingFare());
- order.setDistance(quote.pricing().getDistance());
- order.setFreight(quote.pricing().getFreight());
- // 保存计价快照,后台调价只影响新订单,不追溯修改历史订单金额。
- order.setDistanceFee(quote.breakdown().getDistanceFee());
- order.setBaseDeliveryFee(quote.breakdown().getBaseDeliveryFee());
- order.setUrgentRate(quote.pricing().getUrgentRate());
- order.setMinimumUrgentFee(quote.pricing().getMinimumUrgentFee());
- order.setUrgentFee(quote.breakdown().getUrgentFee());
- order.setTipAmount(quote.breakdown().getTipAmount());
- order.setUserNote(trimToNull(request.getUserNote()));
- order.setVersion(0);
- order.setCreateTime(now);
- order.setUpdateTime(now);
- try {
- orderMapper.insert(order);
- } catch (DataIntegrityViolationException duplicate) {
- // order_no 撞号兜底(时钟回拨/多实例等理论情形):唯一索引 uk_flash_order_no 拒绝后换号重插一次。
- if (isOrderNoConflict(duplicate)) {
- order.setId(null);
- order.setOrderNo(generateOrderNo());
- orderMapper.insert(order);
- } else {
- // 并发重复请求可能同时通过前置查询,最终由数据库唯一键完成幂等收口。
- FlashDeliveryOrder concurrent = orderMapper.selectOne(new QueryWrapper<FlashDeliveryOrder>()
- .eq("user_id", userId).eq("client_request_id", requestId).last("FOR UPDATE"));
- if (concurrent != null) return participantDetail(lockOrder(concurrent.getId()), true, false);
- throw duplicate;
- }
- }
- saveStops(order, quote.pickups(), request.getDelivery(), now);
- saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
- writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
- if (Boolean.TRUE.equals(order.getIsDisplay())) {
- notificationService.notifyAvailable(order);
- }
- return participantDetail(order, true, false);
- }
- /**
- * 闪送订单号:98 + 13 位毫秒时间戳 + 2 位同毫秒递增序列,共 17 位纯数字(2026-09-22 起)。
- * 与外卖订单(99 + 毫秒时间戳)风格同构,便于电话报单与按时间排查;
- * 同毫秒并发由 JVM 内原子递增序列区分,时钟回拨/多实例等理论撞号由
- * order_no 唯一索引(uk_flash_order_no)兜底,插入冲突换号重试。
- * 2026-09-22 之前的存量单为「FD + UUID 前 24 位大写」旧格式,保持不变。
- */
- static String generateOrderNo() {
- long seq = ORDER_NO_SEQ.updateAndGet(current -> (current + 1) % 100);
- return "98" + System.currentTimeMillis() + String.format(Locale.ROOT, "%02d", seq);
- }
- /** 判断唯一键冲突是否来自订单号(区别于 userId+clientRequestId 幂等键)。 */
- private static boolean isOrderNoConflict(DataIntegrityViolationException exception) {
- String message = exception.getMostSpecificCause().getMessage();
- return message != null && message.contains("uk_flash_order_no");
- }
- /** 查询本人订单详情,仅寄件人和匹配到的收件人可见。 */
- public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
- return userDetail(userId, requireOrder(orderId));
- }
- /** 按订单编号查询本人订单详情;App 点击推送消息打开订单详情时使用。 */
- public FlashDeliveryOrderDetailView userDetailByOrderNo(Long userId, String orderNo) {
- return userDetail(userId, requireOrderByNo(orderNo));
- }
- private FlashDeliveryOrderDetailView userDetail(Long userId, FlashDeliveryOrder order) {
- if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
- FlashDeliveryOrderDetailView view = participantDetail(order, true, false, userId);
- view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && Boolean.TRUE.equals(view.getCanUserCancel()));
- return view;
- }
- /** 地址修改先报价,用户确认前不改动订单。 */
- public FlashDeliveryQuoteView quoteAddress(Long userId, Long orderId,
- FlashDeliveryAddressChangeRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
- return calculateAddressQuote(order, request);
- }
- /** 确认修改待接单订单地址:重算报价并与回传值比对,按版本条件更新。 */
- @Transactional
- public FlashDeliveryOrderDetailView updateAddress(Long userId, Long orderId,
- FlashDeliveryAddressConfirmRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
- FlashDeliveryQuoteView quote = calculateAddressQuote(order, request);
- if (!Objects.equals(request.getQuotedDistanceMeters(), quote.getDistanceMeters())
- || !Objects.equals(request.getQuotedBaseDeliveryFee(), quote.getBaseDeliveryFee())
- || !Objects.equals(request.getQuotedDistanceFee(), quote.getDistanceFee())
- || !Objects.equals(request.getQuotedUrgentFee(), quote.getUrgentFee())
- || !Objects.equals(request.getQuotedAmount(), quote.getAmount())) {
- throw new FlashDeliveryQuoteChangedException(quote);
- }
- List<FlashDeliveryPickupRequest> pickups = editablePickups(order, request);
- copyPickupSummary(order, pickups);
- copyAddress(request.getDelivery(), order, false);
- order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
- applyAddressQuote(order, quote);
- Date now = new Date();
- order.setUpdateTime(now);
- // 即使上面的查询仍显示待接单,骑手也可能已接走,最终以条件 UPDATE 为准。
- if (orderMapper.updateWaitingAddress(order, request.getOrderVersion()) != 1) {
- throw fail("flash.delivery.state.changed");
- }
- // 条件更新成功后持有订单行锁至提交,抢单不会读到仅修改一半的地址。
- stopMapper.delete(new QueryWrapper<FlashDeliveryOrderStop>().eq("order_id", orderId));
- saveStops(order, pickups, request.getDelivery(), now);
- order.setVersion(order.getVersion() + 1);
- writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "ADDRESS_UPDATED", now);
- return participantDetail(order, true, false);
- }
- /** 待接单订单追加小费:订单总额与小费同步增加,版本条件防止并发重复加款。 */
- @Transactional
- public FlashDeliveryOrderDetailView addTip(Long userId, Long orderId, FlashDeliveryTipAddRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
- if (request.getAdditionalTipAmount() == null || request.getAdditionalTipAmount().signum() <= 0) {
- throw fail("flash.delivery.tip.additional.invalid");
- }
- long tip;
- long amount;
- try {
- // 保留 JSON 原始数值,禁止 1.9 等小数在绑定时被截断为整数小费。
- long additionalTip = request.getAdditionalTipAmount().longValueExact();
- tip = Math.addExact(order.getTipAmount(), additionalTip);
- amount = Math.addExact(order.getAmount(), additionalTip);
- } catch (ArithmeticException exception) {
- throw fail("flash.delivery.tip.additional.invalid");
- }
- order.setTipAmount(tip);
- order.setAmount(amount);
- Date now = new Date();
- order.setUpdateTime(now);
- if (orderMapper.updateWaitingTip(order, request.getOrderVersion()) != 1) {
- throw fail("flash.delivery.state.changed");
- }
- order.setVersion(order.getVersion() + 1);
- writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "TIP_ADDED", now);
- return participantDetail(order, true, false);
- }
- /** 校验并加载可编辑订单:本人、待接单、未绑定骑手且版本一致。 */
- private FlashDeliveryOrder requireEditableOrder(Long userId, Long orderId, Integer version) {
- requireUserId(userId);
- if (orderId == null || orderId <= 0) throw fail("flash.delivery.order.not.found");
- // 写入路径先锁订单再读取站点,保持与抢单、取货和取消一致的锁顺序。
- FlashDeliveryOrder order = TransactionSynchronizationManager.isActualTransactionActive()
- ? lockOrder(orderId) : requireOrder(orderId);
- if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
- if (!WAITING_ACCEPTANCE.equals(order.getStatus()) || order.getRiderId() != null) {
- throw fail("flash.delivery.edit.not.allowed");
- }
- if (version == null || version < 0) throw fail("flash.delivery.order.version.invalid");
- if (!Objects.equals(version, order.getVersion())) throw fail("flash.delivery.state.changed");
- return order;
- }
- /** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
- private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
- FlashDeliveryAddressChangeRequest request) {
- if (order.getDeliveryStopCount() != null) throw fail("flash.delivery.edit.not.allowed");
- List<FlashDeliveryPickupRequest> pickups = editablePickups(order, request);
- validatePickups(pickups, request.getDelivery());
- RouteDistance route = pickupRoute(pickups, request.getDelivery(), vehicleTypeOf(order));
- // FR-074/093:改址沿用下单快照,只重算整条路线,平台后续调价不影响此单。
- FlashDeliveryPricing pricing = pricingSnapshot(order);
- FlashDeliveryPriceBreakdown breakdown;
- try {
- breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
- order.getDeliveryType(), order.getTipAmount());
- } catch (ArithmeticException exception) {
- throw fail("flash.delivery.tip.invalid");
- }
- FlashDeliveryQuoteRequest quoteRequest = new FlashDeliveryQuoteRequest();
- quoteRequest.setServiceType(order.getServiceType());
- quoteRequest.setDeliveryType(order.getDeliveryType());
- quoteRequest.setVehicleType(vehicleTypeOf(order));
- quoteRequest.setDeliveryMode(order.getDeliveryMode());
- quoteRequest.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
- quoteRequest.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
- FlashDeliveryQuoteView view = quoteView(quoteRequest, new QuoteContext(pricing, route, breakdown, pickups));
- view.setOrderVersion(order.getVersion());
- return view;
- }
- /** v2 待接单改址报价;订单级物品、配送方式与原运价快照保持不变。 */
- public FlashDeliveryQuoteView quoteAddressV2(Long userId, FlashDeliveryV2AddressChangeRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- FlashDeliveryOrder order = requireEditableOrder(userId, request.getOrderId(), request.getOrderVersion());
- return calculateAddressQuoteV2(order, request);
- }
- /** v2 改址必须提交全部站点,条件更新成功后再替换全部站点。 */
- @Transactional
- public FlashDeliveryOrderDetailView updateAddressV2(Long userId, FlashDeliveryV2AddressConfirmRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- FlashDeliveryOrder order = requireEditableOrder(userId, request.getOrderId(), request.getOrderVersion());
- FlashDeliveryQuoteView quote = calculateAddressQuoteV2(order, request);
- if (!Objects.equals(request.getQuotedDistanceMeters(), quote.getDistanceMeters())
- || !Objects.equals(request.getQuotedBaseDeliveryFee(), quote.getBaseDeliveryFee())
- || !Objects.equals(request.getQuotedDistanceFee(), quote.getDistanceFee())
- || !Objects.equals(request.getQuotedUrgentFee(), quote.getUrgentFee())
- || !Objects.equals(request.getQuotedAmount(), quote.getAmount())) {
- throw new FlashDeliveryQuoteChangedException(quote);
- }
- List<FlashDeliveryV2StopRequest> stops = normalizeV2Stops(request.getStops());
- FlashDeliveryV2QuoteRequest orderItems = v2OrderSnapshot(order, stops);
- copyPickupSummary(order, pickupAdapters(stops.stream().filter(s -> "PICKUP".equals(s.getType())).toList(), orderItems));
- FlashDeliveryV2StopRequest firstDelivery = stops.stream().filter(s -> "DELIVERY".equals(s.getType())).findFirst().orElseThrow();
- copyAddress(firstDelivery, order, false);
- order.setReceiverUserId(resolveReceiverUserId(firstDelivery.getPhone()));
- order.setPickupStopCount((int) stops.stream().filter(s -> "PICKUP".equals(s.getType())).count());
- order.setPickedUpStopCount(0);
- order.setDeliveryStopCount((int) stops.stream().filter(s -> "DELIVERY".equals(s.getType())).count());
- order.setDeliveredStopCount(0);
- order.setCurrentTargetStopId(null);
- order.setDeliveryPinCode(null);
- applyAddressQuote(order, quote);
- Date now = new Date();
- order.setUpdateTime(now);
- if (orderMapper.updateWaitingAddress(order, request.getOrderVersion()) != 1) {
- throw fail("flash.delivery.state.changed");
- }
- stopMapper.delete(new QueryWrapper<FlashDeliveryOrderStop>().eq("order_id", order.getId()));
- saveStopsV2(order, stops, Boolean.TRUE.equals(order.getPinRequired()), now);
- order.setVersion(order.getVersion() + 1);
- writeLog(order.getId(), WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "ADDRESS_UPDATED", now);
- return participantDetail(order, true, false);
- }
- private FlashDeliveryQuoteView calculateAddressQuoteV2(FlashDeliveryOrder order,
- FlashDeliveryV2AddressChangeRequest request) {
- if (order.getDeliveryStopCount() == null) throw fail("flash.delivery.edit.not.allowed");
- List<FlashDeliveryV2StopRequest> stops = normalizeV2Stops(request.getStops());
- FlashDeliveryV2QuoteRequest orderItems = v2OrderSnapshot(order, stops);
- validateOrderLevelItem(orderItems);
- FlashDeliveryPricing pricing = pricingSnapshot(order);
- RouteDistance route = v2Route(stops, vehicleTypeOf(order));
- FlashDeliveryPriceBreakdown breakdown;
- try {
- breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
- order.getDeliveryType(), order.getTipAmount());
- } catch (ArithmeticException exception) {
- throw fail("flash.delivery.tip.invalid");
- }
- List<FlashDeliveryPickupRequest> pickups = pickupAdapters(stops.stream()
- .filter(s -> "PICKUP".equals(s.getType())).toList(), orderItems);
- FlashDeliveryQuoteView view = quoteView(quoteAdapter(orderItems),
- new QuoteContext(pricing, route, breakdown, pickups));
- view.setOrderVersion(order.getVersion());
- return view;
- }
- private FlashDeliveryV2QuoteRequest v2OrderSnapshot(FlashDeliveryOrder order, List<FlashDeliveryV2StopRequest> stops) {
- FlashDeliveryV2QuoteRequest snapshot = new FlashDeliveryV2QuoteRequest();
- BeanUtils.copyProperties(order, snapshot);
- snapshot.setStops(stops);
- return snapshot;
- }
- private FlashDeliveryPricing pricingSnapshot(FlashDeliveryOrder order) {
- FlashDeliveryPricing pricing = new FlashDeliveryPricing();
- pricing.setId(order.getPricingId()); pricing.setConfigVersion(order.getPricingVersion());
- pricing.setStartTime(order.getPricingStartTime()); pricing.setEndTime(order.getPricingEndTime());
- pricing.setStartingDistance(order.getStartingDistance()); pricing.setStartingFare(order.getStartingFare());
- pricing.setDistance(order.getDistance()); pricing.setFreight(order.getFreight());
- pricing.setUrgentRate(order.getUrgentRate()); pricing.setMinimumUrgentFee(order.getMinimumUrgentFee());
- return pricing;
- }
- private void applyAddressQuote(FlashDeliveryOrder order, FlashDeliveryQuoteView quote) {
- order.setDistanceMeters(quote.getDistanceMeters());
- order.setDistanceSource(quote.getDistanceSource());
- order.setEstimatedDurationSeconds(quote.getEstimatedDurationSeconds());
- order.setPricingId(quote.getPricingId());
- order.setPricingVersion(quote.getPricingVersion());
- order.setPricingStartTime(quote.getStartTime());
- order.setPricingEndTime(quote.getEndTime());
- order.setStartingDistance(quote.getStartingDistance());
- order.setStartingFare(quote.getStartingFare());
- order.setDistance(quote.getDistance());
- order.setFreight(quote.getFreight());
- order.setUrgentRate(quote.getUrgentRate());
- order.setMinimumUrgentFee(quote.getMinimumUrgentFee());
- order.setDistanceFee(quote.getDistanceFee());
- order.setBaseDeliveryFee(quote.getBaseDeliveryFee());
- order.setUrgentFee(quote.getUrgentFee());
- order.setAmount(quote.getAmount());
- }
- /** 用户取消:仅待接单或已接单(实际取件前)允许。 */
- @Transactional
- public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
- FlashDeliveryOrder order = lockOrder(orderId);
- if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
- if (!canCancelBeforePickup(order, requireStops(order))) throw fail("flash.delivery.cancel.not.allowed");
- String reason = requiredReason(request);
- Date now = new Date();
- if (orderMapper.cancel(orderId, order.getStatus(), "USER", userId, reason, now) != 1) {
- throw fail("flash.delivery.state.changed");
- }
- // 取消和退款意图共用订单锁、同事务提交;外部退款由持久任务执行,不在这里等待网络。
- if ("3".equals(order.getPayType())) flashLinePayFactService.ensureCancellationRefundIntent(orderId);
- writeLog(orderId, order.getStatus(), CANCELLED, "USER", userId, reason, now);
- // 取消成功后通知接单骑手;未接单订单由通知服务内部跳过。
- notificationService.notifyCancelled(order);
- }
- /** 用户(寄件人或收件人)确认收货:已送达推进为已完成。 */
- @Transactional
- public void confirmReceipt(Long userId, Long orderId) {
- FlashDeliveryOrder order = lockOrder(orderId);
- if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
- List<FlashDeliveryOrderStop> rows = requireStops(order);
- if (!allPicked(rows) || !allDelivered(rows)) throw fail("flash.delivery.complete.not.allowed");
- Date now = new Date();
- if (orderMapper.transitionByParticipant(orderId, userId, DELIVERED, COMPLETED, now) != 1) {
- throw fail("flash.delivery.complete.not.allowed");
- }
- writeLog(orderId, DELIVERED, COMPLETED, "USER", userId, null, now);
- recordRiderIncome(order, now);
- }
- /** 寄件人对已完成订单评价骑手:星级 1-5 仅一次,条件更新原子防并发重复提交。 */
- @Transactional
- public void rateRider(Long userId, Long orderId, FlashDeliveryRatingRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- Double stars = request.getStars();
- if (stars == null || stars < 1 || stars > 5) throw fail("flash.delivery.rating.stars.invalid");
- FlashDeliveryOrder order = requireOrder(orderId);
- if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
- if (!COMPLETED.equals(order.getStatus()) || order.getRiderId() == null) {
- throw fail("flash.delivery.rating.not.allowed");
- }
- if (order.getRiderStars() != null) throw fail("flash.delivery.rating.already");
- if (orderMapper.rateRider(orderId, userId, stars, new Date()) != 1) {
- throw fail("flash.delivery.rating.already");
- }
- }
- /**
- * 骑手任务列表:按页签查询(newTask 可抢任务、toPickup 待取件、delivering 配送中、
- * completed 已送达/已完成、cancelled 已取消)。newTask 页签额外按骑手坐标做距离过滤和就近排序,
- * 并返回附近任务数与最高订单金额;其余页签按订单归属骑手查询。
- */
- public FlashDeliveryRiderOrderPageView riderOrders(Long riderId, int pageNum, int pageSize,
- String tab, BigDecimal longitude, BigDecimal latitude) {
- InfoUser rider = requireRider(riderId);
- Set<String> acceptedPayTypes = acceptedRiderPayTypes(rider);
- if (!hasText(tab)) throw fail("flash.delivery.tab.invalid");
- String selectedTab = tab.trim();
- // lambda 无法捕获被重新赋值的参数,统一改用仅 newTask 携带坐标的有效变量。
- BigDecimal effectiveLongitude = "newTask".equals(selectedTab) ? longitude : null;
- BigDecimal effectiveLatitude = "newTask".equals(selectedTab) ? latitude : null;
- if ("newTask".equals(selectedTab)) {
- // 新任务派单按骑手选择的配送类型过滤;本人已接订单页签不受限制。
- if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
- validateRiderCoordinates(effectiveLongitude, effectiveLatitude);
- }
- Integer riderVehicleType = riderVehicleType(rider);
- QueryWrapper<FlashDeliveryOrder> query = riderOrdersQuery(riderId, riderVehicleType, selectedTab,
- effectiveLongitude, effectiveLatitude, true, acceptedPayTypes);
- IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
- Map<Long, List<FlashDeliveryOrderStop>> stopRows = stopRowsFor(orders.getRecords());
- FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
- result.setRecords(orders.getRecords().stream()
- .map(order -> riderOrderListView(riderId, order, effectiveLongitude, effectiveLatitude,
- stopRows.get(order.getId()))).toList());
- result.setTotal(orders.getTotal());
- result.setCurrent(orders.getCurrent());
- result.setSize(orders.getSize());
- if ("newTask".equals(selectedTab)) {
- result.setNearbyTaskCount(orders.getTotal());
- QueryWrapper<FlashDeliveryOrder> maximumQuery = riderOrdersQuery(riderId, riderVehicleType, selectedTab,
- effectiveLongitude, effectiveLatitude, false, acceptedPayTypes)
- .select("MAX(amount)");
- result.setHighestOrderAmount(firstLong(orderMapper.selectObjs(maximumQuery)));
- }
- return result;
- }
- /**
- * 骑手端任务角标数量:一次返回闪送订单 newTask/toPickup/delivering 三个页签的计数,
- * App 无需逐页签切换请求。newTask 与列表同口径(车型、支付方式、可抢时间窗与可选距离上限);
- * 骑手未开通闪送时 newTask 计 0(列表接口保持原有报错行为)。
- */
- public FlashDeliveryRiderOrderCountView riderOrderCounts(Long riderId, BigDecimal longitude, BigDecimal latitude) {
- InfoUser rider = requireRider(riderId);
- Set<String> acceptedPayTypes = acceptedRiderPayTypes(rider);
- Integer riderVehicleType = riderVehicleType(rider);
- FlashDeliveryRiderOrderCountView view = new FlashDeliveryRiderOrderCountView();
- view.setToPickup(orderMapper.selectCount(
- riderOrdersQuery(riderId, riderVehicleType, "toPickup", null, null, false, acceptedPayTypes)));
- view.setDelivering(orderMapper.selectCount(
- riderOrdersQuery(riderId, riderVehicleType, "delivering", null, null, false, acceptedPayTypes)));
- if (rider.supportsDeliveryType("FLASH")) {
- validateRiderCoordinates(longitude, latitude);
- view.setNewTask(orderMapper.selectCount(
- riderOrdersQuery(riderId, riderVehicleType, "newTask", longitude, latitude, false, acceptedPayTypes)));
- } else {
- view.setNewTask(0L);
- }
- return view;
- }
- /** 骑手任务详情:抢单前返回脱敏视图(隐藏联系人、电话、坐标和 PIN);接单后仅中单骑手可见完整信息。 */
- public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
- return riderDetail(riderId, requireOrder(orderId));
- }
- /** 按订单编号查询骑手视角订单详情;脱敏与归属校验和数字 ID 查询一致。 */
- public FlashDeliveryOrderDetailView riderDetailByOrderNo(Long riderId, String orderNo) {
- return riderDetail(riderId, requireOrderByNo(orderNo));
- }
- private FlashDeliveryOrderDetailView riderDetail(Long riderId, FlashDeliveryOrder order) {
- InfoUser rider = requireRider(riderId);
- if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
- // 未开通闪送或车型不匹配时按订单不存在处理,避免泄露待抢任务信息。
- if (!Boolean.TRUE.equals(order.getIsDisplay()) || !isAvailableAt(order, new Date())
- || !rider.supportsDeliveryType("FLASH")
- || !isRiderVehicleCompatible(rider, order)) {
- throw fail("flash.delivery.order.not.found");
- }
- // 接单前显示取送文字地址,但隐藏联系人、电话、精确坐标、实际 PIN 和履约图片。
- return participantDetail(order, false, true);
- }
- if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
- return participantDetail(order, false, false);
- }
- /** 原子抢单:骑手锁内完成互斥校验与条件更新,同一订单仅一名骑手成功。 */
- @Transactional
- public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
- return riderDeliveryLockService.withLock(riderId, () -> {
- FlashDeliveryOrder target = requireOrder(orderId);
- InfoUser rider = requireRider(riderId);
- // 列表过滤不能替代锁内校验,防止骑手绕过列表直接提交不同车型订单。
- if (!isRiderVehicleCompatible(rider, target)) throw fail("flash.delivery.rider.vehicle.not.match");
- Date now = new Date();
- // 预约单必须仍处在取件窗口内;即使骑手持有订单 ID,也不能绕过列表限制提前或逾期接单。
- if (!Boolean.TRUE.equals(target.getIsDisplay()) || !isAvailableAt(target, now)) {
- throw fail("flash.delivery.schedule.not.open");
- }
- // 账号资格、闪送/总任务容量与急送独占统一在骑手锁内复核,列表结果不能替代接单校验。
- riderDeliveryAcceptanceService.assertCanAcceptFlash(riderId, target.getDeliveryType());
- // 列表过滤不是授权,直接传订单 ID 抢单同样复核骑手收款配置和支付选择。
- paymentMethodGateService.assertUsable(target.getPayType(), PaymentMethodGateService.GateScope.RIDER_FLASH, null, riderId);
- // Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
- if (orderMapper.accept(orderId, riderId, now) != 1) {
- throw fail("flash.delivery.order.already.accepted");
- }
- writeLog(orderId, WAITING_ACCEPTANCE, ACCEPTED, "RIDER", riderId, null, now);
- notificationService.notifyAccepted(target);
- return participantDetail(requireOrder(orderId), false, false);
- });
- }
- /** 骑手逐站取件:保存本站凭证,仅在全部取货站完成后推进订单为已取件。 */
- @Transactional
- public void pickup(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
- pickupStation(riderId, orderId, request == null ? null : request.getStopId(), request);
- }
- /** 骑手送达:校验交付 PIN(启用时)并提交送达凭证,订单由已取件推进为已送达。 */
- @Transactional
- public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
- requireRider(riderId);
- FlashDeliveryOrder order = lockOrder(orderId);
- if (order.getDeliveryStopCount() != null) throw fail("flash.delivery.transition.not.allowed");
- if (!Objects.equals(riderId, order.getRiderId()) || !PICKED_UP.equals(order.getStatus())) {
- throw fail("flash.delivery.transition.not.allowed");
- }
- List<FlashDeliveryOrderStop> rows = requireStops(order);
- if (!allPicked(rows)) throw fail("flash.delivery.transition.not.allowed");
- if (Boolean.TRUE.equals(order.getPinRequired())
- && (request == null || !Objects.equals(order.getDeliveryPinCode(), trimToNull(request.getPinCode())))) {
- throw fail("flash.delivery.pin.invalid");
- }
- FlashDeliveryOrderStop delivery = rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).findFirst().orElseThrow();
- if (request != null && request.getStopId() != null && !Objects.equals(request.getStopId(), delivery.getId())) throw fail("flash.delivery.stop.invalid");
- List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
- Date now = new Date();
- if (orderMapper.transitionByRider(orderId, riderId, PICKED_UP, DELIVERED, "delivered_at", now) != 1) {
- throw fail("flash.delivery.transition.not.allowed");
- }
- delivery.setStatus("DELIVERED"); delivery.setDeliveredAt(now);
- if (stopMapper.updateById(delivery) != 1) throw fail("flash.delivery.state.changed");
- saveStationImages(orderId, delivery.getId(), "DELIVERY", "RIDER", riderId, urls, now);
- writeLog(orderId, PICKED_UP, DELIVERED, "RIDER", riderId, null, now);
- // 送达成功后通知寄件用户;重读订单以携带最新状态。
- notificationService.notifyDelivered(requireOrder(orderId));
- }
- /** 逐站送达:v2 允许交错取送;校验本站收货码和凭证,全部取送完成才推进整单已送达。 */
- @Transactional
- public void deliverAtStop(Long riderId, Long orderId, Long stopId, FlashDeliveryDeliverRequest request) {
- requireRider(riderId);
- FlashDeliveryOrder order = lockOrder(orderId);
- if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
- List<FlashDeliveryOrderStop> rows = requireStops(order);
- FlashDeliveryOrderStop stop = requireStop(rows, stopId);
- if (!"DELIVERY".equals(stop.getStopType())) throw fail("flash.delivery.stop.invalid");
- // 已确认站点的重试直接成功,不重复写凭证或推进计数。
- if ("DELIVERED".equals(stop.getStatus())) return;
- boolean v2 = order.getDeliveryStopCount() != null;
- String fromStatus = order.getStatus();
- // v2 未取完时仍归待取件,但已取到的物品可送;旧单继续先取完再送。
- if (v2 ? (!ACCEPTED.equals(fromStatus) && !PICKED_UP.equals(fromStatus)) || !anyPicked(rows)
- : !PICKED_UP.equals(fromStatus) || !allPicked(rows)) {
- throw fail("flash.delivery.transition.not.allowed");
- }
- // 逐站确认以本站 stopId 为准;当前目标只用于导航提示,不限制实际送达顺序。
- // 不建立取送物品关联,骑手按备注核对;但不能漏取站点就确认最后一站并结束整单。
- boolean lastDelivery = rows.stream().filter(s -> "DELIVERY".equals(s.getStopType()))
- .filter(s -> !"DELIVERED".equals(s.getStatus())).count() == 1;
- if (lastDelivery && !allPicked(rows)) throw fail("flash.delivery.transition.not.allowed");
- String expectedPin = order.getDeliveryStopCount() == null
- ? order.getDeliveryPinCode() : stop.getDeliveryPinCode();
- if (Boolean.TRUE.equals(order.getPinRequired())
- && (!hasText(expectedPin) || request == null
- || !Objects.equals(expectedPin, trimToNull(request.getPinCode())))) {
- throw fail("flash.delivery.pin.invalid");
- }
- List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
- Date now = new Date();
- stop.setStatus("DELIVERED"); stop.setDeliveredAt(now);
- if (stopMapper.updateById(stop) != 1) throw fail("flash.delivery.state.changed");
- saveStationImages(orderId, stopId, "DELIVERY", "RIDER", riderId, urls, now);
- order.setDeliveredStopCount((int) rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())
- && "DELIVERED".equals(s.getStatus())).count());
- order.setCurrentTargetStopId(null); order.setUpdateTime(now);
- if (allPicked(rows) && allDelivered(rows)) {
- // 末站完成才整单推进,与取件侧同一行锁与条件更新路径。
- order.setStatus(DELIVERED); order.setDeliveredAt(now);
- }
- if (orderMapper.updateDeliveryFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
- writeLog(orderId, fromStatus, order.getStatus(), "RIDER", riderId, "DELIVERY_STOP:" + stopId, now);
- if (order.getDeliveryStopCount() != null) {
- notificationService.notifyDeliveryStop(order, stop, order.getDeliveredStopCount(), expectedDeliveryStopCount(order));
- } else if (DELIVERED.equals(order.getStatus())) {
- notificationService.notifyDelivered(requireOrder(orderId));
- }
- }
- // ==== v2 多点取送统一契约(2026-10-14):统一 stops 数组、订单级物品、独立计价 ====
- /** v2 实时报价:统一站点数组顺序即路线顺序,物品主体在订单级,独立计价方法。 */
- public FlashDeliveryQuoteView quoteV2(FlashDeliveryV2QuoteRequest request) {
- QuoteContext context = calculateQuoteV2(request);
- return quoteView(quoteAdapter(request), context);
- }
- /** v2 幂等创建:结构与旧创建互斥;按站固化收件人与收货码,回传报价不一致时拒绝。 */
- @Transactional
- public FlashDeliveryOrderDetailView createV2(Long userId, FlashDeliveryV2CreateRequest request) {
- requireUserId(userId);
- if (request == null || !hasText(request.getClientRequestId())
- || request.getClientRequestId().trim().length() > 64) {
- throw fail("flash.delivery.client.request.id.invalid");
- }
- String requestId = request.getClientRequestId().trim();
- FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
- if (existing != null) return participantDetail(lockOrder(existing.getId()), true, false);
- if (request.getUserNote() != null && request.getUserNote().length() > 500) {
- throw fail("flash.delivery.note.too.long");
- }
- List<FlashDeliveryV2StopRequest> stops = normalizeV2Stops(request.getStops());
- QuoteContext quote = calculateQuoteV2(request);
- FlashDeliveryQuoteView latestQuote = quoteView(quoteAdapter(request), quote);
- if (!matchesQuotedPriceV2(request, latestQuote)) {
- throw new FlashDeliveryQuoteChangedException(latestQuote);
- }
- Date now = new Date();
- String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
- validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
- boolean pinRequired = request.getPinRequired() == null || request.getPinRequired();
- String payType = normalizePayType(request.getPayType());
- List<String> senderImages = validateProofUrls(request.getSenderImageUrls(), false);
- List<FlashDeliveryV2StopRequest> deliveries = stops.stream()
- .filter(s -> "DELIVERY".equals(s.getType())).toList();
- FlashDeliveryOrder order = new FlashDeliveryOrder();
- order.setOrderNo(generateOrderNo());
- order.setClientRequestId(requestId);
- order.setUserId(userId);
- order.setServiceType(request.getServiceType());
- order.setDeliveryType(request.getDeliveryType());
- order.setVehicleType(request.getVehicleType());
- order.setStatus(WAITING_ACCEPTANCE);
- copyPickupSummary(order, quote.pickups());
- order.setPickedUpStopCount(0);
- order.setDeliveryStopCount(deliveries.size());
- order.setDeliveredStopCount(0);
- order.setIsDisplay("NOW".equals(deliveryMode));
- order.setDeliveryMode(deliveryMode);
- order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
- order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
- order.setPinRequired(pinRequired);
- // v2 只使用各收货站独立的收货码,订单级字段不生成通用码。
- order.setDeliveryPinCode(null);
- FlashDeliveryV2StopRequest firstDelivery = deliveries.get(0);
- copyAddress(firstDelivery, order, false);
- // 订单级收件人沿用首收货站匹配,逐站收件人保存在站点表。
- order.setReceiverUserId(resolveReceiverUserId(firstDelivery.getPhone()));
- order.setDistanceMeters(quote.route().distanceMeters());
- order.setDistanceSource(quote.route().source());
- order.setEstimatedDurationSeconds(quote.route().durationSeconds());
- order.setAmount(quote.breakdown().getAmount());
- order.setCurrency("TWD");
- order.setPayType(payType);
- order.setPricingId(quote.pricing().getId());
- order.setPricingVersion(quote.pricing().getConfigVersion());
- order.setPricingStartTime(quote.pricing().getStartTime());
- order.setPricingEndTime(quote.pricing().getEndTime());
- order.setStartingDistance(quote.pricing().getStartingDistance());
- order.setStartingFare(quote.pricing().getStartingFare());
- order.setDistance(quote.pricing().getDistance());
- order.setFreight(quote.pricing().getFreight());
- order.setDistanceFee(quote.breakdown().getDistanceFee());
- order.setBaseDeliveryFee(quote.breakdown().getBaseDeliveryFee());
- order.setUrgentRate(quote.pricing().getUrgentRate());
- order.setMinimumUrgentFee(quote.pricing().getMinimumUrgentFee());
- order.setUrgentFee(quote.breakdown().getUrgentFee());
- order.setTipAmount(quote.breakdown().getTipAmount());
- order.setUserNote(trimToNull(request.getUserNote()));
- order.setVersion(0);
- order.setCreateTime(now);
- order.setUpdateTime(now);
- try {
- orderMapper.insert(order);
- } catch (DataIntegrityViolationException duplicate) {
- if (isOrderNoConflict(duplicate)) {
- order.setId(null);
- order.setOrderNo(generateOrderNo());
- orderMapper.insert(order);
- } else {
- FlashDeliveryOrder concurrent = orderMapper.selectOne(new QueryWrapper<FlashDeliveryOrder>()
- .eq("user_id", userId).eq("client_request_id", requestId).last("FOR UPDATE"));
- if (concurrent != null) return participantDetail(lockOrder(concurrent.getId()), true, false);
- throw duplicate;
- }
- }
- saveStopsV2(order, stops, pinRequired, now);
- saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
- writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
- if (Boolean.TRUE.equals(order.getIsDisplay())) {
- notificationService.notifyAvailable(order);
- }
- return participantDetail(order, true, false);
- }
- /** v2 物品主体在订单级一次设置;校验规则与旧逐站物品一致。 */
- private void validateOrderLevelItem(FlashDeliveryV2QuoteRequest request) {
- if (!PACKAGE_TYPES.contains(Objects.toString(request.getPackageType(), ""))) {
- throw fail("flash.delivery.package.type.invalid");
- }
- if (request.getQuantity() == null || request.getQuantity() <= 0
- || hasText(request.getWeightRange()) && !WEIGHT_RANGES.contains(request.getWeightRange())
- || tooLong(request.getSpecification(), 255)) throw fail("flash.delivery.item.invalid");
- }
- /** v2 报价核心:站点结构校验后命中运价,路线经点为全部取货站与收货站。 */
- private QuoteContext calculateQuoteV2(FlashDeliveryV2QuoteRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- validateServiceType(request.getServiceType());
- validateDeliveryType(request.getDeliveryType());
- List<FlashDeliveryV2StopRequest> stops = normalizeV2Stops(request.getStops());
- if (request.getTipAmount() == null || request.getTipAmount() < 0) throw fail("flash.delivery.tip.invalid");
- validateOrderLevelItem(request);
- Date now = new Date();
- String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
- validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
- Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
- Integer vehicle = requireVehicleType(request.getVehicleType());
- FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate), vehicle);
- RouteDistance route = v2Route(stops, vehicle);
- FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
- route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
- List<FlashDeliveryV2StopRequest> pickups = stops.stream()
- .filter(s -> "PICKUP".equals(s.getType())).toList();
- return new QuoteContext(pricing, route, breakdown, pickupAdapters(pickups, request));
- }
- /** v2 物品主体在订单级;内部取货站适配器统一携带订单级物品供既有校验与摘要复用。 */
- private List<FlashDeliveryPickupRequest> pickupAdapters(List<FlashDeliveryV2StopRequest> pickups,
- FlashDeliveryV2QuoteRequest request) {
- return pickups.stream().map(stop -> {
- FlashDeliveryPickupRequest adapter = new FlashDeliveryPickupRequest();
- BeanUtils.copyProperties(stop, adapter);
- adapter.setPackageType(request.getPackageType());
- adapter.setQuantity(request.getQuantity());
- adapter.setWeightRange(request.getWeightRange());
- adapter.setSpecification(request.getSpecification());
- return adapter;
- }).toList();
- }
- /** v2 站点校验:客户原序可交错取送,首站取货、末站收货,保留数量和地址去重校验。 */
- private List<FlashDeliveryV2StopRequest> normalizeV2Stops(List<FlashDeliveryV2StopRequest> stops) {
- if (stops == null || stops.isEmpty()) throw fail("flash.delivery.stops.required");
- for (FlashDeliveryV2StopRequest stop : stops) {
- if (!"PICKUP".equals(stop.getType()) && !"DELIVERY".equals(stop.getType())) {
- throw fail("flash.delivery.stop.type.invalid");
- }
- validateAddress(stop);
- if (tooLong(stop.getRemark(), 200)) throw fail("flash.delivery.stop.remark.too.long");
- }
- List<FlashDeliveryV2StopRequest> pickups = stops.stream()
- .filter(s -> "PICKUP".equals(s.getType())).toList();
- List<FlashDeliveryV2StopRequest> deliveries = stops.stream()
- .filter(s -> "DELIVERY".equals(s.getType())).toList();
- if (pickups.isEmpty() || deliveries.isEmpty()
- || pickups.size() > pickupStopLimit() || deliveries.size() > deliveryStopLimit()) {
- throw fail("flash.delivery.stops.limit.exceeded");
- }
- // 仅约束路线起终点,不按类型重排;报价、创建和完整改址都保留客户填写的顺序。
- if (!"PICKUP".equals(stops.get(0).getType())
- || !"DELIVERY".equals(stops.get(stops.size() - 1).getType())) {
- throw fail("flash.delivery.stops.order.invalid");
- }
- for (int i = 0; i < stops.size(); i++) {
- for (int j = i + 1; j < stops.size(); j++) {
- FlashDeliveryV2StopRequest a = stops.get(i);
- FlashDeliveryV2StopRequest b = stops.get(j);
- if (sameAddressText(a, b)
- || a.getLatitude().compareTo(b.getLatitude()) == 0
- && a.getLongitude().compareTo(b.getLongitude()) == 0) {
- throw fail("flash.delivery.address.same");
- }
- }
- }
- return stops;
- }
- /** v2 路线:经点为按用户顺序排列的全部站点;两点退化为单段计算。 */
- private RouteDistance v2Route(List<FlashDeliveryV2StopRequest> stops, Integer vehicleType) {
- List<GeoPoint> points = stops.stream()
- .map(s -> new GeoPoint(s.getLatitude(), s.getLongitude())).toList();
- RouteDistance route = points.size() == 2
- ? routeService.calculate(points.get(0), points.get(1), vehicleType)
- : routeService.calculate(points, vehicleType);
- if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
- return route;
- }
- /** 复用旧报价视图组装:仅读取订单级字段,站点信息不进入报价视图。 */
- private FlashDeliveryQuoteRequest quoteAdapter(FlashDeliveryV2QuoteRequest request) {
- FlashDeliveryQuoteRequest adapter = new FlashDeliveryQuoteRequest();
- adapter.setServiceType(request.getServiceType());
- adapter.setDeliveryType(request.getDeliveryType());
- adapter.setVehicleType(request.getVehicleType());
- adapter.setPackageType(request.getPackageType());
- adapter.setQuantity(request.getQuantity());
- adapter.setWeightRange(request.getWeightRange());
- adapter.setSpecification(request.getSpecification());
- adapter.setTipAmount(request.getTipAmount());
- adapter.setDeliveryMode(request.getDeliveryMode());
- adapter.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
- adapter.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
- adapter.setPayType(request.getPayType());
- return adapter;
- }
- private boolean matchesQuotedPriceV2(FlashDeliveryV2CreateRequest request, FlashDeliveryQuoteView latest) {
- return Objects.equals(request.getPricingId(), latest.getPricingId())
- && Objects.equals(request.getPricingVersion(), latest.getPricingVersion())
- && Objects.equals(request.getQuotedBaseDeliveryFee(), latest.getBaseDeliveryFee())
- && Objects.equals(request.getQuotedDistanceFee(), latest.getDistanceFee())
- && Objects.equals(request.getQuotedUrgentFee(), latest.getUrgentFee())
- && Objects.equals(request.getQuotedAmount(), latest.getAmount());
- }
- /** 保存 v2 全部站点:按用户顺序落 stop_order,逐站写入备注、收件人与收货码。 */
- private void saveStopsV2(FlashDeliveryOrder order, List<FlashDeliveryV2StopRequest> stops,
- boolean pinRequired, Date now) {
- Set<String> deliveryPins = new HashSet<>();
- for (int i = 0; i < stops.size(); i++) {
- FlashDeliveryV2StopRequest stop = stops.get(i);
- FlashDeliveryOrderStop row = new FlashDeliveryOrderStop();
- row.setName(stop.getName().trim()); row.setPhone(stop.getPhone().trim());
- row.setAddress(stop.getAddress().trim()); row.setAddressDetail(trimToNull(stop.getAddressDetail()));
- row.setHandoffMethod(trimToNull(stop.getHandoffMethod()));
- row.setLongitude(stop.getLongitude()); row.setLatitude(stop.getLatitude());
- row.setRemark(trimToNull(stop.getRemark()));
- row.setOrderId(order.getId()); row.setStopType(stop.getType()); row.setStopOrder(i);
- row.setStatus("PENDING"); row.setCreateTime(now);
- if ("DELIVERY".equals(stop.getType())) {
- row.setReceiverUserId(resolveReceiverUserId(stop.getPhone()));
- if (pinRequired) {
- String pin;
- do {
- pin = String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000));
- } while (!deliveryPins.add(pin));
- row.setDeliveryPinCode(pin);
- }
- }
- if (stopMapper.insert(row) != 1) throw fail("flash.delivery.state.changed");
- }
- }
- /** 平台运价配置列表,按时段开始时间排序。 */
- public List<FlashDeliveryPricing> adminPricing() {
- return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
- .orderByAsc("start_time", "id"));
- }
- /** 新增运价配置:校验参数合法性与时段不重叠后入库,版本号从 1 开始。 */
- @Transactional
- public FlashDeliveryPricing createPricing(Long adminId, FlashDeliveryPricingRequest request) {
- validatePricingRequest(request, false);
- ensureNoPricingOverlap(request, null);
- Date now = new Date();
- FlashDeliveryPricing pricing = pricingFrom(request);
- pricing.setConfigVersion(1);
- pricing.setUpdatedBy(adminId);
- pricing.setCreateTime(now);
- pricing.setUpdateTime(now);
- pricingMapper.insert(pricing);
- return pricing;
- }
- /** 修改运价配置:按 configVersion 乐观锁条件更新,防止管理员并发互相覆盖。 */
- @Transactional
- public FlashDeliveryPricing updatePricing(Long adminId, Long pricingId,
- FlashDeliveryPricingRequest request) {
- validatePricingRequest(request, true);
- FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
- if (preliminary == null) throw fail("flash.delivery.pricing.not.found");
- FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
- if (pricing == null) throw fail("flash.delivery.pricing.not.found");
- ensureNoPricingOverlap(request, pricingId);
- Date now = new Date();
- UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
- .eq("id", pricingId).eq("config_version", request.getConfigVersion())
- .set("vehicle_type", request.getVehicleType())
- .set("start_time", request.getStartTime())
- .set("end_time", request.getEndTime())
- .set("starting_distance", scaleDistance(request.getStartingDistance()))
- .set("starting_fare", request.getStartingFare())
- .set("distance", scaleDistance(request.getDistance()))
- .set("freight", request.getFreight())
- .set("urgent_rate", normalizeRate(request.getUrgentRate()))
- .set("minimum_urgent_fee", request.getMinimumUrgentFee())
- .set("updated_by", adminId)
- .set("update_time", now)
- .setSql("config_version = config_version + 1");
- // 版本条件防止两个管理员同时保存时后提交者静默覆盖先提交者。
- if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
- pricing.setVehicleType(request.getVehicleType());
- pricing.setStartTime(request.getStartTime());
- pricing.setEndTime(request.getEndTime());
- pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
- pricing.setStartingFare(request.getStartingFare());
- pricing.setDistance(scaleDistance(request.getDistance()));
- pricing.setFreight(request.getFreight());
- pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
- pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
- pricing.setConfigVersion(request.getConfigVersion() + 1);
- pricing.setUpdatedBy(adminId);
- pricing.setUpdateTime(now);
- return pricing;
- }
- /** 删除运价配置。 */
- @Transactional
- public void deletePricing(Long pricingId) {
- FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
- if (preliminary == null) {
- throw fail("flash.delivery.pricing.not.found");
- }
- FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
- if (pricing == null) throw fail("flash.delivery.pricing.not.found");
- if (pricingMapper.deleteById(pricingId) != 1) throw fail("flash.delivery.pricing.not.found");
- }
- /** 平台订单分页:支持状态、业务场景、订单号模糊、寄件用户、骑手条件过滤。 */
- public IPage<FlashDeliveryOrder> adminOrders(int pageNum, int pageSize, String status,
- String serviceType, String orderNo,
- Long userId, Long riderId) {
- validateOptionalServiceType(serviceType);
- QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>().orderByDesc("id");
- optionalEq(query, "status", status);
- optionalEq(query, "service_type", serviceType);
- if (hasText(orderNo)) query.like("order_no", orderNo.trim());
- if (userId != null) query.eq("user_id", userId);
- if (riderId != null) query.eq("rider_id", riderId);
- IPage<FlashDeliveryOrder> result = orderMapper.selectPage(page(pageNum, pageSize), query);
- if (!result.getRecords().isEmpty()) {
- List<Long> ids = result.getRecords().stream().map(FlashDeliveryOrder::getId).toList();
- // 批量读取站点,不能只按主状态猜测部分取件;避免列表 N+1 查询。
- Map<Long, List<FlashDeliveryOrderStop>> grouped = stopMapper.selectList(new QueryWrapper<FlashDeliveryOrderStop>()
- .in("order_id", ids)).stream().collect(java.util.stream.Collectors.groupingBy(FlashDeliveryOrderStop::getOrderId));
- result.getRecords().forEach(order -> order.setCanAdminCancel(canCancelBeforePickup(order, grouped.getOrDefault(order.getId(), List.of()))));
- }
- return result;
- }
- /** 平台订单审计详情:返回完整订单实体、全部凭证图片和状态日志。 */
- public FlashDeliveryAdminOrderDetailView adminDetail(Long orderId) {
- FlashDeliveryOrder order = requireOrder(orderId);
- FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
- view.setOrder(order);
- List<FlashDeliveryOrderStop> stationRows = stops(orderId);
- view.setCanAdminCancel(canCancelBeforePickup(order, stationRows));
- order.setCanAdminCancel(view.getCanAdminCancel());
- if ("3".equals(order.getPayType())) view.setLinePayPayment(flashLinePayService.localStatus(orderId));
- view.setPickupStopCount(order.getPickupStopCount()); view.setPickedUpStopCount(order.getPickedUpStopCount());
- view.setCanUserCancel(view.getCanAdminCancel()); view.setCurrentTargetStopId(targetStopId(order, stationRows));
- view.setTargetAdjustments(adjustmentMapper.selectList(new QueryWrapper<FlashDeliveryTargetAdjustment>()
- .eq("order_id", orderId).orderByAsc("create_time", "id")));
- view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
- .eq("order_id", orderId).orderByAsc("proof_type", "sort_order")));
- view.setStops(stopViews(stationRows, true, view.getImages()));
- view.setHandoffImageUrls(view.getImages().stream().filter(i -> "HANDOFF".equals(i.getProofType()))
- .map(FlashDeliveryOrderImage::getImageUrl).toList());
- view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
- .eq("order_id", orderId).orderByAsc("create_time", "id")));
- return view;
- }
- /** 平台取消仅取件前允许;已有任一站取件时只能客服处理,不能取消并自动退款。 */
- @Transactional
- public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
- FlashDeliveryOrder order = lockOrder(orderId);
- if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
- String reason = requiredReason(request);
- List<FlashDeliveryOrderStop> rows = requireStops(order);
- if (!canCancelBeforePickup(order, rows)) throw fail("flash.delivery.cancel.not.allowed");
- if (request.getProblemStopId() == null || !hasText(request.getHandlingResult()) || tooLong(request.getHandlingResult(), 500)) {
- throw fail("flash.delivery.intervention.required");
- }
- requireStop(rows, request.getProblemStopId());
- Date now = new Date();
- // 保留取件前的问题站和处理结果;旧物品交接数据仍可在历史详情中查看。
- UpdateWrapper<FlashDeliveryOrder> audit = new UpdateWrapper<FlashDeliveryOrder>().eq("id", orderId)
- .set("problem_stop_id", request.getProblemStopId()).set("handling_result", request.getHandlingResult().trim());
- if (orderMapper.update(null, audit) != 1) throw fail("flash.delivery.state.changed");
- if (orderMapper.cancel(orderId, order.getStatus(), "ADMIN", adminId, reason, now) != 1) throw fail("flash.delivery.state.changed");
- if ("3".equals(order.getPayType())) flashLinePayFactService.ensureCancellationRefundIntent(orderId);
- writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
- // 平台取消同样只通知接单骑手;寄件用户按规格不推送。
- notificationService.notifyCancelled(order);
- }
- /** 将已送达订单推进为已完成;平台手动与系统自动完成共用此入口,条件更新失败返回 false。 */
- @Transactional
- public boolean complete(Long operatorId, String operatorType, Long orderId) {
- FlashDeliveryOrder order = lockOrder(orderId);
- if (!DELIVERED.equals(order.getStatus())) return false;
- List<FlashDeliveryOrderStop> rows = requireStops(order);
- if (!allPicked(rows) || !allDelivered(rows)) return false;
- Date now = new Date();
- if (orderMapper.transitionByStatus(orderId, DELIVERED, COMPLETED, "completed_at", now) != 1) return false;
- writeLog(orderId, DELIVERED, COMPLETED, operatorType, operatorId, null, now);
- recordRiderIncome(order, now);
- return true;
- }
- /**
- * 仅在锁单后 DELIVERED -> COMPLETED 条件更新成功的事务中调用,三种完成入口共用,
- * 重复请求或并发输家不会记第二笔;账单写入失败让订单完成和日志一起回滚。
- * 闪送款项直接支付给骑手,本账单只记全额收入(含小费),不扣外卖抽成/税,
- * 不调用钱包服务,也不代表骑手已经确认收款。
- */
- private void recordRiderIncome(FlashDeliveryOrder order, Date completedAt) {
- if (order.getRiderId() == null || !hasText(order.getOrderNo())
- || order.getAmount() == null || order.getAmount() < 0) {
- throw fail("flash.delivery.billing.save.failed");
- }
- UserBilling bill = new UserBilling();
- bill.setUserId(order.getRiderId());
- bill.setUserType("2");
- bill.setType("0");
- bill.setState("0");
- bill.setOrderSource("FLASH");
- bill.setDdId(order.getOrderNo());
- bill.setAmount(order.getAmount().doubleValue());
- bill.setDivvy(0.0);
- bill.setDivvyRate(0.0);
- bill.setTax(BigDecimal.ZERO);
- bill.setTaxRate("0");
- bill.setPaymentMethod(order.getPayType());
- bill.setIllustrate(MessageUtils.message("flash.delivery.billing.income"));
- bill.setCretim(completedAt);
- if (userBillingMapper.insert(bill) != 1) throw fail("flash.delivery.billing.save.failed");
- }
- /** 骑手确认实际收款:仅本人已送达订单可确认,条件更新保证重复请求不重复写日志。 */
- @Transactional
- public void confirmPayment(Long riderId, Long orderId) {
- requireRider(riderId);
- FlashDeliveryOrder order = requireOrder(orderId);
- if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
- // LINE Pay 只认网关到账事实,骑手不能用线下收款确认接口伪造线上支付。
- if ("3".equals(order.getPayType())) throw fail("flash.line.manual.forbidden");
- if (!DELIVERED.equals(order.getStatus()) && !COMPLETED.equals(order.getStatus())) {
- throw fail("flash.delivery.payment.confirm.not.allowed");
- }
- if (Integer.valueOf(1).equals(order.getPaymentStatus())) return;
- Date now = new Date();
- if (orderMapper.update(null, new UpdateWrapper<FlashDeliveryOrder>()
- .eq("id", orderId).eq("payment_status", 0)
- .set("payment_status", 1).set("update_time", now)) == 1) {
- writeLog(orderId, order.getStatus(), order.getStatus(), "RIDER", riderId, "PAYMENT_CONFIRMED", now);
- }
- }
- /** 查询送达时间早于截止点且未完成的订单,供自动完成任务分批处理。 */
- public List<FlashDeliveryOrder> autoCompleteCandidates(Date deadline, int limit) {
- return orderMapper.selectAutoCompletable(deadline, Math.min(Math.max(limit, 1), 500));
- }
- /** 查询已经错过预约取件结束时间、但仍无人接单的订单,供 Quartz 任务分批处理。 */
- public List<FlashDeliveryOrder> expiredScheduledCandidates(Date now, int limit) {
- return orderMapper.selectExpiredScheduled(now, Math.min(Math.max(limit, 1), 500));
- }
- /** 查询已进入预约窗口但仍未向骑手开放的订单,供独立 Quartz 任务分批处理。 */
- public List<FlashDeliveryOrder> scheduledAvailabilityCandidates(Date now, int limit) {
- return orderMapper.selectScheduledAvailabilityCandidates(now, Math.min(Math.max(limit, 1), 500));
- }
- /** 原子开放预约订单;仅 false -> true 成功的一方触发一次骑手新单通知。 */
- @Transactional
- public boolean openScheduledOrderForRiders(Long orderId, Date now) {
- if (orderId == null || now == null) return false;
- FlashDeliveryOrder order = orderMapper.selectById(orderId);
- if (order == null || orderMapper.openScheduledForRiders(orderId, now) != 1) return false;
- order.setIsDisplay(true);
- order.setUpdateTime(now);
- notificationService.notifyAvailable(order);
- return true;
- }
- /** 原子取消过期预约单;条件更新失败表示订单已被抢、取消或状态已变化,无需重复写日志。 */
- @Transactional
- public boolean cancelExpiredScheduledOrder(Long orderId, Date now) {
- if (orderId == null || now == null || orderMapper.cancelExpiredScheduled(orderId, now) != 1) return false;
- writeLog(orderId, WAITING_ACCEPTANCE, CANCELLED, "SYSTEM", null, SCHEDULE_EXPIRED_REASON, now);
- return true;
- }
- /** 报价核心:校验枚举、物品、地址与预约窗后,按时段命中运价并计算路线距离,产出报价上下文。 */
- private QuoteContext calculateQuote(FlashDeliveryQuoteRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- validateServiceType(request.getServiceType());
- validateDeliveryType(request.getDeliveryType());
- List<FlashDeliveryPickupRequest> pickups = normalizePickups(request);
- validatePickups(pickups, request.getDelivery());
- if (request.getTipAmount() == null || request.getTipAmount() < 0) throw fail("flash.delivery.tip.invalid");
- Date now = new Date();
- String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
- validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
- Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
- Integer selectedVehicle = requireVehicleType(request.getVehicleType());
- FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate), selectedVehicle);
- RouteDistance route = pickupRoute(pickups, request.getDelivery(), selectedVehicle);
- FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
- route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
- return new QuoteContext(pricing, route, breakdown, pickups);
- }
- /** 查询指定时刻命中的运价配置,未配置时直接报错。 */
- private FlashDeliveryPricing findPricingAt(String targetTime, Integer vehicleType) {
- FlashDeliveryPricing pricing = findPricingAtOrNull(targetTime, vehicleType);
- if (pricing == null) throw fail("flash.delivery.pricing.not.available");
- return pricing;
- }
- /** 查询命中运价;命中多条说明配置重叠,报错交由平台修正。 */
- private FlashDeliveryPricing findPricingAtOrNull(String targetTime, Integer vehicleType) {
- List<FlashDeliveryPricing> pricing = pricingMapper.selectAtTime(targetTime, vehicleType);
- if (pricing == null || pricing.isEmpty()) return null;
- if (pricing.size() > 1) throw fail("flash.delivery.pricing.overlap");
- return pricing.get(0);
- }
- /** 校验运价请求:时间格式与区间顺序、各金额取值范围及修改时的版本号。 */
- private void validatePricingRequest(FlashDeliveryPricingRequest request, boolean requireVersion) {
- if (request == null) throw fail("flash.delivery.pricing.invalid");
- BigDecimal startingDistance = normalizeDistance(request.getStartingDistance());
- BigDecimal distance = normalizeDistance(request.getDistance());
- BigDecimal urgentRate = normalizeRate(request.getUrgentRate());
- if (!FlashDeliveryVehicleType.isSupported(request.getVehicleType())
- || !validStartTime(request.getStartTime()) || !validEndTime(request.getEndTime())
- || toMinute(request.getStartTime()) >= toMinute(request.getEndTime())
- || startingDistance == null || startingDistance.signum() <= 0
- || startingDistance.compareTo(MAX_PRICING_DISTANCE) > 0
- || request.getStartingFare() == null || request.getStartingFare() <= 0
- || distance == null || distance.signum() <= 0
- || distance.compareTo(MAX_PRICING_DISTANCE) > 0
- || request.getFreight() == null || request.getFreight() <= 0
- || urgentRate == null || urgentRate.signum() < 0
- || urgentRate.compareTo(MAX_PRICING_DISTANCE) > 0
- || request.getMinimumUrgentFee() == null || request.getMinimumUrgentFee() < 0
- || requireVersion && (request.getConfigVersion() == null || request.getConfigVersion() <= 0)) {
- throw fail("flash.delivery.pricing.invalid");
- }
- }
- /** 确保新时段与既有运价不重叠,excludedId 用于修改时排除自身。 */
- private void ensureNoPricingOverlap(FlashDeliveryPricingRequest request, Long excludedId) {
- if (pricingMapper.countOverlapping(request.getStartTime(), request.getEndTime(), request.getVehicleType(), excludedId) > 0) {
- throw fail("flash.delivery.pricing.overlap");
- }
- }
- /** 将运价请求转换为实体,距离与比例统一保留两位小数。 */
- private FlashDeliveryPricing pricingFrom(FlashDeliveryPricingRequest request) {
- FlashDeliveryPricing pricing = new FlashDeliveryPricing();
- pricing.setVehicleType(request.getVehicleType());
- pricing.setStartTime(request.getStartTime());
- pricing.setEndTime(request.getEndTime());
- pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
- pricing.setStartingFare(request.getStartingFare());
- pricing.setDistance(scaleDistance(request.getDistance()));
- pricing.setFreight(request.getFreight());
- pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
- pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
- return pricing;
- }
- private boolean validStartTime(String value) {
- return value != null && value.matches("(?:[01]\\d|2[0-3]):[0-5]\\d");
- }
- private boolean validEndTime(String value) {
- return "24:00".equals(value) || validStartTime(value);
- }
- private int toMinute(String value) {
- if ("24:00".equals(value)) return 24 * 60;
- return Integer.parseInt(value.substring(0, 2)) * 60 + Integer.parseInt(value.substring(3));
- }
- private BigDecimal scaleDistance(BigDecimal value) {
- return normalizeDistance(value);
- }
- private BigDecimal normalizeDistance(BigDecimal value) {
- return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
- }
- private BigDecimal normalizeRate(BigDecimal value) {
- return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
- }
- /** 状态、图片和日志处于同一事务;任一图片写入失败会回滚本次业务操作。 */
- private void saveImages(Long orderId, String proofType, String operatorType,
- Long operatorId, List<String> urls, Date now) {
- for (int index = 0; index < urls.size(); index++) {
- FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
- image.setOrderId(orderId);
- image.setProofType(proofType);
- image.setImageUrl(urls.get(index));
- image.setSortOrder(index);
- image.setOperatorType(operatorType);
- image.setOperatorId(operatorId);
- image.setCreateTime(now);
- imageMapper.insert(image);
- }
- }
- /** 校验凭证图片 URL:必填时 1 至 9 张,每项为合法 URI 且不超过 1000 字符。 */
- private List<String> validateProofUrls(List<String> values, boolean required) {
- if (values == null || values.isEmpty()) {
- if (!required) return List.of();
- throw fail("flash.delivery.proof.required");
- }
- if (values.size() > 9) throw fail("flash.delivery.proof.too.many");
- List<String> urls = new ArrayList<>();
- for (String value : values) {
- try {
- String url = value == null ? null : value.trim();
- if (url == null || url.isEmpty() || url.length() > 1000) {
- throw new IllegalArgumentException();
- }
- URI.create(url);
- urls.add(url);
- } catch (IllegalArgumentException exception) {
- throw fail("flash.delivery.proof.url.invalid");
- }
- }
- return urls;
- }
- /** 骑手闪送平均分;尚无评价时按平台骑手展示惯例回退 4.5。 */
- private Double riderAverageStars(Long riderId) {
- Double average = orderMapper.selectRiderAverageStars(riderId);
- return average == null ? 4.5 : average;
- }
- /** 送达后线下转账单返回骑手启用卡三要素(户名/银行名/账号);无启用卡返回 null。 */
- private FlashDeliveryRiderBankCardView riderActiveCardView(Long riderId) {
- if (riderId == null) return null;
- return bankCardMapper.selectList(currentRead(new QueryWrapper<com.ruoyi.system.domain.InfoBankCard>()
- .eq("user_id", riderId).eq("is_active", 1))).stream().findFirst()
- .map(card -> new FlashDeliveryRiderBankCardView(card.getBankName(), card.getAccountNo(), card.getAccountName()))
- .orElse(null);
- }
- /** 组装用户或骑手视角详情;hideSensitiveFields 为 true 时按抢单前脱敏返回。 */
- private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
- boolean userView,
- boolean hideSensitiveFields) {
- return participantDetail(order, userView, hideSensitiveFields, userView ? order.getUserId() : null);
- }
- private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
- boolean userView,
- boolean hideSensitiveFields, Long viewerId) {
- FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
- if (!hideSensitiveFields) view.setOrderVersion(order.getVersion());
- List<FlashDeliveryOrderStop> stationRows = stops(order.getId());
- view.setCanUserCancel(canCancelBeforePickup(order, stationRows));
- List<FlashDeliveryOrderImage> stationImages = hideSensitiveFields ? List.of() : imageMapper.selectList(currentRead(
- new QueryWrapper<FlashDeliveryOrderImage>().eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
- boolean sender = userView && Objects.equals(viewerId, order.getUserId());
- boolean v2Receiver = userView && order.getDeliveryStopCount() != null && !sender;
- if (v2Receiver) {
- Set<Long> ownStopIds = stationRows.stream().filter(s -> "DELIVERY".equals(s.getStopType())
- && Objects.equals(viewerId, s.getReceiverUserId())).map(FlashDeliveryOrderStop::getId)
- .collect(java.util.stream.Collectors.toSet());
- stationImages = stationImages.stream().filter(image -> !"DELIVERY".equals(image.getProofType())
- || ownStopIds.contains(image.getStopId())).toList();
- }
- List<FlashDeliveryStopView> stationViews = stopViews(stationRows, !hideSensitiveFields, stationImages);
- if (userView && order.getDeliveryStopCount() != null) {
- for (int index = 0; index < stationRows.size(); index++) {
- FlashDeliveryOrderStop stop = stationRows.get(index);
- FlashDeliveryStopView stopView = stationViews.get(index);
- if (!"DELIVERY".equals(stop.getStopType())) continue;
- if (sender || Objects.equals(viewerId, stop.getReceiverUserId())) {
- stopView.setDeliveryPinCode(Boolean.TRUE.equals(order.getPinRequired()) ? stop.getDeliveryPinCode() : null);
- } else if (v2Receiver) {
- // 他人收货站开放文字地址、备注与交接方式(与列表口径一致),联系人、电话、坐标仍不返回。
- stopView.setName(null); stopView.setPhone(null);
- stopView.setLongitude(null); stopView.setLatitude(null);
- }
- }
- if (v2Receiver) {
- stationRows.stream().filter(s -> "DELIVERY".equals(s.getStopType())
- && Objects.equals(viewerId, s.getReceiverUserId())).findFirst().ifPresent(own -> {
- FlashDeliveryAddressView delivery = new FlashDeliveryAddressView();
- delivery.setName(own.getName()); delivery.setPhone(own.getPhone());
- delivery.setAddress(own.getAddress()); delivery.setAddressDetail(own.getAddressDetail());
- delivery.setHandoffMethod(own.getHandoffMethod());
- delivery.setLongitude(own.getLongitude()); delivery.setLatitude(own.getLatitude());
- view.setDelivery(delivery);
- });
- }
- }
- view.setStops(stationViews);
- view.setCurrentTargetStopId(targetStopId(order, stationRows));
- if (!hideSensitiveFields) {
- view.setTargetAdjustments(adjustmentMapper.selectList(currentRead(new QueryWrapper<FlashDeliveryTargetAdjustment>()
- .eq("order_id", order.getId()).orderByAsc("create_time", "id"))));
- }
- if (userView) {
- view.setDeliveryPinCode(sender && order.getDeliveryStopCount() == null
- && Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
- // 接单后直接返回骑手完整用户资料;star 填充该骑手的闪送平均分供前端展示。
- if (order.getRiderId() != null) {
- InfoUser rider = userMapper.selectInfoUserByUserId(order.getRiderId());
- if (rider != null) {
- rider.setStar(riderAverageStars(order.getRiderId()));
- }
- view.setRider(rider);
- }
- // 线下转账单送达/完成后返回骑手启用卡,供用户线下转账结清(仅寄件人视角)
- if (sender && PAY_TYPE_OFFLINE_TRANSFER.equals(order.getPayType())
- && (DELIVERED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()))) {
- view.setRiderBankCard(riderActiveCardView(order.getRiderId()));
- }
- } else if (!hideSensitiveFields) {
- // 接单后仅中单骑手可见寄件用户完整资料;抢单前脱敏视图不返回用户身份信息。
- if (order.getUserId() != null) {
- view.setUser(userMapper.selectInfoUserByUserId(order.getUserId()));
- }
- }
- if (!hideSensitiveFields && order.getId() != null) {
- groupImageUrls(view, stationImages);
- }
- return view;
- }
- /** 订单实体转骑手端任务卡片,可附带骑手到取件点的直线距离。 */
- /** 列表页一次 IN 批量取回 v2 多站点单的站点,避免逐单 N+1;单点单卡片不需要逐站展示。 */
- private Map<Long, List<FlashDeliveryOrderStop>> stopRowsFor(List<FlashDeliveryOrder> records) {
- List<Long> orderIds = records.stream()
- .filter(order -> order.getDeliveryStopCount() != null)
- .map(FlashDeliveryOrder::getId).toList();
- if (orderIds.isEmpty()) return Map.of();
- return stopMapper.selectList(new QueryWrapper<FlashDeliveryOrderStop>()
- .in("order_id", orderIds).orderByAsc("stop_order", "id")).stream()
- .collect(java.util.stream.Collectors.groupingBy(FlashDeliveryOrderStop::getOrderId));
- }
- private FlashDeliveryRiderOrderListView riderOrderListView(Long riderId, FlashDeliveryOrder order,
- BigDecimal longitude, BigDecimal latitude,
- List<FlashDeliveryOrderStop> stopRows) {
- FlashDeliveryRiderOrderListView view = new FlashDeliveryRiderOrderListView();
- view.setId(order.getId());
- view.setOrderNo(order.getOrderNo());
- view.setServiceType(order.getServiceType());
- view.setDeliveryType(order.getDeliveryType());
- view.setVehicleType(vehicleTypeOf(order));
- view.setStatus(order.getStatus());
- view.setPickupStopCount(order.getPickupStopCount());
- view.setPickedUpStopCount(order.getPickedUpStopCount());
- view.setDeliveryStopCount(expectedDeliveryStopCount(order));
- view.setDeliveredStopCount(deliveredStopCount(order));
- view.setCanUserCancel(canUserCancel(order));
- view.setPackageType(order.getPackageType());
- view.setQuantity(order.getQuantity());
- view.setWeightRange(order.getWeightRange());
- view.setSpecification(order.getSpecification());
- view.setDeliveryMode(order.getDeliveryMode());
- view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
- view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
- view.setPinRequired(order.getPinRequired());
- view.setPickupAddress(order.getPickupAddress());
- view.setPickupDetailAddress(order.getPickupAddressDetail());
- view.setDeliveryAddress(order.getDeliveryAddress());
- view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
- view.setPayType(order.getPayType());
- view.setPaymentStatus(order.getPaymentStatus());
- view.setPickupDistanceMeters(distanceMeters(longitude, latitude,
- order.getPickupLongitude(), order.getPickupLatitude()));
- view.setDistanceMeters(order.getDistanceMeters());
- view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
- view.setBaseDeliveryFee(order.getBaseDeliveryFee());
- view.setDistanceFee(order.getDistanceFee());
- view.setUrgentFee(order.getUrgentFee());
- view.setTipAmount(order.getTipAmount());
- view.setAmount(order.getAmount());
- view.setCurrency(order.getCurrency());
- view.setCreateTime(order.getCreateTime());
- // 备注仅对中单骑手本人可见;newTask 待抢卡片保持脱敏为空。
- view.setUserNote(Objects.equals(riderId, order.getRiderId()) ? order.getUserNote() : null);
- // v2 多站点单卡片逐站展示取送地址与状态;与详情同口径脱敏联系人、电话、坐标和凭证。
- if (order.getDeliveryStopCount() != null && stopRows != null && !stopRows.isEmpty()) {
- view.setStops(stopViews(stopRows, false, List.of()));
- }
- return view;
- }
- /** 订单实体转详情基础视图。 */
- private FlashDeliveryOrderDetailView detailOrderView(FlashDeliveryOrder order, boolean includeSensitiveFields) {
- FlashDeliveryOrderDetailView view = new FlashDeliveryOrderDetailView();
- populateOrderView(view, order, includeSensitiveFields);
- view.setRiderStars(order.getRiderStars());
- return view;
- }
- /** 按凭证类型把图片分组为寄件、取件、送达三组 URL。 */
- private void groupImageUrls(FlashDeliveryOrderDetailView view, List<FlashDeliveryOrderImage> images) {
- List<String> sender = new ArrayList<>();
- List<String> pickup = new ArrayList<>();
- List<String> delivery = new ArrayList<>();
- for (FlashDeliveryOrderImage image : images) {
- if ("SENDER".equals(image.getProofType())) sender.add(image.getImageUrl());
- else if ("PICKUP".equals(image.getProofType())) pickup.add(image.getImageUrl());
- else if ("DELIVERY".equals(image.getProofType())) delivery.add(image.getImageUrl());
- }
- view.setSenderImageUrls(sender);
- view.setPickupImageUrls(pickup);
- view.setDeliveryImageUrls(delivery);
- }
- /** 运价实体转首页服务摘要视图。 */
- private FlashDeliveryServiceView serviceView(FlashDeliveryPricing pricing, String serviceType) {
- FlashDeliveryServiceView view = new FlashDeliveryServiceView();
- view.setServiceType(serviceType);
- view.setPricingId(pricing.getId());
- view.setStartTime(pricing.getStartTime());
- view.setEndTime(pricing.getEndTime());
- view.setStartingDistance(pricing.getStartingDistance());
- view.setStartingFare(pricing.getStartingFare());
- view.setDistance(pricing.getDistance());
- view.setFreight(pricing.getFreight());
- view.setUrgentRate(pricing.getUrgentRate());
- view.setMinimumUrgentFee(pricing.getMinimumUrgentFee());
- view.setPricingVersion(pricing.getConfigVersion());
- view.setCurrency("TWD");
- return view;
- }
- /** 将订单实体的公共字段填充到视图,includeSensitiveFields 控制是否包含敏感字段。 */
- private void populateOrderView(FlashDeliveryOrderView view, FlashDeliveryOrder order,
- boolean includeSensitiveFields) {
- view.setId(order.getId());
- view.setOrderNo(order.getOrderNo());
- view.setServiceType(order.getServiceType());
- view.setDeliveryType(order.getDeliveryType());
- view.setVehicleType(vehicleTypeOf(order));
- view.setStatus(order.getStatus());
- view.setPickupStopCount(order.getPickupStopCount());
- view.setPickedUpStopCount(order.getPickedUpStopCount());
- view.setDeliveryStopCount(expectedDeliveryStopCount(order));
- view.setDeliveredStopCount(deliveredStopCount(order));
- view.setCanUserCancel(canUserCancel(order));
- view.setPackageType(order.getPackageType());
- view.setQuantity(order.getQuantity());
- view.setWeightRange(order.getWeightRange());
- view.setSpecification(order.getSpecification());
- view.setDeliveryMode(order.getDeliveryMode());
- view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
- view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
- view.setPinRequired(order.getPinRequired());
- view.setPickup(addressView(order, true, includeSensitiveFields));
- view.setDelivery(addressView(order, false, includeSensitiveFields));
- view.setDistanceMeters(order.getDistanceMeters());
- view.setDistanceSource(order.getDistanceSource());
- view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
- view.setBaseDeliveryFee(order.getBaseDeliveryFee());
- view.setDistanceFee(order.getDistanceFee());
- view.setUrgentFee(order.getUrgentFee());
- view.setTipAmount(order.getTipAmount());
- view.setAmount(order.getAmount());
- view.setCurrency(order.getCurrency());
- view.setPayType(order.getPayType());
- view.setPaymentStatus(order.getPaymentStatus());
- view.setUserNote(includeSensitiveFields ? order.getUserNote() : null);
- view.setAcceptedAt(order.getAcceptedAt());
- view.setPickedUpAt(order.getPickedUpAt());
- view.setDeliveredAt(order.getDeliveredAt());
- view.setCompletedAt(order.getCompletedAt());
- view.setCancelledAt(order.getCancelledAt());
- view.setCancelReason(order.getCancelReason());
- view.setCreateTime(order.getCreateTime());
- view.setUpdateTime(order.getUpdateTime());
- }
- /** 从订单实体提取取件或收件地址视图;脱敏时不返回联系人、电话和坐标。 */
- private FlashDeliveryAddressView addressView(FlashDeliveryOrder order, boolean pickup,
- boolean includeSensitiveFields) {
- FlashDeliveryAddressView view = new FlashDeliveryAddressView();
- if (pickup) {
- view.setName(includeSensitiveFields ? order.getPickupName() : null);
- view.setPhone(includeSensitiveFields ? order.getPickupPhone() : null);
- view.setAddress(order.getPickupAddress()); view.setAddressDetail(order.getPickupAddressDetail());
- view.setCity(order.getPickupCity()); view.setArea(order.getPickupArea());
- view.setHandoffMethod(order.getPickupHandoffMethod());
- view.setLongitude(includeSensitiveFields ? order.getPickupLongitude() : null);
- view.setLatitude(includeSensitiveFields ? order.getPickupLatitude() : null);
- } else {
- view.setName(includeSensitiveFields ? order.getDeliveryName() : null);
- view.setPhone(includeSensitiveFields ? order.getDeliveryPhone() : null);
- view.setAddress(order.getDeliveryAddress()); view.setAddressDetail(order.getDeliveryAddressDetail());
- view.setCity(order.getDeliveryCity()); view.setArea(order.getDeliveryArea());
- view.setHandoffMethod(order.getDeliveryHandoffMethod());
- view.setLongitude(includeSensitiveFields ? order.getDeliveryLongitude() : null);
- view.setLatitude(includeSensitiveFields ? order.getDeliveryLatitude() : null);
- }
- return view;
- }
- /** 把费用明细写入报价视图。 */
- private void applyBreakdown(FlashDeliveryQuoteView view, FlashDeliveryPriceBreakdown breakdown) {
- view.setBillableDistance(breakdown.getBillableDistance());
- view.setDistanceFee(breakdown.getDistanceFee());
- view.setBaseDeliveryFee(breakdown.getBaseDeliveryFee());
- view.setUrgentFee(breakdown.getUrgentFee());
- view.setTipAmount(breakdown.getTipAmount());
- view.setAmount(breakdown.getAmount());
- }
- /** 把请求地址复制为订单取件或收件快照字段。 */
- private void copyAddress(FlashDeliveryAddressRequest source, FlashDeliveryOrder target, boolean pickup) {
- if (pickup) {
- target.setPickupName(source.getName().trim()); target.setPickupPhone(source.getPhone().trim());
- target.setPickupAddress(source.getAddress().trim()); target.setPickupAddressDetail(trimToNull(source.getAddressDetail()));
- target.setPickupCity(trimToNull(source.getCity())); target.setPickupArea(trimToNull(source.getArea()));
- target.setPickupHandoffMethod(trimToNull(source.getHandoffMethod()));
- target.setPickupLongitude(source.getLongitude()); target.setPickupLatitude(source.getLatitude());
- } else {
- target.setDeliveryName(source.getName().trim()); target.setDeliveryPhone(source.getPhone().trim());
- target.setDeliveryAddress(source.getAddress().trim()); target.setDeliveryAddressDetail(trimToNull(source.getAddressDetail()));
- target.setDeliveryCity(trimToNull(source.getCity())); target.setDeliveryArea(trimToNull(source.getArea()));
- target.setDeliveryHandoffMethod(trimToNull(source.getHandoffMethod()));
- target.setDeliveryLongitude(source.getLongitude()); target.setDeliveryLatitude(source.getLatitude());
- }
- }
- /** 校验地址请求:必填项、长度限制和经纬度取值范围。 */
- private void validateAddress(FlashDeliveryAddressRequest address) {
- if (address == null || !hasText(address.getName()) || !hasText(address.getPhone())
- || !hasText(address.getAddress()) || address.getLatitude() == null || address.getLongitude() == null
- || address.getName().trim().length() > 64 || address.getPhone().trim().length() > 32
- || address.getAddress().trim().length() > 255
- || (address.getAddressDetail() != null && address.getAddressDetail().trim().length() > 255)
- || tooLong(address.getCity(), 64) || tooLong(address.getArea(), 64)
- || tooLong(address.getHandoffMethod(), 40)
- || address.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0
- || address.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0
- || address.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0
- || address.getLongitude().compareTo(BigDecimal.valueOf(180)) > 0) {
- throw fail("flash.delivery.address.invalid");
- }
- }
- /** 收件手机号归一化后唯一匹配普通用户作为收件人;匹配零个或多个视为无收件人。 */
- private Long resolveReceiverUserId(String phone) {
- if (!hasText(phone)) return null;
- StringBuilder normalized = new StringBuilder(phone.length());
- for (int index = 0; index < phone.length(); index++) {
- char value = phone.charAt(index);
- if (Character.isWhitespace(value) || value == '+' || value == '-'
- || value == '(' || value == ')') {
- continue;
- }
- if (value < '0' || value > '9') return null;
- normalized.append(value);
- }
- if (normalized.isEmpty()) return null;
- List<Long> matches = userMapper.selectOrdinaryUserIdsByNormalizedPhone(normalized.toString());
- return matches != null && matches.size() == 1 ? matches.get(0) : null;
- }
- /** 判断用户是否为订单寄件人或匹配收件人。 */
- private boolean isParticipant(Long userId, FlashDeliveryOrder order) {
- if (userId == null) return false;
- if (Objects.equals(userId, order.getUserId()) || Objects.equals(userId, order.getReceiverUserId())) return true;
- return order.getDeliveryStopCount() != null && stops(order.getId()).stream().anyMatch(s ->
- "DELIVERY".equals(s.getStopType()) && Objects.equals(userId, s.getReceiverUserId()));
- }
- /** 校验 token 用户为骑手(userType=2)并返回其资料。 */
- private InfoUser requireRider(Long riderId) {
- InfoUser rider = riderId == null ? null : userMapper.selectInfoUserByUserId(riderId);
- if (rider == null || !"2".equals(rider.getUserType())) throw fail("flash.delivery.rider.required");
- return rider;
- }
- /** 加载订单,不存在时统一按订单不存在报错。 */
- private FlashDeliveryOrder requireOrder(Long orderId) {
- FlashDeliveryOrder order = orderId == null ? null : orderMapper.selectById(orderId);
- if (order == null) throw fail("flash.delivery.order.not.found");
- return order;
- }
- /** 按订单编号加载订单;编号为空或不存在时按订单不存在处理。 */
- private FlashDeliveryOrder requireOrderByNo(String orderNo) {
- if (orderNo == null || orderNo.isBlank()) throw fail("flash.delivery.order.not.found");
- FlashDeliveryOrder order = orderMapper.selectOne(new QueryWrapper<FlashDeliveryOrder>()
- .eq("order_no", orderNo.trim()).last("limit 1"));
- if (order == null) throw fail("flash.delivery.order.not.found");
- return order;
- }
- /** 写入状态变更审计日志。 */
- private void writeLog(Long orderId, String from, String to, String operatorType,
- Long operatorId, String reason, Date now) {
- FlashDeliveryOrderLog log = new FlashDeliveryOrderLog();
- log.setOrderId(orderId); log.setFromStatus(from); log.setToStatus(to);
- log.setOperatorType(operatorType); log.setOperatorId(operatorId); log.setReason(reason); log.setCreateTime(now);
- logMapper.insert(log);
- }
- /** 校验并提取取消原因。 */
- private String requiredReason(FlashDeliveryReasonRequest request) {
- if (request == null || !hasText(request.getReason()) || request.getReason().trim().length() > 500) {
- throw fail("flash.delivery.cancel.reason.required");
- }
- return request.getReason().trim();
- }
- private void validateServiceType(String value) {
- if (!hasText(value) || !SERVICE_TYPES.contains(value)) throw fail("flash.delivery.service.type.invalid");
- }
- private void validateDeliveryType(String value) {
- if (!hasText(value) || !DELIVERY_TYPES.contains(value)) {
- throw fail("flash.delivery.delivery.type.invalid");
- }
- }
- /** 支付方式由 031 闸门校验;闪送维度按平台开关判定,缺省现金。 */
- private String normalizePayType(String payType) {
- String value = hasText(payType) ? payType.trim() : PAY_TYPE_CASH;
- paymentMethodGateService.assertUsable(value,
- PaymentMethodGateService.GateScope.RIDER_FLASH, null, null);
- return value;
- }
- /** 将时间转为台北时区 HH:mm,用于命中运价时段。 */
- private String pricingTargetTime(Date date) {
- return PRICING_TIME_FORMAT.format(Instant.ofEpochMilli(date.getTime())
- .atZone(TAIPEI).toLocalTime());
- }
- /** 比对客户端回传报价与服务端最新报价是否完全一致。 */
- private boolean matchesQuotedPrice(FlashDeliveryCreateRequest request, FlashDeliveryQuoteView latest) {
- return Objects.equals(request.getPricingId(), latest.getPricingId())
- && Objects.equals(request.getPricingVersion(), latest.getPricingVersion())
- && Objects.equals(request.getQuotedBaseDeliveryFee(), latest.getBaseDeliveryFee())
- && Objects.equals(request.getQuotedDistanceFee(), latest.getDistanceFee())
- && Objects.equals(request.getQuotedUrgentFee(), latest.getUrgentFee())
- && Objects.equals(request.getQuotedAmount(), latest.getAmount());
- }
- private void validateOptionalServiceType(String value) {
- if (hasText(value)) validateServiceType(value);
- }
- private void requireUserId(Long userId) {
- if (userId == null || userId <= 0) throw fail("flash.delivery.auth.required");
- }
- /** 构造分页对象并夹紧页码与每页数量(1 至 100)。 */
- private Page<FlashDeliveryOrder> page(int pageNum, int pageSize) {
- return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
- }
- /** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
- private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, Integer riderVehicleType, String tab,
- BigDecimal longitude, BigDecimal latitude,
- boolean orderResults, Set<String> acceptedPayTypes) {
- QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<>();
- Date now = new Date();
- switch (tab) {
- case "newTask" -> {
- query.eq("status", WAITING_ACCEPTANCE).isNull("rider_id").eq("is_display", true)
- .eq("vehicle_type", riderVehicleType)
- .and(item -> item.eq("delivery_mode", "NOW")
- .or(nested -> nested.le("scheduled_pickup_start_at", now)
- .gt("scheduled_pickup_end_at", now)));
- // 同时满足车型和骑手已接受的支付方式,列表不能替代接单接口的资格复核。
- if (acceptedPayTypes.isEmpty()) query.apply("1 = 0");
- else query.in("pay_type", acceptedPayTypes);
- if (longitude != null) {
- Integer limitKm = newTaskDistanceLimit();
- if (limitKm != null) {
- query.apply("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point({0}, {1})) <= {2}",
- longitude, latitude, limitKm.longValue() * 1000L);
- }
- if (orderResults) {
- query.orderByAsc("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point(" + longitude
- + ", " + latitude + "))");
- }
- } else if (orderResults) {
- query.orderByDesc("create_time");
- }
- }
- case "toPickup" -> {
- query.eq("rider_id", riderId).eq("status", ACCEPTED);
- if (orderResults) query.orderByAsc("create_time");
- }
- case "delivering" -> {
- query.eq("rider_id", riderId).eq("status", PICKED_UP);
- if (orderResults) query.orderByAsc("create_time");
- }
- case "completed" -> {
- query.eq("rider_id", riderId).in("status", DELIVERED, COMPLETED);
- if (orderResults) query.orderByDesc("create_time");
- }
- case "cancelled" -> {
- query.eq("rider_id", riderId).eq("status", CANCELLED);
- if (orderResults) query.orderByDesc("create_time");
- }
- default -> throw fail("flash.delivery.tab.invalid");
- }
- return query;
- }
- /** 骑手可抢支付方式:勾选(NULL=平台开放全部)∩ 平台开关 ∩ 就绪度,统一走闸门判定;现金单始终可见。 */
- private Set<String> acceptedRiderPayTypes(InfoUser rider) {
- Set<String> payTypes = new LinkedHashSet<>();
- payTypes.add(PAY_TYPE_CASH);
- for (PaymentMethodGateService.PayMethodOption option : paymentMethodGateService.listAvailable(
- PaymentMethodGateService.GateScope.RIDER_FLASH, null, rider.getUserId())) {
- if (option.isReady()) payTypes.addAll(PaymentMethodGateService.payTypesOf(option.getMethodCode()));
- }
- return payTypes;
- }
- /** 校验骑手坐标:经纬度必须成对出现且在合法范围。 */
- private void validateRiderCoordinates(BigDecimal longitude, BigDecimal latitude) {
- if ((longitude == null) != (latitude == null)
- || longitude != null && (longitude.compareTo(BigDecimal.valueOf(-180)) < 0
- || longitude.compareTo(BigDecimal.valueOf(180)) > 0
- || latitude.compareTo(BigDecimal.valueOf(-90)) < 0
- || latitude.compareTo(BigDecimal.valueOf(90)) > 0)) {
- throw fail("flash.delivery.coordinates.invalid");
- }
- }
- /** 读取字典 sys_qs_newtask_distance 的新任务距离上限(公里);缺失或非法时不限距离。 */
- private Integer newTaskDistanceLimit() {
- try {
- List<SysDictData> values = DictUtils.getDictCache("sys_qs_newtask_distance");
- if (values != null && !values.isEmpty()) {
- int limit = Integer.parseInt(values.get(0).getDictValue());
- return limit > 0 ? limit : null;
- }
- } catch (Exception ignored) {
- // Keep the established rider-list behaviour: absent or malformed configuration disables distance filtering.
- }
- return null;
- }
- /** Haversine 球面直线距离(米),用于骑手列表展示距取件点的距离。 */
- private Integer distanceMeters(BigDecimal longitude, BigDecimal latitude,
- BigDecimal targetLongitude, BigDecimal targetLatitude) {
- if (longitude == null || targetLongitude == null || targetLatitude == null) return null;
- double latitudeRadians = Math.toRadians(latitude.doubleValue());
- double targetLatitudeRadians = Math.toRadians(targetLatitude.doubleValue());
- double latitudeDelta = targetLatitudeRadians - latitudeRadians;
- double longitudeDelta = Math.toRadians(targetLongitude.doubleValue() - longitude.doubleValue());
- double value = Math.sin(latitudeDelta / 2) * Math.sin(latitudeDelta / 2)
- + Math.cos(latitudeRadians) * Math.cos(targetLatitudeRadians)
- * Math.sin(longitudeDelta / 2) * Math.sin(longitudeDelta / 2);
- return (int) Math.round(6_371_000D * 2D * Math.atan2(Math.sqrt(value), Math.sqrt(1D - value)));
- }
- /** 从 selectObjs 聚合结果中取第一个 Long 值。 */
- private Long firstLong(List<Object> values) {
- if (values == null || values.isEmpty() || values.get(0) == null) return null;
- Object value = values.get(0);
- if (value instanceof Number number) return number.longValue();
- return Long.valueOf(String.valueOf(value));
- }
- /** 取件方式归一化:缺省按 NOW 处理,非法值报错。 */
- private String normalizeDeliveryMode(String value) {
- String mode = hasText(value) ? value.trim() : "NOW";
- if (!DELIVERY_MODES.contains(mode)) throw fail("flash.delivery.mode.invalid");
- return mode;
- }
- /** 校验预约窗:NOW 不得携带时间;SCHEDULED 必须是未来 3 天内恰好 30 分钟的窗。 */
- private void validateSchedule(String mode, Date start, Date end, Date now) {
- if ("NOW".equals(mode)) {
- if (start != null || end != null) throw fail("flash.delivery.schedule.invalid");
- return;
- }
- if (start == null || end == null || start.before(now)
- || end.getTime() - start.getTime() != SCHEDULE_SLOT_MILLIS
- || start.getTime() - now.getTime() > MAX_SCHEDULE_DELAY_MILLIS) {
- throw fail("flash.delivery.schedule.invalid");
- }
- }
- /** 判断订单当前是否开放抢单:立即单随时可抢,预约单仅在开始(含)至结束(不含)之间可抢。 */
- private boolean isAvailableAt(FlashDeliveryOrder order, Date now) {
- return "NOW".equals(order.getDeliveryMode())
- || "SCHEDULED".equals(order.getDeliveryMode())
- && order.getScheduledPickupStartAt() != null
- && order.getScheduledPickupEndAt() != null
- && !order.getScheduledPickupStartAt().after(now)
- && order.getScheduledPickupEndAt().after(now);
- }
- private void optionalEq(QueryWrapper<FlashDeliveryOrder> query, String column, String value) {
- if (hasText(value)) query.eq(column, value.trim());
- }
- private boolean hasText(String value) { return StringUtils.isNotEmpty(value) && !value.trim().isEmpty(); }
- private String trimToNull(String value) { return hasText(value) ? value.trim() : null; }
- private boolean tooLong(String value, int max) { return value != null && value.trim().length() > max; }
- private boolean sameAddressText(FlashDeliveryAddressRequest left, FlashDeliveryAddressRequest right) {
- return normalized(left.getAddress()).equals(normalized(right.getAddress()))
- && normalized(left.getAddressDetail()).equals(normalized(right.getAddressDetail()));
- }
- private String normalized(String value) { return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); }
- private ServiceException fail(String key) { return new ServiceException(MessageUtils.message(key)); }
- /** 将旧单点输入适配为完整取货站;多点请求必须逐站申报物品。 */
- private List<FlashDeliveryPickupRequest> normalizePickups(FlashDeliveryQuoteRequest request) {
- if (request.getPickups() != null) {
- if (request.getPickup() != null || request.getPackageType() != null || request.getQuantity() != null
- || request.getWeightRange() != null || request.getSpecification() != null) {
- throw fail("flash.delivery.pickup.input.conflict");
- }
- return request.getPickups();
- }
- FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
- if (request.getPickup() == null) throw fail("flash.delivery.address.invalid");
- BeanUtils.copyProperties(request.getPickup(), pickup);
- pickup.setPackageType(request.getPackageType());
- pickup.setQuantity(request.getQuantity());
- pickup.setWeightRange(request.getWeightRange());
- pickup.setSpecification(request.getSpecification());
- return List.of(pickup);
- }
- /** 编辑提交完整站点;旧单点地址编辑保留该站原有物品信息。 */
- private List<FlashDeliveryPickupRequest> editablePickups(FlashDeliveryOrder order,
- FlashDeliveryAddressChangeRequest request) {
- List<FlashDeliveryOrderStop> existing = requireStops(order);
- if (request.getPickups() != null) {
- if (request.getPickup() != null) throw fail("flash.delivery.pickup.input.conflict");
- return request.getPickups();
- }
- List<FlashDeliveryOrderStop> pickups = pickupStops(existing);
- if (pickups.size() != 1) throw fail("flash.delivery.pickups.required");
- if (request.getPickup() == null) throw fail("flash.delivery.address.invalid");
- FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
- BeanUtils.copyProperties(pickups.get(0), pickup);
- BeanUtils.copyProperties(request.getPickup(), pickup);
- // 站点不再保存逐站物品;单点改址回退时物品取订单级摘要,与统一契约一致。
- pickup.setPackageType(order.getPackageType()); pickup.setQuantity(order.getQuantity());
- pickup.setWeightRange(order.getWeightRange()); pickup.setSpecification(order.getSpecification());
- return List.of(pickup);
- }
- /** 地图调用前先校验有效点数、开关及每站地址和物品。 */
- private void validatePickups(List<FlashDeliveryPickupRequest> pickups, FlashDeliveryAddressRequest delivery) {
- if (pickups == null || pickups.isEmpty() || pickups.size() > pickupStopLimit()) {
- throw fail("flash.delivery.pickup.limit.exceeded");
- }
- validateAddress(delivery);
- for (FlashDeliveryPickupRequest pickup : pickups) {
- validateAddress(pickup);
- if (!PACKAGE_TYPES.contains(Objects.toString(pickup.getPackageType(), ""))) {
- throw fail("flash.delivery.package.type.invalid");
- }
- // 重量与规格仅供骑手判断物品情况,用户可不填写;填写重量时仍须使用受支持的区间。
- if (pickup.getQuantity() == null || pickup.getQuantity() <= 0
- || hasText(pickup.getWeightRange()) && !WEIGHT_RANGES.contains(pickup.getWeightRange())
- || tooLong(pickup.getSpecification(), 255)) throw fail("flash.delivery.item.invalid");
- if (sameAddressText(pickup, delivery)
- || pickup.getLatitude().compareTo(delivery.getLatitude()) == 0
- && pickup.getLongitude().compareTo(delivery.getLongitude()) == 0) {
- throw fail("flash.delivery.address.same");
- }
- }
- }
- private RouteDistance pickupRoute(List<FlashDeliveryPickupRequest> pickups, FlashDeliveryAddressRequest delivery,
- Integer vehicleType) {
- List<GeoPoint> points = new ArrayList<>();
- for (FlashDeliveryPickupRequest pickup : pickups) points.add(new GeoPoint(pickup.getLatitude(), pickup.getLongitude()));
- points.add(new GeoPoint(delivery.getLatitude(), delivery.getLongitude()));
- RouteDistance route = points.size() == 2
- ? routeService.calculate(points.get(0), points.get(1), vehicleType)
- : routeService.calculate(points, vehicleType);
- if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
- return route;
- }
- private Integer requireVehicleType(Integer vehicleType) {
- if (!FlashDeliveryVehicleType.isSupported(vehicleType)) {
- throw fail("flash.delivery.vehicle.type.invalid");
- }
- return vehicleType;
- }
- /** DDL 执行前存在的旧订单按用户确认的机车规则兼容。 */
- private Integer vehicleTypeOf(FlashDeliveryOrder order) {
- return FlashDeliveryVehicleType.isSupported(order.getVehicleType())
- ? order.getVehicleType() : FlashDeliveryVehicleType.MOTORCYCLE;
- }
- /** 骑手资料复用既有 vehicleType;历史空值和非法值安全回落为机车。 */
- private Integer riderVehicleType(InfoUser rider) {
- try {
- Integer value = Integer.valueOf(rider.getVehicleType());
- return FlashDeliveryVehicleType.isSupported(value) ? value : FlashDeliveryVehicleType.MOTORCYCLE;
- } catch (Exception ignored) {
- return FlashDeliveryVehicleType.MOTORCYCLE;
- }
- }
- private boolean isRiderVehicleCompatible(InfoUser rider, FlashDeliveryOrder order) {
- return riderVehicleType(rider).equals(vehicleTypeOf(order));
- }
- /** 点数配置缺失、非整数或越界时,有效上限回退为1。 */
- private int pickupStopLimit() {
- return stopLimit("flash_pickup_stop_limit");
- }
- /** v2 收货点上限与取货点同口径,配置缺失或越界回退为1。 */
- private int deliveryStopLimit() {
- return stopLimit("flash_delivery_stop_limit");
- }
- private int stopLimit(String dictKey) {
- String value = dictionaryValue(dictKey);
- if (value == null || !value.matches("[0-9]+")) return 1;
- try {
- int limit = Integer.parseInt(value);
- return limit >= 1 && limit <= 10 ? limit : 1;
- } catch (NumberFormatException ignored) { return 1; }
- }
- private String dictionaryValue(String key) {
- try {
- List<SysDictData> values = DictUtils.getDictCache(key);
- return values == null || values.isEmpty() ? null : values.get(0).getDictValue();
- } catch (Exception ignored) { return null; }
- }
- /** 订单旧字段仅维护首站兼容摘要,不代表全部站点物品。 */
- private void copyPickupSummary(FlashDeliveryOrder order, List<FlashDeliveryPickupRequest> pickups) {
- FlashDeliveryPickupRequest first = pickups.get(0);
- copyAddress(first, order, true);
- order.setPackageType(first.getPackageType()); order.setQuantity(first.getQuantity());
- order.setWeightRange(first.getWeightRange()); order.setSpecification(trimToNull(first.getSpecification()));
- order.setPickupStopCount(pickups.size());
- }
- /** 调用方已创建订单或持有订单行锁,全部站点与订单在同一事务保存。 */
- private void saveStops(FlashDeliveryOrder order, List<FlashDeliveryPickupRequest> pickups,
- FlashDeliveryAddressRequest delivery, Date now) {
- for (int i = 0; i < pickups.size(); i++) insertStop(order.getId(), pickups.get(i), "PICKUP", i, now);
- insertStop(order.getId(), delivery, "DELIVERY", pickups.size(), now);
- }
- private void insertStop(Long orderId, FlashDeliveryAddressRequest address, String type, int index, Date now) {
- FlashDeliveryOrderStop stop = new FlashDeliveryOrderStop();
- BeanUtils.copyProperties(address, stop);
- stop.setName(address.getName().trim()); stop.setPhone(address.getPhone().trim());
- stop.setAddress(address.getAddress().trim()); stop.setAddressDetail(trimToNull(address.getAddressDetail()));
- stop.setHandoffMethod(trimToNull(address.getHandoffMethod()));
- stop.setOrderId(orderId); stop.setStopType(type); stop.setStopOrder(index);
- stop.setStatus("PENDING"); stop.setCreateTime(now);
- if (stopMapper.insert(stop) != 1) throw fail("flash.delivery.state.changed");
- }
- /** 事务内使用当前读,避免骑手资料查询建立的旧快照遮蔽锁等待期间的履约变化;GET 不加锁。 */
- private <T> QueryWrapper<T> currentRead(QueryWrapper<T> query) {
- if (TransactionSynchronizationManager.isActualTransactionActive()) {
- query.last("FOR UPDATE");
- }
- return query;
- }
- private List<FlashDeliveryOrderStop> stops(Long orderId) {
- return stopMapper.selectList(currentRead(new QueryWrapper<FlashDeliveryOrderStop>()
- .eq("order_id", orderId).orderByAsc("stop_order", "id")));
- }
- /** 履约必须有真实站点记录,不从旧测试订单的摘要字段合成站点。 */
- private List<FlashDeliveryOrderStop> requireStops(FlashDeliveryOrder order) {
- List<FlashDeliveryOrderStop> rows = stops(order.getId());
- if (rows == null || pickupStops(rows).isEmpty()
- || !Objects.equals(order.getPickupStopCount(), pickupStops(rows).size())
- || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != expectedDeliveryStopCount(order)) {
- throw fail("flash.delivery.stops.invalid");
- }
- return rows;
- }
- /** v2 多收货站订单携带 delivery_stop_count;历史订单与旧接口订单回退为单收货站。 */
- private int expectedDeliveryStopCount(FlashDeliveryOrder order) {
- return order.getDeliveryStopCount() == null ? 1 : order.getDeliveryStopCount();
- }
- private int deliveredStopCount(FlashDeliveryOrder order) {
- if (order.getDeliveryStopCount() != null) return Objects.requireNonNullElse(order.getDeliveredStopCount(), 0);
- return DELIVERED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()) ? 1 : 0;
- }
- private List<FlashDeliveryOrderStop> pickupStops(List<FlashDeliveryOrderStop> rows) {
- return rows.stream().filter(s -> "PICKUP".equals(s.getStopType())).toList();
- }
- private boolean anyPicked(List<FlashDeliveryOrderStop> rows) {
- return rows.stream().anyMatch(s -> "PICKUP".equals(s.getStopType()) && ("PICKED_UP".equals(s.getStatus()) || s.getPickedUpAt() != null));
- }
- private boolean allPicked(List<FlashDeliveryOrderStop> rows) {
- List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
- return !pickups.isEmpty() && pickups.stream().allMatch(s -> "PICKED_UP".equals(s.getStatus()));
- }
- private boolean allDelivered(List<FlashDeliveryOrderStop> rows) {
- List<FlashDeliveryOrderStop> deliveries = rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).toList();
- return !deliveries.isEmpty() && deliveries.stream().allMatch(s -> "DELIVERED".equals(s.getStatus()));
- }
- /** 选择的目标在刷新后保留;本站完成后回到原顺序最早的未完成站点。 */
- private Long targetStopId(FlashDeliveryOrder order, List<FlashDeliveryOrderStop> rows) {
- if (CANCELLED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()) || DELIVERED.equals(order.getStatus())) return null;
- if (order.getDeliveryStopCount() != null) {
- // v2 的主状态用于 Tab;实际下一站由客户原序和骑手选择决定,不能按主状态强制只取或只送。
- if (order.getCurrentTargetStopId() != null && rows.stream().anyMatch(s ->
- Objects.equals(s.getId(), order.getCurrentTargetStopId()) && "PENDING".equals(s.getStatus()))) {
- return order.getCurrentTargetStopId();
- }
- return rows.stream().filter(s -> "PENDING".equals(s.getStatus()))
- .map(FlashDeliveryOrderStop::getId).findFirst().orElse(null);
- }
- String targetType = PICKED_UP.equals(order.getStatus()) ? "DELIVERY" : "PICKUP";
- if (order.getCurrentTargetStopId() != null && rows.stream().anyMatch(s ->
- Objects.equals(s.getId(), order.getCurrentTargetStopId())
- && targetType.equals(s.getStopType()) && "PENDING".equals(s.getStatus()))) {
- return order.getCurrentTargetStopId();
- }
- return rows.stream().filter(s -> "PICKUP".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))
- .map(FlashDeliveryOrderStop::getId).findFirst().orElseGet(() -> rows.stream()
- .filter(s -> "DELIVERY".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))
- .map(FlashDeliveryOrderStop::getId).findFirst().orElse(null));
- }
- /** 事务内先锁订单并读取最新状态,再查询站点,统一取货、调整与取消的并发边界。 */
- private FlashDeliveryOrder lockOrder(Long orderId) {
- FlashDeliveryOrder order = orderId == null ? null : orderMapper.selectByIdForUpdate(orderId);
- if (order == null) throw fail("flash.delivery.order.not.found");
- return order;
- }
- private FlashDeliveryOrderStop requireStop(List<FlashDeliveryOrderStop> rows, Long stopId) {
- return rows.stream().filter(s -> Objects.equals(s.getId(), stopId)).findFirst()
- .orElseThrow(() -> fail("flash.delivery.stop.invalid"));
- }
- /** 选择前往目标只记录履约调整,不修改取件进度、金额或用户站点顺序。 */
- @Transactional
- public FlashDeliveryOrderDetailView selectTarget(Long riderId, Long orderId, Long stopId,
- FlashDeliveryTargetRequest request) {
- requireRider(riderId);
- FlashDeliveryOrder order = lockOrder(orderId);
- if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
- if (request == null || !hasText(request.getClientRequestId()) || tooLong(request.getClientRequestId(), 64)) {
- throw fail("flash.delivery.client.request.id.invalid");
- }
- String requestId = request.getClientRequestId().trim();
- FlashDeliveryTargetAdjustment previous = adjustmentMapper.selectOne(new QueryWrapper<FlashDeliveryTargetAdjustment>()
- .eq("order_id", orderId).eq("rider_id", riderId).eq("client_request_id", requestId).last("FOR UPDATE"));
- if (previous != null) {
- if (!Objects.equals(previous.getToStopId(), stopId)
- || !Objects.equals(previous.getOrderVersion(), request.getOrderVersion())
- || !Objects.equals(previous.getReason(), trimToNull(request.getReason()))
- || !Objects.equals(previous.getConsentDeclared(), Boolean.TRUE.equals(request.getConsentDeclared()))) {
- throw fail("flash.delivery.target.request.conflict");
- }
- // 重试返回当前进度,不把已过期的历史目标重新写回订单。
- return participantDetail(order, false, false);
- }
- if (!ACCEPTED.equals(order.getStatus()) && !PICKED_UP.equals(order.getStatus())) {
- throw fail("flash.delivery.transition.not.allowed");
- }
- if (request.getOrderVersion() == null || !Objects.equals(request.getOrderVersion(), order.getVersion())) {
- throw fail("flash.delivery.state.changed");
- }
- List<FlashDeliveryOrderStop> rows = requireStops(order);
- FlashDeliveryOrderStop target = requireStop(rows, stopId);
- boolean v2 = order.getDeliveryStopCount() != null;
- String targetType = PICKED_UP.equals(order.getStatus()) ? "DELIVERY" : "PICKUP";
- if (!"PENDING".equals(target.getStatus()) || (!v2 && !targetType.equals(target.getStopType()))) {
- throw fail("flash.delivery.stop.invalid");
- }
- // v2 骑手可自主调整取送先后,但没有取到任何货时不能直接选择收货站。
- if (v2 && "DELIVERY".equals(target.getStopType()) && !anyPicked(rows)) {
- throw fail("flash.delivery.transition.not.allowed");
- }
- Long before = targetStopId(order, rows);
- if (tooLong(request.getReason(), 500)) throw fail("flash.delivery.target.reason.too.long");
- Date now = new Date();
- FlashDeliveryTargetAdjustment adjustment = new FlashDeliveryTargetAdjustment();
- adjustment.setOrderId(orderId); adjustment.setFromStopId(before); adjustment.setToStopId(stopId);
- adjustment.setRiderId(riderId); adjustment.setClientRequestId(requestId); adjustment.setOrderVersion(request.getOrderVersion());
- adjustment.setReason(trimToNull(request.getReason())); adjustment.setConsentDeclared(Boolean.TRUE.equals(request.getConsentDeclared())); adjustment.setCreateTime(now);
- if (adjustmentMapper.insert(adjustment) != 1) throw fail("flash.delivery.state.changed");
- order.setCurrentTargetStopId(stopId); order.setUpdateTime(now);
- int updated = PICKED_UP.equals(order.getStatus())
- ? orderMapper.updateDeliveryFulfilment(order) : orderMapper.updateFulfilment(order);
- if (updated != 1) throw fail("flash.delivery.state.changed");
- order.setVersion(order.getVersion() + 1);
- return participantDetail(order, false, false);
- }
- /** 两种取货接口共用同一加锁流程,路径与请求体中的站点编号必须一致。 */
- @Transactional
- public void pickupAtStop(Long riderId, Long orderId, Long stopId, FlashDeliveryProofRequest request) {
- if (request == null) throw fail("flash.delivery.request.required");
- if (request.getStopId() != null && !Objects.equals(request.getStopId(), stopId)) throw fail("flash.delivery.stop.invalid");
- pickupStation(riderId, orderId, stopId, request);
- }
- private void pickupStation(Long riderId, Long orderId, Long requestedStopId, FlashDeliveryProofRequest request) {
- requireRider(riderId);
- FlashDeliveryOrder order = lockOrder(orderId);
- if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
- List<FlashDeliveryOrderStop> rows = requireStops(order);
- List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
- Long stopId = requestedStopId;
- if (stopId == null) {
- if (pickups.size() != 1) throw fail("flash.delivery.stop.id.required");
- stopId = pickups.get(0).getId();
- }
- FlashDeliveryOrderStop stop = requireStop(rows, stopId);
- if (!"PICKUP".equals(stop.getStopType())) throw fail("flash.delivery.stop.invalid");
- // 已确认站点的重试直接成功,不因当前目标或主状态已变化而重复写凭证。
- if ("PICKED_UP".equals(stop.getStatus())) return;
- if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
- // 骑手可直接确认任一待取站;当前目标只用于导航提示,不限制实际取货顺序。
- // 每站取件都在同一订单锁内验证可信资金事实,现金/转账保持原履约路径。
- if ("3".equals(order.getPayType())) flashLinePayFactService.assertPickupPaid(order);
- List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
- Date now = new Date();
- stop.setStatus("PICKED_UP"); stop.setPickedUpAt(now);
- if (stopMapper.updateById(stop) != 1) throw fail("flash.delivery.state.changed");
- saveStationImages(orderId, stopId, "PICKUP", "RIDER", riderId, urls, now);
- order.setPickedUpStopCount((int) pickups.stream().filter(s -> "PICKED_UP".equals(s.getStatus())).count());
- order.setCurrentTargetStopId(null); order.setUpdateTime(now);
- // 已送达进度独立保留;只有全部取完才从待取件 Tab 转配送中,不以是否已送过货判断。
- if (allPicked(rows)) { order.setStatus(PICKED_UP); order.setPickedUpAt(now); }
- if (orderMapper.updateFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
- writeLog(orderId, ACCEPTED, order.getStatus(), "RIDER", riderId, "PICKUP_STOP:" + stopId, now);
- if (order.getDeliveryStopCount() != null) {
- notificationService.notifyPickupStop(order, stop, order.getPickedUpStopCount(), pickups.size());
- } else if (PICKED_UP.equals(order.getStatus())) {
- notificationService.notifyPickedUp(order);
- }
- }
- private void saveStationImages(Long orderId, Long stopId, String proofType, String operatorType,
- Long operatorId, List<String> urls, Date now) {
- for (int index = 0; index < urls.size(); index++) {
- FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
- image.setOrderId(orderId); image.setStopId(stopId); image.setProofType(proofType);
- image.setOperatorType(operatorType); image.setOperatorId(operatorId); image.setImageUrl(urls.get(index));
- image.setSortOrder(index); image.setCreateTime(now);
- if (imageMapper.insert(image) != 1) throw fail("flash.delivery.state.changed");
- }
- }
- private List<FlashDeliveryStopView> stopViews(List<FlashDeliveryOrderStop> rows, boolean sensitive,
- List<FlashDeliveryOrderImage> images) {
- return rows.stream().map(stop -> {
- FlashDeliveryStopView view = new FlashDeliveryStopView();
- BeanUtils.copyProperties(stop, view);
- view.setDeliveryPinCode(null);
- if (sensitive) view.setImageUrls(images.stream().filter(i -> Objects.equals(i.getStopId(), stop.getId())
- && Objects.equals(i.getProofType(), stop.getStopType()))
- .map(FlashDeliveryOrderImage::getImageUrl).toList());
- else { view.setName(null); view.setPhone(null); view.setLongitude(null); view.setLatitude(null); }
- return view;
- }).toList();
- }
- private boolean canUserCancel(FlashDeliveryOrder order) {
- // 卡片用汇总字段显示提示,详情复用已读站点;取消接口始终在锁内核实真实站点。
- return FlashDeliveryStateMachine.canUserCancel(order.getStatus()) && order.getPickedUpAt() == null
- && Integer.valueOf(0).equals(order.getPickedUpStopCount());
- }
- /** 缺失/不一致站点安全拒绝;不能把 ACCEPTED、汇总计数或旧数据当作未取件证据。 */
- private boolean canCancelBeforePickup(FlashDeliveryOrder order, List<FlashDeliveryOrderStop> rows) {
- if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus()) || order.getPickedUpAt() != null
- || order.getPickedUpStopCount() != null && order.getPickedUpStopCount() > 0 || rows == null) return false;
- List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
- if (pickups.isEmpty() || !Objects.equals(order.getPickupStopCount(), pickups.size())
- || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != expectedDeliveryStopCount(order)
- || rows.stream().anyMatch(s -> "DELIVERED".equals(s.getStatus()))) return false;
- return !anyPicked(rows);
- }
- /** 报价中间结果:命中的运价配置、路线距离和费用明细。 */
- private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
- FlashDeliveryPriceBreakdown breakdown, List<FlashDeliveryPickupRequest> pickups) { }
- }
|