|
|
@@ -91,6 +91,7 @@ public class FlashDeliveryApplicationService {
|
|
|
this.riderDeliveryExclusivityService = riderDeliveryExclusivityService;
|
|
|
}
|
|
|
|
|
|
+ /** 闪送首页:返回开放的业务场景、配送等级,以及当前时段各服务的公开运价摘要。 */
|
|
|
public FlashDeliveryHomeView home() {
|
|
|
FlashDeliveryHomeView view = new FlashDeliveryHomeView();
|
|
|
view.setServiceTypes(SERVICE_TYPES);
|
|
|
@@ -101,12 +102,14 @@ public class FlashDeliveryApplicationService {
|
|
|
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());
|
|
|
@@ -132,6 +135,11 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 幂等创建订单:clientRequestId 与 userId 构成唯一键,重复提交直接返回已有订单。
|
|
|
+ * 创建前按最新条件重新报价,客户端回传报价不一致时抛出 FlashDeliveryQuoteChangedException;
|
|
|
+ * 成功后保存计价快照、寄件图片和初始状态日志。
|
|
|
+ */
|
|
|
@Transactional
|
|
|
public FlashDeliveryOrderDetailView create(Long userId, FlashDeliveryCreateRequest request) {
|
|
|
requireUserId(userId);
|
|
|
@@ -214,6 +222,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return participantDetail(order, true, false);
|
|
|
}
|
|
|
|
|
|
+ /** 分页查询当前用户参与的订单卡片;role=sender 按寄件人、receiver 按收件人过滤。 */
|
|
|
public IPage<FlashDeliveryUserOrderListView> 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)) {
|
|
|
@@ -225,6 +234,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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");
|
|
|
@@ -239,6 +249,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return calculateAddressQuote(order, request);
|
|
|
}
|
|
|
|
|
|
+ /** 确认修改待接单订单地址:重算报价并与回传值比对,按版本条件更新。 */
|
|
|
@Transactional
|
|
|
public FlashDeliveryOrderDetailView updateAddress(Long userId, Long orderId,
|
|
|
FlashDeliveryAddressConfirmRequest request) {
|
|
|
@@ -273,6 +284,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return participantDetail(order, true, false);
|
|
|
}
|
|
|
|
|
|
+ /** 待接单订单追加小费:订单总额与小费同步增加,版本条件防止并发重复加款。 */
|
|
|
@Transactional
|
|
|
public FlashDeliveryOrderDetailView addTip(Long userId, Long orderId, FlashDeliveryTipAddRequest request) {
|
|
|
if (request == null) throw fail("flash.delivery.request.required");
|
|
|
@@ -302,6 +314,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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");
|
|
|
@@ -315,6 +328,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return order;
|
|
|
}
|
|
|
|
|
|
+ /** 地址修改报价:使用订单保存的运价快照重算,不采用当前运价,避免改地址顺带变价。 */
|
|
|
private FlashDeliveryQuoteView calculateAddressQuote(FlashDeliveryOrder order,
|
|
|
FlashDeliveryAddressChangeRequest request) {
|
|
|
validateAddress(request.getPickup());
|
|
|
@@ -358,6 +372,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /** 用户取消:仅待接单或已接单(实际取件前)允许。 */
|
|
|
@Transactional
|
|
|
public void userCancel(Long userId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
@@ -371,6 +386,7 @@ public class FlashDeliveryApplicationService {
|
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "USER", userId, reason, now);
|
|
|
}
|
|
|
|
|
|
+ /** 用户(寄件人或收件人)确认收货:已送达推进为已完成。 */
|
|
|
@Transactional
|
|
|
public void confirmReceipt(Long userId, Long orderId) {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
@@ -382,6 +398,11 @@ public class FlashDeliveryApplicationService {
|
|
|
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);
|
|
|
@@ -414,6 +435,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /** 骑手任务详情:抢单前返回脱敏视图(隐藏联系人、电话、坐标和 PIN);接单后仅中单骑手可见完整信息。 */
|
|
|
public FlashDeliveryOrderDetailView riderDetail(Long riderId, Long orderId) {
|
|
|
InfoUser rider = requireRider(riderId);
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
@@ -429,6 +451,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return participantDetail(order, false, false);
|
|
|
}
|
|
|
|
|
|
+ /** 原子抢单:骑手锁内完成互斥校验与条件更新,同一订单仅一名骑手成功。 */
|
|
|
@Transactional
|
|
|
public FlashDeliveryOrderDetailView accept(Long riderId, Long orderId) {
|
|
|
InfoUser rider = requireRider(riderId);
|
|
|
@@ -447,11 +470,13 @@ public class FlashDeliveryApplicationService {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /** 骑手取件:提交取件凭证,订单由已接单推进为已取件。 */
|
|
|
@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);
|
|
|
@@ -466,11 +491,13 @@ public class FlashDeliveryApplicationService {
|
|
|
transitionWithProof(riderId, orderId, request, PICKED_UP, DELIVERED, "DELIVERY", "delivered_at");
|
|
|
}
|
|
|
|
|
|
+ /** 平台运价配置列表,按时段开始时间排序。 */
|
|
|
public List<FlashDeliveryPricing> adminPricing() {
|
|
|
return pricingMapper.selectList(new QueryWrapper<FlashDeliveryPricing>()
|
|
|
.orderByAsc("start_time", "id"));
|
|
|
}
|
|
|
|
|
|
+ /** 新增运价配置:校验参数合法性与时段不重叠后入库,版本号从 1 开始。 */
|
|
|
@Transactional
|
|
|
public FlashDeliveryPricing createPricing(Long adminId, FlashDeliveryPricingRequest request) {
|
|
|
validatePricingRequest(request, false);
|
|
|
@@ -485,6 +512,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return pricing;
|
|
|
}
|
|
|
|
|
|
+ /** 修改运价配置:按 configVersion 乐观锁条件更新,防止管理员并发互相覆盖。 */
|
|
|
@Transactional
|
|
|
public FlashDeliveryPricing updatePricing(Long adminId, Long pricingId,
|
|
|
FlashDeliveryPricingRequest request) {
|
|
|
@@ -524,6 +552,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return pricing;
|
|
|
}
|
|
|
|
|
|
+ /** 删除运价配置。 */
|
|
|
@Transactional
|
|
|
public void deletePricing(Long pricingId) {
|
|
|
FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
|
|
|
@@ -535,6 +564,7 @@ public class FlashDeliveryApplicationService {
|
|
|
if (pricingMapper.deleteById(pricingId) != 1) throw fail("flash.delivery.pricing.not.found");
|
|
|
}
|
|
|
|
|
|
+ /** 平台订单分页:支持状态、业务场景、订单号模糊、寄件用户、骑手条件过滤。 */
|
|
|
public IPage<FlashDeliveryOrder> adminOrders(int pageNum, int pageSize, String status,
|
|
|
String serviceType, String orderNo,
|
|
|
Long userId, Long riderId) {
|
|
|
@@ -548,6 +578,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return orderMapper.selectPage(page(pageNum, pageSize), query);
|
|
|
}
|
|
|
|
|
|
+ /** 平台订单审计详情:返回完整订单实体、全部凭证图片和状态日志。 */
|
|
|
public FlashDeliveryAdminOrderDetailView adminDetail(Long orderId) {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
FlashDeliveryAdminOrderDetailView view = new FlashDeliveryAdminOrderDetailView();
|
|
|
@@ -559,6 +590,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /** 平台取消:任何非终态订单可取消,记录操作人与原因,用于异常履约人工收口。 */
|
|
|
@Transactional
|
|
|
public void adminCancel(Long adminId, Long orderId, FlashDeliveryReasonRequest request) {
|
|
|
FlashDeliveryOrder order = requireOrder(orderId);
|
|
|
@@ -571,6 +603,7 @@ public class FlashDeliveryApplicationService {
|
|
|
writeLog(orderId, order.getStatus(), CANCELLED, "ADMIN", adminId, reason, now);
|
|
|
}
|
|
|
|
|
|
+ /** 将已送达订单推进为已完成;平台手动与系统自动完成共用此入口,条件更新失败返回 false。 */
|
|
|
@Transactional
|
|
|
public boolean complete(Long operatorId, String operatorType, Long orderId) {
|
|
|
Date now = new Date();
|
|
|
@@ -579,10 +612,12 @@ public class FlashDeliveryApplicationService {
|
|
|
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());
|
|
|
@@ -613,12 +648,14 @@ public class FlashDeliveryApplicationService {
|
|
|
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<FlashDeliveryPricing> pricing = pricingMapper.selectAtTime(targetTime);
|
|
|
if (pricing == null || pricing.isEmpty()) return null;
|
|
|
@@ -626,6 +663,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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());
|
|
|
@@ -647,12 +685,14 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 确保新时段与既有运价不重叠,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());
|
|
|
@@ -691,6 +731,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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<String> urls = validateProofUrls(request == null ? null : request.getImageUrls(), true);
|
|
|
@@ -719,6 +760,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 校验凭证图片 URL:必填时 1 至 9 张,每项为合法 URI 且不超过 1000 字符。 */
|
|
|
private List<String> validateProofUrls(List<String> values, boolean required) {
|
|
|
if (values == null || values.isEmpty()) {
|
|
|
if (!required) return List.of();
|
|
|
@@ -741,6 +783,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return urls;
|
|
|
}
|
|
|
|
|
|
+ /** 组装用户或骑手视角详情;hideSensitiveFields 为 true 时按抢单前脱敏返回。 */
|
|
|
private FlashDeliveryOrderDetailView participantDetail(FlashDeliveryOrder order,
|
|
|
boolean userView,
|
|
|
boolean hideSensitiveFields) {
|
|
|
@@ -757,6 +800,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /** 订单实体转用户端订单卡片。 */
|
|
|
private FlashDeliveryUserOrderListView userOrderListView(FlashDeliveryOrder order) {
|
|
|
FlashDeliveryUserOrderListView view = new FlashDeliveryUserOrderListView();
|
|
|
view.setId(order.getId());
|
|
|
@@ -786,6 +830,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /** 订单实体转骑手端任务卡片,可附带骑手到取件点的直线距离。 */
|
|
|
private FlashDeliveryRiderOrderListView riderOrderListView(FlashDeliveryOrder order,
|
|
|
BigDecimal longitude, BigDecimal latitude) {
|
|
|
FlashDeliveryRiderOrderListView view = new FlashDeliveryRiderOrderListView();
|
|
|
@@ -821,12 +866,14 @@ public class FlashDeliveryApplicationService {
|
|
|
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<FlashDeliveryOrderImage> images) {
|
|
|
List<String> sender = new ArrayList<>();
|
|
|
List<String> pickup = new ArrayList<>();
|
|
|
@@ -841,6 +888,7 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setDeliveryImageUrls(delivery);
|
|
|
}
|
|
|
|
|
|
+ /** 运价实体转首页服务摘要视图。 */
|
|
|
private FlashDeliveryServiceView serviceView(FlashDeliveryPricing pricing, String serviceType) {
|
|
|
FlashDeliveryServiceView view = new FlashDeliveryServiceView();
|
|
|
view.setServiceType(serviceType);
|
|
|
@@ -858,6 +906,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /** 将订单实体的公共字段填充到视图,includeSensitiveFields 控制是否包含敏感字段。 */
|
|
|
private void populateOrderView(FlashDeliveryOrderView view, FlashDeliveryOrder order,
|
|
|
boolean includeSensitiveFields) {
|
|
|
view.setId(order.getId());
|
|
|
@@ -896,6 +945,7 @@ public class FlashDeliveryApplicationService {
|
|
|
view.setUpdateTime(order.getUpdateTime());
|
|
|
}
|
|
|
|
|
|
+ /** 从订单实体提取取件或收件地址视图;脱敏时不返回联系人、电话和坐标。 */
|
|
|
private FlashDeliveryAddressView addressView(FlashDeliveryOrder order, boolean pickup,
|
|
|
boolean includeSensitiveFields) {
|
|
|
FlashDeliveryAddressView view = new FlashDeliveryAddressView();
|
|
|
@@ -919,6 +969,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /** 组装骑手公开资料;仅已接单或已取件状态附带骑手实时位置。 */
|
|
|
private FlashDeliveryRiderSummaryView riderSummary(FlashDeliveryOrder order) {
|
|
|
if (order.getRiderId() == null) return null;
|
|
|
InfoUser rider = userMapper.selectInfoUserByUserId(order.getRiderId());
|
|
|
@@ -935,6 +986,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
+ /** 把费用明细写入报价视图。 */
|
|
|
private void applyBreakdown(FlashDeliveryQuoteView view, FlashDeliveryPriceBreakdown breakdown) {
|
|
|
view.setBillableDistance(breakdown.getBillableDistance());
|
|
|
view.setDistanceFee(breakdown.getDistanceFee());
|
|
|
@@ -944,6 +996,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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());
|
|
|
@@ -960,6 +1013,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 校验地址请求:必填项、长度限制和经纬度取值范围。 */
|
|
|
private void validateAddress(FlashDeliveryAddressRequest address) {
|
|
|
if (address == null || !hasText(address.getName()) || !hasText(address.getPhone())
|
|
|
|| !hasText(address.getAddress()) || address.getLatitude() == null || address.getLongitude() == null
|
|
|
@@ -976,6 +1030,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 收件手机号归一化后唯一匹配普通用户作为收件人;匹配零个或多个视为无收件人。 */
|
|
|
private Long resolveReceiverUserId(String phone) {
|
|
|
if (!hasText(phone)) return null;
|
|
|
StringBuilder normalized = new StringBuilder(phone.length());
|
|
|
@@ -993,23 +1048,27 @@ public class FlashDeliveryApplicationService {
|
|
|
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();
|
|
|
@@ -1018,6 +1077,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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");
|
|
|
@@ -1035,6 +1095,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 校验物品信息:数量、重量范围、规格长度和小费。 */
|
|
|
private void validateItem(FlashDeliveryQuoteRequest request) {
|
|
|
if (request.getQuantity() == null || request.getQuantity() <= 0
|
|
|
|| !hasText(request.getWeightRange()) || !WEIGHT_RANGES.contains(request.getWeightRange())
|
|
|
@@ -1050,15 +1111,18 @@ public class FlashDeliveryApplicationService {
|
|
|
private String normalizePayType(String payType) {
|
|
|
if (!hasText(payType)) return PAY_TYPE_CASH;
|
|
|
String trimmed = payType.trim();
|
|
|
- if (PAY_TYPE_CASH.equals(trimmed) || PAY_TYPE_TRANSFER.equals(trimmed)) return trimmed;
|
|
|
- throw fail("flash.delivery.pay.type.invalid");
|
|
|
+ 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())
|
|
|
@@ -1076,10 +1140,12 @@ public class FlashDeliveryApplicationService {
|
|
|
if (userId == null || userId <= 0) throw fail("flash.delivery.auth.required");
|
|
|
}
|
|
|
|
|
|
+ /** 构造分页对象并夹紧页码与每页数量(1 至 100)。 */
|
|
|
private Page<FlashDeliveryOrder> page(int pageNum, int pageSize) {
|
|
|
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());
|
|
|
@@ -1087,6 +1153,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /** 按页签构建骑手任务查询:newTask 限定可抢状态与时间窗,可选距离过滤和就近排序。 */
|
|
|
private QueryWrapper<FlashDeliveryOrder> riderOrdersQuery(Long riderId, String tab,
|
|
|
BigDecimal longitude, BigDecimal latitude,
|
|
|
boolean orderResults) {
|
|
|
@@ -1132,6 +1199,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return query;
|
|
|
}
|
|
|
|
|
|
+ /** 校验骑手坐标:经纬度必须成对出现且在合法范围。 */
|
|
|
private void validateRiderCoordinates(BigDecimal longitude, BigDecimal latitude) {
|
|
|
if ((longitude == null) != (latitude == null)
|
|
|
|| longitude != null && (longitude.compareTo(BigDecimal.valueOf(-180)) < 0
|
|
|
@@ -1142,6 +1210,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 读取字典 sys_qs_newtask_distance 的新任务距离上限(公里);缺失或非法时不限距离。 */
|
|
|
private Integer newTaskDistanceLimit() {
|
|
|
try {
|
|
|
List<SysDictData> values = DictUtils.getDictCache("sys_qs_newtask_distance");
|
|
|
@@ -1155,6 +1224,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ /** Haversine 球面直线距离(米),用于骑手列表展示距取件点的距离。 */
|
|
|
private Integer distanceMeters(BigDecimal longitude, BigDecimal latitude,
|
|
|
BigDecimal targetLongitude, BigDecimal targetLatitude) {
|
|
|
if (longitude == null || targetLongitude == null || targetLatitude == null) return null;
|
|
|
@@ -1168,6 +1238,7 @@ public class FlashDeliveryApplicationService {
|
|
|
return (int) Math.round(6_371_000D * 2D * Math.atan2(Math.sqrt(value), Math.sqrt(1D - value)));
|
|
|
}
|
|
|
|
|
|
+ /** 从 selectObjs 聚合结果中取第一个 Long 值。 */
|
|
|
private Long firstLong(List<Object> values) {
|
|
|
if (values == null || values.isEmpty() || values.get(0) == null) return null;
|
|
|
Object value = values.get(0);
|
|
|
@@ -1175,12 +1246,14 @@ public class FlashDeliveryApplicationService {
|
|
|
return Long.valueOf(String.valueOf(value));
|
|
|
}
|
|
|
|
|
|
+ /** 取件方式归一化:缺省按 NOW 处理,非法值报错。 */
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
+ /** 校验预约窗:NOW 不得携带时间;SCHEDULED 必须是未来 3 天内恰好 30 分钟的窗。 */
|
|
|
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");
|
|
|
@@ -1193,6 +1266,7 @@ public class FlashDeliveryApplicationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 判断订单当前是否开放抢单:立即单随时可抢,预约单到点开放。 */
|
|
|
private boolean isAvailableAt(FlashDeliveryOrder order, Date now) {
|
|
|
return "NOW".equals(order.getDeliveryMode())
|
|
|
|| "SCHEDULED".equals(order.getDeliveryMode())
|
|
|
@@ -1214,6 +1288,7 @@ public class FlashDeliveryApplicationService {
|
|
|
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,
|
|
|
FlashDeliveryPriceBreakdown breakdown) { }
|
|
|
}
|