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