|
@@ -38,7 +38,6 @@ import java.time.ZoneId;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
-import java.util.function.Function;
|
|
|
|
|
|
|
|
|
|
import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
|
|
import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
|
|
|
|
|
|
|
@@ -129,9 +128,13 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
|
|
/** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
|
|
|
public FlashDeliveryHomeView home() {
|
|
public FlashDeliveryHomeView home() {
|
|
|
FlashDeliveryHomeView view = new FlashDeliveryHomeView();
|
|
FlashDeliveryHomeView view = new FlashDeliveryHomeView();
|
|
|
- view.setPickupStopLimit(pickupStopLimit());
|
|
|
|
|
- view.setShowMultiPickup(dictionaryEnabled("flash_show_multi_pickup"));
|
|
|
|
|
- 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.setServiceTypes(SERVICE_TYPES);
|
|
|
view.setDeliveryTypes(List.of("NORMAL", "URGENT"));
|
|
view.setDeliveryTypes(List.of("NORMAL", "URGENT"));
|
|
|
FlashDeliveryPricing pricing = findPricingAtOrNull(pricingTargetTime(new Date()), FlashDeliveryVehicleType.MOTORCYCLE);
|
|
FlashDeliveryPricing pricing = findPricingAtOrNull(pricingTargetTime(new Date()), FlashDeliveryVehicleType.MOTORCYCLE);
|
|
@@ -295,22 +298,6 @@ public class FlashDeliveryApplicationService {
|
|
|
return message != null && message.contains("uk_flash_order_no");
|
|
return message != null && message.contains("uk_flash_order_no");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 分页查询当前用户参与的订单卡片;role=sender 按寄件人、receiver 按收件人过滤。 */
|
|
|
|
|
- public IPage<FlashDeliveryUserOrderListView> userOrders(Long userId, int pageNum, int pageSize, String role) {
|
|
|
|
|
- String selectedRole = hasText(role) ? role.trim().toLowerCase(Locale.ROOT) : "sender";
|
|
|
|
|
- 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");
|
|
|
|
|
- return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), order -> {
|
|
|
|
|
- FlashDeliveryUserOrderListView view = userOrderListView(order);
|
|
|
|
|
- view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && canUserCancel(order));
|
|
|
|
|
- return view;
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/** 查询本人订单详情,仅寄件人和匹配到的收件人可见。 */
|
|
/** 查询本人订单详情,仅寄件人和匹配到的收件人可见。 */
|
|
|
public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
|
|
public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
|
|
|
return userDetail(userId, requireOrder(orderId));
|
|
return userDetail(userId, requireOrder(orderId));
|
|
@@ -323,7 +310,7 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
|
|
private FlashDeliveryOrderDetailView userDetail(Long userId, FlashDeliveryOrder order) {
|
|
private FlashDeliveryOrderDetailView userDetail(Long userId, FlashDeliveryOrder order) {
|
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
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()));
|
|
view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && Boolean.TRUE.equals(view.getCanUserCancel()));
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
@@ -354,24 +341,7 @@ public class FlashDeliveryApplicationService {
|
|
|
copyPickupSummary(order, pickups);
|
|
copyPickupSummary(order, pickups);
|
|
|
copyAddress(request.getDelivery(), order, false);
|
|
copyAddress(request.getDelivery(), order, false);
|
|
|
order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
|
|
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();
|
|
Date now = new Date();
|
|
|
order.setUpdateTime(now);
|
|
order.setUpdateTime(now);
|
|
|
// 即使上面的查询仍显示待接单,骑手也可能已接走,最终以条件 UPDATE 为准。
|
|
// 即使上面的查询仍显示待接单,骑手也可能已接走,最终以条件 UPDATE 为准。
|
|
@@ -435,16 +405,12 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
|
|
/** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
|
|
|
private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
|
|
private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
|
|
|
FlashDeliveryAddressChangeRequest request) {
|
|
FlashDeliveryAddressChangeRequest request) {
|
|
|
|
|
+ if (order.getDeliveryStopCount() != null) throw fail("flash.delivery.edit.not.allowed");
|
|
|
List<FlashDeliveryPickupRequest> pickups = editablePickups(order, request);
|
|
List<FlashDeliveryPickupRequest> pickups = editablePickups(order, request);
|
|
|
validatePickups(pickups, request.getDelivery());
|
|
validatePickups(pickups, request.getDelivery());
|
|
|
RouteDistance route = pickupRoute(pickups, request.getDelivery(), vehicleTypeOf(order));
|
|
RouteDistance route = pickupRoute(pickups, request.getDelivery(), vehicleTypeOf(order));
|
|
|
// FR-074/093:改址沿用下单快照,只重算整条路线,平台后续调价不影响此单。
|
|
// 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;
|
|
FlashDeliveryPriceBreakdown breakdown;
|
|
|
try {
|
|
try {
|
|
|
breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
|
|
breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
|
|
@@ -464,6 +430,111 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
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
|
|
@Transactional
|
|
|
public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
@@ -487,7 +558,8 @@ public class FlashDeliveryApplicationService {
|
|
|
public void confirmReceipt(Long userId, Long orderId) {
|
|
public void confirmReceipt(Long userId, Long orderId) {
|
|
|
FlashDeliveryOrder order = lockOrder(orderId);
|
|
FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
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();
|
|
Date now = new Date();
|
|
|
if (orderMapper.transitionByParticipant(orderId, userId, DELIVERED, COMPLETED, now) != 1) {
|
|
if (orderMapper.transitionByParticipant(orderId, userId, DELIVERED, COMPLETED, now) != 1) {
|
|
|
throw fail("flash.delivery.complete.not.allowed");
|
|
throw fail("flash.delivery.complete.not.allowed");
|
|
@@ -536,9 +608,11 @@ public class FlashDeliveryApplicationService {
|
|
|
QueryWrapper<FlashDeliveryOrder> query = riderOrdersQuery(riderId, riderVehicleType, selectedTab,
|
|
QueryWrapper<FlashDeliveryOrder> query = riderOrdersQuery(riderId, riderVehicleType, selectedTab,
|
|
|
effectiveLongitude, effectiveLatitude, true, acceptedPayTypes);
|
|
effectiveLongitude, effectiveLatitude, true, acceptedPayTypes);
|
|
|
IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
|
|
+ Map<Long, List<FlashDeliveryOrderStop>> stopRows = stopRowsFor(orders.getRecords());
|
|
|
FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
|
|
FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
|
|
|
result.setRecords(orders.getRecords().stream()
|
|
result.setRecords(orders.getRecords().stream()
|
|
|
- .map(order -> riderOrderListView(riderId, order, effectiveLongitude, effectiveLatitude)).toList());
|
|
|
|
|
|
|
+ .map(order -> riderOrderListView(riderId, order, effectiveLongitude, effectiveLatitude,
|
|
|
|
|
+ stopRows.get(order.getId()))).toList());
|
|
|
result.setTotal(orders.getTotal());
|
|
result.setTotal(orders.getTotal());
|
|
|
result.setCurrent(orders.getCurrent());
|
|
result.setCurrent(orders.getCurrent());
|
|
|
result.setSize(orders.getSize());
|
|
result.setSize(orders.getSize());
|
|
@@ -552,6 +626,30 @@ public class FlashDeliveryApplicationService {
|
|
|
return result;
|
|
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);接单后仅中单骑手可见完整信息。 */
|
|
/** 骑手任务详情:抢单前返回脱敏视图(隐藏联系人、电话、坐标和 PIN);接单后仅中单骑手可见完整信息。 */
|
|
|
public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
|
|
public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
|
|
|
return riderDetail(riderId, requireOrder(orderId));
|
|
return riderDetail(riderId, requireOrder(orderId));
|
|
@@ -616,6 +714,7 @@ public class FlashDeliveryApplicationService {
|
|
|
public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
|
|
public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
FlashDeliveryOrder order = lockOrder(orderId);
|
|
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())) {
|
|
if (!Objects.equals(riderId, order.getRiderId()) || !PICKED_UP.equals(order.getStatus())) {
|
|
|
throw fail("flash.delivery.transition.not.allowed");
|
|
throw fail("flash.delivery.transition.not.allowed");
|
|
|
}
|
|
}
|
|
@@ -640,6 +739,312 @@ public class FlashDeliveryApplicationService {
|
|
|
notificationService.notifyDelivered(requireOrder(orderId));
|
|
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() {
|
|
public List<FlashDeliveryPricing> adminPricing() {
|
|
|
return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
|
|
return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
|
|
@@ -788,7 +1193,9 @@ public class FlashDeliveryApplicationService {
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public boolean complete(Long operatorId, String operatorType, Long orderId) {
|
|
public boolean complete(Long operatorId, String operatorType, Long orderId) {
|
|
|
FlashDeliveryOrder order = lockOrder(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();
|
|
Date now = new Date();
|
|
|
if (orderMapper.transitionByStatus(orderId, DELIVERED, COMPLETED, "completed_at", now) != 1) return false;
|
|
if (orderMapper.transitionByStatus(orderId, DELIVERED, COMPLETED, "completed_at", now) != 1) return false;
|
|
|
writeLog(orderId, DELIVERED, COMPLETED, operatorType, operatorId, null, now);
|
|
writeLog(orderId, DELIVERED, COMPLETED, operatorType, operatorId, null, now);
|
|
@@ -1043,20 +1450,62 @@ public class FlashDeliveryApplicationService {
|
|
|
private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
|
|
private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
|
|
|
boolean userView,
|
|
boolean userView,
|
|
|
boolean hideSensitiveFields) {
|
|
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);
|
|
FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
|
|
|
if (!hideSensitiveFields) view.setOrderVersion(order.getVersion());
|
|
if (!hideSensitiveFields) view.setOrderVersion(order.getVersion());
|
|
|
List<FlashDeliveryOrderStop> stationRows = stops(order.getId());
|
|
List<FlashDeliveryOrderStop> stationRows = stops(order.getId());
|
|
|
view.setCanUserCancel(canCancelBeforePickup(order, stationRows));
|
|
view.setCanUserCancel(canCancelBeforePickup(order, stationRows));
|
|
|
List<FlashDeliveryOrderImage> stationImages = hideSensitiveFields ? List.of() : imageMapper.selectList(currentRead(
|
|
List<FlashDeliveryOrderImage> stationImages = hideSensitiveFields ? List.of() : imageMapper.selectList(currentRead(
|
|
|
new QueryWrapper<FlashDeliveryOrderImage>().eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
|
|
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.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));
|
|
view.setCurrentTargetStopId(targetStopId(order, stationRows));
|
|
|
if (!hideSensitiveFields) {
|
|
if (!hideSensitiveFields) {
|
|
|
view.setTargetAdjustments(adjustmentMapper.selectList(currentRead(new QueryWrapper<FlashDeliveryTargetAdjustment>()
|
|
view.setTargetAdjustments(adjustmentMapper.selectList(currentRead(new QueryWrapper<FlashDeliveryTargetAdjustment>()
|
|
|
.eq("order_id", order.getId()).orderByAsc("create_time", "id"))));
|
|
.eq("order_id", order.getId()).orderByAsc("create_time", "id"))));
|
|
|
}
|
|
}
|
|
|
if (userView) {
|
|
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 填充该骑手的闪送平均分供前端展示。
|
|
// 接单后直接返回骑手完整用户资料;star 填充该骑手的闪送平均分供前端展示。
|
|
|
if (order.getRiderId() != null) {
|
|
if (order.getRiderId() != null) {
|
|
|
InfoUser rider = userMapper.selectInfoUserByUserId(order.getRiderId());
|
|
InfoUser rider = userMapper.selectInfoUserByUserId(order.getRiderId());
|
|
@@ -1066,7 +1515,7 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setRider(rider);
|
|
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()))) {
|
|
&& (DELIVERED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()))) {
|
|
|
view.setRiderBankCard(riderActiveCardView(order.getRiderId()));
|
|
view.setRiderBankCard(riderActiveCardView(order.getRiderId()));
|
|
|
}
|
|
}
|
|
@@ -1082,44 +1531,21 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 订单实体转用户端订单卡片。 */
|
|
|
|
|
- private FlashDeliveryUserOrderListView userOrderListView(FlashDeliveryOrder order) {
|
|
|
|
|
- FlashDeliveryUserOrderListView view = new FlashDeliveryUserOrderListView();
|
|
|
|
|
- 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.setCanUserCancel(canUserCancel(order));
|
|
|
|
|
- view.setOrderVersion(order.getVersion());
|
|
|
|
|
- view.setPackageType(order.getPackageType());
|
|
|
|
|
- view.setQuantity(order.getQuantity());
|
|
|
|
|
- view.setWeightRange(order.getWeightRange());
|
|
|
|
|
- view.setDeliveryMode(order.getDeliveryMode());
|
|
|
|
|
- view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
|
|
|
|
|
- view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
|
|
|
|
|
- view.setPickupAddress(order.getPickupAddress());
|
|
|
|
|
- view.setPickupDetailAddress(order.getPickupAddressDetail());
|
|
|
|
|
- view.setDeliveryAddress(order.getDeliveryAddress());
|
|
|
|
|
- view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
|
|
|
|
|
- view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
|
|
|
|
|
- view.setBaseDeliveryFee(order.getBaseDeliveryFee());
|
|
|
|
|
- view.setUrgentFee(order.getUrgentFee());
|
|
|
|
|
- view.setTipAmount(order.getTipAmount());
|
|
|
|
|
- view.setAmount(order.getAmount());
|
|
|
|
|
- view.setCurrency(order.getCurrency());
|
|
|
|
|
- view.setPaymentStatus(order.getPaymentStatus());
|
|
|
|
|
- view.setDeliveredAt(order.getDeliveredAt());
|
|
|
|
|
- view.setCreateTime(order.getCreateTime());
|
|
|
|
|
- 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,
|
|
private FlashDeliveryRiderOrderListView riderOrderListView(Long riderId, FlashDeliveryOrder order,
|
|
|
- BigDecimal longitude, BigDecimal latitude) {
|
|
|
|
|
|
|
+ BigDecimal longitude, BigDecimal latitude,
|
|
|
|
|
+ List<FlashDeliveryOrderStop> stopRows) {
|
|
|
FlashDeliveryRiderOrderListView view = new FlashDeliveryRiderOrderListView();
|
|
FlashDeliveryRiderOrderListView view = new FlashDeliveryRiderOrderListView();
|
|
|
view.setId(order.getId());
|
|
view.setId(order.getId());
|
|
|
view.setOrderNo(order.getOrderNo());
|
|
view.setOrderNo(order.getOrderNo());
|
|
@@ -1129,6 +1555,8 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setStatus(order.getStatus());
|
|
view.setStatus(order.getStatus());
|
|
|
view.setPickupStopCount(order.getPickupStopCount());
|
|
view.setPickupStopCount(order.getPickupStopCount());
|
|
|
view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
|
|
|
+ view.setDeliveryStopCount(expectedDeliveryStopCount(order));
|
|
|
|
|
+ view.setDeliveredStopCount(deliveredStopCount(order));
|
|
|
view.setCanUserCancel(canUserCancel(order));
|
|
view.setCanUserCancel(canUserCancel(order));
|
|
|
view.setPackageType(order.getPackageType());
|
|
view.setPackageType(order.getPackageType());
|
|
|
view.setQuantity(order.getQuantity());
|
|
view.setQuantity(order.getQuantity());
|
|
@@ -1157,6 +1585,10 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setCreateTime(order.getCreateTime());
|
|
view.setCreateTime(order.getCreateTime());
|
|
|
// 备注仅对中单骑手本人可见;newTask 待抢卡片保持脱敏为空。
|
|
// 备注仅对中单骑手本人可见;newTask 待抢卡片保持脱敏为空。
|
|
|
view.setUserNote(Objects.equals(riderId, order.getRiderId()) ? order.getUserNote() : null);
|
|
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;
|
|
return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1212,6 +1644,8 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setStatus(order.getStatus());
|
|
view.setStatus(order.getStatus());
|
|
|
view.setPickupStopCount(order.getPickupStopCount());
|
|
view.setPickupStopCount(order.getPickupStopCount());
|
|
|
view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
|
|
|
+ view.setDeliveryStopCount(expectedDeliveryStopCount(order));
|
|
|
|
|
+ view.setDeliveredStopCount(deliveredStopCount(order));
|
|
|
view.setCanUserCancel(canUserCancel(order));
|
|
view.setCanUserCancel(canUserCancel(order));
|
|
|
view.setPackageType(order.getPackageType());
|
|
view.setPackageType(order.getPackageType());
|
|
|
view.setQuantity(order.getQuantity());
|
|
view.setQuantity(order.getQuantity());
|
|
@@ -1333,8 +1767,10 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
|
|
/** 判断用户是否为订单寄件人或匹配收件人。 */
|
|
/** 判断用户是否为订单寄件人或匹配收件人。 */
|
|
|
private boolean isParticipant(Long userId, FlashDeliveryOrder order) {
|
|
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)并返回其资料。 */
|
|
/** 校验 token 用户为骑手(userType=2)并返回其资料。 */
|
|
@@ -1424,14 +1860,6 @@ public class FlashDeliveryApplicationService {
|
|
|
return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
|
|
return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 分页结果实体转换,保留分页元数据。 */
|
|
|
|
|
- private <T> IPage<T> mapPage(IPage<FlashDeliveryOrder> source,
|
|
|
|
|
- Function<FlashDeliveryOrder, T> mapper) {
|
|
|
|
|
- Page<T> result = new Page<>(source.getCurrent(), source.getSize(), source.getTotal());
|
|
|
|
|
- result.setRecords(source.getRecords().stream().map(mapper).toList());
|
|
|
|
|
- return result;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
|
|
/** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
|
|
|
private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, Integer riderVehicleType, String tab,
|
|
private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, Integer riderVehicleType, String tab,
|
|
|
BigDecimal longitude, BigDecimal latitude,
|
|
BigDecimal longitude, BigDecimal latitude,
|
|
@@ -1618,6 +2046,9 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
|
|
FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
|
|
|
BeanUtils.copyProperties(pickups.get(0), pickup);
|
|
BeanUtils.copyProperties(pickups.get(0), pickup);
|
|
|
BeanUtils.copyProperties(request.getPickup(), 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);
|
|
return List.of(pickup);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1626,9 +2057,6 @@ public class FlashDeliveryApplicationService {
|
|
|
if (pickups == null || pickups.isEmpty() || pickups.size() > pickupStopLimit()) {
|
|
if (pickups == null || pickups.isEmpty() || pickups.size() > pickupStopLimit()) {
|
|
|
throw fail("flash.delivery.pickup.limit.exceeded");
|
|
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);
|
|
validateAddress(delivery);
|
|
|
for (FlashDeliveryPickupRequest pickup : pickups) {
|
|
for (FlashDeliveryPickupRequest pickup : pickups) {
|
|
|
validateAddress(pickup);
|
|
validateAddress(pickup);
|
|
@@ -1688,16 +2116,23 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
|
|
/** 点数配置缺失、非整数或越界时,有效上限回退为1。 */
|
|
/** 点数配置缺失、非整数或越界时,有效上限回退为1。 */
|
|
|
private int pickupStopLimit() {
|
|
private int pickupStopLimit() {
|
|
|
- String value = dictionaryValue("flash_pickup_stop_limit");
|
|
|
|
|
|
|
+ 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;
|
|
if (value == null || !value.matches("[0-9]+")) return 1;
|
|
|
try {
|
|
try {
|
|
|
int limit = Integer.parseInt(value);
|
|
int limit = Integer.parseInt(value);
|
|
|
- return limit >= 1 && limit <= 26 ? limit : 1;
|
|
|
|
|
|
|
+ return limit >= 1 && limit <= 10 ? limit : 1;
|
|
|
} catch (NumberFormatException ignored) { return 1; }
|
|
} catch (NumberFormatException ignored) { return 1; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private boolean dictionaryEnabled(String key) { return "1".equals(dictionaryValue(key)); }
|
|
|
|
|
-
|
|
|
|
|
private String dictionaryValue(String key) {
|
|
private String dictionaryValue(String key) {
|
|
|
try {
|
|
try {
|
|
|
List<SysDictData> values = DictUtils.getDictCache(key);
|
|
List<SysDictData> values = DictUtils.getDictCache(key);
|
|
@@ -1726,8 +2161,7 @@ public class FlashDeliveryApplicationService {
|
|
|
BeanUtils.copyProperties(address, stop);
|
|
BeanUtils.copyProperties(address, stop);
|
|
|
stop.setName(address.getName().trim()); stop.setPhone(address.getPhone().trim());
|
|
stop.setName(address.getName().trim()); stop.setPhone(address.getPhone().trim());
|
|
|
stop.setAddress(address.getAddress().trim()); stop.setAddressDetail(trimToNull(address.getAddressDetail()));
|
|
stop.setAddress(address.getAddress().trim()); stop.setAddressDetail(trimToNull(address.getAddressDetail()));
|
|
|
- stop.setCity(trimToNull(address.getCity())); stop.setArea(trimToNull(address.getArea()));
|
|
|
|
|
- stop.setHandoffMethod(trimToNull(address.getHandoffMethod())); stop.setSpecification(trimToNull(stop.getSpecification()));
|
|
|
|
|
|
|
+ stop.setHandoffMethod(trimToNull(address.getHandoffMethod()));
|
|
|
stop.setOrderId(orderId); stop.setStopType(type); stop.setStopOrder(index);
|
|
stop.setOrderId(orderId); stop.setStopType(type); stop.setStopOrder(index);
|
|
|
stop.setStatus("PENDING"); stop.setCreateTime(now);
|
|
stop.setStatus("PENDING"); stop.setCreateTime(now);
|
|
|
if (stopMapper.insert(stop) != 1) throw fail("flash.delivery.state.changed");
|
|
if (stopMapper.insert(stop) != 1) throw fail("flash.delivery.state.changed");
|
|
@@ -1751,12 +2185,22 @@ public class FlashDeliveryApplicationService {
|
|
|
List<FlashDeliveryOrderStop> rows = stops(order.getId());
|
|
List<FlashDeliveryOrderStop> rows = stops(order.getId());
|
|
|
if (rows == null || pickupStops(rows).isEmpty()
|
|
if (rows == null || pickupStops(rows).isEmpty()
|
|
|
|| !Objects.equals(order.getPickupStopCount(), pickupStops(rows).size())
|
|
|| !Objects.equals(order.getPickupStopCount(), pickupStops(rows).size())
|
|
|
- || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != 1) {
|
|
|
|
|
|
|
+ || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != expectedDeliveryStopCount(order)) {
|
|
|
throw fail("flash.delivery.stops.invalid");
|
|
throw fail("flash.delivery.stops.invalid");
|
|
|
}
|
|
}
|
|
|
return rows;
|
|
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) {
|
|
private List<FlashDeliveryOrderStop> pickupStops(List<FlashDeliveryOrderStop> rows) {
|
|
|
return rows.stream().filter(s -> "PICKUP".equals(s.getStopType())).toList();
|
|
return rows.stream().filter(s -> "PICKUP".equals(s.getStopType())).toList();
|
|
|
}
|
|
}
|
|
@@ -1770,16 +2214,33 @@ public class FlashDeliveryApplicationService {
|
|
|
return !pickups.isEmpty() && pickups.stream().allMatch(s -> "PICKED_UP".equals(s.getStatus()));
|
|
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) {
|
|
private Long targetStopId(FlashDeliveryOrder order, List<FlashDeliveryOrderStop> rows) {
|
|
|
if (CANCELLED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()) || DELIVERED.equals(order.getStatus())) return null;
|
|
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 ->
|
|
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 order.getCurrentTargetStopId();
|
|
|
}
|
|
}
|
|
|
return rows.stream().filter(s -> "PICKUP".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))
|
|
return rows.stream().filter(s -> "PICKUP".equals(s.getStopType()) && "PENDING".equals(s.getStatus()))
|
|
|
.map(FlashDeliveryOrderStop::getId).findFirst().orElseGet(() -> rows.stream()
|
|
.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));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 事务内先锁订单并读取最新状态,再查询站点,统一取货、调整与取消的并发边界。 */
|
|
/** 事务内先锁订单并读取最新状态,再查询站点,统一取货、调整与取消的并发边界。 */
|
|
@@ -1817,17 +2278,25 @@ public class FlashDeliveryApplicationService {
|
|
|
// 重试返回当前进度,不把已过期的历史目标重新写回订单。
|
|
// 重试返回当前进度,不把已过期的历史目标重新写回订单。
|
|
|
return participantDetail(order, false, false);
|
|
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())) {
|
|
if (request.getOrderVersion() == null || !Objects.equals(request.getOrderVersion(), order.getVersion())) {
|
|
|
throw fail("flash.delivery.state.changed");
|
|
throw fail("flash.delivery.state.changed");
|
|
|
}
|
|
}
|
|
|
List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
|
FlashDeliveryOrderStop target = requireStop(rows, stopId);
|
|
FlashDeliveryOrderStop target = requireStop(rows, stopId);
|
|
|
- if (!"PICKUP".equals(target.getStopType()) || !"PENDING".equals(target.getStatus())) throw fail("flash.delivery.stop.invalid");
|
|
|
|
|
|
|
+ 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);
|
|
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");
|
|
|
|
|
- if (tooLong(request.getReason(), 500)) throw fail("flash.delivery.target.consent.required");
|
|
|
|
|
|
|
+ if (tooLong(request.getReason(), 500)) throw fail("flash.delivery.target.reason.too.long");
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
FlashDeliveryTargetAdjustment adjustment = new FlashDeliveryTargetAdjustment();
|
|
FlashDeliveryTargetAdjustment adjustment = new FlashDeliveryTargetAdjustment();
|
|
|
adjustment.setOrderId(orderId); adjustment.setFromStopId(before); adjustment.setToStopId(stopId);
|
|
adjustment.setOrderId(orderId); adjustment.setFromStopId(before); adjustment.setToStopId(stopId);
|
|
@@ -1835,7 +2304,9 @@ public class FlashDeliveryApplicationService {
|
|
|
adjustment.setReason(trimToNull(request.getReason())); adjustment.setConsentDeclared(Boolean.TRUE.equals(request.getConsentDeclared())); adjustment.setCreateTime(now);
|
|
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");
|
|
if (adjustmentMapper.insert(adjustment) != 1) throw fail("flash.delivery.state.changed");
|
|
|
order.setCurrentTargetStopId(stopId); order.setUpdateTime(now);
|
|
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);
|
|
order.setVersion(order.getVersion() + 1);
|
|
|
return participantDetail(order, false, false);
|
|
return participantDetail(order, false, false);
|
|
|
}
|
|
}
|
|
@@ -1864,7 +2335,7 @@ public class FlashDeliveryApplicationService {
|
|
|
// 已确认站点的重试直接成功,不因当前目标或主状态已变化而重复写凭证。
|
|
// 已确认站点的重试直接成功,不因当前目标或主状态已变化而重复写凭证。
|
|
|
if ("PICKED_UP".equals(stop.getStatus())) return;
|
|
if ("PICKED_UP".equals(stop.getStatus())) return;
|
|
|
if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
|
|
if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
|
|
|
- if (!Objects.equals(stopId, targetStopId(order, rows))) throw fail("flash.delivery.target.mismatch");
|
|
|
|
|
|
|
+ // 骑手可直接确认任一待取站;当前目标只用于导航提示,不限制实际取货顺序。
|
|
|
// 每站取件都在同一订单锁内验证可信资金事实,现金/转账保持原履约路径。
|
|
// 每站取件都在同一订单锁内验证可信资金事实,现金/转账保持原履约路径。
|
|
|
if ("3".equals(order.getPayType())) flashLinePayFactService.assertPickupPaid(order);
|
|
if ("3".equals(order.getPayType())) flashLinePayFactService.assertPickupPaid(order);
|
|
|
List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
@@ -1874,11 +2345,15 @@ public class FlashDeliveryApplicationService {
|
|
|
saveStationImages(orderId, stopId, "PICKUP", "RIDER", riderId, urls, now);
|
|
saveStationImages(orderId, stopId, "PICKUP", "RIDER", riderId, urls, now);
|
|
|
order.setPickedUpStopCount((int) pickups.stream().filter(s -> "PICKED_UP".equals(s.getStatus())).count());
|
|
order.setPickedUpStopCount((int) pickups.stream().filter(s -> "PICKED_UP".equals(s.getStatus())).count());
|
|
|
order.setCurrentTargetStopId(null); order.setUpdateTime(now);
|
|
order.setCurrentTargetStopId(null); order.setUpdateTime(now);
|
|
|
|
|
+ // 已送达进度独立保留;只有全部取完才从待取件 Tab 转配送中,不以是否已送过货判断。
|
|
|
if (allPicked(rows)) { order.setStatus(PICKED_UP); order.setPickedUpAt(now); }
|
|
if (allPicked(rows)) { order.setStatus(PICKED_UP); order.setPickedUpAt(now); }
|
|
|
if (orderMapper.updateFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
|
|
if (orderMapper.updateFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
|
|
|
writeLog(orderId, ACCEPTED, order.getStatus(), "RIDER", riderId, "PICKUP_STOP:" + stopId, now);
|
|
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,
|
|
private void saveStationImages(Long orderId, Long stopId, String proofType, String operatorType,
|
|
@@ -1897,6 +2372,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return rows.stream().map(stop -> {
|
|
return rows.stream().map(stop -> {
|
|
|
FlashDeliveryStopView view = new FlashDeliveryStopView();
|
|
FlashDeliveryStopView view = new FlashDeliveryStopView();
|
|
|
BeanUtils.copyProperties(stop, view);
|
|
BeanUtils.copyProperties(stop, view);
|
|
|
|
|
+ view.setDeliveryPinCode(null);
|
|
|
if (sensitive) view.setImageUrls(images.stream().filter(i -> Objects.equals(i.getStopId(), stop.getId())
|
|
if (sensitive) view.setImageUrls(images.stream().filter(i -> Objects.equals(i.getStopId(), stop.getId())
|
|
|
&& Objects.equals(i.getProofType(), stop.getStopType()))
|
|
&& Objects.equals(i.getProofType(), stop.getStopType()))
|
|
|
.map(FlashDeliveryOrderImage::getImageUrl).toList());
|
|
.map(FlashDeliveryOrderImage::getImageUrl).toList());
|
|
@@ -1916,9 +2392,10 @@ public class FlashDeliveryApplicationService {
|
|
|
if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus()) || order.getPickedUpAt() != null
|
|
if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus()) || order.getPickedUpAt() != null
|
|
|
|| order.getPickedUpStopCount() != null && order.getPickedUpStopCount() > 0 || rows == null) return false;
|
|
|| order.getPickedUpStopCount() != null && order.getPickedUpStopCount() > 0 || rows == null) return false;
|
|
|
List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
|
|
List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
|
|
|
- return !pickups.isEmpty() && Objects.equals(order.getPickupStopCount(), pickups.size())
|
|
|
|
|
- && rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() == 1 && !anyPicked(rows)
|
|
|
|
|
- && pickups.stream().allMatch(s -> "PENDING".equals(s.getStatus()));
|
|
|
|
|
|
|
+ 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);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|