|
|
@@ -0,0 +1,488 @@
|
|
|
+package com.ruoyi.app.flashdelivery.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.*;
|
|
|
+import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
|
|
|
+import com.ruoyi.app.flashdelivery.route.GeoPoint;
|
|
|
+import com.ruoyi.app.flashdelivery.route.RouteDistance;
|
|
|
+import com.ruoyi.common.exception.ServiceException;
|
|
|
+import com.ruoyi.common.utils.MessageUtils;
|
|
|
+import com.ruoyi.common.utils.StringUtils;
|
|
|
+import com.ruoyi.system.domain.InfoUser;
|
|
|
+import com.ruoyi.system.domain.flash.*;
|
|
|
+import com.ruoyi.system.mapper.InfoUserMapper;
|
|
|
+import com.ruoyi.system.mapper.flash.*;
|
|
|
+import org.springframework.dao.DataIntegrityViolationException;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import java.net.URI;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class FlashDeliveryApplicationService {
|
|
|
+ private static final Set<String> SERVICE_TYPES = Set.of("HELP_SEND", "HELP_PICKUP", "URGENT");
|
|
|
+ private final FlashDeliveryOrderMapper orderMapper;
|
|
|
+ private final FlashDeliveryPricingMapper pricingMapper;
|
|
|
+ private final FlashDeliveryOrderImageMapper imageMapper;
|
|
|
+ private final FlashDeliveryOrderLogMapper logMapper;
|
|
|
+ private final InfoUserMapper userMapper;
|
|
|
+ private final FlashDeliveryRouteService routeService;
|
|
|
+ private final FlashDeliveryPricingCalculator pricingCalculator;
|
|
|
+
|
|
|
+ public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
|
|
|
+ FlashDeliveryPricingMapper pricingMapper,
|
|
|
+ FlashDeliveryOrderImageMapper imageMapper,
|
|
|
+ FlashDeliveryOrderLogMapper logMapper,
|
|
|
+ InfoUserMapper userMapper,
|
|
|
+ FlashDeliveryRouteService routeService,
|
|
|
+ FlashDeliveryPricingCalculator pricingCalculator) {
|
|
|
+ this.orderMapper = orderMapper;
|
|
|
+ this.pricingMapper = pricingMapper;
|
|
|
+ this.imageMapper = imageMapper;
|
|
|
+ this.logMapper = logMapper;
|
|
|
+ this.userMapper = userMapper;
|
|
|
+ this.routeService = routeService;
|
|
|
+ this.pricingCalculator = pricingCalculator;
|
|
|
+ }
|
|
|
+
|
|
|
+ public FlashDeliveryHomeView home() {
|
|
|
+ FlashDeliveryHomeView view = new FlashDeliveryHomeView();
|
|
|
+ view.setServices(pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
|
|
|
+ .eq("enabled", true).orderByAsc("id")).stream().map(this::serviceView).toList());
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ public FlashDeliveryQuoteView quote(FlashDeliveryQuoteRequest request) {
|
|
|
+ QuoteContext context = calculateQuote(request);
|
|
|
+ FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
|
|
|
+ view.setServiceType(context.pricing().getServiceType());
|
|
|
+ view.setDistanceMeters(context.route().distanceMeters());
|
|
|
+ view.setDistanceSource(context.route().source());
|
|
|
+ view.setEstimatedDurationSeconds(context.route().durationSeconds());
|
|
|
+ view.setAmount(context.amount());
|
|
|
+ view.setCurrency("TWD");
|
|
|
+ view.setPricingVersion(context.pricing().getConfigVersion());
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public FlashDeliveryOrderDetailView create(Long userId, FlashDeliveryCreateRequest 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 detail(existing, false);
|
|
|
+ if (request.getUserNote() != null && request.getUserNote().length() > 500) {
|
|
|
+ throw fail("flash.delivery.note.too.long");
|
|
|
+ }
|
|
|
+ QuoteContext quote = calculateQuote(request);
|
|
|
+ Date now = new Date();
|
|
|
+ FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
+ order.setOrderNo("FD" + UUID.randomUUID().toString().replace("-", "").substring(0, 24).toUpperCase(Locale.ROOT));
|
|
|
+ order.setClientRequestId(requestId);
|
|
|
+ order.setUserId(userId);
|
|
|
+ order.setServiceType(request.getServiceType());
|
|
|
+ order.setStatus(WAITING_ACCEPTANCE);
|
|
|
+ copyAddress(request.getPickup(), order, true);
|
|
|
+ copyAddress(request.getDelivery(), order, false);
|
|
|
+ order.setDistanceMeters(quote.route().distanceMeters());
|
|
|
+ order.setDistanceSource(quote.route().source());
|
|
|
+ order.setEstimatedDurationSeconds(quote.route().durationSeconds());
|
|
|
+ order.setAmount(quote.amount());
|
|
|
+ order.setCurrency("TWD");
|
|
|
+ order.setPricingVersion(quote.pricing().getConfigVersion());
|
|
|
+ order.setStartPrice(quote.pricing().getStartPrice());
|
|
|
+ order.setStartDistanceMeters(quote.pricing().getStartDistanceMeters());
|
|
|
+ order.setPerKmPrice(quote.pricing().getPerKmPrice());
|
|
|
+ order.setUserNote(trimToNull(request.getUserNote()));
|
|
|
+ order.setVersion(0);
|
|
|
+ order.setCreateTime(now);
|
|
|
+ order.setUpdateTime(now);
|
|
|
+ try {
|
|
|
+ orderMapper.insert(order);
|
|
|
+ } catch (DataIntegrityViolationException duplicate) {
|
|
|
+ FlashDeliveryOrder concurrent = orderMapper.selectByUserRequestId(userId, requestId);
|
|
|
+ if (concurrent != null) return detail(concurrent, false);
|
|
|
+ throw duplicate;
|
|
|
+ }
|
|
|
+ writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
|
|
|
+ return detail(order, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ public IPage<FlashDeliveryOrder> userOrders(Long userId, int pageNum, int pageSize,
|
|
|
+ String status, String serviceType) {
|
|
|
+ QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
+ .eq("user_id", userId).orderByDesc("create_time");
|
|
|
+ optionalEq(query, "status", status);
|
|
|
+ validateOptionalServiceType(serviceType);
|
|
|
+ optionalEq(query, "service_type", serviceType);
|
|
|
+ return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
+ }
|
|
|
+
|
|
|
+ public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
|
|
|
+ FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
+ if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
|
|
|
+ return detail(order, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
+ FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
+ 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");
|
|
|
+ String reason = requiredReason(request);
|
|
|
+ Date now = new Date();
|
|
|
+ if (orderMapper.cancel(orderId, order.getStatus(), "USER", userId, reason, now) != 1) {
|
|
|
+ throw fail("flash.delivery.state.changed");
|
|
|
+ }
|
|
|
+ writeLog(orderId, order.getStatus(), CANCELLED, "USER", userId, reason, now);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public void confirmReceipt(Long userId, Long orderId) {
|
|
|
+ FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
+ if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
|
|
|
+ Date now = new Date();
|
|
|
+ if (orderMapper.transitionByUser(orderId, userId, DELIVERED, COMPLETED, now) != 1) {
|
|
|
+ throw fail("flash.delivery.complete.not.allowed");
|
|
|
+ }
|
|
|
+ writeLog(orderId, DELIVERED, COMPLETED, "USER", userId, null, now);
|
|
|
+ }
|
|
|
+
|
|
|
+ public IPage<FlashDeliveryAvailableOrderView> available(Long riderId, int pageNum, int pageSize,
|
|
|
+ String serviceType) {
|
|
|
+ requireRider(riderId);
|
|
|
+ validateOptionalServiceType(serviceType);
|
|
|
+ QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
+ .eq("status", WAITING_ACCEPTANCE).isNull("rider_id");
|
|
|
+ optionalEq(query, "service_type", serviceType);
|
|
|
+ query.orderByAsc("case when service_type = 'URGENT' then 0 else 1 end", "create_time");
|
|
|
+ IPage<FlashDeliveryOrder> orders = orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
+ Page<FlashDeliveryAvailableOrderView> result = new Page<>(orders.getCurrent(), orders.getSize(), orders.getTotal());
|
|
|
+ result.setRecords(orders.getRecords().stream().map(this::availableView).toList());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public IPage<FlashDeliveryOrder> riderOrders(Long riderId, int pageNum, int pageSize, String status) {
|
|
|
+ requireRider(riderId);
|
|
|
+ QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>()
|
|
|
+ .eq("rider_id", riderId).orderByDesc("create_time");
|
|
|
+ optionalEq(query, "status", status);
|
|
|
+ return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Object riderDetail(Long riderId, Long orderId) {
|
|
|
+ requireRider(riderId);
|
|
|
+ FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
+ if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
|
|
|
+ return availableView(order);
|
|
|
+ }
|
|
|
+ if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
|
|
|
+ return detail(order, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
|
|
|
+ requireRider(riderId);
|
|
|
+ Date now = new Date();
|
|
|
+ if (orderMapper.accept(orderId, riderId, now) != 1) throw fail("flash.delivery.order.already.accepted");
|
|
|
+ writeLog(orderId, WAITING_ACCEPTANCE, ACCEPTED, "RIDER", riderId, null, now);
|
|
|
+ return detail(requireOrder(orderId), false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public void pickup(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
|
|
|
+ transitionWithProof(riderId, orderId, request, ACCEPTED, PICKED_UP, "PICKUP", "picked_up_at");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public void deliver(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
|
|
|
+ transitionWithProof(riderId, orderId, request, PICKED_UP, DELIVERED, "DELIVERY", "delivered_at");
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<FlashDeliveryPricing> adminPricing() {
|
|
|
+ return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>().orderByAsc("id"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public FlashDeliveryPricing updatePricing(Long adminId, String serviceType,
|
|
|
+ FlashDeliveryPricingUpdateRequest request) {
|
|
|
+ validateServiceType(serviceType);
|
|
|
+ if (request == null || request.getStartPrice() == null || request.getStartPrice().signum() <= 0
|
|
|
+ || request.getStartDistanceMeters() == null || request.getStartDistanceMeters() <= 0
|
|
|
+ || request.getPerKmPrice() == null || request.getPerKmPrice().signum() <= 0
|
|
|
+ || request.getEnabled() == null) throw fail("flash.delivery.pricing.invalid");
|
|
|
+ FlashDeliveryPricing pricing = findPricing(serviceType, false);
|
|
|
+ if (pricing == null) throw fail("flash.delivery.pricing.not.found");
|
|
|
+ int expectedVersion = pricing.getConfigVersion() == null ? 0 : pricing.getConfigVersion();
|
|
|
+ Date now = new Date();
|
|
|
+ BigDecimal startPrice = request.getStartPrice().setScale(2, RoundingMode.HALF_UP);
|
|
|
+ BigDecimal perKmPrice = request.getPerKmPrice().setScale(2, RoundingMode.HALF_UP);
|
|
|
+ UpdateWrapper<FlashDeliveryPricing> update = new UpdateWrapper<FlashDeliveryPricing>()
|
|
|
+ .eq("id", pricing.getId()).eq("config_version", expectedVersion)
|
|
|
+ .set("start_price", startPrice)
|
|
|
+ .set("start_distance_meters", request.getStartDistanceMeters())
|
|
|
+ .set("per_km_price", perKmPrice)
|
|
|
+ .set("enabled", request.getEnabled())
|
|
|
+ .set("updated_by", adminId)
|
|
|
+ .set("update_time", now)
|
|
|
+ .setSql("config_version = config_version + 1");
|
|
|
+ if (pricingMapper.update(null, update) != 1) throw fail("flash.delivery.pricing.changed");
|
|
|
+ pricing.setStartPrice(startPrice);
|
|
|
+ pricing.setStartDistanceMeters(request.getStartDistanceMeters());
|
|
|
+ pricing.setPerKmPrice(perKmPrice);
|
|
|
+ pricing.setEnabled(request.getEnabled());
|
|
|
+ pricing.setConfigVersion(expectedVersion + 1);
|
|
|
+ pricing.setUpdatedBy(adminId);
|
|
|
+ pricing.setUpdateTime(now);
|
|
|
+ return pricing;
|
|
|
+ }
|
|
|
+
|
|
|
+ public IPage<FlashDeliveryOrder> adminOrders(int pageNum, int pageSize, String status,
|
|
|
+ String serviceType, String orderNo,
|
|
|
+ Long userId, Long riderId) {
|
|
|
+ validateOptionalServiceType(serviceType);
|
|
|
+ QueryWrapper<FlashDeliveryOrder> query = new QueryWrapper<FlashDeliveryOrder>().orderByDesc("create_time");
|
|
|
+ optionalEq(query, "status", status);
|
|
|
+ optionalEq(query, "service_type", serviceType);
|
|
|
+ if (hasText(orderNo)) query.like("order_no", orderNo.trim());
|
|
|
+ if (userId != null) query.eq("user_id", userId);
|
|
|
+ if (riderId != null) query.eq("rider_id", riderId);
|
|
|
+ return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
+ }
|
|
|
+
|
|
|
+ public FlashDeliveryOrderDetailView adminDetail(Long orderId) {
|
|
|
+ return detail(requireOrder(orderId), false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
+ FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
+ if (!FlashDeliveryStateMachine.canAdminCancel(order.getStatus())) throw fail("flash.delivery.cancel.not.allowed");
|
|
|
+ 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");
|
|
|
+ }
|
|
|
+ writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public boolean complete(Long operatorId, String operatorType, Long orderId) {
|
|
|
+ Date now = new Date();
|
|
|
+ if (orderMapper.transitionByStatus(orderId, DELIVERED, COMPLETED, "completed_at", now) != 1) return false;
|
|
|
+ writeLog(orderId, DELIVERED, COMPLETED, operatorType, operatorId, null, now);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<FlashDeliveryOrder> autoCompleteCandidates(Date deadline, int limit) {
|
|
|
+ return orderMapper.selectAutoCompletable(deadline, Math.min(Math.max(limit, 1), 500));
|
|
|
+ }
|
|
|
+
|
|
|
+ private QuoteContext calculateQuote(FlashDeliveryQuoteRequest request) {
|
|
|
+ if (request == null) throw fail("flash.delivery.request.required");
|
|
|
+ validateServiceType(request.getServiceType());
|
|
|
+ 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");
|
|
|
+ }
|
|
|
+ FlashDeliveryPricing pricing = findPricing(request.getServiceType(), true);
|
|
|
+ RouteDistance route = routeService.calculate(
|
|
|
+ new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
|
|
|
+ new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
|
|
|
+ return new QuoteContext(pricing, route, pricingCalculator.calculate(pricing, route.distanceMeters()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryPricing findPricing(String serviceType, boolean enabledOnly) {
|
|
|
+ QueryWrapper<FlashDeliveryPricing> query = new QueryWrapper<FlashDeliveryPricing>()
|
|
|
+ .eq("service_type", serviceType);
|
|
|
+ if (enabledOnly) query.eq("enabled", true);
|
|
|
+ FlashDeliveryPricing pricing = pricingMapper.selectOne(query);
|
|
|
+ if (enabledOnly && pricing == null) throw fail("flash.delivery.service.unavailable");
|
|
|
+ return pricing;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void transitionWithProof(Long riderId, Long orderId, FlashDeliveryProofRequest request,
|
|
|
+ String expected, String next, String proofType, String timeColumn) {
|
|
|
+ List<String> urls = validateProof(request);
|
|
|
+ requireRider(riderId);
|
|
|
+ Date now = new Date();
|
|
|
+ if (orderMapper.transitionByRider(orderId, riderId, expected, next, timeColumn, now) != 1) {
|
|
|
+ throw fail("flash.delivery.transition.not.allowed");
|
|
|
+ }
|
|
|
+ for (int index = 0; index < urls.size(); index++) {
|
|
|
+ FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
|
|
|
+ image.setOrderId(orderId);
|
|
|
+ image.setProofType(proofType);
|
|
|
+ image.setImageUrl(urls.get(index));
|
|
|
+ image.setSortOrder(index);
|
|
|
+ image.setOperatorId(riderId);
|
|
|
+ image.setCreateTime(now);
|
|
|
+ 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()) {
|
|
|
+ throw fail("flash.delivery.proof.required");
|
|
|
+ }
|
|
|
+ if (request.getImageUrls().size() > 9) throw fail("flash.delivery.proof.too.many");
|
|
|
+ List<String> urls = new ArrayList<>();
|
|
|
+ for (String value : request.getImageUrls()) {
|
|
|
+ try {
|
|
|
+ String url = value == null ? null : value.trim();
|
|
|
+ URI uri = url == null ? null : URI.create(url);
|
|
|
+ if (uri == null || url.length() > 1000 || uri.getHost() == null
|
|
|
+ || !("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme()))) {
|
|
|
+ throw new IllegalArgumentException();
|
|
|
+ }
|
|
|
+ urls.add(url);
|
|
|
+ } catch (IllegalArgumentException exception) {
|
|
|
+ throw fail("flash.delivery.proof.url.invalid");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return urls;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryOrderDetailView detail(FlashDeliveryOrder order, boolean hideEvidence) {
|
|
|
+ FlashDeliveryOrderDetailView view = new FlashDeliveryOrderDetailView();
|
|
|
+ view.setOrder(order);
|
|
|
+ if (hideEvidence || order.getId() == null) {
|
|
|
+ view.setImages(List.of());
|
|
|
+ view.setLogs(List.of());
|
|
|
+ } else {
|
|
|
+ view.setImages(imageMapper.selectList(new QueryWrapper<FlashDeliveryOrderImage>()
|
|
|
+ .eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
|
|
|
+ view.setLogs(logMapper.selectList(new QueryWrapper<FlashDeliveryOrderLog>()
|
|
|
+ .eq("order_id", order.getId()).orderByAsc("create_time", "id")));
|
|
|
+ }
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryAvailableOrderView availableView(FlashDeliveryOrder order) {
|
|
|
+ FlashDeliveryAvailableOrderView view = new FlashDeliveryAvailableOrderView();
|
|
|
+ view.setId(order.getId());
|
|
|
+ view.setOrderNo(order.getOrderNo());
|
|
|
+ view.setServiceType(order.getServiceType());
|
|
|
+ view.setStatus(order.getStatus());
|
|
|
+ view.setPickupLatitudeApprox(approximate(order.getPickupLatitude()));
|
|
|
+ view.setPickupLongitudeApprox(approximate(order.getPickupLongitude()));
|
|
|
+ view.setDeliveryLatitudeApprox(approximate(order.getDeliveryLatitude()));
|
|
|
+ view.setDeliveryLongitudeApprox(approximate(order.getDeliveryLongitude()));
|
|
|
+ view.setDistanceMeters(order.getDistanceMeters());
|
|
|
+ view.setAmount(order.getAmount());
|
|
|
+ view.setCurrency(order.getCurrency());
|
|
|
+ view.setCreateTime(order.getCreateTime());
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryServiceView serviceView(FlashDeliveryPricing pricing) {
|
|
|
+ FlashDeliveryServiceView view = new FlashDeliveryServiceView();
|
|
|
+ view.setServiceType(pricing.getServiceType());
|
|
|
+ view.setStartPrice(pricing.getStartPrice());
|
|
|
+ view.setStartDistanceMeters(pricing.getStartDistanceMeters());
|
|
|
+ view.setPerKmPrice(pricing.getPerKmPrice());
|
|
|
+ view.setPricingVersion(pricing.getConfigVersion());
|
|
|
+ view.setCurrency("TWD");
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void copyAddress(FlashDeliveryAddressRequest source, FlashDeliveryOrder target, boolean pickup) {
|
|
|
+ if (pickup) {
|
|
|
+ target.setPickupName(source.getName().trim()); target.setPickupPhone(source.getPhone().trim());
|
|
|
+ target.setPickupAddress(source.getAddress().trim()); target.setPickupAddressDetail(trimToNull(source.getAddressDetail()));
|
|
|
+ target.setPickupLongitude(source.getLongitude()); target.setPickupLatitude(source.getLatitude());
|
|
|
+ } else {
|
|
|
+ target.setDeliveryName(source.getName().trim()); target.setDeliveryPhone(source.getPhone().trim());
|
|
|
+ target.setDeliveryAddress(source.getAddress().trim()); target.setDeliveryAddressDetail(trimToNull(source.getAddressDetail()));
|
|
|
+ target.setDeliveryLongitude(source.getLongitude()); target.setDeliveryLatitude(source.getLatitude());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateAddress(FlashDeliveryAddressRequest address) {
|
|
|
+ if (address == null || !hasText(address.getName()) || !hasText(address.getPhone())
|
|
|
+ || !hasText(address.getAddress()) || address.getLatitude() == null || address.getLongitude() == null
|
|
|
+ || address.getName().trim().length() > 64 || address.getPhone().trim().length() > 32
|
|
|
+ || address.getAddress().trim().length() > 255
|
|
|
+ || (address.getAddressDetail() != null && address.getAddressDetail().trim().length() > 255)
|
|
|
+ || 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) {
|
|
|
+ throw fail("flash.delivery.address.invalid");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void requireRider(Long riderId) {
|
|
|
+ InfoUser rider = riderId == null ? null : userMapper.selectInfoUserByUserId(riderId);
|
|
|
+ if (rider == null || !"2".equals(rider.getUserType())) throw fail("flash.delivery.rider.required");
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryOrder requireOrder(Long orderId) {
|
|
|
+ FlashDeliveryOrder order = orderId == null ? null : orderMapper.selectById(orderId);
|
|
|
+ if (order == null) throw fail("flash.delivery.order.not.found");
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeLog(Long orderId, String from, String to, String operatorType,
|
|
|
+ Long operatorId, String reason, Date now) {
|
|
|
+ FlashDeliveryOrderLog log = new FlashDeliveryOrderLog();
|
|
|
+ log.setOrderId(orderId); log.setFromStatus(from); log.setToStatus(to);
|
|
|
+ log.setOperatorType(operatorType); log.setOperatorId(operatorId); log.setReason(reason); log.setCreateTime(now);
|
|
|
+ logMapper.insert(log);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String requiredReason(FlashDeliveryReasonRequest request) {
|
|
|
+ if (request == null || !hasText(request.getReason()) || request.getReason().trim().length() > 500) {
|
|
|
+ throw fail("flash.delivery.cancel.reason.required");
|
|
|
+ }
|
|
|
+ return request.getReason().trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateServiceType(String value) {
|
|
|
+ if (!hasText(value) || !SERVICE_TYPES.contains(value)) throw fail("flash.delivery.service.type.invalid");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateOptionalServiceType(String value) {
|
|
|
+ if (hasText(value)) validateServiceType(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void requireUserId(Long userId) {
|
|
|
+ if (userId == null || userId <= 0) throw fail("flash.delivery.auth.required");
|
|
|
+ }
|
|
|
+
|
|
|
+ private Page<FlashDeliveryOrder> page(int pageNum, int pageSize) {
|
|
|
+ return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void optionalEq(QueryWrapper<FlashDeliveryOrder> query, String column, String value) {
|
|
|
+ if (hasText(value)) query.eq(column, value.trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasText(String value) { return StringUtils.isNotEmpty(value) && !value.trim().isEmpty(); }
|
|
|
+ private String trimToNull(String value) { return hasText(value) ? value.trim() : null; }
|
|
|
+ private BigDecimal approximate(BigDecimal value) { return value == null ? null : value.setScale(2, RoundingMode.HALF_UP); }
|
|
|
+ private boolean sameAddressText(FlashDeliveryAddressRequest left, FlashDeliveryAddressRequest right) {
|
|
|
+ return normalized(left.getAddress()).equals(normalized(right.getAddress()))
|
|
|
+ && normalized(left.getAddressDetail()).equals(normalized(right.getAddressDetail()));
|
|
|
+ }
|
|
|
+ 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 record QuoteContext(FlashDeliveryPricing pricing, RouteDistance route, BigDecimal amount) { }
|
|
|
+}
|