Sfoglia il codice sorgente

开发订单相关的接口

jinshihui 7 ore fa
parent
commit
86f6261221
20 ha cambiato i file con 1357 aggiunte e 11 eliminazioni
  1. 18 0
      nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/TCommentUserController.java
  2. 45 0
      nightFragrance-massage/src/main/java/com/ylx/order/controller/AdminOrderController.java
  3. 250 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/OrderDispatch.java
  4. 41 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/AdminOrderDispatchDTO.java
  5. 35 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/AdminOrderDispatchMerchantQueryDTO.java
  6. 107 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/AdminOrderDispatchMerchantVO.java
  7. 65 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/AdminOrderDispatchOrderVO.java
  8. 35 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/TCommentUserAuditStatusCountVO.java
  9. 30 0
      nightFragrance-massage/src/main/java/com/ylx/order/mapper/AdminOrderMapper.java
  10. 9 0
      nightFragrance-massage/src/main/java/com/ylx/order/mapper/TCommentUserMapper.java
  11. 7 0
      nightFragrance-massage/src/main/java/com/ylx/order/service/AdminOrderService.java
  12. 9 0
      nightFragrance-massage/src/main/java/com/ylx/order/service/TCommentUserService.java
  13. 191 0
      nightFragrance-massage/src/main/java/com/ylx/order/service/impl/AdminOrderServiceImpl.java
  14. 16 0
      nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TCommentUserServiceImpl.java
  15. 266 0
      nightFragrance-massage/src/main/resources/mapper/order/AdminOrderMapper.xml
  16. 31 10
      nightFragrance-massage/src/main/resources/mapper/order/TCommentUserMapper.xml
  17. 27 0
      nightFragrance-massage/src/test/java/com/ylx/order/mapper/AdminOrderMapperXmlTest.java
  18. 12 1
      nightFragrance-massage/src/test/java/com/ylx/order/mapper/TCommentUserMapperXmlTest.java
  19. 130 0
      nightFragrance-massage/src/test/java/com/ylx/order/service/impl/AdminOrderServiceImplTest.java
  20. 33 0
      nightFragrance-massage/src/test/java/com/ylx/order/service/impl/TCommentUserServiceImplTest.java

+ 18 - 0
nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/TCommentUserController.java

@@ -9,6 +9,7 @@ import com.ylx.common.utils.SecurityUtils;
 import com.ylx.order.service.TCommentUserAuditService;
 import com.ylx.order.service.TCommentUserService;
 import com.ylx.order.domain.TCommentUser;
+import com.ylx.order.domain.vo.TCommentUserAuditStatusCountVO;
 import com.ylx.system.domain.vo.ReviewUserCommentsDto;
 import com.ylx.system.domain.vo.BatchAuditCommentsDto;
 import io.swagger.annotations.Api;
@@ -56,6 +57,23 @@ public class TCommentUserController extends BaseController {
         }
     }
 
+    /**
+     * 查询评论审核状态统计。
+     *
+     * @param tCommentUser 查询参数
+     * @return R<TCommentUserAuditStatusCountVO>
+     */
+    @GetMapping("auditStatusCount")
+    @ApiOperation("查询评论审核状态统计")
+    public R<TCommentUserAuditStatusCountVO> auditStatusCount(TCommentUser tCommentUser) {
+        try {
+            return R.ok(tCommentUserService.selectAuditStatusCount(tCommentUser));
+        } catch (Exception e) {
+            logger.error("查询评论审核状态统计失败", e);
+            return R.fail("查询评论审核状态统计失败");
+        }
+    }
+
     /**
      * 通过主键查询单条评论数据
      *

+ 45 - 0
nightFragrance-massage/src/main/java/com/ylx/order/controller/AdminOrderController.java

@@ -3,8 +3,11 @@ package com.ylx.order.controller;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.core.domain.R;
 import com.ylx.common.exception.ServiceException;
+import com.ylx.order.domain.dto.AdminOrderDispatchDTO;
+import com.ylx.order.domain.dto.AdminOrderDispatchMerchantQueryDTO;
 import com.ylx.order.domain.dto.AdminOrderQueryDTO;
 import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderDispatchMerchantVO;
 import com.ylx.order.domain.vo.AdminOrderPageVO;
 import com.ylx.order.domain.vo.AdminOrderServiceCategoryVO;
 import com.ylx.order.service.AdminOrderService;
@@ -15,6 +18,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -166,4 +170,45 @@ public class AdminOrderController {
             return R.fail("查询后台订单详情失败");
         }
     }
+
+    /**
+     * 查询订单派单候选商户列表
+     *
+     * @param id 订单ID
+     * @param dto 查询参数
+     * @return R<List<AdminOrderDispatchMerchantVO>>
+     */
+    @GetMapping("/{id}/dispatch/merchant/list")
+    @ApiOperation("查询订单派单候选商户列表")
+    public R<List<AdminOrderDispatchMerchantVO>> dispatchMerchantList(@PathVariable("id") Long id, AdminOrderDispatchMerchantQueryDTO dto) {
+        try {
+            return R.ok(adminOrderService.listDispatchMerchants(id, dto));
+        } catch (ServiceException e) {
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("查询订单派单候选商户列表异常,订单ID:{}", id, e);
+            return R.fail("查询派单候选商户列表失败");
+        }
+    }
+
+    /**
+     * 确认派单
+     *
+     * @param id 订单ID
+     * @param dto 派单参数
+     * @return R<?>
+     */
+    @PostMapping("/{id}/dispatch")
+    @ApiOperation("确认派单")
+    public R<?> dispatch(@PathVariable("id") Long id, @RequestBody AdminOrderDispatchDTO dto) {
+        try {
+            adminOrderService.dispatch(id, dto);
+            return R.ok("派单成功");
+        } catch (ServiceException e) {
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("确认派单异常,订单ID:{}", id, e);
+            return R.fail("确认派单失败");
+        }
+    }
 }

+ 250 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/OrderDispatch.java

@@ -0,0 +1,250 @@
+package com.ylx.order.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 订单派单记录表
+ *
+ * @author ylx
+ */
+@Data
+@TableName(value = "t_order_dispatch", autoResultMap = true)
+@ApiModel(value = "OrderDispatch", description = "订单派单记录表")
+public class OrderDispatch implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID。
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    /**
+     * 订单ID,关联订单表 t_order.id。
+     */
+    @ApiModelProperty("订单ID,关联t_order.id")
+    private Long orderId;
+
+    /**
+     * 订单编号快照。
+     */
+    @ApiModelProperty("订单号快照")
+    private String orderNo;
+
+    /**
+     * 被派单商户ID,关联商户表 ma_technician.id。
+     */
+    @ApiModelProperty("被派单商户ID,关联ma_technician.id")
+    private Integer merchantId;
+
+    /**
+     * 派单时的商户姓名快照。
+     */
+    @ApiModelProperty("商户姓名快照")
+    private String merchantName;
+
+    /**
+     * 派单时的商户昵称快照。
+     */
+    @ApiModelProperty("商户昵称快照")
+    private String merchantNickName;
+
+    /**
+     * 派单时的商户手机号快照。
+     */
+    @ApiModelProperty("商户手机号快照")
+    private String merchantPhone;
+
+    /**
+     * 派单时的商户头像快照。
+     */
+    @ApiModelProperty("商户头像快照")
+    private String merchantAvatar;
+
+    /**
+     * 商户类型:0真实商户,1虚拟商户。
+     */
+    @ApiModelProperty("商户类型:0真实商户 1虚拟商户")
+    private Integer merchantType;
+
+    /**
+     * 订单服务项目ID快照。
+     */
+    @ApiModelProperty("订单服务项目ID快照")
+    private Long projectId;
+
+    /**
+     * 订单服务项目名称快照。
+     */
+    @ApiModelProperty("订单服务项目名称快照")
+    private String projectName;
+
+    /**
+     * 服务类目ID快照。
+     */
+    @ApiModelProperty("服务类目ID快照")
+    private Integer categoryId;
+
+    /**
+     * 派单城市编码快照。
+     */
+    @ApiModelProperty("派单城市编码快照")
+    private String cityCode;
+
+    /**
+     * 用户下单纬度快照。
+     */
+    @ApiModelProperty("用户下单纬度快照")
+    private BigDecimal userLatitude;
+
+    /**
+     * 用户下单经度快照。
+     */
+    @ApiModelProperty("用户下单经度快照")
+    private BigDecimal userLongitude;
+
+    /**
+     * 派单时商户纬度快照。
+     */
+    @ApiModelProperty("商户纬度快照")
+    private BigDecimal merchantLatitude;
+
+    /**
+     * 派单时商户经度快照。
+     */
+    @ApiModelProperty("商户经度快照")
+    private BigDecimal merchantLongitude;
+
+    /**
+     * 派单时商户距离用户的直线距离,单位米。
+     */
+    @ApiModelProperty("派单时商户距离用户的直线距离,单位米")
+    private BigDecimal distanceMeters;
+
+    /**
+     * 派单状态:1已派单待接单,2已接单,3已拒单,4已取消,5已转派。
+     */
+    @ApiModelProperty("派单状态:1已派单待接单 2已接单 3已拒单 4已取消 5已转派")
+    private Integer dispatchStatus;
+
+    /**
+     * 派单来源:1后台手动,2系统自动。
+     */
+    @ApiModelProperty("派单来源:1后台手动 2系统自动")
+    private Integer dispatchSource;
+
+    /**
+     * 是否当前有效派单:1是,NULL否。
+     */
+    @ApiModelProperty("是否当前有效派单:1是 NULL否")
+    private Integer currentFlag;
+
+    /**
+     * 派单前订单状态。
+     */
+    @ApiModelProperty("派单前订单状态")
+    private Integer orderStatusBefore;
+
+    /**
+     * 派单后订单状态。
+     */
+    @ApiModelProperty("派单后订单状态")
+    private Integer orderStatusAfter;
+
+    /**
+     * 派单时间。
+     */
+    @ApiModelProperty("派单时间")
+    private LocalDateTime dispatchTime;
+
+    /**
+     * 商户接单时间。
+     */
+    @ApiModelProperty("商户接单时间")
+    private LocalDateTime acceptTime;
+
+    /**
+     * 商户拒单时间。
+     */
+    @ApiModelProperty("商户拒单时间")
+    private LocalDateTime rejectTime;
+
+    /**
+     * 取消派单时间。
+     */
+    @ApiModelProperty("取消派单时间")
+    private LocalDateTime cancelTime;
+
+    /**
+     * 商户拒单原因。
+     */
+    @ApiModelProperty("拒单原因")
+    private String rejectReason;
+
+    /**
+     * 取消派单原因。
+     */
+    @ApiModelProperty("取消派单原因")
+    private String cancelReason;
+
+    /**
+     * 派单备注。
+     */
+    @ApiModelProperty("备注")
+    private String remark;
+
+    /**
+     * 后台操作人ID。
+     */
+    @ApiModelProperty("后台操作人ID")
+    private Long operatorId;
+
+    /**
+     * 后台操作人名称。
+     */
+    @ApiModelProperty("后台操作人名称")
+    private String operatorName;
+
+    /**
+     * 创建者。
+     */
+    @ApiModelProperty("创建者")
+    private String createBy;
+
+    /**
+     * 创建时间。
+     */
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者。
+     */
+    @ApiModelProperty("更新者")
+    private String updateBy;
+
+    /**
+     * 更新时间。
+     */
+    @ApiModelProperty("更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志:0存在,1删除。
+     */
+    @TableLogic
+    @ApiModelProperty("删除标志:0存在 1删除")
+    private Integer isDelete;
+}

+ 41 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/AdminOrderDispatchDTO.java

@@ -0,0 +1,41 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 后台订单确认派单参数
+ *
+ * @author ylx
+ */
+@Data
+@ApiModel("后台订单确认派单参数")
+public class AdminOrderDispatchDTO {
+
+    /**
+     * 商户ID。
+     */
+    @ApiModelProperty("商户ID")
+    private Integer merchantId;
+
+    /**
+     * 派单城市编码。
+     */
+    @ApiModelProperty("派单城市编码")
+    private String cityCode;
+
+    /**
+     * 查询半径,单位米;为空时默认 10000 米。
+     */
+    @ApiModelProperty("查询半径,单位米;为空时默认10000米")
+    private BigDecimal radiusMeters;
+
+    /**
+     * 派单备注。
+     */
+    @ApiModelProperty("派单备注")
+    private String remark;
+}

+ 35 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/AdminOrderDispatchMerchantQueryDTO.java

@@ -0,0 +1,35 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 后台订单派单候选商户查询参数
+ *
+ * @author ylx
+ */
+@Data
+@ApiModel("后台订单派单候选商户查询参数")
+public class AdminOrderDispatchMerchantQueryDTO {
+
+    /**
+     * 城市编码。
+     */
+    @ApiModelProperty("城市编码")
+    private String cityCode;
+
+    /**
+     * 商户姓名、昵称或手机号关键字。
+     */
+    @ApiModelProperty("商户姓名、昵称或手机号关键字")
+    private String keyword;
+
+    /**
+     * 查询半径,单位米;为空时默认 10000 米。
+     */
+    @ApiModelProperty("查询半径,单位米;为空时默认10000米")
+    private BigDecimal radiusMeters;
+}

+ 107 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/AdminOrderDispatchMerchantVO.java

@@ -0,0 +1,107 @@
+package com.ylx.order.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 后台订单派单候选商户
+ *
+ * @author ylx
+ */
+@Data
+@ApiModel("后台订单派单候选商户")
+public class AdminOrderDispatchMerchantVO {
+
+    /**
+     * 商户ID。
+     */
+    @ApiModelProperty("商户ID")
+    private Integer merchantId;
+
+    /**
+     * 商户姓名。
+     */
+    @ApiModelProperty("商户姓名")
+    private String merchantName;
+
+    /**
+     * 商户昵称。
+     */
+    @ApiModelProperty("商户昵称")
+    private String merchantNickName;
+
+    /**
+     * 商户手机号。
+     */
+    @ApiModelProperty("商户手机号")
+    private String merchantPhone;
+
+    /**
+     * 商户头像。
+     */
+    @ApiModelProperty("商户头像")
+    private String merchantAvatar;
+
+    /**
+     * 商户类型:0真实商户,1虚拟商户。
+     */
+    @ApiModelProperty("商户类型:0真实商户 1虚拟商户")
+    private Integer merchantType;
+
+    /**
+     * 商户接单状态:0休息中,1在线接单。
+     */
+    @ApiModelProperty("商户接单状态:0休息中 1在线接单")
+    private Integer postState;
+
+    /**
+     * 商户接单状态名称。
+     */
+    @ApiModelProperty("商户接单状态名称")
+    private String postStateName;
+
+    /**
+     * 商户服务状态。
+     */
+    @ApiModelProperty("商户服务状态")
+    private Integer serviceState;
+
+    /**
+     * 商户服务状态名称。
+     */
+    @ApiModelProperty("商户服务状态名称")
+    private String serviceStateName;
+
+    /**
+     * 商户定位地址。
+     */
+    @ApiModelProperty("商户定位地址")
+    private String merchantAddress;
+
+    /**
+     * 商户纬度。
+     */
+    @ApiModelProperty("商户纬度")
+    private BigDecimal merchantLatitude;
+
+    /**
+     * 商户经度。
+     */
+    @ApiModelProperty("商户经度")
+    private BigDecimal merchantLongitude;
+
+    /**
+     * 商户距用户下单地址距离,单位米。
+     */
+    @ApiModelProperty("商户距用户下单地址距离,单位米")
+    private BigDecimal distanceMeters;
+
+    /**
+     * 距离展示文案。
+     */
+    @ApiModelProperty("距离展示文案")
+    private String distanceShow;
+}

+ 65 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/AdminOrderDispatchOrderVO.java

@@ -0,0 +1,65 @@
+package com.ylx.order.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 后台订单派单订单上下文
+ *
+ * @author ylx
+ */
+@Data
+@ApiModel("后台订单派单订单上下文")
+public class AdminOrderDispatchOrderVO {
+
+    /**
+     * 订单ID。
+     */
+    @ApiModelProperty("订单ID")
+    private Long orderId;
+
+    /**
+     * 订单号。
+     */
+    @ApiModelProperty("订单号")
+    private String orderNo;
+
+    /**
+     * 订单状态。
+     */
+    @ApiModelProperty("订单状态")
+    private Integer status;
+
+    /**
+     * 服务项目ID。
+     */
+    @ApiModelProperty("服务项目ID")
+    private Long projectId;
+
+    /**
+     * 服务项目名称。
+     */
+    @ApiModelProperty("服务项目名称")
+    private String projectName;
+
+    /**
+     * 服务类目ID。
+     */
+    @ApiModelProperty("服务类目ID")
+    private Integer categoryId;
+
+    /**
+     * 用户下单纬度。
+     */
+    @ApiModelProperty("用户下单纬度")
+    private BigDecimal userLatitude;
+
+    /**
+     * 用户下单经度。
+     */
+    @ApiModelProperty("用户下单经度")
+    private BigDecimal userLongitude;
+}

+ 35 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/TCommentUserAuditStatusCountVO.java

@@ -0,0 +1,35 @@
+package com.ylx.order.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 用户评论审核状态统计。
+ */
+@Data
+@ApiModel(value = "TCommentUserAuditStatusCountVO", description = "用户评论审核状态统计")
+public class TCommentUserAuditStatusCountVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 待审核总数。
+     */
+    @ApiModelProperty("待审核总数")
+    private Long pendingCount;
+
+    /**
+     * 审核通过总数。
+     */
+    @ApiModelProperty("审核通过总数")
+    private Long approvedCount;
+
+    /**
+     * 审核驳回总数。
+     */
+    @ApiModelProperty("审核驳回总数")
+    private Long rejectedCount;
+}

+ 30 - 0
nightFragrance-massage/src/main/java/com/ylx/order/mapper/AdminOrderMapper.java

@@ -2,8 +2,12 @@ package com.ylx.order.mapper;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.massage.domain.CancelOrderApplication;
+import com.ylx.order.domain.OrderDispatch;
+import com.ylx.order.domain.dto.AdminOrderDispatchMerchantQueryDTO;
 import com.ylx.order.domain.dto.AdminOrderQueryDTO;
 import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderDispatchMerchantVO;
+import com.ylx.order.domain.vo.AdminOrderDispatchOrderVO;
 import com.ylx.order.domain.vo.AdminOrderPageVO;
 import com.ylx.order.domain.vo.AdminOrderServiceCategoryVO;
 import org.apache.ibatis.annotations.Mapper;
@@ -35,4 +39,30 @@ public interface AdminOrderMapper {
     AdminOrderDetailVO.ProjectInfoVO selectOrderProjectDetailById(@Param("id") Long id);
 
     List<AdminOrderDetailVO.OperationLogVO> selectOrderOperationLogs(@Param("id") Long id);
+
+    AdminOrderDispatchOrderVO selectDispatchOrderById(@Param("id") Long id);
+
+    List<AdminOrderDispatchMerchantVO> selectDispatchMerchantCandidates(@Param("order") AdminOrderDispatchOrderVO order, @Param("query") AdminOrderDispatchMerchantQueryDTO query);
+
+    AdminOrderDispatchMerchantVO selectDispatchMerchantById(@Param("order") AdminOrderDispatchOrderVO order,
+                                                            @Param("merchantId") Integer merchantId,
+                                                            @Param("radiusMeters") java.math.BigDecimal radiusMeters);
+
+    /**
+     * 查询订单当前派单状态
+     * @param orderId
+     * @return int 订单当前派单状态
+     */
+    int countCurrentDispatchByOrderId(@Param("orderId") Long orderId);
+
+    int insertOrderDispatch(OrderDispatch dispatch);
+
+    int updateOrderDispatch(@Param("orderId") Long orderId,
+                            @Param("merchant") AdminOrderDispatchMerchantVO merchant,
+                            @Param("statusBefore") Integer statusBefore,
+                            @Param("statusAfter") Integer statusAfter);
+
+    int insertOrderStatusFlow(@Param("orderId") Long orderId,
+                              @Param("status") Integer status,
+                              @Param("operator") String operator);
 }

+ 9 - 0
nightFragrance-massage/src/main/java/com/ylx/order/mapper/TCommentUserMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Param;
 import com.ylx.order.domain.TCommentUser;
+import com.ylx.order.domain.vo.TCommentUserAuditStatusCountVO;
 
 import java.util.List;
 
@@ -40,5 +41,13 @@ public interface TCommentUserMapper extends BaseMapper<TCommentUser> {
      */
      Page<TCommentUser> selectAll(Page<TCommentUser> page, @Param("tCommentUser") TCommentUser tCommentUser);
 
+    /**
+     * 查询用户评论审核状态统计。
+     *
+     * @param tCommentUser 查询参数
+     * @return 用户评论审核状态统计
+     */
+    TCommentUserAuditStatusCountVO selectAuditStatusCount(@Param("tCommentUser") TCommentUser tCommentUser);
+
 }
 

+ 7 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/AdminOrderService.java

@@ -1,8 +1,11 @@
 package com.ylx.order.service;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.order.domain.dto.AdminOrderDispatchDTO;
+import com.ylx.order.domain.dto.AdminOrderDispatchMerchantQueryDTO;
 import com.ylx.order.domain.dto.AdminOrderQueryDTO;
 import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderDispatchMerchantVO;
 import com.ylx.order.domain.vo.AdminOrderPageVO;
 import com.ylx.order.domain.vo.AdminOrderServiceCategoryVO;
 
@@ -23,4 +26,8 @@ public interface AdminOrderService {
     void applyRefund(Long id);
 
     AdminOrderDetailVO detail(Long id);
+
+    List<AdminOrderDispatchMerchantVO> listDispatchMerchants(Long id, AdminOrderDispatchMerchantQueryDTO dto);
+
+    void dispatch(Long id, AdminOrderDispatchDTO dto);
 }

+ 9 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/TCommentUserService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.ylx.common.core.domain.model.WxLoginUser;
 import com.ylx.order.domain.TCommentUser;
 import com.ylx.order.domain.dto.OrderCommentDTO;
+import com.ylx.order.domain.vo.TCommentUserAuditStatusCountVO;
 
 /**
  * 用户评论表(TCommentUser)表服务接口
@@ -31,6 +32,14 @@ public interface TCommentUserService extends IService<TCommentUser> {
      */
      Page<TCommentUser> selectAll(Page<TCommentUser> page, TCommentUser tCommentUser);
 
+    /**
+     * 查询用户评论审核状态统计。
+     *
+     * @param tCommentUser 查询参数
+     * @return 用户评论审核状态统计
+     */
+    TCommentUserAuditStatusCountVO selectAuditStatusCount(TCommentUser tCommentUser);
+
     /**
      * 提交评论(入库+生成审核记录)
      * @param dto 评论DTO

+ 191 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/AdminOrderServiceImpl.java

@@ -1,20 +1,29 @@
 package com.ylx.order.service.impl;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.common.core.domain.model.LoginUser;
 import com.ylx.common.exception.ServiceException;
 import com.ylx.massage.domain.CancelOrderApplication;
+import com.ylx.order.domain.OrderDispatch;
+import com.ylx.order.domain.dto.AdminOrderDispatchDTO;
+import com.ylx.order.domain.dto.AdminOrderDispatchMerchantQueryDTO;
 import com.ylx.order.domain.dto.AdminOrderQueryDTO;
 import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderDispatchMerchantVO;
+import com.ylx.order.domain.vo.AdminOrderDispatchOrderVO;
 import com.ylx.order.domain.vo.AdminOrderPageVO;
 import com.ylx.order.domain.vo.AdminOrderServiceCategoryVO;
 import com.ylx.order.enums.AfterSaleServiceStatusEnum;
 import com.ylx.order.mapper.AdminOrderMapper;
 import com.ylx.order.service.AdminOrderService;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
 import javax.annotation.Resource;
+import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -38,6 +47,13 @@ public class AdminOrderServiceImpl implements AdminOrderService {
     private static final String USER_SEARCH_NAME = "name";
     private static final String MERCHANT_SEARCH_NICK_NAME = "nickName";
     private static final String MERCHANT_SEARCH_PHONE = "phone";
+    private static final BigDecimal DEFAULT_DISPATCH_RADIUS_METERS = new BigDecimal("10000");
+    private static final Integer ORDER_STATUS_PENDING_DISPATCH = 1;
+    private static final Integer ORDER_STATUS_PENDING_ACCEPT = 2;
+    private static final Integer DISPATCH_STATUS_PENDING_ACCEPT = 1;
+    private static final Integer DISPATCH_SOURCE_ADMIN = 1;
+    private static final Integer CURRENT_DISPATCH = 1;
+    private static final Integer NOT_DELETED = 0;
 
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -157,6 +173,176 @@ public class AdminOrderServiceImpl implements AdminOrderService {
         return detail;
     }
 
+    @Override
+    public List<AdminOrderDispatchMerchantVO> listDispatchMerchants(Long id, AdminOrderDispatchMerchantQueryDTO dto) {
+        AdminOrderDispatchOrderVO order = getDispatchOrder(id);
+        validatePendingDispatchOrder(order);
+        validateDispatchOrderLocation(order);
+
+        AdminOrderDispatchMerchantQueryDTO query = normalizeDispatchMerchantQuery(dto);
+        return adminOrderMapper.selectDispatchMerchantCandidates(order, query);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void dispatch(Long id, AdminOrderDispatchDTO dto) {
+        if (dto == null || dto.getMerchantId() == null) {
+            throw new ServiceException("请选择商户");
+        }
+        if (dto.getMerchantId() <= 0) {
+            throw new ServiceException("商户ID不正确");
+        }
+
+        AdminOrderDispatchOrderVO order = getDispatchOrder(id);
+        validatePendingDispatchOrder(order);
+        validateDispatchOrderLocation(order);
+
+        BigDecimal radiusMeters = normalizeDispatchRadius(dto.getRadiusMeters());
+        AdminOrderDispatchMerchantVO merchant = adminOrderMapper.selectDispatchMerchantById(order, dto.getMerchantId(), radiusMeters);
+        if (merchant == null) {
+            throw new ServiceException("请选择可派单商户");
+        }
+
+        if (adminOrderMapper.countCurrentDispatchByOrderId(id) > 0) {
+            throw new ServiceException("该订单已存在有效派单记录,请刷新重试");
+        }
+
+        int orderRows = adminOrderMapper.updateOrderDispatch(id, merchant, ORDER_STATUS_PENDING_DISPATCH, ORDER_STATUS_PENDING_ACCEPT);
+        if (orderRows <= 0) {
+            throw new ServiceException("操作失败,请刷新重试");
+        }
+
+        OrderDispatch dispatch = buildOrderDispatch(order, merchant, dto);
+        int dispatchRows = adminOrderMapper.insertOrderDispatch(dispatch);
+        if (dispatchRows <= 0) {
+            throw new ServiceException("保存派单记录失败");
+        }
+
+        int flowRows = adminOrderMapper.insertOrderStatusFlow(id, ORDER_STATUS_PENDING_ACCEPT, getCurrentOperator());
+        if (flowRows <= 0) {
+            throw new ServiceException("保存订单状态流转失败");
+        }
+    }
+
+    /**
+     * 获取待派单订单详情
+     * @param id
+     * @return AdminOrderDispatchOrderVO
+     */
+    private AdminOrderDispatchOrderVO getDispatchOrder(Long id) {
+        validateOrderId(id);
+        AdminOrderDispatchOrderVO order = adminOrderMapper.selectDispatchOrderById(id);
+        if (order == null) {
+            throw new ServiceException("订单不存在或已删除");
+        }
+        return order;
+    }
+
+    /**
+     * 验证待派单订单的状态
+     *
+     * @param order
+     */
+    private void validatePendingDispatchOrder(AdminOrderDispatchOrderVO order) {
+        if (!ORDER_STATUS_PENDING_DISPATCH.equals(order.getStatus())) {
+            throw new ServiceException("当前订单状态不是待派单状态,请刷新重试");
+        }
+    }
+
+    private void validateDispatchOrderLocation(AdminOrderDispatchOrderVO order) {
+        if (order.getUserLatitude() == null || order.getUserLongitude() == null) {
+            throw new ServiceException("订单下单坐标不能为空");
+        }
+    }
+
+    /**
+     * 构建派单商户查询参数,处理默认值和空值
+     *
+     * @param dto
+     * @return AdminOrderDispatchMerchantQueryDTO
+     */
+    private AdminOrderDispatchMerchantQueryDTO normalizeDispatchMerchantQuery(AdminOrderDispatchMerchantQueryDTO dto) {
+        AdminOrderDispatchMerchantQueryDTO query = dto == null ? new AdminOrderDispatchMerchantQueryDTO() : dto;
+        query.setRadiusMeters(normalizeDispatchRadius(query.getRadiusMeters()));
+        if (StringUtils.hasText(query.getKeyword())) {
+            query.setKeyword(query.getKeyword().trim());
+        } else {
+            query.setKeyword(null);
+        }
+        if (StringUtils.hasText(query.getCityCode())) {
+            query.setCityCode(query.getCityCode().trim());
+        } else {
+            query.setCityCode(null);
+        }
+        return query;
+    }
+
+    /**
+     * 处理派单半径,默认值为1000米
+     *
+     * @param radiusMeters
+     * @return BigDecimal
+     */
+    private BigDecimal normalizeDispatchRadius(BigDecimal radiusMeters) {
+        if (radiusMeters == null || radiusMeters.compareTo(BigDecimal.ZERO) <= 0) {
+            return DEFAULT_DISPATCH_RADIUS_METERS;
+        }
+        return radiusMeters;
+    }
+
+    private OrderDispatch buildOrderDispatch(AdminOrderDispatchOrderVO order, AdminOrderDispatchMerchantVO merchant, AdminOrderDispatchDTO dto) {
+        LocalDateTime now = LocalDateTime.now();
+        String operator = getCurrentOperator();
+        OrderDispatch dispatch = new OrderDispatch();
+        dispatch.setOrderId(order.getOrderId());
+        dispatch.setOrderNo(order.getOrderNo());
+        dispatch.setMerchantId(merchant.getMerchantId());
+        dispatch.setMerchantName(merchant.getMerchantName());
+        dispatch.setMerchantNickName(merchant.getMerchantNickName());
+        dispatch.setMerchantPhone(merchant.getMerchantPhone());
+        dispatch.setMerchantAvatar(merchant.getMerchantAvatar());
+        dispatch.setMerchantType(merchant.getMerchantType());
+        dispatch.setProjectId(order.getProjectId());
+        dispatch.setProjectName(order.getProjectName());
+        dispatch.setCategoryId(order.getCategoryId());
+        dispatch.setCityCode(StringUtils.hasText(dto.getCityCode()) ? dto.getCityCode().trim() : null);
+        dispatch.setUserLatitude(order.getUserLatitude());
+        dispatch.setUserLongitude(order.getUserLongitude());
+        dispatch.setMerchantLatitude(merchant.getMerchantLatitude());
+        dispatch.setMerchantLongitude(merchant.getMerchantLongitude());
+        dispatch.setDistanceMeters(merchant.getDistanceMeters());
+        dispatch.setDispatchStatus(DISPATCH_STATUS_PENDING_ACCEPT);
+        dispatch.setDispatchSource(DISPATCH_SOURCE_ADMIN);
+        dispatch.setCurrentFlag(CURRENT_DISPATCH);
+        dispatch.setOrderStatusBefore(ORDER_STATUS_PENDING_DISPATCH);
+        dispatch.setOrderStatusAfter(ORDER_STATUS_PENDING_ACCEPT);
+        dispatch.setDispatchTime(now);
+        dispatch.setRemark(StringUtils.hasText(dto.getRemark()) ? dto.getRemark().trim() : null);
+        dispatch.setOperatorName(operator);
+        dispatch.setCreateBy(operator);
+        dispatch.setCreateTime(now);
+        dispatch.setUpdateBy(operator);
+        dispatch.setUpdateTime(now);
+        dispatch.setIsDelete(NOT_DELETED);
+        return dispatch;
+    }
+
+    private String getCurrentOperator() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null || authentication.getPrincipal() == null) {
+            return "系统";
+        }
+        Object principal = authentication.getPrincipal();
+        if (principal instanceof LoginUser) {
+            String username = ((LoginUser) principal).getUsername();
+            return StringUtils.hasText(username) ? username : "系统";
+        }
+        if (principal instanceof String && StringUtils.hasText((String) principal)) {
+            return (String) principal;
+        }
+        return "系统";
+    }
+
     /**
      * 填充订单详情显示字段
      * @param orderInfo
@@ -195,6 +381,11 @@ public class AdminOrderServiceImpl implements AdminOrderService {
                 .collect(Collectors.toList());
     }
 
+    /**
+     * 验证订单ID
+     *
+     * @param id
+     */
     private void validateOrderId(Long id) {
         if (id == null) {
             throw new ServiceException("订单ID不能为空");

+ 16 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TCommentUserServiceImpl.java

@@ -12,6 +12,7 @@ import com.ylx.order.domain.TCommentUser;
 import com.ylx.order.mapper.TCommentUserMapper;
 import com.ylx.order.service.TCommentUserService;
 import com.ylx.order.domain.dto.OrderCommentDTO;
+import com.ylx.order.domain.vo.TCommentUserAuditStatusCountVO;
 import com.ylx.order.enums.AuditStatusEnum;
 import com.ylx.order.service.TCommentPictureService;
 import lombok.extern.slf4j.Slf4j;
@@ -52,6 +53,13 @@ public class TCommentUserServiceImpl extends ServiceImpl<TCommentUserMapper, TCo
         return tCommentUserMapper.selectAll(pageParam, query);
     }
 
+    @Override
+    public TCommentUserAuditStatusCountVO selectAuditStatusCount(TCommentUser tCommentUser) {
+        TCommentUser query = tCommentUser == null ? new TCommentUser() : tCommentUser;
+        TCommentUserAuditStatusCountVO countVO = tCommentUserMapper.selectAuditStatusCount(query);
+        return countVO == null ? buildEmptyAuditStatusCount() : countVO;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void submitComment(OrderCommentDTO dto) {
@@ -141,4 +149,12 @@ public class TCommentUserServiceImpl extends ServiceImpl<TCommentUserMapper, TCo
             throw new IllegalArgumentException(fieldName + "必须在1~5分之间");
         }
     }
+
+    private TCommentUserAuditStatusCountVO buildEmptyAuditStatusCount() {
+        TCommentUserAuditStatusCountVO countVO = new TCommentUserAuditStatusCountVO();
+        countVO.setPendingCount(0L);
+        countVO.setApprovedCount(0L);
+        countVO.setRejectedCount(0L);
+        return countVO;
+    }
 }

+ 266 - 0
nightFragrance-massage/src/main/resources/mapper/order/AdminOrderMapper.xml

@@ -371,4 +371,270 @@
         WHERE osf.order_id = #{id}
         ORDER BY osf.create_time ASC
     </select>
+
+    <select id="selectDispatchOrderById" resultType="com.ylx.order.domain.vo.AdminOrderDispatchOrderVO">
+        SELECT
+            o.id AS orderId,
+            o.order_no AS orderNo,
+            o.status AS status,
+            o.project_id AS projectId,
+            COALESCE(p.title, o.project_name) AS projectName,
+            p.category_id AS categoryId,
+            o.user_latitude AS userLatitude,
+            o.user_longitude AS userLongitude
+        FROM t_order o
+        LEFT JOIN project p ON p.id = o.project_id AND p.is_delete = 0
+        WHERE o.id = #{id}
+          AND o.is_delete = 0
+        LIMIT 1
+    </select>
+
+    <!-- 查询派单商户候选 -->
+    <select id="selectDispatchMerchantCandidates" resultType="com.ylx.order.domain.vo.AdminOrderDispatchMerchantVO">
+        SELECT
+            candidate.*,
+            CASE
+                WHEN candidate.distance_meters &lt; 1000 THEN CONCAT(ROUND(candidate.distance_meters, 0), 'm')
+                ELSE CONCAT(ROUND(candidate.distance_meters / 1000, 1), 'km')
+            END AS distanceShow
+        FROM (
+            SELECT
+                t.id AS merchantId,
+                t.te_name AS merchantName,
+                t.te_nick_name AS merchantNickName,
+                t.te_phone AS merchantPhone,
+                COALESCE(t.te_avatar, t.avatar) AS merchantAvatar,
+                t.tech_type AS merchantType,
+                t.post_state AS postState,
+                CASE t.post_state
+                    WHEN 1 THEN '在线接单'
+                    WHEN 0 THEN '休息中'
+                    ELSE ''
+                END AS postStateName,
+                t.service_state AS serviceState,
+                CASE t.service_state
+                    WHEN 0 THEN '服务中'
+                    WHEN 1 THEN '待接单'
+                    WHEN 2 THEN '休息中'
+                    ELSE ''
+                END AS serviceStateName,
+                COALESCE(addr.detail_address, addr.address, addr.atlas_add, t.te_address) AS merchantAddress,
+                addr.latitude AS merchantLatitude,
+                addr.longitude AS merchantLongitude,
+                ROUND(ST_Distance_Sphere(POINT(addr.longitude, addr.latitude), POINT(#{order.userLongitude}, #{order.userLatitude})), 2) AS distance_meters
+            FROM ma_technician t
+            INNER JOIN ma_project mp ON mp.merchant_id = t.id
+                AND mp.is_delete = 0
+                AND mp.audit_status = 1
+                AND mp.project_is_enable = 1
+            INNER JOIN project p ON p.id = mp.project_id
+                AND p.is_delete = 0
+            INNER JOIN t_address addr ON addr.merchant_id = t.id
+                AND addr.user_type = 2
+                AND addr.type = 1
+                AND addr.is_delete = 0
+            WHERE t.is_delete = 0
+              AND t.audit_status = 2
+              AND t.n_status2 = 0
+              AND t.merchant_status = '0'
+              AND addr.longitude IS NOT NULL
+              AND addr.latitude IS NOT NULL
+              <choose>
+                  <when test="order.categoryId != null">
+                      AND p.category_id = #{order.categoryId}
+                  </when>
+                  <otherwise>
+                      AND mp.project_id = #{order.projectId}
+                  </otherwise>
+              </choose>
+              <if test="query.cityCode != null and query.cityCode != ''">
+                  AND t.te_area_code = #{query.cityCode}
+              </if>
+              <if test="query.keyword != null and query.keyword != ''">
+                  AND (
+                      t.te_name LIKE CONCAT('%', #{query.keyword}, '%')
+                      OR t.te_nick_name LIKE CONCAT('%', #{query.keyword}, '%')
+                      OR t.te_phone LIKE CONCAT('%', #{query.keyword}, '%')
+                  )
+              </if>
+        ) candidate
+        WHERE candidate.distance_meters &lt;= #{query.radiusMeters}
+        ORDER BY candidate.distance_meters ASC, candidate.merchantId ASC
+    </select>
+
+    <select id="selectDispatchMerchantById" resultType="com.ylx.order.domain.vo.AdminOrderDispatchMerchantVO">
+        SELECT
+            candidate.*,
+            CASE
+                WHEN candidate.distance_meters &lt; 1000 THEN CONCAT(ROUND(candidate.distance_meters, 0), 'm')
+                ELSE CONCAT(ROUND(candidate.distance_meters / 1000, 1), 'km')
+            END AS distanceShow
+        FROM (
+            SELECT
+                t.id AS merchantId,
+                t.te_name AS merchantName,
+                t.te_nick_name AS merchantNickName,
+                t.te_phone AS merchantPhone,
+                COALESCE(t.te_avatar, t.avatar) AS merchantAvatar,
+                t.tech_type AS merchantType,
+                t.post_state AS postState,
+                CASE t.post_state
+                    WHEN 1 THEN '在线接单'
+                    WHEN 0 THEN '休息中'
+                    ELSE ''
+                END AS postStateName,
+                t.service_state AS serviceState,
+                CASE t.service_state
+                    WHEN 0 THEN '服务中'
+                    WHEN 1 THEN '待接单'
+                    WHEN 2 THEN '休息中'
+                    ELSE ''
+                END AS serviceStateName,
+                COALESCE(addr.detail_address, addr.address, addr.atlas_add, t.te_address) AS merchantAddress,
+                addr.latitude AS merchantLatitude,
+                addr.longitude AS merchantLongitude,
+                ROUND(ST_Distance_Sphere(POINT(addr.longitude, addr.latitude), POINT(#{order.userLongitude}, #{order.userLatitude})), 2) AS distance_meters
+            FROM ma_technician t
+            INNER JOIN ma_project mp ON mp.merchant_id = t.id
+                AND mp.is_delete = 0
+                AND mp.audit_status = 1
+                AND mp.project_is_enable = 1
+            INNER JOIN project p ON p.id = mp.project_id
+                AND p.is_delete = 0
+            INNER JOIN t_address addr ON addr.merchant_id = t.id
+                AND addr.user_type = 2
+                AND addr.type = 1
+                AND addr.is_delete = 0
+            WHERE t.id = #{merchantId}
+              AND t.is_delete = 0
+              AND t.audit_status = 2
+              AND t.n_status2 = 0
+              AND t.merchant_status = '0'
+              AND addr.longitude IS NOT NULL
+              AND addr.latitude IS NOT NULL
+              <choose>
+                  <when test="order.categoryId != null">
+                      AND p.category_id = #{order.categoryId}
+                  </when>
+                  <otherwise>
+                      AND mp.project_id = #{order.projectId}
+                  </otherwise>
+              </choose>
+        ) candidate
+        WHERE candidate.distance_meters &lt;= #{radiusMeters}
+        LIMIT 1
+    </select>
+
+    <!-- 查询订单当前派单状态 -->
+    <select id="countCurrentDispatchByOrderId" resultType="java.lang.Integer">
+        SELECT COUNT(1)
+        FROM t_order_dispatch
+        WHERE order_id = #{orderId}
+          AND current_flag = 1
+          AND is_delete = 0
+    </select>
+
+    <insert id="insertOrderDispatch" parameterType="com.ylx.order.domain.OrderDispatch">
+        INSERT INTO t_order_dispatch (
+            order_id,
+            order_no,
+            merchant_id,
+            merchant_name,
+            merchant_nick_name,
+            merchant_phone,
+            merchant_avatar,
+            merchant_type,
+            project_id,
+            project_name,
+            category_id,
+            city_code,
+            user_latitude,
+            user_longitude,
+            merchant_latitude,
+            merchant_longitude,
+            distance_meters,
+            dispatch_status,
+            dispatch_source,
+            current_flag,
+            order_status_before,
+            order_status_after,
+            dispatch_time,
+            remark,
+            operator_id,
+            operator_name,
+            create_by,
+            create_time,
+            update_by,
+            update_time,
+            is_delete
+        ) VALUES (
+            #{orderId},
+            #{orderNo},
+            #{merchantId},
+            #{merchantName},
+            #{merchantNickName},
+            #{merchantPhone},
+            #{merchantAvatar},
+            #{merchantType},
+            #{projectId},
+            #{projectName},
+            #{categoryId},
+            #{cityCode},
+            #{userLatitude},
+            #{userLongitude},
+            #{merchantLatitude},
+            #{merchantLongitude},
+            #{distanceMeters},
+            #{dispatchStatus},
+            #{dispatchSource},
+            #{currentFlag},
+            #{orderStatusBefore},
+            #{orderStatusAfter},
+            #{dispatchTime},
+            #{remark},
+            #{operatorId},
+            #{operatorName},
+            #{createBy},
+            #{createTime},
+            #{updateBy},
+            #{updateTime},
+            #{isDelete}
+        )
+    </insert>
+
+    <!-- 更新订单派单状态 -->
+    <update id="updateOrderDispatch">
+        UPDATE t_order
+        SET merchant_id = #{merchant.merchantId},
+            merchant_type = #{merchant.merchantType},
+            merchant_nick_name = #{merchant.merchantNickName},
+            merchant_avatar = #{merchant.merchantAvatar},
+            merchant_latitude = #{merchant.merchantLatitude},
+            merchant_longitude = #{merchant.merchantLongitude},
+            status = #{statusAfter},
+            dispatched_status = 1,
+            dispatched_time = NOW(),
+            update_time = NOW()
+        WHERE id = #{orderId}
+          AND is_delete = 0
+          AND status = #{statusBefore}
+    </update>
+
+    <insert id="insertOrderStatusFlow">
+        INSERT INTO t_order_status_flow (
+            order_id,
+            status,
+            create_by,
+            create_time,
+            update_by,
+            update_time
+        ) VALUES (
+            #{orderId},
+            #{status},
+            #{operator},
+            NOW(),
+            #{operator},
+            NOW()
+        )
+    </insert>
 </mapper>

+ 31 - 10
nightFragrance-massage/src/main/resources/mapper/order/TCommentUserMapper.xml

@@ -36,6 +36,19 @@
         is_delete = values(is_delete)
     </insert>
 
+    <sql id="CommentUserAuditBaseWhere">
+        tcu.is_delete = 0
+        AND tcua.is_delete = 0
+        <!-- 技师姓名查询 -->
+        <if test="tCommentUser.merchantName != null and tCommentUser.merchantName != ''">
+            AND tcu.merchant_name = #{tCommentUser.merchantName}
+        </if>
+        <!--敏感词查询-->
+        <if test="tCommentUser.sensitiveWord != null">
+            AND tcu.sensitive_word = #{tCommentUser.sensitiveWord}
+        </if>
+    </sql>
+
     <!-- 分页查询用户评论 -->
     <select id="selectAll" resultType="com.ylx.order.domain.TCommentUser">
         SELECT
@@ -47,26 +60,34 @@
             tcu.merchant_id as merchantId,
             tcu.merchant_name as merchantName,
             tcu.sensitive_word as sensitiveWord,
+            tcu.experience_comment as experienceComment,
+            tcu.price_comment as priceComment,
+            tcu.attitude_comment as attitudeComment,
+            tcu.grooming_comment as groomingComment,
             tcua.audit_status as auditStatus
         FROM
             t_comment_user tcu
                 LEFT JOIN t_comment_user_audit tcua ON tcu.id = tcua.comment_id
         WHERE
-            tcu.is_delete = 0
-          AND tcua.is_delete = 0
-          <!-- 技师姓名查询 -->
-        <if test="tCommentUser.merchantName != null and tCommentUser.merchantName != ''">
-            AND tcu.merchant_name = #{tCommentUser.merchantName}
-        </if>
-        <!--敏感词查询-->
-        <if test="tCommentUser.sensitiveWord != null">
-            AND tcu.sensitive_word = #{tCommentUser.sensitiveWord}
-        </if>
+        <include refid="CommentUserAuditBaseWhere"/>
         <!-- 审核状态查询 -->
         <if test="tCommentUser.auditStatus != null">
             AND tcua.audit_status = #{tCommentUser.auditStatus}
         </if>
     </select>
 
+    <!-- 查询用户评论审核状态统计 -->
+    <select id="selectAuditStatusCount" resultType="com.ylx.order.domain.vo.TCommentUserAuditStatusCountVO">
+        SELECT
+            COALESCE(SUM(CASE WHEN tcua.audit_status = 0 THEN 1 ELSE 0 END), 0) AS pendingCount,
+            COALESCE(SUM(CASE WHEN tcua.audit_status = 1 THEN 1 ELSE 0 END), 0) AS approvedCount,
+            COALESCE(SUM(CASE WHEN tcua.audit_status = 2 THEN 1 ELSE 0 END), 0) AS rejectedCount
+        FROM
+            t_comment_user tcu
+                LEFT JOIN t_comment_user_audit tcua ON tcu.id = tcua.comment_id
+        WHERE
+        <include refid="CommentUserAuditBaseWhere"/>
+    </select>
+
 </mapper>
 

+ 27 - 0
nightFragrance-massage/src/test/java/com/ylx/order/mapper/AdminOrderMapperXmlTest.java

@@ -118,6 +118,33 @@ public class AdminOrderMapperXmlTest {
         assertTrue(xml.contains("ORDER BY osf.create_time ASC"));
     }
 
+    @Test
+    public void dispatchSqlReadsOrderCandidatesUpdatesOrderAndWritesFlow() throws Exception {
+        String xml = readMapperXml();
+
+        assertTrue(xml.contains("selectDispatchOrderById"));
+        assertTrue(xml.contains("o.status AS status"));
+        assertTrue(xml.contains("p.category_id AS categoryId"));
+        assertTrue(xml.contains("o.user_latitude AS userLatitude"));
+        assertTrue(xml.contains("o.user_longitude AS userLongitude"));
+        assertTrue(xml.contains("selectDispatchMerchantCandidates"));
+        assertTrue(xml.contains("ST_Distance_Sphere"));
+        assertTrue(xml.contains("t.te_area_code = #{query.cityCode}"));
+        assertTrue(xml.contains("distance_meters &lt;= #{query.radiusMeters}"));
+        assertTrue(xml.contains("ORDER BY candidate.distance_meters ASC"));
+        assertTrue(xml.contains("countCurrentDispatchByOrderId"));
+        assertTrue(xml.contains("FROM t_order_dispatch"));
+        assertTrue(xml.contains("insertOrderDispatch"));
+        assertTrue(xml.contains("INSERT INTO t_order_dispatch"));
+        assertTrue(xml.contains("updateOrderDispatch"));
+        assertTrue(xml.contains("dispatched_time = NOW()"));
+        assertTrue(xml.contains("dispatched_status = 1"));
+        assertTrue(xml.contains("WHERE id = #{orderId}"));
+        assertTrue(xml.contains("AND status = #{statusBefore}"));
+        assertTrue(xml.contains("insertOrderStatusFlow"));
+        assertTrue(xml.contains("INSERT INTO t_order_status_flow"));
+    }
+
     private String readMapperXml() throws Exception {
         try (InputStream inputStream = getClass().getClassLoader()
                 .getResourceAsStream("mapper/order/AdminOrderMapper.xml")) {

+ 12 - 1
nightFragrance-massage/src/test/java/com/ylx/order/mapper/TCommentUserMapperXmlTest.java

@@ -18,11 +18,22 @@ public class TCommentUserMapperXmlTest {
         assertTrue(xml.contains("LEFT JOIN t_comment_user_audit tcua ON tcu.id = tcua.comment_id"));
         assertTrue(xml.contains("tcu.is_delete = 0"));
         assertTrue(xml.contains("tcua.is_delete = 0"));
-        assertTrue(xml.contains("tcu.name = #{tCommentUser.name}"));
+        assertTrue(xml.contains("tcu.merchant_name = #{tCommentUser.merchantName}"));
         assertTrue(xml.contains("tcu.sensitive_word = #{tCommentUser.sensitiveWord}"));
         assertTrue(xml.contains("tcua.audit_status = #{tCommentUser.auditStatus}"));
     }
 
+    @Test
+    public void selectAuditStatusCountKeepsSameBaseFiltersAndCountsThreeStatuses() throws Exception {
+        String xml = readMapperXml();
+
+        assertTrue(xml.contains("select id=\"selectAuditStatusCount\""));
+        assertTrue(xml.contains("COALESCE(SUM(CASE WHEN tcua.audit_status = 0 THEN 1 ELSE 0 END), 0) AS pendingCount"));
+        assertTrue(xml.contains("COALESCE(SUM(CASE WHEN tcua.audit_status = 1 THEN 1 ELSE 0 END), 0) AS approvedCount"));
+        assertTrue(xml.contains("COALESCE(SUM(CASE WHEN tcua.audit_status = 2 THEN 1 ELSE 0 END), 0) AS rejectedCount"));
+        assertTrue(xml.contains("<include refid=\"CommentUserAuditBaseWhere\"/>"));
+    }
+
     private String readMapperXml() throws Exception {
         try (InputStream inputStream = getClass().getClassLoader()
                 .getResourceAsStream("mapper/order/TCommentUserMapper.xml")) {

+ 130 - 0
nightFragrance-massage/src/test/java/com/ylx/order/service/impl/AdminOrderServiceImplTest.java

@@ -3,7 +3,12 @@ package com.ylx.order.service.impl;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.exception.ServiceException;
 import com.ylx.massage.domain.CancelOrderApplication;
+import com.ylx.order.domain.OrderDispatch;
+import com.ylx.order.domain.dto.AdminOrderDispatchDTO;
+import com.ylx.order.domain.dto.AdminOrderDispatchMerchantQueryDTO;
 import com.ylx.order.domain.dto.AdminOrderQueryDTO;
+import com.ylx.order.domain.vo.AdminOrderDispatchMerchantVO;
+import com.ylx.order.domain.vo.AdminOrderDispatchOrderVO;
 import com.ylx.order.domain.vo.AdminOrderDetailVO;
 import com.ylx.order.domain.vo.AdminOrderPageVO;
 import com.ylx.order.mapper.AdminOrderMapper;
@@ -239,6 +244,104 @@ public class AdminOrderServiceImplTest {
         assertEquals("服务中", detail.getOperationLogList().get(0).getStatusName());
     }
 
+    @Test
+    public void dispatchMerchantListRejectsOrderNotPendingDispatch() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        AdminOrderDispatchOrderVO order = buildDispatchOrder();
+        order.setStatus(2);
+        when(mapper.selectDispatchOrderById(1L)).thenReturn(order);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.listDispatchMerchants(1L, new AdminOrderDispatchMerchantQueryDTO()));
+
+        assertEquals("操作失败,请刷新重试", exception.getMessage());
+    }
+
+    @Test
+    public void dispatchMerchantListUsesDefaultRadiusAndTrimmedKeyword() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        AdminOrderDispatchOrderVO order = buildDispatchOrder();
+        when(mapper.selectDispatchOrderById(1L)).thenReturn(order);
+        when(mapper.selectDispatchMerchantCandidates(
+                org.mockito.ArgumentMatchers.any(AdminOrderDispatchOrderVO.class),
+                org.mockito.ArgumentMatchers.any(AdminOrderDispatchMerchantQueryDTO.class)))
+                .thenReturn(Collections.emptyList());
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+        AdminOrderDispatchMerchantQueryDTO query = new AdminOrderDispatchMerchantQueryDTO();
+        query.setKeyword(" NaNa ");
+
+        service.listDispatchMerchants(1L, query);
+
+        ArgumentCaptor<AdminOrderDispatchMerchantQueryDTO> queryCaptor =
+                ArgumentCaptor.forClass(AdminOrderDispatchMerchantQueryDTO.class);
+        verify(mapper).selectDispatchMerchantCandidates(
+                org.mockito.ArgumentMatchers.eq(order), queryCaptor.capture());
+        assertEquals("NaNa", queryCaptor.getValue().getKeyword());
+        assertEquals(new BigDecimal("10000"), queryCaptor.getValue().getRadiusMeters());
+    }
+
+    @Test
+    public void dispatchOrderRejectsEmptyMerchant() {
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        AdminOrderDispatchDTO dto = new AdminOrderDispatchDTO();
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.dispatch(1L, dto));
+
+        assertEquals("请选择商户", exception.getMessage());
+    }
+
+    @Test
+    public void dispatchOrderUpdatesOrderInsertsDispatchAndStatusFlow() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        AdminOrderDispatchOrderVO order = buildDispatchOrder();
+        AdminOrderDispatchMerchantVO merchant = buildDispatchMerchant();
+        when(mapper.selectDispatchOrderById(1L)).thenReturn(order);
+        when(mapper.selectDispatchMerchantById(
+                org.mockito.ArgumentMatchers.any(AdminOrderDispatchOrderVO.class),
+                org.mockito.ArgumentMatchers.eq(10),
+                org.mockito.ArgumentMatchers.any(BigDecimal.class))).thenReturn(merchant);
+        when(mapper.countCurrentDispatchByOrderId(1L)).thenReturn(0);
+        when(mapper.updateOrderDispatch(
+                org.mockito.ArgumentMatchers.eq(1L),
+                org.mockito.ArgumentMatchers.eq(merchant),
+                org.mockito.ArgumentMatchers.eq(1),
+                org.mockito.ArgumentMatchers.eq(2))).thenReturn(1);
+        when(mapper.insertOrderDispatch(org.mockito.ArgumentMatchers.any(OrderDispatch.class))).thenReturn(1);
+        when(mapper.insertOrderStatusFlow(
+                org.mockito.ArgumentMatchers.eq(1L),
+                org.mockito.ArgumentMatchers.eq(2),
+                org.mockito.ArgumentMatchers.anyString())).thenReturn(1);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+        AdminOrderDispatchDTO dto = new AdminOrderDispatchDTO();
+        dto.setMerchantId(10);
+
+        service.dispatch(1L, dto);
+
+        ArgumentCaptor<OrderDispatch> dispatchCaptor = ArgumentCaptor.forClass(OrderDispatch.class);
+        verify(mapper).insertOrderDispatch(dispatchCaptor.capture());
+        OrderDispatch dispatch = dispatchCaptor.getValue();
+        assertEquals(1L, dispatch.getOrderId());
+        assertEquals("NO001", dispatch.getOrderNo());
+        assertEquals(10, dispatch.getMerchantId());
+        assertEquals("NaNa", dispatch.getMerchantNickName());
+        assertEquals(1, dispatch.getDispatchStatus());
+        assertEquals(1, dispatch.getDispatchSource());
+        assertEquals(1, dispatch.getCurrentFlag());
+        assertEquals(1, dispatch.getOrderStatusBefore());
+        assertEquals(2, dispatch.getOrderStatusAfter());
+        assertNotNull(dispatch.getDispatchTime());
+        verify(mapper).updateOrderDispatch(1L, merchant, 1, 2);
+        verify(mapper).insertOrderStatusFlow(
+                org.mockito.ArgumentMatchers.eq(1L),
+                org.mockito.ArgumentMatchers.eq(2),
+                org.mockito.ArgumentMatchers.anyString());
+    }
+
     private CancelOrderApplication buildRefundApplicationOrder() {
         CancelOrderApplication application = new CancelOrderApplication();
         application.setOrderNo("NO001");
@@ -247,4 +350,31 @@ public class AdminOrderServiceImplTest {
         application.setOrderStatus(5);
         return application;
     }
+
+    private AdminOrderDispatchOrderVO buildDispatchOrder() {
+        AdminOrderDispatchOrderVO order = new AdminOrderDispatchOrderVO();
+        order.setOrderId(1L);
+        order.setOrderNo("NO001");
+        order.setStatus(1);
+        order.setProjectId(100L);
+        order.setProjectName("按摩");
+        order.setCategoryId(2);
+        order.setUserLatitude(new BigDecimal("30.0000000"));
+        order.setUserLongitude(new BigDecimal("120.0000000"));
+        return order;
+    }
+
+    private AdminOrderDispatchMerchantVO buildDispatchMerchant() {
+        AdminOrderDispatchMerchantVO merchant = new AdminOrderDispatchMerchantVO();
+        merchant.setMerchantId(10);
+        merchant.setMerchantName("NaNa");
+        merchant.setMerchantNickName("NaNa");
+        merchant.setMerchantPhone("13812345678");
+        merchant.setMerchantAvatar("avatar.jpg");
+        merchant.setMerchantType(0);
+        merchant.setMerchantLatitude(new BigDecimal("30.0100000"));
+        merchant.setMerchantLongitude(new BigDecimal("120.0100000"));
+        merchant.setDistanceMeters(new BigDecimal("1000"));
+        return merchant;
+    }
 }

+ 33 - 0
nightFragrance-massage/src/test/java/com/ylx/order/service/impl/TCommentUserServiceImplTest.java

@@ -2,11 +2,13 @@ package com.ylx.order.service.impl;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.order.domain.TCommentUser;
+import com.ylx.order.domain.vo.TCommentUserAuditStatusCountVO;
 import com.ylx.order.mapper.TCommentUserMapper;
 import org.junit.jupiter.api.Test;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -29,4 +31,35 @@ public class TCommentUserServiceImplTest {
         assertEquals(mapperResult, result);
         verify(mapper).selectAll(eq(page), eq(query));
     }
+
+    @Test
+    public void selectAuditStatusCountUsesMapperAndNormalizesNullQuery() {
+        TCommentUserMapper mapper = mock(TCommentUserMapper.class);
+        TCommentUserAuditStatusCountVO mapperResult = new TCommentUserAuditStatusCountVO();
+        mapperResult.setPendingCount(1L);
+        mapperResult.setApprovedCount(2L);
+        mapperResult.setRejectedCount(3L);
+        when(mapper.selectAuditStatusCount(any(TCommentUser.class))).thenReturn(mapperResult);
+        TCommentUserServiceImpl service = new TCommentUserServiceImpl();
+        ReflectionTestUtils.setField(service, "tCommentUserMapper", mapper);
+
+        TCommentUserAuditStatusCountVO result = service.selectAuditStatusCount(null);
+
+        assertEquals(mapperResult, result);
+        verify(mapper).selectAuditStatusCount(any(TCommentUser.class));
+    }
+
+    @Test
+    public void selectAuditStatusCountReturnsZeroWhenMapperReturnsNull() {
+        TCommentUserMapper mapper = mock(TCommentUserMapper.class);
+        when(mapper.selectAuditStatusCount(any(TCommentUser.class))).thenReturn(null);
+        TCommentUserServiceImpl service = new TCommentUserServiceImpl();
+        ReflectionTestUtils.setField(service, "tCommentUserMapper", mapper);
+
+        TCommentUserAuditStatusCountVO result = service.selectAuditStatusCount(new TCommentUser());
+
+        assertEquals(0L, result.getPendingCount());
+        assertEquals(0L, result.getApprovedCount());
+        assertEquals(0L, result.getRejectedCount());
+    }
 }