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