From a575907623e85434b1bcedfd7a451183c104c0e4 Mon Sep 17 00:00:00 2001 From: xm <1350250847@qq.com> Date: Tue, 5 May 2026 09:31:45 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=B8=8B=E5=8D=95=E5=A2=9E=E5=8A=A0=E5=95=86?= =?UTF-8?q?=E5=93=81=E5=88=A4=E6=96=AD=E6=98=AF=E5=90=A6=E6=B0=B4=E7=A5=A8?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E3=80=81=E9=BB=98=E8=AE=A4=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=8A=9F=E8=83=BD=202.=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=9F=E6=88=90=E6=A0=B8=E9=94=80=E7=A0=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=203.=E8=87=AA=E6=8F=90=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E4=B8=8B=E5=8D=95=E4=B8=8D=E6=A0=A1=E9=AA=8C=E7=94=B5=E5=AD=90?= =?UTF-8?q?=E5=9B=B4=E6=A0=8F=EF=BC=9B=E9=85=8D=E9=80=81=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=A6=82=E5=9C=A8=E7=94=B5=E5=AD=90=E5=9B=B4=E6=A0=8F=E5=86=85?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E8=87=AA=E9=85=8D=E9=80=81=EF=BC=8C=E5=9C=A8?= =?UTF-8?q?=E7=94=B5=E5=AD=90=E5=9B=B4=E6=A0=8F=E5=A4=96=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=8F=91=E5=BF=AB=E9=80=92=204.=E5=95=86=E5=93=81=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=EF=BC=9A=E9=85=8D=E9=80=81=E6=96=B9=E5=BC=8F=E3=80=81?= =?UTF-8?q?=E6=B0=B4=E7=A5=A8=E6=A0=87=E8=AF=86=E4=B8=9A=E5=8A=A1=EF=BC=9B?= =?UTF-8?q?=E5=95=86=E5=93=81=E8=AE=A2=E5=8D=95=E5=A2=9E=E5=8A=A0=EF=BC=9A?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=B1=BB=E5=9E=8B=E3=80=81=E6=B0=B4=E7=A5=A8?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E3=80=81=E6=A0=B8=E9=94=80=E7=A0=81=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gxwebsoft/common/system/entity/User.java | 3 + .../glt/service/GltTicketIssueService.java | 2 +- .../shop/controller/ShopOrderController.java | 10 +- .../shop/dto/OrderCreateRequest.java | 3 + .../shop/dto/VerifyShopOrderDto.java | 25 +++++ .../com/gxwebsoft/shop/entity/ShopGoods.java | 12 ++ .../com/gxwebsoft/shop/entity/ShopOrder.java | 21 ++++ .../shop/service/OrderBusinessService.java | 104 +++++++++++++++++- .../shop/service/ShopOrderService.java | 8 ++ .../shop/service/ShopStoreFenceService.java | 15 +++ .../impl/ShopStoreFenceServiceImpl.java | 45 ++++++++ 11 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/shop/dto/VerifyShopOrderDto.java diff --git a/src/main/java/com/gxwebsoft/common/system/entity/User.java b/src/main/java/com/gxwebsoft/common/system/entity/User.java index ca83722..f4dee75 100644 --- a/src/main/java/com/gxwebsoft/common/system/entity/User.java +++ b/src/main/java/com/gxwebsoft/common/system/entity/User.java @@ -262,6 +262,9 @@ public class User implements UserDetails { @Schema(description = "微信unionid") private String unionid; + @Schema(description = "核销权限是否开启 0-未开启 1-已开启") + private Integer verifyFlag; + @Schema(description = "关联用户ID") @TableField(exist = false) private Integer sysUserId; diff --git a/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java b/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java index 75c20c8..5cd7fbb 100644 --- a/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java +++ b/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java @@ -131,7 +131,7 @@ public class GltTicketIssueService { } @Async -// @Scheduled(cron = "0/1 * 4-22 * * ?") 没秒钟执行一次 +// @Scheduled(cron = "0/1 * 4-22 * * ?") 每秒钟执行一次 public void paySuccessExecute(String orderNo, Integer tenantId){ suerTicketRelease(orderNo, tenantId); } diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java index 2b241e0..32a3bc7 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.glt.service.GltTicketIssueService; +import com.gxwebsoft.shop.dto.VerifyShopOrderDto; import com.gxwebsoft.shop.entity.ShopOrderDelivery; import com.gxwebsoft.shop.entity.ShopUserAddress; import com.gxwebsoft.shop.service.*; @@ -136,14 +137,13 @@ public class ShopOrderController extends BaseController { return success(shopOrderService.getByIdRel(id)); } - @Operation(summary = "添加订单") + @Operation(summary = "添加订单【单商品添加】") @PostMapping() public ApiResult save(@RequestBody OrderCreateRequest request) { User loginUser = getLoginUser(); if (loginUser == null) { return fail("用户未登录"); } - try { Map wxOrderInfo = orderBusinessService.createOrder(request, loginUser); return success("下单成功", wxOrderInfo); @@ -961,6 +961,12 @@ public class ShopOrderController extends BaseController { return "fail"; } + @Operation(summary = "核销订单", description = "核销员扫码核销用户订单") + @PutMapping("/verifyOrder") + public ApiResult verifyOrder(@RequestBody VerifyShopOrderDto verifyDto){ + return success(shopOrderService.verifyOrder(verifyDto)); + } + @Operation(summary = "更新订单支付状态", description = "用户支付成功后主动同步订单状态") @PutMapping("/payment-status") public ApiResult updateOrderPaymentStatus(@RequestBody UpdatePaymentStatusRequest request) { diff --git a/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java b/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java index 07455ef..3cef01d 100644 --- a/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java +++ b/src/main/java/com/gxwebsoft/shop/dto/OrderCreateRequest.java @@ -27,6 +27,9 @@ public class OrderCreateRequest { @Max(value = 2, message = "订单类型值无效") private Integer type; + @Schema(description = "订单类型 1-及时自配送 2-自提 3-预约自配送 4-发快递 5-配送【系统自动识别电子围栏内转及时配送,电子围栏外发快递】") + private Integer orderType; + @Size(max = 60, message = "备注长度不能超过60个字符") @Schema(description = "订单标题") private String title; diff --git a/src/main/java/com/gxwebsoft/shop/dto/VerifyShopOrderDto.java b/src/main/java/com/gxwebsoft/shop/dto/VerifyShopOrderDto.java new file mode 100644 index 0000000..b4d7101 --- /dev/null +++ b/src/main/java/com/gxwebsoft/shop/dto/VerifyShopOrderDto.java @@ -0,0 +1,25 @@ +package com.gxwebsoft.shop.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 更新订单支付状态请求DTO + * + * @author xm + * @since 2026-05-04 + */ +@Data +@Schema(name = "VerifyShopOrderDto", description = "更新订单支付状态请求") +public class VerifyShopOrderDto { + + @Schema(description = "核销码") + @NotBlank(message = "核销码不能为空") + private String verifyCode; + + @Schema(description = "核销码类型 1-普通核销【无推广佣金】 2-推广结算【有订单佣金】") + private Integer verifyType; + +} diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java b/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java index 75bc1a3..51709cf 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java @@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonFormat; import java.io.Serializable; @@ -21,6 +23,7 @@ import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = false) @Schema(name = "ShopGoods对象", description = "商品") +@TableName("shop_goods") public class ShopGoods implements Serializable { private static final long serialVersionUID = 1L; @@ -171,6 +174,15 @@ public class ShopGoods implements Serializable { @Schema(description = "租户id") private Integer tenantId; + @Schema(description = "配送方式:1-自配送 2-自提 4-发快递 多属性用','隔开") + private String deliveryType; + + @Schema(description = "水票标识 0-否 1-是") + private Integer waterTicketFlag; + + @Schema(description = "水票ID") + private Integer waterTickerId; + @Schema(description = "创建时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java b/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java index fab56e5..0c96f1d 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopOrder.java @@ -33,6 +33,9 @@ public class ShopOrder implements Serializable { @TableId(value = "order_id", type = IdType.AUTO) private Integer orderId; + @Schema(description = "订单类型 1-及时自配送 2-自提 3-预约自配送 4-发快递") + private Integer orderType; + @Schema(description = "订单编号") private String orderNo; @@ -310,6 +313,24 @@ public class ShopOrder implements Serializable { @Schema(description = "秒杀活动ID") private Integer activityId; + @Schema(description = "水票订单标识 0-否 1-是") + private Integer waterTicketFlag; + + @Schema(description = "核销码") + private String verifyCode; + + @Schema(description = "核销状态 0-未核销 1-已核销") + private Integer verifyStatus; + + @Schema(description = "推广佣金结算核销过期时间") + private LocalDateTime verifyExpTime; + + @Schema(description = "核销时间") + private LocalDateTime verifyTime; + + @Schema(description = "核销人") + private Integer verifyUser; + @Schema(description = "修改时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime; diff --git a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java index 1e9a1b9..232bbd7 100644 --- a/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java +++ b/src/main/java/com/gxwebsoft/shop/service/OrderBusinessService.java @@ -1,15 +1,15 @@ package com.gxwebsoft.shop.service; import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.gxwebsoft.common.core.exception.BusinessException; -import com.gxwebsoft.common.core.exception.enums.GlobalErrorCodeConstants; 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.mapper.ShopFlashSaleActivityMapper; -import com.gxwebsoft.shop.service.ShopStoreFenceService; +import com.gxwebsoft.shop.mapper.ShopOrderMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -19,6 +19,7 @@ import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; +import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -37,9 +38,16 @@ public class OrderBusinessService { private static final int DEDUCT_STOCK_TYPE_ORDER = 10; // 下单减库存 private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存 + // 去除易混淆字符:0 O 1 I L 8 B Z + private static final String CHAR_POOL = "2345679ACDEFGHJKMNPQRSTUVWXY"; + private static final SecureRandom RANDOM = new SecureRandom(); + @Resource private ShopOrderService shopOrderService; + @Resource + private ShopOrderMapper shopOrderMapper; + @Resource private ShopOrderGoodsService shopOrderGoodsService; @@ -291,6 +299,10 @@ public class OrderBusinessService { } shopOrder.setExpirationTime(shopOrder.getCreateTime().plusMinutes(10)); + //设置核销码及过期时间 + shopOrder.setVerifyCode(getVerifyCode()); + shopOrder.setVerifyExpTime(shopOrder.getCreateTime().plusMinutes(10)); + // 确保租户ID正确设置(关键字段,影响微信支付证书路径) shopOrder.setTenantId(loginUser.getTenantId()); @@ -356,9 +368,76 @@ public class OrderBusinessService { processCoupon(shopOrder, loginUser); } + //订单类型、水票判断 TODO 目前没有购物车业务,每笔订单只下一个商品,后期加购物车多商品同时下单业务需调整 + Integer orderType = request.getOrderType(); + List goodsItems = request.getGoodsItems(); + if(!CollectionUtils.isEmpty(goodsItems)){ + OrderCreateRequest.OrderGoodsItem goodsItem = goodsItems.get(0); + ShopGoods shopGoods = shopGoodsService.getById(goodsItem.getGoodsId()); + if(shopGoods != null){ + Integer waterTicketFlag = shopGoods.getWaterTicketFlag(); + if(waterTicketFlag != null && waterTicketFlag == 1){ + shopOrder.setWaterTicketFlag(1); + }else { + shopOrder.setWaterTicketFlag(0); + } + + //单一配送方式商品如用户没选择配送方式按商品默认配送属性设定订单类型 + String deliveryType = shopGoods.getDeliveryType(); + if(orderType == null && StrUtil.isNotBlank(deliveryType) && !deliveryType.contains(",")){ + switch (deliveryType){ + case "1": + case "3":{ + orderType = 1; + break; + } + case "2":{ + orderType = 2; + break; + } + case "4":{ + orderType = 4; + break; + } + default:{ + orderType = 1; + } + } + } + } + } + shopOrder.setOrderType(orderType); + return shopOrder; } + /** + * 生成8位字母数字混合核销码(推荐) + */ + public static String generate6Mix() { + StringBuilder sb = new StringBuilder(6); + int len = CHAR_POOL.length(); + for (int i = 0; i < 6; i++) { + int idx = RANDOM.nextInt(len); + sb.append(CHAR_POOL.charAt(idx)); + } + return sb.toString(); + } + + /** + * 获取不重复的核销码 + * @return + */ + public String getVerifyCode(){ + String verifyCode = generate6Mix(); + LambdaQueryWrapper orderLambdaQueryWrapper = new LambdaQueryWrapper().select(ShopOrder::getOrderId).eq(ShopOrder::getVerifyCode, verifyCode); + List shopOrders = shopOrderMapper.selectList(orderLambdaQueryWrapper); + if(!CollectionUtils.isEmpty(shopOrders)){ + getVerifyCode(); + } + return verifyCode; + } + /** * 处理优惠券 */ @@ -556,6 +635,12 @@ public class OrderBusinessService { return; } + //自提订单类型不用校验电子围栏 + Integer orderType = shopOrder.getOrderType(); + if(orderType != null && orderType == 2){ + return; + } + // 若已配置围栏,则必须有 addressId 才能从地址表取坐标进行校验 if (shopOrder.getAddressId() == null) { if (shopStoreFenceService.hasEnabledFences(shopOrder.getTenantId())) { @@ -586,8 +671,17 @@ public class OrderBusinessService { throw new BusinessException("收货地址坐标异常,请重新选择收货地址"); } - // 只做校验;具体围栏列表/points 异常处理由围栏服务统一处理 - shopStoreFenceService.validatePointInEnabledFences(shopOrder.getTenantId(), lng, lat); + //订单类型:5-配送【系统自动识别电子围栏内转及时配送,电子围栏外发快递】 + if(orderType != null && orderType == 5){ + Boolean exit = shopStoreFenceService.validatePointInEnabled(shopOrder.getTenantId(), lng, lat); + if(exit){ + shopOrder.setOrderType(1); + }else { + shopOrder.setOrderType(4); + } + }else { + shopStoreFenceService.validatePointInEnabledFences(shopOrder.getTenantId(), lng, lat); + } } /** diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopOrderService.java b/src/main/java/com/gxwebsoft/shop/service/ShopOrderService.java index e2ebdea..e7df2db 100644 --- a/src/main/java/com/gxwebsoft/shop/service/ShopOrderService.java +++ b/src/main/java/com/gxwebsoft/shop/service/ShopOrderService.java @@ -3,6 +3,7 @@ package com.gxwebsoft.shop.service; import com.baomidou.mybatisplus.extension.service.IService; import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.shop.dto.UserOrderStats; +import com.gxwebsoft.shop.dto.VerifyShopOrderDto; import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.param.ShopOrderParam; @@ -86,4 +87,11 @@ public interface ShopOrderService extends IService { * @param type 订单类型(可为空) */ UserOrderStats getUserOrderStats(Integer userId, Integer tenantId, Integer type); + + /** + * 核销订单 + * @param verifyDto 核销码 + * @return + */ + Boolean verifyOrder(VerifyShopOrderDto verifyDto); } diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java b/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java index b930b30..0954bcb 100644 --- a/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java +++ b/src/main/java/com/gxwebsoft/shop/service/ShopStoreFenceService.java @@ -59,4 +59,19 @@ public interface ShopStoreFenceService extends IService { */ void validatePointInEnabledFences(Integer tenantId, double lng, double lat); + /** + * 校验坐标是否落在任一启用围栏内。 + *

+ * 约定: + * - 围栏按 tenantId + status=0 过滤; + * - 支持多个围栏:命中任意一个即通过; + * - 无围栏配置:直接放行; + * - 围栏 points 异常:抛出异常(避免误送)。 + * + * @param tenantId 租户ID + * @param lng 经度 + * @param lat 纬度 + */ + Boolean validatePointInEnabled(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 7d597cc..3c404b7 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopStoreFenceServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopStoreFenceServiceImpl.java @@ -106,4 +106,49 @@ public class ShopStoreFenceServiceImpl extends ServiceImpl fences = this.list(new LambdaQueryWrapper() + .eq(ShopStoreFence::getTenantId, tenantId) + .eq(ShopStoreFence::getStatus, 0) + .orderByAsc(ShopStoreFence::getSortNumber) + .orderByDesc(ShopStoreFence::getCreateTime)); + + // 无围栏配置:默认放行 + if (fences == null || fences.isEmpty()) { + return true; + } + + for (ShopStoreFence fence : fences) { + if (fence == null) { + continue; + } + List polygon; + try { + polygon = GeoFenceUtil.parsePolygonPoints(fence.getPoints()); + } catch (Exception e) { + // points 异常:直接拒单并记录日志,避免误送 + log.error("围栏 points 解析失败,tenantId={}, fenceId={}, points={}", + tenantId, fence.getId(), fence.getPoints(), e); + throw new BusinessException("配送范围配置异常,请联系商家"); + } + + if (polygon == null || polygon.size() < 3) { + log.error("围栏 points 点数不足,tenantId={}, fenceId={}, points={}", + tenantId, fence.getId(), fence.getPoints()); + throw new BusinessException("配送范围配置异常,请联系商家"); + } + + if (GeoFenceUtil.containsInclusive(polygon, lng, lat)) { + return true; // 命中任一围栏即通过 + } + } + return false; + } + }