Forráskód Böngészése

商户端订单搜索列表代码调整

wangzhijun 1 hete
szülő
commit
511ef28163

+ 21 - 0
nightFragrance-massage/src/main/java/com/ylx/order/constant/OrderConstant.java

@@ -0,0 +1,21 @@
+package com.ylx.order.constant;
+
+public class OrderConstant {
+    /** 删除状态 0-未删 1-已删 */
+    public static final Integer NOT_DELETE = 0;
+    public static final Integer DELETED = 1;
+    /** 预约缓冲分钟 */
+    public static final int APPOINT_BUFFER_MINUTES = 30;
+    /** 复购天数阈值 */
+    public static final int REPURCHASE_DAY_LIMIT = 14;
+    /** 字典类型 */
+    public static final String DICT_UNIT_TYPE = "unit_type";
+    /** 用户类型 2-商户 */
+    public static final Integer USER_TYPE_MERCHANT = 2;
+    /** 商户地址类型 1门店 2虚拟 */
+    public static final Integer ADDR_TYPE_SHOP = 1;
+    public static final Integer ADDR_TYPE_VIRTUAL = 2;
+
+    /** 商户地址类型 1默认 */
+    public static final Integer ADDR_DEFAULT_YES = 1;
+}

+ 17 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/merchant/OrderPageVO.java

@@ -1,5 +1,6 @@
 package com.ylx.order.domain.vo.merchant;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.ylx.order.service.IAfterSaleDisplay;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -15,6 +16,9 @@ public class OrderPageVO<T> implements IAfterSaleDisplay {
     @ApiModelProperty("订单ID")
     private Long id;
 
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
     @ApiModelProperty("售后单ID")
     private Long afterSalesServiceId;
 
@@ -71,4 +75,17 @@ public class OrderPageVO<T> implements IAfterSaleDisplay {
 
     @ApiModelProperty("订单状态")
     private Integer status;
+
+    @ApiModelProperty("客户标签编码 0新客/1复购/2老客 ")
+    private Integer customerTagCode;
+
+    @ApiModelProperty("客户标签展示文案")
+    private String customerTagDesc;
+
+    @ApiModelProperty("服务类目ID")
+    private Long categoryId;
+
+    @ApiModelProperty("下单时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
 }

+ 43 - 0
nightFragrance-massage/src/main/java/com/ylx/order/enums/CustomerTagEnum.java

@@ -0,0 +1,43 @@
+package com.ylx.order.enums;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.Getter;
+
+@Getter
+public enum CustomerTagEnum {
+    NEW_CUSTOMER(0, "新客收单"),
+    REPURCHASE(1, "再次复购"),
+    OLD_CUSTOMER(2, "老客户");
+
+    private final Integer code;
+    private final String desc;
+
+    CustomerTagEnum(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    /**
+     * 根据code获取枚举
+     */
+    public static CustomerTagEnum getByCode(Integer code) {
+        if (ObjectUtil.isNull(code)) {
+            return null;
+        }
+        for (CustomerTagEnum tag : values()) {
+            if (tag.getCode().equals(code)) {
+                return tag;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 根据code获取展示文案
+     */
+    public static String getDescByCode(Integer code) {
+        CustomerTagEnum tagEnum = getByCode(code);
+        return ObjectUtil.isNotNull(tagEnum) ? tagEnum.getDesc() : null;
+    }
+
+}

+ 5 - 0
nightFragrance-massage/src/main/java/com/ylx/order/mapper/TOrderMapper.java

@@ -50,4 +50,9 @@ public interface TOrderMapper extends BaseMapper<TOrder> {
     );
 
     Page<OrderPageVO> queryMerchantOrderList(Page<OrderPageVO> page, @Param("dto") OrderDateQueryDTO dto);
+
+    LocalDateTime getLastOrderDateBeforeCurrent(@Param("userId") Long userId,
+                                                @Param("merchantId") Long merchantId,
+                                                @Param("categoryId") Long categoryId,
+                                                @Param("currentTime") LocalDateTime currentTime);
 }

+ 181 - 213
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java

@@ -37,6 +37,7 @@ import com.ylx.order.domain.vo.merchant.MerchantCancelOrderDTO;
 import com.ylx.order.domain.vo.merchant.OrderCustomerPhoneVO;
 import com.ylx.order.domain.vo.merchant.OrderPageVO;
 import com.ylx.order.enums.AfterSaleServiceStatusEnum;
+import com.ylx.order.enums.CustomerTagEnum;
 import com.ylx.order.enums.OrderStatusEnum;
 import com.ylx.order.enums.PaymentMethodEnum;
 import com.ylx.order.mapper.TOrderMapper;
@@ -59,12 +60,16 @@ import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.stream.Collectors;
 
+import static com.ylx.order.constant.OrderConstant.*;
+
 
 /**
  * 订单表 服务实现类
@@ -109,12 +114,9 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     private MaProjectMapper maProjectMapper;
     @Resource
     private IAfterSalesServiceService afterSalesServiceService;
-    private static final String DICT_TYPE = "unit_type";
-    private static final int BUFFER_MINUTES = 30; // 30分钟缓冲期
     @Resource
     private IMessageService messageService;
 
-    private static final int NOT_DELETE = 0;
     @Resource
     private TGeoFenceService geoFenceService;
 
@@ -283,10 +285,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 5. 获取商户地址信息
-        List<TAddress> merchantAddressList = this.addressService.list(new LambdaQueryWrapper<TAddress>()
-                .eq(TAddress::getMerchantId, dto.getMerchantId())
-                .eq(TAddress::getUserType, 2)
-                .eq(TAddress::getIsDelete, 0));
+        List<TAddress> merchantAddressList = this.addressService.list(new LambdaQueryWrapper<TAddress>().eq(TAddress::getMerchantId, dto.getMerchantId()).eq(TAddress::getUserType, 2).eq(TAddress::getIsDelete, 0));
         if (CollUtil.isEmpty(merchantAddressList)) {
             throw new ServiceException("商户地址不存在");
         }
@@ -304,12 +303,10 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 实付金额 = 商品原价 - 优惠券优惠 + 车费
-        BigDecimal finalAmount = basePrice.subtract(couponDiscount).add(trafficFee)
-                .setScale(2, RoundingMode.HALF_UP);
+        BigDecimal finalAmount = basePrice.subtract(couponDiscount).add(trafficFee).setScale(2, RoundingMode.HALF_UP);
 
         // 7. 组装订单对象
-        TOrder order = buildOrder(dto, project, maTechnician, address, merchantAddressList,
-                basePrice, trafficFee, couponDiscount, finalAmount, userId);
+        TOrder order = buildOrder(dto, project, maTechnician, address, merchantAddressList, basePrice, trafficFee, couponDiscount, finalAmount, userId);
 
         // 8. 计算商户、平台分佣金额
         this.calculateOrderIncome(order, finalAmount, project.getMerchantShareRatio());
@@ -376,9 +373,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         Page<TOrder> orderPage = baseMapper.selectPage(page, wrapper);
 
         // 4. 转换 VO
-        List<OrderDateQueryVo> voList = orderPage.getRecords().stream()
-                .map(this::convertToVo)
-                .collect(Collectors.toList());
+        List<OrderDateQueryVo> voList = orderPage.getRecords().stream().map(this::convertToVo).collect(Collectors.toList());
 
 
         // 5. 返回分页结果
@@ -408,7 +403,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 4. 执行逻辑删除
-        order.setIsDelete(1);
+        order.setIsDelete(DELETED);
         order.setDeletedTime(LocalDateTime.now());
         boolean updated = updateById(order);
         if (!updated) {
@@ -421,10 +416,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     @Transactional(rollbackFor = Exception.class)
     public void processOrderPayment(WxPayOrderNotifyV3Result.DecryptNotifyResult result, TWxUser wxUser, TOrder order) {
         // 更新订单状态
-        this.lambdaUpdate()
-                .set(TOrder::getStatus, OrderStatusEnum.PENDING_DISPATCH.getCode())
-                .eq(TOrder::getId, order.getId())
-                .update();
+        this.lambdaUpdate().set(TOrder::getStatus, OrderStatusEnum.PENDING_DISPATCH.getCode()).eq(TOrder::getId, order.getId()).update();
 
         // 插入订单流转记录
         OrderUpdateStatusDTO dto = new OrderUpdateStatusDTO();
@@ -521,10 +513,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     /**
      * 组装订单对象
      */
-    private TOrder buildOrder(OrderSubmitDTO dto, Project project, MaTechnician maTechnician,
-                              TAddress address, List<TAddress> merchantAddressList,
-                              BigDecimal basePrice, BigDecimal trafficFee, BigDecimal couponDiscount,
-                              BigDecimal finalAmount, Long userId) {
+    private TOrder buildOrder(OrderSubmitDTO dto, Project project, MaTechnician maTechnician, TAddress address, List<TAddress> merchantAddressList, BigDecimal basePrice, BigDecimal trafficFee, BigDecimal couponDiscount, BigDecimal finalAmount, Long userId) {
         TOrder order = new TOrder();
         order.setOrderNo(orderNumberGenerator.generateNextOrderNumber(""));
         order.setUserId(userId);
@@ -584,12 +573,9 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
      */
     private void handleBalancePayment(Long userId, BigDecimal finalAmount, TOrder order) {
         // 1. 数据库层面原子扣减余额,并校验余额是否充足
-        boolean deductSuccess = this.wxUserService.lambdaUpdate()
-                .setSql("d_balance = d_balance - " + finalAmount)
-                .eq(TWxUser::getId, userId)
+        boolean deductSuccess = this.wxUserService.lambdaUpdate().setSql("d_balance = d_balance - " + finalAmount).eq(TWxUser::getId, userId)
                 // 防止余额超扣的核心:当前余额必须大于等于扣减金额
-                .apply("d_balance >= {0}", finalAmount)
-                .update();
+                .apply("d_balance >= {0}", finalAmount).update();
 
         if (!deductSuccess) {
             throw new ServiceException("余额不足,扣款失败");
@@ -619,13 +605,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
      */
     private Map<String, Object> createWxPayOrder(TOrder order, WxLoginUser wxLoginUser) {
         try {
-            return wxPayV3Service.createV3JsapiOrder(
-                    order.getOrderNo(),
-                    order.getFinalAmount(),
-                    "购买情绪价值商品",
-                    wxLoginUser.getCOpenid(),
-                    WxPayTypeEnum.EMOTION_GOODS.getCode()
-            );
+            return wxPayV3Service.createV3JsapiOrder(order.getOrderNo(), order.getFinalAmount(), "购买情绪价值商品", wxLoginUser.getCOpenid(), WxPayTypeEnum.EMOTION_GOODS.getCode());
         } catch (Exception e) {
             log.error("微信支付下单失败,订单号: {}", order.getOrderNo(), e);
             throw new ServiceException("支付服务异常,请稍后重试");
@@ -643,14 +623,12 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
         // 1. 根据ID查询订单主表
         TOrder order = this.baseMapper.selectById(orderId);
-        if (order == null || order.getIsDelete() == 1) {
+        if (order == null || order.getIsDelete() == DELETED) {
             throw new ServiceException("订单不存在,订单ID:" + orderId);
         }
 
         // 2. 查询订单状态流转记录
-        LambdaQueryWrapper<OrderStatusFlow> flowWrapper = Wrappers.<OrderStatusFlow>lambdaQuery()
-                .eq(OrderStatusFlow::getOrderId, order.getId())
-                .orderByAsc(OrderStatusFlow::getCreateTime);
+        LambdaQueryWrapper<OrderStatusFlow> flowWrapper = Wrappers.<OrderStatusFlow>lambdaQuery().eq(OrderStatusFlow::getOrderId, order.getId()).orderByAsc(OrderStatusFlow::getCreateTime);
         List<OrderStatusFlow> flowList = orderStatusFlowService.getBaseMapper().selectList(flowWrapper);
 
         // 3. 组装VO
@@ -670,10 +648,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         // 1. 获取服务次数(从项目表获取 projectUsersNum)
         Integer serviceNum = null;
         if (order.getProjectId() != null && order.getMerchantId() != null) {
-            MaProject project = maProjectMapper.selectByProjectIdAndMerchantId(
-                    order.getProjectId(),
-                    order.getMerchantId()
-            );
+            MaProject project = maProjectMapper.selectByProjectIdAndMerchantId(order.getProjectId(), order.getMerchantId());
             if (project != null && project.getProjectUsersNum() != null) {
                 serviceNum = project.getProjectUsersNum().intValue();
             }
@@ -742,10 +717,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         this.fillCurrentAfterSaleInfo(vo, order.getId());
 
         // 订单状态流转列表转换
-        List<OrderStatusFlowVO> flowVOList = flowList.stream()
-                .map(this::convertToFlowVO)
-                .sorted(Comparator.comparing(OrderStatusFlowVO::getCreateTime))
-                .collect(Collectors.toList());
+        List<OrderStatusFlowVO> flowVOList = flowList.stream().map(this::convertToFlowVO).sorted(Comparator.comparing(OrderStatusFlowVO::getCreateTime)).collect(Collectors.toList());
         vo.setOrderStatusFlowList(flowVOList);
 
         return vo;
@@ -862,18 +834,13 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         // 2. 组装目标时间段
         LocalDateTime newStart = LocalDateTime.of(dto.getAppointmentDate(), dto.getStartTime());
         LocalDateTime actualEnd = newStart.plusMinutes(duration); // 实际服务结束时间
-        LocalDateTime newEndWithBuffer = actualEnd.plusMinutes(BUFFER_MINUTES); // 含30分钟缓冲的结束时间
+        LocalDateTime newEndWithBuffer = actualEnd.plusMinutes(APPOINT_BUFFER_MINUTES); // 含30分钟缓冲的结束时间
 
         // 3. 查询该商户当天的【有效占用】订单
         LocalDateTime dayStart = dto.getAppointmentDate().atStartOfDay();
         LocalDateTime dayEnd = dto.getAppointmentDate().atTime(LocalTime.MAX);
 
-        List<TOrder> validOrders = this.baseMapper.selectValidOrdersByMerchantAndDate(
-                dto.getMerchantId(),
-                dto.getProjectId(),
-                dayStart,
-                dayEnd
-        );
+        List<TOrder> validOrders = this.baseMapper.selectValidOrdersByMerchantAndDate(dto.getMerchantId(), dto.getProjectId(), dayStart, dayEnd);
 
         // 4. 内存中遍历判断重叠
         for (TOrder order : validOrders) {
@@ -898,99 +865,36 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
         // 2.获取当前登录用户
         WxLoginUser wxLoginUser = getCurrentWxLoginUser();
-        Long userId = Long.parseLong(wxLoginUser.getId());
-        dto.setMerchantId(userId);
-
-        // 3. 获取当前用户的默认地址
-        List<TAddress> merchantAddressList = this.addressService.list(new LambdaQueryWrapper<TAddress>()
-                .eq(TAddress::getMerchantId, userId)
-                .eq(TAddress::getUserType, 2)
-                .eq(TAddress::getIsDefault, 1)
-                .eq(TAddress::getIsDelete, NOT_DELETE));
-        if (CollUtil.isEmpty(merchantAddressList)) {
-            throw new ServiceException("商户地址不存在,请先完善商户地址");
-        }
-
-        TAddress merchantAddress = CollUtil.getFirst(merchantAddressList);
-        BigDecimal merchantLat = merchantAddress.getLatitude();
-        BigDecimal merchantLon = merchantAddress.getLongitude();
-
-        // 商户坐标为空拦截
-        if (ObjectUtil.hasNull(merchantLat, merchantLon)) {
-            log.warn("商户id:{} 默认地址经纬度为空,无法计算围栏距离", userId);
-            dto.setLatitude(null);
-            dto.setLongitude(null);
-        } else {
-            dto.setLatitude(merchantLat);
-            dto.setLongitude(merchantLon);
-        }
-
-        page = this.baseMapper.queryMerchantOrderList(page, dto);
-        List<OrderPageVO> records = page.getRecords();
-        if (CollUtil.isEmpty(records)) {
+        Long merchantId = Long.parseLong(wxLoginUser.getId());
+        dto.setMerchantId(merchantId);
+
+        // 3. 查询商户默认地址
+        TAddress merchantAddress = getMerchantDefaultAddress(merchantId);
+        // 填充商户坐标到查询DTO,用于SQL距离筛选(如有)
+        fillMerchantLonLatToDto(dto, merchantAddress);
+
+        // 4. Mapper分页查询订单基础数据
+        page = baseMapper.queryMerchantOrderList(page, dto);
+        List<OrderPageVO> voRecords = page.getRecords();
+        // 无订单直接返回,跳过围栏、标签计算逻辑
+        if (CollUtil.isEmpty(voRecords)) {
             return page;
         }
 
-        // 4. 获取所有有效的地理围栏
+        // 5. 查询当前城市有效地理围栏,提前过滤非法围栏
         List<TGeoFence> validFenceList = geoFenceService.selectValidFences(dto.getCityCode());
-        if (CollUtil.isEmpty(validFenceList)) {
-            log.info("城市编码{}无有效地理围栏,全部订单非高风险", dto.getCityCode());
-            for (OrderPageVO vo : records) {
-                vo.setIsHighRiskArea(false);
-            }
-            return page;
-        }
-
-        // 5. 遍历订单,在内存中进行空间计算
-        for (OrderPageVO orderVO : records) {
-            BigDecimal userLat = orderVO.getUserLatitude();
-            BigDecimal userLon = orderVO.getUserLongitude();
-            // 用户坐标为空,跳过计算
-            if (ObjectUtil.hasNull(userLat, userLon)) {
-                orderVO.setIsHighRiskArea(false);
-                continue;
-            }
+        List<TGeoFence> usableFence = filterUsableFence(validFenceList);
 
-            // 存储最小距离、对应围栏、是否命中风险圈
-            BigDecimal minDistanceKm = null;
-            TGeoFence nearestFence = null;
-
-            for (TGeoFence fence : validFenceList) {
-                BigDecimal fenceLat = fence.getLatitude();
-                BigDecimal fenceLon = fence.getLongitude();
-                BigDecimal radiusKm = fence.getRadiusKm();
-                // 围栏坐标/半径非法则跳过当前围栏
-                if (ObjectUtil.hasNull(fenceLat, fenceLon, radiusKm)
-                        || radiusKm.compareTo(BigDecimal.ZERO) <= 0) {
-                    continue;
-                }
-
-                // 【修复】传参顺序:用户纬度、用户经度,围栏纬度、围栏经度
-                BigDecimal distKm = DistanceUtil.formatDistanceInKilometers(userLat, userLon, fenceLat, fenceLon);
-                // 距离非法跳过
-                if (ObjectUtil.isNull(distKm)) {
-                    continue;
-                }
-
-                // 更新全局最小距离围栏
-                if (ObjectUtil.isNull(minDistanceKm) || distKm.compareTo(minDistanceKm) < 0) {
-                    minDistanceKm = distKm;
-                    nearestFence = fence;
-                }
-            }
 
-            // 赋值VO
-            if (ObjectUtil.isNull(minDistanceKm) || ObjectUtil.isNull(nearestFence)) {
-                orderVO.setIsHighRiskArea(false);
-                orderVO.setDistanceKm(null);
-                orderVO.setFenceIntro(null);
-            } else {
-                orderVO.setDistanceKm(minDistanceKm);
-                // 判断是否在围栏半径内
-                int compare = minDistanceKm.compareTo(nearestFence.getRadiusKm());
-                orderVO.setIsHighRiskArea(compare <= 0);
-                orderVO.setFenceIntro(nearestFence.getFenceIntro());
-            }
+        // 6. 批量遍历订单,计算风险区域+客户标签
+        LocalDateTime now = DateUtils.getNowLocalDateTime();
+        for (OrderPageVO vo : voRecords) {
+            // 计算高风险围栏信息
+            calcOrderGeoFenceInfo(vo, usableFence);
+            // 计算客户标签
+            Integer tagCode = calcCustomerTagCode(vo, merchantId, now);
+            vo.setCustomerTagCode(tagCode);
+            vo.setCustomerTagDesc(CustomerTagEnum.getDescByCode(tagCode));
         }
 
         return page;
@@ -1059,11 +963,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         Long orderId = dto.getOrderId();
 
         // 2. 查询订单:只查归属商户、未删除、手机号字段
-        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class)
-                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getContactPhoneNumber)
-                .eq(TOrder::getId, orderId)
-                .eq(TOrder::getMerchantId, merchantId)
-                .eq(TOrder::getIsDelete, 0));
+        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class).select(TOrder::getId, TOrder::getMerchantId, TOrder::getContactPhoneNumber).eq(TOrder::getId, orderId).eq(TOrder::getMerchantId, merchantId).eq(TOrder::getIsDelete, NOT_DELETE));
 
         // 3. 校验订单
         if (ObjectUtil.isNull(order)) {
@@ -1090,12 +990,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         String cancelReason = dto.getCancelReason();
 
         // 2. 查询订单基础信息
-        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class)
-                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus,
-                        TOrder::getUserId, TOrder::getFinalAmount, TOrder::getProjectId)
-                .eq(TOrder::getId, orderId)
-                .eq(TOrder::getMerchantId, merchantId)
-                .eq(TOrder::getIsDelete, NOT_DELETE));
+        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class).select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus, TOrder::getUserId, TOrder::getFinalAmount, TOrder::getProjectId).eq(TOrder::getId, orderId).eq(TOrder::getMerchantId, merchantId).eq(TOrder::getIsDelete, NOT_DELETE));
 
         // 订单不存在/不属于当前商户
         if (ObjectUtil.isNull(order)) {
@@ -1105,9 +1000,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         // 3. 状态校验:仅【商户已接单】允许取消转售后
         Integer statusCode = order.getStatus();
         OrderStatusEnum orderStatus = OrderStatusEnum.fromCode(statusCode);
-        if (orderStatus == null
-                || (!OrderStatusEnum.PENDING_SERVICE.getCode().equals(statusCode)
-                && !OrderStatusEnum.IN_SERVICE.getCode().equals(statusCode))) {
+        if (orderStatus == null || (!OrderStatusEnum.PENDING_SERVICE.getCode().equals(statusCode) && !OrderStatusEnum.IN_SERVICE.getCode().equals(statusCode))) {
             String currentText = OrderStatusEnum.getInfoByCode(statusCode);
             throw new ServiceException("仅【待服务、服务中】订单可操作,当前订单状态:" + currentText);
         }
@@ -1115,14 +1008,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         // 4. 更新订单为售后状态,并发乐观锁校验
         if (OrderStatusEnum.PENDING_SERVICE.getCode().equals(statusCode)) {
             // ========== 分支1:待服务 → 退回待派单,无售后、无退款 ==========
-            LambdaUpdateWrapper<TOrder> updateWrapper = Wrappers.lambdaUpdate(TOrder.class)
-                    .set(TOrder::getStatus, OrderStatusEnum.PENDING_DISPATCH.getCode())
-                    .set(TOrder::getCancelledTime,LocalDateTime.now())
-                    .set(TOrder::getCancelledReason, cancelReason)
-                    .set(TOrder::getUpdateTime, LocalDateTime.now())
-                    .set(TOrder::getUpdateBy,loginUser.getCNickName())
-                    .eq(TOrder::getId, orderId)
-                    .eq(TOrder::getStatus, statusCode);
+            LambdaUpdateWrapper<TOrder> updateWrapper = Wrappers.lambdaUpdate(TOrder.class).set(TOrder::getStatus, OrderStatusEnum.PENDING_DISPATCH.getCode()).set(TOrder::getCancelledTime, LocalDateTime.now()).set(TOrder::getCancelledReason, cancelReason).set(TOrder::getUpdateTime, LocalDateTime.now()).set(TOrder::getUpdateBy, loginUser.getCNickName()).eq(TOrder::getId, orderId).eq(TOrder::getStatus, statusCode);
             int row = baseMapper.update(null, updateWrapper);
             if (row <= 0) {
                 throw new ServiceException("订单状态已变更,请刷新重试");
@@ -1130,13 +1016,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         } else if (OrderStatusEnum.IN_SERVICE.getCode().equals(statusCode)) {
             // ========== 分支2:服务中 → 转售后、生成售后单、发起退款 ==========
             // 1. 更新订单信息
-            LambdaUpdateWrapper<TOrder> updateWrapper = Wrappers.lambdaUpdate(TOrder.class)
-                    .set(TOrder::getCancelledTime,LocalDateTime.now())
-                    .set(TOrder::getCancelledReason, cancelReason)
-                    .set(TOrder::getUpdateTime, LocalDateTime.now())
-                    .set(TOrder::getUpdateBy,loginUser.getCNickName())
-                    .eq(TOrder::getId, orderId)
-                    .eq(TOrder::getStatus, statusCode);
+            LambdaUpdateWrapper<TOrder> updateWrapper = Wrappers.lambdaUpdate(TOrder.class).set(TOrder::getCancelledTime, LocalDateTime.now()).set(TOrder::getCancelledReason, cancelReason).set(TOrder::getUpdateTime, LocalDateTime.now()).set(TOrder::getUpdateBy, loginUser.getCNickName()).eq(TOrder::getId, orderId).eq(TOrder::getStatus, statusCode);
             int row = baseMapper.update(null, updateWrapper);
             if (row <= 0) {
                 throw new ServiceException("订单状态已变更,请刷新重试");
@@ -1160,12 +1040,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         Long merchantId = Long.parseLong(loginUser.getId());
         Long orderId = dto.getOrderId();
 
-        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class)
-                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus,
-                        TOrder::getArrivedTime)
-                .eq(TOrder::getId, orderId)
-                .eq(TOrder::getMerchantId, merchantId)
-                .eq(TOrder::getIsDelete, NOT_DELETE));
+        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class).select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus, TOrder::getArrivedTime).eq(TOrder::getId, orderId).eq(TOrder::getMerchantId, merchantId).eq(TOrder::getIsDelete, NOT_DELETE));
         if (ObjectUtil.isNull(order)) {
             throw new ServiceException("订单不存在或不属于当前商户");
         }
@@ -1181,13 +1056,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 更新状态+服务开始时间
-        LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class)
-                .set(TOrder::getStatus, OrderStatusEnum.IN_SERVICE.getCode())
-                .set(TOrder::getStartTime, LocalDateTime.now())
-                .set(TOrder::getUpdateTime, LocalDateTime.now())
-                .eq(TOrder::getId, orderId)
-                .set(TOrder::getUpdateBy,loginUser.getCNickName())
-                .eq(TOrder::getStatus, status);
+        LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class).set(TOrder::getStatus, OrderStatusEnum.IN_SERVICE.getCode()).set(TOrder::getStartTime, LocalDateTime.now()).set(TOrder::getUpdateTime, LocalDateTime.now()).eq(TOrder::getId, orderId).set(TOrder::getUpdateBy, loginUser.getCNickName()).eq(TOrder::getStatus, status);
         int count = baseMapper.update(null, update);
         if (count <= 0) {
             throw new ServiceException("订单状态已变更,请刷新重试");
@@ -1203,12 +1072,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         Long merchantId = Long.parseLong(loginUser.getId());
         Long orderId = dto.getOrderId();
 
-        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class)
-                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus,
-                        TOrder::getStartTime)
-                .eq(TOrder::getId, orderId)
-                .eq(TOrder::getMerchantId, merchantId)
-                .eq(TOrder::getIsDelete, NOT_DELETE));
+        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class).select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus, TOrder::getStartTime).eq(TOrder::getId, orderId).eq(TOrder::getMerchantId, merchantId).eq(TOrder::getIsDelete, NOT_DELETE));
         if (ObjectUtil.isNull(order)) {
             throw new ServiceException("订单不存在或不属于当前商户");
         }
@@ -1224,13 +1088,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 更新状态、结束时间
-        LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class)
-                .set(TOrder::getStatus, OrderStatusEnum.COMPLETED.getCode())
-                .set(TOrder::getCompletedTime, LocalDateTime.now())
-                .set(TOrder::getUpdateTime, LocalDateTime.now())
-                .eq(TOrder::getId, orderId)
-                .set(TOrder::getUpdateBy,loginUser.getCNickName())
-                .eq(TOrder::getStatus, status);
+        LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class).set(TOrder::getStatus, OrderStatusEnum.COMPLETED.getCode()).set(TOrder::getCompletedTime, LocalDateTime.now()).set(TOrder::getUpdateTime, LocalDateTime.now()).eq(TOrder::getId, orderId).set(TOrder::getUpdateBy, loginUser.getCNickName()).eq(TOrder::getStatus, status);
         int count = baseMapper.update(null, update);
         if (count <= 0) {
             throw new ServiceException("订单状态已变更,请刷新重试");
@@ -1258,11 +1116,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 查询订单
-        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class)
-                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus)
-                .eq(TOrder::getId, orderId)
-                .eq(TOrder::getMerchantId, merchantId)
-                .eq(TOrder::getIsDelete, NOT_DELETE));
+        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class).select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus).eq(TOrder::getId, orderId).eq(TOrder::getMerchantId, merchantId).eq(TOrder::getIsDelete, NOT_DELETE));
         if (ObjectUtil.isNull(order)) {
             throw new ServiceException("订单不存在或不属于当前商户");
         }
@@ -1274,12 +1128,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 更新到达信息
-        LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class)
-                .set(TOrder::getArrivedTime, LocalDateTime.now())
-                .set(TOrder::getUpdateTime, LocalDateTime.now())
-                .set(TOrder::getUpdateBy,loginUser.getCNickName())
-                .eq(TOrder::getId, orderId)
-                .eq(TOrder::getStatus, status);
+        LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class).set(TOrder::getArrivedTime, LocalDateTime.now()).set(TOrder::getUpdateTime, LocalDateTime.now()).set(TOrder::getUpdateBy, loginUser.getCNickName()).eq(TOrder::getId, orderId).eq(TOrder::getStatus, status);
         int count = baseMapper.update(null, update);
         if (count <= 0) {
             throw new ServiceException("订单状态已变更,请刷新重试");
@@ -1288,9 +1137,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
     private void fillCurrentAfterSaleInfo(IAfterSaleDisplay vo, Long orderId) {
         LambdaQueryWrapper<AfterSalesService> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AfterSalesService::getOrderId, orderId)
-                .orderByDesc(AfterSalesService::getCreateTime)
-                .last("LIMIT 1");
+        wrapper.eq(AfterSalesService::getOrderId, orderId).orderByDesc(AfterSalesService::getCreateTime).last("LIMIT 1");
 
         AfterSalesService afterSalesService = this.afterSalesServiceService.getOne(wrapper);
         if (ObjectUtil.isNull(afterSalesService)) {
@@ -1322,7 +1169,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
         // 2. 从字典缓存中获取数据(若依的 DictUtils 读取成本极低,无需自己做 DCL 锁)
-        List<SysDictData> dictList = DictUtils.getSortedDictCache(DICT_TYPE);
+        List<SysDictData> dictList = DictUtils.getSortedDictCache(DICT_UNIT_TYPE);
         if (CollUtil.isEmpty(dictList)) {
             return duration;
         }
@@ -1365,8 +1212,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         BigDecimal hundred = new BigDecimal("100");
 
         // 1. 计算商户收益 = 实付金额 * 商户比例 / 100,保留两位小数,四舍五入
-        BigDecimal merchantIncome = finalAmount.multiply(ratio)
-                .divide(hundred, 2, RoundingMode.HALF_UP);
+        BigDecimal merchantIncome = finalAmount.multiply(ratio).divide(hundred, 2, RoundingMode.HALF_UP);
 
         // 2. 计算平台收益 = 实付金额 - 商户收益
         BigDecimal platformIncome = finalAmount.subtract(merchantIncome);
@@ -1428,4 +1274,126 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
     }
 
+    /**
+     * 计算用户客户标签编码
+     *
+     * @param vo         当前订单VO
+     * @param merchantId 当前商户ID
+     * @return CustomerTagEnum.code
+     */
+    private Integer calcCustomerTagCode(OrderPageVO vo, Long merchantId, LocalDateTime currentTime) {
+        Long userId = vo.getUserId();
+        Long categoryId = vo.getCategoryId();
+        // 关键参数缺失,返回null
+        if (ObjectUtil.hasNull(userId, categoryId, currentTime)) {
+            return null;
+        }
+        // 查询该用户、商户、分类下,当前订单之前最近一笔下单时间
+        LocalDateTime lastOrderTime = this.baseMapper.getLastOrderDateBeforeCurrent(userId, merchantId, categoryId, currentTime);
+        // 1. 无历史订单 = 新客收单 code=0
+        if (ObjectUtil.isNull(lastOrderTime)) {
+            return CustomerTagEnum.NEW_CUSTOMER.getCode();
+        }
+        // 计算自然日间隔
+        LocalDate currentDate = currentTime.toLocalDate();
+        LocalDate lastDate = lastOrderTime.toLocalDate();
+        long dayDiff = ChronoUnit.DAYS.between(lastDate, currentDate);
+
+        // 2. 间隔≤14天:再次复购 code=1
+        if (dayDiff <= REPURCHASE_DAY_LIMIT) {
+            return CustomerTagEnum.REPURCHASE.getCode();
+        }
+        // 3. 间隔>15天:老客户 code=2
+        return CustomerTagEnum.OLD_CUSTOMER.getCode();
+    }
+
+
+    /**
+     * 获取商户默认地址,无地址直接抛业务异常
+     */
+    private TAddress getMerchantDefaultAddress(Long merchantId) {
+        LambdaQueryWrapper<TAddress> addrWrapper = Wrappers.lambdaQuery(TAddress.class)
+                .eq(TAddress::getMerchantId, merchantId)
+                .eq(TAddress::getUserType, USER_TYPE_MERCHANT)
+                .eq(TAddress::getIsDefault, ADDR_DEFAULT_YES)
+                .eq(TAddress::getIsDelete, NOT_DELETE);
+        List<TAddress> merchantAddressList = addressService.list(addrWrapper);
+        if (CollUtil.isEmpty(merchantAddressList)) {
+            throw new ServiceException("商户地址不存在,请先完善商户地址");
+        }
+        return CollUtil.getFirst(merchantAddressList);
+    }
+
+    /**
+     * 将商户经纬度填充至查询DTO,坐标为空置null
+     */
+    private void fillMerchantLonLatToDto(OrderDateQueryDTO dto, TAddress merchantAddress) {
+        BigDecimal lat = merchantAddress.getLatitude();
+        BigDecimal lon = merchantAddress.getLongitude();
+        if (ObjectUtil.hasNull(lat, lon)) {
+            log.warn("商户id:{} 默认地址经纬度为空,无法计算围栏距离", dto.getMerchantId());
+            dto.setLatitude(null);
+            dto.setLongitude(null);
+            return;
+        }
+        dto.setLatitude(lat);
+        dto.setLongitude(lon);
+    }
+
+    /**
+     * 过滤可用围栏:剔除坐标/半径为空、半径<=0的无效围栏,减少内层循环次数
+     */
+    private List<TGeoFence> filterUsableFence(List<TGeoFence> allFence) {
+        if (CollUtil.isEmpty(allFence)) {
+            return Collections.emptyList();
+        }
+        return allFence.stream()
+                .filter(fence -> !ObjectUtil.hasNull(fence.getLatitude(), fence.getLongitude(), fence.getRadiusKm()))
+                .filter(fence -> fence.getRadiusKm().compareTo(BigDecimal.ZERO) > 0)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 计算单条订单围栏距离、是否高风险区域、围栏备注
+     */
+    private void calcOrderGeoFenceInfo(OrderPageVO orderVO, List<TGeoFence> usableFence) {
+        // 用户坐标为空直接标记非风险
+        BigDecimal userLat = orderVO.getUserLatitude();
+        BigDecimal userLon = orderVO.getUserLongitude();
+        if (ObjectUtil.hasNull(userLat, userLon) || CollUtil.isEmpty(usableFence)) {
+            orderVO.setIsHighRiskArea(false);
+            orderVO.setDistanceKm(null);
+            orderVO.setFenceIntro(null);
+            return;
+        }
+
+        BigDecimal minDistanceKm = null;
+        TGeoFence nearestFence = null;
+        // 遍历可用围栏,计算最小距离围栏
+        for (TGeoFence fence : usableFence) {
+            BigDecimal distKm = DistanceUtil.formatDistanceInKilometers(userLat, userLon, fence.getLatitude(), fence.getLongitude());
+            if (ObjectUtil.isNull(distKm)) {
+                continue;
+            }
+            // 更新最近围栏
+            if (ObjectUtil.isNull(minDistanceKm) || distKm.compareTo(minDistanceKm) < 0) {
+                minDistanceKm = distKm;
+                nearestFence = fence;
+            }
+        }
+
+        // 赋值围栏相关信息
+        if (ObjectUtil.isNull(minDistanceKm) || ObjectUtil.isNull(nearestFence)) {
+            orderVO.setIsHighRiskArea(false);
+            orderVO.setDistanceKm(null);
+            orderVO.setFenceIntro(null);
+            return;
+        }
+        orderVO.setDistanceKm(minDistanceKm);
+        // 判断是否在围栏半径内
+        boolean isHighRisk = minDistanceKm.compareTo(nearestFence.getRadiusKm()) <= 0;
+        orderVO.setIsHighRiskArea(isHighRisk);
+        orderVO.setFenceIntro(nearestFence.getFenceIntro());
+    }
+
 }

+ 23 - 1
nightFragrance-massage/src/main/resources/mapper/order/TOrderMapper.xml

@@ -309,10 +309,12 @@
 
     <resultMap id="OrderPageVOResultMap" type="com.ylx.order.domain.vo.merchant.OrderPageVO">
         <id column="id" property="id"/>
+        <result column="user_id" property="userId"/>
         <result column="project_name" property="projectName"/>
         <result column="project_cover" property="projectCover"/>
         <result column="project_duration" property="projectDuration"/>
         <result column="base_price" property="basePrice"/>
+        <result column="category_id" property="categoryId"/>
         <result column="traffic_fee" property="trafficFee"/>
         <result column="final_amount" property="finalAmount"/>
         <result column="appointment_start_time" property="appointmentStartTime"/>
@@ -324,15 +326,18 @@
         <result column="user_longitude" property="userLongitude"/>
         <result column="status" property="status"/>
         <result column="distance_km" property="distanceKm"/>
+        <result column="create_time" property="createTime"/>
     </resultMap>
 
     <select id="queryMerchantOrderList" resultMap="OrderPageVOResultMap">
         SELECT
             o.id,
+            o.user_id,
             p.project_name,
             p.cover_url AS project_cover,
             p.duration AS project_duration,
             p.base_price,
+            p.category_id,
             o.traffic_fee,
             o.final_amount,
             o.appointment_start_time,
@@ -348,7 +353,8 @@
                 WHEN o.user_longitude IS NULL OR o.user_latitude IS NULL THEN NULL
                 WHEN o.user_longitude = 0 OR o.user_latitude = 0 THEN NULL
                 ELSE ROUND(ST_Distance_Sphere(POINT(o.user_longitude, o.user_latitude), POINT(#{dto.longitude}, #{dto.latitude})) / 1000, 2)
-            END AS distance_km
+            END AS distance_km,
+            o.create_time
         FROM t_order o
         LEFT JOIN t_project p ON o.project_id = p.id
         <where>
@@ -394,4 +400,20 @@
         ORDER BY o.create_time DESC
     </select>
 
+    <!-- 根据用户、商户、分类,查询当前订单之前最近一笔下单时间 -->
+    <select id="getLastOrderDateBeforeCurrent" resultType="java.time.LocalDateTime">
+        SELECT
+            MAX(o.create_time)
+        FROM
+            t_order o LEFT JOIN project p ON o.project_id = p.id
+        WHERE
+            o.user_id = #{userId}
+        AND o.merchant_id = #{merchantId}
+        AND p.category_id = #{categoryId}
+        AND o.is_delete = 0
+        AND p.is_delete = 0
+        <!-- 排除当前时间,只查历史单 -->
+        AND o.create_time &lt; #{currentTime}
+    </select>
+
 </mapper>