|
@@ -9,7 +9,7 @@ import com.ruoyi.app.flashdelivery.exception.FlashDeliveryQuoteChangedException;
|
|
|
import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
|
|
import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
|
|
|
import com.ruoyi.app.flashdelivery.route.GeoPoint;
|
|
import com.ruoyi.app.flashdelivery.route.GeoPoint;
|
|
|
import com.ruoyi.app.flashdelivery.route.RouteDistance;
|
|
import com.ruoyi.app.flashdelivery.route.RouteDistance;
|
|
|
-import com.ruoyi.app.order.RiderDeliveryExclusivityService;
|
|
|
|
|
|
|
+import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
|
|
|
import com.ruoyi.app.order.RiderDeliveryLockService;
|
|
import com.ruoyi.app.order.RiderDeliveryLockService;
|
|
|
import com.ruoyi.common.core.domain.entity.SysDictData;
|
|
import com.ruoyi.common.core.domain.entity.SysDictData;
|
|
|
import com.ruoyi.common.exception.ServiceException;
|
|
import com.ruoyi.common.exception.ServiceException;
|
|
@@ -19,9 +19,11 @@ import com.ruoyi.common.utils.StringUtils;
|
|
|
import com.ruoyi.system.domain.InfoUser;
|
|
import com.ruoyi.system.domain.InfoUser;
|
|
|
import com.ruoyi.system.domain.flash.*;
|
|
import com.ruoyi.system.domain.flash.*;
|
|
|
import com.ruoyi.system.mapper.InfoUserMapper;
|
|
import com.ruoyi.system.mapper.InfoUserMapper;
|
|
|
-import com.ruoyi.system.service.PaymentMethodGateService;
|
|
|
|
|
import com.ruoyi.system.mapper.flash.*;
|
|
import com.ruoyi.system.mapper.flash.*;
|
|
|
|
|
+import com.ruoyi.system.service.PaymentMethodGateService;
|
|
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
import org.springframework.dao.DataIntegrityViolationException;
|
|
import org.springframework.dao.DataIntegrityViolationException;
|
|
|
|
|
+import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
@@ -54,7 +56,6 @@ public class FlashDeliveryApplicationService {
|
|
|
private static final Set<String> DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
|
|
private static final Set<String> DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
|
|
|
/** 支付方式取值与主订单系统同域:4=现金、6=线下转账(027 线下转账支付)。 */
|
|
/** 支付方式取值与主订单系统同域:4=现金、6=线下转账(027 线下转账支付)。 */
|
|
|
private static final String PAY_TYPE_CASH = "4";
|
|
private static final String PAY_TYPE_CASH = "4";
|
|
|
- private static final String PAY_TYPE_TRANSFER = "6";
|
|
|
|
|
private static final DateTimeFormatter PRICING_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
|
|
private static final DateTimeFormatter PRICING_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
|
|
|
private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
|
|
private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
|
|
|
private static final int MAX_DISTANCE_METERS = 40_000;
|
|
private static final int MAX_DISTANCE_METERS = 40_000;
|
|
@@ -63,6 +64,8 @@ public class FlashDeliveryApplicationService {
|
|
|
private static final long MAX_SCHEDULE_DELAY_MILLIS = 3L * 24L * 60L * 60L * 1000L;
|
|
private static final long MAX_SCHEDULE_DELAY_MILLIS = 3L * 24L * 60L * 60L * 1000L;
|
|
|
private static final SecureRandom PIN_RANDOM = new SecureRandom();
|
|
private static final SecureRandom PIN_RANDOM = new SecureRandom();
|
|
|
private final FlashDeliveryOrderMapper orderMapper;
|
|
private final FlashDeliveryOrderMapper orderMapper;
|
|
|
|
|
+ private final FlashDeliveryOrderStopMapper stopMapper;
|
|
|
|
|
+ private final FlashDeliveryTargetAdjustmentMapper adjustmentMapper;
|
|
|
private final FlashDeliveryPricingMapper pricingMapper;
|
|
private final FlashDeliveryPricingMapper pricingMapper;
|
|
|
private final FlashDeliveryOrderImageMapper imageMapper;
|
|
private final FlashDeliveryOrderImageMapper imageMapper;
|
|
|
private final FlashDeliveryOrderLogMapper logMapper;
|
|
private final FlashDeliveryOrderLogMapper logMapper;
|
|
@@ -70,8 +73,8 @@ public class FlashDeliveryApplicationService {
|
|
|
private final FlashDeliveryRouteService routeService;
|
|
private final FlashDeliveryRouteService routeService;
|
|
|
private final FlashDeliveryPricingCalculator pricingCalculator;
|
|
private final FlashDeliveryPricingCalculator pricingCalculator;
|
|
|
private final RiderDeliveryLockService riderDeliveryLockService;
|
|
private final RiderDeliveryLockService riderDeliveryLockService;
|
|
|
- private final RiderDeliveryExclusivityService riderDeliveryExclusivityService;
|
|
|
|
|
- /** 支付方式闸门(031):闪送下单校验与抢单过滤的唯一判定来源。 */
|
|
|
|
|
|
|
+ private final RiderDeliveryAcceptanceService riderDeliveryAcceptanceService;
|
|
|
|
|
+ /** 031 支付闸门:闪送下单、报价展示和骑手待抢列表使用同一可用性规则。 */
|
|
|
private final PaymentMethodGateService paymentMethodGateService;
|
|
private final PaymentMethodGateService paymentMethodGateService;
|
|
|
|
|
|
|
|
public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
|
|
public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
|
|
@@ -82,9 +85,13 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryRouteService routeService,
|
|
FlashDeliveryRouteService routeService,
|
|
|
FlashDeliveryPricingCalculator pricingCalculator,
|
|
FlashDeliveryPricingCalculator pricingCalculator,
|
|
|
RiderDeliveryLockService riderDeliveryLockService,
|
|
RiderDeliveryLockService riderDeliveryLockService,
|
|
|
- RiderDeliveryExclusivityService riderDeliveryExclusivityService,
|
|
|
|
|
|
|
+ RiderDeliveryAcceptanceService riderDeliveryAcceptanceService,
|
|
|
|
|
+ FlashDeliveryOrderStopMapper stopMapper,
|
|
|
|
|
+ FlashDeliveryTargetAdjustmentMapper adjustmentMapper,
|
|
|
PaymentMethodGateService paymentMethodGateService) {
|
|
PaymentMethodGateService paymentMethodGateService) {
|
|
|
this.orderMapper = orderMapper;
|
|
this.orderMapper = orderMapper;
|
|
|
|
|
+ this.stopMapper = stopMapper;
|
|
|
|
|
+ this.adjustmentMapper = adjustmentMapper;
|
|
|
this.pricingMapper = pricingMapper;
|
|
this.pricingMapper = pricingMapper;
|
|
|
this.imageMapper = imageMapper;
|
|
this.imageMapper = imageMapper;
|
|
|
this.logMapper = logMapper;
|
|
this.logMapper = logMapper;
|
|
@@ -92,19 +99,21 @@ public class FlashDeliveryApplicationService {
|
|
|
this.routeService = routeService;
|
|
this.routeService = routeService;
|
|
|
this.pricingCalculator = pricingCalculator;
|
|
this.pricingCalculator = pricingCalculator;
|
|
|
this.riderDeliveryLockService = riderDeliveryLockService;
|
|
this.riderDeliveryLockService = riderDeliveryLockService;
|
|
|
- this.riderDeliveryExclusivityService = riderDeliveryExclusivityService;
|
|
|
|
|
|
|
+ this.riderDeliveryAcceptanceService = riderDeliveryAcceptanceService;
|
|
|
this.paymentMethodGateService = paymentMethodGateService;
|
|
this.paymentMethodGateService = paymentMethodGateService;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
|
|
/** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
|
|
|
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"));
|
|
|
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()));
|
|
|
|
|
|
|
+ FlashDeliveryPricing pricing = findPricingAtOrNull(pricingTargetTime(new Date()), FlashDeliveryVehicleType.MOTORCYCLE);
|
|
|
view.setServices(pricing == null ? List.of() : SERVICE_TYPES.stream()
|
|
view.setServices(pricing == null ? List.of() : SERVICE_TYPES.stream()
|
|
|
.map(serviceType -> serviceView(pricing, serviceType)).toList());
|
|
.map(serviceType -> serviceView(pricing, serviceType)).toList());
|
|
|
- // 031:首页返回闪送维度可选支付方式(平台开关开放集合),App 下单渲染用
|
|
|
|
|
view.setPayMethods(paymentMethodGateService.listAvailable(
|
|
view.setPayMethods(paymentMethodGateService.listAvailable(
|
|
|
PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
|
|
PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
|
|
|
return view;
|
|
return view;
|
|
@@ -122,6 +131,7 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
|
|
FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
|
|
|
view.setServiceType(request.getServiceType());
|
|
view.setServiceType(request.getServiceType());
|
|
|
view.setDeliveryType(request.getDeliveryType());
|
|
view.setDeliveryType(request.getDeliveryType());
|
|
|
|
|
+ view.setVehicleType(request.getVehicleType());
|
|
|
view.setDeliveryMode(normalizeDeliveryMode(request.getDeliveryMode()));
|
|
view.setDeliveryMode(normalizeDeliveryMode(request.getDeliveryMode()));
|
|
|
view.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
|
|
view.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
|
|
|
view.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
|
|
view.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
|
|
@@ -140,7 +150,6 @@ public class FlashDeliveryApplicationService {
|
|
|
applyBreakdown(view, context.breakdown());
|
|
applyBreakdown(view, context.breakdown());
|
|
|
view.setCurrency("TWD");
|
|
view.setCurrency("TWD");
|
|
|
view.setPricingVersion(context.pricing().getConfigVersion());
|
|
view.setPricingVersion(context.pricing().getConfigVersion());
|
|
|
- // 031:报价响应同样携带可选支付方式,与首页同源
|
|
|
|
|
view.setPayMethods(paymentMethodGateService.listAvailable(
|
|
view.setPayMethods(paymentMethodGateService.listAvailable(
|
|
|
PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
|
|
PaymentMethodGateService.GateScope.RIDER_FLASH, null, null));
|
|
|
return view;
|
|
return view;
|
|
@@ -161,7 +170,7 @@ public class FlashDeliveryApplicationService {
|
|
|
String requestId = request.getClientRequestId().trim();
|
|
String requestId = request.getClientRequestId().trim();
|
|
|
// 用户 ID + clientRequestId 构成幂等键;重复请求直接返回第一次创建的订单。
|
|
// 用户 ID + clientRequestId 构成幂等键;重复请求直接返回第一次创建的订单。
|
|
|
FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
|
|
FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
|
|
|
- if (existing != null) return participantDetail(existing, true, false);
|
|
|
|
|
|
|
+ if (existing != null) return participantDetail(lockOrder(existing.getId()), true, false);
|
|
|
if (request.getUserNote() != null && request.getUserNote().length() > 500) {
|
|
if (request.getUserNote() != null && request.getUserNote().length() > 500) {
|
|
|
throw fail("flash.delivery.note.too.long");
|
|
throw fail("flash.delivery.note.too.long");
|
|
|
}
|
|
}
|
|
@@ -182,17 +191,16 @@ public class FlashDeliveryApplicationService {
|
|
|
order.setUserId(userId);
|
|
order.setUserId(userId);
|
|
|
order.setServiceType(request.getServiceType());
|
|
order.setServiceType(request.getServiceType());
|
|
|
order.setDeliveryType(request.getDeliveryType());
|
|
order.setDeliveryType(request.getDeliveryType());
|
|
|
|
|
+ order.setVehicleType(request.getVehicleType());
|
|
|
order.setStatus(WAITING_ACCEPTANCE);
|
|
order.setStatus(WAITING_ACCEPTANCE);
|
|
|
- order.setPackageType(request.getPackageType());
|
|
|
|
|
- order.setQuantity(request.getQuantity());
|
|
|
|
|
- order.setWeightRange(request.getWeightRange());
|
|
|
|
|
- order.setSpecification(trimToNull(request.getSpecification()));
|
|
|
|
|
|
|
+ copyPickupSummary(order, quote.pickups());
|
|
|
|
|
+ order.setPickedUpStopCount(0);
|
|
|
order.setDeliveryMode(deliveryMode);
|
|
order.setDeliveryMode(deliveryMode);
|
|
|
order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
|
|
order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
|
|
|
order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
|
|
order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
|
|
|
order.setPinRequired(pinRequired);
|
|
order.setPinRequired(pinRequired);
|
|
|
order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
|
|
order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
|
|
|
- copyAddress(request.getPickup(), order, true);
|
|
|
|
|
|
|
+
|
|
|
copyAddress(request.getDelivery(), order, false);
|
|
copyAddress(request.getDelivery(), order, false);
|
|
|
order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
|
|
order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
|
|
|
order.setDistanceMeters(quote.route().distanceMeters());
|
|
order.setDistanceMeters(quote.route().distanceMeters());
|
|
@@ -224,10 +232,12 @@ public class FlashDeliveryApplicationService {
|
|
|
orderMapper.insert(order);
|
|
orderMapper.insert(order);
|
|
|
} catch (DataIntegrityViolationException duplicate) {
|
|
} catch (DataIntegrityViolationException duplicate) {
|
|
|
// 并发重复请求可能同时通过前置查询,最终由数据库唯一键完成幂等收口。
|
|
// 并发重复请求可能同时通过前置查询,最终由数据库唯一键完成幂等收口。
|
|
|
- FlashDeliveryOrder concurrent = orderMapper.selectByUserRequestId(userId, requestId);
|
|
|
|
|
- if (concurrent != null) return participantDetail(concurrent, true, false);
|
|
|
|
|
|
|
+ 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;
|
|
throw duplicate;
|
|
|
}
|
|
}
|
|
|
|
|
+ saveStops(order, quote.pickups(), request.getDelivery(), now);
|
|
|
saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
|
|
saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
|
|
|
writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
|
|
writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
|
|
|
return participantDetail(order, true, false);
|
|
return participantDetail(order, true, false);
|
|
@@ -242,14 +252,20 @@ public class FlashDeliveryApplicationService {
|
|
|
String participantColumn = "receiver".equals(selectedRole) ? "receiver_user_id" : "user_id";
|
|
String participantColumn = "receiver".equals(selectedRole) ? "receiver_user_id" : "user_id";
|
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
.eq(participantColumn, userId).orderByDesc("create_time");
|
|
.eq(participantColumn, userId).orderByDesc("create_time");
|
|
|
- return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), this::userOrderListView);
|
|
|
|
|
|
|
+ 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) {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
|
- return participantDetail(order, true, false);
|
|
|
|
|
|
|
+ FlashDeliveryOrderDetailView view = participantDetail(order, true, false);
|
|
|
|
|
+ view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && canUserCancel(order));
|
|
|
|
|
+ return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 地址修改先报价,用户确认前不改动订单。 */
|
|
/** 地址修改先报价,用户确认前不改动订单。 */
|
|
@@ -274,13 +290,14 @@ public class FlashDeliveryApplicationService {
|
|
|
|| !Objects.equals(request.getQuotedAmount(), quote.getAmount())) {
|
|
|| !Objects.equals(request.getQuotedAmount(), quote.getAmount())) {
|
|
|
throw new FlashDeliveryQuoteChangedException(quote);
|
|
throw new FlashDeliveryQuoteChangedException(quote);
|
|
|
}
|
|
}
|
|
|
- copyAddress(request.getPickup(), order, true);
|
|
|
|
|
|
|
+ List<FlashDeliveryPickupRequest> pickups = editablePickups(order, request);
|
|
|
|
|
+ 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.setDistanceMeters(quote.getDistanceMeters());
|
|
|
order.setDistanceSource(quote.getDistanceSource());
|
|
order.setDistanceSource(quote.getDistanceSource());
|
|
|
order.setEstimatedDurationSeconds(quote.getEstimatedDurationSeconds());
|
|
order.setEstimatedDurationSeconds(quote.getEstimatedDurationSeconds());
|
|
|
- // 运价快照与金额同事务刷新为本次计价依据(快照=金额的最后一次计算依据,而非最初依据)
|
|
|
|
|
|
|
+ // 改址报价携带原运价快照,金额与完整站点同事务保存,不引入最新运价。
|
|
|
order.setPricingId(quote.getPricingId());
|
|
order.setPricingId(quote.getPricingId());
|
|
|
order.setPricingVersion(quote.getPricingVersion());
|
|
order.setPricingVersion(quote.getPricingVersion());
|
|
|
order.setPricingStartTime(quote.getStartTime());
|
|
order.setPricingStartTime(quote.getStartTime());
|
|
@@ -301,6 +318,9 @@ public class FlashDeliveryApplicationService {
|
|
|
if (orderMapper.updateWaitingAddress(order, request.getOrderVersion()) != 1) {
|
|
if (orderMapper.updateWaitingAddress(order, request.getOrderVersion()) != 1) {
|
|
|
throw fail("flash.delivery.state.changed");
|
|
throw fail("flash.delivery.state.changed");
|
|
|
}
|
|
}
|
|
|
|
|
+ // 条件更新成功后持有订单行锁至提交,抢单不会读到仅修改一半的地址。
|
|
|
|
|
+ stopMapper.delete(new QueryWrapper<FlashDeliveryOrderStop>().eq("order_id", orderId));
|
|
|
|
|
+ saveStops(order, pickups, request.getDelivery(), now);
|
|
|
order.setVersion(order.getVersion() + 1);
|
|
order.setVersion(order.getVersion() + 1);
|
|
|
writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "ADDRESS_UPDATED", now);
|
|
writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "ADDRESS_UPDATED", now);
|
|
|
return participantDetail(order, true, false);
|
|
return participantDetail(order, true, false);
|
|
@@ -340,7 +360,9 @@ public class FlashDeliveryApplicationService {
|
|
|
private FlashDeliveryOrder requireEditableOrder(Long userId, Long orderId, Integer version) {
|
|
private FlashDeliveryOrder requireEditableOrder(Long userId, Long orderId, Integer version) {
|
|
|
requireUserId(userId);
|
|
requireUserId(userId);
|
|
|
if (orderId == null || orderId <= 0) throw fail("flash.delivery.order.not.found");
|
|
if (orderId == null || orderId <= 0) throw fail("flash.delivery.order.not.found");
|
|
|
- FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
|
|
|
|
+ // 写入路径先锁订单再读取站点,保持与抢单、取货和取消一致的锁顺序。
|
|
|
|
|
+ FlashDeliveryOrder order = TransactionSynchronizationManager.isActualTransactionActive()
|
|
|
|
|
+ ? lockOrder(orderId) : requireOrder(orderId);
|
|
|
if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
|
|
if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
|
|
|
if (!WAITING_ACCEPTANCE.equals(order.getStatus()) || order.getRiderId() != null) {
|
|
if (!WAITING_ACCEPTANCE.equals(order.getStatus()) || order.getRiderId() != null) {
|
|
|
throw fail("flash.delivery.edit.not.allowed");
|
|
throw fail("flash.delivery.edit.not.allowed");
|
|
@@ -353,22 +375,16 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
|
|
/** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
|
|
|
private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
|
|
private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
|
|
|
FlashDeliveryAddressChangeRequest request) {
|
|
FlashDeliveryAddressChangeRequest request) {
|
|
|
- validateAddress(request.getPickup());
|
|
|
|
|
- validateAddress(request.getDelivery());
|
|
|
|
|
- if (sameAddressText(request.getPickup(), request.getDelivery())
|
|
|
|
|
- || request.getPickup().getLatitude().compareTo(request.getDelivery().getLatitude()) == 0
|
|
|
|
|
- && request.getPickup().getLongitude().compareTo(request.getDelivery().getLongitude()) == 0) {
|
|
|
|
|
- throw fail("flash.delivery.address.same");
|
|
|
|
|
- }
|
|
|
|
|
- RouteDistance route = routeService.calculate(
|
|
|
|
|
- new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
|
|
|
|
|
- new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
|
|
|
|
|
- if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
|
|
|
|
|
- // 待接单尚未成交,改址按"订单定价时点"的最新运价重算(预约单取预约取件时刻、
|
|
|
|
|
- // 立即单取当前),管理员调价对未接单的改址立即生效;时段语义保住,预约单不会用到错误时段的价格。
|
|
|
|
|
- Date pricingDate = "SCHEDULED".equals(order.getDeliveryMode())
|
|
|
|
|
- ? order.getScheduledPickupStartAt() : new Date();
|
|
|
|
|
- FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate));
|
|
|
|
|
|
|
+ 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());
|
|
|
FlashDeliveryPriceBreakdown breakdown;
|
|
FlashDeliveryPriceBreakdown breakdown;
|
|
|
try {
|
|
try {
|
|
|
breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
|
|
breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
|
|
@@ -379,10 +395,11 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryQuoteRequest quoteRequest = new FlashDeliveryQuoteRequest();
|
|
FlashDeliveryQuoteRequest quoteRequest = new FlashDeliveryQuoteRequest();
|
|
|
quoteRequest.setServiceType(order.getServiceType());
|
|
quoteRequest.setServiceType(order.getServiceType());
|
|
|
quoteRequest.setDeliveryType(order.getDeliveryType());
|
|
quoteRequest.setDeliveryType(order.getDeliveryType());
|
|
|
|
|
+ quoteRequest.setVehicleType(vehicleTypeOf(order));
|
|
|
quoteRequest.setDeliveryMode(order.getDeliveryMode());
|
|
quoteRequest.setDeliveryMode(order.getDeliveryMode());
|
|
|
quoteRequest.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
|
|
quoteRequest.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
|
|
|
quoteRequest.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
|
|
quoteRequest.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
|
|
|
- FlashDeliveryQuoteView view = quoteView(quoteRequest, new QuoteContext(pricing, route, breakdown));
|
|
|
|
|
|
|
+ FlashDeliveryQuoteView view = quoteView(quoteRequest, new QuoteContext(pricing, route, breakdown, pickups));
|
|
|
view.setOrderVersion(order.getVersion());
|
|
view.setOrderVersion(order.getVersion());
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
@@ -390,9 +407,9 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 用户取消:仅待接单或已接单(实际取件前)允许。 */
|
|
/** 用户取消:仅待接单或已接单(实际取件前)允许。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
- FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
|
|
|
|
+ FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
|
|
if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
|
|
|
- if (!FlashDeliveryStateMachine.canUserCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
|
|
|
|
|
|
|
+ if (anyPicked(requireStops(order)) || !FlashDeliveryStateMachine.canUserCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
|
|
|
String reason = requiredReason(request);
|
|
String reason = requiredReason(request);
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
if (orderMapper.cancel(orderId, order.getStatus(), "USER", userId, reason, now) != 1) {
|
|
if (orderMapper.cancel(orderId, order.getStatus(), "USER", userId, reason, now) != 1) {
|
|
@@ -404,8 +421,9 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 用户(寄件人或收件人)确认收货:已送达推进为已完成。 */
|
|
/** 用户(寄件人或收件人)确认收货:已送达推进为已完成。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void confirmReceipt(Long userId, Long orderId) {
|
|
public void confirmReceipt(Long userId, Long orderId) {
|
|
|
- FlashDeliveryOrder order = requireOrder(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");
|
|
|
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");
|
|
@@ -421,7 +439,6 @@ public class FlashDeliveryApplicationService {
|
|
|
public FlashDeliveryRiderOrderPageView riderOrders(Long riderId, int pageNum, int pageSize,
|
|
public FlashDeliveryRiderOrderPageView riderOrders(Long riderId, int pageNum, int pageSize,
|
|
|
String tab, BigDecimal longitude, BigDecimal latitude) {
|
|
String tab, BigDecimal longitude, BigDecimal latitude) {
|
|
|
InfoUser rider = requireRider(riderId);
|
|
InfoUser rider = requireRider(riderId);
|
|
|
- // 031:骑手接受的支付方式集合(未设置=平台开放全部),newTask 页签按此过滤
|
|
|
|
|
Set<String> acceptedPayTypes = acceptedRiderPayTypes(rider);
|
|
Set<String> acceptedPayTypes = acceptedRiderPayTypes(rider);
|
|
|
if (!hasText(tab)) throw fail("flash.delivery.tab.invalid");
|
|
if (!hasText(tab)) throw fail("flash.delivery.tab.invalid");
|
|
|
String selectedTab = tab.trim();
|
|
String selectedTab = tab.trim();
|
|
@@ -433,7 +450,8 @@ public class FlashDeliveryApplicationService {
|
|
|
if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
|
|
if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
|
|
|
validateRiderCoordinates(effectiveLongitude, effectiveLatitude);
|
|
validateRiderCoordinates(effectiveLongitude, effectiveLatitude);
|
|
|
}
|
|
}
|
|
|
- QueryWrapper<FlashDeliveryOrder> query = riderOrdersQuery(riderId, selectedTab,
|
|
|
|
|
|
|
+ Integer riderVehicleType = riderVehicleType(rider);
|
|
|
|
|
+ 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);
|
|
|
FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
|
|
FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
|
|
@@ -444,7 +462,7 @@ public class FlashDeliveryApplicationService {
|
|
|
result.setSize(orders.getSize());
|
|
result.setSize(orders.getSize());
|
|
|
if ("newTask".equals(selectedTab)) {
|
|
if ("newTask".equals(selectedTab)) {
|
|
|
result.setNearbyTaskCount(orders.getTotal());
|
|
result.setNearbyTaskCount(orders.getTotal());
|
|
|
- QueryWrapper<FlashDeliveryOrder> maximumQuery = riderOrdersQuery(riderId, selectedTab,
|
|
|
|
|
|
|
+ QueryWrapper<FlashDeliveryOrder> maximumQuery = riderOrdersQuery(riderId, riderVehicleType, selectedTab,
|
|
|
effectiveLongitude, effectiveLatitude, false, acceptedPayTypes)
|
|
effectiveLongitude, effectiveLatitude, false, acceptedPayTypes)
|
|
|
.select("MAX(amount)");
|
|
.select("MAX(amount)");
|
|
|
result.setHighestOrderAmount(firstLong(orderMapper.selectObjs(maximumQuery)));
|
|
result.setHighestOrderAmount(firstLong(orderMapper.selectObjs(maximumQuery)));
|
|
@@ -458,7 +476,8 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
|
|
if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
|
|
|
// 不兼容闪送的骑手按订单不存在处理,避免泄露待抢任务信息。
|
|
// 不兼容闪送的骑手按订单不存在处理,避免泄露待抢任务信息。
|
|
|
- if (!isAvailableAt(order, new Date()) || !rider.supportsDeliveryType("FLASH")) {
|
|
|
|
|
|
|
+ if (!isAvailableAt(order, new Date()) || !rider.supportsDeliveryType("FLASH")
|
|
|
|
|
+ || !isRiderVehicleCompatible(rider, order)) {
|
|
|
throw fail("flash.delivery.order.not.found");
|
|
throw fail("flash.delivery.order.not.found");
|
|
|
}
|
|
}
|
|
|
// 接单前显示取送文字地址,但隐藏联系人、电话、精确坐标、实际 PIN 和履约图片。
|
|
// 接单前显示取送文字地址,但隐藏联系人、电话、精确坐标、实际 PIN 和履约图片。
|
|
@@ -471,12 +490,12 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 原子抢单:骑手锁内完成互斥校验与条件更新,同一订单仅一名骑手成功。 */
|
|
/** 原子抢单:骑手锁内完成互斥校验与条件更新,同一订单仅一名骑手成功。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
|
|
public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
|
|
|
- InfoUser rider = requireRider(riderId);
|
|
|
|
|
- // 抢单属于获取新任务,要求骑手承接闪送配送。
|
|
|
|
|
- if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
|
|
|
|
|
return riderDeliveryLockService.withLock(riderId, () -> {
|
|
return riderDeliveryLockService.withLock(riderId, () -> {
|
|
|
FlashDeliveryOrder target = requireOrder(orderId);
|
|
FlashDeliveryOrder target = requireOrder(orderId);
|
|
|
- riderDeliveryExclusivityService.assertCanAcceptFlash(riderId, target.getDeliveryType());
|
|
|
|
|
|
|
+ InfoUser rider = requireRider(riderId);
|
|
|
|
|
+ if (!isRiderVehicleCompatible(rider, target)) throw fail("flash.delivery.order.not.found");
|
|
|
|
|
+ // 账号资格、闪送/总任务容量与急送独占统一在骑手锁内复核,列表结果不能替代接单校验。
|
|
|
|
|
+ riderDeliveryAcceptanceService.assertCanAcceptFlash(riderId, target.getDeliveryType());
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
// Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
|
|
// Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
|
|
|
if (orderMapper.accept(orderId, riderId, now) != 1) {
|
|
if (orderMapper.accept(orderId, riderId, now) != 1) {
|
|
@@ -487,25 +506,37 @@ public class FlashDeliveryApplicationService {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 骑手取件:提交取件凭证,订单由已接单推进为已取件。 */
|
|
|
|
|
|
|
+ /** 骑手逐站取件:保存本站凭证,仅在全部取货站完成后推进订单为已取件。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void pickup(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
|
|
public void pickup(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
|
|
|
- transitionWithProof(riderId, orderId, request, ACCEPTED, PICKED_UP, "PICKUP", "picked_up_at");
|
|
|
|
|
|
|
+ pickupStation(riderId, orderId, request == null ? null : request.getStopId(), request);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 骑手送达:校验交付 PIN(启用时)并提交送达凭证,订单由已取件推进为已送达。 */
|
|
/** 骑手送达:校验交付 PIN(启用时)并提交送达凭证,订单由已取件推进为已送达。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
|
|
public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
- FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
|
|
|
|
+ FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
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");
|
|
|
}
|
|
}
|
|
|
|
|
+ List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
|
|
|
+ if (!allPicked(rows)) throw fail("flash.delivery.transition.not.allowed");
|
|
|
if (Boolean.TRUE.equals(order.getPinRequired())
|
|
if (Boolean.TRUE.equals(order.getPinRequired())
|
|
|
&& (request == null || !Objects.equals(order.getDeliveryPinCode(), trimToNull(request.getPinCode())))) {
|
|
&& (request == null || !Objects.equals(order.getDeliveryPinCode(), trimToNull(request.getPinCode())))) {
|
|
|
throw fail("flash.delivery.pin.invalid");
|
|
throw fail("flash.delivery.pin.invalid");
|
|
|
}
|
|
}
|
|
|
- transitionWithProof(riderId, orderId, request, PICKED_UP, DELIVERED, "DELIVERY", "delivered_at");
|
|
|
|
|
|
|
+ FlashDeliveryOrderStop delivery = rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).findFirst().orElseThrow();
|
|
|
|
|
+ if (request != null && request.getStopId() != null && !Objects.equals(request.getStopId(), delivery.getId())) throw fail("flash.delivery.stop.invalid");
|
|
|
|
|
+ List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ if (orderMapper.transitionByRider(orderId, riderId, PICKED_UP, DELIVERED, "delivered_at", now) != 1) {
|
|
|
|
|
+ throw fail("flash.delivery.transition.not.allowed");
|
|
|
|
|
+ }
|
|
|
|
|
+ delivery.setStatus("DELIVERED"); delivery.setDeliveredAt(now);
|
|
|
|
|
+ if (stopMapper.updateById(delivery) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ saveStationImages(orderId, delivery.getId(), "DELIVERY", "RIDER", riderId, urls, now);
|
|
|
|
|
+ writeLog(orderId, PICKED_UP, DELIVERED, "RIDER", riderId, null, now);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 平台运价配置列表,按时段开始时间排序。 */
|
|
/** 平台运价配置列表,按时段开始时间排序。 */
|
|
@@ -542,6 +573,7 @@ public class FlashDeliveryApplicationService {
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
|
|
UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
|
|
|
.eq("id", pricingId).eq("config_version", request.getConfigVersion())
|
|
.eq("id", pricingId).eq("config_version", request.getConfigVersion())
|
|
|
|
|
+ .set("vehicle_type", request.getVehicleType())
|
|
|
.set("start_time", request.getStartTime())
|
|
.set("start_time", request.getStartTime())
|
|
|
.set("end_time", request.getEndTime())
|
|
.set("end_time", request.getEndTime())
|
|
|
.set("starting_distance", scaleDistance(request.getStartingDistance()))
|
|
.set("starting_distance", scaleDistance(request.getStartingDistance()))
|
|
@@ -555,6 +587,7 @@ public class FlashDeliveryApplicationService {
|
|
|
.setSql("config_version = config_version + 1");
|
|
.setSql("config_version = config_version + 1");
|
|
|
// 版本条件防止两个管理员同时保存时后提交者静默覆盖先提交者。
|
|
// 版本条件防止两个管理员同时保存时后提交者静默覆盖先提交者。
|
|
|
if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
|
|
if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
|
|
|
|
|
+ pricing.setVehicleType(request.getVehicleType());
|
|
|
pricing.setStartTime(request.getStartTime());
|
|
pricing.setStartTime(request.getStartTime());
|
|
|
pricing.setEndTime(request.getEndTime());
|
|
pricing.setEndTime(request.getEndTime());
|
|
|
pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
|
|
pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
|
|
@@ -600,8 +633,16 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
|
|
FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
|
|
|
view.setOrder(order);
|
|
view.setOrder(order);
|
|
|
|
|
+ List<FlashDeliveryOrderStop> stationRows = stops(orderId);
|
|
|
|
|
+ view.setPickupStopCount(order.getPickupStopCount()); view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
|
|
|
+ view.setCanUserCancel(canUserCancel(order)); view.setCurrentTargetStopId(targetStopId(order, stationRows));
|
|
|
|
|
+ view.setTargetAdjustments(adjustmentMapper.selectList(new QueryWrapper<FlashDeliveryTargetAdjustment>()
|
|
|
|
|
+ .eq("order_id", orderId).orderByAsc("create_time", "id")));
|
|
|
view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
|
.eq("order_id", orderId).orderByAsc("proof_type", "sort_order")));
|
|
.eq("order_id", orderId).orderByAsc("proof_type", "sort_order")));
|
|
|
|
|
+ view.setStops(stopViews(stationRows, true, view.getImages()));
|
|
|
|
|
+ view.setHandoffImageUrls(view.getImages().stream().filter(i -> "HANDOFF".equals(i.getProofType()))
|
|
|
|
|
+ .map(FlashDeliveryOrderImage::getImageUrl).toList());
|
|
|
view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
|
|
view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
|
|
|
.eq("order_id", orderId).orderByAsc("create_time", "id")));
|
|
.eq("order_id", orderId).orderByAsc("create_time", "id")));
|
|
|
return view;
|
|
return view;
|
|
@@ -610,31 +651,42 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 平台取消:任何非终态订单可取消,记录操作人与原因,用于异常履约人工收口。 */
|
|
/** 平台取消:任何非终态订单可取消,记录操作人与原因,用于异常履约人工收口。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
- FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
|
|
|
|
+ FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
|
|
if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
|
|
|
String reason = requiredReason(request);
|
|
String reason = requiredReason(request);
|
|
|
- Date now = new Date();
|
|
|
|
|
- if (orderMapper.cancel(orderId, order.getStatus(), "ADMIN", adminId, reason, now) != 1) {
|
|
|
|
|
- throw fail("flash.delivery.state.changed");
|
|
|
|
|
|
|
+ List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
|
|
|
+ if (request.getProblemStopId() == null || !hasText(request.getHandlingResult()) || tooLong(request.getHandlingResult(), 500)) {
|
|
|
|
|
+ throw fail("flash.delivery.intervention.required");
|
|
|
|
|
+ }
|
|
|
|
|
+ requireStop(rows, request.getProblemStopId());
|
|
|
|
|
+ boolean collected = anyPicked(rows);
|
|
|
|
|
+ if ((collected && !hasText(request.getPickedGoodsDisposition())) || tooLong(request.getPickedGoodsDisposition(), 500)) {
|
|
|
|
|
+ throw fail("flash.delivery.handoff.required");
|
|
|
}
|
|
}
|
|
|
|
|
+ List<String> proofs = validateProofUrls(request.getHandoffImageUrls(), collected);
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ // 关闭异常订单前保存介入结果和物品去向;凭证写入失败时整笔事务回滚。
|
|
|
|
|
+ UpdateWrapper<FlashDeliveryOrder> audit = new UpdateWrapper<FlashDeliveryOrder>().eq("id", orderId)
|
|
|
|
|
+ .set("problem_stop_id", request.getProblemStopId()).set("handling_result", request.getHandlingResult().trim())
|
|
|
|
|
+ .set("picked_goods_disposition", trimToNull(request.getPickedGoodsDisposition()));
|
|
|
|
|
+ if (orderMapper.update(null, audit) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ saveStationImages(orderId, request.getProblemStopId(), "HANDOFF", "ADMIN", adminId, proofs, now);
|
|
|
|
|
+ if (orderMapper.cancel(orderId, order.getStatus(), "ADMIN", adminId, reason, now) != 1) throw fail("flash.delivery.state.changed");
|
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 将已送达订单推进为已完成;平台手动与系统自动完成共用此入口,条件更新失败返回 false。 */
|
|
/** 将已送达订单推进为已完成;平台手动与系统自动完成共用此入口,条件更新失败返回 false。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public boolean complete(Long operatorId, String operatorType, Long orderId) {
|
|
public boolean complete(Long operatorId, String operatorType, Long orderId) {
|
|
|
|
|
+ FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
|
|
+ if (!DELIVERED.equals(order.getStatus()) || !allPicked(requireStops(order))) 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);
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 查询送达时间早于截止点且未完成的订单,供自动完成任务分批处理。 */
|
|
|
|
|
- /**
|
|
|
|
|
- * 骑手确认收款(031):本人中单且订单已达已送达(或之后)才可确认;
|
|
|
|
|
- * 条件更新 0→1 保证幂等与并发安全,仅首次确认写审计日志;
|
|
|
|
|
- * 收款状态不影响订单状态流转(转账到账有时差,平台不经手资金不设硬闸门,spec FR-013)。
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /** 骑手确认实际收款:仅本人已送达订单可确认,条件更新保证重复请求不重复写日志。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void confirmPayment(Long riderId, Long orderId) {
|
|
public void confirmPayment(Long riderId, Long orderId) {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
@@ -645,16 +697,14 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
if (Integer.valueOf(1).equals(order.getPaymentStatus())) return;
|
|
if (Integer.valueOf(1).equals(order.getPaymentStatus())) return;
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
- // 条件更新 0→1;并发重复确认只有一方成功,另一方按幂等成功返回
|
|
|
|
|
if (orderMapper.update(null, new UpdateWrapper<FlashDeliveryOrder>()
|
|
if (orderMapper.update(null, new UpdateWrapper<FlashDeliveryOrder>()
|
|
|
- .eq("id", orderId)
|
|
|
|
|
- .eq("payment_status", 0)
|
|
|
|
|
- .set("payment_status", 1)
|
|
|
|
|
- .set("update_time", now)) == 1) {
|
|
|
|
|
|
|
+ .eq("id", orderId).eq("payment_status", 0)
|
|
|
|
|
+ .set("payment_status", 1).set("update_time", now)) == 1) {
|
|
|
writeLog(orderId, order.getStatus(), order.getStatus(), "RIDER", riderId, "PAYMENT_CONFIRMED", now);
|
|
writeLog(orderId, order.getStatus(), order.getStatus(), "RIDER", riderId, "PAYMENT_CONFIRMED", now);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 查询送达时间早于截止点且未完成的订单,供自动完成任务分批处理。 */
|
|
|
public List<FlashDeliveryOrder> autoCompleteCandidates(Date deadline, int limit) {
|
|
public List<FlashDeliveryOrder> autoCompleteCandidates(Date deadline, int limit) {
|
|
|
return orderMapper.selectAutoCompletable(deadline, Math.min(Math.max(limit, 1), 500));
|
|
return orderMapper.selectAutoCompletable(deadline, Math.min(Math.max(limit, 1), 500));
|
|
|
}
|
|
}
|
|
@@ -664,42 +714,31 @@ public class FlashDeliveryApplicationService {
|
|
|
if (request == null) throw fail("flash.delivery.request.required");
|
|
if (request == null) throw fail("flash.delivery.request.required");
|
|
|
validateServiceType(request.getServiceType());
|
|
validateServiceType(request.getServiceType());
|
|
|
validateDeliveryType(request.getDeliveryType());
|
|
validateDeliveryType(request.getDeliveryType());
|
|
|
- if (!hasText(request.getPackageType()) || !PACKAGE_TYPES.contains(request.getPackageType())) {
|
|
|
|
|
- throw fail("flash.delivery.package.type.invalid");
|
|
|
|
|
- }
|
|
|
|
|
- validateItem(request);
|
|
|
|
|
|
|
+ List<FlashDeliveryPickupRequest> pickups = normalizePickups(request);
|
|
|
|
|
+ validatePickups(pickups, request.getDelivery());
|
|
|
|
|
+ if (request.getTipAmount() == null || request.getTipAmount() < 0) throw fail("flash.delivery.tip.invalid");
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
|
|
String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
|
|
|
validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
|
|
validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
|
|
|
- validateAddress(request.getPickup());
|
|
|
|
|
- validateAddress(request.getDelivery());
|
|
|
|
|
- if (sameAddressText(request.getPickup(), request.getDelivery())
|
|
|
|
|
- || request.getPickup().getLatitude().compareTo(request.getDelivery().getLatitude()) == 0
|
|
|
|
|
- && request.getPickup().getLongitude().compareTo(request.getDelivery().getLongitude()) == 0) {
|
|
|
|
|
- throw fail("flash.delivery.address.same");
|
|
|
|
|
- }
|
|
|
|
|
Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
|
|
Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
|
|
|
- FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate));
|
|
|
|
|
- // 路线服务优先使用 Google Routes;不可用时在服务内部降级为球面直线距离。
|
|
|
|
|
- RouteDistance route = routeService.calculate(
|
|
|
|
|
- new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
|
|
|
|
|
- new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
|
|
|
|
|
- if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
|
|
|
|
|
|
|
+ Integer selectedVehicle = requireVehicleType(request.getVehicleType());
|
|
|
|
|
+ FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate), selectedVehicle);
|
|
|
|
|
+ RouteDistance route = pickupRoute(pickups, request.getDelivery(), selectedVehicle);
|
|
|
FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
|
|
FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
|
|
|
route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
|
|
route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
|
|
|
- return new QuoteContext(pricing, route, breakdown);
|
|
|
|
|
|
|
+ return new QuoteContext(pricing, route, breakdown, pickups);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 查询指定时刻命中的运价配置,未配置时直接报错。 */
|
|
/** 查询指定时刻命中的运价配置,未配置时直接报错。 */
|
|
|
- private FlashDeliveryPricing findPricingAt(String targetTime) {
|
|
|
|
|
- FlashDeliveryPricing pricing = findPricingAtOrNull(targetTime);
|
|
|
|
|
|
|
+ private FlashDeliveryPricing findPricingAt(String targetTime, Integer vehicleType) {
|
|
|
|
|
+ FlashDeliveryPricing pricing = findPricingAtOrNull(targetTime, vehicleType);
|
|
|
if (pricing == null) throw fail("flash.delivery.pricing.not.available");
|
|
if (pricing == null) throw fail("flash.delivery.pricing.not.available");
|
|
|
return pricing;
|
|
return pricing;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 查询命中运价;命中多条说明配置重叠,报错交由平台修正。 */
|
|
/** 查询命中运价;命中多条说明配置重叠,报错交由平台修正。 */
|
|
|
- private FlashDeliveryPricing findPricingAtOrNull(String targetTime) {
|
|
|
|
|
- List<FlashDeliveryPricing> pricing = pricingMapper.selectAtTime(targetTime);
|
|
|
|
|
|
|
+ private FlashDeliveryPricing findPricingAtOrNull(String targetTime, Integer vehicleType) {
|
|
|
|
|
+ List<FlashDeliveryPricing> pricing = pricingMapper.selectAtTime(targetTime, vehicleType);
|
|
|
if (pricing == null || pricing.isEmpty()) return null;
|
|
if (pricing == null || pricing.isEmpty()) return null;
|
|
|
if (pricing.size() > 1) throw fail("flash.delivery.pricing.overlap");
|
|
if (pricing.size() > 1) throw fail("flash.delivery.pricing.overlap");
|
|
|
return pricing.get(0);
|
|
return pricing.get(0);
|
|
@@ -711,7 +750,8 @@ public class FlashDeliveryApplicationService {
|
|
|
BigDecimal startingDistance = normalizeDistance(request.getStartingDistance());
|
|
BigDecimal startingDistance = normalizeDistance(request.getStartingDistance());
|
|
|
BigDecimal distance = normalizeDistance(request.getDistance());
|
|
BigDecimal distance = normalizeDistance(request.getDistance());
|
|
|
BigDecimal urgentRate = normalizeRate(request.getUrgentRate());
|
|
BigDecimal urgentRate = normalizeRate(request.getUrgentRate());
|
|
|
- if (!validStartTime(request.getStartTime()) || !validEndTime(request.getEndTime())
|
|
|
|
|
|
|
+ if (!FlashDeliveryVehicleType.isSupported(request.getVehicleType())
|
|
|
|
|
+ || !validStartTime(request.getStartTime()) || !validEndTime(request.getEndTime())
|
|
|
|| toMinute(request.getStartTime()) >= toMinute(request.getEndTime())
|
|
|| toMinute(request.getStartTime()) >= toMinute(request.getEndTime())
|
|
|
|| startingDistance == null || startingDistance.signum() <= 0
|
|
|| startingDistance == null || startingDistance.signum() <= 0
|
|
|
|| startingDistance.compareTo(MAX_PRICING_DISTANCE) > 0
|
|
|| startingDistance.compareTo(MAX_PRICING_DISTANCE) > 0
|
|
@@ -729,7 +769,7 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
|
|
/** 确保新时段与既有运价不重叠,excludedId 用于修改时排除自身。 */
|
|
/** 确保新时段与既有运价不重叠,excludedId 用于修改时排除自身。 */
|
|
|
private void ensureNoPricingOverlap(FlashDeliveryPricingRequest request, Long excludedId) {
|
|
private void ensureNoPricingOverlap(FlashDeliveryPricingRequest request, Long excludedId) {
|
|
|
- if (pricingMapper.countOverlapping(request.getStartTime(), request.getEndTime(), excludedId) > 0) {
|
|
|
|
|
|
|
+ if (pricingMapper.countOverlapping(request.getStartTime(), request.getEndTime(), request.getVehicleType(), excludedId) > 0) {
|
|
|
throw fail("flash.delivery.pricing.overlap");
|
|
throw fail("flash.delivery.pricing.overlap");
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -737,6 +777,7 @@ public class FlashDeliveryApplicationService {
|
|
|
/** 将运价请求转换为实体,距离与比例统一保留两位小数。 */
|
|
/** 将运价请求转换为实体,距离与比例统一保留两位小数。 */
|
|
|
private FlashDeliveryPricing pricingFrom(FlashDeliveryPricingRequest request) {
|
|
private FlashDeliveryPricing pricingFrom(FlashDeliveryPricingRequest request) {
|
|
|
FlashDeliveryPricing pricing = new FlashDeliveryPricing();
|
|
FlashDeliveryPricing pricing = new FlashDeliveryPricing();
|
|
|
|
|
+ pricing.setVehicleType(request.getVehicleType());
|
|
|
pricing.setStartTime(request.getStartTime());
|
|
pricing.setStartTime(request.getStartTime());
|
|
|
pricing.setEndTime(request.getEndTime());
|
|
pricing.setEndTime(request.getEndTime());
|
|
|
pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
|
|
pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
|
|
@@ -773,19 +814,6 @@ public class FlashDeliveryApplicationService {
|
|
|
return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
|
|
return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 骑手带凭证的状态推进:校验凭证后按骑手归属条件更新,再落图片与日志。 */
|
|
|
|
|
- private void transitionWithProof(Long riderId, Long orderId, FlashDeliveryProofRequest request,
|
|
|
|
|
- String expected, String next, String proofType, String timeColumn) {
|
|
|
|
|
- List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
|
|
|
- requireRider(riderId);
|
|
|
|
|
- Date now = new Date();
|
|
|
|
|
- if (orderMapper.transitionByRider(orderId, riderId, expected, next, timeColumn, now) != 1) {
|
|
|
|
|
- throw fail("flash.delivery.transition.not.allowed");
|
|
|
|
|
- }
|
|
|
|
|
- saveImages(orderId, proofType, "RIDER", riderId, urls, now);
|
|
|
|
|
- writeLog(orderId, expected, next, "RIDER", riderId, null, now);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/** 状态、图片和日志处于同一事务;任一图片写入失败会回滚本次业务操作。 */
|
|
/** 状态、图片和日志处于同一事务;任一图片写入失败会回滚本次业务操作。 */
|
|
|
private void saveImages(Long orderId, String proofType, String operatorType,
|
|
private void saveImages(Long orderId, String proofType, String operatorType,
|
|
|
Long operatorId, List<String> urls, Date now) {
|
|
Long operatorId, List<String> urls, Date now) {
|
|
@@ -830,14 +858,22 @@ public class FlashDeliveryApplicationService {
|
|
|
boolean userView,
|
|
boolean userView,
|
|
|
boolean hideSensitiveFields) {
|
|
boolean hideSensitiveFields) {
|
|
|
FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
|
|
FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
|
|
|
|
|
+ if (!hideSensitiveFields) view.setOrderVersion(order.getVersion());
|
|
|
|
|
+ List<FlashDeliveryOrderStop> stationRows = stops(order.getId());
|
|
|
|
|
+ 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));
|
|
|
|
|
+ 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) {
|
|
if (userView) {
|
|
|
- view.setOrderVersion(order.getVersion());
|
|
|
|
|
view.setDeliveryPinCode(Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
|
|
view.setDeliveryPinCode(Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
|
|
|
view.setRider(riderSummary(order));
|
|
view.setRider(riderSummary(order));
|
|
|
}
|
|
}
|
|
|
if (!hideSensitiveFields && order.getId() != null) {
|
|
if (!hideSensitiveFields && order.getId() != null) {
|
|
|
- groupImageUrls(view, imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
|
|
|
- .eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
|
|
|
|
|
|
|
+ groupImageUrls(view, stationImages);
|
|
|
}
|
|
}
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
@@ -849,7 +885,11 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setOrderNo(order.getOrderNo());
|
|
view.setOrderNo(order.getOrderNo());
|
|
|
view.setServiceType(order.getServiceType());
|
|
view.setServiceType(order.getServiceType());
|
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
|
|
|
+ view.setVehicleType(vehicleTypeOf(order));
|
|
|
view.setStatus(order.getStatus());
|
|
view.setStatus(order.getStatus());
|
|
|
|
|
+ view.setPickupStopCount(order.getPickupStopCount());
|
|
|
|
|
+ view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
|
|
|
+ view.setCanUserCancel(canUserCancel(order));
|
|
|
view.setOrderVersion(order.getVersion());
|
|
view.setOrderVersion(order.getVersion());
|
|
|
view.setPackageType(order.getPackageType());
|
|
view.setPackageType(order.getPackageType());
|
|
|
view.setQuantity(order.getQuantity());
|
|
view.setQuantity(order.getQuantity());
|
|
@@ -881,7 +921,11 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setOrderNo(order.getOrderNo());
|
|
view.setOrderNo(order.getOrderNo());
|
|
|
view.setServiceType(order.getServiceType());
|
|
view.setServiceType(order.getServiceType());
|
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
|
|
|
+ view.setVehicleType(vehicleTypeOf(order));
|
|
|
view.setStatus(order.getStatus());
|
|
view.setStatus(order.getStatus());
|
|
|
|
|
+ view.setPickupStopCount(order.getPickupStopCount());
|
|
|
|
|
+ view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
|
|
|
+ view.setCanUserCancel(canUserCancel(order));
|
|
|
view.setPackageType(order.getPackageType());
|
|
view.setPackageType(order.getPackageType());
|
|
|
view.setQuantity(order.getQuantity());
|
|
view.setQuantity(order.getQuantity());
|
|
|
view.setWeightRange(order.getWeightRange());
|
|
view.setWeightRange(order.getWeightRange());
|
|
@@ -957,7 +1001,11 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setOrderNo(order.getOrderNo());
|
|
view.setOrderNo(order.getOrderNo());
|
|
|
view.setServiceType(order.getServiceType());
|
|
view.setServiceType(order.getServiceType());
|
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
|
|
|
+ view.setVehicleType(vehicleTypeOf(order));
|
|
|
view.setStatus(order.getStatus());
|
|
view.setStatus(order.getStatus());
|
|
|
|
|
+ view.setPickupStopCount(order.getPickupStopCount());
|
|
|
|
|
+ view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
|
|
|
+ view.setCanUserCancel(canUserCancel(order));
|
|
|
view.setPackageType(order.getPackageType());
|
|
view.setPackageType(order.getPackageType());
|
|
|
view.setQuantity(order.getQuantity());
|
|
view.setQuantity(order.getQuantity());
|
|
|
view.setWeightRange(order.getWeightRange());
|
|
view.setWeightRange(order.getWeightRange());
|
|
@@ -1140,22 +1188,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 校验物品信息:数量、重量范围、规格长度和小费。 */
|
|
|
|
|
- private void validateItem(FlashDeliveryQuoteRequest request) {
|
|
|
|
|
- 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");
|
|
|
|
|
- }
|
|
|
|
|
- if (request.getTipAmount() == null || request.getTipAmount() < 0) {
|
|
|
|
|
- throw fail("flash.delivery.tip.invalid");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 支付方式经 031 闸门校验(闪送维度仅平台开关层——抢单前无收款方),
|
|
|
|
|
- * 缺省现金 4;取代原先注释掉的白名单硬编码,改由平台配置管控(spec FR-004)。
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /** 支付方式由 031 闸门校验;闪送维度按平台开关判定,缺省现金。 */
|
|
|
private String normalizePayType(String payType) {
|
|
private String normalizePayType(String payType) {
|
|
|
String value = hasText(payType) ? payType.trim() : PAY_TYPE_CASH;
|
|
String value = hasText(payType) ? payType.trim() : PAY_TYPE_CASH;
|
|
|
paymentMethodGateService.assertUsable(value,
|
|
paymentMethodGateService.assertUsable(value,
|
|
@@ -1201,23 +1234,20 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
|
|
/** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
|
|
|
- private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, String tab,
|
|
|
|
|
|
|
+ private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, Integer riderVehicleType, String tab,
|
|
|
BigDecimal longitude, BigDecimal latitude,
|
|
BigDecimal longitude, BigDecimal latitude,
|
|
|
- boolean orderResults,
|
|
|
|
|
- Set<String> acceptedPayTypes) {
|
|
|
|
|
|
|
+ boolean orderResults, Set<String> acceptedPayTypes) {
|
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<>();
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<>();
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
switch (tab) {
|
|
switch (tab) {
|
|
|
case "newTask" -> {
|
|
case "newTask" -> {
|
|
|
query.eq("status", WAITING_ACCEPTANCE).isNull("rider_id")
|
|
query.eq("status", WAITING_ACCEPTANCE).isNull("rider_id")
|
|
|
|
|
+ .eq("vehicle_type", riderVehicleType)
|
|
|
.and(item -> item.eq("delivery_mode", "NOW")
|
|
.and(item -> item.eq("delivery_mode", "NOW")
|
|
|
.or().le("scheduled_pickup_start_at", now));
|
|
.or().le("scheduled_pickup_start_at", now));
|
|
|
- // 031:只展示骑手接受其支付方式的订单(现金单恒可见,见 acceptedRiderPayTypes)
|
|
|
|
|
- if (acceptedPayTypes.isEmpty()) {
|
|
|
|
|
- query.apply("1 = 0");
|
|
|
|
|
- } else {
|
|
|
|
|
- query.in("pay_type", acceptedPayTypes);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 同时满足车型和骑手已接受的支付方式,列表不能替代接单接口的资格复核。
|
|
|
|
|
+ if (acceptedPayTypes.isEmpty()) query.apply("1 = 0");
|
|
|
|
|
+ else query.in("pay_type", acceptedPayTypes);
|
|
|
if (longitude != null) {
|
|
if (longitude != null) {
|
|
|
Integer limitKm = newTaskDistanceLimit();
|
|
Integer limitKm = newTaskDistanceLimit();
|
|
|
if (limitKm != null) {
|
|
if (limitKm != null) {
|
|
@@ -1253,33 +1283,27 @@ public class FlashDeliveryApplicationService {
|
|
|
return query;
|
|
return query;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 校验骑手坐标:经纬度必须成对出现且在合法范围。 */
|
|
|
|
|
- /**
|
|
|
|
|
- * 骑手接受的支付方式展开为 payType 集合。
|
|
|
|
|
- * 未设置(NULL)=平台闪送维度开放的全部组;现金 4 不纳入管控但闪送现金单恒可见,
|
|
|
|
|
- * 因此始终并入集合(与闸门对现金直接放行的口径一致)。
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /** 骑手未设置闪送收款方式时继承平台开放项;现金单按既有规则始终可见。 */
|
|
|
private Set<String> acceptedRiderPayTypes(InfoUser rider) {
|
|
private Set<String> acceptedRiderPayTypes(InfoUser rider) {
|
|
|
String csv = rider.getFlashPayMethods();
|
|
String csv = rider.getFlashPayMethods();
|
|
|
List<String> groups = new ArrayList<>();
|
|
List<String> groups = new ArrayList<>();
|
|
|
- if (csv == null || csv.trim().isEmpty()) {
|
|
|
|
|
|
|
+ if (!hasText(csv)) {
|
|
|
for (PaymentMethodGateService.PayMethodOption option : paymentMethodGateService.listAvailable(
|
|
for (PaymentMethodGateService.PayMethodOption option : paymentMethodGateService.listAvailable(
|
|
|
PaymentMethodGateService.GateScope.RIDER_FLASH, null, null)) {
|
|
PaymentMethodGateService.GateScope.RIDER_FLASH, null, null)) {
|
|
|
if (!groups.contains(option.getMethodCode())) groups.add(option.getMethodCode());
|
|
if (!groups.contains(option.getMethodCode())) groups.add(option.getMethodCode());
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
for (String part : csv.split(",")) {
|
|
for (String part : csv.split(",")) {
|
|
|
- if (!part.trim().isEmpty()) groups.add(part.trim());
|
|
|
|
|
|
|
+ if (hasText(part)) groups.add(part.trim());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
Set<String> payTypes = new LinkedHashSet<>();
|
|
Set<String> payTypes = new LinkedHashSet<>();
|
|
|
payTypes.add(PAY_TYPE_CASH);
|
|
payTypes.add(PAY_TYPE_CASH);
|
|
|
- for (String group : groups) {
|
|
|
|
|
- payTypes.addAll(PaymentMethodGateService.payTypesOf(group));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for (String group : groups) payTypes.addAll(PaymentMethodGateService.payTypesOf(group));
|
|
|
return payTypes;
|
|
return payTypes;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 校验骑手坐标:经纬度必须成对出现且在合法范围。 */
|
|
|
private void validateRiderCoordinates(BigDecimal longitude, BigDecimal latitude) {
|
|
private void validateRiderCoordinates(BigDecimal longitude, BigDecimal latitude) {
|
|
|
if ((longitude == null) != (latitude == null)
|
|
if ((longitude == null) != (latitude == null)
|
|
|
|| longitude != null && (longitude.compareTo(BigDecimal.valueOf(-180)) < 0
|
|
|| longitude != null && (longitude.compareTo(BigDecimal.valueOf(-180)) < 0
|
|
@@ -1368,7 +1392,329 @@ public class FlashDeliveryApplicationService {
|
|
|
private String normalized(String value) { return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); }
|
|
private String normalized(String value) { return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); }
|
|
|
private ServiceException fail(String key) { return new ServiceException(MessageUtils.message(key)); }
|
|
private ServiceException fail(String key) { return new ServiceException(MessageUtils.message(key)); }
|
|
|
|
|
|
|
|
|
|
+ /** 将旧单点输入适配为完整取货站;多点请求必须逐站申报物品。 */
|
|
|
|
|
+ private List<FlashDeliveryPickupRequest> normalizePickups(FlashDeliveryQuoteRequest request) {
|
|
|
|
|
+ if (request.getPickups() != null) {
|
|
|
|
|
+ if (request.getPickup() != null || request.getPackageType() != null || request.getQuantity() != null
|
|
|
|
|
+ || request.getWeightRange() != null || request.getSpecification() != null) {
|
|
|
|
|
+ throw fail("flash.delivery.pickup.input.conflict");
|
|
|
|
|
+ }
|
|
|
|
|
+ return request.getPickups();
|
|
|
|
|
+ }
|
|
|
|
|
+ FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
|
|
|
|
|
+ if (request.getPickup() == null) throw fail("flash.delivery.address.invalid");
|
|
|
|
|
+ BeanUtils.copyProperties(request.getPickup(), pickup);
|
|
|
|
|
+ pickup.setPackageType(request.getPackageType());
|
|
|
|
|
+ pickup.setQuantity(request.getQuantity());
|
|
|
|
|
+ pickup.setWeightRange(request.getWeightRange());
|
|
|
|
|
+ pickup.setSpecification(request.getSpecification());
|
|
|
|
|
+ return List.of(pickup);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 编辑提交完整站点;旧单点地址编辑保留该站原有物品信息。 */
|
|
|
|
|
+ private List<FlashDeliveryPickupRequest> editablePickups(FlashDeliveryOrder order,
|
|
|
|
|
+ FlashDeliveryAddressChangeRequest request) {
|
|
|
|
|
+ List<FlashDeliveryOrderStop> existing = requireStops(order);
|
|
|
|
|
+ if (request.getPickups() != null) {
|
|
|
|
|
+ if (request.getPickup() != null) throw fail("flash.delivery.pickup.input.conflict");
|
|
|
|
|
+ return request.getPickups();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<FlashDeliveryOrderStop> pickups = pickupStops(existing);
|
|
|
|
|
+ if (pickups.size() != 1) throw fail("flash.delivery.pickups.required");
|
|
|
|
|
+ if (request.getPickup() == null) throw fail("flash.delivery.address.invalid");
|
|
|
|
|
+ FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
|
|
|
|
|
+ BeanUtils.copyProperties(pickups.get(0), pickup);
|
|
|
|
|
+ BeanUtils.copyProperties(request.getPickup(), pickup);
|
|
|
|
|
+ return List.of(pickup);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 地图调用前先校验有效点数、开关及每站地址和物品。 */
|
|
|
|
|
+ private void validatePickups(List<FlashDeliveryPickupRequest> pickups, FlashDeliveryAddressRequest delivery) {
|
|
|
|
|
+ if (pickups == null || pickups.isEmpty() || pickups.size() > pickupStopLimit()) {
|
|
|
|
|
+ throw fail("flash.delivery.pickup.limit.exceeded");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pickups.size() > 1 && !dictionaryEnabled("flash_show_multi_pickup")) {
|
|
|
|
|
+ throw fail("flash.delivery.multi.pickup.disabled");
|
|
|
|
|
+ }
|
|
|
|
|
+ validateAddress(delivery);
|
|
|
|
|
+ for (FlashDeliveryPickupRequest pickup : pickups) {
|
|
|
|
|
+ validateAddress(pickup);
|
|
|
|
|
+ if (!PACKAGE_TYPES.contains(Objects.toString(pickup.getPackageType(), ""))) {
|
|
|
|
|
+ throw fail("flash.delivery.package.type.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 重量与规格仅供骑手判断物品情况,用户可不填写;填写重量时仍须使用受支持的区间。
|
|
|
|
|
+ if (pickup.getQuantity() == null || pickup.getQuantity() <= 0
|
|
|
|
|
+ || hasText(pickup.getWeightRange()) && !WEIGHT_RANGES.contains(pickup.getWeightRange())
|
|
|
|
|
+ || tooLong(pickup.getSpecification(), 255)) throw fail("flash.delivery.item.invalid");
|
|
|
|
|
+ if (sameAddressText(pickup, delivery)
|
|
|
|
|
+ || pickup.getLatitude().compareTo(delivery.getLatitude()) == 0
|
|
|
|
|
+ && pickup.getLongitude().compareTo(delivery.getLongitude()) == 0) {
|
|
|
|
|
+ throw fail("flash.delivery.address.same");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private RouteDistance pickupRoute(List<FlashDeliveryPickupRequest> pickups, FlashDeliveryAddressRequest delivery,
|
|
|
|
|
+ Integer vehicleType) {
|
|
|
|
|
+ List<GeoPoint> points = new ArrayList<>();
|
|
|
|
|
+ for (FlashDeliveryPickupRequest pickup : pickups) points.add(new GeoPoint(pickup.getLatitude(), pickup.getLongitude()));
|
|
|
|
|
+ points.add(new GeoPoint(delivery.getLatitude(), delivery.getLongitude()));
|
|
|
|
|
+ RouteDistance route = points.size() == 2
|
|
|
|
|
+ ? routeService.calculate(points.get(0), points.get(1), vehicleType)
|
|
|
|
|
+ : routeService.calculate(points, vehicleType);
|
|
|
|
|
+ if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
|
|
|
|
|
+ return route;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Integer requireVehicleType(Integer vehicleType) {
|
|
|
|
|
+ if (!FlashDeliveryVehicleType.isSupported(vehicleType)) {
|
|
|
|
|
+ throw fail("flash.delivery.vehicle.type.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ return vehicleType;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** DDL 执行前存在的旧订单按用户确认的机车规则兼容。 */
|
|
|
|
|
+ private Integer vehicleTypeOf(FlashDeliveryOrder order) {
|
|
|
|
|
+ return FlashDeliveryVehicleType.isSupported(order.getVehicleType())
|
|
|
|
|
+ ? order.getVehicleType() : FlashDeliveryVehicleType.MOTORCYCLE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 骑手资料复用既有 vehicleType;历史空值和非法值安全回落为机车。 */
|
|
|
|
|
+ private Integer riderVehicleType(InfoUser rider) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Integer value = Integer.valueOf(rider.getVehicleType());
|
|
|
|
|
+ return FlashDeliveryVehicleType.isSupported(value) ? value : FlashDeliveryVehicleType.MOTORCYCLE;
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ return FlashDeliveryVehicleType.MOTORCYCLE;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isRiderVehicleCompatible(InfoUser rider, FlashDeliveryOrder order) {
|
|
|
|
|
+ return riderVehicleType(rider).equals(vehicleTypeOf(order));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 点数配置缺失、非整数或越界时,有效上限回退为1。 */
|
|
|
|
|
+ private int pickupStopLimit() {
|
|
|
|
|
+ String value = dictionaryValue("flash_pickup_stop_limit");
|
|
|
|
|
+ if (value == null || !value.matches("[0-9]+")) return 1;
|
|
|
|
|
+ try {
|
|
|
|
|
+ int limit = Integer.parseInt(value);
|
|
|
|
|
+ return limit >= 1 && limit <= 26 ? limit : 1;
|
|
|
|
|
+ } 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);
|
|
|
|
|
+ return values == null || values.isEmpty() ? null : values.get(0).getDictValue();
|
|
|
|
|
+ } catch (Exception ignored) { return null; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 订单旧字段仅维护首站兼容摘要,不代表全部站点物品。 */
|
|
|
|
|
+ private void copyPickupSummary(FlashDeliveryOrder order, List<FlashDeliveryPickupRequest> pickups) {
|
|
|
|
|
+ FlashDeliveryPickupRequest first = pickups.get(0);
|
|
|
|
|
+ copyAddress(first, order, true);
|
|
|
|
|
+ order.setPackageType(first.getPackageType()); order.setQuantity(first.getQuantity());
|
|
|
|
|
+ order.setWeightRange(first.getWeightRange()); order.setSpecification(trimToNull(first.getSpecification()));
|
|
|
|
|
+ order.setPickupStopCount(pickups.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 调用方已创建订单或持有订单行锁,全部站点与订单在同一事务保存。 */
|
|
|
|
|
+ private void saveStops(FlashDeliveryOrder order, List<FlashDeliveryPickupRequest> pickups,
|
|
|
|
|
+ FlashDeliveryAddressRequest delivery, Date now) {
|
|
|
|
|
+ for (int i = 0; i < pickups.size(); i++) insertStop(order.getId(), pickups.get(i), "PICKUP", i, now);
|
|
|
|
|
+ insertStop(order.getId(), delivery, "DELIVERY", pickups.size(), now);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void insertStop(Long orderId, FlashDeliveryAddressRequest address, String type, int index, Date now) {
|
|
|
|
|
+ FlashDeliveryOrderStop stop = new FlashDeliveryOrderStop();
|
|
|
|
|
+ BeanUtils.copyProperties(address, stop);
|
|
|
|
|
+ stop.setName(address.getName().trim()); stop.setPhone(address.getPhone().trim());
|
|
|
|
|
+ stop.setAddress(address.getAddress().trim()); stop.setAddressDetail(trimToNull(address.getAddressDetail()));
|
|
|
|
|
+ stop.setCity(trimToNull(address.getCity())); stop.setArea(trimToNull(address.getArea()));
|
|
|
|
|
+ stop.setHandoffMethod(trimToNull(address.getHandoffMethod())); stop.setSpecification(trimToNull(stop.getSpecification()));
|
|
|
|
|
+ stop.setOrderId(orderId); stop.setStopType(type); stop.setStopOrder(index);
|
|
|
|
|
+ stop.setStatus("PENDING"); stop.setCreateTime(now);
|
|
|
|
|
+ if (stopMapper.insert(stop) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 事务内使用当前读,避免骑手资料查询建立的旧快照遮蔽锁等待期间的履约变化;GET 不加锁。 */
|
|
|
|
|
+ private <T> QueryWrapper<T> currentRead(QueryWrapper<T> query) {
|
|
|
|
|
+ if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
|
|
|
|
+ query.last("FOR UPDATE");
|
|
|
|
|
+ }
|
|
|
|
|
+ return query;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<FlashDeliveryOrderStop> stops(Long orderId) {
|
|
|
|
|
+ return stopMapper.selectList(currentRead(new QueryWrapper<FlashDeliveryOrderStop>()
|
|
|
|
|
+ .eq("order_id", orderId).orderByAsc("stop_order", "id")));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 履约必须有真实站点记录,不从旧测试订单的摘要字段合成站点。 */
|
|
|
|
|
+ private List<FlashDeliveryOrderStop> requireStops(FlashDeliveryOrder order) {
|
|
|
|
|
+ List<FlashDeliveryOrderStop> rows = stops(order.getId());
|
|
|
|
|
+ if (rows == null || pickupStops(rows).isEmpty()
|
|
|
|
|
+ || !Objects.equals(order.getPickupStopCount(), pickupStops(rows).size())
|
|
|
|
|
+ || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != 1) {
|
|
|
|
|
+ throw fail("flash.delivery.stops.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ return rows;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<FlashDeliveryOrderStop> pickupStops(List<FlashDeliveryOrderStop> rows) {
|
|
|
|
|
+ return rows.stream().filter(s -> "PICKUP".equals(s.getStopType())).toList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean anyPicked(List<FlashDeliveryOrderStop> rows) {
|
|
|
|
|
+ return rows.stream().anyMatch(s -> "PICKUP".equals(s.getStopType()) && "PICKED_UP".equals(s.getStatus()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean allPicked(List<FlashDeliveryOrderStop> rows) {
|
|
|
|
|
+ List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
|
|
|
|
|
+ return !pickups.isEmpty() && pickups.stream().allMatch(s -> "PICKED_UP".equals(s.getStatus()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 选择的目标在刷新后保留;该站取货后回到原顺序最早的待取站点。 */
|
|
|
|
|
+ private Long targetStopId(FlashDeliveryOrder order, List<FlashDeliveryOrderStop> rows) {
|
|
|
|
|
+ if (CANCELLED.equals(order.getStatus()) || COMPLETED.equals(order.getStatus()) || DELIVERED.equals(order.getStatus())) return null;
|
|
|
|
|
+ if (order.getCurrentTargetStopId() != null && rows.stream().anyMatch(s ->
|
|
|
|
|
+ Objects.equals(s.getId(), order.getCurrentTargetStopId()) && "PICKUP".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));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 事务内先锁订单并读取最新状态,再查询站点,统一取货、调整与取消的并发边界。 */
|
|
|
|
|
+ private FlashDeliveryOrder lockOrder(Long orderId) {
|
|
|
|
|
+ FlashDeliveryOrder order = orderId == null ? null : orderMapper.selectByIdForUpdate(orderId);
|
|
|
|
|
+ if (order == null) throw fail("flash.delivery.order.not.found");
|
|
|
|
|
+ return order;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FlashDeliveryOrderStop requireStop(List<FlashDeliveryOrderStop> rows, Long stopId) {
|
|
|
|
|
+ return rows.stream().filter(s -> Objects.equals(s.getId(), stopId)).findFirst()
|
|
|
|
|
+ .orElseThrow(() -> fail("flash.delivery.stop.invalid"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 选择前往目标只记录履约调整,不修改取件进度、金额或用户站点顺序。 */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public FlashDeliveryOrderDetailView selectTarget(Long riderId, Long orderId, Long stopId,
|
|
|
|
|
+ FlashDeliveryTargetRequest request) {
|
|
|
|
|
+ requireRider(riderId);
|
|
|
|
|
+ FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
|
|
+ if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
|
|
|
|
|
+ if (request == null || !hasText(request.getClientRequestId()) || tooLong(request.getClientRequestId(), 64)) {
|
|
|
|
|
+ throw fail("flash.delivery.client.request.id.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ String requestId = request.getClientRequestId().trim();
|
|
|
|
|
+ FlashDeliveryTargetAdjustment previous = adjustmentMapper.selectOne(new QueryWrapper<FlashDeliveryTargetAdjustment>()
|
|
|
|
|
+ .eq("order_id", orderId).eq("rider_id", riderId).eq("client_request_id", requestId).last("FOR UPDATE"));
|
|
|
|
|
+ if (previous != null) {
|
|
|
|
|
+ if (!Objects.equals(previous.getToStopId(), stopId)
|
|
|
|
|
+ || !Objects.equals(previous.getOrderVersion(), request.getOrderVersion())
|
|
|
|
|
+ || !Objects.equals(previous.getReason(), trimToNull(request.getReason()))
|
|
|
|
|
+ || !Objects.equals(previous.getConsentDeclared(), Boolean.TRUE.equals(request.getConsentDeclared()))) {
|
|
|
|
|
+ throw fail("flash.delivery.target.request.conflict");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 重试返回当前进度,不把已过期的历史目标重新写回订单。
|
|
|
|
|
+ return participantDetail(order, false, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!ACCEPTED.equals(order.getStatus())) 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");
|
|
|
|
|
+ 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");
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ FlashDeliveryTargetAdjustment adjustment = new FlashDeliveryTargetAdjustment();
|
|
|
|
|
+ adjustment.setOrderId(orderId); adjustment.setFromStopId(before); adjustment.setToStopId(stopId);
|
|
|
|
|
+ adjustment.setRiderId(riderId); adjustment.setClientRequestId(requestId); adjustment.setOrderVersion(request.getOrderVersion());
|
|
|
|
|
+ adjustment.setReason(trimToNull(request.getReason())); adjustment.setConsentDeclared(Boolean.TRUE.equals(request.getConsentDeclared())); adjustment.setCreateTime(now);
|
|
|
|
|
+ if (adjustmentMapper.insert(adjustment) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ order.setCurrentTargetStopId(stopId); order.setUpdateTime(now);
|
|
|
|
|
+ if (orderMapper.updateFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ order.setVersion(order.getVersion() + 1);
|
|
|
|
|
+ return participantDetail(order, false, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 两种取货接口共用同一加锁流程,路径与请求体中的站点编号必须一致。 */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public void pickupAtStop(Long riderId, Long orderId, Long stopId, FlashDeliveryProofRequest request) {
|
|
|
|
|
+ if (request == null) throw fail("flash.delivery.request.required");
|
|
|
|
|
+ if (request.getStopId() != null && !Objects.equals(request.getStopId(), stopId)) throw fail("flash.delivery.stop.invalid");
|
|
|
|
|
+ pickupStation(riderId, orderId, stopId, request);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void pickupStation(Long riderId, Long orderId, Long requestedStopId, FlashDeliveryProofRequest request) {
|
|
|
|
|
+ requireRider(riderId);
|
|
|
|
|
+ FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
|
|
+ if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
|
|
|
|
|
+ List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
|
|
|
+ List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
|
|
|
|
|
+ Long stopId = requestedStopId;
|
|
|
|
|
+ if (stopId == null) {
|
|
|
|
|
+ if (pickups.size() != 1) throw fail("flash.delivery.stop.id.required");
|
|
|
|
|
+ stopId = pickups.get(0).getId();
|
|
|
|
|
+ }
|
|
|
|
|
+ FlashDeliveryOrderStop stop = requireStop(rows, stopId);
|
|
|
|
|
+ if (!"PICKUP".equals(stop.getStopType())) throw fail("flash.delivery.stop.invalid");
|
|
|
|
|
+ // 已确认站点的重试直接成功,不因当前目标或主状态已变化而重复写凭证。
|
|
|
|
|
+ if ("PICKED_UP".equals(stop.getStatus())) return;
|
|
|
|
|
+ if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
|
|
|
|
|
+ if (!Objects.equals(stopId, targetStopId(order, rows))) throw fail("flash.delivery.target.mismatch");
|
|
|
|
|
+ List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ stop.setStatus("PICKED_UP"); stop.setPickedUpAt(now);
|
|
|
|
|
+ if (stopMapper.updateById(stop) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ saveStationImages(orderId, stopId, "PICKUP", "RIDER", riderId, urls, now);
|
|
|
|
|
+ order.setPickedUpStopCount((int) pickups.stream().filter(s -> "PICKED_UP".equals(s.getStatus())).count());
|
|
|
|
|
+ order.setCurrentTargetStopId(null); order.setUpdateTime(now);
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void saveStationImages(Long orderId, Long stopId, String proofType, String operatorType,
|
|
|
|
|
+ Long operatorId, List<String> urls, Date now) {
|
|
|
|
|
+ for (int index = 0; index < urls.size(); index++) {
|
|
|
|
|
+ FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
|
|
|
|
|
+ image.setOrderId(orderId); image.setStopId(stopId); image.setProofType(proofType);
|
|
|
|
|
+ image.setOperatorType(operatorType); image.setOperatorId(operatorId); image.setImageUrl(urls.get(index));
|
|
|
|
|
+ image.setSortOrder(index); image.setCreateTime(now);
|
|
|
|
|
+ if (imageMapper.insert(image) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<FlashDeliveryStopView> stopViews(List<FlashDeliveryOrderStop> rows, boolean sensitive,
|
|
|
|
|
+ List<FlashDeliveryOrderImage> images) {
|
|
|
|
|
+ return rows.stream().map(stop -> {
|
|
|
|
|
+ FlashDeliveryStopView view = new FlashDeliveryStopView();
|
|
|
|
|
+ BeanUtils.copyProperties(stop, view);
|
|
|
|
|
+ if (sensitive) view.setImageUrls(images.stream().filter(i -> Objects.equals(i.getStopId(), stop.getId())
|
|
|
|
|
+ && Objects.equals(i.getProofType(), stop.getStopType()))
|
|
|
|
|
+ .map(FlashDeliveryOrderImage::getImageUrl).toList());
|
|
|
|
|
+ else { view.setName(null); view.setPhone(null); view.setLongitude(null); view.setLatitude(null); }
|
|
|
|
|
+ return view;
|
|
|
|
|
+ }).toList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean canUserCancel(FlashDeliveryOrder order) {
|
|
|
|
|
+ return FlashDeliveryStateMachine.canUserCancel(order.getStatus())
|
|
|
|
|
+ && Integer.valueOf(0).equals(order.getPickedUpStopCount());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
/** 报价中间结果:命中的运价配置、路线距离和费用明细。 */
|
|
/** 报价中间结果:命中的运价配置、路线距离和费用明细。 */
|
|
|
private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
|
|
private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
|
|
|
- FlashDeliveryPriceBreakdown breakdown) { }
|
|
|
|
|
|
|
+ FlashDeliveryPriceBreakdown breakdown, List<FlashDeliveryPickupRequest> pickups) { }
|
|
|
}
|
|
}
|