Przeglądaj źródła

开发订单相关的接口

jinshihui 6 godzin temu
rodzic
commit
1920123445

+ 0 - 4
nightFragrance-massage/src/main/java/com/ylx/massage/domain/CancelOrderApplication.java

@@ -26,10 +26,6 @@ public class CancelOrderApplication implements Serializable {
     @TableId(type = IdType.ASSIGN_ID)
     private String id;
 
-    /**
-     * 订单ID
-     */
-    private String orderId;
 
     /**
      * 订单号

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

@@ -0,0 +1,129 @@
+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.AdminOrderQueryDTO;
+import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderPageVO;
+import com.ylx.order.domain.vo.AdminOrderServiceCategoryVO;
+import com.ylx.order.service.AdminOrderService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/admin/order")
+@Api(tags = {"后台服务订单"})
+public class AdminOrderController {
+
+    @Resource
+    private AdminOrderService adminOrderService;
+
+    /**
+     * 分页查询服务订单
+     *
+     * @param page
+     * @param dto
+     * @return R<Page<AdminOrderPageVO>>
+     */
+    @GetMapping("/page")
+    @ApiOperation("分页查询服务订单")
+    public R<Page<AdminOrderPageVO>> page(Page<AdminOrderPageVO> page, AdminOrderQueryDTO dto) {
+        try {
+            return R.ok(adminOrderService.page(page, dto));
+        } catch (ServiceException e) {
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("分页查询服务订单异常", e);
+            return R.fail("分页查询服务订单失败");
+        }
+    }
+
+    /**
+     * 查询服务分类选项
+     *
+     * @return R<List<AdminOrderServiceCategoryVO>>
+     */
+    @GetMapping("/serviceCategory/options")
+    @ApiOperation("查询服务分类选项")
+    public R<List<AdminOrderServiceCategoryVO>> serviceCategoryOptions() {
+        try {
+            return R.ok(adminOrderService.listServiceCategoryOptions());
+        } catch (ServiceException e) {
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("查询服务分类选项异常", e);
+            return R.fail("查询服务分类选项失败");
+        }
+    }
+
+    /**
+     * 根据订单ID逻辑删除服务订单
+     *
+     * @param id 订单ID
+     * @return R<?>
+     */
+    @DeleteMapping("/{id}")
+    @ApiOperation("根据订单ID逻辑删除服务订单")
+    public R<?> delete(@PathVariable("id") Long id) {
+        try {
+            adminOrderService.deleteById(id);
+            return R.ok("删除成功");
+        } catch (ServiceException e) {
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("逻辑删除服务订单异常,订单ID:{}", id, e);
+            return R.fail("删除服务订单失败");
+        }
+    }
+
+    /**
+     * 根据订单ID发起退款申请
+     *
+     * @param id 订单ID
+     * @return R<?>
+     */
+    @PostMapping("/{id}/refund/apply")
+    @ApiOperation("根据订单ID发起退款申请")
+    public R<?> applyRefund(@PathVariable("id") Long id) {
+        try {
+            adminOrderService.applyRefund(id);
+            return R.ok("退款申请已发起");
+        } catch (ServiceException e) {
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("发起退款申请异常,订单ID:{}", id, e);
+            return R.fail("发起退款申请失败");
+        }
+    }
+
+    /**
+     * 根据订单ID查询后台订单详情
+     *
+     * @param id 订单ID
+     * @return R<AdminOrderDetailVO>
+     */
+    @GetMapping("/{id}/detail")
+    @ApiOperation("根据订单ID查询后台订单详情")
+    public R<AdminOrderDetailVO> detail(@PathVariable("id") Long id) {
+        try {
+            return R.ok(adminOrderService.detail(id));
+        } catch (ServiceException e) {
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("查询后台订单详情异常,订单ID:{}", id, e);
+            return R.fail("查询后台订单详情失败");
+        }
+    }
+}

+ 108 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/AdminOrderQueryDTO.java

@@ -0,0 +1,108 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("后台服务订单分页查询DTO")
+public class AdminOrderQueryDTO {
+
+    /**
+     * 订单号关键字,支持模糊查询。
+     */
+    @ApiModelProperty("订单号,支持模糊查询")
+    private String orderNo;
+
+    /**
+     * 下单开始时间,支持 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss 格式。
+     */
+    @ApiModelProperty("下单开始时间,格式 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss")
+    private String startTime;
+
+    /**
+     * 下单结束时间,支持 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss 格式。
+     */
+    @ApiModelProperty("下单结束时间,格式 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss")
+    private String endTime;
+
+    /**
+     * 用户信息搜索类型。
+     * phone 表示手机号,nickName 表示用户昵称,name 表示下单联系人姓名。
+     */
+    @ApiModelProperty("用户搜索类型:phone=手机号,nickName=用户昵称,name=下单联系人姓名;默认 phone")
+    private String userSearchType;
+
+    /**
+     * 用户信息搜索关键字。
+     */
+    @ApiModelProperty("用户搜索关键字")
+    private String userKeyword;
+
+    /**
+     * 服务分类 ID。
+     */
+    @ApiModelProperty("服务分类ID")
+    private Integer categoryId;
+
+    /**
+     * 服务项目名称关键字,支持模糊查询。
+     */
+    @ApiModelProperty("项目名称,支持模糊查询")
+    private String projectName;
+
+    /**
+     * 商户信息搜索类型。
+     * nickName 表示商户昵称,phone 表示商户手机号。
+     */
+    @ApiModelProperty("商户搜索类型:nickName=商户昵称,phone=商户手机号;默认 nickName")
+    private String merchantSearchType;
+
+    /**
+     * 商户信息搜索关键字。
+     */
+    @ApiModelProperty("商户搜索关键字")
+    private String merchantKeyword;
+
+    /**
+     * 支付方式。
+     * 0 表示购物卡支付,1 表示微信支付。
+     */
+    @ApiModelProperty("支付方式:0=购物卡支付,1=微信支付")
+    private Integer paymentMethod;
+
+    /**
+     * 订单状态。
+     * 0 待支付,1 待派单,2 待接单,3 待服务,4 服务中,5 已完成,6 已取消。
+     */
+    @ApiModelProperty("订单状态:0=待支付,1=待派单,2=待接单,3=待服务,4=服务中,5=已完成,6=已取消")
+    private Integer status;
+
+    /**
+     * 是否查询异常订单。
+     * 为 true 时查询关闭、拒绝、退款成功等异常状态订单。
+     */
+    @ApiModelProperty("异常订单:true 时查询关闭/拒绝/退款成功等异常状态")
+    private Boolean abnormalOrder;
+
+    /**
+     * 订单归属,也即订单关联的商户类型。
+     * 0 表示真实商户,1 表示虚拟商户。
+     */
+    @ApiModelProperty("订单归属/商户类型:0=真实商户,1=虚拟商户")
+    private Integer merchantType;
+
+    /**
+     * 服务层解析后的下单开始时间,用于 mapper 查询。
+     */
+    @ApiModelProperty(hidden = true)
+    private Date queryStartTime;
+
+    /**
+     * 服务层解析后的下单结束时间,用于 mapper 查询。
+     */
+    @ApiModelProperty(hidden = true)
+    private Date queryEndTime;
+}

+ 135 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/AdminOrderDetailVO.java

@@ -0,0 +1,135 @@
+package com.ylx.order.domain.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class AdminOrderDetailVO {
+
+    @ApiModelProperty("订单信息")
+    private OrderInfoVO orderInfo;
+
+    @ApiModelProperty("项目信息")
+    private ProjectInfoVO projectInfo;
+
+    @ApiModelProperty("服务凭证图片")
+    private List<String> serviceVoucherList;
+
+    @ApiModelProperty("订单操作日志")
+    private List<OperationLogVO> operationLogList;
+
+    @Data
+    public static class OrderInfoVO {
+        @ApiModelProperty("订单ID")
+        private Long id;
+        @ApiModelProperty("订单号")
+        private String orderNo;
+        @ApiModelProperty("订单状态")
+        private Integer status;
+
+        /**
+         * 订单状态名称
+         */
+        @ApiModelProperty("订单状态名称")
+        private String statusName;
+        @ApiModelProperty("下单用户")
+        private String userNickName;
+        @ApiModelProperty("联系人")
+        private String contactPersonName;
+        @ApiModelProperty("联系电话")
+        private String contactPhoneNumber;
+        @ApiModelProperty("联系地址")
+        private String contactAddressInfo;
+        @ApiModelProperty("商户昵称")
+        private String merchantNickName;
+        @ApiModelProperty("商户电话")
+        private String merchantPhone;
+        @ApiModelProperty("预约开始时间")
+        private String appointmentStartTime;
+        @ApiModelProperty("服务开始时间")
+        private String startTime;
+        @ApiModelProperty("服务结束时间")
+        private String completedTime;
+        @ApiModelProperty("下单时间")
+        private String createTime;
+        @ApiModelProperty("付款时间")
+        private String paidTime;
+        @ApiModelProperty("付款方式")
+        private Integer paymentMethod;
+        @ApiModelProperty("付款方式名称")
+        private String paymentMethodName;
+        @ApiModelProperty("项目时长")
+        private Integer projectDuration;
+        @ApiModelProperty("用户纬度")
+        private BigDecimal userLatitude;
+        @ApiModelProperty("用户经度")
+        private BigDecimal userLongitude;
+        @ApiModelProperty("商户真实纬度")
+        private BigDecimal merchantLatitude;
+        @ApiModelProperty("商户真实经度")
+        private BigDecimal merchantLongitude;
+        @ApiModelProperty("商户虚拟纬度")
+        private BigDecimal virtualLatitude;
+        @ApiModelProperty("商户虚拟经度")
+        private BigDecimal virtualLongitude;
+        @ApiModelProperty("服务凭证原始字段")
+        private String startPhoto;
+        @ApiModelProperty("售后单ID")
+        private Long afterSalesServiceId;
+        @ApiModelProperty("售后状态")
+        private Integer afterSalesServiceStatus;
+        @ApiModelProperty("售后状态名称")
+        private String afterSalesServiceStatusName;
+        @ApiModelProperty("平台收益")
+        private BigDecimal platformIncome;
+        @ApiModelProperty("商户收益")
+        private BigDecimal merchantIncome;
+    }
+
+    @Data
+    public static class ProjectInfoVO {
+        @ApiModelProperty("项目名称")
+        private String projectName;
+        @ApiModelProperty("项目封面")
+        private String projectCover;
+        @ApiModelProperty("计费单位")
+        private Integer unitType;
+        @ApiModelProperty("计费单位名称")
+        private String unitName;
+        @ApiModelProperty("售价")
+        private BigDecimal unitPrice;
+        @ApiModelProperty("商户佣金比例")
+        private BigDecimal merchantCommission;
+        @ApiModelProperty("实付金额")
+        private BigDecimal finalAmount;
+        @ApiModelProperty("订单应收")
+        private BigDecimal basePrice;
+        @ApiModelProperty("出行车费")
+        private BigDecimal trafficFee;
+        @ApiModelProperty("优惠券抵扣")
+        private BigDecimal couponDiscount;
+        @ApiModelProperty("售后单ID")
+        private Long afterSalesServiceId;
+        @ApiModelProperty("售后状态")
+        private Integer afterSalesServiceStatus;
+        @ApiModelProperty("售后状态名称")
+        private String afterSalesServiceStatusName;
+    }
+
+    @Data
+    public static class OperationLogVO {
+        @ApiModelProperty("操作人")
+        private String operator;
+        @ApiModelProperty("操作时间")
+        private String operationTime;
+        @ApiModelProperty("状态")
+        private Integer status;
+        @ApiModelProperty("状态名称")
+        private String statusName;
+        @ApiModelProperty("操作内容")
+        private String operationContent;
+    }
+}

+ 188 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/AdminOrderPageVO.java

@@ -0,0 +1,188 @@
+package com.ylx.order.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+@Data
+@ApiModel("后台服务订单分页VO")
+public class AdminOrderPageVO {
+
+    /**
+     * 订单主键 ID。
+     */
+    @ApiModelProperty("订单ID")
+    private Long id;
+
+    /**
+     * 订单编号。
+     */
+    @ApiModelProperty("订单号")
+    private String orderNo;
+
+    /**
+     * 用户下单时间。
+     */
+    @ApiModelProperty("下单时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date orderTime;
+
+    /**
+     * 订单关联的服务项目名称。
+     */
+    @ApiModelProperty("服务项目名称")
+    private String projectName;
+
+    /**
+     * 服务项目时长,单位为分钟。
+     */
+    @ApiModelProperty("服务时长,单位:分钟")
+    private Integer projectDuration;
+
+    /**
+     * 用户预约的服务开始时间。
+     */
+    @ApiModelProperty("预约时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime appointmentStartTime;
+
+    /**
+     * 服务项目单价。
+     */
+    @ApiModelProperty("单价")
+    private BigDecimal unitPrice;
+
+    /**
+     * 服务项目数量。
+     */
+    @ApiModelProperty("数量")
+    private Integer quantity;
+
+    /**
+     * 计费单位编码。
+     */
+    @ApiModelProperty("计费单位编码")
+    private Integer unitType;
+
+    /**
+     * 计费单位名称。
+     */
+    @ApiModelProperty("计费单位名称")
+    private String unitName;
+
+    /**
+     * 订单金额。
+     * 待支付订单表示应付金额,已支付订单表示实付金额。
+     */
+    @ApiModelProperty("订单金额:未付款为应付,已付款为实付")
+    private BigDecimal orderAmount;
+
+    /**
+     * 订单金额类型名称。
+     * 可取值为应付或实付。
+     */
+    @ApiModelProperty("金额类型:应付/实付")
+    private String amountTypeName;
+
+    /**
+     * 出行费用。
+     */
+    @ApiModelProperty("出行费用")
+    private BigDecimal trafficFee;
+
+    /**
+     * 支付方式编码。
+     */
+    @ApiModelProperty("支付方式编码")
+    private Integer paymentMethod;
+
+    /**
+     * 支付方式名称。
+     * 待付款订单显示为 --。
+     */
+    @ApiModelProperty("支付方式名称;待付款显示 --")
+    private String paymentMethodName;
+
+    /**
+     * 下单用户注册昵称。
+     */
+    @ApiModelProperty("下单用户昵称")
+    private String userNickName;
+
+    /**
+     * 订单内保存的下单联系人姓名。
+     */
+    @ApiModelProperty("下单联系人姓名")
+    private String contactPersonName;
+
+    /**
+     * 订单内保存的下单联系人手机号。
+     */
+    @ApiModelProperty("下单联系人手机号")
+    private String contactPhoneNumber;
+
+    /**
+     * 脱敏后的下单联系人手机号。
+     */
+    @ApiModelProperty("下单联系人手机号脱敏")
+    private String contactPhoneMasked;
+
+    /**
+     * 订单内保存的联系地址。
+     */
+    @ApiModelProperty("联系地址")
+    private String contactAddressInfo;
+
+    /**
+     * 订单状态编码。
+     */
+    @ApiModelProperty("订单状态")
+    private Integer status;
+
+    /**
+     * 订单状态名称。
+     */
+    @ApiModelProperty("订单状态名称")
+    private String statusName;
+
+    /**
+     * 履约状态编码。
+     */
+    @ApiModelProperty("履约状态")
+    private Integer execStatus;
+
+    /**
+     * 履约状态名称。
+     */
+    @ApiModelProperty("履约状态名称")
+    private String execStatusName;
+
+    /**
+     * 服务商户昵称。
+     */
+    @ApiModelProperty("服务商户昵称")
+    private String merchantNickName;
+
+    /**
+     * 服务商户注册手机号。
+     */
+    @ApiModelProperty("服务商户手机号")
+    private String merchantPhone;
+
+    /**
+     * 服务分类 ID。
+     */
+    @ApiModelProperty("服务分类ID")
+    private Integer categoryId;
+
+    /**
+     * 服务分类名称。
+     */
+    @ApiModelProperty("服务分类名称")
+    private String categoryName;
+}

+ 19 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/AdminOrderServiceCategoryVO.java

@@ -0,0 +1,19 @@
+package com.ylx.order.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("后台订单服务分类选项VO")
+public class AdminOrderServiceCategoryVO {
+
+    @ApiModelProperty("服务分类ID")
+    private Integer id;
+
+    @ApiModelProperty("服务标签:1=按摩,2=陪玩")
+    private Integer serviceTag;
+
+    @ApiModelProperty("服务分类名称")
+    private String name;
+}

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

@@ -0,0 +1,34 @@
+package com.ylx.order.mapper;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.massage.domain.CancelOrderApplication;
+import com.ylx.order.domain.dto.AdminOrderQueryDTO;
+import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderPageVO;
+import com.ylx.order.domain.vo.AdminOrderServiceCategoryVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface AdminOrderMapper {
+
+    Page<AdminOrderPageVO> selectAdminOrderPage(Page<AdminOrderPageVO> page, @Param("dto") AdminOrderQueryDTO dto);
+
+    List<AdminOrderServiceCategoryVO> selectServiceCategoryOptions();
+
+    int logicDeleteById(@Param("id") Long id);
+
+    CancelOrderApplication selectRefundApplicationOrderById(@Param("id") Long id);
+
+    int countPendingRefundApplicationByOrderNo(@Param("orderNo") String orderNo);
+
+    int insertRefundApplication(@Param("application") CancelOrderApplication application);
+
+    AdminOrderDetailVO.OrderInfoVO selectOrderDetailInfoById(@Param("id") Long id);
+
+    AdminOrderDetailVO.ProjectInfoVO selectOrderProjectDetailById(@Param("id") Long id);
+
+    List<AdminOrderDetailVO.OperationLogVO> selectOrderOperationLogs(@Param("id") Long id);
+}

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

@@ -0,0 +1,22 @@
+package com.ylx.order.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.order.domain.dto.AdminOrderQueryDTO;
+import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderPageVO;
+import com.ylx.order.domain.vo.AdminOrderServiceCategoryVO;
+
+import java.util.List;
+
+public interface AdminOrderService {
+
+    Page<AdminOrderPageVO> page(Page<AdminOrderPageVO> page, AdminOrderQueryDTO dto);
+
+    List<AdminOrderServiceCategoryVO> listServiceCategoryOptions();
+
+    void deleteById(Long id);
+
+    void applyRefund(Long id);
+
+    AdminOrderDetailVO detail(Long id);
+}

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

@@ -0,0 +1,339 @@
+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.dto.AdminOrderQueryDTO;
+import com.ylx.order.domain.vo.AdminOrderDetailVO;
+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.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Service
+public class AdminOrderServiceImpl implements AdminOrderService {
+
+    private static final String PLATFORM_REFUND_REASON = "平台发起退款申请";
+
+    private static final String USER_SEARCH_PHONE = "phone";
+    private static final String USER_SEARCH_NICK_NAME = "nickName";
+    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 DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Resource
+    private AdminOrderMapper adminOrderMapper;
+
+    @Override
+    public Page<AdminOrderPageVO> page(Page<AdminOrderPageVO> page, AdminOrderQueryDTO dto) {
+        AdminOrderQueryDTO query = dto == null ? new AdminOrderQueryDTO() : dto;
+        normalizeAndValidate(query);
+
+        Page<AdminOrderPageVO> pageParam = page == null ? new Page<>(1, 10) : page;
+        Page<AdminOrderPageVO> result = adminOrderMapper.selectAdminOrderPage(pageParam, query);
+        result.getRecords().forEach(this::fillDisplayFields);
+        return result;
+    }
+
+    @Override
+    public List<AdminOrderServiceCategoryVO> listServiceCategoryOptions() {
+        return adminOrderMapper.selectServiceCategoryOptions();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteById(Long id) {
+        if (id == null) {
+            throw new ServiceException("订单ID不能为空");
+        }
+        if (id <= 0) {
+            throw new ServiceException("订单ID不正确");
+        }
+
+        int rows = adminOrderMapper.logicDeleteById(id);
+        if (rows <= 0) {
+            throw new ServiceException("订单不存在或已删除");
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void applyRefund(Long id) {
+        validateOrderId(id);
+
+        CancelOrderApplication application = adminOrderMapper.selectRefundApplicationOrderById(id);
+        if (application == null) {
+            throw new ServiceException("订单不存在或已删除");
+        }
+        if (adminOrderMapper.countPendingRefundApplicationByOrderNo(application.getOrderNo()) > 0) {
+            throw new ServiceException("该订单已存在待审核退款申请");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        application.setId(UUID.randomUUID().toString().replace("-", ""));
+        application.setCancelOrderReason(PLATFORM_REFUND_REASON);
+        application.setAuditStatus(0);
+        application.setApplicationTime(now);
+        application.setCreateTime(now);
+        application.setUpdateTime(now);
+        application.setIsDelete(0);
+
+        int rows = adminOrderMapper.insertRefundApplication(application);
+        if (rows <= 0) {
+            throw new ServiceException("发起退款申请失败");
+        }
+    }
+
+    @Override
+    public AdminOrderDetailVO detail(Long id) {
+        validateOrderId(id);
+
+        AdminOrderDetailVO.OrderInfoVO orderInfo = adminOrderMapper.selectOrderDetailInfoById(id);
+        if (orderInfo == null) {
+            throw new ServiceException("订单不存在或已删除");
+        }
+
+        fillOrderDetailDisplayFields(orderInfo);
+
+        AdminOrderDetailVO.ProjectInfoVO projectInfo = adminOrderMapper.selectOrderProjectDetailById(id);
+        if (projectInfo != null) {
+            fillProjectDetailDisplayFields(projectInfo);
+        }
+
+        List<AdminOrderDetailVO.OperationLogVO> operationLogs = adminOrderMapper.selectOrderOperationLogs(id);
+        if (operationLogs == null) {
+            operationLogs = Collections.emptyList();
+        }
+        operationLogs.forEach(this::fillOperationLogDisplayFields);
+
+        AdminOrderDetailVO detail = new AdminOrderDetailVO();
+        detail.setOrderInfo(orderInfo);
+        detail.setProjectInfo(projectInfo);
+        detail.setServiceVoucherList(splitStartPhoto(orderInfo.getStartPhoto()));
+        detail.setOperationLogList(operationLogs);
+        return detail;
+    }
+
+    /**
+     * 填充订单详情显示字段
+     * @param orderInfo
+     */
+    private void fillOrderDetailDisplayFields(AdminOrderDetailVO.OrderInfoVO orderInfo) {
+        orderInfo.setStatusName(getStatusName(orderInfo.getStatus()));
+        orderInfo.setPaymentMethodName(getPaymentMethodName(orderInfo.getStatus(), orderInfo.getPaymentMethod()));
+        orderInfo.setAfterSalesServiceStatusName(getAfterSalesServiceStatusName(orderInfo.getAfterSalesServiceStatus()));
+        orderInfo.setPlatformIncome(null);
+        orderInfo.setMerchantIncome(null);
+    }
+
+    private void fillProjectDetailDisplayFields(AdminOrderDetailVO.ProjectInfoVO projectInfo) {
+        projectInfo.setUnitName(getUnitName(projectInfo.getUnitType()));
+        projectInfo.setAfterSalesServiceStatusName(getAfterSalesServiceStatusName(projectInfo.getAfterSalesServiceStatus()));
+    }
+
+    private void fillOperationLogDisplayFields(AdminOrderDetailVO.OperationLogVO log) {
+        String statusName = getStatusName(log.getStatus());
+        log.setStatusName(statusName);
+        log.setOperationContent(statusName);
+    }
+
+    private String getAfterSalesServiceStatusName(Integer status) {
+        AfterSaleServiceStatusEnum statusEnum = AfterSaleServiceStatusEnum.valueOf(status);
+        return statusEnum == null ? "" : statusEnum.getDesc();
+    }
+
+    private List<String> splitStartPhoto(String startPhoto) {
+        if (!StringUtils.hasText(startPhoto)) {
+            return new ArrayList<>();
+        }
+        return java.util.Arrays.stream(startPhoto.split(","))
+                .map(String::trim)
+                .filter(StringUtils::hasText)
+                .collect(Collectors.toList());
+    }
+
+    private void validateOrderId(Long id) {
+        if (id == null) {
+            throw new ServiceException("订单ID不能为空");
+        }
+        if (id <= 0) {
+            throw new ServiceException("订单ID不正确");
+        }
+    }
+
+    /**
+     * 校验并格式化查询参数
+     * @param dto
+     */
+    private void normalizeAndValidate(AdminOrderQueryDTO dto) {
+        dto.setUserSearchType(normalizeUserSearchType(dto.getUserSearchType()));
+        dto.setMerchantSearchType(normalizeMerchantSearchType(dto.getMerchantSearchType()));
+
+        Date startTime = parseQueryTime(dto.getStartTime(), true);
+        Date endTime = parseQueryTime(dto.getEndTime(), false);
+        if (startTime != null && endTime != null && startTime.after(endTime)) {
+            throw new ServiceException("开始时间不能大于结束时间");
+        }
+        dto.setQueryStartTime(startTime);
+        dto.setQueryEndTime(endTime);
+    }
+
+    private String normalizeUserSearchType(String searchType) {
+        if (!StringUtils.hasText(searchType)) {
+            return USER_SEARCH_PHONE;
+        }
+        if (USER_SEARCH_PHONE.equals(searchType) || USER_SEARCH_NICK_NAME.equals(searchType)
+                || USER_SEARCH_NAME.equals(searchType)) {
+            return searchType;
+        }
+        throw new ServiceException("用户搜索类型不正确");
+    }
+
+    private String normalizeMerchantSearchType(String searchType) {
+        if (!StringUtils.hasText(searchType)) {
+            return MERCHANT_SEARCH_NICK_NAME;
+        }
+        if (MERCHANT_SEARCH_NICK_NAME.equals(searchType) || MERCHANT_SEARCH_PHONE.equals(searchType)) {
+            return searchType;
+        }
+        throw new ServiceException("商户搜索类型不正确");
+    }
+
+    private Date parseQueryTime(String value, boolean startOfDay) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        String text = value.trim();
+        try {
+            LocalDateTime dateTime;
+            if (text.length() == 10) {
+                LocalDate date = LocalDate.parse(text, DATE_FORMATTER);
+                dateTime = startOfDay ? date.atStartOfDay() : date.atTime(LocalTime.MAX);
+            } else {
+                dateTime = LocalDateTime.parse(text, DATE_TIME_FORMATTER);
+            }
+            return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
+        } catch (DateTimeParseException e) {
+            throw new ServiceException("下单时间格式错误,请使用 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss");
+        }
+    }
+
+    /**
+     * 填充订单显示字段
+     * @param vo
+     */
+    private void fillDisplayFields(AdminOrderPageVO vo) {
+        vo.setContactPhoneMasked(maskPhone(vo.getContactPhoneNumber()));
+        vo.setStatusName(getStatusName(vo.getStatus()));
+        vo.setExecStatusName(getExecStatusName(vo.getExecStatus()));
+        vo.setPaymentMethodName(getPaymentMethodName(vo.getStatus(), vo.getPaymentMethod()));
+        vo.setAmountTypeName(Integer.valueOf(0).equals(vo.getStatus()) ? "应付" : "实付");
+        vo.setUnitName(getUnitName(vo.getUnitType()));
+        if (vo.getQuantity() == null) {
+            vo.setQuantity(1);
+        }
+    }
+
+    private String maskPhone(String phone) {
+        if (!StringUtils.hasText(phone) || phone.length() < 7) {
+            return phone;
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(7);
+    }
+
+    private String getPaymentMethodName(Integer status, Integer paymentMethod) {
+        if (Integer.valueOf(0).equals(status)) {
+            return "--";
+        }
+        if (Integer.valueOf(0).equals(paymentMethod)) {
+            return "购物卡支付";
+        }
+        if (Integer.valueOf(1).equals(paymentMethod)) {
+            return "微信支付";
+        }
+        return "";
+    }
+
+    private String getStatusName(Integer status) {
+        if (status == null) {
+            return "";
+        }
+        switch (status) {
+            case 0:
+                return "待支付";
+            case 1:
+                return "待派单";
+            case 2:
+                return "待接单";
+            case 3:
+                return "待服务";
+            case 4:
+                return "服务中";
+            case 5:
+                return "已完成";
+            case 6:
+                return "已取消";
+            case 7:
+                return "已关闭";
+            case 8:
+                return "拒绝接单";
+            case 9:
+                return "退款成功";
+            default:
+                return "未知";
+        }
+    }
+
+    private String getExecStatusName(Integer execStatus) {
+        if (execStatus == null) {
+            return "";
+        }
+        switch (execStatus) {
+            case 0:
+                return "待出发/待签到";
+            case 1:
+                return "已出发";
+            case 2:
+                return "已到达/已签到";
+            default:
+                return "未知";
+        }
+    }
+
+    private String getUnitName(Integer unitType) {
+        if (unitType == null) {
+            return "";
+        }
+        switch (unitType) {
+            case 1:
+                return "次";
+            case 2:
+                return "小时";
+            case 3:
+                return "分钟";
+            default:
+                return String.valueOf(unitType);
+        }
+    }
+}

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

@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ylx.order.mapper.AdminOrderMapper">
+
+    <select id="selectAdminOrderPage" resultType="com.ylx.order.domain.vo.AdminOrderPageVO">
+        SELECT
+            o.id AS id,
+            o.order_no AS orderNo,
+            o.create_time AS orderTime,
+            COALESCE(p.title, o.project_name) AS projectName,
+            o.project_duration AS projectDuration,
+            o.appointment_start_time AS appointmentStartTime,
+            COALESCE(p.price, o.base_price) AS unitPrice,
+            1 AS quantity,
+            p.unit_type AS unitType,
+            o.final_amount AS orderAmount,
+            o.traffic_fee AS trafficFee,
+            o.payment_method AS paymentMethod,
+            u.c_nick_name AS userNickName,
+            o.contact_person_name AS contactPersonName,
+            o.contact_phone_number AS contactPhoneNumber,
+            o.contact_address_info AS contactAddressInfo,
+            o.status AS status,
+            o.exec_status AS execStatus,
+            COALESCE(mt.te_nick_name, o.merchant_nick_name) AS merchantNickName,
+            mt.te_phone AS merchantPhone,
+            p.category_id AS categoryId,
+            sc.name AS categoryName
+        FROM t_order o
+        LEFT JOIN t_wx_user u ON u.id = CAST(o.user_id AS CHAR) AND u.is_delete = 0
+        LEFT JOIN ma_technician mt ON mt.id = o.merchant_id AND mt.is_delete = 0
+        LEFT JOIN project p ON p.id = o.project_id AND p.is_delete = 0
+        LEFT JOIN service_category sc ON sc.id = p.category_id AND sc.is_delete = 0
+        <where>
+            o.is_delete = 0
+            <if test="dto.orderNo != null and dto.orderNo != ''">
+                AND o.order_no LIKE CONCAT('%', #{dto.orderNo}, '%')
+            </if>
+            <if test="dto.queryStartTime != null">
+                AND o.create_time &gt;= #{dto.queryStartTime}
+            </if>
+            <if test="dto.queryEndTime != null">
+                AND o.create_time &lt;= #{dto.queryEndTime}
+            </if>
+            <if test="dto.userKeyword != null and dto.userKeyword != ''">
+                <choose>
+                    <when test="dto.userSearchType == 'nickName'">
+                        AND u.c_nick_name LIKE CONCAT('%', #{dto.userKeyword}, '%')
+                    </when>
+                    <when test="dto.userSearchType == 'name'">
+                        AND o.contact_person_name LIKE CONCAT('%', #{dto.userKeyword}, '%')
+                    </when>
+                    <otherwise>
+                        AND u.c_phone LIKE CONCAT('%', #{dto.userKeyword}, '%')
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="dto.categoryId != null">
+                AND p.category_id = #{dto.categoryId}
+            </if>
+            <if test="dto.projectName != null and dto.projectName != ''">
+                AND o.project_name LIKE CONCAT('%', #{dto.projectName}, '%')
+            </if>
+            <if test="dto.merchantKeyword != null and dto.merchantKeyword != ''">
+                <choose>
+                    <when test="dto.merchantSearchType == 'phone'">
+                        AND mt.te_phone LIKE CONCAT('%', #{dto.merchantKeyword}, '%')
+                    </when>
+                    <otherwise>
+                        AND o.merchant_nick_name LIKE CONCAT('%', #{dto.merchantKeyword}, '%')
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="dto.paymentMethod != null">
+                AND o.payment_method = #{dto.paymentMethod}
+            </if>
+            <choose>
+                <when test="dto.abnormalOrder != null and dto.abnormalOrder">
+                    AND o.status IN (7, 8, 9)
+                </when>
+                <when test="dto.status != null">
+                    AND o.status = #{dto.status}
+                </when>
+            </choose>
+            <if test="dto.merchantType != null">
+                AND o.merchant_type = #{dto.merchantType}
+            </if>
+        </where>
+        ORDER BY o.create_time DESC
+    </select>
+
+    <!-- 查询服务分类选项 -->
+    <select id="selectServiceCategoryOptions" resultType="com.ylx.order.domain.vo.AdminOrderServiceCategoryVO">
+        SELECT
+            id,
+            service_tag AS serviceTag,
+            name
+        FROM service_category
+        WHERE is_delete = 0
+        ORDER BY sort ASC, id ASC
+    </select>
+
+    <!-- 根据订单ID逻辑删除服务订单 -->
+    <update id="logicDeleteById">
+        UPDATE t_order
+        SET is_delete = 1,
+            deleted_time = NOW(),
+            update_time = NOW()
+        WHERE id = #{id}
+          AND is_delete = 0
+    </update>
+
+    <select id="selectRefundApplicationOrderById" resultType="com.ylx.massage.domain.CancelOrderApplication">
+        SELECT
+            o.order_no AS orderNo,
+            COALESCE(u.c_openid, CAST(o.user_id AS CHAR)) AS openId,
+            COALESCE(o.contact_person_name, u.c_nick_name) AS userName,
+            COALESCE(o.contact_phone_number, u.c_phone) AS userPhone,
+            CAST(o.merchant_id AS CHAR) AS techId,
+            mt.te_name AS techName,
+            COALESCE(mt.te_nick_name, o.merchant_nick_name) AS techNickName,
+            mt.te_phone AS techPhone,
+            COALESCE(p.title, o.project_name) AS projectName,
+            CAST(o.project_duration AS CHAR) AS projectDuration,
+            CAST(o.project_duration AS CHAR) AS serviceDuration,
+            COALESCE(o.final_amount, 0) AS orderAmount,
+            COALESCE(o.final_amount, 0) AS refundAmount,
+            o.status AS orderStatus
+        FROM t_order o
+        LEFT JOIN t_wx_user u ON u.id = CAST(o.user_id AS CHAR) AND u.is_delete = 0
+        LEFT JOIN ma_technician mt ON mt.id = o.merchant_id AND mt.is_delete = 0
+        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="countPendingRefundApplicationByOrderNo" resultType="java.lang.Integer">
+        SELECT COUNT(1)
+        FROM t_cancel_order_application
+        WHERE order_no = #{orderNo}
+          AND audit_status = 0
+          AND is_delete = 0
+    </select>
+
+    <insert id="insertRefundApplication">
+        INSERT INTO t_cancel_order_application (
+            id,
+            order_no,
+            open_id,
+            user_name,
+            user_phone,
+            tech_id,
+            tech_name,
+            tech_nick_name,
+            tech_phone,
+            project_name,
+            project_duration,
+            service_duration,
+            order_amount,
+            refund_amount,
+            order_status,
+            audit_status,
+            application_time,
+            cancel_order_reason,
+            create_time,
+            update_time,
+            is_delete
+        ) VALUES (
+            #{application.id},
+            #{application.orderNo},
+            #{application.openId},
+            #{application.userName},
+            #{application.userPhone},
+            #{application.techId},
+            #{application.techName},
+            #{application.techNickName},
+            #{application.techPhone},
+            #{application.projectName},
+            #{application.projectDuration},
+            #{application.serviceDuration},
+            #{application.orderAmount},
+            #{application.refundAmount},
+            #{application.orderStatus},
+            #{application.auditStatus},
+            #{application.applicationTime},
+            #{application.cancelOrderReason},
+            #{application.createTime},
+            #{application.updateTime},
+            #{application.isDelete}
+        )
+    </insert>
+
+    <select id="selectOrderDetailInfoById" resultType="com.ylx.order.domain.vo.AdminOrderDetailVO$OrderInfoVO">
+        SELECT
+            o.id,
+            o.order_no AS orderNo,
+            o.status,
+            u.c_nick_name AS userNickName,
+            o.contact_person_name AS contactPersonName,
+            o.contact_phone_number AS contactPhoneNumber,
+            o.contact_address_info AS contactAddressInfo,
+            COALESCE(mt.te_nick_name, o.merchant_nick_name) AS merchantNickName,
+            mt.te_phone AS merchantPhone,
+            DATE_FORMAT(o.appointment_start_time, '%Y-%m-%d %H:%i:%s') AS appointmentStartTime,
+            DATE_FORMAT(o.start_time, '%Y-%m-%d %H:%i:%s') AS startTime,
+            DATE_FORMAT(o.completed_time, '%Y-%m-%d %H:%i:%s') AS completedTime,
+            DATE_FORMAT(o.create_time, '%Y-%m-%d %H:%i:%s') AS createTime,
+            DATE_FORMAT(o.paid_time, '%Y-%m-%d %H:%i:%s') AS paidTime,
+            o.payment_method AS paymentMethod,
+            o.project_duration AS projectDuration,
+            o.user_latitude AS userLatitude,
+            o.user_longitude AS userLongitude,
+            o.merchant_latitude AS merchantLatitude,
+            o.merchant_longitude AS merchantLongitude,
+            o.virtual_latitude AS virtualLatitude,
+            o.virtual_longitude AS virtualLongitude,
+            o.start_photo AS startPhoto,
+            ass.id AS afterSalesServiceId,
+            ass.status AS afterSalesServiceStatus
+        FROM t_order o
+        LEFT JOIN t_wx_user u ON u.id = CAST(o.user_id AS CHAR) AND u.is_delete = 0
+        LEFT JOIN ma_technician mt ON mt.id = o.merchant_id AND mt.is_delete = 0
+        LEFT JOIN after_sales_service ass ON ass.id = (
+            SELECT ass_inner.id
+            FROM after_sales_service ass_inner
+            WHERE ass_inner.order_id = o.id
+              AND ass_inner.is_delete = 0
+            ORDER BY ass_inner.create_time DESC, ass_inner.id DESC
+            LIMIT 1
+        )
+        WHERE o.id = #{id}
+          AND o.is_delete = 0
+        LIMIT 1
+    </select>
+
+    <select id="selectOrderProjectDetailById" resultType="com.ylx.order.domain.vo.AdminOrderDetailVO$ProjectInfoVO">
+        SELECT
+            COALESCE(p.title, o.project_name) AS projectName,
+            COALESCE(p.cover, o.project_cover) AS projectCover,
+            p.unit_type AS unitType,
+            COALESCE(p.price, o.base_price) AS unitPrice,
+            p.merchant_share_ratio AS merchantCommission,
+            o.final_amount AS finalAmount,
+            o.base_price AS basePrice,
+            o.traffic_fee AS trafficFee,
+            o.coupon_discount AS couponDiscount,
+            ass.id AS afterSalesServiceId,
+            ass.status AS afterSalesServiceStatus
+        FROM t_order o
+        LEFT JOIN project p ON p.id = o.project_id AND p.is_delete = 0
+        LEFT JOIN after_sales_service ass ON ass.id = (
+            SELECT ass_inner.id
+            FROM after_sales_service ass_inner
+            WHERE ass_inner.order_id = o.id
+              AND ass_inner.is_delete = 0
+            ORDER BY ass_inner.create_time DESC, ass_inner.id DESC
+            LIMIT 1
+        )
+        WHERE o.id = #{id}
+          AND o.is_delete = 0
+        LIMIT 1
+    </select>
+
+    <select id="selectOrderOperationLogs" resultType="com.ylx.order.domain.vo.AdminOrderDetailVO$OperationLogVO">
+        SELECT
+            COALESCE(osf.create_by, '系统') AS operator,
+            DATE_FORMAT(osf.create_time, '%Y-%m-%d %H:%i:%s') AS operationTime,
+            osf.status
+        FROM t_order_status_flow osf
+        WHERE osf.order_id = #{id}
+        ORDER BY osf.create_time ASC
+    </select>
+</mapper>

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

@@ -0,0 +1,96 @@
+package com.ylx.order.mapper;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AdminOrderMapperXmlTest {
+
+    @Test
+    public void adminOrderPageSqlJoinsOrderUserMerchantProjectAndCategory() throws Exception {
+        String xml = readMapperXml();
+
+        assertTrue(xml.contains("selectAdminOrderPage"));
+        assertTrue(xml.contains("FROM t_order o"));
+        assertTrue(xml.contains("LEFT JOIN t_wx_user u ON u.id = CAST(o.user_id AS CHAR)"));
+        assertTrue(xml.contains("LEFT JOIN ma_technician mt ON mt.id = o.merchant_id"));
+        assertTrue(xml.contains("LEFT JOIN project p ON p.id = o.project_id"));
+        assertTrue(xml.contains("LEFT JOIN service_category sc ON sc.id = p.category_id"));
+        assertTrue(xml.contains("o.order_no LIKE CONCAT('%', #{dto.orderNo}, '%')"));
+        assertTrue(xml.contains("u.c_phone LIKE CONCAT('%', #{dto.userKeyword}, '%')"));
+        assertTrue(xml.contains("u.c_nick_name LIKE CONCAT('%', #{dto.userKeyword}, '%')"));
+        assertTrue(xml.contains("o.contact_person_name LIKE CONCAT('%', #{dto.userKeyword}, '%')"));
+        assertTrue(xml.contains("mt.te_phone LIKE CONCAT('%', #{dto.merchantKeyword}, '%')"));
+        assertTrue(xml.contains("o.merchant_nick_name LIKE CONCAT('%', #{dto.merchantKeyword}, '%')"));
+        assertTrue(xml.contains("p.category_id = #{dto.categoryId}"));
+        assertTrue(xml.contains("o.create_time DESC"));
+    }
+
+    @Test
+    public void serviceCategoryOptionSqlReturnsAllCreatedCategories() throws Exception {
+        String xml = readMapperXml();
+
+        assertTrue(xml.contains("selectServiceCategoryOptions"));
+        assertTrue(xml.contains("FROM service_category"));
+        assertTrue(xml.contains("is_delete = 0"));
+        assertTrue(xml.contains("ORDER BY sort ASC, id ASC"));
+    }
+
+    @Test
+    public void logicDeleteByIdOnlyMarksExistingOrderDeleted() throws Exception {
+        String xml = readMapperXml();
+
+        assertTrue(xml.contains("update id=\"logicDeleteById\""));
+        assertTrue(xml.contains("UPDATE t_order"));
+        assertTrue(xml.contains("is_delete = 1"));
+        assertTrue(xml.contains("deleted_time = NOW()"));
+        assertTrue(xml.contains("WHERE id = #{id}"));
+        assertTrue(xml.contains("AND is_delete = 0"));
+    }
+
+    @Test
+    public void refundApplicationSqlReadsOrderChecksPendingAndInsertsApplication() throws Exception {
+        String xml = readMapperXml();
+
+        assertTrue(xml.contains("selectRefundApplicationOrderById"));
+        assertTrue(xml.contains("o.order_no AS orderNo"));
+        assertTrue(xml.contains("COALESCE(u.c_openid, CAST(o.user_id AS CHAR)) AS openId"));
+        assertTrue(xml.contains("COALESCE(o.final_amount, 0) AS refundAmount"));
+        assertTrue(xml.contains("countPendingRefundApplicationByOrderNo"));
+        assertTrue(xml.contains("FROM t_cancel_order_application"));
+        assertTrue(xml.contains("audit_status = 0"));
+        assertTrue(xml.contains("insertRefundApplication"));
+        assertTrue(xml.contains("cancel_order_reason"));
+        assertTrue(xml.contains("#{application.cancelOrderReason}"));
+    }
+
+    @Test
+    public void adminOrderDetailSqlReadsOrderAfterSaleProjectVoucherAndFlow() throws Exception {
+        String xml = readMapperXml();
+
+        assertTrue(xml.contains("selectOrderDetailInfoById"));
+        assertTrue(xml.contains("o.start_photo AS startPhoto"));
+        assertTrue(xml.contains("LEFT JOIN after_sales_service ass"));
+        assertTrue(xml.contains("ass_inner.order_id = o.id"));
+        assertTrue(xml.contains("selectOrderProjectDetailById"));
+        assertTrue(xml.contains("p.merchant_share_ratio AS merchantCommission"));
+        assertTrue(xml.contains("selectOrderOperationLogs"));
+        assertTrue(xml.contains("FROM t_order_status_flow"));
+        assertTrue(xml.contains("WHERE osf.order_id = #{id}"));
+        assertTrue(xml.contains("ORDER BY osf.create_time ASC"));
+    }
+
+    private String readMapperXml() throws Exception {
+        try (InputStream inputStream = getClass().getClassLoader()
+                .getResourceAsStream("mapper/order/AdminOrderMapper.xml")) {
+            assertNotNull(inputStream, "AdminOrderMapper.xml should exist in test classpath");
+            byte[] bytes = new byte[inputStream.available()];
+            inputStream.read(bytes);
+            return new String(bytes, StandardCharsets.UTF_8);
+        }
+    }
+}

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

@@ -0,0 +1,200 @@
+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.dto.AdminOrderQueryDTO;
+import com.ylx.order.domain.vo.AdminOrderDetailVO;
+import com.ylx.order.domain.vo.AdminOrderPageVO;
+import com.ylx.order.mapper.AdminOrderMapper;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AdminOrderServiceImplTest {
+
+    @Test
+    public void adminOrderPageRejectsStartTimeAfterEndTime() {
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        AdminOrderQueryDTO dto = new AdminOrderQueryDTO();
+        dto.setStartTime("2026-06-12 00:00:00");
+        dto.setEndTime("2026-06-11 00:00:00");
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.page(new Page<AdminOrderPageVO>(), dto));
+
+        assertEquals("开始时间不能大于结束时间", exception.getMessage());
+    }
+
+    @Test
+    public void deleteByIdRejectsEmptyId() {
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.deleteById(null));
+
+        assertEquals("订单ID不能为空", exception.getMessage());
+    }
+
+    @Test
+    public void deleteByIdRejectsInvalidId() {
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.deleteById(0L));
+
+        assertEquals("订单ID不正确", exception.getMessage());
+    }
+
+    @Test
+    public void deleteByIdThrowsWhenOrderMissingOrDeleted() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        when(mapper.logicDeleteById(1L)).thenReturn(0);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.deleteById(1L));
+
+        assertEquals("订单不存在或已删除", exception.getMessage());
+    }
+
+    @Test
+    public void deleteByIdLogicDeletesExistingOrder() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        when(mapper.logicDeleteById(1L)).thenReturn(1);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        service.deleteById(1L);
+
+        verify(mapper).logicDeleteById(1L);
+    }
+
+    @Test
+    public void applyRefundRejectsEmptyId() {
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.applyRefund(null));
+
+        assertEquals("订单ID不能为空", exception.getMessage());
+    }
+
+    @Test
+    public void applyRefundRejectsMissingOrder() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        when(mapper.selectRefundApplicationOrderById(1L)).thenReturn(null);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.applyRefund(1L));
+
+        assertEquals("订单不存在或已删除", exception.getMessage());
+    }
+
+    @Test
+    public void applyRefundRejectsPendingApplication() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        CancelOrderApplication application = buildRefundApplicationOrder();
+        when(mapper.selectRefundApplicationOrderById(1L)).thenReturn(application);
+        when(mapper.countPendingRefundApplicationByOrderNo("NO001")).thenReturn(1);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.applyRefund(1L));
+
+        assertEquals("该订单已存在待审核退款申请", exception.getMessage());
+    }
+
+    @Test
+    public void applyRefundCreatesApplicationWithDefaultReason() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        CancelOrderApplication application = buildRefundApplicationOrder();
+        when(mapper.selectRefundApplicationOrderById(1L)).thenReturn(application);
+        when(mapper.countPendingRefundApplicationByOrderNo("NO001")).thenReturn(0);
+        when(mapper.insertRefundApplication(org.mockito.ArgumentMatchers.any(CancelOrderApplication.class))).thenReturn(1);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        service.applyRefund(1L);
+
+        ArgumentCaptor<CancelOrderApplication> captor = ArgumentCaptor.forClass(CancelOrderApplication.class);
+        verify(mapper).insertRefundApplication(captor.capture());
+        CancelOrderApplication saved = captor.getValue();
+        assertNotNull(saved.getId());
+        assertEquals("NO001", saved.getOrderNo());
+        assertEquals("平台发起退款申请", saved.getCancelOrderReason());
+        assertEquals(0, saved.getAuditStatus());
+        assertEquals(0, saved.getIsDelete());
+        assertEquals(new BigDecimal("100.00"), saved.getRefundAmount());
+        assertNotNull(saved.getApplicationTime());
+        assertNotNull(saved.getCreateTime());
+        assertNotNull(saved.getUpdateTime());
+    }
+
+    @Test
+    public void detailRejectsMissingOrder() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        when(mapper.selectOrderDetailInfoById(1L)).thenReturn(null);
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.detail(1L));
+
+        assertEquals("订单不存在或已删除", exception.getMessage());
+    }
+
+    @Test
+    public void detailBuildsVoucherListAndOperationContent() {
+        AdminOrderMapper mapper = mock(AdminOrderMapper.class);
+        AdminOrderDetailVO.OrderInfoVO orderInfo = new AdminOrderDetailVO.OrderInfoVO();
+        orderInfo.setStatus(4);
+        orderInfo.setPaymentMethod(1);
+        orderInfo.setAfterSalesServiceStatus(3);
+        orderInfo.setStartPhoto("a.jpg,b.jpg");
+        AdminOrderDetailVO.ProjectInfoVO projectInfo = new AdminOrderDetailVO.ProjectInfoVO();
+        projectInfo.setMerchantCommission(new BigDecimal("80.00"));
+        AdminOrderDetailVO.OperationLogVO log = new AdminOrderDetailVO.OperationLogVO();
+        log.setStatus(4);
+
+        when(mapper.selectOrderDetailInfoById(1L)).thenReturn(orderInfo);
+        when(mapper.selectOrderProjectDetailById(1L)).thenReturn(projectInfo);
+        when(mapper.selectOrderOperationLogs(1L)).thenReturn(Collections.singletonList(log));
+        AdminOrderServiceImpl service = new AdminOrderServiceImpl();
+        ReflectionTestUtils.setField(service, "adminOrderMapper", mapper);
+
+        AdminOrderDetailVO detail = service.detail(1L);
+
+        assertEquals("服务中", detail.getOrderInfo().getStatusName());
+        assertEquals("微信支付", detail.getOrderInfo().getPaymentMethodName());
+        assertEquals("退款成功", detail.getOrderInfo().getAfterSalesServiceStatusName());
+        assertEquals(2, detail.getServiceVoucherList().size());
+        assertEquals("a.jpg", detail.getServiceVoucherList().get(0));
+        assertEquals(new BigDecimal("80.00"), detail.getProjectInfo().getMerchantCommission());
+        assertEquals("服务中", detail.getOperationLogList().get(0).getOperationContent());
+        assertEquals("服务中", detail.getOperationLogList().get(0).getStatusName());
+    }
+
+    private CancelOrderApplication buildRefundApplicationOrder() {
+        CancelOrderApplication application = new CancelOrderApplication();
+        application.setOrderNo("NO001");
+        application.setOrderAmount(new BigDecimal("100.00"));
+        application.setRefundAmount(new BigDecimal("100.00"));
+        application.setOrderStatus(5);
+        return application;
+    }
+}