瀏覽代碼

Merge remote-tracking branch 'origin/dev' into dev

jinshihui 1 周之前
父節點
當前提交
549ffd0981
共有 29 個文件被更改,包括 721 次插入175 次删除
  1. 84 49
      nightFragrance-common/src/main/java/com/ylx/common/utils/DistanceUtil.java
  2. 3 2
      nightFragrance-massage/src/main/java/com/ylx/fareSetting/domian/dto/FareCalculateDTO.java
  3. 14 20
      nightFragrance-massage/src/main/java/com/ylx/fareSetting/service/impl/MaProjectFareSettingServiceImpl.java
  4. 10 9
      nightFragrance-massage/src/main/java/com/ylx/massage/controller/MassageRecommendController.java
  5. 2 2
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/TAddress.java
  6. 4 2
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/CoordinateDTO.java
  7. 7 10
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/MassageAllMerchantsDto.java
  8. 23 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/MassageProjectRecommendDTO.java
  9. 1 1
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/MassageAllMerchantsVo.java
  10. 2 5
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/MassageProjectRecommendVo.java
  11. 1 1
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/MerchantVo.java
  12. 2 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/TGeoFenceService.java
  13. 13 2
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/AreaServiceImpl.java
  14. 1 1
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/MaTechnicianServiceImpl.java
  15. 10 1
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/TGeoFenceServiceImpl.java
  16. 3 2
      nightFragrance-massage/src/main/java/com/ylx/merchant/domain/dto/MerchantDetailDTO.java
  17. 2 1
      nightFragrance-massage/src/main/java/com/ylx/merchant/domain/vo/MerchantDetailVO.java
  18. 35 7
      nightFragrance-massage/src/main/java/com/ylx/order/controller/MerchantOrderController.java
  19. 23 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/MerchantOrderOperateDTO.java
  20. 6 2
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderDateQueryDTO.java
  21. 17 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderPhoneQueryDTO.java
  22. 22 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/merchant/MerchantCancelOrderDTO.java
  23. 13 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/merchant/OrderCustomerPhoneVO.java
  24. 4 1
      nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/merchant/OrderPageVO.java
  25. 12 0
      nightFragrance-massage/src/main/java/com/ylx/order/service/TOrderService.java
  26. 352 26
      nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java
  27. 4 4
      nightFragrance-massage/src/main/resources/mapper/massage/MaProjectMapper.xml
  28. 1 1
      nightFragrance-massage/src/main/resources/mapper/massage/MaTechnicianMapper.xml
  29. 50 26
      nightFragrance-massage/src/main/resources/mapper/order/TOrderMapper.xml

+ 84 - 49
nightFragrance-common/src/main/java/com/ylx/common/utils/DistanceUtil.java

@@ -1,71 +1,90 @@
 package com.ylx.common.utils;
 
 import cn.hutool.core.util.ObjectUtil;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 
 /**
- * 计算经纬度之间的直线距离
+ * Haversine公式计算两点经纬度球面直线距离工具类
+ * 所有坐标入参统一使用 BigDecimal,保证数据库高精度存储兼容
  */
-public class DistanceUtil {
+public final class DistanceUtil {
 
-    private static final double EARTH_RADIUS_M = 6_371_000; // 地球半径,单位米
-    private static final double TO_RADIANS = Math.PI / 180.0;
+    // 地球平均半径,单位:米
+    private static final double EARTH_RADIUS_M = 6_371_000D;
+    // 角度转弧度系数
+    private static final double TO_RADIANS = Math.PI / 180.0D;
+    // 米转公里换算值
+    private static final BigDecimal METER_TO_KM = new BigDecimal("1000");
+    // 经纬度极值常量
+    private static final BigDecimal MAX_LAT = new BigDecimal("90");
+    private static final BigDecimal MAX_LON = new BigDecimal("180");
+    private static final BigDecimal ZERO = BigDecimal.ZERO;
 
+    // 私有构造,禁止实例化工具类
+    private DistanceUtil() {
+        throw new AssertionError("工具类不可实例化");
+    }
+
+    // ===================== 对外业务方法 =====================
     /**
-     * 格式化两点间距离
-     *
+     * 获取距离(单位:米,四舍五入整数字符串)
      * @param userLat 用户纬度
      * @param userLon 用户经度
      * @param shopLat 门店纬度
      * @param shopLon 门店经度
-     * @return 距离(米),若坐标无效则返回 "未知"
+     * @return 有效距离返回米数字,坐标为空/0/超出范围 返回 "未知"
      */
-    public static String formatDistance(Double userLat, Double userLon, Double shopLat, Double shopLon) {
-        // 任意坐标空/0 → 未知
-        if (ObjectUtil.hasNull(userLat, userLon, shopLat, shopLon)
-                || userLat == 0 || userLon == 0 || shopLat == 0 || shopLon == 0) {
+    public static String formatDistance(BigDecimal userLat, BigDecimal userLon,
+                                        BigDecimal shopLat, BigDecimal shopLon) {
+        if (!isAllCoordinateValid(userLat, userLon, shopLat, shopLon)) {
             return "未知";
         }
-
-        // 验证坐标范围(可选,增加健壮性)
-        if (!isValidCoordinate(userLat, userLon) || !isValidCoordinate(shopLat, shopLon)) {
-            return "未知"; // 或抛出异常
-        }
-
-        double meter = getDistance(userLat, userLon, shopLat, shopLon);
+        double meter = getDistance(userLat.doubleValue(), userLon.doubleValue(),
+                shopLat.doubleValue(), shopLon.doubleValue());
         return String.valueOf(Math.round(meter));
     }
 
     /**
-     * 格式化两点间距离(单位:公里)
-     *
+     * 获取距离(单位:公里,保留2位小数)
      * @param userLat 用户纬度
      * @param userLon 用户经度
      * @param shopLat 门店纬度
      * @param shopLon 门店经度
-     * @return 距离(公里),若坐标无效则返回 "未知"
+     * @return 公里字符串,坐标非法返回 "未知"
      */
-    public static String formatDistanceInKilometers(Double userLat, Double userLon, Double shopLat, Double shopLon) {
-        if (ObjectUtil.hasNull(userLat, userLon, shopLat, shopLon)
-                || userLat == 0 || userLon == 0 || shopLat == 0 || shopLon == 0) {
-            return "未知";
+    public static BigDecimal formatDistanceInKilometers(BigDecimal userLat, BigDecimal userLon,
+                                                    BigDecimal shopLat, BigDecimal shopLon) {
+        if (!isAllCoordinateValid(userLat, userLon, shopLat, shopLon)) {
+            return null;
         }
-        if (!isValidCoordinate(userLat, userLon) || !isValidCoordinate(shopLat, shopLon)) {
+        double meterVal = getDistance(userLat.doubleValue(), userLon.doubleValue(),
+                shopLat.doubleValue(), shopLon.doubleValue());
+        BigDecimal meter = new BigDecimal(meterVal);
+        return meter.divide(METER_TO_KM, 2, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 前端友好展示距离
+     * 小于1000米:xx米;大于等于1000米:xx.xx公里;坐标异常:未知
+     */
+    public static String formatDisplay(BigDecimal userLat, BigDecimal userLon,
+                                       BigDecimal shopLat, BigDecimal shopLon) {
+        String meterStr = formatDistance(userLat, userLon, shopLat, shopLon);
+        if ("未知".equals(meterStr)) {
             return "未知";
         }
-        double meter = getDistance(userLat, userLon, shopLat, shopLon);
-        double kilometer = meter / 1000.0;
-        // 四舍五入到两位小数
-        return String.format("%.2f", kilometer);
+        long meter = Long.parseLong(meterStr);
+        if (meter < METER_TO_KM.longValue()) {
+            return meter + "米";
+        }
+        BigDecimal meterNum = new BigDecimal(meter);
+        BigDecimal km = meterNum.divide(METER_TO_KM, 2, RoundingMode.HALF_UP);
+        return km + "公里";
     }
 
     /**
-     * 获取两点间球面距离(米)
-     *
-     * @param lat1 纬度1
-     * @param lon1 经度1
-     * @param lat2 纬度2
-     * @param lon2 经度2
-     * @return 距离(米)
+     * 底层计算:返回两点球面距离(单位米)
      */
     public static double getDistance(double lat1, double lon1, double lat2, double lon2) {
         double lat1Rad = lat1 * TO_RADIANS;
@@ -73,27 +92,43 @@ public class DistanceUtil {
         double lat2Rad = lat2 * TO_RADIANS;
         double lon2Rad = lon2 * TO_RADIANS;
 
-        // 使用 Haversine 公式(更精确,避免 acos 在极近距离下的精度问题)
         double deltaLat = lat2Rad - lat1Rad;
         double deltaLon = lon2Rad - lon1Rad;
 
-        double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
-                Math.cos(lat1Rad) * Math.cos(lat2Rad) *
-                        Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
+        double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2)
+                + Math.cos(lat1Rad) * Math.cos(lat2Rad)
+                * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
 
         double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
-
         return EARTH_RADIUS_M * c;
     }
 
+    // ===================== 坐标校验私有方法 =====================
+    /**
+     * 批量校验四个坐标:非空、不等于0、经纬度范围合法
+     */
+    private static boolean isAllCoordinateValid(BigDecimal lat1, BigDecimal lon1,
+                                                BigDecimal lat2, BigDecimal lon2) {
+        // 任意坐标为null
+        if (ObjectUtil.hasNull(lat1, lon1, lat2, lon2)) {
+            return false;
+        }
+        // 任意坐标数值等于0
+        if (lat1.compareTo(ZERO) == 0
+                || lon1.compareTo(ZERO) == 0
+                || lat2.compareTo(ZERO) == 0
+                || lon2.compareTo(ZERO) == 0) {
+            return false;
+        }
+        // 两组坐标分别校验范围
+        return isSingleCoordinateValid(lat1, lon1) && isSingleCoordinateValid(lat2, lon2);
+    }
+
     /**
-     * 验证坐标是否在有效范围内
-     *
-     * @param lat 纬度
-     * @param lon 经度
-     * @return 是否有效
+     * 校验单组经纬度范围:纬度[-90,90] 经度[-180,180]
      */
-    private static boolean isValidCoordinate(double lat, double lon) {
-        return Math.abs(lat) <= 90.0 && Math.abs(lon) <= 180.0;
+    private static boolean isSingleCoordinateValid(BigDecimal lat, BigDecimal lon) {
+        return lat.abs().compareTo(MAX_LAT) <= 0
+                && lon.abs().compareTo(MAX_LON) <= 0;
     }
 }

+ 3 - 2
nightFragrance-massage/src/main/java/com/ylx/fareSetting/domian/dto/FareCalculateDTO.java

@@ -7,6 +7,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 @Data
@@ -34,10 +35,10 @@ public class FareCalculateDTO implements Serializable {
 
     @ApiModelProperty("用户下单经度")
     @NotNull(message = "用户下单经度不能为空")
-    private Double longitude;
+    private BigDecimal longitude;
 
     @ApiModelProperty("用户下单纬度")
     @NotNull(message = "用户下单纬度不能为空")
-    private Double latitude;
+    private BigDecimal latitude;
 
 }

+ 14 - 20
nightFragrance-massage/src/main/java/com/ylx/fareSetting/service/impl/MaProjectFareSettingServiceImpl.java

@@ -56,20 +56,10 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
         }
 
         // 2. 计算直线距离(公里)
-        String distanceStr = DistanceUtil.formatDistanceInKilometers(
-                dto.getLatitude(),dto.getLongitude(),
-                address.getLatitude(),address.getLongitude()
+        BigDecimal straightLineBigDecimal = DistanceUtil.formatDistanceInKilometers(
+                dto.getLatitude(), dto.getLongitude(),
+                address.getLatitude(), address.getLongitude()
         );
-        double straightLineKm;
-        if ("未知".equals(distanceStr)) {
-            throw new ServiceException("无法获取客户地址信息");
-        }
-        try {
-            straightLineKm = Double.parseDouble(distanceStr);
-        } catch (NumberFormatException e) {
-            log.error("距离字符串解析失败: {}", distanceStr, e);
-            throw new ServiceException("距离数据异常");
-        }
 
         // 3.根据时间段判断是否白天时间段
         LocalDateTime appointmentStartTime = dto.getAppointmentStartTime();
@@ -79,13 +69,15 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
         BigDecimal merchantFreeKm = getMerchantFreeKm(dto.getMerchantId(), dto.getProjectId(), isDay);
 
         // 5. 计算【打车距离】(即计费里程)
-        BigDecimal straightLineBigDecimal = new BigDecimal(straightLineKm).setScale(6, RoundingMode.HALF_UP);
+        if (ObjectUtil.isNull(straightLineBigDecimal)) {
+            throw new ServiceException("距离未知");
+        }
         BigDecimal effectiveDistance = straightLineBigDecimal.subtract(merchantFreeKm);
         if (effectiveDistance.compareTo(BigDecimal.ZERO) < 0) {
             effectiveDistance = BigDecimal.ZERO;
         }
         log.info("直线距离 {} km, 商户免车费距离 {} km -> 打车距离 {} km",
-                straightLineKm, merchantFreeKm, effectiveDistance);
+                straightLineBigDecimal, merchantFreeKm, effectiveDistance);
 
         // 6. 获取城市车费规则(用于最终计费)
         TFareSettingVo cityFare = fareSettingService.getFareSetting(appointmentStartTime, dto.getCityCode());
@@ -194,6 +186,7 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
             return targetTime.equals(startTime);
         }
     }
+
     /**
      * 保存免车费设置信息
      *
@@ -202,7 +195,7 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
      * @return
      */
     @Override
-    public void saveOrUpdateFee(DriverFeeDTO dto){
+    public void saveOrUpdateFee(DriverFeeDTO dto) {
         // 1. 基础校验:检查是否有空值 (对应UI中的“判断必填项是否为空”)
         validateInput(dto);
 
@@ -210,7 +203,7 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
             // --- 模式一:统一设置 ---
             // 逻辑:删除该用户所有具体的项目配置,只保留一条 categoryId=null 的记录
             baseMapper.delete(new LambdaQueryWrapper<MaProjectFareSetting>()
-                                               .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId()));
+                    .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId()));
 
             MaProjectFareSetting config = new MaProjectFareSetting();
             config.setMerchantId(dto.getMerchantId());
@@ -223,7 +216,7 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
             // --- 模式二:按项目设置 ---
             // 逻辑:先删除旧数据,再批量插入新数据
             baseMapper.delete(new LambdaQueryWrapper<MaProjectFareSetting>()
-                                               .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId()));
+                    .eq(MaProjectFareSetting::getMerchantId, dto.getMerchantId()));
 
             List<MaProjectFareSetting> list = new ArrayList<>();
             for (DriverFeeDTO.CategoryFeeItem item : dto.getCategoryConfigs()) {
@@ -243,14 +236,15 @@ public class MaProjectFareSettingServiceImpl extends ServiceImpl<MaProjectFareSe
         }
 
     }
+
     /**
      * 校验输入是否合法 (对应UI中的 Toast 提示逻辑)
      */
     private void validateInput(DriverFeeDTO dto) {
         if (dto.getMode() == 1) {
             if (dto.getUnifiedConfig() == null ||
-                        dto.getUnifiedConfig().getDayFreeKm() == null ||
-                        dto.getUnifiedConfig().getNightFreeKm() == null) {
+                    dto.getUnifiedConfig().getDayFreeKm() == null ||
+                    dto.getUnifiedConfig().getNightFreeKm() == null) {
                 throw new RuntimeException("请输入完整的统一设置里程");
             }
         } else if (dto.getMode() == 2) {

+ 10 - 9
nightFragrance-massage/src/main/java/com/ylx/massage/controller/MassageRecommendController.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.core.domain.R;
 import com.ylx.massage.domain.dto.MassageAllMerchantsDto;
 import com.ylx.massage.domain.dto.MassageMerchantRecommendDto;
+import com.ylx.massage.domain.dto.MassageProjectRecommendDTO;
 import com.ylx.massage.domain.vo.MassageAllMerchantsVo;
 import com.ylx.massage.domain.vo.MassageProjectRecommendVo;
 import com.ylx.massage.domain.vo.MerchantVo;
@@ -33,25 +34,25 @@ public class MassageRecommendController {
     @Autowired
     private IMassageRecommendService massageRecommendService;
 
-    @ApiOperation(value="首页按摩商户推荐", notes="首页按摩商户推荐")
+    @ApiOperation(value = "首页按摩商户推荐", notes = "首页按摩商户推荐")
     @PostMapping(value = "/getMerchantRecommend")
     public R<List<MerchantVo>> getMerchantRecommend(@RequestBody MassageMerchantRecommendDto dto) {
         List<MerchantVo> massageRecommendVos = massageRecommendService.getMerchantRecommend(dto);
         return R.ok(massageRecommendVos);
     }
 
-    @ApiOperation(value="首页按摩项目推荐", notes="首页按摩项目推荐")
-    @GetMapping(value = "/getMassageProjectRecommend")
-    public R<Page<MassageProjectRecommendVo>> getMassageProjectRecommend(@RequestParam("pageNo") Integer pageNo, @RequestParam("pageSize") Integer pageSize,@RequestParam("cityCode") String cityCode) {
-        Page<MassageProjectRecommendVo> page = new Page<>(pageNo, pageSize);
-        return R.ok( massageRecommendService.getMassageProjectRecommend(page,cityCode));
+    @ApiOperation(value = "首页按摩项目推荐", notes = "首页按摩项目推荐")
+    @PostMapping(value = "/getMassageProjectRecommend")
+    public R<Page<MassageProjectRecommendVo>> getMassageProjectRecommend(@Validated @RequestBody MassageProjectRecommendDTO dto) {
+        Page<MassageProjectRecommendVo> page = new Page<>(dto.getCurrent(), dto.getSize());
+        return R.ok(massageRecommendService.getMassageProjectRecommend(page, dto.getCityCode()));
     }
 
-    @ApiOperation(value="首页按摩推荐商户点击全部查询按摩所有商户", notes="首页按摩推荐商户点击全部查询按摩所有商户")
+    @ApiOperation(value = "首页按摩推荐商户点击全部查询按摩所有商户", notes = "首页按摩推荐商户点击全部查询按摩所有商户")
     @PostMapping(value = "/getMassageAllMerchants")
     public R<Page<MassageAllMerchantsVo>> getMassageAllMerchants(@Validated @RequestBody MassageAllMerchantsDto dto) {
-        Page<MassageAllMerchantsVo> page = new Page<>(dto.getPageNo(), dto.getPageSize());
-        return R.ok( massageRecommendService.getMassageAllMerchants(page,dto));
+        Page<MassageAllMerchantsVo> page = new Page<>(dto.getCurrent(), dto.getSize());
+        return R.ok(massageRecommendService.getMassageAllMerchants(page, dto));
     }
 
 

+ 2 - 2
nightFragrance-massage/src/main/java/com/ylx/massage/domain/TAddress.java

@@ -67,10 +67,10 @@ public class TAddress extends Model<TAddress> {
     private String atlasAdd;
     //经度
     @ApiModelProperty("经度")
-    private Double longitude;
+    private BigDecimal longitude;
     //纬度
     @ApiModelProperty("纬度")
-    private Double latitude;
+    private BigDecimal latitude;
     //地址
     @ApiModelProperty("地址")
     private String address;

+ 4 - 2
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/CoordinateDTO.java

@@ -4,14 +4,16 @@ import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 @Data
 @ApiModel(description = "经纬度查询dto")
 public class CoordinateDTO {
 
     @ApiModelProperty(value = "经度", required = true)
-    private double longitude;
+    private BigDecimal longitude;
 
     @ApiModelProperty(value = "纬度", required = true)
-    private double latitude;
+    private BigDecimal latitude;
 
 }

+ 7 - 10
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/MassageAllMerchantsDto.java

@@ -1,10 +1,13 @@
 package com.ylx.massage.domain.dto;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.massage.domain.vo.MassageAllMerchantsVo;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotNull;
+import java.io.Serializable;
 import java.math.BigDecimal;
 
 /**
@@ -14,16 +17,11 @@ import java.math.BigDecimal;
  * @version 1.0
  * @date 2026/6/4 9:31
  */
+@EqualsAndHashCode(callSuper = true)
 @Data
-public class MassageAllMerchantsDto {
+public class MassageAllMerchantsDto extends Page<MassageAllMerchantsVo> implements Serializable {
+    private static final long serialVersionUID = -2201674487204015309L;
 
-    @NotNull(message = "页码不能为空")
-    @ApiModelProperty("页码")
-    private Integer pageNo;
-
-    @NotNull(message = "每页大小不能为空")
-    @ApiModelProperty("每页大小")
-    private Integer pageSize;
     /**
      * 经度
      * 用户当前位置的经度坐标
@@ -51,5 +49,4 @@ public class MassageAllMerchantsDto {
     @ApiModelProperty("项目id")
     private String projectId;
 
-
 }

+ 23 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/MassageProjectRecommendDTO.java

@@ -0,0 +1,23 @@
+package com.ylx.massage.domain.dto;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.massage.domain.vo.MassageProjectRecommendVo;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@ApiModel("按摩项目推荐DTO")
+public class MassageProjectRecommendDTO extends Page<MassageProjectRecommendVo> implements Serializable {
+    private static final long serialVersionUID = 4958777236645803139L;
+
+    @NotBlank(message = "城市编码不能为空")
+    @ApiModelProperty("城市编码")
+    private String cityCode;
+
+}

+ 1 - 1
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/MassageAllMerchantsVo.java

@@ -45,7 +45,7 @@ public class MassageAllMerchantsVo {
      * 技师距离(公里)
      */
     @ApiModelProperty("技师距离(公里)")
-    private BigDecimal distanceShow;
+    private String distanceShow;
     /**
      * 头像
      */

+ 2 - 5
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/MassageProjectRecommendVo.java

@@ -1,6 +1,5 @@
 package com.ylx.massage.domain.vo;
 
-import com.ylx.common.annotation.Excel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
@@ -15,15 +14,13 @@ import java.math.BigDecimal;
  */
 @Data
 public class MassageProjectRecommendVo {
-    @ApiModelProperty("项目id")
-    private Long projectId;
 
+    @ApiModelProperty("项目id")
+    private String projectId;
 
-    /** 项目名称 */
     @ApiModelProperty("项目名称")
     private String projectName;
 
-    /** 项目时长(分) */
     @ApiModelProperty("项目时长(分)")
     private Long projectDuration;
 

+ 1 - 1
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/MerchantVo.java

@@ -42,7 +42,7 @@ public class MerchantVo implements Serializable {
     private Integer nNum;
 
     @ApiModelProperty("技师距离(公里)")
-    private BigDecimal distanceShow;
+    private String distanceShow;
 
     /**
      * 商户开通技能时设置的最低价格

+ 2 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/TGeoFenceService.java

@@ -51,4 +51,6 @@ public interface TGeoFenceService extends IService<TGeoFence> {
      * @return 是否删除成功
      */
     Boolean deleteGeoFence(TGeoFence geoFence);
+
+    List<TGeoFence> selectValidFences(String cityCode);
 }

+ 13 - 2
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/AreaServiceImpl.java

@@ -24,6 +24,7 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -166,10 +167,20 @@ public class AreaServiceImpl extends ServiceImpl<AreaMapper, Area> implements Ar
         if (dto == null) {
             throw new IllegalArgumentException("坐标信息不能为空");
         }
-        if (dto.getLongitude() < -180 || dto.getLongitude() > 180) {
+
+        // 1. 先判空
+        if (dto.getLongitude() == null || dto.getLatitude() == null) {
+            throw new IllegalArgumentException("经纬度数值不能为空");
+        }
+
+        // 2. 再比较(此时已经确定不为 null,可以安全拆箱)
+        double lon = dto.getLongitude().doubleValue();
+        double lat = dto.getLatitude().doubleValue();
+
+        if (lon < -180 || lon > 180) {
             throw new IllegalArgumentException("经度必须在-180到180之间");
         }
-        if (dto.getLatitude() < -90 || dto.getLatitude() > 90) {
+        if (lat < -90 || lat > 90) {
             throw new IllegalArgumentException("纬度必须在-90到90之间");
         }
     }

+ 1 - 1
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/MaTechnicianServiceImpl.java

@@ -1329,7 +1329,7 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
         }
 
         // 3. 计算当前用户距离商户距离
-        String distanceStr = DistanceUtil.formatDistanceInKilometers(
+        BigDecimal distanceStr = DistanceUtil.formatDistanceInKilometers(
                 dto.getLatitude(), dto.getLongitude(),
                 address.getLatitude(), address.getLongitude()
         );

+ 10 - 1
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/TGeoFenceServiceImpl.java

@@ -107,6 +107,14 @@ public class TGeoFenceServiceImpl extends ServiceImpl<TGeoFenceMapper, TGeoFence
         return true;
     }
 
+    @Override
+    public List<TGeoFence> selectValidFences(String cityCode) {
+        LambdaQueryWrapper<TGeoFence> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StringUtils.isNotBlank(cityCode), TGeoFence::getCityCode, cityCode)
+                .eq(TGeoFence::getIsDelete, NOT_DELETE);
+        return this.list(queryWrapper);
+    }
+
     private void validateUpdateParam(TGeoFence geoFence) {
         if (geoFence == null) {
             throw new ServiceException("参数不能为空");
@@ -158,6 +166,7 @@ public class TGeoFenceServiceImpl extends ServiceImpl<TGeoFenceMapper, TGeoFence
 
     /**
      * 构建查询条件。
+     *
      * @param geoFence
      * @return LambdaQueryWrapper<TGeoFence>
      */
@@ -175,7 +184,7 @@ public class TGeoFenceServiceImpl extends ServiceImpl<TGeoFenceMapper, TGeoFence
      * 验证地理坐标栏中心点坐标是否有效。
      *
      * @param longitude 经度
-     * @param latitude 纬度
+     * @param latitude  纬度
      */
     private void validateCoordinate(BigDecimal longitude, BigDecimal latitude) {
         if (longitude == null) {

+ 3 - 2
nightFragrance-massage/src/main/java/com/ylx/merchant/domain/dto/MerchantDetailDTO.java

@@ -6,6 +6,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 @Data
 @ApiModel("客户端商户详情DTO")
@@ -18,10 +19,10 @@ public class MerchantDetailDTO implements Serializable {
 
     @ApiModelProperty("用户经度")
     @NotNull(message = "用户经度不能为空")
-    private Double longitude;
+    private BigDecimal longitude;
 
     @ApiModelProperty("用户纬度")
     @NotNull(message = "用户纬度不能为空")
-    private Double latitude;
+    private BigDecimal latitude;
 
 }

+ 2 - 1
nightFragrance-massage/src/main/java/com/ylx/merchant/domain/vo/MerchantDetailVO.java

@@ -6,6 +6,7 @@ import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -48,7 +49,7 @@ public class MerchantDetailVO implements Serializable {
     private String brief;
 
     @ApiModelProperty("距离:千米")
-    private String distance;
+    private BigDecimal distance;
 
     @ApiModelProperty("商户评分")
     private Double score;

+ 35 - 7
nightFragrance-massage/src/main/java/com/ylx/order/controller/MerchantOrderController.java

@@ -2,9 +2,9 @@ package com.ylx.order.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.core.domain.R;
-import com.ylx.order.domain.dto.OrderAcceptDTO;
-import com.ylx.order.domain.dto.OrderDateQueryDTO;
-import com.ylx.order.domain.dto.OrderRejectDTO;
+import com.ylx.order.domain.dto.*;
+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.service.TOrderService;
 import io.swagger.annotations.Api;
@@ -43,10 +43,38 @@ public class MerchantOrderController {
         return R.ok();
     }
 
-    @ApiOperation("商户立即接单接口")
-    @PostMapping("/accept")
-    public R<Void> acceptOrder(@Validated @RequestBody OrderAcceptDTO dto) {
-        this.orderService.acceptOrder(dto);
+    @PostMapping("/getCustomerPhone")
+    @ApiOperation("根据订单ID获取客户完整手机号(用于电话联系)")
+    public R<OrderCustomerPhoneVO> getCustomerPhone(@Validated @RequestBody OrderPhoneQueryDTO dto) {
+        OrderCustomerPhoneVO vo = orderService.getCustomerPhoneByOrderId(dto);
+        return R.ok(vo);
+    }
+
+    @PostMapping("/arriveSign")
+    @ApiOperation("商户签到到达客户地址")
+    public R<Void> orderArriveSign(@Validated @RequestBody MerchantOrderOperateDTO dto) {
+        this.orderService.merchantArriveSign(dto);
+        return R.ok();
+    }
+
+    @PostMapping("/cancelAfterReceive")
+    @ApiOperation("商户接单后取消订单")
+    public R<Void> merchantCancelOrder(@Validated @RequestBody MerchantCancelOrderDTO dto) {
+        this.orderService.merchantCancelOrderToAfterSale(dto);
+        return R.ok();
+    }
+
+    @PostMapping("/startService")
+    @ApiOperation("商户开始服务,订单流转服务中")
+    public R<Void> startService(@Validated @RequestBody MerchantOrderOperateDTO dto) {
+        this.orderService.startService(dto);
+        return R.ok();
+    }
+
+    @PostMapping("/finishService")
+    @ApiOperation("商户完成服务,订单流转已完成")
+    public R<Void> finishService(@Validated @RequestBody MerchantOrderOperateDTO dto) {
+        this.orderService.finishService(dto);
         return R.ok();
     }
 

+ 23 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/MerchantOrderOperateDTO.java

@@ -0,0 +1,23 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+@ApiModel("商户操作订单通用入参")
+public class MerchantOrderOperateDTO {
+
+    @NotNull(message = "订单ID不能为空")
+    @ApiModelProperty("订单主键ID")
+    private Long orderId;
+
+    @ApiModelProperty("商户手机号")
+    private String phone;
+
+    @ApiModelProperty("短信验证码")
+    private String code;
+
+}

+ 6 - 2
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderDateQueryDTO.java

@@ -5,6 +5,7 @@ import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
 
 /**
@@ -54,8 +55,11 @@ public class OrderDateQueryDTO extends Page<OrderDateQueryDTO> {
     private Long merchantId;
 
     @ApiModelProperty("经度")
-    private Double longitude;
+    private BigDecimal longitude;
 
     @ApiModelProperty("纬度")
-    private Double latitude;
+    private BigDecimal latitude;
+
+    @ApiModelProperty("城市编码")
+    private String cityCode;
 }

+ 17 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderPhoneQueryDTO.java

@@ -0,0 +1,17 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+@ApiModel("根据订单ID获取客户明文手机号入参")
+public class OrderPhoneQueryDTO {
+
+    @NotNull(message = "订单ID不能为空")
+    @ApiModelProperty(value = "订单主键ID")
+    private Long orderId;
+
+}

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

@@ -0,0 +1,22 @@
+package com.ylx.order.domain.vo.merchant;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+@ApiModel("商户接单后取消订单入参")
+public class MerchantCancelOrderDTO {
+
+    @NotNull(message = "订单ID不能为空")
+    @ApiModelProperty("订单ID")
+    private Long orderId;
+
+    @NotBlank(message = "取消原因不能为空")
+    @ApiModelProperty("商户取消订单原因,用于售后备注")
+    private String cancelReason;
+
+}

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

@@ -0,0 +1,13 @@
+package com.ylx.order.domain.vo.merchant;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("客户手机号返回VO")
+public class OrderCustomerPhoneVO {
+
+    @ApiModelProperty("客户完整明文手机号")
+    private String contactPhoneNumber;
+}

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

@@ -55,7 +55,7 @@ public class OrderPageVO<T> implements IAfterSaleDisplay {
     private String contactAddressInfo;
 
     @ApiModelProperty("距离")
-    private Double distanceKm;
+    private BigDecimal distanceKm;
 
     @ApiModelProperty("用户下单时纬度")
     private BigDecimal userLatitude;
@@ -63,6 +63,9 @@ public class OrderPageVO<T> implements IAfterSaleDisplay {
     @ApiModelProperty("用户下单时经度")
     private BigDecimal userLongitude;
 
+    @ApiModelProperty("围栏介绍")
+    private String fenceIntro;
+
     @ApiModelProperty("风险提示")
     private Boolean isHighRiskArea;
 

+ 12 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/TOrderService.java

@@ -12,6 +12,8 @@ import com.ylx.massage.domain.vo.TechnicianAvailabilityVo;
 import com.ylx.order.domain.dto.*;
 import com.ylx.order.domain.vo.OrderDateQueryVo;
 import com.ylx.order.domain.vo.OrderDetailVO;
+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 java.math.BigDecimal;
@@ -198,4 +200,14 @@ public interface TOrderService extends IService<TOrder> {
     void rejectOrder(OrderRejectDTO dto);
 
     void acceptOrder(OrderAcceptDTO dto);
+
+    OrderCustomerPhoneVO getCustomerPhoneByOrderId(OrderPhoneQueryDTO dto);
+
+    void merchantCancelOrderToAfterSale(MerchantCancelOrderDTO dto);
+
+    void startService(MerchantOrderOperateDTO dto);
+
+    void finishService(MerchantOrderOperateDTO dto);
+
+    void merchantArriveSign(MerchantOrderOperateDTO dto);
 }

+ 352 - 26
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -12,25 +13,16 @@ import com.ylx.common.core.domain.R;
 import com.ylx.common.core.domain.entity.SysDictData;
 import com.ylx.common.core.domain.model.WxLoginUser;
 import com.ylx.common.exception.ServiceException;
-import com.ylx.common.utils.DateUtils;
-import com.ylx.common.utils.DictUtils;
-import com.ylx.common.utils.SecurityUtils;
-import com.ylx.common.utils.StringUtils;
+import com.ylx.common.utils.*;
 import com.ylx.common.weixinPay.enums.WxPayTypeEnum;
 import com.ylx.common.weixinPay.service.WxPayV3Service;
-import com.ylx.massage.domain.MaProject;
-import com.ylx.massage.domain.MaTechnician;
-import com.ylx.massage.domain.TAddress;
-import com.ylx.massage.domain.TWxUser;
+import com.ylx.massage.domain.*;
 import com.ylx.massage.domain.vo.HomeBlock;
 import com.ylx.massage.domain.vo.OrderVerificationVo;
 import com.ylx.massage.domain.vo.TechnicianAvailabilityVo;
 import com.ylx.massage.mapper.MaProjectMapper;
 import com.ylx.massage.mapper.MaTechnicianMapper;
-import com.ylx.massage.service.CouponService;
-import com.ylx.massage.service.IMaTechnicianService;
-import com.ylx.massage.service.TAddressService;
-import com.ylx.massage.service.TWxUserService;
+import com.ylx.massage.service.*;
 import com.ylx.massage.utils.OrderNumberGenerator;
 import com.ylx.message.enums.TriggerEventEnum;
 import com.ylx.message.service.IMessageService;
@@ -41,6 +33,8 @@ import com.ylx.order.domain.dto.*;
 import com.ylx.order.domain.vo.OrderDateQueryVo;
 import com.ylx.order.domain.vo.OrderDetailVO;
 import com.ylx.order.domain.vo.OrderStatusFlowVO;
+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.OrderStatusEnum;
@@ -57,6 +51,7 @@ import com.ylx.shopingfundsdetail.enums.ShoppingFundsExpenseTypeEnum;
 import com.ylx.shopingfundsdetail.service.ShoppingFundsDetailService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -120,6 +115,14 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     private IMessageService messageService;
 
     private static final int NOT_DELETE = 0;
+    @Resource
+    private TGeoFenceService geoFenceService;
+
+    @Value("${ylx.sendSmsEnabled:false}")
+    private boolean sendSmsEnabled;
+
+    @Value("${ylx.fixedVerifyCode:123456}")
+    private String fixedVerifyCode;
 
     @Override
     public TOrder addOrder(TOrder order) {
@@ -554,16 +557,16 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         order.setPaymentMethod(dto.getPaymentMethod());
 
         // 经纬度安全赋值
-        order.setUserLatitude(new BigDecimal(address.getLatitude()));
-        order.setUserLongitude(new BigDecimal(address.getLongitude()));
+        order.setUserLatitude(address.getLatitude());
+        order.setUserLongitude(address.getLongitude());
 
         merchantAddressList.stream().filter(a -> a.getType() == 1).findFirst().ifPresent(addr -> {
-            order.setMerchantLongitude(new BigDecimal(addr.getLongitude()));
-            order.setMerchantLatitude(new BigDecimal(addr.getLatitude()));
+            order.setMerchantLongitude(addr.getLongitude());
+            order.setMerchantLatitude(addr.getLatitude());
         });
         merchantAddressList.stream().filter(a -> a.getType() == 2).findFirst().ifPresent(addr -> {
-            order.setVirtualLongitude(new BigDecimal(addr.getLongitude()));
-            order.setVirtualLatitude(new BigDecimal(addr.getLatitude()));
+            order.setVirtualLongitude(addr.getLongitude());
+            order.setVirtualLatitude(addr.getLatitude());
         });
 
         // 设置支付状态
@@ -905,19 +908,88 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
                 .eq(TAddress::getIsDefault, 1)
                 .eq(TAddress::getIsDelete, NOT_DELETE));
         if (CollUtil.isEmpty(merchantAddressList)) {
-            throw new ServiceException("商户地址不存在");
+            throw new ServiceException("商户地址不存在,请先完善商户地址");
         }
 
-        TAddress address = CollUtil.getFirst(merchantAddressList);
-        dto.setLongitude(address.getLongitude());
-        dto.setLatitude(address.getLatitude());
+        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)) {
+            return page;
+        }
 
-        if (ObjectUtil.isNotNull(page) && CollUtil.isNotEmpty(page.getRecords())) {
-            List<OrderPageVO> records = page.getRecords();
-            for (OrderPageVO record : records) {
-                this.fillCurrentAfterSaleInfo(record, record.getId());
+        // 4. 获取所有有效的地理围栏
+        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;
+            }
+
+            // 存储最小距离、对应围栏、是否命中风险圈
+            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());
             }
         }
 
@@ -979,6 +1051,241 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
     }
 
+    @Override
+    public OrderCustomerPhoneVO getCustomerPhoneByOrderId(OrderPhoneQueryDTO dto) {
+        // 1. 获取当前登录商户
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
+        Long merchantId = Long.parseLong(wxLoginUser.getId());
+        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));
+
+        // 3. 校验订单
+        if (ObjectUtil.isNull(order)) {
+            throw new ServiceException("订单不存在或不属于当前商户");
+        }
+        String phone = order.getContactPhoneNumber();
+        if (StrUtil.isBlank(phone)) {
+            throw new ServiceException("该订单未预留客户联系电话");
+        }
+
+        // 4. 封装返回明文手机号
+        OrderCustomerPhoneVO vo = new OrderCustomerPhoneVO();
+        vo.setContactPhoneNumber(phone);
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void merchantCancelOrderToAfterSale(MerchantCancelOrderDTO dto) {
+        // 1. 获取当前登录商户
+        WxLoginUser loginUser = getCurrentWxLoginUser();
+        Long merchantId = Long.parseLong(loginUser.getId());
+        Long orderId = dto.getOrderId();
+        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));
+
+        // 订单不存在/不属于当前商户
+        if (ObjectUtil.isNull(order)) {
+            throw new ServiceException("订单不存在,无法操作");
+        }
+
+        // 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))) {
+            String currentText = OrderStatusEnum.getInfoByCode(statusCode);
+            throw new ServiceException("仅【待服务、服务中】订单可操作,当前订单状态:" + currentText);
+        }
+
+        // 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);
+            int row = baseMapper.update(null, updateWrapper);
+            if (row <= 0) {
+                throw new ServiceException("订单状态已变更,请刷新重试");
+            }
+        } 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);
+            int row = baseMapper.update(null, updateWrapper);
+            if (row <= 0) {
+                throw new ServiceException("订单状态已变更,请刷新重试");
+            }
+
+            // 2. 创建售后单
+            AfterSalesServiceDTO afterSalesServiceDTO = new AfterSalesServiceDTO();
+            afterSalesServiceDTO.setOrderId(orderId);
+            afterSalesServiceDTO.setReason(cancelReason);
+            this.afterSalesServiceService.applyAfterSale(afterSalesServiceDTO);
+        }
+
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void startService(MerchantOrderOperateDTO dto) {
+
+        // 1. 获取当前登录商户
+        WxLoginUser loginUser = getCurrentWxLoginUser();
+        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));
+        if (ObjectUtil.isNull(order)) {
+            throw new ServiceException("订单不存在或不属于当前商户");
+        }
+
+        Integer status = order.getStatus();
+        if (!OrderStatusEnum.PENDING_SERVICE.getCode().equals(status)) {
+            throw new ServiceException("仅【待服务】订单可开始服务,当前状态:" + OrderStatusEnum.getInfoByCode(status));
+        }
+
+        // 可选强校验:必须先签到到达才能开始服务
+        if (ObjectUtil.isNull(order.getArrivedTime())) {
+            throw new ServiceException("请先完成到达签到,再开始服务");
+        }
+
+        // 更新状态+服务开始时间
+        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("订单状态已变更,请刷新重试");
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void finishService(MerchantOrderOperateDTO dto) {
+
+        // 1. 获取当前登录商户
+        WxLoginUser loginUser = getCurrentWxLoginUser();
+        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));
+        if (ObjectUtil.isNull(order)) {
+            throw new ServiceException("订单不存在或不属于当前商户");
+        }
+
+        Integer status = order.getStatus();
+        if (!OrderStatusEnum.IN_SERVICE.getCode().equals(status)) {
+            throw new ServiceException("仅【服务中】订单可完成服务,当前状态:" + OrderStatusEnum.getInfoByCode(status));
+        }
+
+        // 必须存在服务开始时间才能结束
+        if (ObjectUtil.isNull(order.getStartTime())) {
+            throw new ServiceException("未开始服务,无法完成");
+        }
+
+        // 更新状态、结束时间
+        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("订单状态已变更,请刷新重试");
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void merchantArriveSign(MerchantOrderOperateDTO dto) {
+
+        // 1. 获取当前登录商户
+        WxLoginUser loginUser = getCurrentWxLoginUser();
+        Long merchantId = Long.parseLong(loginUser.getId());
+        Long orderId = dto.getOrderId();
+        String code = dto.getCode();
+        String phone = dto.getPhone();
+
+        // 手机号或者验证码 校验
+        if (ObjectUtil.hasNull(phone, code)) {
+            throw new ServiceException("手机号或者验证码不能为空");
+        }
+
+        if (!validateVerifyCode(phone, code)) {
+            throw new ServiceException("验证码错误");
+        }
+
+        // 查询订单
+        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("订单不存在或不属于当前商户");
+        }
+
+        // 状态仅允许待服务
+        Integer status = order.getStatus();
+        if (!OrderStatusEnum.PENDING_SERVICE.getCode().equals(status)) {
+            throw new ServiceException("仅【待服务】订单可签到到达,当前状态:" + OrderStatusEnum.getInfoByCode(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("订单状态已变更,请刷新重试");
+        }
+    }
+
     private void fillCurrentAfterSaleInfo(IAfterSaleDisplay vo, Long orderId) {
         LambdaQueryWrapper<AfterSalesService> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AfterSalesService::getOrderId, orderId)
@@ -1102,4 +1409,23 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         return order;
     }
 
+    /**
+     * 校验验证码
+     *
+     * @param phone 手机号
+     * @param code  用户输入的验证码
+     * @return boolean 校验是否通过
+     */
+    private boolean validateVerifyCode(String phone, String code) {
+        if (sendSmsEnabled) {
+            // TODO: 从Redis或其他存储中获取真实发送的验证码进行校验
+            // String realCode = redisCache.getCacheObject("sms:bindPhone:" + phone);
+            // return code.equals(realCode);
+            return true;
+        } else {
+            // 使用写死的验证码进行校验
+            return fixedVerifyCode.equals(code);
+        }
+    }
+
 }

+ 4 - 4
nightFragrance-massage/src/main/resources/mapper/massage/MaProjectMapper.xml

@@ -319,7 +319,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                                     AND tech.is_delete = 0
                                     AND tech.audit_status = 2
                                     AND tech.service_state = 1
-            WHERE mp.merchant_type = 1
+            WHERE mp.service_tag = 1
               AND mp.is_delete = 0
               AND mp.project_is_enable = 1
             GROUP BY CAST(mp.project_id AS UNSIGNED)
@@ -369,7 +369,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ON p.merchant_id = CAST(t.id AS CHAR)
         AND p.audit_status = 1
         AND p.is_delete = 0
-        AND p.merchant_type = 1
+        AND p.service_tag = 1
         AND p.project_is_enable = 0
         <if test="dto.projectId != null and dto.projectId != ''">
             AND p.project_id = #{dto.projectId}
@@ -477,7 +477,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                                     AND tech.is_delete = 0
                                     AND tech.audit_status = 2
                                     AND tech.service_state = 1
-            WHERE mp.merchant_type = 2
+            WHERE mp.service_tag = 2
               AND mp.is_delete = 0
               AND mp.project_is_enable = 1
             GROUP BY CAST(mp.project_id AS UNSIGNED)
@@ -531,7 +531,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ON p.merchant_id = CAST(t.id AS CHAR)
         AND p.audit_status = 1
         AND p.is_delete = 0
-        AND p.merchant_type = 2
+        AND p.service_tag = 2
         AND p.project_is_enable = 0
         <if test="dto.projectId != null and dto.projectId != ''">
             AND p.project_id = #{dto.projectId}

+ 1 - 1
nightFragrance-massage/src/main/resources/mapper/massage/MaTechnicianMapper.xml

@@ -508,7 +508,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                  INNER JOIN (
             SELECT merchant_id, MIN(project_current_price) AS min_price
             FROM ma_project
-            WHERE is_delete = 0 AND audit_status = 1 AND merchant_type = 1 AND project_is_enable = 1
+            WHERE is_delete = 0 AND audit_status = 1 AND service_tag = 1 AND project_is_enable = 1
             GROUP BY merchant_id
         ) p ON t.id = p.merchant_id
         ORDER BY COALESCE(o.sales, 0) DESC

+ 50 - 26
nightFragrance-massage/src/main/resources/mapper/order/TOrderMapper.xml

@@ -307,33 +307,56 @@
           AND appointment_start_time &lt;= #{dayEnd}
     </select>
 
-    <select id="queryMerchantOrderList" resultType="com.ylx.order.domain.vo.merchant.OrderPageVO">
+    <resultMap id="OrderPageVOResultMap" type="com.ylx.order.domain.vo.merchant.OrderPageVO">
+        <id column="id" property="id"/>
+        <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="traffic_fee" property="trafficFee"/>
+        <result column="final_amount" property="finalAmount"/>
+        <result column="appointment_start_time" property="appointmentStartTime"/>
+        <result column="appointment_end_time" property="appointmentEndTime"/>
+        <result column="contact_person_name" property="contactPersonName"/>
+        <result column="contact_phone_number" property="contactPhoneNumber"/>
+        <result column="service_address_detail" property="contactAddressInfo"/>
+        <result column="user_latitude" property="userLatitude"/>
+        <result column="user_longitude" property="userLongitude"/>
+        <result column="status" property="status"/>
+        <result column="distance_km" property="distanceKm"/>
+    </resultMap>
+
+    <select id="queryMerchantOrderList" resultMap="OrderPageVOResultMap">
         SELECT
-        o.id,
-        p.project_name AS projectName,
-        p.cover_url AS projectCover,
-        p.duration AS projectDuration,
-        p.base_price AS basePrice,
-        o.traffic_fee AS trafficFee,
-        o.final_amount AS finalAmount,
-        o.appointment_start_time AS appointmentStartTime,
-        o.appointment_end_time AS appointmentEndTime,
-        o.contact_person_name AS contactPersonName,
-        o.contact_phone_number AS contactPhoneNumber,
-        o.service_address_detail AS contactAddressInfo,
-        o.user_latitude AS userLatitude,
-        o.user_longitude AS userLongitude,
-        o.status,
-        -- 计算距离 (如果需要,根据你的数据库类型调整,这里是 MySQL 示例)
-        ST_Distance_Sphere(point(o.user_longitude, o.user_latitude), point(dto.longitude, dto.latitude)) AS distanceKm
+            o.id,
+            p.project_name,
+            p.cover_url AS project_cover,
+            p.duration AS project_duration,
+            p.base_price,
+            o.traffic_fee,
+            o.final_amount,
+            o.appointment_start_time,
+            o.appointment_end_time,
+            o.contact_person_name,
+            IF(o.contact_phone_number IS NULL, '', CONCAT(SUBSTRING(o.contact_phone_number,1,3), '******', SUBSTRING(o.contact_phone_number,10,2))) AS contactPhoneNumber
+            o.service_address_detail,
+            o.user_latitude,
+            o.user_longitude,
+            o.status,
+            CASE
+                WHEN #{dto.longitude} IS NULL OR #{dto.latitude} IS NULL THEN NULL
+                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
         FROM t_order o
         LEFT JOIN t_project p ON o.project_id = p.id
         <where>
             o.is_delete = 0
-            <!-- 商户ID过滤 -->
+            -- 商户ID
             AND o.merchant_id = #{dto.merchantId}
 
-            <!-- 日期范围过滤 -->
+            -- 创建时间范围
             <if test="dto.startDate != null">
                 AND o.create_time &gt;= #{dto.startDate}
             </if>
@@ -341,28 +364,29 @@
                 AND o.create_time &lt;= #{dto.endDate}
             </if>
 
-            <!-- 项目名称模糊搜索 -->
+            -- 项目名称模糊
             <if test="dto.projectName != null and dto.projectName != ''">
                 AND p.project_name LIKE CONCAT('%', #{dto.projectName}, '%')
             </if>
 
-            <!-- 联系人电话号码模糊搜索 -->
+            -- 联系人手机号【修复:订单表字段o,不是项目表p】
             <if test="dto.contactPhoneNumber != null and dto.contactPhoneNumber != ''">
-                AND p.contact_phone_number LIKE CONCAT('%', #{dto.contactPhoneNumber}, '%')
+                AND o.contact_phone_number LIKE CONCAT('%', #{dto.contactPhoneNumber}, '%')
             </if>
 
-            <!-- 状态过滤 -->
+            -- 订单状态
             <if test="dto.status != null">
                 AND o.status = #{dto.status}
             </if>
 
+            -- 是否免交通费
             <if test="dto.isTrafficFree != null">
                 <choose>
                     <when test="dto.isTrafficFree == 0">
-                        AND traffic_fee = 0
+                        AND o.traffic_fee = 0
                     </when>
                     <when test="dto.isTrafficFree == 1">
-                        AND traffic_fee > 0
+                        AND o.traffic_fee &gt; 0
                     </when>
                 </choose>
             </if>