diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/controller/PlayOrderInfoController.java b/play-admin/src/main/java/com/starry/admin/modules/order/controller/PlayOrderInfoController.java index 7844b5f..ab088fa 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/controller/PlayOrderInfoController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/controller/PlayOrderInfoController.java @@ -124,6 +124,18 @@ public class PlayOrderInfoController { return R.ok("退款成功"); } + /** + * 管理后台强制取消进行中订单 + */ + @ApiOperation(value = "强制取消订单", notes = "管理员强制取消已接单或服务中的订单") + @ApiResponses({@ApiResponse(code = 200, message = "操作成功"), @ApiResponse(code = 500, message = "操作失败")}) + @PostMapping("/forceCancel") + public R forceCancel(@ApiParam(value = "取消参数", required = true) @Validated @RequestBody PlayOrderForceCancelVo vo) { + orderInfoService.forceCancelOngoingOrder("2", CustomSecurityContextHolder.getUserId(), vo.getOrderId(), + vo.getRefundAmount(), vo.getRefundReason(), vo.getImages()); + return R.ok("操作成功"); + } + /** * 更换店员 */ diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/module/vo/PlayOrderForceCancelVo.java b/play-admin/src/main/java/com/starry/admin/modules/order/module/vo/PlayOrderForceCancelVo.java new file mode 100644 index 0000000..5f72300 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/order/module/vo/PlayOrderForceCancelVo.java @@ -0,0 +1,42 @@ +package com.starry.admin.modules.order.module.vo; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 强制取消订单请求对象 + * + *

用于管理员或店员在订单已接单/服务中时发起取消操作。

+ */ +@Data +public class PlayOrderForceCancelVo { + + /** + * 订单ID + */ + @NotBlank(message = "订单ID不能为空") + private String orderId; + + /** + * 退款原因 + */ + @NotBlank(message = "请填写退款原因") + @Size(max = 200, message = "退款原因不能超过200个字符") + private String refundReason; + + /** + * 退款金额,可选。不填默认退回订单支付金额 + */ + @DecimalMin(value = "0.00", message = "退款金额不能小于0") + private BigDecimal refundAmount; + + /** + * 退款凭证图片 + */ + private List images = new ArrayList<>(); +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java index f0c8af2..17a315c 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java @@ -271,6 +271,19 @@ public interface IPlayOrderInfoService extends IService { void updateStateTo4(String operatorByType, String operatorBy, String orderId, String refundReason, List images); + /** + * 已接单/服务中的订单强制取消 + * + * @param operatorByType 操作人类型(1:店员;2:管理员) + * @param operatorBy 操作人ID + * @param orderId 订单ID + * @param refundAmount 退款金额(为空则退回实际支付金额) + * @param refundReason 取消原因 + * @param images 退款凭证图片 + */ + void forceCancelOngoingOrder(String operatorByType, String operatorBy, String orderId, BigDecimal refundAmount, + String refundReason, List images); + /** * 修改订单 * diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java index 0fdd6ce..d17d9bb 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java @@ -1015,6 +1015,56 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl images) { + if (!"2".equals(operatorByType)) { + throw new CustomException("禁止操作"); + } + PlayOrderInfoEntity orderInfo = this.selectOrderInfoById(orderId); + if (!OrderConstant.ORDER_STATUS_1.equals(orderInfo.getOrderStatus()) + && !OrderConstant.ORDER_STATUS_2.equals(orderInfo.getOrderStatus())) { + throw new CustomException("订单状态异常,无法取消"); + } + BigDecimal actualRefundAmount = refundAmount != null ? refundAmount : orderInfo.getFinalAmount(); + if (actualRefundAmount.compareTo(BigDecimal.ZERO) < 0) { + throw new CustomException("退款金额不能小于0"); + } + if (actualRefundAmount.compareTo(orderInfo.getFinalAmount()) > 0) { + throw new CustomException("退款金额不能大于支付金额"); + } + + LocalDateTime now = LocalDateTime.now(); + PlayOrderInfoEntity updateEntity = new PlayOrderInfoEntity(orderId, OrderConstant.ORDER_STATUS_4); + updateEntity.setRefundAmount(actualRefundAmount); + updateEntity.setRefundReason(refundReason); + updateEntity.setRefundType("1"); + updateEntity.setOrderCancelTime(now); + if (OrderConstant.ORDER_STATUS_2.equals(orderInfo.getOrderStatus())) { + updateEntity.setOrderEndTime(now); + } + this.baseMapper.updateById(updateEntity); + + PlayCustomUserInfoEntity customUserInfo = customUserInfoService.getById(orderInfo.getPurchaserBy()); + if (customUserInfo == null) { + throw new CustomException("顾客信息不存在"); + } + + customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), + customUserInfo.getAccountBalance().add(actualRefundAmount), "3", "订单取消退款", + actualRefundAmount, BigDecimal.ZERO, orderId); + + playOrderRefundInfoService.add(orderId, orderInfo.getPurchaserBy(), orderInfo.getAcceptBy(), + orderInfo.getPayMethod(), "0", actualRefundAmount, refundReason, operatorByType, operatorBy, "0", "0"); + + PlayOrderInfoEntity latest = this.selectOrderInfoById(orderId); + wxCustomMpService.sendOrderCancelMessage(latest, refundReason); + } + @Override public PlayOrderInfoEntity queryByOrderNo(String orderNo) { LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); diff --git a/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java b/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java index 3f46aa2..2b51b8f 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/order/service/PlayOrderInfoServiceTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.*; import com.starry.admin.common.exception.CustomException; import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService; import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; +import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper; import com.starry.admin.modules.order.module.constant.OrderConstant; @@ -15,10 +16,12 @@ import com.starry.admin.modules.order.module.dto.OrderCreationRequest; import com.starry.admin.modules.order.module.dto.PaymentInfo; import com.starry.admin.modules.order.module.dto.RandomOrderRequirements; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; +import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService; import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl; import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; import com.starry.admin.modules.shop.service.IPlayCouponDetailsService; import com.starry.admin.modules.weichat.service.WxCustomMpService; +import com.starry.admin.modules.withdraw.service.IEarningsService; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; @@ -64,6 +67,12 @@ class PlayOrderInfoServiceTest { @Mock private IPlayPersonnelGroupInfoService playClerkGroupInfoService; + @Mock + private IPlayOrderRefundInfoService playOrderRefundInfoService; + + @Mock + private IEarningsService earningsService; + @InjectMocks private PlayOrderInfoServiceImpl orderService; @@ -434,4 +443,65 @@ class PlayOrderInfoServiceTest { // 3. 复杂业务流程的正确执行 // 实际收入计算:185元 * 20% = 37元,但由于优惠券由店员承担,需要减去15元,最终收入22元 } + + @Test + @DisplayName("管理员强制取消已接单/服务中订单 - 成功流程") + void testForceCancelOngoingOrderByAdminSuccess() { + String orderId = "order_force_cancel"; + PlayOrderInfoEntity inProgressOrder = new PlayOrderInfoEntity(); + inProgressOrder.setId(orderId); + inProgressOrder.setOrderStatus(OrderConstant.ORDER_STATUS_2); + inProgressOrder.setAcceptBy("clerk-1"); + inProgressOrder.setPurchaserBy("customer-1"); + inProgressOrder.setFinalAmount(BigDecimal.valueOf(100)); + inProgressOrder.setPayMethod("1"); + + PlayOrderInfoEntity cancelledOrder = new PlayOrderInfoEntity(); + cancelledOrder.setId(orderId); + cancelledOrder.setOrderStatus(OrderConstant.ORDER_STATUS_4); + + PlayCustomUserInfoEntity customUserInfo = new PlayCustomUserInfoEntity(); + customUserInfo.setId("customer-1"); + customUserInfo.setAccountBalance(BigDecimal.valueOf(200)); + + when(orderInfoMapper.selectById(orderId)).thenReturn(inProgressOrder, cancelledOrder); + when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1); + when(customUserInfoService.getById("customer-1")).thenReturn(customUserInfo); + + doNothing().when(customUserInfoService).updateAccountBalanceById(eq("customer-1"), any(BigDecimal.class), + any(BigDecimal.class), anyString(), anyString(), any(BigDecimal.class), any(BigDecimal.class), eq(orderId)); + doNothing().when(playOrderRefundInfoService).add(eq(orderId), eq("customer-1"), eq("clerk-1"), anyString(), + anyString(), any(BigDecimal.class), anyString(), anyString(), anyString(), anyString(), anyString()); + doNothing().when(wxCustomMpService).sendOrderCancelMessage(any(PlayOrderInfoEntity.class), anyString()); + + assertDoesNotThrow(() -> orderService.forceCancelOngoingOrder("2", "admin-1", orderId, + BigDecimal.valueOf(80), "管理员取消测试", Collections.emptyList())); + + verify(orderInfoMapper, times(1)).updateById(any(PlayOrderInfoEntity.class)); + verify(customUserInfoService, times(1)).updateAccountBalanceById(eq("customer-1"), any(BigDecimal.class), + any(BigDecimal.class), anyString(), anyString(), eq(BigDecimal.valueOf(80)), eq(BigDecimal.ZERO), + eq(orderId)); + verify(playOrderRefundInfoService, times(1)).add(eq(orderId), eq("customer-1"), eq("clerk-1"), anyString(), + eq("0"), eq(BigDecimal.valueOf(80)), eq("管理员取消测试"), eq("2"), eq("admin-1"), eq("0"), eq("0")); + verify(wxCustomMpService, times(1)).sendOrderCancelMessage(any(PlayOrderInfoEntity.class), + eq("管理员取消测试")); + } + + @Test + @DisplayName("强制取消订单 - 非进行中状态抛出异常") + void testForceCancelOngoingOrderInvalidStatus() { + String orderId = "order_invalid_force_cancel"; + PlayOrderInfoEntity pendingOrder = new PlayOrderInfoEntity(); + pendingOrder.setId(orderId); + pendingOrder.setOrderStatus(OrderConstant.ORDER_STATUS_0); + pendingOrder.setAcceptBy("clerk-1"); + pendingOrder.setPurchaserBy("customer-1"); + pendingOrder.setFinalAmount(BigDecimal.valueOf(50)); + + when(orderInfoMapper.selectById(orderId)).thenReturn(pendingOrder); + + assertThrows(CustomException.class, () -> orderService.forceCancelOngoingOrder("2", "admin-1", orderId, + null, "原因", Collections.emptyList())); + verify(orderInfoMapper, never()).updateById(any(PlayOrderInfoEntity.class)); + } }