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.exception.FlashDeliveryQuoteChangedException;
import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
import com.ruoyi.app.flashdelivery.route.GeoPoint;
import com.ruoyi.app.flashdelivery.route.RouteDistance;
import com.ruoyi.app.order.RiderDeliveryExclusivityService;
import com.ruoyi.app.order.RiderDeliveryLockService;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DictUtils;
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.security.SecureRandom;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.*;
/**
* 闪送业务编排服务。
*
*
负责报价、订单创建、抢单、履约状态流转、平台介入和审计日志。
* 订单身份只接受 Controller 从 token 解析后的用户 ID,客户端传入的金额、距离或操作人均不可信。
*/
@Service
public class FlashDeliveryApplicationService {
private static final List SERVICE_TYPES = List.of("HELP_SEND", "HELP_PICKUP");
private static final Set DELIVERY_TYPES = Set.of("NORMAL", "URGENT");
private static final Set PACKAGE_TYPES = Set.of("DOCUMENT", "GIFT", "CLOTHING", "BEAUTY",
"DAILY_NECESSITIES", "FOOD_INGREDIENTS", "ELECTRONICS", "SMALL_APPLIANCE", "OTHER");
private static final Set WEIGHT_RANGES = Set.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
"OVER_10_TO_15_KG", "OVER_15_TO_20_KG");
private static final Set DELIVERY_MODES = Set.of("NOW", "SCHEDULED");
/** 支付方式取值与主订单系统同域:4=现金、6=线下转账(027 线下转账支付)。 */
private static final String PAY_TYPE_CASH = "4";
private static final String PAY_TYPE_TRANSFER = "6";
private static final DateTimeFormatter PRICING_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
private static final int MAX_DISTANCE_METERS = 40_000;
private static final BigDecimal MAX_PRICING_DISTANCE = new BigDecimal("999999.99");
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 FlashDeliveryPricingMapper pricingMapper;
private final FlashDeliveryOrderImageMapper imageMapper;
private final FlashDeliveryOrderLogMapper logMapper;
private final InfoUserMapper userMapper;
private final FlashDeliveryRouteService routeService;
private final FlashDeliveryPricingCalculator pricingCalculator;
private final RiderDeliveryLockService riderDeliveryLockService;
private final RiderDeliveryExclusivityService riderDeliveryExclusivityService;
public FlashDeliveryApplicationService(FlashDeliveryOrderMapper orderMapper,
FlashDeliveryPricingMapper pricingMapper,
FlashDeliveryOrderImageMapper imageMapper,
FlashDeliveryOrderLogMapper logMapper,
InfoUserMapper userMapper,
FlashDeliveryRouteService routeService,
FlashDeliveryPricingCalculator pricingCalculator,
RiderDeliveryLockService riderDeliveryLockService,
RiderDeliveryExclusivityService riderDeliveryExclusivityService) {
this.orderMapper = orderMapper;
this.pricingMapper = pricingMapper;
this.imageMapper = imageMapper;
this.logMapper = logMapper;
this.userMapper = userMapper;
this.routeService = routeService;
this.pricingCalculator = pricingCalculator;
this.riderDeliveryLockService = riderDeliveryLockService;
this.riderDeliveryExclusivityService = riderDeliveryExclusivityService;
}
/** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
public FlashDeliveryHomeView home() {
FlashDeliveryHomeView view = new FlashDeliveryHomeView();
view.setServiceTypes(SERVICE_TYPES);
view.setDeliveryTypes(List.of("NORMAL", "URGENT"));
FlashDeliveryPricing pricing = findPricingAtOrNull(pricingTargetTime(new Date()));
view.setServices(pricing == null ? List.of() : SERVICE_TYPES.stream()
.map(serviceType -> serviceView(pricing, serviceType)).toList());
return view;
}
/** 实时报价:校验请求后按服务端路线距离和命中时段的运价计算费用(立即单取当前时段、预约单取预约开始时刻),不写库。 */
public FlashDeliveryQuoteView quote(FlashDeliveryQuoteRequest request) {
// 报价只使用服务端路线距离和当前启用的计价配置,避免客户端篡改金额。
QuoteContext context = calculateQuote(request);
return quoteView(request, context);
}
/** 将报价上下文组装为对外报价视图,含运价快照字段与费用明细。 */
private FlashDeliveryQuoteView quoteView(FlashDeliveryQuoteRequest request, QuoteContext context) {
FlashDeliveryQuoteView view = new FlashDeliveryQuoteView();
view.setServiceType(request.getServiceType());
view.setDeliveryType(request.getDeliveryType());
view.setDeliveryMode(normalizeDeliveryMode(request.getDeliveryMode()));
view.setScheduledPickupStartAt(request.getScheduledPickupStartAt());
view.setScheduledPickupEndAt(request.getScheduledPickupEndAt());
view.setPricingId(context.pricing().getId());
view.setStartTime(context.pricing().getStartTime());
view.setEndTime(context.pricing().getEndTime());
view.setDistanceMeters(context.route().distanceMeters());
view.setDistanceSource(context.route().source());
view.setEstimatedDurationSeconds(context.route().durationSeconds());
view.setStartingDistance(context.pricing().getStartingDistance());
view.setStartingFare(context.pricing().getStartingFare());
view.setDistance(context.pricing().getDistance());
view.setFreight(context.pricing().getFreight());
view.setUrgentRate(context.pricing().getUrgentRate());
view.setMinimumUrgentFee(context.pricing().getMinimumUrgentFee());
applyBreakdown(view, context.breakdown());
view.setCurrency("TWD");
view.setPricingVersion(context.pricing().getConfigVersion());
return view;
}
/**
* 幂等创建订单:clientRequestId 与 userId 构成唯一键,重复提交直接返回已有订单。
* 创建前按最新条件重新报价,客户端回传报价不一致时抛出 FlashDeliveryQuoteChangedException;
* 成功后保存计价快照、寄件图片和初始状态日志。
*/
@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();
// 用户 ID + clientRequestId 构成幂等键;重复请求直接返回第一次创建的订单。
FlashDeliveryOrder existing = orderMapper.selectByUserRequestId(userId, requestId);
if (existing != null) return participantDetail(existing, true, false);
if (request.getUserNote() != null && request.getUserNote().length() > 500) {
throw fail("flash.delivery.note.too.long");
}
QuoteContext quote = calculateQuote(request);
FlashDeliveryQuoteView latestQuote = quoteView(request, quote);
if (!matchesQuotedPrice(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 senderImages = validateProofUrls(request.getSenderImageUrls(), false);
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.setDeliveryType(request.getDeliveryType());
order.setStatus(WAITING_ACCEPTANCE);
order.setPackageType(request.getPackageType());
order.setQuantity(request.getQuantity());
order.setWeightRange(request.getWeightRange());
order.setSpecification(trimToNull(request.getSpecification()));
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.getDelivery(), order, false);
order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
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) {
// 并发重复请求可能同时通过前置查询,最终由数据库唯一键完成幂等收口。
FlashDeliveryOrder concurrent = orderMapper.selectByUserRequestId(userId, requestId);
if (concurrent != null) return participantDetail(concurrent, true, false);
throw duplicate;
}
saveImages(order.getId(), "SENDER", "USER", userId, senderImages, now);
writeLog(order.getId(), null, WAITING_ACCEPTANCE, "USER", userId, null, now);
return participantDetail(order, true, false);
}
/** 分页查询当前用户参与的订单卡片;role=sender 按寄件人、receiver 按收件人过滤。 */
public IPage userOrders(Long userId, int pageNum, int pageSize, String role) {
String selectedRole = hasText(role) ? role.trim().toLowerCase(Locale.ROOT) : "sender";
if (!"sender".equals(selectedRole) && !"receiver".equals(selectedRole)) {
throw fail("flash.delivery.tab.invalid");
}
String participantColumn = "receiver".equals(selectedRole) ? "receiver_user_id" : "user_id";
QueryWrapper query = new QueryWrapper()
.eq(participantColumn, userId).orderByDesc("create_time");
return mapPage(orderMapper.selectPage(page(pageNum, pageSize), query), this::userOrderListView);
}
/** 查询本人订单详情,仅寄件人和匹配到的收件人可见。 */
public FlashDeliveryOrderDetailView userDetail(Long userId, Long orderId) {
FlashDeliveryOrder order = requireOrder(orderId);
if (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
return participantDetail(order, true, false);
}
/** 地址修改先报价,用户确认前不改动订单。 */
public FlashDeliveryQuoteView quoteAddress(Long userId, Long orderId,
FlashDeliveryAddressChangeRequest request) {
if (request == null) throw fail("flash.delivery.request.required");
FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
return calculateAddressQuote(order, request);
}
/** 确认修改待接单订单地址:重算报价并与回传值比对,按版本条件更新。 */
@Transactional
public FlashDeliveryOrderDetailView updateAddress(Long userId, Long orderId,
FlashDeliveryAddressConfirmRequest request) {
if (request == null) throw fail("flash.delivery.request.required");
FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
FlashDeliveryQuoteView quote = calculateAddressQuote(order, request);
if (!Objects.equals(request.getQuotedDistanceMeters(), quote.getDistanceMeters())
|| !Objects.equals(request.getQuotedBaseDeliveryFee(), quote.getBaseDeliveryFee())
|| !Objects.equals(request.getQuotedDistanceFee(), quote.getDistanceFee())
|| !Objects.equals(request.getQuotedUrgentFee(), quote.getUrgentFee())
|| !Objects.equals(request.getQuotedAmount(), quote.getAmount())) {
throw new FlashDeliveryQuoteChangedException(quote);
}
copyAddress(request.getPickup(), order, true);
copyAddress(request.getDelivery(), order, false);
order.setReceiverUserId(resolveReceiverUserId(order.getDeliveryPhone()));
order.setDistanceMeters(quote.getDistanceMeters());
order.setDistanceSource(quote.getDistanceSource());
order.setEstimatedDurationSeconds(quote.getEstimatedDurationSeconds());
// 运价快照与金额同事务刷新为本次计价依据(快照=金额的最后一次计算依据,而非最初依据)
order.setPricingId(quote.getPricingId());
order.setPricingVersion(quote.getPricingVersion());
order.setPricingStartTime(quote.getStartTime());
order.setPricingEndTime(quote.getEndTime());
order.setStartingDistance(quote.getStartingDistance());
order.setStartingFare(quote.getStartingFare());
order.setDistance(quote.getDistance());
order.setFreight(quote.getFreight());
order.setUrgentRate(quote.getUrgentRate());
order.setMinimumUrgentFee(quote.getMinimumUrgentFee());
order.setDistanceFee(quote.getDistanceFee());
order.setBaseDeliveryFee(quote.getBaseDeliveryFee());
order.setUrgentFee(quote.getUrgentFee());
order.setAmount(quote.getAmount());
Date now = new Date();
order.setUpdateTime(now);
// 即使上面的查询仍显示待接单,骑手也可能已接走,最终以条件 UPDATE 为准。
if (orderMapper.updateWaitingAddress(order, request.getOrderVersion()) != 1) {
throw fail("flash.delivery.state.changed");
}
order.setVersion(order.getVersion() + 1);
writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "ADDRESS_UPDATED", now);
return participantDetail(order, true, false);
}
/** 待接单订单追加小费:订单总额与小费同步增加,版本条件防止并发重复加款。 */
@Transactional
public FlashDeliveryOrderDetailView addTip(Long userId, Long orderId, FlashDeliveryTipAddRequest request) {
if (request == null) throw fail("flash.delivery.request.required");
FlashDeliveryOrder order = requireEditableOrder(userId, orderId, request.getOrderVersion());
if (request.getAdditionalTipAmount() == null || request.getAdditionalTipAmount().signum() <= 0) {
throw fail("flash.delivery.tip.additional.invalid");
}
long tip;
long amount;
try {
// 保留 JSON 原始数值,禁止 1.9 等小数在绑定时被截断为整数小费。
long additionalTip = request.getAdditionalTipAmount().longValueExact();
tip = Math.addExact(order.getTipAmount(), additionalTip);
amount = Math.addExact(order.getAmount(), additionalTip);
} catch (ArithmeticException exception) {
throw fail("flash.delivery.tip.additional.invalid");
}
order.setTipAmount(tip);
order.setAmount(amount);
Date now = new Date();
order.setUpdateTime(now);
if (orderMapper.updateWaitingTip(order, request.getOrderVersion()) != 1) {
throw fail("flash.delivery.state.changed");
}
order.setVersion(order.getVersion() + 1);
writeLog(orderId, WAITING_ACCEPTANCE, WAITING_ACCEPTANCE, "USER", userId, "TIP_ADDED", now);
return participantDetail(order, true, false);
}
/** 校验并加载可编辑订单:本人、待接单、未绑定骑手且版本一致。 */
private FlashDeliveryOrder requireEditableOrder(Long userId, Long orderId, Integer version) {
requireUserId(userId);
if (orderId == null || orderId <= 0) throw fail("flash.delivery.order.not.found");
FlashDeliveryOrder order = requireOrder(orderId);
if (!Objects.equals(userId, order.getUserId())) throw fail("flash.delivery.order.not.found");
if (!WAITING_ACCEPTANCE.equals(order.getStatus()) || order.getRiderId() != null) {
throw fail("flash.delivery.edit.not.allowed");
}
if (version == null || version < 0) throw fail("flash.delivery.order.version.invalid");
if (!Objects.equals(version, order.getVersion())) throw fail("flash.delivery.state.changed");
return order;
}
/** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
FlashDeliveryAddressChangeRequest request) {
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");
}
RouteDistance route = routeService.calculate(
new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
// 待接单尚未成交,改址按"订单定价时点"的最新运价重算(预约单取预约取件时刻、
// 立即单取当前),管理员调价对未接单的改址立即生效;时段语义保住,预约单不会用到错误时段的价格。
Date pricingDate = "SCHEDULED".equals(order.getDeliveryMode())
? order.getScheduledPickupStartAt() : new Date();
FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate));
FlashDeliveryPriceBreakdown breakdown;
try {
breakdown = pricingCalculator.calculateBreakdown(pricing, route.distanceMeters(),
order.getDeliveryType(), order.getTipAmount());
} catch (ArithmeticException exception) {
throw fail("flash.delivery.tip.invalid");
}
FlashDeliveryQuoteRequest quoteRequest = new FlashDeliveryQuoteRequest();
quoteRequest.setServiceType(order.getServiceType());
quoteRequest.setDeliveryType(order.getDeliveryType());
quoteRequest.setDeliveryMode(order.getDeliveryMode());
quoteRequest.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
quoteRequest.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
FlashDeliveryQuoteView view = quoteView(quoteRequest, new QuoteContext(pricing, route, breakdown));
view.setOrderVersion(order.getVersion());
return view;
}
/** 用户取消:仅待接单或已接单(实际取件前)允许。 */
@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 (!isParticipant(userId, order)) throw fail("flash.delivery.order.not.found");
Date now = new Date();
if (orderMapper.transitionByParticipant(orderId, userId, DELIVERED, COMPLETED, now) != 1) {
throw fail("flash.delivery.complete.not.allowed");
}
writeLog(orderId, DELIVERED, COMPLETED, "USER", userId, null, now);
}
/**
* 骑手任务列表:按页签查询(newTask 可抢任务、toPickup 待取件、delivering 配送中、
* completed 已送达/已完成、cancelled 已取消)。newTask 页签额外按骑手坐标做距离过滤和就近排序,
* 并返回附近任务数与最高订单金额;其余页签按订单归属骑手查询。
*/
public FlashDeliveryRiderOrderPageView riderOrders(Long riderId, int pageNum, int pageSize,
String tab, BigDecimal longitude, BigDecimal latitude) {
InfoUser rider = requireRider(riderId);
if (!hasText(tab)) throw fail("flash.delivery.tab.invalid");
String selectedTab = tab.trim();
// lambda 无法捕获被重新赋值的参数,统一改用仅 newTask 携带坐标的有效变量。
BigDecimal effectiveLongitude = "newTask".equals(selectedTab) ? longitude : null;
BigDecimal effectiveLatitude = "newTask".equals(selectedTab) ? latitude : null;
if ("newTask".equals(selectedTab)) {
// 新任务派单按骑手选择的配送类型过滤;本人已接订单页签不受限制。
if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
validateRiderCoordinates(effectiveLongitude, effectiveLatitude);
}
QueryWrapper query = riderOrdersQuery(riderId, selectedTab,
effectiveLongitude, effectiveLatitude, true);
IPage orders = orderMapper.selectPage(page(pageNum, pageSize), query);
FlashDeliveryRiderOrderPageView result = new FlashDeliveryRiderOrderPageView();
result.setRecords(orders.getRecords().stream()
.map(order -> riderOrderListView(order, effectiveLongitude, effectiveLatitude)).toList());
result.setTotal(orders.getTotal());
result.setCurrent(orders.getCurrent());
result.setSize(orders.getSize());
if ("newTask".equals(selectedTab)) {
result.setNearbyTaskCount(orders.getTotal());
QueryWrapper maximumQuery = riderOrdersQuery(riderId, selectedTab,
effectiveLongitude, effectiveLatitude, false)
.select("MAX(amount)");
result.setHighestOrderAmount(firstLong(orderMapper.selectObjs(maximumQuery)));
}
return result;
}
/** 骑手任务详情:抢单前返回脱敏视图(隐藏联系人、电话、坐标和 PIN);接单后仅中单骑手可见完整信息。 */
public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
InfoUser rider = requireRider(riderId);
FlashDeliveryOrder order = requireOrder(orderId);
if (WAITING_ACCEPTANCE.equals(order.getStatus()) && order.getRiderId() == null) {
// 不兼容闪送的骑手按订单不存在处理,避免泄露待抢任务信息。
if (!isAvailableAt(order, new Date()) || !rider.supportsDeliveryType("FLASH")) {
throw fail("flash.delivery.order.not.found");
}
// 接单前显示取送文字地址,但隐藏联系人、电话、精确坐标、实际 PIN 和履约图片。
return participantDetail(order, false, true);
}
if (!Objects.equals(riderId, order.getRiderId())) throw fail("flash.delivery.order.not.found");
return participantDetail(order, false, false);
}
/** 原子抢单:骑手锁内完成互斥校验与条件更新,同一订单仅一名骑手成功。 */
@Transactional
public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
InfoUser rider = requireRider(riderId);
// 抢单属于获取新任务,要求骑手承接闪送配送。
if (!rider.supportsDeliveryType("FLASH")) throw fail("flash.delivery.rider.type.not.enabled");
return riderDeliveryLockService.withLock(riderId, () -> {
FlashDeliveryOrder target = requireOrder(orderId);
riderDeliveryExclusivityService.assertCanAcceptFlash(riderId, target.getDeliveryType());
Date now = new Date();
// Mapper 使用“待接单且 rider_id 为空”的条件更新,受影响行数为 1 才表示抢单成功。
if (orderMapper.accept(orderId, riderId, now) != 1) {
throw fail("flash.delivery.order.already.accepted");
}
writeLog(orderId, WAITING_ACCEPTANCE, ACCEPTED, "RIDER", riderId, null, now);
return participantDetail(requireOrder(orderId), false, false);
});
}
/** 骑手取件:提交取件凭证,订单由已接单推进为已取件。 */
@Transactional
public void pickup(Long riderId, Long orderId, FlashDeliveryProofRequest request) {
transitionWithProof(riderId, orderId, request, ACCEPTED, PICKED_UP, "PICKUP", "picked_up_at");
}
/** 骑手送达:校验交付 PIN(启用时)并提交送达凭证,订单由已取件推进为已送达。 */
@Transactional
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");
}
/** 平台运价配置列表,按时段开始时间排序。 */
public List adminPricing() {
return pricingMapper.selectList(new QueryWrapper()
.orderByAsc("start_time", "id"));
}
/** 新增运价配置:校验参数合法性与时段不重叠后入库,版本号从 1 开始。 */
@Transactional
public FlashDeliveryPricing createPricing(Long adminId, FlashDeliveryPricingRequest request) {
validatePricingRequest(request, false);
ensureNoPricingOverlap(request, null);
Date now = new Date();
FlashDeliveryPricing pricing = pricingFrom(request);
pricing.setConfigVersion(1);
pricing.setUpdatedBy(adminId);
pricing.setCreateTime(now);
pricing.setUpdateTime(now);
pricingMapper.insert(pricing);
return pricing;
}
/** 修改运价配置:按 configVersion 乐观锁条件更新,防止管理员并发互相覆盖。 */
@Transactional
public FlashDeliveryPricing updatePricing(Long adminId, Long pricingId,
FlashDeliveryPricingRequest request) {
validatePricingRequest(request, true);
FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
if (preliminary == null) throw fail("flash.delivery.pricing.not.found");
FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
if (pricing == null) throw fail("flash.delivery.pricing.not.found");
ensureNoPricingOverlap(request, pricingId);
Date now = new Date();
UpdateWrapper update = new UpdateWrapper()
.eq("id", pricingId).eq("config_version", request.getConfigVersion())
.set("start_time", request.getStartTime())
.set("end_time", request.getEndTime())
.set("starting_distance", scaleDistance(request.getStartingDistance()))
.set("starting_fare", request.getStartingFare())
.set("distance", scaleDistance(request.getDistance()))
.set("freight", request.getFreight())
.set("urgent_rate", normalizeRate(request.getUrgentRate()))
.set("minimum_urgent_fee", request.getMinimumUrgentFee())
.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.setStartTime(request.getStartTime());
pricing.setEndTime(request.getEndTime());
pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
pricing.setStartingFare(request.getStartingFare());
pricing.setDistance(scaleDistance(request.getDistance()));
pricing.setFreight(request.getFreight());
pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
pricing.setConfigVersion(request.getConfigVersion() + 1);
pricing.setUpdatedBy(adminId);
pricing.setUpdateTime(now);
return pricing;
}
/** 删除运价配置。 */
@Transactional
public void deletePricing(Long pricingId) {
FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
if (preliminary == null) {
throw fail("flash.delivery.pricing.not.found");
}
FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
if (pricing == null) throw fail("flash.delivery.pricing.not.found");
if (pricingMapper.deleteById(pricingId) != 1) throw fail("flash.delivery.pricing.not.found");
}
/** 平台订单分页:支持状态、业务场景、订单号模糊、寄件用户、骑手条件过滤。 */
public IPage adminOrders(int pageNum, int pageSize, String status,
String serviceType, String orderNo,
Long userId, Long riderId) {
validateOptionalServiceType(serviceType);
QueryWrapper query = new QueryWrapper().orderByDesc("id");
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 FlashDeliveryAdminOrderDetailView adminDetail(Long orderId) {
FlashDeliveryOrder order = requireOrder(orderId);
FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
view.setOrder(order);
view.setImages(imageMapper.selectList(new QueryWrapper()
.eq("order_id", orderId).orderByAsc("proof_type", "sort_order")));
view.setLogs(logMapper.selectList(new QueryWrapper()
.eq("order_id", orderId).orderByAsc("create_time", "id")));
return view;
}
/** 平台取消:任何非终态订单可取消,记录操作人与原因,用于异常履约人工收口。 */
@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);
}
/** 将已送达订单推进为已完成;平台手动与系统自动完成共用此入口,条件更新失败返回 false。 */
@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 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());
validateDeliveryType(request.getDeliveryType());
if (!hasText(request.getPackageType()) || !PACKAGE_TYPES.contains(request.getPackageType())) {
throw fail("flash.delivery.package.type.invalid");
}
validateItem(request);
Date now = new Date();
String deliveryMode = normalizeDeliveryMode(request.getDeliveryMode());
validateSchedule(deliveryMode, request.getScheduledPickupStartAt(), request.getScheduledPickupEndAt(), now);
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");
}
Date pricingDate = "SCHEDULED".equals(deliveryMode) ? request.getScheduledPickupStartAt() : now;
FlashDeliveryPricing pricing = findPricingAt(pricingTargetTime(pricingDate));
// 路线服务优先使用 Google Routes;不可用时在服务内部降级为球面直线距离。
RouteDistance route = routeService.calculate(
new GeoPoint(request.getPickup().getLatitude(), request.getPickup().getLongitude()),
new GeoPoint(request.getDelivery().getLatitude(), request.getDelivery().getLongitude()));
if (route.distanceMeters() > MAX_DISTANCE_METERS) throw fail("flash.delivery.distance.too.far");
FlashDeliveryPriceBreakdown breakdown = pricingCalculator.calculateBreakdown(pricing,
route.distanceMeters(), request.getDeliveryType(), request.getTipAmount());
return new QuoteContext(pricing, route, breakdown);
}
/** 查询指定时刻命中的运价配置,未配置时直接报错。 */
private FlashDeliveryPricing findPricingAt(String targetTime) {
FlashDeliveryPricing pricing = findPricingAtOrNull(targetTime);
if (pricing == null) throw fail("flash.delivery.pricing.not.available");
return pricing;
}
/** 查询命中运价;命中多条说明配置重叠,报错交由平台修正。 */
private FlashDeliveryPricing findPricingAtOrNull(String targetTime) {
List pricing = pricingMapper.selectAtTime(targetTime);
if (pricing == null || pricing.isEmpty()) return null;
if (pricing.size() > 1) throw fail("flash.delivery.pricing.overlap");
return pricing.get(0);
}
/** 校验运价请求:时间格式与区间顺序、各金额取值范围及修改时的版本号。 */
private void validatePricingRequest(FlashDeliveryPricingRequest request, boolean requireVersion) {
if (request == null) throw fail("flash.delivery.pricing.invalid");
BigDecimal startingDistance = normalizeDistance(request.getStartingDistance());
BigDecimal distance = normalizeDistance(request.getDistance());
BigDecimal urgentRate = normalizeRate(request.getUrgentRate());
if (!validStartTime(request.getStartTime()) || !validEndTime(request.getEndTime())
|| toMinute(request.getStartTime()) >= toMinute(request.getEndTime())
|| startingDistance == null || startingDistance.signum() <= 0
|| startingDistance.compareTo(MAX_PRICING_DISTANCE) > 0
|| request.getStartingFare() == null || request.getStartingFare() <= 0
|| distance == null || distance.signum() <= 0
|| distance.compareTo(MAX_PRICING_DISTANCE) > 0
|| request.getFreight() == null || request.getFreight() <= 0
|| urgentRate == null || urgentRate.signum() < 0
|| urgentRate.compareTo(MAX_PRICING_DISTANCE) > 0
|| request.getMinimumUrgentFee() == null || request.getMinimumUrgentFee() < 0
|| requireVersion && (request.getConfigVersion() == null || request.getConfigVersion() <= 0)) {
throw fail("flash.delivery.pricing.invalid");
}
}
/** 确保新时段与既有运价不重叠,excludedId 用于修改时排除自身。 */
private void ensureNoPricingOverlap(FlashDeliveryPricingRequest request, Long excludedId) {
if (pricingMapper.countOverlapping(request.getStartTime(), request.getEndTime(), excludedId) > 0) {
throw fail("flash.delivery.pricing.overlap");
}
}
/** 将运价请求转换为实体,距离与比例统一保留两位小数。 */
private FlashDeliveryPricing pricingFrom(FlashDeliveryPricingRequest request) {
FlashDeliveryPricing pricing = new FlashDeliveryPricing();
pricing.setStartTime(request.getStartTime());
pricing.setEndTime(request.getEndTime());
pricing.setStartingDistance(scaleDistance(request.getStartingDistance()));
pricing.setStartingFare(request.getStartingFare());
pricing.setDistance(scaleDistance(request.getDistance()));
pricing.setFreight(request.getFreight());
pricing.setUrgentRate(normalizeRate(request.getUrgentRate()));
pricing.setMinimumUrgentFee(request.getMinimumUrgentFee());
return pricing;
}
private boolean validStartTime(String value) {
return value != null && value.matches("(?:[01]\\d|2[0-3]):[0-5]\\d");
}
private boolean validEndTime(String value) {
return "24:00".equals(value) || validStartTime(value);
}
private int toMinute(String value) {
if ("24:00".equals(value)) return 24 * 60;
return Integer.parseInt(value.substring(0, 2)) * 60 + Integer.parseInt(value.substring(3));
}
private BigDecimal scaleDistance(BigDecimal value) {
return normalizeDistance(value);
}
private BigDecimal normalizeDistance(BigDecimal value) {
return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal normalizeRate(BigDecimal value) {
return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
}
/** 骑手带凭证的状态推进:校验凭证后按骑手归属条件更新,再落图片与日志。 */
private void transitionWithProof(Long riderId, Long orderId, FlashDeliveryProofRequest request,
String expected, String next, String proofType, String timeColumn) {
List urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
requireRider(riderId);
Date now = new Date();
if (orderMapper.transitionByRider(orderId, riderId, expected, next, timeColumn, now) != 1) {
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 urls, Date now) {
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.setOperatorType(operatorType);
image.setOperatorId(operatorId);
image.setCreateTime(now);
imageMapper.insert(image);
}
}
/** 校验凭证图片 URL:必填时 1 至 9 张,每项为合法 URI 且不超过 1000 字符。 */
private List validateProofUrls(List values, boolean required) {
if (values == null || values.isEmpty()) {
if (!required) return List.of();
throw fail("flash.delivery.proof.required");
}
if (values.size() > 9) throw fail("flash.delivery.proof.too.many");
List urls = new ArrayList<>();
for (String value : values) {
try {
String url = value == null ? null : value.trim();
if (url == null || url.isEmpty() || url.length() > 1000) {
throw new IllegalArgumentException();
}
URI.create(url);
urls.add(url);
} catch (IllegalArgumentException exception) {
throw fail("flash.delivery.proof.url.invalid");
}
}
return urls;
}
/** 组装用户或骑手视角详情;hideSensitiveFields 为 true 时按抢单前脱敏返回。 */
private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
boolean userView,
boolean hideSensitiveFields) {
FlashDeliveryOrderDetailView view = detailOrderView(order, !hideSensitiveFields);
if (userView) {
view.setOrderVersion(order.getVersion());
view.setDeliveryPinCode(Boolean.TRUE.equals(order.getPinRequired()) ? order.getDeliveryPinCode() : null);
view.setRider(riderSummary(order));
}
if (!hideSensitiveFields && order.getId() != null) {
groupImageUrls(view, imageMapper.selectList(new QueryWrapper()
.eq("order_id", order.getId()).orderByAsc("proof_type", "sort_order")));
}
return view;
}
/** 订单实体转用户端订单卡片。 */
private FlashDeliveryUserOrderListView userOrderListView(FlashDeliveryOrder order) {
FlashDeliveryUserOrderListView view = new FlashDeliveryUserOrderListView();
view.setId(order.getId());
view.setOrderNo(order.getOrderNo());
view.setServiceType(order.getServiceType());
view.setDeliveryType(order.getDeliveryType());
view.setStatus(order.getStatus());
view.setOrderVersion(order.getVersion());
view.setPackageType(order.getPackageType());
view.setQuantity(order.getQuantity());
view.setWeightRange(order.getWeightRange());
view.setDeliveryMode(order.getDeliveryMode());
view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
view.setPickupAddress(order.getPickupAddress());
view.setPickupDetailAddress(order.getPickupAddressDetail());
view.setDeliveryAddress(order.getDeliveryAddress());
view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
view.setBaseDeliveryFee(order.getBaseDeliveryFee());
view.setUrgentFee(order.getUrgentFee());
view.setTipAmount(order.getTipAmount());
view.setAmount(order.getAmount());
view.setCurrency(order.getCurrency());
view.setDeliveredAt(order.getDeliveredAt());
view.setCreateTime(order.getCreateTime());
return view;
}
/** 订单实体转骑手端任务卡片,可附带骑手到取件点的直线距离。 */
private FlashDeliveryRiderOrderListView riderOrderListView(FlashDeliveryOrder order,
BigDecimal longitude, BigDecimal latitude) {
FlashDeliveryRiderOrderListView view = new FlashDeliveryRiderOrderListView();
view.setId(order.getId());
view.setOrderNo(order.getOrderNo());
view.setServiceType(order.getServiceType());
view.setDeliveryType(order.getDeliveryType());
view.setStatus(order.getStatus());
view.setPackageType(order.getPackageType());
view.setQuantity(order.getQuantity());
view.setWeightRange(order.getWeightRange());
view.setSpecification(order.getSpecification());
view.setDeliveryMode(order.getDeliveryMode());
view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
view.setPinRequired(order.getPinRequired());
view.setPickupAddress(order.getPickupAddress());
view.setPickupDetailAddress(order.getPickupAddressDetail());
view.setDeliveryAddress(order.getDeliveryAddress());
view.setDeliveryDetailAddress(order.getDeliveryAddressDetail());
view.setPayType(order.getPayType());
view.setPickupDistanceMeters(distanceMeters(longitude, latitude,
order.getPickupLongitude(), order.getPickupLatitude()));
view.setDistanceMeters(order.getDistanceMeters());
view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
view.setBaseDeliveryFee(order.getBaseDeliveryFee());
view.setDistanceFee(order.getDistanceFee());
view.setUrgentFee(order.getUrgentFee());
view.setTipAmount(order.getTipAmount());
view.setAmount(order.getAmount());
view.setCurrency(order.getCurrency());
view.setCreateTime(order.getCreateTime());
return view;
}
/** 订单实体转详情基础视图。 */
private FlashDeliveryOrderDetailView detailOrderView(FlashDeliveryOrder order, boolean includeSensitiveFields) {
FlashDeliveryOrderDetailView view = new FlashDeliveryOrderDetailView();
populateOrderView(view, order, includeSensitiveFields);
return view;
}
/** 按凭证类型把图片分组为寄件、取件、送达三组 URL。 */
private void groupImageUrls(FlashDeliveryOrderDetailView view, List images) {
List sender = new ArrayList<>();
List pickup = new ArrayList<>();
List delivery = new ArrayList<>();
for (FlashDeliveryOrderImage image : images) {
if ("SENDER".equals(image.getProofType())) sender.add(image.getImageUrl());
else if ("PICKUP".equals(image.getProofType())) pickup.add(image.getImageUrl());
else if ("DELIVERY".equals(image.getProofType())) delivery.add(image.getImageUrl());
}
view.setSenderImageUrls(sender);
view.setPickupImageUrls(pickup);
view.setDeliveryImageUrls(delivery);
}
/** 运价实体转首页服务摘要视图。 */
private FlashDeliveryServiceView serviceView(FlashDeliveryPricing pricing, String serviceType) {
FlashDeliveryServiceView view = new FlashDeliveryServiceView();
view.setServiceType(serviceType);
view.setPricingId(pricing.getId());
view.setStartTime(pricing.getStartTime());
view.setEndTime(pricing.getEndTime());
view.setStartingDistance(pricing.getStartingDistance());
view.setStartingFare(pricing.getStartingFare());
view.setDistance(pricing.getDistance());
view.setFreight(pricing.getFreight());
view.setUrgentRate(pricing.getUrgentRate());
view.setMinimumUrgentFee(pricing.getMinimumUrgentFee());
view.setPricingVersion(pricing.getConfigVersion());
view.setCurrency("TWD");
return view;
}
/** 将订单实体的公共字段填充到视图,includeSensitiveFields 控制是否包含敏感字段。 */
private void populateOrderView(FlashDeliveryOrderView view, FlashDeliveryOrder order,
boolean includeSensitiveFields) {
view.setId(order.getId());
view.setOrderNo(order.getOrderNo());
view.setServiceType(order.getServiceType());
view.setDeliveryType(order.getDeliveryType());
view.setStatus(order.getStatus());
view.setPackageType(order.getPackageType());
view.setQuantity(order.getQuantity());
view.setWeightRange(order.getWeightRange());
view.setSpecification(order.getSpecification());
view.setDeliveryMode(order.getDeliveryMode());
view.setScheduledPickupStartAt(order.getScheduledPickupStartAt());
view.setScheduledPickupEndAt(order.getScheduledPickupEndAt());
view.setPinRequired(order.getPinRequired());
view.setPickup(addressView(order, true, includeSensitiveFields));
view.setDelivery(addressView(order, false, includeSensitiveFields));
view.setDistanceMeters(order.getDistanceMeters());
view.setDistanceSource(order.getDistanceSource());
view.setEstimatedDurationSeconds(order.getEstimatedDurationSeconds());
view.setBaseDeliveryFee(order.getBaseDeliveryFee());
view.setDistanceFee(order.getDistanceFee());
view.setUrgentFee(order.getUrgentFee());
view.setTipAmount(order.getTipAmount());
view.setAmount(order.getAmount());
view.setCurrency(order.getCurrency());
view.setPayType(order.getPayType());
view.setUserNote(includeSensitiveFields ? order.getUserNote() : null);
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());
}
/** 从订单实体提取取件或收件地址视图;脱敏时不返回联系人、电话和坐标。 */
private FlashDeliveryAddressView addressView(FlashDeliveryOrder order, boolean pickup,
boolean includeSensitiveFields) {
FlashDeliveryAddressView view = new FlashDeliveryAddressView();
if (pickup) {
view.setName(includeSensitiveFields ? order.getPickupName() : null);
view.setPhone(includeSensitiveFields ? order.getPickupPhone() : null);
view.setAddress(order.getPickupAddress()); view.setAddressDetail(order.getPickupAddressDetail());
view.setCity(order.getPickupCity()); view.setArea(order.getPickupArea());
view.setHandoffMethod(order.getPickupHandoffMethod());
view.setLongitude(includeSensitiveFields ? order.getPickupLongitude() : null);
view.setLatitude(includeSensitiveFields ? order.getPickupLatitude() : null);
} else {
view.setName(includeSensitiveFields ? order.getDeliveryName() : null);
view.setPhone(includeSensitiveFields ? order.getDeliveryPhone() : null);
view.setAddress(order.getDeliveryAddress()); view.setAddressDetail(order.getDeliveryAddressDetail());
view.setCity(order.getDeliveryCity()); view.setArea(order.getDeliveryArea());
view.setHandoffMethod(order.getDeliveryHandoffMethod());
view.setLongitude(includeSensitiveFields ? order.getDeliveryLongitude() : null);
view.setLatitude(includeSensitiveFields ? order.getDeliveryLatitude() : null);
}
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.setBillableDistance(breakdown.getBillableDistance());
view.setDistanceFee(breakdown.getDistanceFee());
view.setBaseDeliveryFee(breakdown.getBaseDeliveryFee());
view.setUrgentFee(breakdown.getUrgentFee());
view.setTipAmount(breakdown.getTipAmount());
view.setAmount(breakdown.getAmount());
}
/** 把请求地址复制为订单取件或收件快照字段。 */
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.setPickupCity(trimToNull(source.getCity())); target.setPickupArea(trimToNull(source.getArea()));
target.setPickupHandoffMethod(trimToNull(source.getHandoffMethod()));
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.setDeliveryCity(trimToNull(source.getCity())); target.setDeliveryArea(trimToNull(source.getArea()));
target.setDeliveryHandoffMethod(trimToNull(source.getHandoffMethod()));
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)
|| 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.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0
|| address.getLongitude().compareTo(BigDecimal.valueOf(180)) > 0) {
throw fail("flash.delivery.address.invalid");
}
}
/** 收件手机号归一化后唯一匹配普通用户作为收件人;匹配零个或多个视为无收件人。 */
private Long resolveReceiverUserId(String phone) {
if (!hasText(phone)) return null;
StringBuilder normalized = new StringBuilder(phone.length());
for (int index = 0; index < phone.length(); index++) {
char value = phone.charAt(index);
if (Character.isWhitespace(value) || value == '+' || value == '-'
|| value == '(' || value == ')') {
continue;
}
if (value < '0' || value > '9') return null;
normalized.append(value);
}
if (normalized.isEmpty()) return null;
List matches = userMapper.selectOrdinaryUserIdsByNormalizedPhone(normalized.toString());
return matches != null && matches.size() == 1 ? matches.get(0) : null;
}
/** 判断用户是否为订单寄件人或匹配收件人。 */
private boolean isParticipant(Long userId, FlashDeliveryOrder order) {
return Objects.equals(userId, order.getUserId())
|| Objects.equals(userId, order.getReceiverUserId());
}
/** 校验 token 用户为骑手(userType=2)并返回其资料。 */
private InfoUser requireRider(Long riderId) {
InfoUser rider = riderId == null ? null : userMapper.selectInfoUserByUserId(riderId);
if (rider == null || !"2".equals(rider.getUserType())) throw fail("flash.delivery.rider.required");
return rider;
}
/** 加载订单,不存在时统一按订单不存在报错。 */
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 validateDeliveryType(String value) {
if (!hasText(value) || !DELIVERY_TYPES.contains(value)) {
throw fail("flash.delivery.delivery.type.invalid");
}
}
/** 校验物品信息:数量、重量范围、规格长度和小费。 */
private void validateItem(FlashDeliveryQuoteRequest request) {
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");
}
if (request.getTipAmount() == null || request.getTipAmount() < 0) {
throw fail("flash.delivery.tip.invalid");
}
}
/** 支付方式与主订单系统同域(4=现金、6=线下转账);闪送款项直达骑手,仅允许线下方式,缺省现金,下单后锁定。 */
private String normalizePayType(String payType) {
if (!hasText(payType)) return PAY_TYPE_CASH;
String trimmed = payType.trim();
return trimmed;
// if (PAY_TYPE_CASH.equals(trimmed) || PAY_TYPE_TRANSFER.equals(trimmed)) return trimmed;
// throw fail("flash.delivery.pay.type.invalid");
}
/** 将时间转为台北时区 HH:mm,用于命中运价时段。 */
private String pricingTargetTime(Date date) {
return PRICING_TIME_FORMAT.format(Instant.ofEpochMilli(date.getTime())
.atZone(TAIPEI).toLocalTime());
}
/** 比对客户端回传报价与服务端最新报价是否完全一致。 */
private boolean matchesQuotedPrice(FlashDeliveryCreateRequest 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());
}
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");
}
/** 构造分页对象并夹紧页码与每页数量(1 至 100)。 */
private Page page(int pageNum, int pageSize) {
return new Page<>(Math.max(1, pageNum), Math.min(Math.max(1, pageSize), 100));
}
/** 分页结果实体转换,保留分页元数据。 */
private IPage mapPage(IPage source,
Function mapper) {
Page result = new Page<>(source.getCurrent(), source.getSize(), source.getTotal());
result.setRecords(source.getRecords().stream().map(mapper).toList());
return result;
}
/** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
private QueryWrapper riderOrdersQuery(Long riderId, String tab,
BigDecimal longitude, BigDecimal latitude,
boolean orderResults) {
QueryWrapper query = new QueryWrapper<>();
Date now = new Date();
switch (tab) {
case "newTask" -> {
query.eq("status", WAITING_ACCEPTANCE).isNull("rider_id")
.and(item -> item.eq("delivery_mode", "NOW")
.or().le("scheduled_pickup_start_at", now));
if (longitude != null) {
Integer limitKm = newTaskDistanceLimit();
if (limitKm != null) {
query.apply("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point({0}, {1})) <= {2}",
longitude, latitude, limitKm.longValue() * 1000L);
}
if (orderResults) {
query.orderByAsc("ST_Distance_Sphere(point(pickup_longitude, pickup_latitude), point(" + longitude
+ ", " + latitude + "))");
}
} else if (orderResults) {
query.orderByDesc("create_time");
}
}
case "toPickup" -> {
query.eq("rider_id", riderId).eq("status", ACCEPTED);
if (orderResults) query.orderByAsc("create_time");
}
case "delivering" -> {
query.eq("rider_id", riderId).eq("status", PICKED_UP);
if (orderResults) query.orderByAsc("create_time");
}
case "completed" -> {
query.eq("rider_id", riderId).in("status", DELIVERED, COMPLETED);
if (orderResults) query.orderByDesc("create_time");
}
case "cancelled" -> {
query.eq("rider_id", riderId).eq("status", CANCELLED);
if (orderResults) query.orderByDesc("create_time");
}
default -> throw fail("flash.delivery.tab.invalid");
}
return query;
}
/** 校验骑手坐标:经纬度必须成对出现且在合法范围。 */
private void validateRiderCoordinates(BigDecimal longitude, BigDecimal latitude) {
if ((longitude == null) != (latitude == null)
|| longitude != null && (longitude.compareTo(BigDecimal.valueOf(-180)) < 0
|| longitude.compareTo(BigDecimal.valueOf(180)) > 0
|| latitude.compareTo(BigDecimal.valueOf(-90)) < 0
|| latitude.compareTo(BigDecimal.valueOf(90)) > 0)) {
throw fail("flash.delivery.coordinates.invalid");
}
}
/** 读取字典 sys_qs_newtask_distance 的新任务距离上限(公里);缺失或非法时不限距离。 */
private Integer newTaskDistanceLimit() {
try {
List values = DictUtils.getDictCache("sys_qs_newtask_distance");
if (values != null && !values.isEmpty()) {
int limit = Integer.parseInt(values.get(0).getDictValue());
return limit > 0 ? limit : null;
}
} catch (Exception ignored) {
// Keep the established rider-list behaviour: absent or malformed configuration disables distance filtering.
}
return null;
}
/** Haversine 球面直线距离(米),用于骑手列表展示距取件点的距离。 */
private Integer distanceMeters(BigDecimal longitude, BigDecimal latitude,
BigDecimal targetLongitude, BigDecimal targetLatitude) {
if (longitude == null || targetLongitude == null || targetLatitude == null) return null;
double latitudeRadians = Math.toRadians(latitude.doubleValue());
double targetLatitudeRadians = Math.toRadians(targetLatitude.doubleValue());
double latitudeDelta = targetLatitudeRadians - latitudeRadians;
double longitudeDelta = Math.toRadians(targetLongitude.doubleValue() - longitude.doubleValue());
double value = Math.sin(latitudeDelta / 2) * Math.sin(latitudeDelta / 2)
+ Math.cos(latitudeRadians) * Math.cos(targetLatitudeRadians)
* Math.sin(longitudeDelta / 2) * Math.sin(longitudeDelta / 2);
return (int) Math.round(6_371_000D * 2D * Math.atan2(Math.sqrt(value), Math.sqrt(1D - value)));
}
/** 从 selectObjs 聚合结果中取第一个 Long 值。 */
private Long firstLong(List