Browse Source

商户端订单流转相关接口开发

wangzhijun 1 week ago
parent
commit
64f05c0aad

+ 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;
     /**
      * 头像
      */

+ 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;
+
+}

+ 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;
+}

+ 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);
 }

+ 264 - 0
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;
@@ -32,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;
@@ -48,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;
 
@@ -114,6 +118,12 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     @Resource
     private TGeoFenceService geoFenceService;
 
+    @Value("${ylx.sendSmsEnabled:false}")
+    private boolean sendSmsEnabled;
+
+    @Value("${ylx.fixedVerifyCode:123456}")
+    private String fixedVerifyCode;
+
     @Override
     public TOrder addOrder(TOrder order) {
         return null;
@@ -1041,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)
@@ -1164,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);
+        }
+    }
+
 }

+ 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>