|
|
@@ -131,6 +131,8 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryHomeView view = new FlashDeliveryHomeView();
|
|
|
view.setPickupStopLimit(pickupStopLimit());
|
|
|
view.setShowMultiPickup(dictionaryEnabled("flash_show_multi_pickup"));
|
|
|
+ view.setDeliveryStopLimit(deliveryStopLimit());
|
|
|
+ view.setShowMultiDropoff(dictionaryEnabled("flash_show_multi_dropoff"));
|
|
|
view.setShowUrgentOption(dictionaryEnabled("flash_show_urgent_option"));
|
|
|
view.setServiceTypes(SERVICE_TYPES);
|
|
|
view.setDeliveryTypes(List.of("NORMAL", "URGENT"));
|
|
|
@@ -664,6 +666,302 @@ public class FlashDeliveryApplicationService {
|
|
|
notificationService.notifyDelivered(requireOrder(orderId));
|
|
|
}
|
|
|
|
|
|
+ /** v2 逐站送达:校验按站收货码并提交凭证;全部收货站完成才整单推进为已送达,仅推进一次。 */
|
|
|
+ @Transactional
|
|
|
+ public void deliverAtStop(Long riderId, Long orderId, Long stopId, FlashDeliveryDeliverRequest request) {
|
|
|
+ requireRider(riderId);
|
|
|
+ FlashDeliveryOrder order = lockOrder(orderId);
|
|
|
+ if (!Objects.equals(riderId, order.getRiderId()) || !PICKED_UP.equals(order.getStatus())) {
|
|
|
+ throw fail("flash.delivery.transition.not.allowed");
|
|
|
+ }
|
|
|
+ List<FlashDeliveryOrderStop> rows = requireStops(order);
|
|
|
+ if (!allPicked(rows)) throw fail("flash.delivery.transition.not.allowed");
|
|
|
+ FlashDeliveryOrderStop stop = requireStop(rows, stopId);
|
|
|
+ if (!"DELIVERY".equals(stop.getStopType())) throw fail("flash.delivery.stop.invalid");
|
|
|
+ // 已确认站点的重试直接成功,不重复写凭证或推进计数。
|
|
|
+ if ("DELIVERED".equals(stop.getStatus())) return;
|
|
|
+ // 优先使用按站收货码;订单级摘要字段作为回退。
|
|
|
+ String expectedPin = stop.getDeliveryPinCode() != null ? stop.getDeliveryPinCode() : order.getDeliveryPinCode();
|
|
|
+ if (Boolean.TRUE.equals(order.getPinRequired())
|
|
|
+ && (request == null || !Objects.equals(expectedPin, trimToNull(request.getPinCode())))) {
|
|
|
+ throw fail("flash.delivery.pin.invalid");
|
|
|
+ }
|
|
|
+ List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
|
+ Date now = new Date();
|
|
|
+ stop.setStatus("DELIVERED"); stop.setDeliveredAt(now);
|
|
|
+ if (stopMapper.updateById(stop) != 1) throw fail("flash.delivery.state.changed");
|
|
|
+ saveStationImages(orderId, stopId, "DELIVERY", "RIDER", riderId, urls, now);
|
|
|
+ order.setDeliveredStopCount((int) rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())
|
|
|
+ && "DELIVERED".equals(s.getStatus())).count());
|
|
|
+ order.setUpdateTime(now);
|
|
|
+ boolean allDelivered = rows.stream().filter(s -> "DELIVERY".equals(s.getStopType()))
|
|
|
+ .allMatch(s -> "DELIVERED".equals(s.getStatus()));
|
|
|
+ if (allDelivered) {
|
|
|
+ // 末站完成才整单推进,与取件侧同一行锁与条件更新路径。
|
|
|
+ order.setStatus(DELIVERED); order.setDeliveredAt(now);
|
|
|
+ }
|
|
|
+ if (orderMapper.updateDeliveryFulfilment(order) != 1) throw fail("flash.delivery.state.changed");
|
|
|
+ writeLog(orderId, PICKED_UP, order.getStatus(), "RIDER", riderId, "DELIVERY_STOP:" + stopId, now);
|
|
|
+ if (DELIVERED.equals(order.getStatus())) notificationService.notifyDelivered(requireOrder(orderId));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==== v2 多点取送统一契约(2026-10-14):统一 stops 数组、订单级物品、独立计价 ====
|
|
|
+
|
|
|
+ /** v2 实时报价:统一站点数组顺序即路线顺序,物品主体在订单级,独立计价方法。 */
|
|
|
+ public FlashDeliveryQuoteView quoteV2(FlashDeliveryV2QuoteRequest request) {
|
|
|
+ QuoteContext context = calculateQuoteV2(request);
|
|
|
+ return quoteView(quoteAdapter(request), context);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** v2 幂等创建:结构与旧创建互斥;按站固化收件人与收货码,回传报价不一致时拒绝。 */
|
|
|
+ @Transactional
|
|
|
+ public FlashDeliveryOrderDetailView createV2(Long userId, FlashDeliveryV2CreateRequest request) {
|
|
|
+ requireUserId(userId);
|
|
|
+ if (request == null || !hasText(request.getClientRequestId())
|
|
|
+ || request.getClientRequestId().trim().length() > 64) {
|
|
|
+ throw fail("flash.delivery.client.request.id.invalid");
|
|
|
+ }
|
|
|
+ String requestId = request.getClientRequestId().trim();
|
|
|
+ FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
|
|
|
+ if (existing != null) return participantDetail(lockOrder(existing.getId()), true, false);
|
|
|
+ if (request.getUserNote() != null && request.getUserNote().length() > 500) {
|
|
|
+ throw fail("flash.delivery.note.too.long");
|
|
|
+ }
|
|
|
+ List<FlashDeliveryV2StopRequest> stops = normalizeV2Stops(request.getStops());
|
|
|
+ QuoteContext quote = calculateQuoteV2(request);
|
|
|
+ FlashDeliveryQuoteView latestQuote = quoteView(quoteAdapter(request), quote);
|
|
|
+ if (!matchesQuotedPriceV2(request, latestQuote)) {
|
|
|
+ throw new FlashDeliveryQuoteChangedException(latestQuote);
|
|
|
+ }
|
|
|
+ Date now = new Date();
|
|
|
+ String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
|
|
|
+ validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
|
|
|
+ boolean pinRequired = request.getPinRequired() == null || request.getPinRequired();
|
|
|
+ String payType = normalizePayType(request.getPayType());
|
|
|
+ List<String> senderImages = validateProofUrls(request.getSenderImageUrls(), false);
|
|
|
+ List<FlashDeliveryV2StopRequest> deliveries = stops.stream()
|
|
|
+ .filter(s -> "DELIVERY".equals(s.getType())).toList();
|
|
|
+ FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
+ order.setOrderNo(generateOrderNo());
|
|
|
+ order.setClientRequestId(requestId);
|
|
|
+ order.setUserId(userId);
|
|
|
+ order.setServiceType(request.getServiceType());
|
|
|
+ order.setDeliveryType(request.getDeliveryType());
|
|
|
+ order.setVehicleType(request.getVehicleType());
|
|
|
+ order.setStatus(WAITING_ACCEPTANCE);
|
|
|
+ copyPickupSummary(order, quote.pickups());
|
|
|
+ order.setPickedUpStopCount(0);
|
|
|
+ order.setDeliveryStopCount(deliveries.size());
|
|
|
+ order.setDeliveredStopCount(0);
|
|
|
+ order.setIsDisplay("NOW".equals(deliveryMode));
|
|
|
+ order.setDeliveryMode(deliveryMode);
|
|
|
+ order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
|
|
|
+ order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
|
|
|
+ order.setPinRequired(pinRequired);
|
|
|
+ // 订单级收货码作为按站收货码的回退摘要;v2 各收货站独立生成。
|
|
|
+ order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
|
|
|
+ FlashDeliveryV2StopRequest firstDelivery = deliveries.get(0);
|
|
|
+ copyAddress(firstDelivery, order, false);
|
|
|
+ // 订单级收件人沿用首收货站匹配,逐站收件人保存在站点表。
|
|
|
+ order.setReceiverUserId(resolveReceiverUserId(firstDelivery.getPhone()));
|
|
|
+ order.setDistanceMeters(quote.route().distanceMeters());
|
|
|
+ order.setDistanceSource(quote.route().source());
|
|
|
+ order.setEstimatedDurationSeconds(quote.route().durationSeconds());
|
|
|
+ order.setAmount(quote.breakdown().getAmount());
|
|
|
+ order.setCurrency("TWD");
|
|
|
+ order.setPayType(payType);
|
|
|
+ order.setPricingId(quote.pricing().getId());
|
|
|
+ order.setPricingVersion(quote.pricing().getConfigVersion());
|
|
|
+ order.setPricingStartTime(quote.pricing().getStartTime());
|
|
|
+ order.setPricingEndTime(quote.pricing().getEndTime());
|
|
|
+ order.setStartingDistance(quote.pricing().getStartingDistance());
|
|
|
+ order.setStartingFare(quote.pricing().getStartingFare());
|
|
|
+ order.setDistance(quote.pricing().getDistance());
|
|
|
+ order.setFreight(quote.pricing().getFreight());
|
|
|
+ order.setDistanceFee(quote.breakdown().getDistanceFee());
|
|
|
+ order.setBaseDeliveryFee(quote.breakdown().getBaseDeliveryFee());
|
|
|
+ order.setUrgentRate(quote.pricing().getUrgentRate());
|
|
|
+ order.setMinimumUrgentFee(quote.pricing().getMinimumUrgentFee());
|
|
|
+ order.setUrgentFee(quote.breakdown().getUrgentFee());
|
|
|
+ order.setTipAmount(quote.breakdown().getTipAmount());
|
|
|
+ order.setUserNote(trimToNull(request.getUserNote()));
|
|
|
+ order.setVersion(0);
|
|
|
+ order.setCreateTime(now);
|
|
|
+ order.setUpdateTime(now);
|
|
|
+ try {
|
|
|
+ orderMapper.insert(order);
|
|
|
+ } catch (DataIntegrityViolationException duplicate) {
|
|
|
+ if (isOrderNoConflict(duplicate)) {
|
|
|
+ order.setId(null);
|
|
|
+ order.setOrderNo(generateOrderNo());
|
|
|
+ orderMapper.insert(order);
|
|
|
+ } else {
|
|
|
+ FlashDeliveryOrder concurrent = orderMapper.selectOne(new QueryWrapper<FlashDeliveryOrder>()
|
|
|
+ .eq("user_id", userId).eq("client_request_id", requestId).last("FOR UPDATE"));
|
|
|
+ if (concurrent != null) return participantDetail(lockOrder(concurrent.getId()), true, false);
|
|
|
+ throw duplicate;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ saveStopsV2(order, stops, pinRequired, now);
|
|
|
+ saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
|
|
|
+ writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
|
|
|
+ if (Boolean.TRUE.equals(order.getIsDisplay())) {
|
|
|
+ notificationService.notifyAvailable(order);
|
|
|
+ }
|
|
|
+ return participantDetail(order, true, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** v2 物品主体在订单级一次设置;校验规则与旧逐站物品一致。 */
|
|
|
+ private void validateOrderLevelItem(FlashDeliveryV2QuoteRequest request) {
|
|
|
+ if (!PACKAGE_TYPES.contains(Objects.toString(request.getPackageType(), ""))) {
|
|
|
+ throw fail("flash.delivery.package.type.invalid");
|
|
|
+ }
|
|
|
+ if (request.getQuantity() == null || request.getQuantity() <= 0
|
|
|
+ || hasText(request.getWeightRange()) && !WEIGHT_RANGES.contains(request.getWeightRange())
|
|
|
+ || tooLong(request.getSpecification(), 255)) throw fail("flash.delivery.item.invalid");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** v2 报价核心:站点结构校验后命中运价,路线经点为全部取货站与收货站。 */
|
|
|
+ private QuoteContext calculateQuoteV2(FlashDeliveryV2QuoteRequest request) {
|
|
|
+ if (request == null) throw fail("flash.delivery.request.required");
|
|
|
+ validateServiceType(request.getServiceType());
|
|
|
+ validateDeliveryType(request.getDeliveryType());
|
|
|
+ List<FlashDeliveryV2StopRequest> stops = normalizeV2Stops(request.getStops());
|
|
|
+ if (request.getTipAmount() == null || request.getTipAmount() < 0) throw fail("flash.delivery.tip.invalid");
|
|
|
+ validateOrderLevelItem(request);
|
|
|
+ Date now = new Date();
|
|
|
+ String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
|
|
|
+ validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
|
|
|
+ Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
|
|
|
+ Integer vehicle = requireVehicleType(request.getVehicleType());
|
|
|
+ FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate), vehicle);
|
|
|
+ RouteDistance route = v2Route(stops, vehicle);
|
|
|
+ FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
|
|
|
+ route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
|
|
|
+ List<FlashDeliveryV2StopRequest> pickups = stops.stream()
|
|
|
+ .filter(s -> "PICKUP".equals(s.getType())).toList();
|
|
|
+ return new QuoteContext(pricing, route, breakdown, pickupAdapters(pickups, request));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** v2 物品主体在订单级;内部取货站适配器统一携带订单级物品供既有校验与摘要复用。 */
|
|
|
+ private List<FlashDeliveryPickupRequest> pickupAdapters(List<FlashDeliveryV2StopRequest> pickups,
|
|
|
+ FlashDeliveryV2QuoteRequest request) {
|
|
|
+ return pickups.stream().map(stop -> {
|
|
|
+ FlashDeliveryPickupRequest adapter = new FlashDeliveryPickupRequest();
|
|
|
+ BeanUtils.copyProperties(stop, adapter);
|
|
|
+ adapter.setPackageType(request.getPackageType());
|
|
|
+ adapter.setQuantity(request.getQuantity());
|
|
|
+ adapter.setWeightRange(request.getWeightRange());
|
|
|
+ adapter.setSpecification(request.getSpecification());
|
|
|
+ return adapter;
|
|
|
+ }).toList();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** v2 站点结构校验:类型、地址、备注、上限与开关、取前收后及站点去重。 */
|
|
|
+ private List<FlashDeliveryV2StopRequest> normalizeV2Stops(List<FlashDeliveryV2StopRequest> stops) {
|
|
|
+ if (stops == null || stops.isEmpty()) throw fail("flash.delivery.stops.required");
|
|
|
+ for (FlashDeliveryV2StopRequest stop : stops) {
|
|
|
+ if (!"PICKUP".equals(stop.getType()) && !"DELIVERY".equals(stop.getType())) {
|
|
|
+ throw fail("flash.delivery.stop.type.invalid");
|
|
|
+ }
|
|
|
+ validateAddress(stop);
|
|
|
+ if (tooLong(stop.getRemark(), 200)) throw fail("flash.delivery.stop.remark.too.long");
|
|
|
+ }
|
|
|
+ List<FlashDeliveryV2StopRequest> pickups = stops.stream()
|
|
|
+ .filter(s -> "PICKUP".equals(s.getType())).toList();
|
|
|
+ List<FlashDeliveryV2StopRequest> deliveries = stops.stream()
|
|
|
+ .filter(s -> "DELIVERY".equals(s.getType())).toList();
|
|
|
+ if (pickups.isEmpty() || deliveries.isEmpty()
|
|
|
+ || pickups.size() > pickupStopLimit() || deliveries.size() > deliveryStopLimit()) {
|
|
|
+ throw fail("flash.delivery.stops.limit.exceeded");
|
|
|
+ }
|
|
|
+ if (pickups.size() > 1 && !dictionaryEnabled("flash_show_multi_pickup")) {
|
|
|
+ throw fail("flash.delivery.multi.pickup.disabled");
|
|
|
+ }
|
|
|
+ if (deliveries.size() > 1 && !dictionaryEnabled("flash_show_multi_dropoff")) {
|
|
|
+ throw fail("flash.delivery.multi.dropoff.disabled");
|
|
|
+ }
|
|
|
+ // 顺序即路线:先取完再送;交错取送属后续增量。
|
|
|
+ boolean deliverySeen = false;
|
|
|
+ for (FlashDeliveryV2StopRequest stop : stops) {
|
|
|
+ if ("DELIVERY".equals(stop.getType())) deliverySeen = true;
|
|
|
+ else if (deliverySeen) throw fail("flash.delivery.stops.order.invalid");
|
|
|
+ }
|
|
|
+ for (int i = 0; i < stops.size(); i++) {
|
|
|
+ for (int j = i + 1; j < stops.size(); j++) {
|
|
|
+ FlashDeliveryV2StopRequest a = stops.get(i);
|
|
|
+ FlashDeliveryV2StopRequest b = stops.get(j);
|
|
|
+ if (sameAddressText(a, b)
|
|
|
+ || a.getLatitude().compareTo(b.getLatitude()) == 0
|
|
|
+ && a.getLongitude().compareTo(b.getLongitude()) == 0) {
|
|
|
+ throw fail("flash.delivery.address.same");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return stops;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** v2 路线:经点为按用户顺序排列的全部站点;两点退化为单段计算。 */
|
|
|
+ private RouteDistance v2Route(List<FlashDeliveryV2StopRequest> stops, Integer vehicleType) {
|
|
|
+ List<GeoPoint> points = stops.stream()
|
|
|
+ .map(s -> new GeoPoint(s.getLatitude(), s.getLongitude())).toList();
|
|
|
+ RouteDistance route = points.size() == 2
|
|
|
+ ? routeService.calculate(points.get(0), points.get(1), vehicleType)
|
|
|
+ : routeService.calculate(points, vehicleType);
|
|
|
+ if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
|
|
|
+ return route;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 复用旧报价视图组装:仅读取订单级字段,站点信息不进入报价视图。 */
|
|
|
+ private FlashDeliveryQuoteRequest quoteAdapter(FlashDeliveryV2QuoteRequest request) {
|
|
|
+ FlashDeliveryQuoteRequest adapter = new FlashDeliveryQuoteRequest();
|
|
|
+ adapter.setServiceType(request.getServiceType());
|
|
|
+ adapter.setDeliveryType(request.getDeliveryType());
|
|
|
+ adapter.setVehicleType(request.getVehicleType());
|
|
|
+ adapter.setPackageType(request.getPackageType());
|
|
|
+ adapter.setQuantity(request.getQuantity());
|
|
|
+ adapter.setWeightRange(request.getWeightRange());
|
|
|
+ adapter.setSpecification(request.getSpecification());
|
|
|
+ adapter.setTipAmount(request.getTipAmount());
|
|
|
+ adapter.setDeliveryMode(request.getDeliveryMode());
|
|
|
+ adapter.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
|
|
|
+ adapter.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
|
|
|
+ adapter.setPayType(request.getPayType());
|
|
|
+ return adapter;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean matchesQuotedPriceV2(FlashDeliveryV2CreateRequest request, FlashDeliveryQuoteView latest) {
|
|
|
+ return Objects.equals(request.getPricingId(), latest.getPricingId())
|
|
|
+ && Objects.equals(request.getPricingVersion(), latest.getPricingVersion())
|
|
|
+ && Objects.equals(request.getQuotedBaseDeliveryFee(), latest.getBaseDeliveryFee())
|
|
|
+ && Objects.equals(request.getQuotedDistanceFee(), latest.getDistanceFee())
|
|
|
+ && Objects.equals(request.getQuotedUrgentFee(), latest.getUrgentFee())
|
|
|
+ && Objects.equals(request.getQuotedAmount(), latest.getAmount());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 保存 v2 全部站点:按用户顺序落 stop_order,逐站写入备注、收件人与收货码。 */
|
|
|
+ private void saveStopsV2(FlashDeliveryOrder order, List<FlashDeliveryV2StopRequest> stops,
|
|
|
+ boolean pinRequired, Date now) {
|
|
|
+ for (int i = 0; i < stops.size(); i++) {
|
|
|
+ FlashDeliveryV2StopRequest stop = stops.get(i);
|
|
|
+ FlashDeliveryOrderStop row = new FlashDeliveryOrderStop();
|
|
|
+ row.setName(stop.getName().trim()); row.setPhone(stop.getPhone().trim());
|
|
|
+ row.setAddress(stop.getAddress().trim()); row.setAddressDetail(trimToNull(stop.getAddressDetail()));
|
|
|
+ row.setHandoffMethod(trimToNull(stop.getHandoffMethod()));
|
|
|
+ row.setLongitude(stop.getLongitude()); row.setLatitude(stop.getLatitude());
|
|
|
+ row.setRemark(trimToNull(stop.getRemark()));
|
|
|
+ row.setOrderId(order.getId()); row.setStopType(stop.getType()); row.setStopOrder(i);
|
|
|
+ row.setStatus("PENDING"); row.setCreateTime(now);
|
|
|
+ if ("DELIVERY".equals(stop.getType())) {
|
|
|
+ row.setReceiverUserId(resolveReceiverUserId(stop.getPhone()));
|
|
|
+ if (pinRequired) {
|
|
|
+ row.setDeliveryPinCode(String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (stopMapper.insert(row) != 1) throw fail("flash.delivery.state.changed");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/** 平台运价配置列表,按时段开始时间排序。 */
|
|
|
public List<FlashDeliveryPricing> adminPricing() {
|
|
|
return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
|
|
|
@@ -1642,6 +1940,9 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryPickupRequest pickup = new FlashDeliveryPickupRequest();
|
|
|
BeanUtils.copyProperties(pickups.get(0), pickup);
|
|
|
BeanUtils.copyProperties(request.getPickup(), pickup);
|
|
|
+ // 站点不再保存逐站物品;单点改址回退时物品取订单级摘要,与统一契约一致。
|
|
|
+ pickup.setPackageType(order.getPackageType()); pickup.setQuantity(order.getQuantity());
|
|
|
+ pickup.setWeightRange(order.getWeightRange()); pickup.setSpecification(order.getSpecification());
|
|
|
return List.of(pickup);
|
|
|
}
|
|
|
|
|
|
@@ -1712,11 +2013,20 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
/** 点数配置缺失、非整数或越界时,有效上限回退为1。 */
|
|
|
private int pickupStopLimit() {
|
|
|
- String value = dictionaryValue("flash_pickup_stop_limit");
|
|
|
+ return stopLimit("flash_pickup_stop_limit");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** v2 收货点上限与取货点同口径,配置缺失或越界回退为1。 */
|
|
|
+ private int deliveryStopLimit() {
|
|
|
+ return stopLimit("flash_delivery_stop_limit");
|
|
|
+ }
|
|
|
+
|
|
|
+ private int stopLimit(String dictKey) {
|
|
|
+ String value = dictionaryValue(dictKey);
|
|
|
if (value == null || !value.matches("[0-9]+")) return 1;
|
|
|
try {
|
|
|
int limit = Integer.parseInt(value);
|
|
|
- return limit >= 1 && limit <= 26 ? limit : 1;
|
|
|
+ return limit >= 1 && limit <= 10 ? limit : 1;
|
|
|
} catch (NumberFormatException ignored) { return 1; }
|
|
|
}
|
|
|
|
|
|
@@ -1750,8 +2060,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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.setHandoffMethod(trimToNull(address.getHandoffMethod()));
|
|
|
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");
|
|
|
@@ -1775,12 +2084,17 @@ public class FlashDeliveryApplicationService {
|
|
|
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) {
|
|
|
+ || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != expectedDeliveryStopCount(order)) {
|
|
|
throw fail("flash.delivery.stops.invalid");
|
|
|
}
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
+ /** v2 多收货站订单携带 delivery_stop_count;历史订单与旧接口订单回退为单收货站。 */
|
|
|
+ private int expectedDeliveryStopCount(FlashDeliveryOrder order) {
|
|
|
+ return order.getDeliveryStopCount() == null ? 1 : order.getDeliveryStopCount();
|
|
|
+ }
|
|
|
+
|
|
|
private List<FlashDeliveryOrderStop> pickupStops(List<FlashDeliveryOrderStop> rows) {
|
|
|
return rows.stream().filter(s -> "PICKUP".equals(s.getStopType())).toList();
|
|
|
}
|
|
|
@@ -1940,9 +2254,10 @@ public class FlashDeliveryApplicationService {
|
|
|
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()));
|
|
|
+ if (pickups.isEmpty() || !Objects.equals(order.getPickupStopCount(), pickups.size())
|
|
|
+ || rows.stream().filter(s -> "DELIVERY".equals(s.getStopType())).count() != expectedDeliveryStopCount(order)
|
|
|
+ || rows.stream().anyMatch(s -> "DELIVERED".equals(s.getStatus()))) return false;
|
|
|
+ return !anyPicked(rows);
|
|
|
}
|
|
|
|
|
|
|