Browse Source

test: 补充见知制单账户类型说明和透传验证

lfx 3 days ago
parent
commit
af3c6d477e

+ 29 - 0
README.md

@@ -317,6 +317,35 @@ Content-Type: application/json
 }
 ```
 
+### 4.7 见知单笔支付制单
+
+以下 POST 接口的 JSON 请求体均支持 `toAccountType`:
+
+- `/api/banking/payments`
+- `/api/v2/payment/commonPay`(制单接口别名)
+- `/api/banking/payments/submit-and-query-status`(制单后查询支付状态)
+
+| 字段 | 类型 | 是否必传 | 说明 |
+| --- | --- | --- | --- |
+| `toAccountType` | String | 否 | `"0"` 对公账号,`"1"` 对私账号;未传、`null`、空字符串或纯空格时默认 `"0"` |
+
+对私制单请求示例:
+
+```json
+{
+  "businessTransNo": "PAY-EXAMPLE-001",
+  "payAmount": 1.00,
+  "fromAccountNo": "<付款账号>",
+  "fromAccountName": "<付款户名>",
+  "toAccountNo": "<收款账号>",
+  "toAccountName": "<收款户名>",
+  "toAccountType": "1",
+  "toBankName": "<收款开户行全称>"
+}
+```
+
+服务端将最终账户类型以字符串形式写入见知 `/api/v2/payment/commonPay` 报文;开启国密加密时,该字段包含在加密、签名的支付报文中。
+
 ## 5. ERP 推送规则
 
 ### 5.1 支持的类型

+ 5 - 0
src/main/java/com/malk/minglei/dto/banking/SinglePaymentRequest.java

@@ -1,5 +1,7 @@
 package com.malk.minglei.dto.banking;
 
+import io.swagger.v3.oas.annotations.media.Schema;
+
 import java.math.BigDecimal;
 import java.util.Map;
 
@@ -14,6 +16,9 @@ public class SinglePaymentRequest {
     private String fromAccountName;
     private String toAccountNo;
     private String toAccountName;
+    /** 收款账户类型:0 对公,1 对私;未传或为空时由制单服务默认填 0。 */
+    @Schema(description = "收款账户类型:0-对公账号,1-对私账号;未传、null 或空白时默认 0",
+            allowableValues = {"0", "1"}, defaultValue = "0", example = "1")
     private String toAccountType;
     private String toBankName;
     private String toBankNo;

+ 108 - 0
src/test/java/com/malk/minglei/controller/BankingPaymentAccountTypeTest.java

@@ -0,0 +1,108 @@
+package com.malk.minglei.controller;
+
+import cn.hutool.core.util.HexUtil;
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.SM2;
+import cn.hutool.crypto.symmetric.SM4;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.malk.minglei.config.BankingProperties;
+import com.malk.minglei.service.impl.banking.BankingServiceImpl;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.client.RestTemplate;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class BankingPaymentAccountTypeTest {
+
+    @ParameterizedTest
+    @MethodSource("accountTypes")
+    void forwardsAccountTypeFromHttpRequestInPlainAndEncryptedPayments(String field, String expected, boolean encrypted) throws Exception {
+        assertEquals(expected, submitAndReadPayload("/api/banking/payments", field, encrypted).get("toAccountType"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"/api/v2/payment/commonPay", "/api/banking/payments/submit-and-query-status"})
+    void forwardsPrivateAccountTypeThroughOtherPaymentEntrypoints(String path) throws Exception {
+        assertEquals("1", submitAndReadPayload(path, ",\"toAccountType\":\"1\"", false).get("toAccountType"));
+    }
+
+    private static Stream<Arguments> accountTypes() {
+        return Stream.of(false, true).flatMap(encrypted -> Stream.of(
+                Arguments.of(",\"toAccountType\":\"0\"", "0", encrypted),
+                Arguments.of(",\"toAccountType\":\"1\"", "1", encrypted),
+                Arguments.of("", "0", encrypted),
+                Arguments.of(",\"toAccountType\":null", "0", encrypted),
+                Arguments.of(",\"toAccountType\":\"\"", "0", encrypted),
+                Arguments.of(",\"toAccountType\":\"   \"", "0", encrypted)));
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private Map<String, Object> submitAndReadPayload(String path, String field, boolean encrypted) throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        RestTemplate restTemplate = mock(RestTemplate.class);
+        BankingProperties properties = new BankingProperties();
+        properties.setBaseUrl("https://bank.example");
+        properties.setAppId("test-app");
+        properties.setAppSecret("test-secret");
+        properties.setCustomerCode("test-customer");
+        properties.setIntraBankName("Test Bank");
+        properties.setPaymentEncryptionEnabled(encrypted);
+        SM2 bankKey = encrypted ? SmUtil.sm2() : null;
+        SM2 enterpriseKey = encrypted ? SmUtil.sm2() : null;
+        if (encrypted) {
+            properties.setXencioPublicKey(bankKey.getPublicKeyBase64());
+            properties.setEnterprisePrivateKey(enterpriseKey.getPrivateKeyBase64());
+        }
+        when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class)))
+                .thenReturn(new ResponseEntity<String>("{\"code\":\"200\",\"data\":{\"bankBranchList\":["
+                        + "{\"name\":\"Test Bank\",\"code\":\"001\"}]}}", HttpStatus.OK));
+        when(restTemplate.exchange(eq(URI.create("https://bank.example/api/v2/payment/transferStatus?businessTransNo=PAY-TEST")),
+                eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class)))
+                .thenReturn(new ResponseEntity<String>("{\"code\":\"200\",\"data\":{\"transferStatus\":\"X5555\"}}", HttpStatus.OK));
+        when(restTemplate.exchange(eq("https://bank.example/api/v2/payment/commonPay"), eq(HttpMethod.POST),
+                any(HttpEntity.class), eq(String.class)))
+                .thenReturn(new ResponseEntity<String>("{\"code\":\"200\"}", HttpStatus.OK));
+        BankingServiceImpl service = new BankingServiceImpl(restTemplate, properties, mapper);
+        String request = "{\"businessTransNo\":\"PAY-TEST\",\"payAmount\":1.00,"
+                + "\"fromAccountNo\":\"from-account\",\"fromAccountName\":\"from-name\","
+                + "\"toAccountNo\":\"to-account\",\"toAccountName\":\"to-name\",\"toBankName\":\"Test Bank\"" + field + "}";
+        MockMvcBuilders.standaloneSetup(new BankingController(service, properties)).build()
+                .perform(post(path).contentType(MediaType.APPLICATION_JSON).content(request))
+                .andExpect(status().isOk());
+        ArgumentCaptor<HttpEntity> entity = ArgumentCaptor.forClass(HttpEntity.class);
+        verify(restTemplate).exchange(eq("https://bank.example/api/v2/payment/commonPay"), eq(HttpMethod.POST),
+                entity.capture(), eq(String.class));
+        Map<String, Object> payload = (Map<String, Object>) entity.getValue().getBody();
+        if (encrypted) {
+            String key = bankKey.decryptStr(entity.getValue().getHeaders().getFirst("xencio-key"), KeyType.PrivateKey);
+            String source = new SM4(key.getBytes(StandardCharsets.UTF_8)).decryptStr((String) payload.get("reqData"));
+            assertTrue(enterpriseKey.verify(source.getBytes(StandardCharsets.UTF_8), HexUtil.decodeHex((String) payload.get("sign"))));
+            payload = mapper.readValue(source, Map.class);
+        }
+        return payload;
+    }
+}