Procházet zdrojové kódy

fix: render.pay 返回短链接 initialize_code 而非 HTML 表单

用户实测成功响应 {"code":"10000","initialize_code":"https://p.tb.cn/..."}——
pageExecute 把 JSON body 当支付页返回,前端 document.write 出整页 JSON。
- 后端: 改 certificateExecute(与其他调用统一)+ 取 response.getInitializeCode() 作为 pay_url,成功无短链接抛错
- 前端: 两处 handlePay 改 window.open(payUrl) 直接打开收银台短链接
测试: 56/56 Service 全绿(+renderPay_successWithoutInitializeCode_throws + I5 改短链接语义)
alphaH před 4 dny
rodič
revize
4b2402004c

+ 6 - 9
frontend/src/views/module_payment/account/components/BatchPayDetail.vue

@@ -178,18 +178,15 @@ async function handlePay() {
   payLoading.value = true;
   try {
     const res = await BatchPayAPI.renderPay(enterpriseId.value, order.value.out_batch_no);
-    // pageExecute 返回 HTML 表单,自带自动提交(与充值/签约链接处理一致)
-    const payHtml = res.data.data?.pay_url || "";
-    if (!payHtml) {
+    // render.pay 返回支付宝收银台短链接 initialize_code(后端实证: 成功响应 {"code":"10000","initialize_code":"https://p.tb.cn/..."}),
+    // 直接新窗口打开,无需 document.write
+    const payUrl = res.data.data?.pay_url || "";
+    if (!payUrl) {
       ElMessage.warning("未获取到支付页面");
       return;
     }
-    // 新窗口打开支付页(设计 M4): 避免 document.write 同页替换销毁当前 SPA
-    const w = window.open("about:blank", "_blank");
-    if (w) {
-      w.document.write(payHtml);
-      w.document.close();
-    } else {
+    const w = window.open(payUrl, "_blank");
+    if (!w) {
       ElMessage.warning("支付页弹窗被浏览器拦截,请允许弹窗后重试");
     }
   } finally {

+ 6 - 9
frontend/src/views/module_payment/account/components/BatchPayList.vue

@@ -221,18 +221,15 @@ async function handlePay(row: BatchOrderVO) {
   payingNo.value = row.out_batch_no;
   try {
     const res = await BatchPayAPI.renderPay(enterpriseId, row.out_batch_no);
-    // pageExecute 返回 HTML 表单,自带自动提交(与充值/签约链接处理一致)
-    const payHtml = res.data.data?.pay_url || "";
-    if (!payHtml) {
+    // render.pay 返回支付宝收银台短链接 initialize_code(后端实证: 成功响应 {"code":"10000","initialize_code":"https://p.tb.cn/..."}),
+    // 直接新窗口打开,无需 document.write
+    const payUrl = res.data.data?.pay_url || "";
+    if (!payUrl) {
       ElMessage.warning("未获取到支付页面");
       return;
     }
-    // 新窗口打开支付页(设计 M4): 避免 document.write 同页替换销毁当前 SPA
-    const w = window.open("about:blank", "_blank");
-    if (w) {
-      w.document.write(payHtml);
-      w.document.close();
-    } else {
+    const w = window.open(payUrl, "_blank");
+    if (!w) {
       ElMessage.warning("支付页弹窗被浏览器拦截,请允许弹窗后重试");
     }
   } finally {

+ 11 - 6
java/src/main/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayService.java

@@ -433,16 +433,21 @@ public class AlipayBatchPayService {
             AlipayFundTransRenderPayRequest request = new AlipayFundTransRenderPayRequest();
             request.setBizModel(model);
             AlipayFundTransRenderPayResponse response =
-                    alipayClientFactory.getClient(enterpriseId, BIZ_TYPE).pageExecute(request);
-            if (response == null || response.getBody() == null)
+                    alipayClientFactory.getClient(enterpriseId, BIZ_TYPE).certificateExecute(request);
+            if (response == null)
                 throw new BusinessException(400, "生成支付页面失败: 无响应");
-            // 支付宝业务失败时 pageExecute 返回错误 JSON 而非 HTML 表单——若当 pay_url 返回,
-            // 前端新窗口会 document.write 出整页 JSON(用户实测)。必须先校验成功再给前端,同文件其他 5 处调用同款。
+            // 先校验成功再给前端(同文件其他 5 处调用同款)——业务失败时若直接把 body 当链接返回,
+            // 前端会拿错误 JSON 开新窗口(用户实测: 窗口显示整页 INVALID_PARAMETER JSON)
             if (!response.isSuccess())
                 throw new BusinessException(400, "生成支付页面失败: " + response.getMsg() + " (" + response.getSubCode() + ")");
-            order.setPayUrl(response.getBody());
+            // render.pay 返回收银台短链接 initialize_code(用户实测: 成功响应为
+            // {"code":"10000","initialize_code":"https://p.tb.cn/..."}),不是 HTML 表单——直接作为支付链接
+            String payUrl = response.getInitializeCode();
+            if (payUrl == null || payUrl.isBlank())
+                throw new BusinessException(400, "生成支付页面失败: 支付宝未返回跳转链接");
+            order.setPayUrl(payUrl);
             batchOrderMapper.updateById(order);
-            return Map.of("pay_url", response.getBody());
+            return Map.of("pay_url", payUrl);
         } catch (AlipayApiException e) {
             throw new BusinessException(400, "生成支付页面失败: " + e.getMessage());
         }

+ 36 - 16
java/src/test/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayServiceTest.java

@@ -330,20 +330,21 @@ class AlipayBatchPayServiceTest {
         order.setStatus("INIT");
         when(batchOrderMapper.selectOne(any())).thenReturn(order);
         AlipayFundTransRenderPayResponse resp = new AlipayFundTransRenderPayResponse();
-        resp.setOrderId("BT1");
-        resp.setBody("https://render.alipay.com/paypage?orderId=BT1");
-        when(alipayClient.pageExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
+        resp.setCode("10000");
+        resp.setInitializeCode("https://p.tb.cn/_2PG4jfHMVvg9vqBUdtx1HZ");
+        when(alipayClient.certificateExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
 
         Map<String, String> result = service.renderPay("E100", "B1");
 
-        assertNotNull(result.get("pay_url"));
+        // render.pay 返回收银台短链接 initialize_code(用户实测成功响应实证),直接作为 pay_url
+        assertEquals("https://p.tb.cn/_2PG4jfHMVvg9vqBUdtx1HZ", result.get("pay_url"));
         verify(batchOrderMapper).updateById(order);
         // render.pay 必传参数必须携带(文档实证枚举值: target_terminal_type=PC / initialize_code_type=SHORT_URL),
         // 缺失时支付宝返回 INVALID_PARAMETER 报错(用户实测: 参数[target_terminal_type]错误)
         @SuppressWarnings({ "unchecked", "rawtypes" })
         ArgumentCaptor<AlipayFundTransRenderPayRequest> cap =
                 ArgumentCaptor.forClass(AlipayFundTransRenderPayRequest.class);
-        verify(alipayClient).pageExecute(cap.capture());
+        verify(alipayClient).certificateExecute(cap.capture());
         AlipayFundTransRenderPayModel model =
                 (AlipayFundTransRenderPayModel) cap.getValue().getBizModel();
         assertEquals("PC", model.getTargetTerminalType());
@@ -352,8 +353,8 @@ class AlipayBatchPayServiceTest {
 
     @Test
     void renderPay_alipayBusinessError_throwsAndNotPersists() throws AlipayApiException {
-        // 支付宝业务失败时 pageExecute 返回错误 JSON body(非 HTML 表单)——
-        // 必须 isSuccess 校验后抛错,否则错误 JSON 会被当 pay_url 返回、前端 document.write 出整页 JSON(用户实测路径
+        // 支付宝业务失败(非 10000)必须抛错且不落库——
+        // 否则错误响应会被当 pay_url 返回、前端拿错误内容开新窗口(用户实测: 窗口显示整页 INVALID_PARAMETER JSON
         BatchOrderEntity order = new BatchOrderEntity();
         order.setEnterpriseId("E100");
         order.setOutBatchNo("B1");
@@ -364,8 +365,7 @@ class AlipayBatchPayServiceTest {
         resp.setCode("40004");
         resp.setSubCode("INVALID_PARAMETER");
         resp.setSubMsg("参数[target_terminal_type]错误, 原因: 必须传入参数[target_terminal_type]");
-        resp.setBody("{\"alipay_fund_trans_render_pay_response\":{\"code\":\"40004\"}}");
-        when(alipayClient.pageExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
+        when(alipayClient.certificateExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
 
         BusinessException ex = assertThrows(BusinessException.class, () -> service.renderPay("E100", "B1"));
 
@@ -374,6 +374,26 @@ class AlipayBatchPayServiceTest {
         verify(batchOrderMapper, never()).updateById(any());
     }
 
+    @Test
+    void renderPay_successWithoutInitializeCode_throws() throws AlipayApiException {
+        // 成功响应但支付宝未返回跳转链接(防御: 不把 null 当 pay_url 返回)
+        BatchOrderEntity order = new BatchOrderEntity();
+        order.setEnterpriseId("E100");
+        order.setOutBatchNo("B1");
+        order.setBatchTransId("BT1");
+        order.setStatus("INIT");
+        when(batchOrderMapper.selectOne(any())).thenReturn(order);
+        AlipayFundTransRenderPayResponse resp = new AlipayFundTransRenderPayResponse();
+        resp.setCode("10000");
+        when(alipayClient.certificateExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
+
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.renderPay("E100", "B1"));
+
+        assertEquals(400, ex.getCode());
+        assertTrue(ex.getMessage().contains("跳转链接"), ex.getMessage());
+        verify(batchOrderMapper, never()).updateById(any());
+    }
+
     @Test
     void batchQuery_syncsBatchStatus() throws AlipayApiException {
         BatchOrderEntity order = new BatchOrderEntity();
@@ -657,10 +677,10 @@ class AlipayBatchPayServiceTest {
         assertTrue(bytes.length > 0);
     }
 
-    // ==================== I5: pay_url 长 HTML body(>1024) ====================
+    // ==================== I5: pay_url 落库(render.pay 返回短链接 initialize_code) ====================
 
     @Test
-    void renderPay_longHtmlBody_persistsWithoutError() throws AlipayApiException {
+    void renderPay_initializeCode_persistsAndReturns() throws AlipayApiException {
         BatchOrderEntity order = new BatchOrderEntity();
         order.setEnterpriseId("E100");
         order.setOutBatchNo("B1");
@@ -668,15 +688,15 @@ class AlipayBatchPayServiceTest {
         order.setStatus("INIT");
         when(batchOrderMapper.selectOne(any())).thenReturn(order);
 
-        // 真实 pageExecute 返回自动提交 HTML 表单(1.5-4KB),远超原 varchar(1024)
-        String html = "<html><body><form action=\"https://render.alipay.com/paypage\">" + "x".repeat(2000) + "</form></body></html>";
+        // 用户实测成功响应: {"code":"10000","initialize_code":"https://p.tb.cn/_2PG4jfHMVvg9vqBUdtx1HZ",...}
         AlipayFundTransRenderPayResponse resp = new AlipayFundTransRenderPayResponse();
-        resp.setBody(html);
-        when(alipayClient.pageExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
+        resp.setCode("10000");
+        resp.setInitializeCode("https://p.tb.cn/_2PG4jfHMVvg9vqBUdtx1HZ");
+        when(alipayClient.certificateExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
 
         Map<String, String> result = service.renderPay("E100", "B1");
 
-        assertEquals(html, result.get("pay_url"));
+        assertEquals("https://p.tb.cn/_2PG4jfHMVvg9vqBUdtx1HZ", result.get("pay_url"));
         verify(batchOrderMapper).updateById(order);
     }