|
@@ -89,6 +89,10 @@ public class FlashDeliveryApplicationService {
|
|
|
private final PaymentMethodGateService paymentMethodGateService;
|
|
private final PaymentMethodGateService paymentMethodGateService;
|
|
|
private final FlashDeliveryNotificationService notificationService;
|
|
private final FlashDeliveryNotificationService notificationService;
|
|
|
private final UserBillingMapper userBillingMapper;
|
|
private final UserBillingMapper userBillingMapper;
|
|
|
|
|
+ @org.springframework.beans.factory.annotation.Autowired
|
|
|
|
|
+ private com.ruoyi.app.flashdelivery.payment.FlashLinePayFactService flashLinePayFactService;
|
|
|
|
|
+ @org.springframework.beans.factory.annotation.Autowired
|
|
|
|
|
+ private com.ruoyi.app.flashdelivery.payment.FlashLinePayService flashLinePayService;
|
|
|
|
|
|
|
|
public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
|
|
public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
|
|
|
FlashDeliveryPricingMapper pricingMapper,
|
|
FlashDeliveryPricingMapper pricingMapper,
|
|
@@ -320,7 +324,7 @@ public class FlashDeliveryApplicationService {
|
|
|
private FlashDeliveryOrderDetailView userDetail(Long userId, FlashDeliveryOrder order) {
|
|
private FlashDeliveryOrderDetailView userDetail(Long userId, FlashDeliveryOrder order) {
|
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
|
|
|
FlashDeliveryOrderDetailView view = participantDetail(order, true, false);
|
|
FlashDeliveryOrderDetailView view = participantDetail(order, true, false);
|
|
|
- view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && canUserCancel(order));
|
|
|
|
|
|
|
+ view.setCanUserCancel(Objects.equals(userId, order.getUserId()) && Boolean.TRUE.equals(view.getCanUserCancel()));
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -465,12 +469,14 @@ public class FlashDeliveryApplicationService {
|
|
|
public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
FlashDeliveryOrder order = lockOrder(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 (anyPicked(requireStops(order)) || !FlashDeliveryStateMachine.canUserCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
|
|
|
|
|
|
|
+ if (!canCancelBeforePickup(order, requireStops(order))) 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) {
|
|
|
throw fail("flash.delivery.state.changed");
|
|
throw fail("flash.delivery.state.changed");
|
|
|
}
|
|
}
|
|
|
|
|
+ // 取消和退款意图共用订单锁、同事务提交;外部退款由持久任务执行,不在这里等待网络。
|
|
|
|
|
+ if ("3".equals(order.getPayType())) flashLinePayFactService.ensureCancellationRefundIntent(orderId);
|
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "USER", userId, reason, now);
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "USER", userId, reason, now);
|
|
|
// 取消成功后通知接单骑手;未接单订单由通知服务内部跳过。
|
|
// 取消成功后通知接单骑手;未接单订单由通知服务内部跳过。
|
|
|
notificationService.notifyCancelled(order);
|
|
notificationService.notifyCancelled(order);
|
|
@@ -587,6 +593,8 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
// 账号资格、闪送/总任务容量与急送独占统一在骑手锁内复核,列表结果不能替代接单校验。
|
|
// 账号资格、闪送/总任务容量与急送独占统一在骑手锁内复核,列表结果不能替代接单校验。
|
|
|
riderDeliveryAcceptanceService.assertCanAcceptFlash(riderId, target.getDeliveryType());
|
|
riderDeliveryAcceptanceService.assertCanAcceptFlash(riderId, target.getDeliveryType());
|
|
|
|
|
+ // 列表过滤不是授权,直接传订单 ID 抢单同样复核骑手收款配置和支付选择。
|
|
|
|
|
+ paymentMethodGateService.assertUsable(target.getPayType(), PaymentMethodGateService.GateScope.RIDER_FLASH, null, riderId);
|
|
|
// Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
|
|
// Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
|
|
|
if (orderMapper.accept(orderId, riderId, now) != 1) {
|
|
if (orderMapper.accept(orderId, riderId, now) != 1) {
|
|
|
throw fail("flash.delivery.order.already.accepted");
|
|
throw fail("flash.delivery.order.already.accepted");
|
|
@@ -718,7 +726,15 @@ public class FlashDeliveryApplicationService {
|
|
|
if (hasText(orderNo)) query.like("order_no", orderNo.trim());
|
|
if (hasText(orderNo)) query.like("order_no", orderNo.trim());
|
|
|
if (userId != null) query.eq("user_id", userId);
|
|
if (userId != null) query.eq("user_id", userId);
|
|
|
if (riderId != null) query.eq("rider_id", riderId);
|
|
if (riderId != null) query.eq("rider_id", riderId);
|
|
|
- return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
|
|
|
|
+ IPage<FlashDeliveryOrder> result = orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
|
|
+ if (!result.getRecords().isEmpty()) {
|
|
|
|
|
+ List<Long> ids = result.getRecords().stream().map(FlashDeliveryOrder::getId).toList();
|
|
|
|
|
+ // 批量读取站点,不能只按主状态猜测部分取件;避免列表 N+1 查询。
|
|
|
|
|
+ Map<Long, List<FlashDeliveryOrderStop>> grouped = stopMapper.selectList(new QueryWrapper<FlashDeliveryOrderStop>()
|
|
|
|
|
+ .in("order_id", ids)).stream().collect(java.util.stream.Collectors.groupingBy(FlashDeliveryOrderStop::getOrderId));
|
|
|
|
|
+ result.getRecords().forEach(order -> order.setCanAdminCancel(canCancelBeforePickup(order, grouped.getOrDefault(order.getId(), List.of()))));
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 平台订单审计详情:返回完整订单实体、全部凭证图片和状态日志。 */
|
|
/** 平台订单审计详情:返回完整订单实体、全部凭证图片和状态日志。 */
|
|
@@ -727,8 +743,11 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
|
|
FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
|
|
|
view.setOrder(order);
|
|
view.setOrder(order);
|
|
|
List<FlashDeliveryOrderStop> stationRows = stops(orderId);
|
|
List<FlashDeliveryOrderStop> stationRows = stops(orderId);
|
|
|
|
|
+ view.setCanAdminCancel(canCancelBeforePickup(order, stationRows));
|
|
|
|
|
+ order.setCanAdminCancel(view.getCanAdminCancel());
|
|
|
|
|
+ if ("3".equals(order.getPayType())) view.setLinePayPayment(flashLinePayService.localStatus(orderId));
|
|
|
view.setPickupStopCount(order.getPickupStopCount()); view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
view.setPickupStopCount(order.getPickupStopCount()); view.setPickedUpStopCount(order.getPickedUpStopCount());
|
|
|
- view.setCanUserCancel(canUserCancel(order)); view.setCurrentTargetStopId(targetStopId(order, stationRows));
|
|
|
|
|
|
|
+ view.setCanUserCancel(view.getCanAdminCancel()); view.setCurrentTargetStopId(targetStopId(order, stationRows));
|
|
|
view.setTargetAdjustments(adjustmentMapper.selectList(new QueryWrapper<FlashDeliveryTargetAdjustment>()
|
|
view.setTargetAdjustments(adjustmentMapper.selectList(new QueryWrapper<FlashDeliveryTargetAdjustment>()
|
|
|
.eq("order_id", orderId).orderByAsc("create_time", "id")));
|
|
.eq("order_id", orderId).orderByAsc("create_time", "id")));
|
|
|
view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
@@ -741,30 +760,25 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 平台取消:任何非终态订单可取消,记录操作人与原因,用于异常履约人工收口。 */
|
|
|
|
|
|
|
+ /** 平台取消仅取件前允许;已有任一站取件时只能客服处理,不能取消并自动退款。 */
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
FlashDeliveryOrder order = lockOrder(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);
|
|
|
List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
|
|
|
+ if (!canCancelBeforePickup(order, rows)) throw fail("flash.delivery.cancel.not.allowed");
|
|
|
if (request.getProblemStopId() == null || !hasText(request.getHandlingResult()) || tooLong(request.getHandlingResult(), 500)) {
|
|
if (request.getProblemStopId() == null || !hasText(request.getHandlingResult()) || tooLong(request.getHandlingResult(), 500)) {
|
|
|
throw fail("flash.delivery.intervention.required");
|
|
throw fail("flash.delivery.intervention.required");
|
|
|
}
|
|
}
|
|
|
requireStop(rows, request.getProblemStopId());
|
|
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();
|
|
Date now = new Date();
|
|
|
- // 关闭异常订单前保存介入结果和物品去向;凭证写入失败时整笔事务回滚。
|
|
|
|
|
|
|
+ // 保留取件前的问题站和处理结果;旧物品交接数据仍可在历史详情中查看。
|
|
|
UpdateWrapper<FlashDeliveryOrder> audit = new UpdateWrapper<FlashDeliveryOrder>().eq("id", orderId)
|
|
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()));
|
|
|
|
|
|
|
+ .set("problem_stop_id", request.getProblemStopId()).set("handling_result", request.getHandlingResult().trim());
|
|
|
if (orderMapper.update(null, audit) != 1) throw fail("flash.delivery.state.changed");
|
|
if (orderMapper.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");
|
|
if (orderMapper.cancel(orderId, order.getStatus(), "ADMIN", adminId, reason, now) != 1) throw fail("flash.delivery.state.changed");
|
|
|
|
|
+ if ("3".equals(order.getPayType())) flashLinePayFactService.ensureCancellationRefundIntent(orderId);
|
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
|
|
|
// 平台取消同样只通知接单骑手;寄件用户按规格不推送。
|
|
// 平台取消同样只通知接单骑手;寄件用户按规格不推送。
|
|
|
notificationService.notifyCancelled(order);
|
|
notificationService.notifyCancelled(order);
|
|
@@ -817,6 +831,8 @@ public class FlashDeliveryApplicationService {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
|
|
if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
|
|
|
|
|
+ // LINE Pay 只认网关到账事实,骑手不能用线下收款确认接口伪造线上支付。
|
|
|
|
|
+ if ("3".equals(order.getPayType())) throw fail("flash.line.manual.forbidden");
|
|
|
if (!DELIVERED.equals(order.getStatus()) && !COMPLETED.equals(order.getStatus())) {
|
|
if (!DELIVERED.equals(order.getStatus()) && !COMPLETED.equals(order.getStatus())) {
|
|
|
throw fail("flash.delivery.payment.confirm.not.allowed");
|
|
throw fail("flash.delivery.payment.confirm.not.allowed");
|
|
|
}
|
|
}
|
|
@@ -1030,6 +1046,7 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
|
|
FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
|
|
|
if (!hideSensitiveFields) view.setOrderVersion(order.getVersion());
|
|
if (!hideSensitiveFields) view.setOrderVersion(order.getVersion());
|
|
|
List<FlashDeliveryOrderStop> stationRows = stops(order.getId());
|
|
List<FlashDeliveryOrderStop> stationRows = stops(order.getId());
|
|
|
|
|
+ view.setCanUserCancel(canCancelBeforePickup(order, stationRows));
|
|
|
List<FlashDeliveryOrderImage> stationImages = hideSensitiveFields ? List.of() : imageMapper.selectList(currentRead(
|
|
List<FlashDeliveryOrderImage> stationImages = hideSensitiveFields ? List.of() : imageMapper.selectList(currentRead(
|
|
|
new QueryWrapper<FlashDeliveryOrderImage>().eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
|
|
new QueryWrapper<FlashDeliveryOrderImage>().eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
|
|
|
view.setStops(stopViews(stationRows, !hideSensitiveFields, stationImages));
|
|
view.setStops(stopViews(stationRows, !hideSensitiveFields, stationImages));
|
|
@@ -1745,7 +1762,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private boolean anyPicked(List<FlashDeliveryOrderStop> rows) {
|
|
private boolean anyPicked(List<FlashDeliveryOrderStop> rows) {
|
|
|
- return rows.stream().anyMatch(s -> "PICKUP".equals(s.getStopType()) && "PICKED_UP".equals(s.getStatus()));
|
|
|
|
|
|
|
+ return rows.stream().anyMatch(s -> "PICKUP".equals(s.getStopType()) && ("PICKED_UP".equals(s.getStatus()) || s.getPickedUpAt() != null));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private boolean allPicked(List<FlashDeliveryOrderStop> rows) {
|
|
private boolean allPicked(List<FlashDeliveryOrderStop> rows) {
|
|
@@ -1848,6 +1865,8 @@ public class FlashDeliveryApplicationService {
|
|
|
if ("PICKED_UP".equals(stop.getStatus())) return;
|
|
if ("PICKED_UP".equals(stop.getStatus())) return;
|
|
|
if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
|
|
if (!ACCEPTED.equals(order.getStatus())) throw fail("flash.delivery.transition.not.allowed");
|
|
|
if (!Objects.equals(stopId, targetStopId(order, rows))) throw fail("flash.delivery.target.mismatch");
|
|
if (!Objects.equals(stopId, targetStopId(order, rows))) throw fail("flash.delivery.target.mismatch");
|
|
|
|
|
+ // 每站取件都在同一订单锁内验证可信资金事实,现金/转账保持原履约路径。
|
|
|
|
|
+ if ("3".equals(order.getPayType())) flashLinePayFactService.assertPickupPaid(order);
|
|
|
List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
stop.setStatus("PICKED_UP"); stop.setPickedUpAt(now);
|
|
stop.setStatus("PICKED_UP"); stop.setPickedUpAt(now);
|
|
@@ -1887,10 +1906,21 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private boolean canUserCancel(FlashDeliveryOrder order) {
|
|
private boolean canUserCancel(FlashDeliveryOrder order) {
|
|
|
- return FlashDeliveryStateMachine.canUserCancel(order.getStatus())
|
|
|
|
|
|
|
+ // 卡片用汇总字段显示提示,详情复用已读站点;取消接口始终在锁内核实真实站点。
|
|
|
|
|
+ return FlashDeliveryStateMachine.canUserCancel(order.getStatus()) && order.getPickedUpAt() == null
|
|
|
&& Integer.valueOf(0).equals(order.getPickedUpStopCount());
|
|
&& Integer.valueOf(0).equals(order.getPickedUpStopCount());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 缺失/不一致站点安全拒绝;不能把 ACCEPTED、汇总计数或旧数据当作未取件证据。 */
|
|
|
|
|
+ private boolean canCancelBeforePickup(FlashDeliveryOrder order, List<FlashDeliveryOrderStop> rows) {
|
|
|
|
|
+ if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus()) || order.getPickedUpAt() != null
|
|
|
|
|
+ || order.getPickedUpStopCount() != null && order.getPickedUpStopCount() > 0 || rows == null) return false;
|
|
|
|
|
+ List<FlashDeliveryOrderStop> pickups = pickupStops(rows);
|
|
|
|
|
+ return !pickups.isEmpty() && Objects.equals(order.getPickupStopCount(), pickups.size())
|
|
|
|
|
+ && rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() == 1 && !anyPicked(rows)
|
|
|
|
|
+ && pickups.stream().allMatch(s -> "PENDING".equals(s.getStatus()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
/** 报价中间结果:命中的运价配置、路线距离和费用明细。 */
|
|
/** 报价中间结果:命中的运价配置、路线距离和费用明细。 */
|
|
|
private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
|
|
private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
|