PortableShipmentPositionRepository.java
package com.ecommerce.fulfillment.infrastructure.geo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ecommerce.fulfillment.application.port.ShipmentPositionRepository;
import com.ecommerce.fulfillment.infrastructure.persistence.entity.FulfillmentOrderEntity;
import com.ecommerce.fulfillment.infrastructure.persistence.entity.ShipmentLatestPositionEntity;
import com.ecommerce.fulfillment.infrastructure.persistence.mapper.FulfillmentOrderMapper;
import com.ecommerce.fulfillment.infrastructure.persistence.mapper.ShipmentLatestPositionMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@Repository
@ConditionalOnProperty(
prefix = "ecommerce.fulfillment.geo",
name = "mysql-spatial-enabled",
havingValue = "false")
public class PortableShipmentPositionRepository implements ShipmentPositionRepository {
private static final double EARTH_RADIUS_METERS = 6_371_008.8d;
private final ShipmentLatestPositionMapper positionMapper;
private final FulfillmentOrderMapper orderMapper;
public PortableShipmentPositionRepository(
ShipmentLatestPositionMapper positionMapper,
FulfillmentOrderMapper orderMapper) {
this.positionMapper = positionMapper;
this.orderMapper = orderMapper;
}
@Override
public Optional<Position> findByFulfillmentIdForUpdate(Long fulfillmentId) {
return Optional.ofNullable(positionMapper.selectByFulfillmentIdForUpdate(fulfillmentId))
.map(this::toPosition);
}
@Override
public Optional<Position> findByFulfillmentId(Long fulfillmentId) {
return Optional.ofNullable(positionMapper.selectById(fulfillmentId))
.map(this::toPosition);
}
@Override
public void insert(Position position) {
requireSingleRow(positionMapper.insert(toEntity(position)));
}
@Override
public void update(Position position) {
requireSingleRow(positionMapper.updateById(toEntity(position)));
}
@Override
public List<NearbyPosition> findNearby(
BigDecimal longitude,
BigDecimal latitude,
long radiusMeters,
int limit) {
List<ShipmentLatestPositionEntity> positions = positionMapper.selectList(
new LambdaQueryWrapper<ShipmentLatestPositionEntity>()
.orderByDesc(ShipmentLatestPositionEntity::getOccurredAt)
.orderByDesc(ShipmentLatestPositionEntity::getFulfillmentId));
if (positions.isEmpty()) {
return List.of();
}
Map<Long, FulfillmentOrderEntity> orders = orderMapper.selectByIds(
positions.stream()
.map(ShipmentLatestPositionEntity::getFulfillmentId)
.toList())
.stream()
.collect(Collectors.toMap(FulfillmentOrderEntity::getId, Function.identity()));
return positions.stream()
.map(position -> nearby(position, orders.get(position.getFulfillmentId()),
longitude, latitude))
.filter(item -> item != null
&& item.distanceMeters().compareTo(BigDecimal.valueOf(radiusMeters)) <= 0)
.sorted((left, right) -> {
int distance = left.distanceMeters().compareTo(right.distanceMeters());
if (distance != 0) {
return distance;
}
return right.position().occurredAt().compareTo(left.position().occurredAt());
})
.limit(limit)
.toList();
}
@Override
public List<Position> listLatest(int limit) {
return positionMapper.selectLatest(limit).stream().map(this::toPosition).toList();
}
private NearbyPosition nearby(
ShipmentLatestPositionEntity entity,
FulfillmentOrderEntity order,
BigDecimal longitude,
BigDecimal latitude) {
if (order == null) {
return null;
}
BigDecimal distance = BigDecimal.valueOf(distanceMeters(
latitude.doubleValue(), longitude.doubleValue(),
entity.getLatitude().doubleValue(), entity.getLongitude().doubleValue()))
.setScale(2, RoundingMode.HALF_UP);
return new NearbyPosition(
toPosition(entity),
order.getOrderNo(),
order.getUserId(),
order.getStatus(),
distance);
}
private double distanceMeters(
double sourceLatitude,
double sourceLongitude,
double targetLatitude,
double targetLongitude) {
double latitudeDelta = Math.toRadians(targetLatitude - sourceLatitude);
double longitudeDelta = Math.toRadians(targetLongitude - sourceLongitude);
double sourceLatitudeRadians = Math.toRadians(sourceLatitude);
double targetLatitudeRadians = Math.toRadians(targetLatitude);
double haversine = Math.sin(latitudeDelta / 2.0d) * Math.sin(latitudeDelta / 2.0d)
+ Math.cos(sourceLatitudeRadians) * Math.cos(targetLatitudeRadians)
* Math.sin(longitudeDelta / 2.0d) * Math.sin(longitudeDelta / 2.0d);
return EARTH_RADIUS_METERS * 2.0d
* Math.atan2(Math.sqrt(haversine), Math.sqrt(1.0d - haversine));
}
private Position toPosition(ShipmentLatestPositionEntity entity) {
return new Position(
entity.getFulfillmentId(),
entity.getFulfillmentNo(),
entity.getTraceId(),
entity.getExternalEventId(),
entity.getNodeType(),
entity.getLocationName(),
entity.getLongitude(),
entity.getLatitude(),
entity.getOccurredAt(),
entity.getUpdatedAt());
}
private ShipmentLatestPositionEntity toEntity(Position position) {
ShipmentLatestPositionEntity entity = new ShipmentLatestPositionEntity();
entity.setFulfillmentId(position.fulfillmentId());
entity.setFulfillmentNo(position.fulfillmentNo());
entity.setTraceId(position.traceId());
entity.setExternalEventId(position.externalEventId());
entity.setNodeType(position.nodeType());
entity.setLocationName(position.locationName());
entity.setLongitude(position.longitude());
entity.setLatitude(position.latitude());
entity.setOccurredAt(position.occurredAt());
entity.setUpdatedAt(position.updatedAt());
return entity;
}
private void requireSingleRow(int rows) {
if (rows != 1) {
throw new IllegalStateException("Shipment position write affected an unexpected row count");
}
}
}