|
|
@@ -6,11 +6,15 @@ import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
import com.payment.platform.common.exception.BusinessException;
|
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyNotifyLogEntity;
|
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyOrderEntity;
|
|
|
+import com.payment.platform.module.payment.leshouying.dto.LsyOrderRefundDTO;
|
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyRefundEntity;
|
|
|
import com.payment.platform.module.payment.leshouying.mapper.LsyNotifyLogMapper;
|
|
|
import com.payment.platform.module.payment.leshouying.mapper.LsyOrderMapper;
|
|
|
import com.payment.platform.module.payment.leshouying.mapper.LsyRefundMapper;
|
|
|
+import com.payment.platform.module.payment.leshouying.service.LsyRefundService;
|
|
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+import org.mockito.ArgumentMatchers;
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
@@ -49,12 +53,13 @@ class LeshouyingOrderControllerTest {
|
|
|
@Mock private LsyOrderMapper orderMapper;
|
|
|
@Mock private LsyNotifyLogMapper notifyLogMapper;
|
|
|
@Mock private LsyRefundMapper refundMapper;
|
|
|
+ @Mock private LsyRefundService refundService;
|
|
|
|
|
|
private LeshouyingOrderController controller;
|
|
|
|
|
|
@BeforeEach
|
|
|
void setUp() {
|
|
|
- controller = new LeshouyingOrderController(orderMapper, notifyLogMapper, refundMapper);
|
|
|
+ controller = new LeshouyingOrderController(orderMapper, notifyLogMapper, refundMapper, refundService);
|
|
|
// 初始化 MyBatis-Plus lambda 元数据缓存,使 LambdaQueryWrapper.getSqlSegment() 可在
|
|
|
// 无 Spring 上下文的单测中工作(否则抛 can not find lambda cache for this entity)。
|
|
|
// 本类**每一条**断言都读 wrapper,所以这一句是必需的,不是保险起见。
|
|
|
@@ -208,4 +213,80 @@ class LeshouyingOrderControllerTest {
|
|
|
assertEquals(404, e.getCode());
|
|
|
verify(refundMapper, never()).selectList(any());
|
|
|
}
|
|
|
+
|
|
|
+ // ───────────────────────── 发起退款(动钱) ─────────────────────────
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 不传金额 ⇒ 不自己算,把「算剩余」留给服务层。
|
|
|
+ *
|
|
|
+ * <p>★ 这条是本端点与开放 API 那条腿的**唯一**差别:那边 {@code out_refund_no}
|
|
|
+ * 与金额都由调用方给,管理端两者都不该让操作员填(退款单号重复/金额算错都是钱的事)。
|
|
|
+ * 所以这里断言两件事:第三参**是 null**(不是 0、也不是事先算好的余额),
|
|
|
+ * 以及第二个参是服务端新生成的号。
|
|
|
+ *
|
|
|
+ * <p>★ 「让服务层算」不是洁癖:剩余 = 总额 - max(台账, 快照) 这个式子
|
|
|
+ * 已经在 {@code LsyRefundService} 里有一份,控制器再算一次就是第二个副本 ——
|
|
|
+ * 而两处漂移的后果是「页面预填的数」与「真正发出去的数」不是同一个。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void refund_不传金额_把算剩余留给服务层_且退款单号由服务端生成() {
|
|
|
+ LsyOrderEntity o = order();
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ when(refundService.createRefund(any(), anyString(), any())).thenReturn(new LsyRefundEntity());
|
|
|
+
|
|
|
+ controller.refund("OUT001", null);
|
|
|
+
|
|
|
+ ArgumentCaptor<String> no = ArgumentCaptor.forClass(String.class);
|
|
|
+ verify(refundService).createRefund(same(o), no.capture(), ArgumentMatchers.<Long>isNull());
|
|
|
+ assertTrue(no.getValue().matches("RF\\d+"),
|
|
|
+ "退款单号必须是服务端生成的 RF + 雪花数字(操作员不该有机会填这个);实测=" + no.getValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refund_带金额_原样透传() {
|
|
|
+ LsyOrderEntity o = order();
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ when(refundService.createRefund(any(), anyString(), any())).thenReturn(new LsyRefundEntity());
|
|
|
+ LsyOrderRefundDTO dto = new LsyOrderRefundDTO();
|
|
|
+ dto.setRefundFee(30L);
|
|
|
+
|
|
|
+ controller.refund("OUT001", dto);
|
|
|
+
|
|
|
+ ArgumentCaptor<Long> fee = ArgumentCaptor.forClass(Long.class);
|
|
|
+ verify(refundService).createRefund(same(o), anyString(), fee.capture());
|
|
|
+ assertEquals(30L, fee.getValue(),
|
|
|
+ "部分退款时控制器**不得**改写调用方给的金额(改写就等于页面上的数不是退的数)");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refund_订单不存在_is404_且一步都不碰上游() {
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(null);
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> controller.refund("NOPE", null));
|
|
|
+
|
|
|
+ assertEquals(404, e.getCode(),
|
|
|
+ "与其它方法同一条纪律:不区分「不存在」与「不属于你」");
|
|
|
+ verifyNoInteractions(refundService);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 本端点**不复用** {@code order:list} —— 能查一笔单不等于能动这笔钱。
|
|
|
+ *
|
|
|
+ * <p>★ 为什么不能只靠 {@code LeshouyingAdminWiringTest}:那个类明确写着它
|
|
|
+ * 「不校验哪个权限串配哪个控制器」—— 把 refund 的串抄成 list 的串不会让它变红
|
|
|
+ * (两条串都在菜单契约里)。所以「这一个端点的串是不是它自己的」必须在本类里钉。
|
|
|
+ * 同 {@code HarOrderController#refund} 的取舍(那边有同款用例)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void refund_有自己独立的权限串_不复用orderList() throws Exception {
|
|
|
+ PreAuthorize pa = LeshouyingOrderController.class
|
|
|
+ .getMethod("refund", String.class, LsyOrderRefundDTO.class)
|
|
|
+ .getAnnotation(PreAuthorize.class);
|
|
|
+
|
|
|
+ assertNotNull(pa, "退款端点必须有 @PreAuthorize");
|
|
|
+ assertEquals("@perm.hasAny('module_payment:leshouying:order:refund')", pa.value(),
|
|
|
+ "退款必须有自己的权限串。复用 order:list 的后果不是「少一层保护」而是**放大权限**:"
|
|
|
+ + "任何能查一笔单的人都能把这笔钱退出去");
|
|
|
+ }
|
|
|
}
|