diff --git a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java index b0672cd..f7e49ee 100644 --- a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java +++ b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java @@ -13,6 +13,7 @@ import com.gxwebsoft.glt.param.GltTicketOrderParam; import com.gxwebsoft.glt.service.GltTicketOrderService; import com.gxwebsoft.shop.entity.ShopStoreRider; import com.gxwebsoft.shop.entity.ShopUserAddress; +import com.gxwebsoft.shop.service.ShopStoreFenceService; import com.gxwebsoft.shop.service.ShopStoreRiderService; import com.gxwebsoft.shop.service.ShopUserAddressService; import cn.hutool.core.util.StrUtil; @@ -40,6 +41,8 @@ public class GltTicketOrderController extends BaseController { @Resource private ShopUserAddressService shopUserAddressService; @Resource + private ShopStoreFenceService shopStoreFenceService; + @Resource private ShopStoreRiderService shopStoreRiderService; @Operation(summary = "分页查询送水订单") @@ -119,6 +122,18 @@ public class GltTicketOrderController extends BaseController { gltTicketOrder.setAddressId(userAddress.getId()); gltTicketOrder.setAddress(buildAddressSnapshot(userAddress)); + // 下单时校验配送范围(电子围栏):不信任前端传经纬度,使用地址表坐标校验 + if (StrUtil.isBlank(userAddress.getLat()) || StrUtil.isBlank(userAddress.getLng())) { + return fail("收货地址坐标缺失,请重新选择收货地址"); + } + try { + double lat = Double.parseDouble(userAddress.getLat().trim()); + double lng = Double.parseDouble(userAddress.getLng().trim()); + shopStoreFenceService.validatePointInEnabledFences(loginUser.getTenantId(), lng, lat); + } catch (Exception e) { + return fail(e.getMessage() == null ? "收货地址不在配送范围内" : e.getMessage()); + } + gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId()); return success("下单成功"); } diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java index de69242..5db5277 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java +++ b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java @@ -14,6 +14,7 @@ import com.gxwebsoft.common.core.utils.WechatPayConfigValidator; import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.system.entity.Payment; import com.gxwebsoft.shop.entity.ShopOrderDelivery; +import com.gxwebsoft.shop.entity.ShopUserAddress; import com.gxwebsoft.shop.service.*; import com.gxwebsoft.shop.service.impl.KuaiDi100Impl; import com.gxwebsoft.shop.task.OrderAutoCancelTask; @@ -99,6 +100,8 @@ public class ShopOrderController extends BaseController { private ShopWechatShippingSyncService shopWechatShippingSyncService; @Resource private PaymentService paymentService; + @Resource + private ShopStoreFenceService shopStoreFenceService; @Operation(summary = "分页查询订单") @GetMapping("/page") @@ -148,6 +151,9 @@ public class ShopOrderController extends BaseController { shopOrder.setUserId(loginUser.getUserId()); shopOrder.setOpenid(loginUser.getOpenid()); shopOrder.setPayUserId(loginUser.getUserId()); + if (shopOrder.getTenantId() == null) { + shopOrder.setTenantId(loginUser.getTenantId()); + } // 下单时间 & 订单过期时间:默认下单后10分钟过期(用于发起支付等场景校验) LocalDateTime now = LocalDateTime.now(); @@ -178,6 +184,39 @@ public class ShopOrderController extends BaseController { shopOrder.setPrice(new BigDecimal("0.01")); shopOrder.setTotalPrice(new BigDecimal("0.01")); } + + // 下单时校验配送范围(电子围栏):不信任前端传经纬度,使用 addressId 反查地址表坐标 + try { + // 自提/无需物流跳过 + boolean needFenceCheck = !(shopOrder.getSelfTakeMerchantId() != null && shopOrder.getSelfTakeMerchantId() > 0) + && !(shopOrder.getDeliveryType() != null && shopOrder.getDeliveryType().equals(1)); + if (needFenceCheck && shopOrder.getAddressId() == null) { + if (shopStoreFenceService.hasEnabledFences(shopOrder.getTenantId())) { + return fail("请先选择收货地址"); + } + } + if (needFenceCheck && shopOrder.getAddressId() != null) { + ShopUserAddress address = shopUserAddressService.getById(shopOrder.getAddressId()); + if (address == null || address.getUserId() == null || !address.getUserId().equals(loginUser.getUserId())) { + return fail("收货地址不存在"); + } + if (address.getTenantId() != null && shopOrder.getTenantId() != null && !address.getTenantId().equals(shopOrder.getTenantId())) { + return fail("收货地址不存在"); + } + if (StrUtil.isBlank(address.getLat()) || StrUtil.isBlank(address.getLng())) { + return fail("收货地址坐标缺失,请重新选择收货地址"); + } + double lat = Double.parseDouble(address.getLat().trim()); + double lng = Double.parseDouble(address.getLng().trim()); + shopOrder.setAddressLat(address.getLat()); + shopOrder.setAddressLng(address.getLng()); + shopStoreFenceService.validatePointInEnabledFences(shopOrder.getTenantId(), lng, lat); + } + } catch (Exception e) { + // BusinessException message 直接透出;其他异常做兜底 + return fail(e.getMessage() == null ? "收货地址不在配送范围内" : e.getMessage()); + } + if (shopOrderService.save(shopOrder)) { return success("下单成功", shopOrderService.createWxOrder(shopOrder)); } diff --git a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java index e660b3b..337b32c 100644 --- a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java +++ b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java @@ -7,6 +7,7 @@ import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.shop.config.OrderConfigProperties; import com.gxwebsoft.shop.dto.OrderCreateRequest; import com.gxwebsoft.shop.entity.*; +import com.gxwebsoft.shop.service.ShopStoreFenceService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -51,6 +52,8 @@ public class OrderBusinessService { private ShopUserAddressService shopUserAddressService; @Resource private ShopUserCouponService shopUserCouponService; + @Resource + private ShopStoreFenceService shopStoreFenceService; /** * 创建订单 @@ -71,6 +74,9 @@ public class OrderBusinessService { // 3. 处理收货地址信息 processDeliveryAddress(shopOrder, request, loginUser); + // 3.1 下单时校验配送范围(电子围栏) + validateDeliveryFenceIfNeeded(shopOrder, loginUser); + // 4. 应用业务规则 applyBusinessRules(shopOrder, loginUser); @@ -402,13 +408,9 @@ public class OrderBusinessService { */ private void processDeliveryAddress(ShopOrder shopOrder, OrderCreateRequest request, User loginUser) { try { - // 1. 如果前端已经传入了完整的收货地址信息,直接使用 - if (isAddressInfoComplete(request)) { - log.info("使用前端传入的收货地址信息,用户ID:{}", loginUser.getUserId()); - return; - } + // 前端可能会传 addressLat/addressLng,但不可信;这里一律以地址表中的坐标为准(需 addressId)。 - // 2. 如果指定了地址ID,获取该地址信息 + // 1. 如果指定了地址ID,优先使用该地址 if (request.getAddressId() != null) { ShopUserAddress userAddress = shopUserAddressService.getById(request.getAddressId()); if (userAddress != null && userAddress.getUserId().equals(loginUser.getUserId())) { @@ -420,6 +422,13 @@ public class OrderBusinessService { request.getAddressId(), loginUser.getUserId()); } + // 2. 若前端已传完整地址快照但没传 addressId:保持快照,不在这里“猜测”绑定地址。 + // 围栏校验会根据是否配置围栏决定是否强制要求 addressId。 + if (isAddressInfoComplete(request)) { + log.warn("前端传入了收货地址快照字段,但未传 addressId,用户ID:{}", loginUser.getUserId()); + return; + } + // 3. 获取用户默认收货地址 ShopUserAddress defaultAddress = shopUserAddressService.getDefaultAddress(loginUser.getUserId()); if (defaultAddress != null) { @@ -482,12 +491,12 @@ public class OrderBusinessService { request.setRealName(userAddress.getName()); } - // 复制经纬度信息 - if (request.getAddressLat() == null && userAddress.getLat() != null) { + // 复制经纬度信息(不信任前端传入坐标,一律以地址表为准) + if (userAddress.getLat() != null && !userAddress.getLat().trim().isEmpty()) { shopOrder.setAddressLat(userAddress.getLat()); request.setAddressLat(userAddress.getLat()); } - if (request.getAddressLng() == null && userAddress.getLng() != null) { + if (userAddress.getLng() != null && !userAddress.getLng().trim().isEmpty()) { shopOrder.setAddressLng(userAddress.getLng()); request.setAddressLng(userAddress.getLng()); } @@ -496,6 +505,61 @@ public class OrderBusinessService { userAddress.getId(), userAddress.getName(), shopOrder.getAddress()); } + /** + * 下单时校验配送范围(电子围栏)。 + *
+ * 规则:
+ * - 仅对需要配送到地址的订单做校验;自提/无需物流跳过;
+ * - 不信任前端传经纬度:用 addressId 反查地址表中的 lat/lng;
+ * - 无围栏配置默认放行;围栏配置异常则拒单(避免误送)。
+ */
+ private void validateDeliveryFenceIfNeeded(ShopOrder shopOrder, User loginUser) {
+ if (shopOrder == null || loginUser == null) {
+ return;
+ }
+
+ // 自提/无需物流:不校验配送围栏
+ if (shopOrder.getSelfTakeMerchantId() != null && shopOrder.getSelfTakeMerchantId() > 0) {
+ return;
+ }
+ if (shopOrder.getDeliveryType() != null && shopOrder.getDeliveryType().equals(1)) {
+ return;
+ }
+
+ // 若已配置围栏,则必须有 addressId 才能从地址表取坐标进行校验
+ if (shopOrder.getAddressId() == null) {
+ if (shopStoreFenceService.hasEnabledFences(shopOrder.getTenantId())) {
+ throw new BusinessException("请先选择收货地址");
+ }
+ return;
+ }
+
+ ShopUserAddress address = shopUserAddressService.getById(shopOrder.getAddressId());
+ if (address == null || address.getUserId() == null || !address.getUserId().equals(loginUser.getUserId())) {
+ throw new BusinessException("收货地址不存在");
+ }
+ if (address.getTenantId() != null && shopOrder.getTenantId() != null && !address.getTenantId().equals(shopOrder.getTenantId())) {
+ throw new BusinessException("收货地址不存在");
+ }
+
+ String latStr = address.getLat();
+ String lngStr = address.getLng();
+ if (latStr == null || latStr.trim().isEmpty() || lngStr == null || lngStr.trim().isEmpty()) {
+ throw new BusinessException("收货地址坐标缺失,请重新选择收货地址");
+ }
+ double lat;
+ double lng;
+ try {
+ lat = Double.parseDouble(latStr.trim());
+ lng = Double.parseDouble(lngStr.trim());
+ } catch (Exception e) {
+ throw new BusinessException("收货地址坐标异常,请重新选择收货地址");
+ }
+
+ // 只做校验;具体围栏列表/points 异常处理由围栏服务统一处理
+ shopStoreFenceService.validatePointInEnabledFences(shopOrder.getTenantId(), lng, lat);
+ }
+
/**
* 应用业务规则
*/
diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java b/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java
index 8b1751c..b930b30 100644
--- a/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java
+++ b/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java
@@ -39,4 +39,24 @@ public interface ShopStoreFenceService extends IService
+ * 约定:
+ * - 围栏按 tenantId + status=0 过滤;
+ * - 支持多个围栏:命中任意一个即通过;
+ * - 无围栏配置:直接放行;
+ * - 围栏 points 异常:抛出异常(避免误送)。
+ *
+ * @param tenantId 租户ID
+ * @param lng 经度
+ * @param lat 纬度
+ */
+ void validatePointInEnabledFences(Integer tenantId, double lng, double lat);
+
}
diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopStoreFenceServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopStoreFenceServiceImpl.java
index 77b001c..7d597cc 100644
--- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopStoreFenceServiceImpl.java
+++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopStoreFenceServiceImpl.java
@@ -1,12 +1,17 @@
package com.gxwebsoft.shop.service.impl;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.shop.mapper.ShopStoreFenceMapper;
import com.gxwebsoft.shop.service.ShopStoreFenceService;
import com.gxwebsoft.shop.entity.ShopStoreFence;
import com.gxwebsoft.shop.param.ShopStoreFenceParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
+import com.gxwebsoft.shop.util.GeoFenceUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -19,6 +24,7 @@ import java.util.List;
*/
@Service
public class ShopStoreFenceServiceImpl extends ServiceImpl
+ * 支持格式示例:
+ * - [[lng,lat],[lng,lat],...]
+ * - [{"lng":..,"lat":..}, ...] / [{"longitude":..,"latitude":..}, ...]
+ * - "lng,lat;lng,lat;..."
+ * - "lng,lat|lng,lat|..."
+ * - "lng,lat,lng,lat,lng,lat,..."(纯逗号序列,偶数个数字)
+ */
+ public static List