|
@@ -20,7 +20,9 @@ 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.mapper.flash.*;
|
|
import com.ruoyi.system.mapper.flash.*;
|
|
|
|
|
+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;
|
|
|
|
|
|
|
@@ -62,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;
|
|
@@ -79,8 +83,12 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryRouteService routeService,
|
|
FlashDeliveryRouteService routeService,
|
|
|
FlashDeliveryPricingCalculator pricingCalculator,
|
|
FlashDeliveryPricingCalculator pricingCalculator,
|
|
|
RiderDeliveryLockService riderDeliveryLockService,
|
|
RiderDeliveryLockService riderDeliveryLockService,
|
|
|
- RiderDeliveryExclusivityService riderDeliveryExclusivityService) {
|
|
|
|
|
|
|
+ RiderDeliveryExclusivityService riderDeliveryExclusivityService,
|
|
|
|
|
+ FlashDeliveryOrderStopMapper stopMapper,
|
|
|
|
|
+ FlashDeliveryTargetAdjustmentMapper adjustmentMapper) {
|
|
|
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;
|
|
@@ -94,6 +102,9 @@ 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"));
|
|
|
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()));
|
|
@@ -150,7 +161,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");
|
|
|
}
|
|
}
|
|
@@ -172,16 +183,14 @@ public class FlashDeliveryApplicationService {
|
|
|
order.setServiceType(request.getServiceType());
|
|
order.setServiceType(request.getServiceType());
|
|
|
order.setDeliveryType(request.getDeliveryType());
|
|
order.setDeliveryType(request.getDeliveryType());
|
|
|
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());
|
|
@@ -213,10 +222,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);
|
|
@@ -231,14 +242,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;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 地址修改先报价,用户确认前不改动订单。 */
|
|
/** 地址修改先报价,用户确认前不改动订单。 */
|
|
@@ -263,13 +280,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());
|
|
@@ -290,6 +308,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);
|
|
@@ -329,7 +350,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");
|
|
@@ -342,22 +365,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());
|
|
|
|
|
+ // 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(),
|
|
@@ -371,7 +388,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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;
|
|
|
}
|
|
}
|
|
@@ -379,9 +396,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) {
|
|
@@ -393,8 +410,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");
|
|
@@ -474,25 +492,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);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 平台运价配置列表,按时段开始时间排序。 */
|
|
/** 平台运价配置列表,按时段开始时间排序。 */
|
|
@@ -587,8 +617,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;
|
|
@@ -597,19 +635,35 @@ 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);
|
|
@@ -626,30 +680,18 @@ 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));
|
|
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");
|
|
|
|
|
|
|
+ RouteDistance route = pickupRoute(pickups, request.getDelivery());
|
|
|
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);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 查询指定时刻命中的运价配置,未配置时直接报错。 */
|
|
/** 查询指定时刻命中的运价配置,未配置时直接报错。 */
|
|
@@ -735,19 +777,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) {
|
|
@@ -792,14 +821,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;
|
|
|
}
|
|
}
|
|
@@ -812,6 +849,9 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setServiceType(order.getServiceType());
|
|
view.setServiceType(order.getServiceType());
|
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
|
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());
|
|
@@ -843,6 +883,9 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setServiceType(order.getServiceType());
|
|
view.setServiceType(order.getServiceType());
|
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
|
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());
|
|
@@ -918,6 +961,9 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setServiceType(order.getServiceType());
|
|
view.setServiceType(order.getServiceType());
|
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
view.setDeliveryType(order.getDeliveryType());
|
|
|
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());
|
|
@@ -1099,18 +1145,6 @@ 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");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/** 支付方式与主订单系统同域(4=现金、6=线下转账);闪送款项直达骑手,仅允许线下方式,缺省现金,下单后锁定。 */
|
|
/** 支付方式与主订单系统同域(4=现金、6=线下转账);闪送款项直达骑手,仅允许线下方式,缺省现金,下单后锁定。 */
|
|
|
private String normalizePayType(String payType) {
|
|
private String normalizePayType(String payType) {
|
|
|
if (!hasText(payType)) return PAY_TYPE_CASH;
|
|
if (!hasText(payType)) return PAY_TYPE_CASH;
|
|
@@ -1292,7 +1326,298 @@ 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
|
|
|
|
|
+ || !WEIGHT_RANGES.contains(Objects.toString(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) {
|
|
|
|
|
+ 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)) : routeService.calculate(points);
|
|
|
|
|
+ if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
|
|
|
|
|
+ return route;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 点数配置缺失、非整数或越界时,有效上限回退为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) { }
|
|
|
}
|
|
}
|