|
|
@@ -0,0 +1,440 @@
|
|
|
+package com.payment.platform.module.payment.unified.controller;
|
|
|
+
|
|
|
+import com.payment.platform.common.exception.BusinessException;
|
|
|
+import com.payment.platform.common.response.PageResult;
|
|
|
+import com.payment.platform.common.response.Result;
|
|
|
+import com.payment.platform.core.security.LoginUser;
|
|
|
+import com.payment.platform.module.payment.unified.client.UnifiedApiException;
|
|
|
+import com.payment.platform.module.payment.unified.dto.query.UnifiedOrderQueryDTO;
|
|
|
+import com.payment.platform.module.payment.unified.dto.req.UnifiedAdminRefundDTO;
|
|
|
+import com.payment.platform.module.payment.unified.dto.req.UnifiedRefundCreateCommand;
|
|
|
+import com.payment.platform.module.payment.unified.dto.vo.UnifiedOrderVO;
|
|
|
+import com.payment.platform.module.payment.unified.dto.vo.UnifiedRefundVO;
|
|
|
+import com.payment.platform.module.payment.unified.entity.UnifiedOrderEntity;
|
|
|
+import com.payment.platform.module.payment.unified.entity.UnifiedRefundEntity;
|
|
|
+import com.payment.platform.module.payment.unified.enums.UnifiedSource;
|
|
|
+import com.payment.platform.module.payment.unified.service.UnifiedOrderService;
|
|
|
+import com.payment.platform.module.payment.unified.service.UnifiedRefundService;
|
|
|
+import com.payment.platform.module.payment.unified.util.UnifiedTenantGuard;
|
|
|
+import jakarta.validation.constraints.NotBlank;
|
|
|
+import jakarta.validation.constraints.NotNull;
|
|
|
+import jakarta.validation.constraints.Positive;
|
|
|
+import jakarta.validation.constraints.Size;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.mockito.InOrder;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertSame;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.ArgumentMatchers.isNull;
|
|
|
+import static org.mockito.Mockito.inOrder;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 后台订单控制器(Task 16)的四类职责:
|
|
|
+ * <ol>
|
|
|
+ * <li><b>列表不碰可退金额</b>:{@code refundable_amount} 为 null 来自服务层,
|
|
|
+ * 控制器不得为每一行再查一次退款表;</li>
|
|
|
+ * <li><b>详情算可退金额</b>:与开放 API 同一口径({@link UnifiedOrderService#refundableAmount});</li>
|
|
|
+ * <li><b>手动同步手动腿</b>:先订单、后<b>仅非终态</b>退款;上游异常转统一 500,
|
|
|
+ * 业务异常原样上抛 —— 任何一条腿失败都不许返回成功应答(R39 / R47);</li>
|
|
|
+ * <li><b>契约</b>:路径、方法、权限串逐字钉死(Task 17 菜单 SQL 与前端依赖)。</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * <p>直调方法,{@code @Valid} 不生效 —— 缺 {@code outRefundNo} 的 400 必须由控制器
|
|
|
+ * 自己显式校验,这里按真实调用路径断言。
|
|
|
+ */
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+class UnifiedOrderControllerTest {
|
|
|
+
|
|
|
+ private static final long ORDER_ID = 11L;
|
|
|
+
|
|
|
+ @Mock private UnifiedOrderService orderService;
|
|
|
+ @Mock private UnifiedRefundService refundService;
|
|
|
+ @Mock private UnifiedTenantGuard guard;
|
|
|
+
|
|
|
+ private UnifiedOrderController controller;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ controller = new UnifiedOrderController(orderService, refundService, guard);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static LoginUser user() {
|
|
|
+ LoginUser u = new LoginUser();
|
|
|
+ u.setUserId(1L);
|
|
|
+ u.setUsername("tester");
|
|
|
+ u.setStatus("0");
|
|
|
+ u.setTenantId(2L);
|
|
|
+ u.setIsSuperuser(false);
|
|
|
+ u.setAuthorities(List.of());
|
|
|
+ return u;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static UnifiedOrderEntity order() {
|
|
|
+ UnifiedOrderEntity entity = new UnifiedOrderEntity();
|
|
|
+ entity.setId(ORDER_ID);
|
|
|
+ entity.setTenantId(2L);
|
|
|
+ entity.setOrderNo("UO1");
|
|
|
+ entity.setOutTradeNo("T1");
|
|
|
+ entity.setMchNo("M001");
|
|
|
+ entity.setWayCode("QR_CASHIER");
|
|
|
+ entity.setAmount(1000L);
|
|
|
+ entity.setRefundedAmount(0L);
|
|
|
+ entity.setTradeStatus("2");
|
|
|
+ return entity;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static UnifiedRefundEntity refund(long id, String state) {
|
|
|
+ UnifiedRefundEntity entity = new UnifiedRefundEntity();
|
|
|
+ entity.setId(id);
|
|
|
+ entity.setTenantId(2L);
|
|
|
+ entity.setOrderId(ORDER_ID);
|
|
|
+ entity.setOrderNo("UO1");
|
|
|
+ entity.setRefundNo("UR" + id);
|
|
|
+ entity.setOutRefundNo("RF" + id);
|
|
|
+ entity.setAmount(100L);
|
|
|
+ entity.setState(state);
|
|
|
+ return entity;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static UnifiedAdminRefundDTO refundDto(String outRefundNo) {
|
|
|
+ UnifiedAdminRefundDTO dto = new UnifiedAdminRefundDTO();
|
|
|
+ dto.setOutRefundNo(outRefundNo);
|
|
|
+ dto.setRefundAmount(300L);
|
|
|
+ return dto;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ────────────────────────────── 列表 / 详情 ──────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void list_passesEveryFilter_andNeverComputesRefundable() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ UnifiedOrderVO row = UnifiedOrderVO.of(order(), null);
|
|
|
+ PageResult<UnifiedOrderVO> page = PageResult.of(1, 20, 1, List.of(row));
|
|
|
+ when(orderService.list(any())).thenReturn(page);
|
|
|
+
|
|
|
+ Result<PageResult<UnifiedOrderVO>> result = controller.list(
|
|
|
+ 1, 20, "UO1", "T1", "M001", "QR_CASHIER", "2", "2026-10-01", "2026-10-20");
|
|
|
+
|
|
|
+ assertSame(page, result.getData());
|
|
|
+ assertNull(result.getData().getList().get(0).getRefundableAmount(),
|
|
|
+ "列表不算可退金额(每行一条退款聚合会放大成 N+1)");
|
|
|
+
|
|
|
+ ArgumentCaptor<UnifiedOrderQueryDTO> cap = ArgumentCaptor.forClass(UnifiedOrderQueryDTO.class);
|
|
|
+ verify(orderService).list(cap.capture());
|
|
|
+ assertEquals(1, cap.getValue().getPageNo());
|
|
|
+ assertEquals(20, cap.getValue().getPageSize());
|
|
|
+ assertEquals("UO1", cap.getValue().getOrderNo());
|
|
|
+ assertEquals("T1", cap.getValue().getOutTradeNo());
|
|
|
+ assertEquals("M001", cap.getValue().getMchNo());
|
|
|
+ assertEquals("QR_CASHIER", cap.getValue().getWayCode());
|
|
|
+ assertEquals("2", cap.getValue().getTradeStatus());
|
|
|
+ assertEquals("2026-10-01", cap.getValue().getStartDate());
|
|
|
+ assertEquals("2026-10-20", cap.getValue().getEndDate());
|
|
|
+ verify(orderService, never()).refundableAmount(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void detail_computesRefundableAmount() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ UnifiedOrderEntity order = order();
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order);
|
|
|
+ when(orderService.refundableAmount(order)).thenReturn(700L);
|
|
|
+
|
|
|
+ Result<UnifiedOrderVO> result = controller.detail("11");
|
|
|
+
|
|
|
+ verify(orderService).requireById(ORDER_ID);
|
|
|
+ assertEquals("UO1", result.getData().getOrderNo());
|
|
|
+ assertEquals(700L, result.getData().getRefundableAmount(),
|
|
|
+ "后台详情必须带与开放 API 同口径的可退金额");
|
|
|
+ assertEquals(2L, result.getData().getTenantId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void detail_nonNumericId_is400_beforeTouchingAnyService() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> controller.detail("abc"));
|
|
|
+
|
|
|
+ assertEquals(400, e.getCode(), "实测消息: " + e.getMessage());
|
|
|
+ assertEquals("无效的 id", e.getMessage());
|
|
|
+ verifyNoInteractions(orderService, refundService);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void detail_overflowId_is400_notFramework500() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> controller.detail("99999999999999999999"));
|
|
|
+
|
|
|
+ assertEquals(400, e.getCode(), "超出 Long 的入参同样是无效 id,实测消息: " + e.getMessage());
|
|
|
+ verifyNoInteractions(orderService, refundService);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ────────────────────────────── 手动同步 ──────────────────────────────
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 顺序与集合都必须对:先订单、后按 {@code order_no} 退款;退款只同步 0/1,
|
|
|
+ * 2/3/4 已是终态再打上游只会白耗一次调用。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void sync_ordersFirst_thenOnlyPendingRefunds() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ UnifiedOrderEntity order = order();
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order);
|
|
|
+ UnifiedRefundEntity generated = refund(1L, "0");
|
|
|
+ UnifiedRefundEntity refunding = refund(2L, "1");
|
|
|
+ UnifiedRefundEntity success = refund(3L, "2");
|
|
|
+ UnifiedRefundEntity failed = refund(4L, "3");
|
|
|
+ UnifiedRefundEntity closed = refund(5L, "4");
|
|
|
+ when(refundService.findByOrderNo("UO1"))
|
|
|
+ .thenReturn(List.of(generated, refunding, success, failed, closed));
|
|
|
+
|
|
|
+ Result<Void> result = controller.sync("11");
|
|
|
+
|
|
|
+ assertEquals(0, result.getCode());
|
|
|
+ InOrder inOrder = inOrder(orderService, refundService);
|
|
|
+ inOrder.verify(orderService).syncManually(order);
|
|
|
+ inOrder.verify(refundService).findByOrderNo("UO1");
|
|
|
+
|
|
|
+ verify(refundService).syncManually(generated);
|
|
|
+ verify(refundService).syncManually(refunding);
|
|
|
+ verify(refundService, times(2)).syncManually(any());
|
|
|
+ verify(refundService, never()).syncManually(success);
|
|
|
+ verify(refundService, never()).syncManually(failed);
|
|
|
+ verify(refundService, never()).syncManually(closed);
|
|
|
+ verify(orderService, never()).syncFromUpstream(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void sync_orderUpstreamFailure_isGeneric500_andHidesUpstreamDetail() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ UnifiedOrderEntity order = order();
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order);
|
|
|
+ org.mockito.Mockito.doThrow(
|
|
|
+ new UnifiedApiException(UnifiedApiException.CODE_TRANSPORT,
|
|
|
+ "connect timed out: internal-host:8080"))
|
|
|
+ .when(orderService).syncManually(order);
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> controller.sync("11"));
|
|
|
+
|
|
|
+ assertEquals(500, e.getCode());
|
|
|
+ assertEquals("上游同步失败", e.getMessage(), "不得回显上游原始 message(R47)");
|
|
|
+ assertFalse(e.getMessage().contains("internal-host"), "实测消息: " + e.getMessage());
|
|
|
+ verify(refundService, never()).findByOrderNo(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void sync_refundUpstreamFailure_isAlsoGeneric500() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ UnifiedOrderEntity order = order();
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order);
|
|
|
+ UnifiedRefundEntity pending = refund(1L, "0");
|
|
|
+ when(refundService.findByOrderNo("UO1")).thenReturn(List.of(pending));
|
|
|
+ org.mockito.Mockito.doThrow(new UnifiedApiException(UnifiedApiException.CODE_SIGN, "bad sign"))
|
|
|
+ .when(refundService).syncManually(pending);
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> controller.sync("11"));
|
|
|
+
|
|
|
+ assertEquals(500, e.getCode(), "退款腿失败同样不得返回成功,实测消息: " + e.getMessage());
|
|
|
+ assertEquals("上游同步失败", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 业务异常(订单不存在/配置缺失/数据损坏)由服务层给出精确语义,控制器不得
|
|
|
+ * 把它吞成「同步成功」,也不该二次包装。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void sync_businessException_propagatesUnchanged() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ UnifiedOrderEntity order = order();
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order);
|
|
|
+ BusinessException original = new BusinessException(404, "商户配置不存在");
|
|
|
+ org.mockito.Mockito.doThrow(original).when(orderService).syncManually(order);
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> controller.sync("11"));
|
|
|
+
|
|
|
+ assertSame(original, e, "BusinessException 必须原样上抛(R47)");
|
|
|
+ assertEquals(404, e.getCode());
|
|
|
+ assertEquals("商户配置不存在", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ────────────────────────────── 发起退款 ──────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refund_blankOutRefundNo_is400_withoutCallingCreateRefund() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order());
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> controller.refund("11", refundDto(" ")));
|
|
|
+
|
|
|
+ assertEquals(400, e.getCode(), "实测消息: " + e.getMessage());
|
|
|
+ assertTrue(e.getMessage().contains("outRefundNo"), "实测消息: " + e.getMessage());
|
|
|
+ verifyNoInteractions(refundService);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refund_buildsCommandFromPathOrder_withDefaultReason_andAdminSource() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ UnifiedOrderEntity order = order();
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order);
|
|
|
+ UnifiedRefundEntity created = refund(31L, "0");
|
|
|
+ when(refundService.createRefund(any(), eq(2L), isNull(), eq(UnifiedSource.ADMIN)))
|
|
|
+ .thenReturn(created);
|
|
|
+
|
|
|
+ UnifiedAdminRefundDTO dto = refundDto("RF1");
|
|
|
+ dto.setRefundReason(" ");
|
|
|
+ Result<UnifiedRefundVO> result = controller.refund("11", dto);
|
|
|
+
|
|
|
+ ArgumentCaptor<UnifiedRefundCreateCommand> cap =
|
|
|
+ ArgumentCaptor.forClass(UnifiedRefundCreateCommand.class);
|
|
|
+ verify(refundService).createRefund(cap.capture(), eq(2L), isNull(),
|
|
|
+ eq(UnifiedSource.ADMIN));
|
|
|
+ UnifiedRefundCreateCommand cmd = cap.getValue();
|
|
|
+ assertEquals("UO1", cmd.getOrderNo(), "后台退款按路径订单定位,不接受请求体里的订单号");
|
|
|
+ assertEquals(300L, cmd.getRefundAmount());
|
|
|
+ assertEquals("RF1", cmd.getOutRefundNo());
|
|
|
+ assertEquals("商户申请退款", cmd.getRefundReason(), "空原因必须补默认文案");
|
|
|
+ assertNull(cmd.getOutTradeNo());
|
|
|
+
|
|
|
+ assertEquals("UR31", result.getData().getRefundNo());
|
|
|
+ assertNotNull(result.getData().getId(), "后台视图带内部主键(序列化为字符串)");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refund_keepsExplicitReason() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order());
|
|
|
+ when(refundService.createRefund(any(), eq(2L), isNull(), eq(UnifiedSource.ADMIN)))
|
|
|
+ .thenReturn(refund(31L, "0"));
|
|
|
+
|
|
|
+ UnifiedAdminRefundDTO dto = refundDto("RF1");
|
|
|
+ dto.setRefundReason("客户投诉");
|
|
|
+ controller.refund("11", dto);
|
|
|
+
|
|
|
+ ArgumentCaptor<UnifiedRefundCreateCommand> cap =
|
|
|
+ ArgumentCaptor.forClass(UnifiedRefundCreateCommand.class);
|
|
|
+ verify(refundService).createRefund(cap.capture(), eq(2L), isNull(),
|
|
|
+ eq(UnifiedSource.ADMIN));
|
|
|
+ assertEquals("客户投诉", cap.getValue().getRefundReason());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ────────────────────────────── 退款列表 ──────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refunds_returnsEveryRefundOfTheOrder() {
|
|
|
+ when(guard.require()).thenReturn(user());
|
|
|
+ when(orderService.requireById(ORDER_ID)).thenReturn(order());
|
|
|
+ when(refundService.findByOrderNo("UO1"))
|
|
|
+ .thenReturn(List.of(refund(1L, "0"), refund(2L, "2")));
|
|
|
+
|
|
|
+ Result<List<UnifiedRefundVO>> result = controller.refunds("11");
|
|
|
+
|
|
|
+ assertEquals(2, result.getData().size());
|
|
|
+ assertEquals("UR1", result.getData().get(0).getRefundNo());
|
|
|
+ assertEquals("UR2", result.getData().get(1).getRefundNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ────────────────────────────── 契约 ──────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void classPath_isTheFrontendContract() {
|
|
|
+ RequestMapping rm = UnifiedOrderController.class.getAnnotation(RequestMapping.class);
|
|
|
+
|
|
|
+ assertNotNull(rm, "类级 @RequestMapping 不得为空");
|
|
|
+ assertEquals("/payment/unified/order", rm.value()[0],
|
|
|
+ "前端 unified.ts 与菜单 SQL 依赖这一条路径");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void fiveMappings_andFourPermissions_areLiteral() throws Exception {
|
|
|
+ assertEquals("@perm.hasAny('module_payment:unified:order:list')",
|
|
|
+ permission("list", int.class, int.class, String.class, String.class,
|
|
|
+ String.class, String.class, String.class, String.class, String.class));
|
|
|
+ assertEquals("@perm.hasAny('module_payment:unified:order:detail')",
|
|
|
+ permission("detail", String.class));
|
|
|
+ assertEquals("@perm.hasAny('module_payment:unified:order:sync')",
|
|
|
+ permission("sync", String.class));
|
|
|
+ assertEquals("@perm.hasAny('module_payment:unified:order:refund')",
|
|
|
+ permission("refund", String.class, UnifiedAdminRefundDTO.class));
|
|
|
+ assertEquals("@perm.hasAny('module_payment:unified:order:detail')",
|
|
|
+ permission("refunds", String.class), "退款列表复用详情权限串");
|
|
|
+
|
|
|
+ assertNotNull(method("list", int.class, int.class, String.class, String.class,
|
|
|
+ String.class, String.class, String.class, String.class, String.class)
|
|
|
+ .getAnnotation(GetMapping.class));
|
|
|
+ assertEquals("/{id}", mappingValue(method("detail", String.class)));
|
|
|
+ assertEquals("/{id}/sync", mappingValue(method("sync", String.class)));
|
|
|
+ assertEquals("/{id}/refund", mappingValue(method("refund", String.class,
|
|
|
+ UnifiedAdminRefundDTO.class)));
|
|
|
+ assertEquals("/{id}/refunds", mappingValue(method("refunds", String.class)));
|
|
|
+ assertEquals("POST", method("sync", String.class).getAnnotation(PostMapping.class) != null
|
|
|
+ ? "POST" : null);
|
|
|
+ assertEquals("POST", method("refund", String.class, UnifiedAdminRefundDTO.class)
|
|
|
+ .getAnnotation(PostMapping.class) != null ? "POST" : null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** HTTP 绑定路径会跑 {@code @Valid};这条只钉注解本身没被删掉。 */
|
|
|
+ @Test
|
|
|
+ void adminRefundDto_declaresValidationForHttpBinding() throws Exception {
|
|
|
+ Field outRefundNo = UnifiedAdminRefundDTO.class.getDeclaredField("outRefundNo");
|
|
|
+ assertNotNull(outRefundNo.getAnnotation(NotBlank.class));
|
|
|
+ assertEquals(64, outRefundNo.getAnnotation(Size.class).max());
|
|
|
+
|
|
|
+ Field refundAmount = UnifiedAdminRefundDTO.class.getDeclaredField("refundAmount");
|
|
|
+ assertNotNull(refundAmount.getAnnotation(NotNull.class));
|
|
|
+ assertNotNull(refundAmount.getAnnotation(Positive.class));
|
|
|
+
|
|
|
+ Field refundReason = UnifiedAdminRefundDTO.class.getDeclaredField("refundReason");
|
|
|
+ assertEquals(64, refundReason.getAnnotation(Size.class).max());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ────────────────────────────── 工具 ──────────────────────────────
|
|
|
+
|
|
|
+ private static java.lang.reflect.Method method(String name, Class<?>... params)
|
|
|
+ throws NoSuchMethodException {
|
|
|
+ return UnifiedOrderController.class.getMethod(name, params);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String permission(String name, Class<?>... params) throws Exception {
|
|
|
+ PreAuthorize pa = method(name, params).getAnnotation(PreAuthorize.class);
|
|
|
+ assertNotNull(pa, name + " 必须有 @PreAuthorize");
|
|
|
+ return pa.value();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String mappingValue(java.lang.reflect.Method m) {
|
|
|
+ GetMapping get = m.getAnnotation(GetMapping.class);
|
|
|
+ if (get != null) {
|
|
|
+ return get.value().length == 0 ? "" : get.value()[0];
|
|
|
+ }
|
|
|
+ PostMapping post = m.getAnnotation(PostMapping.class);
|
|
|
+ assertNotNull(post, "端点必须有 GET/POST 映射");
|
|
|
+ return post.value()[0];
|
|
|
+ }
|
|
|
+}
|