Sfoglia il codice sorgente

售后单详情接口;完善提交售后单校验

wangzhijun 14 ore fa
parent
commit
d118686c82

+ 9 - 0
nightFragrance-massage/src/main/java/com/ylx/order/controller/AfterSalesServiceController.java

@@ -8,6 +8,7 @@ import com.ylx.common.utils.DictUtils;
 import com.ylx.order.domain.dto.AfterSalesServiceDTO;
 import com.ylx.order.domain.vo.RefundCalculationVO;
 import com.ylx.order.domain.vo.RegulationConfigVO;
+import com.ylx.order.domain.vo.afterSalesService.AfterSalesServiceDetailVO;
 import com.ylx.order.enums.AfterSaleServiceDictTypeEnum;
 import com.ylx.order.service.IAfterSalesServiceService;
 import com.ylx.order.service.RegulationService;
@@ -74,6 +75,14 @@ public class AfterSalesServiceController {
         return R.ok(vo);
     }
 
+    @PreAuthorize("@customerAuth.isCustomer()")
+    @ApiOperation("客户端查看售后单详情")
+    @GetMapping("/detail/{id}")
+    public R<AfterSalesServiceDetailVO> getDetailById(@PathVariable("id") Long id) {
+        AfterSalesServiceDetailVO vo = this.afterSalesServiceService.getDetailById(id);
+        return R.ok(vo);
+    }
+
 
     @ApiOperation("根据商户履约状态获取退款描述")
     @GetMapping("/desc/list")

+ 3 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/AfterSalesService.java

@@ -34,6 +34,9 @@ public class AfterSalesService extends BaseEntity {
     @ApiModelProperty("售后状态:0=待审核, 1=审核通过(退款中), 2=审核拒绝, 3=退款成功, 4=退款失败 ")
     private Integer status;
 
+    @ApiModelProperty("售后原因")
+    private String reason;
+
     @ApiModelProperty("退款金额")
     private BigDecimal actualRefundAmount;
 

+ 3 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/AfterSalesServiceDTO.java

@@ -16,6 +16,9 @@ public class AfterSalesServiceDTO implements Serializable {
     @ApiModelProperty("关联的主订单ID")
     private Long orderId;
 
+    @ApiModelProperty("售后原因")
+    private String reason;
+
     @ApiModelProperty("退款说明")
     private String remark;
 

+ 71 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/afterSalesService/AfterSalesServiceDetailVO.java

@@ -0,0 +1,71 @@
+package com.ylx.order.domain.vo.afterSalesService;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@ApiModel("售后服务详情VO")
+@Data
+public class AfterSalesServiceDetailVO {
+
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @ApiModelProperty("售后单号")
+    private String serviceNo;
+
+    @ApiModelProperty("关联的主订单ID")
+    private Long orderId;
+
+    @ApiModelProperty("售后状态:0=待审核, 1=审核通过(退款中), 2=审核拒绝, 3=退款成功, 4=退款失败 ")
+    private Integer status;
+
+    @ApiModelProperty("退款金额")
+    private BigDecimal actualRefundAmount;
+
+    @ApiModelProperty("退款规则描述")
+    private String refundDesc;
+
+    @ApiModelProperty("审核时间")
+    private LocalDateTime auditTime;
+
+    @ApiModelProperty("拒绝原因")
+    private String rejectReason;
+
+    @ApiModelProperty("审核备注")
+    private String auditRemark;
+
+    @ApiModelProperty("退款成功时间")
+    private LocalDateTime refundSuccessTime;
+
+    @ApiModelProperty("商户ID")
+    private Long merchantId;
+
+    @ApiModelProperty("商户昵称")
+    private String merchantNickName;
+
+    @ApiModelProperty("项目ID")
+    private Long projectId;
+
+    @ApiModelProperty("项目名称")
+    private String projectName;
+
+    @ApiModelProperty("项目封面图")
+    private String projectCover;
+
+    @ApiModelProperty("详细服务地址")
+    private String contactAddressInfo;
+
+    @ApiModelProperty("项目标价/售价")
+    private BigDecimal basePrice;
+
+    @ApiModelProperty("最终应付/实付金额")
+    private BigDecimal finalAmount;
+
+    @ApiModelProperty("订单状态:0=待付款,1=待派单,2=待接单,3=待服务,4=服务中,5=售后中,6=已完成,7=已退款,8=已取消,9=已关闭,10=拒绝接单")
+    private Integer orderStatus;
+
+}

+ 2 - 0
nightFragrance-massage/src/main/java/com/ylx/order/mapper/AfterSalesServiceMapper.java

@@ -2,6 +2,8 @@ package com.ylx.order.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ylx.order.domain.AfterSalesService;
+import com.ylx.order.domain.vo.afterSalesService.AfterSalesServiceDetailVO;
 
 public interface AfterSalesServiceMapper extends BaseMapper<AfterSalesService> {
+    AfterSalesServiceDetailVO getDetailById(Long id);
 }

+ 3 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/IAfterSalesServiceService.java

@@ -4,9 +4,12 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.ylx.order.domain.AfterSalesService;
 import com.ylx.order.domain.dto.AfterSalesServiceDTO;
 import com.ylx.order.domain.vo.RefundCalculationVO;
+import com.ylx.order.domain.vo.afterSalesService.AfterSalesServiceDetailVO;
 
 public interface IAfterSalesServiceService extends IService<AfterSalesService> {
     void submitAfterSale(AfterSalesServiceDTO dto);
 
     RefundCalculationVO calculateRefund(AfterSalesServiceDTO dto);
+
+    AfterSalesServiceDetailVO getDetailById(Long id);
 }

+ 192 - 78
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/AfterSalesServiceServiceImpl.java

@@ -1,6 +1,7 @@
 package com.ylx.order.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.UUID;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -13,6 +14,7 @@ import com.ylx.order.domain.RefundRuleDetail;
 import com.ylx.order.domain.TOrder;
 import com.ylx.order.domain.dto.AfterSalesServiceDTO;
 import com.ylx.order.domain.vo.RefundCalculationVO;
+import com.ylx.order.domain.vo.afterSalesService.AfterSalesServiceDetailVO;
 import com.ylx.order.enums.AfterSaleServiceStatusEnum;
 import com.ylx.order.enums.OrderStatusEnum;
 import com.ylx.order.enums.RefundStageTypeEnum;
@@ -29,10 +31,16 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.Duration;
 import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
 
+/**
+ * 售后单服务实现
+ *
+ * @author xxx
+ */
 @Slf4j
 @Service
 public class AfterSalesServiceServiceImpl extends ServiceImpl<AfterSalesServiceMapper, AfterSalesService>
@@ -43,168 +51,274 @@ public class AfterSalesServiceServiceImpl extends ServiceImpl<AfterSalesServiceM
     @Resource
     private RefundRuleDetailService refundRuleDetailService;
 
+    // ===================== 业务常量 =====================
+    /**
+     * 订单完成后允许退款最大时长(小时)
+     */
+    private static final int MAX_REFUND_HOURS = 48;
+    /**
+     * 售后单号前缀
+     */
+    private static final String SERVICE_NO_PREFIX = "ASS";
+
+    // ===================== 对外接口 =====================
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void submitAfterSale(AfterSalesServiceDTO dto) {
-
-        // 1. 获取当前用户
+        // 1. 登录校验
         WxLoginUser wxLoginUser = SecurityUtils.getWxLoginUser();
         if (ObjectUtil.isNull(wxLoginUser)) {
-            log.warn("用户未登录,无法创建订单");
-            throw new ServiceException("用户未登录");
+            log.warn("提交售后失败:用户未登录");
+            throw new ServiceException("请先登录");
         }
-
         Long currentUserId = Long.valueOf(wxLoginUser.getId());
 
-        // 2. 获取订单信息
-        TOrder order = this.tOrderService.getById(dto.getOrderId());
+        // 2. 订单存在 & 权限校验
+        Long orderId = dto.getOrderId();
+        TOrder order = tOrderService.getById(orderId);
         if (ObjectUtil.isNull(order) || ObjectUtil.equals(1, order.getIsDelete())) {
             throw new ServiceException("订单不存在");
         }
-        if (ObjectUtil.notEqual(order.getUserId(), currentUserId)) {
-            throw new ServiceException("您无权操作此订单");
+        if (!currentUserId.equals(order.getUserId())) {
+            throw new ServiceException("无权操作他人订单");
         }
 
+        // 3. 前置业务校验(顺序不可乱)
         validateOrderStatus(order);
-        validateNoPendingAfterSale(dto.getOrderId());
+        validateNoPendingAfterSale(orderId);
+        validateNoRefundedAfterSale(orderId);
 
+        // 4. 计算退款金额
         RefundCalculationVO refundResult = calculateRefund(order);
 
-        AfterSalesService afterSalesService = new AfterSalesService();
-        afterSalesService.setServiceNo(generateServiceNo(dto.getOrderId()));
-        afterSalesService.setOrderId(dto.getOrderId());
-        afterSalesService.setUserId(currentUserId);
-        afterSalesService.setStatus(AfterSaleServiceStatusEnum.PENDING_AUDIT.getCode());
-        afterSalesService.setActualRefundAmount(refundResult.getRefundAmount());
-        afterSalesService.setRefundDesc(refundResult.getRefundDesc());
-        afterSalesService.setCreateTime(DateUtils.getNowDate());
-        afterSalesService.setRemark(dto.getRemark());
-
-        if (!this.save(afterSalesService)) {
-            throw new ServiceException("创建售后服务单失败");
+        // 5. 构建售后单并保存
+        AfterSalesService afterSalesService = buildAfterSalesService(dto, orderId, currentUserId, refundResult);
+        boolean saveSuccess = save(afterSalesService);
+        if (!saveSuccess) {
+            throw new ServiceException("创建售后单失败,请稍后重试");
         }
 
-        log.info("用户提交售后成功, orderId={}, serviceNo={}, refundAmount={}",
-                dto.getOrderId(), afterSalesService.getServiceNo(), refundResult.getRefundAmount());
+        log.info("售后提交成功,orderId={},serviceNo={},退款金额={}",
+                orderId, afterSalesService.getServiceNo(), refundResult.getRefundAmount());
     }
 
     @Override
     public RefundCalculationVO calculateRefund(AfterSalesServiceDTO dto) {
-
-        TOrder order = this.tOrderService.getById(dto.getOrderId());
+        Long orderId = dto.getOrderId();
+        TOrder order = tOrderService.getById(orderId);
         if (ObjectUtil.isNull(order) || ObjectUtil.equals(1, order.getIsDelete())) {
             throw new ServiceException("订单不存在");
         }
-
         return calculateRefund(order);
     }
 
+    @Override
+    public AfterSalesServiceDetailVO getDetailById(Long id) {
+        AfterSalesServiceDetailVO detailVO = baseMapper.getDetailById(id);
+        if (ObjectUtil.isNull(detailVO)) {
+            throw new ServiceException("暂无该售后单详情");
+        }
+        return detailVO;
+    }
+
+    // ===================== 校验类私有方法 =====================
+
+    /**
+     * 校验订单状态是否允许发起售后
+     */
     private void validateOrderStatus(TOrder order) {
-        Integer status = order.getStatus();
-        if (OrderStatusEnum.IN_SERVICE.getCode().equals(status)) {
-            throw new ServiceException("操作错误,请重试");
+        Integer orderStatus = order.getStatus();
+        OrderStatusEnum statusEnum = OrderStatusEnum.fromCode(orderStatus);
+
+        switch (statusEnum) {
+            case IN_SERVICE:
+                log.info("订单[{}]服务中,禁止发起售后", order.getId());
+                throw new ServiceException("操作错误,请重试");
+            case CANCELLED:
+                log.info("订单[{}]已取消,禁止发起售后", order.getId());
+                throw new ServiceException("操作错误,请重试");
+            case COMPLETED:
+                validateCompletedRefundTime(order);
+                break;
+            default:
+                // 其他状态放行
+        }
+    }
+
+    /**
+     * 校验已完成订单退款时效
+     */
+    private void validateCompletedRefundTime(TOrder order) {
+        LocalDateTime completedTime = order.getCompletedTime();
+        if (ObjectUtil.isNull(completedTime)) {
+            throw new ServiceException("订单完成时间异常");
         }
-        if (OrderStatusEnum.CANCELLED.getCode().equals(status)) {
-            throw new ServiceException("已取消订单不支持售后");
+        long passHours = Duration.between(completedTime, LocalDateTime.now()).toHours();
+        if (passHours > MAX_REFUND_HOURS) {
+            log.info("订单[{}]已完成超过{}小时,禁止退款", order.getId(), MAX_REFUND_HOURS);
+            throw new ServiceException("已完成超过48小时的订单不支持退款");
         }
     }
 
+    /**
+     * 校验:不存在【待审核、已同意、已退款】等进行中的售后单
+     */
     private void validateNoPendingAfterSale(Long orderId) {
-        long count = this.count(new LambdaQueryWrapper<AfterSalesService>()
+        List<Integer> pendingStatus = Arrays.asList(
+                AfterSaleServiceStatusEnum.PENDING_AUDIT.getCode(),
+                AfterSaleServiceStatusEnum.APPROVED.getCode()
+        );
+
+        long pendingCount = lambdaQuery()
+                .eq(AfterSalesService::getOrderId, orderId)
+                .in(AfterSalesService::getStatus, pendingStatus)
+                .count();
+
+        if (pendingCount > 0) {
+            throw new ServiceException("该订单已有处理中的售后单,请勿重复提交");
+        }
+    }
+
+    /**
+     * 校验:该订单不存在【已退款】售后单
+     */
+    private void validateNoRefundedAfterSale(Long orderId) {
+        long refundedCount = lambdaQuery()
                 .eq(AfterSalesService::getOrderId, orderId)
-                .in(AfterSalesService::getStatus,
-                        AfterSaleServiceStatusEnum.PENDING_AUDIT.getCode(),
-                        AfterSaleServiceStatusEnum.APPROVED.getCode(),
-                        AfterSaleServiceStatusEnum.REFUND_SUCCESS.getCode()
-                ));
-        if (count > 0) {
-            throw new ServiceException("操作错误,请重试");
+                .eq(AfterSalesService::getStatus, AfterSaleServiceStatusEnum.REFUND_SUCCESS.getCode())
+                .count();
+
+        if (refundedCount > 0) {
+            log.info("订单[{}]已存在已退款售后单,禁止再次申请", orderId);
+            throw new ServiceException("该订单已完成退款,无法再次申请");
         }
     }
 
+    // ===================== 退款计算核心 =====================
     private RefundCalculationVO calculateRefund(TOrder order) {
-        Integer stageType = resolveStageType(order);
-        List<RefundRuleDetail> details = listDetailsByStage(stageType);
-        if (CollUtil.isEmpty(details)) {
-            throw new ServiceException("未配置退款规则,请联系客服");
+        Integer stageType = resolveRefundStageType(order);
+        List<RefundRuleDetail> ruleList = listRefundRuleByStage(stageType);
+
+        if (CollUtil.isEmpty(ruleList)) {
+            throw new ServiceException("暂无可用退款规则,请联系客服");
         }
 
-        RefundRuleDetail matchedRule;
+        RefundRuleDetail matchRule;
         if (RefundStageTypeEnum.PRE_DEPARTURE.getCode().equals(stageType)) {
-            matchedRule = matchPreDepartureRule(details, order.getAppointmentStartTime());
+            matchRule = matchPreDepartureRule(ruleList, order.getAppointmentStartTime());
         } else {
-            matchedRule = CollUtil.getFirst(details);
+            matchRule = CollUtil.getFirst(ruleList);
         }
 
-        BigDecimal percent = Optional.ofNullable(matchedRule.getRefundPercent()).orElse(BigDecimal.ZERO);
-        BigDecimal finalAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
-        BigDecimal refundAmount = finalAmount.multiply(percent)
+        BigDecimal refundPercent = Optional.ofNullable(matchRule.getRefundPercent()).orElse(BigDecimal.ZERO);
+        BigDecimal orderFinalAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
+        BigDecimal refundAmount = orderFinalAmount
+                .multiply(refundPercent)
                 .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
 
-        return new RefundCalculationVO(refundAmount, matchedRule.getRefundDesc());
+        return new RefundCalculationVO(refundAmount, matchRule.getRefundDesc());
     }
 
-    private Integer resolveStageType(TOrder order) {
+    /**
+     * 解析当前退款阶段类型
+     */
+    private Integer resolveRefundStageType(TOrder order) {
         if (OrderStatusEnum.IN_SERVICE.getCode().equals(order.getStatus())) {
             return RefundStageTypeEnum.IN_SERVICE.getCode();
         }
         return ObjectUtil.defaultIfNull(order.getExecStatus(), RefundStageTypeEnum.PRE_DEPARTURE.getCode());
     }
 
-    private List<RefundRuleDetail> listDetailsByStage(Integer stageType) {
+    /**
+     * 根据阶段查询退款规则
+     */
+    private List<RefundRuleDetail> listRefundRuleByStage(Integer stageType) {
         LambdaQueryWrapper<RefundRuleDetail> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(RefundRuleDetail::getStageType, stageType)
                 .eq(RefundRuleDetail::getIsDelete, 0)
                 .orderByAsc(RefundRuleDetail::getSortOrder)
                 .orderByAsc(RefundRuleDetail::getId);
-        return this.refundRuleDetailService.list(wrapper);
+        return refundRuleDetailService.list(wrapper);
     }
 
+    /**
+     * 匹配未出发阶段退款规则
+     */
     private RefundRuleDetail matchPreDepartureRule(List<RefundRuleDetail> details, LocalDateTime appointmentStartTime) {
-
-        // 预排序:确保按时间区间顺序匹配
         details.sort(Comparator.comparing(RefundRuleDetail::getSortOrder, Comparator.nullsLast(Integer::compareTo)));
+        RefundRuleDetail firstRule = CollUtil.getFirst(details);
 
-        RefundRuleDetail first = CollUtil.getFirst(details);
-        if (details.size() == 1 && ObjectUtil.equals(0, first.getRefundType())) {
-            return first;
+        if (details.size() == 1 && ObjectUtil.equals(0, firstRule.getRefundType())) {
+            return firstRule;
         }
 
-        BigDecimal hoursUntilStart = calcHoursUntilStart(appointmentStartTime);
-        if (hoursUntilStart.compareTo(BigDecimal.ZERO) <= 0) {
-            throw new ServiceException("已过预约服务时间,无法申请退款");
+        BigDecimal hoursLeft = calcHoursUntilAppointStart(appointmentStartTime);
+        if (hoursLeft.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new ServiceException("已过预约服务时间,无法退款");
         }
 
-        for (RefundRuleDetail detail : details) {
-            if (matchTimeRange(detail, hoursUntilStart)) {
-                return detail;
+        for (RefundRuleDetail rule : details) {
+            if (matchTimeRangeRule(rule, hoursLeft)) {
+                return rule;
             }
         }
-        throw new ServiceException("未匹配到适用的退款规则");
+        throw new ServiceException("未匹配到对应退款规则");
     }
 
-    private boolean matchTimeRange(RefundRuleDetail detail, BigDecimal hoursUntilStart) {
-        BigDecimal start = detail.getTimeStartHours();
-        BigDecimal end = detail.getTimeEndHours();
-        if (start != null && start.compareTo(BigDecimal.ZERO) > 0) {
-            return hoursUntilStart.compareTo(end) > 0 && hoursUntilStart.compareTo(start) <= 0;
+    /**
+     * 校验是否命中时间区间规则
+     */
+    private boolean matchTimeRangeRule(RefundRuleDetail detail, BigDecimal hoursLeft) {
+        BigDecimal timeStart = detail.getTimeStartHours();
+        BigDecimal timeEnd = detail.getTimeEndHours();
+
+        if (ObjectUtil.isNotNull(timeStart) && timeStart.compareTo(BigDecimal.ZERO) > 0) {
+            return hoursLeft.compareTo(timeEnd) > 0 && hoursLeft.compareTo(timeStart) <= 0;
         }
-        if (end != null) {
-            return hoursUntilStart.compareTo(end) <= 0;
+        if (ObjectUtil.isNotNull(timeEnd)) {
+            return hoursLeft.compareTo(timeEnd) <= 0;
         }
         return false;
     }
 
-    private BigDecimal calcHoursUntilStart(LocalDateTime appointmentStartTime) {
+    /**
+     * 计算距离预约开始剩余小时数
+     */
+    private BigDecimal calcHoursUntilAppointStart(LocalDateTime appointmentStartTime) {
         if (ObjectUtil.isNull(appointmentStartTime)) {
-            throw new ServiceException("订单预约时间缺失,无法计算退款金额");
+            throw new ServiceException("订单预约时间缺失,无法计算退款");
         }
         long minutes = Duration.between(LocalDateTime.now(), appointmentStartTime).toMinutes();
-        return BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
+        return BigDecimal.valueOf(minutes)
+                .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
+    }
+
+    // ===================== 构建 & 工具方法 =====================
+
+    /**
+     * 组装售后单实体
+     */
+    private AfterSalesService buildAfterSalesService(AfterSalesServiceDTO dto, Long orderId,
+                                                     Long userId, RefundCalculationVO refundVO) {
+        AfterSalesService service = new AfterSalesService();
+        service.setServiceNo(generateServiceNo(orderId));
+        service.setOrderId(orderId);
+        service.setUserId(userId);
+        service.setStatus(AfterSaleServiceStatusEnum.PENDING_AUDIT.getCode());
+        service.setActualRefundAmount(refundVO.getRefundAmount());
+        service.setRefundDesc(refundVO.getRefundDesc());
+        service.setCreateTime(DateUtils.getNowDate());
+        service.setRemark(dto.getRemark());
+        service.setReason(dto.getReason());
+        return service;
     }
 
+    /**
+     * 生成唯一售后单号(高可用,杜绝重复)
+     */
     private String generateServiceNo(Long orderId) {
-        return "ASS" + orderId + System.currentTimeMillis() + (int)(Math.random() * 1000);
+        // 前缀 + 订单ID + 时间戳 + 短UUID,全局唯一
+        return SERVICE_NO_PREFIX + orderId + System.currentTimeMillis() + UUID.fastUUID().toString(true).substring(0, 6);
     }
 
-}
+}

+ 59 - 0
nightFragrance-massage/src/main/resources/mapper/order/AfterSalesServiceMapper.xml

@@ -4,4 +4,63 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ylx.order.mapper.AfterSalesServiceMapper">
 
+
+    <!-- 定义结果映射:将数据库字段与 VO 属性一一对应 -->
+    <resultMap id="AfterSalesServiceDetailVOMap" type="com.ylx.order.domain.vo.afterSalesService.AfterSalesServiceDetailVO">
+        <!-- 售后单信息 (after_sales_service) -->
+        <id property="id" column="ass_id"/>
+        <result property="serviceNo" column="service_no"/>
+        <result property="orderId" column="order_id"/>
+        <result property="status" column="ass_status"/>
+        <result property="actualRefundAmount" column="actual_refund_amount"/>
+        <result property="refundDesc" column="refund_desc"/>
+        <result property="auditTime" column="audit_time"/>
+        <result property="rejectReason" column="reject_reason"/>
+        <result property="auditRemark" column="audit_remark"/>
+        <result property="refundSuccessTime" column="refund_success_time"/>
+
+        <!-- 订单/商户/项目信息 (t_order) -->
+        <result property="merchantId" column="merchant_id"/>
+        <result property="merchantNickName" column="merchant_nick_name"/>
+        <result property="projectId" column="project_id"/>
+        <result property="projectName" column="project_name"/>
+        <result property="projectCover" column="project_cover"/>
+        <result property="contactAddressInfo" column="contact_address_info"/>
+        <result property="basePrice" column="base_price"/>
+        <result property="finalAmount" column="final_amount"/>
+        <result property="orderStatus" column="order_status"/>
+    </resultMap>
+
+    <!-- 根据售后单ID查询详情 -->
+    <select id="getDetailById" parameterType="java.lang.Long" resultMap="AfterSalesServiceDetailVOMap">
+        SELECT
+            -- 1. 售后单核心字段
+            ass.id AS ass_id,
+            ass.service_no,
+            ass.order_id,
+            ass.status AS ass_status,
+            ass.actual_refund_amount,
+            ass.refund_desc,
+            ass.audit_time,
+            ass.reject_reason,
+            ass.audit_remark,
+            ass.refund_success_time,
+
+            -- 2. 关联订单、商户及项目冗余字段
+            o.merchant_id,
+            o.merchant_nick_name,
+            o.project_id,
+            o.project_name,
+            o.project_cover,
+            o.contact_address_info,
+            o.base_price,
+            o.final_amount,
+            o.status AS order_status
+
+        FROM after_sales_service ass
+                 LEFT JOIN t_order o ON ass.order_id = o.id
+        WHERE ass.id = #{id}
+          AND ass.is_delete = 0
+    </select>
+
 </mapper>