|
@@ -22,13 +22,29 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
import java.net.URI;
|
|
import java.net.URI;
|
|
|
import java.math.BigDecimal;
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
import java.math.RoundingMode;
|
|
|
|
|
+import java.security.SecureRandom;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
|
|
+import java.util.function.Function;
|
|
|
|
|
|
|
|
import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
|
|
import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 闪送业务编排服务。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>负责报价、订单创建、抢单、履约状态流转、平台介入和审计日志。
|
|
|
|
|
+ * 订单身份只接受 Controller 从 token 解析后的用户 ID,客户端传入的金额、距离或操作人均不可信。</p>
|
|
|
|
|
+ */
|
|
|
@Service
|
|
@Service
|
|
|
public class FlashDeliveryApplicationService {
|
|
public class FlashDeliveryApplicationService {
|
|
|
private static final Set<String> SERVICE_TYPES = Set.of("HELP_SEND", "HELP_PICKUP", "URGENT");
|
|
private static final Set<String> SERVICE_TYPES = Set.of("HELP_SEND", "HELP_PICKUP", "URGENT");
|
|
|
|
|
+ private static final Set<String> PACKAGE_TYPES = Set.of("DOCUMENT", "GIFT", "CLOTHING", "BEAUTY",
|
|
|
|
|
+ "DAILY_NECESSITIES", "FOOD_INGREDIENTS", "ELECTRONICS", "SMALL_APPLIANCE", "OTHER");
|
|
|
|
|
+ private static final Set<String> PACKAGE_SIZES = Set.of("SMALL", "MEDIUM", "LARGE");
|
|
|
|
|
+ private static final Set<String> DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
|
|
|
|
|
+ private static final int MAX_DISTANCE_METERS = 40_000;
|
|
|
|
|
+ private static final long SCHEDULE_SLOT_MILLIS = 30L * 60L * 1000L;
|
|
|
|
|
+ private static final long MAX_SCHEDULE_DELAY_MILLIS = 3L * 24L * 60L * 60L * 1000L;
|
|
|
|
|
+ private static final SecureRandom PIN_RANDOM = new SecureRandom();
|
|
|
private final FlashDeliveryOrderMapper orderMapper;
|
|
private final FlashDeliveryOrderMapper orderMapper;
|
|
|
private final FlashDeliveryPricingMapper pricingMapper;
|
|
private final FlashDeliveryPricingMapper pricingMapper;
|
|
|
private final FlashDeliveryOrderImageMapper imageMapper;
|
|
private final FlashDeliveryOrderImageMapper imageMapper;
|
|
@@ -61,13 +77,14 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public FlashDeliveryQuoteView quote(FlashDeliveryQuoteRequest request) {
|
|
public FlashDeliveryQuoteView quote(FlashDeliveryQuoteRequest request) {
|
|
|
|
|
+ // 报价只使用服务端路线距离和当前启用的计价配置,避免客户端篡改金额。
|
|
|
QuoteContext context = calculateQuote(request);
|
|
QuoteContext context = calculateQuote(request);
|
|
|
FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
|
|
FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
|
|
|
view.setServiceType(context.pricing().getServiceType());
|
|
view.setServiceType(context.pricing().getServiceType());
|
|
|
view.setDistanceMeters(context.route().distanceMeters());
|
|
view.setDistanceMeters(context.route().distanceMeters());
|
|
|
view.setDistanceSource(context.route().source());
|
|
view.setDistanceSource(context.route().source());
|
|
|
view.setEstimatedDurationSeconds(context.route().durationSeconds());
|
|
view.setEstimatedDurationSeconds(context.route().durationSeconds());
|
|
|
- view.setAmount(context.amount());
|
|
|
|
|
|
|
+ applyBreakdown(view, context.breakdown());
|
|
|
view.setCurrency("TWD");
|
|
view.setCurrency("TWD");
|
|
|
view.setPricingVersion(context.pricing().getConfigVersion());
|
|
view.setPricingVersion(context.pricing().getConfigVersion());
|
|
|
return view;
|
|
return view;
|
|
@@ -81,30 +98,49 @@ public class FlashDeliveryApplicationService {
|
|
|
throw fail("flash.delivery.client.request.id.invalid");
|
|
throw fail("flash.delivery.client.request.id.invalid");
|
|
|
}
|
|
}
|
|
|
String requestId = request.getClientRequestId().trim();
|
|
String requestId = request.getClientRequestId().trim();
|
|
|
|
|
+ // 用户 ID + clientRequestId 构成幂等键;重复请求直接返回第一次创建的订单。
|
|
|
FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
|
|
FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
|
|
|
- if (existing != null) return detail(existing, false);
|
|
|
|
|
|
|
+ if (existing != null) return participantDetail(existing, 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");
|
|
|
}
|
|
}
|
|
|
QuoteContext quote = calculateQuote(request);
|
|
QuoteContext quote = calculateQuote(request);
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
|
|
+ String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
|
|
|
|
|
+ validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
|
|
|
|
|
+ boolean pinRequired = request.getPinRequired() == null || request.getPinRequired();
|
|
|
|
|
+ List<String> senderImages = validateProofUrls(request.getSenderImageUrls(), false);
|
|
|
FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
order.setOrderNo("FD" + UUID.randomUUID().toString().replace("-", "").substring(0, 24).toUpperCase(Locale.ROOT));
|
|
order.setOrderNo("FD" + UUID.randomUUID().toString().replace("-", "").substring(0, 24).toUpperCase(Locale.ROOT));
|
|
|
order.setClientRequestId(requestId);
|
|
order.setClientRequestId(requestId);
|
|
|
order.setUserId(userId);
|
|
order.setUserId(userId);
|
|
|
order.setServiceType(request.getServiceType());
|
|
order.setServiceType(request.getServiceType());
|
|
|
order.setStatus(WAITING_ACCEPTANCE);
|
|
order.setStatus(WAITING_ACCEPTANCE);
|
|
|
|
|
+ order.setPackageType(request.getPackageType());
|
|
|
|
|
+ order.setPackageSize(request.getPackageSize());
|
|
|
|
|
+ order.setDeliveryMode(deliveryMode);
|
|
|
|
|
+ order.setScheduledPickupStartAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : null);
|
|
|
|
|
+ order.setScheduledPickupEndAt("SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupEndAt() : null);
|
|
|
|
|
+ order.setPinRequired(pinRequired);
|
|
|
|
|
+ order.setDeliveryPinCode(pinRequired ? String.format(Locale.ROOT, "%04d", PIN_RANDOM.nextInt(10_000)) : null);
|
|
|
copyAddress(request.getPickup(), order, true);
|
|
copyAddress(request.getPickup(), order, true);
|
|
|
copyAddress(request.getDelivery(), order, false);
|
|
copyAddress(request.getDelivery(), order, false);
|
|
|
order.setDistanceMeters(quote.route().distanceMeters());
|
|
order.setDistanceMeters(quote.route().distanceMeters());
|
|
|
order.setDistanceSource(quote.route().source());
|
|
order.setDistanceSource(quote.route().source());
|
|
|
order.setEstimatedDurationSeconds(quote.route().durationSeconds());
|
|
order.setEstimatedDurationSeconds(quote.route().durationSeconds());
|
|
|
- order.setAmount(quote.amount());
|
|
|
|
|
|
|
+ order.setAmount(quote.breakdown().getAmount());
|
|
|
order.setCurrency("TWD");
|
|
order.setCurrency("TWD");
|
|
|
order.setPricingVersion(quote.pricing().getConfigVersion());
|
|
order.setPricingVersion(quote.pricing().getConfigVersion());
|
|
|
|
|
+ // 保存计价快照,后台调价只影响新订单,不追溯修改历史订单金额。
|
|
|
order.setStartPrice(quote.pricing().getStartPrice());
|
|
order.setStartPrice(quote.pricing().getStartPrice());
|
|
|
|
|
+ order.setMinimumPrice(quote.pricing().getMinimumPrice());
|
|
|
order.setStartDistanceMeters(quote.pricing().getStartDistanceMeters());
|
|
order.setStartDistanceMeters(quote.pricing().getStartDistanceMeters());
|
|
|
order.setPerKmPrice(quote.pricing().getPerKmPrice());
|
|
order.setPerKmPrice(quote.pricing().getPerKmPrice());
|
|
|
|
|
+ order.setPerMinutePrice(quote.pricing().getPerMinutePrice());
|
|
|
|
|
+ order.setPeakMultiplier(quote.pricing().getPeakMultiplier());
|
|
|
|
|
+ order.setDistanceFee(quote.breakdown().getDistanceFee());
|
|
|
|
|
+ order.setDurationFee(quote.breakdown().getDurationFee());
|
|
|
|
|
+ order.setPeakFee(quote.breakdown().getPeakFee());
|
|
|
order.setUserNote(trimToNull(request.getUserNote()));
|
|
order.setUserNote(trimToNull(request.getUserNote()));
|
|
|
order.setVersion(0);
|
|
order.setVersion(0);
|
|
|
order.setCreateTime(now);
|
|
order.setCreateTime(now);
|
|
@@ -112,28 +148,32 @@ public class FlashDeliveryApplicationService {
|
|
|
try {
|
|
try {
|
|
|
orderMapper.insert(order);
|
|
orderMapper.insert(order);
|
|
|
} catch (DataIntegrityViolationException duplicate) {
|
|
} catch (DataIntegrityViolationException duplicate) {
|
|
|
|
|
+ // 并发重复请求可能同时通过前置查询,最终由数据库唯一键完成幂等收口。
|
|
|
FlashDeliveryOrder concurrent = orderMapper.selectByUserRequestId(userId, requestId);
|
|
FlashDeliveryOrder concurrent = orderMapper.selectByUserRequestId(userId, requestId);
|
|
|
- if (concurrent != null) return detail(concurrent, false);
|
|
|
|
|
|
|
+ if (concurrent != null) return participantDetail(concurrent, true, false);
|
|
|
throw duplicate;
|
|
throw duplicate;
|
|
|
}
|
|
}
|
|
|
|
|
+ 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 detail(order, false);
|
|
|
|
|
|
|
+ return participantDetail(order, true, false);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public IPage<FlashDeliveryOrder> userOrders(Long userId, int pageNum, int pageSize,
|
|
|
|
|
- String status, String serviceType) {
|
|
|
|
|
|
|
+ public IPage<FlashDeliveryOrderView> userOrders(Long userId, int pageNum, int pageSize,
|
|
|
|
|
+ String status, String scene, String serviceType) {
|
|
|
|
|
+ if (hasText(status) && hasText(scene)) throw fail("flash.delivery.scene.invalid");
|
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
.eq("user_id", userId).orderByDesc("create_time");
|
|
.eq("user_id", userId).orderByDesc("create_time");
|
|
|
optionalEq(query, "status", status);
|
|
optionalEq(query, "status", status);
|
|
|
|
|
+ applyUserScene(query, scene);
|
|
|
validateOptionalServiceType(serviceType);
|
|
validateOptionalServiceType(serviceType);
|
|
|
optionalEq(query, "service_type", serviceType);
|
|
optionalEq(query, "service_type", serviceType);
|
|
|
- return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
|
|
|
|
+ return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), this::orderView);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
|
|
public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
FlashDeliveryOrder order = 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");
|
|
|
- return detail(order, false);
|
|
|
|
|
|
|
+ return participantDetail(order, true, false);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Transactional
|
|
@Transactional
|
|
@@ -165,8 +205,11 @@ public class FlashDeliveryApplicationService {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
validateOptionalServiceType(serviceType);
|
|
validateOptionalServiceType(serviceType);
|
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
- .eq("status", WAITING_ACCEPTANCE).isNull("rider_id");
|
|
|
|
|
|
|
+ .eq("status", WAITING_ACCEPTANCE).isNull("rider_id")
|
|
|
|
|
+ .and(item -> item.eq("delivery_mode", "NOW")
|
|
|
|
|
+ .or().le("scheduled_pickup_start_at", new Date()));
|
|
|
optionalEq(query, "service_type", serviceType);
|
|
optionalEq(query, "service_type", serviceType);
|
|
|
|
|
+ // 第一版不自动派单:加急单优先,其余订单按发布时间由骑手主动抢单。
|
|
|
query.orderByAsc("case when service_type = 'URGENT' then 0 else 1 end", "create_time");
|
|
query.orderByAsc("case when service_type = 'URGENT' then 0 else 1 end", "create_time");
|
|
|
IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
Page<FlashDeliveryAvailableOrderView> result = new Page<>(orders.getCurrent(), orders.getSize(), orders.getTotal());
|
|
Page<FlashDeliveryAvailableOrderView> result = new Page<>(orders.getCurrent(), orders.getSize(), orders.getTotal());
|
|
@@ -174,31 +217,37 @@ public class FlashDeliveryApplicationService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public IPage<FlashDeliveryOrder> riderOrders(Long riderId, int pageNum, int pageSize, String status) {
|
|
|
|
|
|
|
+ public IPage<FlashDeliveryOrderView> riderOrders(Long riderId, int pageNum, int pageSize,
|
|
|
|
|
+ String status, String scene) {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
|
|
+ if (hasText(status) && hasText(scene)) throw fail("flash.delivery.scene.invalid");
|
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
.eq("rider_id", riderId).orderByDesc("create_time");
|
|
.eq("rider_id", riderId).orderByDesc("create_time");
|
|
|
optionalEq(query, "status", status);
|
|
optionalEq(query, "status", status);
|
|
|
- return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
|
|
|
|
+ applyRiderScene(query, scene);
|
|
|
|
|
+ return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), this::orderView);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public Object riderDetail(Long riderId, Long orderId) {
|
|
public Object riderDetail(Long riderId, Long orderId) {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
|
|
if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
|
|
|
|
|
+ if (!isAvailableAt(order, new Date())) throw fail("flash.delivery.order.not.found");
|
|
|
|
|
+ // 未接单骑手只能看到白名单字段和近似坐标,联系人及精确地址在抢单成功后开放。
|
|
|
return availableView(order);
|
|
return availableView(order);
|
|
|
}
|
|
}
|
|
|
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");
|
|
|
- return detail(order, false);
|
|
|
|
|
|
|
+ return participantDetail(order, false, false);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
|
|
public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
|
|
+ // Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
|
|
|
if (orderMapper.accept(orderId, riderId, now) != 1) throw fail("flash.delivery.order.already.accepted");
|
|
if (orderMapper.accept(orderId, riderId, now) != 1) throw fail("flash.delivery.order.already.accepted");
|
|
|
writeLog(orderId, WAITING_ACCEPTANCE, ACCEPTED, "RIDER", riderId, null, now);
|
|
writeLog(orderId, WAITING_ACCEPTANCE, ACCEPTED, "RIDER", riderId, null, now);
|
|
|
- return detail(requireOrder(orderId), false);
|
|
|
|
|
|
|
+ return participantDetail(requireOrder(orderId), false, false);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Transactional
|
|
@Transactional
|
|
@@ -207,7 +256,16 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Transactional
|
|
@Transactional
|
|
|
- public void deliver(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
|
|
|
|
|
|
|
+ public void deliver(Long riderId, Long orderId, FlashDeliveryDeliverRequest request) {
|
|
|
|
|
+ requireRider(riderId);
|
|
|
|
|
+ FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
|
|
+ if (!Objects.equals(riderId, order.getRiderId()) || !PICKED_UP.equals(order.getStatus())) {
|
|
|
|
|
+ throw fail("flash.delivery.transition.not.allowed");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Boolean.TRUE.equals(order.getPinRequired())
|
|
|
|
|
+ && (request == null || !Objects.equals(order.getDeliveryPinCode(), trimToNull(request.getPinCode())))) {
|
|
|
|
|
+ throw fail("flash.delivery.pin.invalid");
|
|
|
|
|
+ }
|
|
|
transitionWithProof(riderId, orderId, request, PICKED_UP, DELIVERED, "DELIVERY", "delivered_at");
|
|
transitionWithProof(riderId, orderId, request, PICKED_UP, DELIVERED, "DELIVERY", "delivered_at");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -220,28 +278,42 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryPricingUpdateRequest request) {
|
|
FlashDeliveryPricingUpdateRequest request) {
|
|
|
validateServiceType(serviceType);
|
|
validateServiceType(serviceType);
|
|
|
if (request == null || request.getStartPrice() == null || request.getStartPrice().signum() <= 0
|
|
if (request == null || request.getStartPrice() == null || request.getStartPrice().signum() <= 0
|
|
|
|
|
+ || request.getMinimumPrice() == null || request.getMinimumPrice().signum() <= 0
|
|
|
|| request.getStartDistanceMeters() == null || request.getStartDistanceMeters() <= 0
|
|
|| request.getStartDistanceMeters() == null || request.getStartDistanceMeters() <= 0
|
|
|
|| request.getPerKmPrice() == null || request.getPerKmPrice().signum() <= 0
|
|
|| request.getPerKmPrice() == null || request.getPerKmPrice().signum() <= 0
|
|
|
|
|
+ || request.getPerMinutePrice() == null || request.getPerMinutePrice().signum() < 0
|
|
|
|
|
+ || request.getPeakMultiplier() == null
|
|
|
|
|
+ || request.getPeakMultiplier().compareTo(BigDecimal.ONE) < 0
|
|
|
|| request.getEnabled() == null) throw fail("flash.delivery.pricing.invalid");
|
|
|| request.getEnabled() == null) throw fail("flash.delivery.pricing.invalid");
|
|
|
FlashDeliveryPricing pricing = findPricing(serviceType, false);
|
|
FlashDeliveryPricing pricing = findPricing(serviceType, false);
|
|
|
if (pricing == null) throw fail("flash.delivery.pricing.not.found");
|
|
if (pricing == null) throw fail("flash.delivery.pricing.not.found");
|
|
|
int expectedVersion = pricing.getConfigVersion() == null ? 0 : pricing.getConfigVersion();
|
|
int expectedVersion = pricing.getConfigVersion() == null ? 0 : pricing.getConfigVersion();
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
BigDecimal startPrice = request.getStartPrice().setScale(2, RoundingMode.HALF_UP);
|
|
BigDecimal startPrice = request.getStartPrice().setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
+ BigDecimal minimumPrice = request.getMinimumPrice().setScale(2, RoundingMode.HALF_UP);
|
|
|
BigDecimal perKmPrice = request.getPerKmPrice().setScale(2, RoundingMode.HALF_UP);
|
|
BigDecimal perKmPrice = request.getPerKmPrice().setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
+ BigDecimal perMinutePrice = request.getPerMinutePrice().setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
+ BigDecimal peakMultiplier = request.getPeakMultiplier().setScale(2, RoundingMode.HALF_UP);
|
|
|
UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
|
|
UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
|
|
|
.eq("id", pricing.getId()).eq("config_version", expectedVersion)
|
|
.eq("id", pricing.getId()).eq("config_version", expectedVersion)
|
|
|
.set("start_price", startPrice)
|
|
.set("start_price", startPrice)
|
|
|
|
|
+ .set("minimum_price", minimumPrice)
|
|
|
.set("start_distance_meters", request.getStartDistanceMeters())
|
|
.set("start_distance_meters", request.getStartDistanceMeters())
|
|
|
.set("per_km_price", perKmPrice)
|
|
.set("per_km_price", perKmPrice)
|
|
|
|
|
+ .set("per_minute_price", perMinutePrice)
|
|
|
|
|
+ .set("peak_multiplier", peakMultiplier)
|
|
|
.set("enabled", request.getEnabled())
|
|
.set("enabled", request.getEnabled())
|
|
|
.set("updated_by", adminId)
|
|
.set("updated_by", adminId)
|
|
|
.set("update_time", now)
|
|
.set("update_time", now)
|
|
|
.setSql("config_version = config_version + 1");
|
|
.setSql("config_version = config_version + 1");
|
|
|
|
|
+ // 版本条件防止两个管理员同时保存时后提交者静默覆盖先提交者。
|
|
|
if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
|
|
if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
|
|
|
pricing.setStartPrice(startPrice);
|
|
pricing.setStartPrice(startPrice);
|
|
|
|
|
+ pricing.setMinimumPrice(minimumPrice);
|
|
|
pricing.setStartDistanceMeters(request.getStartDistanceMeters());
|
|
pricing.setStartDistanceMeters(request.getStartDistanceMeters());
|
|
|
pricing.setPerKmPrice(perKmPrice);
|
|
pricing.setPerKmPrice(perKmPrice);
|
|
|
|
|
+ pricing.setPerMinutePrice(perMinutePrice);
|
|
|
|
|
+ pricing.setPeakMultiplier(peakMultiplier);
|
|
|
pricing.setEnabled(request.getEnabled());
|
|
pricing.setEnabled(request.getEnabled());
|
|
|
pricing.setConfigVersion(expectedVersion + 1);
|
|
pricing.setConfigVersion(expectedVersion + 1);
|
|
|
pricing.setUpdatedBy(adminId);
|
|
pricing.setUpdatedBy(adminId);
|
|
@@ -262,8 +334,15 @@ public class FlashDeliveryApplicationService {
|
|
|
return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public FlashDeliveryOrderDetailView adminDetail(Long orderId) {
|
|
|
|
|
- return detail(requireOrder(orderId), false);
|
|
|
|
|
|
|
+ public FlashDeliveryAdminOrderDetailView adminDetail(Long orderId) {
|
|
|
|
|
+ FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
|
|
+ FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
|
|
|
|
|
+ view.setOrder(order);
|
|
|
|
|
+ view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
|
|
|
+ .eq("order_id", orderId).orderByAsc("proof_type", "sort_order")));
|
|
|
|
|
+ view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
|
|
|
|
|
+ .eq("order_id", orderId).orderByAsc("create_time", "id")));
|
|
|
|
|
+ return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Transactional
|
|
@Transactional
|
|
@@ -293,6 +372,12 @@ public class FlashDeliveryApplicationService {
|
|
|
private QuoteContext calculateQuote(FlashDeliveryQuoteRequest request) {
|
|
private QuoteContext calculateQuote(FlashDeliveryQuoteRequest request) {
|
|
|
if (request == null) throw fail("flash.delivery.request.required");
|
|
if (request == null) throw fail("flash.delivery.request.required");
|
|
|
validateServiceType(request.getServiceType());
|
|
validateServiceType(request.getServiceType());
|
|
|
|
|
+ if (!hasText(request.getPackageType()) || !PACKAGE_TYPES.contains(request.getPackageType())) {
|
|
|
|
|
+ throw fail("flash.delivery.package.type.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!hasText(request.getPackageSize()) || !PACKAGE_SIZES.contains(request.getPackageSize())) {
|
|
|
|
|
+ throw fail("flash.delivery.package.size.invalid");
|
|
|
|
|
+ }
|
|
|
validateAddress(request.getPickup());
|
|
validateAddress(request.getPickup());
|
|
|
validateAddress(request.getDelivery());
|
|
validateAddress(request.getDelivery());
|
|
|
if (sameAddressText(request.getPickup(), request.getDelivery())
|
|
if (sameAddressText(request.getPickup(), request.getDelivery())
|
|
@@ -301,10 +386,14 @@ public class FlashDeliveryApplicationService {
|
|
|
throw fail("flash.delivery.address.same");
|
|
throw fail("flash.delivery.address.same");
|
|
|
}
|
|
}
|
|
|
FlashDeliveryPricing pricing = findPricing(request.getServiceType(), true);
|
|
FlashDeliveryPricing pricing = findPricing(request.getServiceType(), true);
|
|
|
|
|
+ // 路线服务优先使用 Google Routes;不可用时在服务内部降级为球面直线距离。
|
|
|
RouteDistance route = routeService.calculate(
|
|
RouteDistance route = routeService.calculate(
|
|
|
new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
|
|
new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
|
|
|
new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
|
|
new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
|
|
|
- return new QuoteContext(pricing, route, pricingCalculator.calculate(pricing, route.distanceMeters()));
|
|
|
|
|
|
|
+ if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
|
|
|
|
|
+ FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
|
|
|
|
|
+ route.distanceMeters(), route.durationSeconds() == null ? 0 : route.durationSeconds());
|
|
|
|
|
+ return new QuoteContext(pricing, route, breakdown);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private FlashDeliveryPricing findPricing(String serviceType, boolean enabledOnly) {
|
|
private FlashDeliveryPricing findPricing(String serviceType, boolean enabledOnly) {
|
|
@@ -318,32 +407,40 @@ public class FlashDeliveryApplicationService {
|
|
|
|
|
|
|
|
private void transitionWithProof(Long riderId, Long orderId, FlashDeliveryProofRequest request,
|
|
private void transitionWithProof(Long riderId, Long orderId, FlashDeliveryProofRequest request,
|
|
|
String expected, String next, String proofType, String timeColumn) {
|
|
String expected, String next, String proofType, String timeColumn) {
|
|
|
- List<String> urls = validateProof(request);
|
|
|
|
|
|
|
+ List<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
|
requireRider(riderId);
|
|
requireRider(riderId);
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
if (orderMapper.transitionByRider(orderId, riderId, expected, next, timeColumn, now) != 1) {
|
|
if (orderMapper.transitionByRider(orderId, riderId, expected, next, timeColumn, now) != 1) {
|
|
|
throw fail("flash.delivery.transition.not.allowed");
|
|
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,
|
|
|
|
|
+ Long operatorId, List<String> urls, Date now) {
|
|
|
for (int index = 0; index < urls.size(); index++) {
|
|
for (int index = 0; index < urls.size(); index++) {
|
|
|
FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
|
|
FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
|
|
|
image.setOrderId(orderId);
|
|
image.setOrderId(orderId);
|
|
|
image.setProofType(proofType);
|
|
image.setProofType(proofType);
|
|
|
image.setImageUrl(urls.get(index));
|
|
image.setImageUrl(urls.get(index));
|
|
|
image.setSortOrder(index);
|
|
image.setSortOrder(index);
|
|
|
- image.setOperatorId(riderId);
|
|
|
|
|
|
|
+ image.setOperatorType(operatorType);
|
|
|
|
|
+ image.setOperatorId(operatorId);
|
|
|
image.setCreateTime(now);
|
|
image.setCreateTime(now);
|
|
|
imageMapper.insert(image);
|
|
imageMapper.insert(image);
|
|
|
}
|
|
}
|
|
|
- writeLog(orderId, expected, next, "RIDER", riderId, null, now);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private List<String> validateProof(FlashDeliveryProofRequest request) {
|
|
|
|
|
- if (request == null || request.getImageUrls() == null || request.getImageUrls().isEmpty()) {
|
|
|
|
|
|
|
+ private List<String> validateProofUrls(List<String> values, boolean required) {
|
|
|
|
|
+ if (values == null || values.isEmpty()) {
|
|
|
|
|
+ if (!required) return List.of();
|
|
|
throw fail("flash.delivery.proof.required");
|
|
throw fail("flash.delivery.proof.required");
|
|
|
}
|
|
}
|
|
|
- if (request.getImageUrls().size() > 9) throw fail("flash.delivery.proof.too.many");
|
|
|
|
|
|
|
+ if (values.size() > 9) throw fail("flash.delivery.proof.too.many");
|
|
|
List<String> urls = new ArrayList<>();
|
|
List<String> urls = new ArrayList<>();
|
|
|
- for (String value : request.getImageUrls()) {
|
|
|
|
|
|
|
+ for (String value : values) {
|
|
|
try {
|
|
try {
|
|
|
String url = value == null ? null : value.trim();
|
|
String url = value == null ? null : value.trim();
|
|
|
URI uri = url == null ? null : URI.create(url);
|
|
URI uri = url == null ? null : URI.create(url);
|
|
@@ -359,32 +456,51 @@ public class FlashDeliveryApplicationService {
|
|
|
return urls;
|
|
return urls;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private FlashDeliveryOrderDetailView detail(FlashDeliveryOrder order, boolean hideEvidence) {
|
|
|
|
|
|
|
+ private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
|
|
|
|
|
+ boolean userView,
|
|
|
|
|
+ boolean hideEvidence) {
|
|
|
FlashDeliveryOrderDetailView view = new FlashDeliveryOrderDetailView();
|
|
FlashDeliveryOrderDetailView view = new FlashDeliveryOrderDetailView();
|
|
|
- view.setOrder(order);
|
|
|
|
|
|
|
+ view.setOrder(orderView(order));
|
|
|
|
|
+ if (userView) {
|
|
|
|
|
+ view.setDeliveryPinCode(Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
|
|
|
|
|
+ view.setRider(riderSummary(order));
|
|
|
|
|
+ }
|
|
|
if (hideEvidence || order.getId() == null) {
|
|
if (hideEvidence || order.getId() == null) {
|
|
|
view.setImages(List.of());
|
|
view.setImages(List.of());
|
|
|
view.setLogs(List.of());
|
|
view.setLogs(List.of());
|
|
|
} else {
|
|
} else {
|
|
|
view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
|
- .eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
|
|
|
|
|
|
|
+ .eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order"))
|
|
|
|
|
+ .stream().map(this::imageView).toList());
|
|
|
view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
|
|
view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
|
|
|
- .eq("order_id", order.getId()).orderByAsc("create_time", "id")));
|
|
|
|
|
|
|
+ .eq("order_id", order.getId()).orderByAsc("create_time", "id"))
|
|
|
|
|
+ .stream().map(this::logView).toList());
|
|
|
}
|
|
}
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private FlashDeliveryAvailableOrderView availableView(FlashDeliveryOrder order) {
|
|
private FlashDeliveryAvailableOrderView availableView(FlashDeliveryOrder order) {
|
|
|
|
|
+ // 明确逐字段组装白名单 DTO,禁止直接复制订单实体造成未来新增敏感字段意外泄露。
|
|
|
FlashDeliveryAvailableOrderView view = new FlashDeliveryAvailableOrderView();
|
|
FlashDeliveryAvailableOrderView view = new FlashDeliveryAvailableOrderView();
|
|
|
view.setId(order.getId());
|
|
view.setId(order.getId());
|
|
|
view.setOrderNo(order.getOrderNo());
|
|
view.setOrderNo(order.getOrderNo());
|
|
|
view.setServiceType(order.getServiceType());
|
|
view.setServiceType(order.getServiceType());
|
|
|
view.setStatus(order.getStatus());
|
|
view.setStatus(order.getStatus());
|
|
|
|
|
+ view.setPackageType(order.getPackageType());
|
|
|
|
|
+ view.setPackageSize(order.getPackageSize());
|
|
|
|
|
+ view.setDeliveryMode(order.getDeliveryMode());
|
|
|
|
|
+ view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
|
|
|
|
|
+ view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
|
|
|
|
|
+ view.setPickupCity(order.getPickupCity());
|
|
|
|
|
+ view.setPickupArea(order.getPickupArea());
|
|
|
|
|
+ view.setDeliveryCity(order.getDeliveryCity());
|
|
|
|
|
+ view.setDeliveryArea(order.getDeliveryArea());
|
|
|
view.setPickupLatitudeApprox(approximate(order.getPickupLatitude()));
|
|
view.setPickupLatitudeApprox(approximate(order.getPickupLatitude()));
|
|
|
view.setPickupLongitudeApprox(approximate(order.getPickupLongitude()));
|
|
view.setPickupLongitudeApprox(approximate(order.getPickupLongitude()));
|
|
|
view.setDeliveryLatitudeApprox(approximate(order.getDeliveryLatitude()));
|
|
view.setDeliveryLatitudeApprox(approximate(order.getDeliveryLatitude()));
|
|
|
view.setDeliveryLongitudeApprox(approximate(order.getDeliveryLongitude()));
|
|
view.setDeliveryLongitudeApprox(approximate(order.getDeliveryLongitude()));
|
|
|
view.setDistanceMeters(order.getDistanceMeters());
|
|
view.setDistanceMeters(order.getDistanceMeters());
|
|
|
|
|
+ view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
|
|
|
view.setAmount(order.getAmount());
|
|
view.setAmount(order.getAmount());
|
|
|
view.setCurrency(order.getCurrency());
|
|
view.setCurrency(order.getCurrency());
|
|
|
view.setCreateTime(order.getCreateTime());
|
|
view.setCreateTime(order.getCreateTime());
|
|
@@ -395,21 +511,119 @@ public class FlashDeliveryApplicationService {
|
|
|
FlashDeliveryServiceView view = new FlashDeliveryServiceView();
|
|
FlashDeliveryServiceView view = new FlashDeliveryServiceView();
|
|
|
view.setServiceType(pricing.getServiceType());
|
|
view.setServiceType(pricing.getServiceType());
|
|
|
view.setStartPrice(pricing.getStartPrice());
|
|
view.setStartPrice(pricing.getStartPrice());
|
|
|
|
|
+ view.setMinimumPrice(pricing.getMinimumPrice());
|
|
|
view.setStartDistanceMeters(pricing.getStartDistanceMeters());
|
|
view.setStartDistanceMeters(pricing.getStartDistanceMeters());
|
|
|
view.setPerKmPrice(pricing.getPerKmPrice());
|
|
view.setPerKmPrice(pricing.getPerKmPrice());
|
|
|
|
|
+ view.setPerMinutePrice(pricing.getPerMinutePrice());
|
|
|
|
|
+ view.setPeakMultiplier(pricing.getPeakMultiplier());
|
|
|
view.setPricingVersion(pricing.getConfigVersion());
|
|
view.setPricingVersion(pricing.getConfigVersion());
|
|
|
view.setCurrency("TWD");
|
|
view.setCurrency("TWD");
|
|
|
return view;
|
|
return view;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private FlashDeliveryOrderView orderView(FlashDeliveryOrder order) {
|
|
|
|
|
+ FlashDeliveryOrderView view = new FlashDeliveryOrderView();
|
|
|
|
|
+ view.setId(order.getId());
|
|
|
|
|
+ view.setOrderNo(order.getOrderNo());
|
|
|
|
|
+ view.setServiceType(order.getServiceType());
|
|
|
|
|
+ view.setStatus(order.getStatus());
|
|
|
|
|
+ view.setPackageType(order.getPackageType());
|
|
|
|
|
+ view.setPackageSize(order.getPackageSize());
|
|
|
|
|
+ view.setDeliveryMode(order.getDeliveryMode());
|
|
|
|
|
+ view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
|
|
|
|
|
+ view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
|
|
|
|
|
+ view.setPinRequired(order.getPinRequired());
|
|
|
|
|
+ view.setPickup(addressView(order, true));
|
|
|
|
|
+ view.setDelivery(addressView(order, false));
|
|
|
|
|
+ view.setDistanceMeters(order.getDistanceMeters());
|
|
|
|
|
+ view.setDistanceSource(order.getDistanceSource());
|
|
|
|
|
+ view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
|
|
|
|
|
+ view.setAmount(order.getAmount());
|
|
|
|
|
+ view.setCurrency(order.getCurrency());
|
|
|
|
|
+ view.setUserNote(order.getUserNote());
|
|
|
|
|
+ view.setAcceptedAt(order.getAcceptedAt());
|
|
|
|
|
+ view.setPickedUpAt(order.getPickedUpAt());
|
|
|
|
|
+ view.setDeliveredAt(order.getDeliveredAt());
|
|
|
|
|
+ view.setCompletedAt(order.getCompletedAt());
|
|
|
|
|
+ view.setCancelledAt(order.getCancelledAt());
|
|
|
|
|
+ view.setCancelReason(order.getCancelReason());
|
|
|
|
|
+ view.setCreateTime(order.getCreateTime());
|
|
|
|
|
+ view.setUpdateTime(order.getUpdateTime());
|
|
|
|
|
+ return view;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FlashDeliveryAddressView addressView(FlashDeliveryOrder order, boolean pickup) {
|
|
|
|
|
+ FlashDeliveryAddressView view = new FlashDeliveryAddressView();
|
|
|
|
|
+ if (pickup) {
|
|
|
|
|
+ view.setName(order.getPickupName()); view.setPhone(order.getPickupPhone());
|
|
|
|
|
+ view.setAddress(order.getPickupAddress()); view.setAddressDetail(order.getPickupAddressDetail());
|
|
|
|
|
+ view.setCity(order.getPickupCity()); view.setArea(order.getPickupArea());
|
|
|
|
|
+ view.setHandoffMethod(order.getPickupHandoffMethod());
|
|
|
|
|
+ view.setLongitude(order.getPickupLongitude()); view.setLatitude(order.getPickupLatitude());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ view.setName(order.getDeliveryName()); view.setPhone(order.getDeliveryPhone());
|
|
|
|
|
+ view.setAddress(order.getDeliveryAddress()); view.setAddressDetail(order.getDeliveryAddressDetail());
|
|
|
|
|
+ view.setCity(order.getDeliveryCity()); view.setArea(order.getDeliveryArea());
|
|
|
|
|
+ view.setHandoffMethod(order.getDeliveryHandoffMethod());
|
|
|
|
|
+ view.setLongitude(order.getDeliveryLongitude()); view.setLatitude(order.getDeliveryLatitude());
|
|
|
|
|
+ }
|
|
|
|
|
+ return view;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FlashDeliveryImageView imageView(FlashDeliveryOrderImage image) {
|
|
|
|
|
+ FlashDeliveryImageView view = new FlashDeliveryImageView();
|
|
|
|
|
+ view.setProofType(image.getProofType()); view.setImageUrl(image.getImageUrl());
|
|
|
|
|
+ view.setSortOrder(image.getSortOrder()); view.setCreateTime(image.getCreateTime());
|
|
|
|
|
+ return view;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FlashDeliveryLogView logView(FlashDeliveryOrderLog log) {
|
|
|
|
|
+ FlashDeliveryLogView view = new FlashDeliveryLogView();
|
|
|
|
|
+ view.setFromStatus(log.getFromStatus()); view.setToStatus(log.getToStatus());
|
|
|
|
|
+ view.setOperatorType(log.getOperatorType()); view.setReason(log.getReason());
|
|
|
|
|
+ view.setCreateTime(log.getCreateTime());
|
|
|
|
|
+ return view;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FlashDeliveryRiderSummaryView riderSummary(FlashDeliveryOrder order) {
|
|
|
|
|
+ if (order.getRiderId() == null) return null;
|
|
|
|
|
+ InfoUser rider = userMapper.selectInfoUserByUserId(order.getRiderId());
|
|
|
|
|
+ if (rider == null) return null;
|
|
|
|
|
+ FlashDeliveryRiderSummaryView view = new FlashDeliveryRiderSummaryView();
|
|
|
|
|
+ view.setName(hasText(rider.getNickName()) ? rider.getNickName() : rider.getUserName());
|
|
|
|
|
+ view.setAvatar(rider.getAvatar());
|
|
|
|
|
+ view.setRating(rider.getStar());
|
|
|
|
|
+ view.setImUserId(rider.getImUserId() == null ? null : String.valueOf(rider.getImUserId()));
|
|
|
|
|
+ if (ACCEPTED.equals(order.getStatus()) || PICKED_UP.equals(order.getStatus())) {
|
|
|
|
|
+ view.setLongitude(rider.getLongitude());
|
|
|
|
|
+ view.setLatitude(rider.getLatitude());
|
|
|
|
|
+ }
|
|
|
|
|
+ return view;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void applyBreakdown(FlashDeliveryQuoteView view, FlashDeliveryPriceBreakdown breakdown) {
|
|
|
|
|
+ view.setStartPrice(breakdown.getStartPrice());
|
|
|
|
|
+ view.setMinimumPrice(breakdown.getMinimumPrice());
|
|
|
|
|
+ view.setDistanceFee(breakdown.getDistanceFee());
|
|
|
|
|
+ view.setDurationFee(breakdown.getDurationFee());
|
|
|
|
|
+ view.setSubtotal(breakdown.getSubtotal());
|
|
|
|
|
+ view.setPeakMultiplier(breakdown.getPeakMultiplier());
|
|
|
|
|
+ view.setPeakFee(breakdown.getPeakFee());
|
|
|
|
|
+ view.setAmount(breakdown.getAmount());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private void copyAddress(FlashDeliveryAddressRequest source, FlashDeliveryOrder target, boolean pickup) {
|
|
private void copyAddress(FlashDeliveryAddressRequest source, FlashDeliveryOrder target, boolean pickup) {
|
|
|
if (pickup) {
|
|
if (pickup) {
|
|
|
target.setPickupName(source.getName().trim()); target.setPickupPhone(source.getPhone().trim());
|
|
target.setPickupName(source.getName().trim()); target.setPickupPhone(source.getPhone().trim());
|
|
|
target.setPickupAddress(source.getAddress().trim()); target.setPickupAddressDetail(trimToNull(source.getAddressDetail()));
|
|
target.setPickupAddress(source.getAddress().trim()); target.setPickupAddressDetail(trimToNull(source.getAddressDetail()));
|
|
|
|
|
+ target.setPickupCity(trimToNull(source.getCity())); target.setPickupArea(trimToNull(source.getArea()));
|
|
|
|
|
+ target.setPickupHandoffMethod(trimToNull(source.getHandoffMethod()));
|
|
|
target.setPickupLongitude(source.getLongitude()); target.setPickupLatitude(source.getLatitude());
|
|
target.setPickupLongitude(source.getLongitude()); target.setPickupLatitude(source.getLatitude());
|
|
|
} else {
|
|
} else {
|
|
|
target.setDeliveryName(source.getName().trim()); target.setDeliveryPhone(source.getPhone().trim());
|
|
target.setDeliveryName(source.getName().trim()); target.setDeliveryPhone(source.getPhone().trim());
|
|
|
target.setDeliveryAddress(source.getAddress().trim()); target.setDeliveryAddressDetail(trimToNull(source.getAddressDetail()));
|
|
target.setDeliveryAddress(source.getAddress().trim()); target.setDeliveryAddressDetail(trimToNull(source.getAddressDetail()));
|
|
|
|
|
+ target.setDeliveryCity(trimToNull(source.getCity())); target.setDeliveryArea(trimToNull(source.getArea()));
|
|
|
|
|
+ target.setDeliveryHandoffMethod(trimToNull(source.getHandoffMethod()));
|
|
|
target.setDeliveryLongitude(source.getLongitude()); target.setDeliveryLatitude(source.getLatitude());
|
|
target.setDeliveryLongitude(source.getLongitude()); target.setDeliveryLatitude(source.getLatitude());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -420,6 +634,8 @@ public class FlashDeliveryApplicationService {
|
|
|
|| address.getName().trim().length() > 64 || address.getPhone().trim().length() > 32
|
|
|| address.getName().trim().length() > 64 || address.getPhone().trim().length() > 32
|
|
|
|| address.getAddress().trim().length() > 255
|
|
|| address.getAddress().trim().length() > 255
|
|
|
|| (address.getAddressDetail() != null && address.getAddressDetail().trim().length() > 255)
|
|
|| (address.getAddressDetail() != null && address.getAddressDetail().trim().length() > 255)
|
|
|
|
|
+ || tooLong(address.getCity(), 64) || tooLong(address.getArea(), 64)
|
|
|
|
|
+ || tooLong(address.getHandoffMethod(), 40)
|
|
|
|| address.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0
|
|
|| address.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0
|
|
|
|| address.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0
|
|
|| address.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0
|
|
|
|| address.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0
|
|
|| address.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0
|
|
@@ -470,12 +686,67 @@ public class FlashDeliveryApplicationService {
|
|
|
return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
|
|
return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private <T> IPage<T> mapPage(IPage<FlashDeliveryOrder> source,
|
|
|
|
|
+ Function<FlashDeliveryOrder, T> mapper) {
|
|
|
|
|
+ Page<T> result = new Page<>(source.getCurrent(), source.getSize(), source.getTotal());
|
|
|
|
|
+ result.setRecords(source.getRecords().stream().map(mapper).toList());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void applyUserScene(QueryWrapper<FlashDeliveryOrder> query, String scene) {
|
|
|
|
|
+ if (!hasText(scene) || "all".equals(scene)) return;
|
|
|
|
|
+ switch (scene.trim()) {
|
|
|
|
|
+ case "matching" -> query.eq("status", WAITING_ACCEPTANCE);
|
|
|
|
|
+ case "progress" -> query.in("status", ACCEPTED, PICKED_UP, DELIVERED);
|
|
|
|
|
+ case "done" -> query.eq("status", COMPLETED);
|
|
|
|
|
+ case "cancelled" -> query.eq("status", CANCELLED);
|
|
|
|
|
+ default -> throw fail("flash.delivery.scene.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void applyRiderScene(QueryWrapper<FlashDeliveryOrder> query, String scene) {
|
|
|
|
|
+ if (!hasText(scene) || "all".equals(scene)) return;
|
|
|
|
|
+ switch (scene.trim()) {
|
|
|
|
|
+ case "pickup" -> query.eq("status", ACCEPTED);
|
|
|
|
|
+ case "delivering" -> query.eq("status", PICKED_UP);
|
|
|
|
|
+ case "completed" -> query.in("status", DELIVERED, COMPLETED);
|
|
|
|
|
+ case "cancelled" -> query.eq("status", CANCELLED);
|
|
|
|
|
+ default -> throw fail("flash.delivery.scene.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String normalizeDeliveryMode(String value) {
|
|
|
|
|
+ String mode = hasText(value) ? value.trim() : "NOW";
|
|
|
|
|
+ if (!DELIVERY_MODES.contains(mode)) throw fail("flash.delivery.mode.invalid");
|
|
|
|
|
+ return mode;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void validateSchedule(String mode, Date start, Date end, Date now) {
|
|
|
|
|
+ if ("NOW".equals(mode)) {
|
|
|
|
|
+ if (start != null || end != null) throw fail("flash.delivery.schedule.invalid");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (start == null || end == null || start.before(now)
|
|
|
|
|
+ || end.getTime() - start.getTime() != SCHEDULE_SLOT_MILLIS
|
|
|
|
|
+ || start.getTime() - now.getTime() > MAX_SCHEDULE_DELAY_MILLIS) {
|
|
|
|
|
+ throw fail("flash.delivery.schedule.invalid");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isAvailableAt(FlashDeliveryOrder order, Date now) {
|
|
|
|
|
+ return "NOW".equals(order.getDeliveryMode())
|
|
|
|
|
+ || "SCHEDULED".equals(order.getDeliveryMode())
|
|
|
|
|
+ && order.getScheduledPickupStartAt() != null
|
|
|
|
|
+ && !order.getScheduledPickupStartAt().after(now);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private void optionalEq(QueryWrapper<FlashDeliveryOrder> query, String column, String value) {
|
|
private void optionalEq(QueryWrapper<FlashDeliveryOrder> query, String column, String value) {
|
|
|
if (hasText(value)) query.eq(column, value.trim());
|
|
if (hasText(value)) query.eq(column, value.trim());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private boolean hasText(String value) { return StringUtils.isNotEmpty(value) && !value.trim().isEmpty(); }
|
|
private boolean hasText(String value) { return StringUtils.isNotEmpty(value) && !value.trim().isEmpty(); }
|
|
|
private String trimToNull(String value) { return hasText(value) ? value.trim() : null; }
|
|
private String trimToNull(String value) { return hasText(value) ? value.trim() : null; }
|
|
|
|
|
+ private boolean tooLong(String value, int max) { return value != null && value.trim().length() > max; }
|
|
|
private BigDecimal approximate(BigDecimal value) { return value == null ? null : value.setScale(2, RoundingMode.HALF_UP); }
|
|
private BigDecimal approximate(BigDecimal value) { return value == null ? null : value.setScale(2, RoundingMode.HALF_UP); }
|
|
|
private boolean sameAddressText(FlashDeliveryAddressRequest left, FlashDeliveryAddressRequest right) {
|
|
private boolean sameAddressText(FlashDeliveryAddressRequest left, FlashDeliveryAddressRequest right) {
|
|
|
return normalized(left.getAddress()).equals(normalized(right.getAddress()))
|
|
return normalized(left.getAddress()).equals(normalized(right.getAddress()))
|
|
@@ -484,5 +755,6 @@ 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 record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route, BigDecimal amount) { }
|
|
|
|
|
|
|
+ private record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route,
|
|
|
|
|
+ FlashDeliveryPriceBreakdown breakdown) { }
|
|
|
}
|
|
}
|