|
|
@@ -129,11 +129,13 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
|
|
|
public FlashDeliveryHomeView home() {
|
|
|
FlashDeliveryHomeView view = new FlashDeliveryHomeView();
|
|
|
- view.setPickupStopLimit(pickupStopLimit());
|
|
|
- view.setShowMultiPickup(dictionaryEnabled("flash_show_multi_pickup"));
|
|
|
- view.setDeliveryStopLimit(deliveryStopLimit());
|
|
|
- view.setShowMultiDropoff(dictionaryEnabled("flash_show_multi_dropoff"));
|
|
|
- view.setShowUrgentOption(dictionaryEnabled("flash_show_urgent_option"));
|
|
|
+ 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);
|
|
|
@@ -303,11 +305,25 @@ public class FlashDeliveryApplicationService {
|
|
|
if (!"sender".equals(selectedRole) && !"receiver".equals(selectedRole)) {
|
|
|
throw fail("flash.delivery.tab.invalid");
|
|
|
}
|
|
|
- String participantColumn = "receiver".equals(selectedRole) ? "receiver_user_id" : "user_id";
|
|
|
- QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
- .eq(participantColumn, userId).orderByDesc("create_time");
|
|
|
+ QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<>();
|
|
|
+ if ("receiver".equals(selectedRole)) {
|
|
|
+ query.and(participant -> participant.eq("receiver_user_id", userId)
|
|
|
+ .or().apply("exists (select 1 from flash_delivery_order_stop s where s.order_id = flash_delivery_order.id "
|
|
|
+ + "and s.stop_type = 'DELIVERY' and s.receiver_user_id = {0})", userId));
|
|
|
+ } else {
|
|
|
+ query.eq("user_id", userId);
|
|
|
+ }
|
|
|
+ query.orderByDesc("create_time");
|
|
|
return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), order -> {
|
|
|
FlashDeliveryUserOrderListView view = userOrderListView(order);
|
|
|
+ if ("receiver".equals(selectedRole) && order.getDeliveryStopCount() != null
|
|
|
+ && !Objects.equals(userId, order.getUserId())) {
|
|
|
+ stops(order.getId()).stream().filter(s -> "DELIVERY".equals(s.getStopType())
|
|
|
+ && Objects.equals(userId, s.getReceiverUserId())).findFirst().ifPresent(own -> {
|
|
|
+ view.setDeliveryAddress(own.getAddress());
|
|
|
+ view.setDeliveryDetailAddress(own.getAddressDetail());
|
|
|
+ });
|
|
|
+ }
|
|
|
view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && canUserCancel(order));
|
|
|
return view;
|
|
|
});
|
|
|
@@ -325,7 +341,7 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
private FlashDeliveryOrderDetailView userDetail(Long userId, FlashDeliveryOrder order) {
|
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
|
- FlashDeliveryOrderDetailView view = participantDetail(order, true, false);
|
|
|
+ FlashDeliveryOrderDetailView view = participantDetail(order, true, false, userId);
|
|
|
view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && Boolean.TRUE.equals(view.getCanUserCancel()));
|
|
|
return view;
|
|
|
}
|
|
|
@@ -356,24 +372,7 @@ public class FlashDeliveryApplicationService {
|
|
|
copyPickupSummary(order, pickups);
|
|
|
copyAddress(request.getDelivery(), order, false);
|
|
|
order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
|
|
|
- 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());
|
|
|
+ applyAddressQuote(order, quote);
|
|
|
Date now = new Date();
|
|
|
order.setUpdateTime(now);
|
|
|
// 即使上面的查询仍显示待接单,骑手也可能已接走,最终以条件 UPDATE 为准。
|
|
|
@@ -437,16 +436,12 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
|
|
|
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 = 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());
|
|
|
+ FlashDeliveryPricing pricing = pricingSnapshot(order);
|
|
|
FlashDeliveryPriceBreakdown breakdown;
|
|
|
try {
|
|
|
breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
|
|
|
@@ -466,6 +461,111 @@ public class FlashDeliveryApplicationService {
|
|
|
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) {
|
|
|
@@ -489,7 +589,8 @@ public class FlashDeliveryApplicationService {
|
|
|
public void confirmReceipt(Long userId, Long orderId) {
|
|
|
FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
|
- if (!allPicked(requireStops(order))) throw fail("flash.delivery.complete.not.allowed");
|
|
|
+ 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");
|
|
|
@@ -642,6 +743,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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");
|
|
|
}
|
|
|
@@ -671,19 +773,21 @@ public class FlashDeliveryApplicationService {
|
|
|
public void deliverAtStop(Long riderId, Long orderId, Long stopId, FlashDeliveryDeliverRequest request) {
|
|
|
requireRider(riderId);
|
|
|
FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
- if (!Objects.equals(riderId, order.getRiderId()) || !PICKED_UP.equals(order.getStatus())) {
|
|
|
- throw fail("flash.delivery.transition.not.allowed");
|
|
|
- }
|
|
|
+ if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
|
|
|
List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
|
- if (!allPicked(rows)) throw fail("flash.delivery.transition.not.allowed");
|
|
|
FlashDeliveryOrderStop stop = requireStop(rows, stopId);
|
|
|
if (!"DELIVERY".equals(stop.getStopType())) throw fail("flash.delivery.stop.invalid");
|
|
|
// 已确认站点的重试直接成功,不重复写凭证或推进计数。
|
|
|
if ("DELIVERED".equals(stop.getStatus())) return;
|
|
|
- // 优先使用按站收货码;订单级摘要字段作为回退。
|
|
|
- String expectedPin = stop.getDeliveryPinCode() != null ? stop.getDeliveryPinCode() : order.getDeliveryPinCode();
|
|
|
+ if (!PICKED_UP.equals(order.getStatus()) || !allPicked(rows)) {
|
|
|
+ throw fail("flash.delivery.transition.not.allowed");
|
|
|
+ }
|
|
|
+ if (!Objects.equals(stopId, targetStopId(order, rows))) throw fail("flash.delivery.target.mismatch");
|
|
|
+ String expectedPin = order.getDeliveryStopCount() == null
|
|
|
+ ? order.getDeliveryPinCode() : stop.getDeliveryPinCode();
|
|
|
if (Boolean.TRUE.equals(order.getPinRequired())
|
|
|
- && (request == null || !Objects.equals(expectedPin, trimToNull(request.getPinCode())))) {
|
|
|
+ && (!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);
|
|
|
@@ -693,16 +797,18 @@ public class FlashDeliveryApplicationService {
|
|
|
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.setUpdateTime(now);
|
|
|
- boolean allDelivered = rows.stream().filter(s -> "DELIVERY".equals(s.getStopType()))
|
|
|
- .allMatch(s -> "DELIVERED".equals(s.getStatus()));
|
|
|
- if (allDelivered) {
|
|
|
+ order.setCurrentTargetStopId(null); order.setUpdateTime(now);
|
|
|
+ if (allDelivered(rows)) {
|
|
|
// 末站完成才整单推进,与取件侧同一行锁与条件更新路径。
|
|
|
order.setStatus(DELIVERED); order.setDeliveredAt(now);
|
|
|
}
|
|
|
if (orderMapper.updateDeliveryFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
|
|
|
writeLog(orderId, PICKED_UP, order.getStatus(), "RIDER", riderId, "DELIVERY_STOP:" + stopId, now);
|
|
|
- if (DELIVERED.equals(order.getStatus())) notificationService.notifyDelivered(requireOrder(orderId));
|
|
|
+ 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 数组、订单级物品、独立计价 ====
|
|
|
@@ -758,8 +864,8 @@ public class FlashDeliveryApplicationService {
|
|
|
order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
|
|
|
order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
|
|
|
order.setPinRequired(pinRequired);
|
|
|
- // 订单级收货码作为按站收货码的回退摘要;v2 各收货站独立生成。
|
|
|
- order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
|
|
|
+ // v2 只使用各收货站独立的收货码,订单级字段不生成通用码。
|
|
|
+ order.setDeliveryPinCode(null);
|
|
|
FlashDeliveryV2StopRequest firstDelivery = deliveries.get(0);
|
|
|
copyAddress(firstDelivery, order, false);
|
|
|
// 订单级收件人沿用首收货站匹配,逐站收件人保存在站点表。
|
|
|
@@ -875,12 +981,6 @@ public class FlashDeliveryApplicationService {
|
|
|
|| pickups.size() > pickupStopLimit() || deliveries.size() > deliveryStopLimit()) {
|
|
|
throw fail("flash.delivery.stops.limit.exceeded");
|
|
|
}
|
|
|
- if (pickups.size() > 1 && !dictionaryEnabled("flash_show_multi_pickup")) {
|
|
|
- throw fail("flash.delivery.multi.pickup.disabled");
|
|
|
- }
|
|
|
- if (deliveries.size() > 1 && !dictionaryEnabled("flash_show_multi_dropoff")) {
|
|
|
- throw fail("flash.delivery.multi.dropoff.disabled");
|
|
|
- }
|
|
|
// 顺序即路线:先取完再送;交错取送属后续增量。
|
|
|
boolean deliverySeen = false;
|
|
|
for (FlashDeliveryV2StopRequest stop : stops) {
|
|
|
@@ -942,6 +1042,7 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 保存 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();
|
|
|
@@ -955,7 +1056,11 @@ public class FlashDeliveryApplicationService {
|
|
|
if ("DELIVERY".equals(stop.getType())) {
|
|
|
row.setReceiverUserId(resolveReceiverUserId(stop.getPhone()));
|
|
|
if (pinRequired) {
|
|
|
- row.setDeliveryPinCode(String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)));
|
|
|
+ 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");
|
|
|
@@ -1110,7 +1215,9 @@ public class FlashDeliveryApplicationService {
|
|
|
@Transactional
|
|
|
public boolean complete(Long operatorId, String operatorType, Long orderId) {
|
|
|
FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
- if (!DELIVERED.equals(order.getStatus()) || !allPicked(requireStops(order))) return false;
|
|
|
+ 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);
|
|
|
@@ -1365,20 +1472,62 @@ public class FlashDeliveryApplicationService {
|
|
|
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")));
|
|
|
- view.setStops(stopViews(stationRows, !hideSensitiveFields, stationImages));
|
|
|
+ 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.setAddress(null); stopView.setAddressDetail(null);
|
|
|
+ stopView.setHandoffMethod(null); stopView.setLongitude(null); stopView.setLatitude(null);
|
|
|
+ stopView.setRemark(null); stopView.setImageUrls(List.of());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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(Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
|
|
|
+ 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());
|
|
|
@@ -1388,7 +1537,7 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setRider(rider);
|
|
|
}
|
|
|
// 线下转账单送达/完成后返回骑手启用卡,供用户线下转账结清(仅寄件人视角)
|
|
|
- if (PAY_TYPE_OFFLINE_TRANSFER.equals(order.getPayType())
|
|
|
+ if (sender && PAY_TYPE_OFFLINE_TRANSFER.equals(order.getPayType())
|
|
|
&& (DELIVERED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()))) {
|
|
|
view.setRiderBankCard(riderActiveCardView(order.getRiderId()));
|
|
|
}
|
|
|
@@ -1415,6 +1564,8 @@ public class FlashDeliveryApplicationService {
|
|
|
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.setOrderVersion(order.getVersion());
|
|
|
view.setPackageType(order.getPackageType());
|
|
|
@@ -1451,6 +1602,8 @@ public class FlashDeliveryApplicationService {
|
|
|
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());
|
|
|
@@ -1534,6 +1687,8 @@ public class FlashDeliveryApplicationService {
|
|
|
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());
|
|
|
@@ -1655,8 +1810,10 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
/** 判断用户是否为订单寄件人或匹配收件人。 */
|
|
|
private boolean isParticipant(Long userId, FlashDeliveryOrder order) {
|
|
|
- return Objects.equals(userId, order.getUserId())
|
|
|
- || Objects.equals(userId, order.getReceiverUserId());
|
|
|
+ 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)并返回其资料。 */
|
|
|
@@ -1951,9 +2108,6 @@ public class FlashDeliveryApplicationService {
|
|
|
if (pickups == null || pickups.isEmpty() || pickups.size() > pickupStopLimit()) {
|
|
|
throw fail("flash.delivery.pickup.limit.exceeded");
|
|
|
}
|
|
|
- if (pickups.size() > 1 && !dictionaryEnabled("flash_show_multi_pickup")) {
|
|
|
- throw fail("flash.delivery.multi.pickup.disabled");
|
|
|
- }
|
|
|
validateAddress(delivery);
|
|
|
for (FlashDeliveryPickupRequest pickup : pickups) {
|
|
|
validateAddress(pickup);
|
|
|
@@ -2030,8 +2184,6 @@ public class FlashDeliveryApplicationService {
|
|
|
} catch (NumberFormatException ignored) { return 1; }
|
|
|
}
|
|
|
|
|
|
- private boolean dictionaryEnabled(String key) { return "1".equals(dictionaryValue(key)); }
|
|
|
-
|
|
|
private String dictionaryValue(String key) {
|
|
|
try {
|
|
|
List<SysDictData> values = DictUtils.getDictCache(key);
|
|
|
@@ -2095,6 +2247,11 @@ public class FlashDeliveryApplicationService {
|
|
|
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();
|
|
|
}
|
|
|
@@ -2108,16 +2265,24 @@ public class FlashDeliveryApplicationService {
|
|
|
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;
|
|
|
+ String targetType = PICKED_UP.equals(order.getStatus()) ? "DELIVERY" : "PICKUP";
|
|
|
if (order.getCurrentTargetStopId() != null && rows.stream().anyMatch(s ->
|
|
|
- Objects.equals(s.getId(), order.getCurrentTargetStopId()) && "PICKUP".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))) {
|
|
|
+ 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())).map(FlashDeliveryOrderStop::getId).findFirst().orElse(null));
|
|
|
+ .filter(s -> "DELIVERY".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))
|
|
|
+ .map(FlashDeliveryOrderStop::getId).findFirst().orElse(null));
|
|
|
}
|
|
|
|
|
|
/** 事务内先锁订单并读取最新状态,再查询站点,统一取货、调整与取消的并发边界。 */
|
|
|
@@ -2155,13 +2320,18 @@ public class FlashDeliveryApplicationService {
|
|
|
// 重试返回当前进度,不把已过期的历史目标重新写回订单。
|
|
|
return participantDetail(order, false, false);
|
|
|
}
|
|
|
- if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
|
|
|
+ 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);
|
|
|
- if (!"PICKUP".equals(target.getStopType()) || !"PENDING".equals(target.getStatus())) throw fail("flash.delivery.stop.invalid");
|
|
|
+ String targetType = PICKED_UP.equals(order.getStatus()) ? "DELIVERY" : "PICKUP";
|
|
|
+ if (!targetType.equals(target.getStopType()) || !"PENDING".equals(target.getStatus())) {
|
|
|
+ throw fail("flash.delivery.stop.invalid");
|
|
|
+ }
|
|
|
Long before = targetStopId(order, rows);
|
|
|
if (!Objects.equals(before, stopId) && (!hasText(request.getReason()) || tooLong(request.getReason(), 500)
|
|
|
|| !Boolean.TRUE.equals(request.getConsentDeclared()))) throw fail("flash.delivery.target.consent.required");
|
|
|
@@ -2173,7 +2343,9 @@ public class FlashDeliveryApplicationService {
|
|
|
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);
|
|
|
- if (orderMapper.updateFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
|
|
|
+ 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);
|
|
|
}
|
|
|
@@ -2215,8 +2387,11 @@ public class FlashDeliveryApplicationService {
|
|
|
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 (PICKED_UP.equals(order.getStatus())) notificationService.notifyPickedUp(order);
|
|
|
+ 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,
|
|
|
@@ -2235,6 +2410,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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());
|