فهرست منبع

fix: 会员费虚拟支付开关最终审查修复

- MembershipService.createOrderWithPayType 加 @Transactional 防孤儿订单
- 新增 null 缺省虚拟支付路由测试
- 新增 PaymentServiceORDRoutingTest 覆盖 ORD/PO/其他前缀回调路由
- 简化 orderNo 断言为 startsWith
- 移除 pay.vue else 分支重复 orderNo 赋值
iwt 3 هفته پیش
والد
کامیت
75627206aa

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -466,6 +466,7 @@ public class MembershipService implements MembershipServiceInterface {
      * @return 统一响应 map,含 payType("virtual"/"wechat"/"none")及对应字段;
      *         虚拟支付道具未配置时返回 null(调用方转 46001)
      */
+    @Transactional(rollbackFor = Exception.class)
     public Map<String, Object> createOrderWithPayType(Long userId, Long familyId, String levelCode,
             String paymentType, String period, Long userCouponId, String sessionKey, String openid) {
 

+ 31 - 1
cfc-backend/src/test/java/com/etotem/cfc/service/MembershipPaySwitchTest.java

@@ -70,11 +70,41 @@ class MembershipPaySwitchTest {
 
         assertNotNull(result);
         assertEquals("virtual", result.get("payType"));
-        assertEquals("ORD", result.get("orderNo") != null ? result.get("orderNo").toString().substring(0, 3) : "");
+        assertTrue(String.valueOf(result.get("orderNo")).startsWith("ORD"));
         assertNotNull(result.get("orderInfo"));
         assertNotNull(result.get("sign"));
     }
 
+    // --- 虚拟支付开启(缺省 null = 虚拟支付) ---
+
+    @Test
+    void createOrderWithPayType_virtualEnabled_defaultNull_returnsVirtualPayType() {
+        when(sysConfigService.getValue("member_pay_virtual_enabled")).thenReturn(null);
+        when(sysConfigService.getValue("member_fee_family")).thenReturn("131400");
+        when(levelMapper.selectOne(any())).thenReturn(buildLevel("FAMILY", 131400, null, null));
+        when(paymentOrderMapper.selectCount(any())).thenReturn(0L);
+        when(couponService.apply(anyLong(), anyLong(), anyString(), anyInt())).thenReturn(null);
+        when(paymentOrderMapper.insert(any())).thenReturn(1);
+
+        VirtualPayParamsDTO mockDto = new VirtualPayParamsDTO();
+        mockDto.setOrderInfo(new HashMap<>());
+        mockDto.setSign("mock-sign");
+        mockDto.setSignature("mock-sig");
+        when(virtualPayService.generateMembershipPayParams(anyString(), anyString())).thenReturn(mockDto);
+
+        User user = new User();
+        user.setFamilyId(1L);
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Map<String, Object> result = membershipService.createOrderWithPayType(
+                1L, 1L, "FAMILY", "pay", "yearly", null, "mockSessionKey", null);
+
+        assertNotNull(result);
+        assertEquals("virtual", result.get("payType"));
+        assertTrue(String.valueOf(result.get("orderNo")).startsWith("ORD"));
+        assertNotNull(result.get("sign"));
+    }
+
     // --- 虚拟支付关闭 ---
 
     @Test

+ 93 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/PaymentServiceORDRoutingTest.java

@@ -0,0 +1,93 @@
+package com.etotem.cfc.service;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.mapper.ProductOrderMapper;
+
+@MockitoSettings(strictness = Strictness.LENIENT)
+@ExtendWith(MockitoExtension.class)
+class PaymentServiceORDRoutingTest {
+
+    @Mock
+    private PackageOrderMapper packageOrderMapper;
+    @Mock
+    private ProductOrderMapper productOrderMapper;
+    @Mock
+    private CommissionService commissionService;
+    @Mock
+    private ProductOrderService productOrderService;
+    @Mock
+    private MembershipService membershipService;
+
+    @Spy
+    @InjectMocks
+    private PaymentService paymentService;
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void ordPrefix_routesToMembershipService() throws Exception {
+        String json = "{\"out_trade_no\":\"ORD202608291234\",\"transaction_id\":\"wx_tx1\"}";
+        doReturn(json).when(paymentService).decryptAes256Gcm(anyString(), anyString(), anyString());
+
+        String requestBody = "{\"resource\":{\"ciphertext\":\"abc\",\"associated_data\":\"def\",\"nonce\":\"ghi\"}}";
+
+        Map<String, Object> result = paymentService.handleWechatNotify(requestBody, "mock-signature");
+
+        assertEquals("SUCCESS", result.get("code"));
+        verify(membershipService).processPaymentCallback("ORD202608291234", "wx_tx1", "wechat");
+        verify(productOrderService, never()).handlePaymentSuccess(any(), any());
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void poPrefix_routesToProductOrderService() throws Exception {
+        String json = "{\"out_trade_no\":\"PO202608290001\",\"transaction_id\":\"wx_tx2\"}";
+        doReturn(json).when(paymentService).decryptAes256Gcm(anyString(), anyString(), anyString());
+
+        String requestBody = "{\"resource\":{\"ciphertext\":\"abc\",\"associated_data\":\"def\",\"nonce\":\"ghi\"}}";
+
+        Map<String, Object> result = paymentService.handleWechatNotify(requestBody, "mock-signature");
+
+        assertEquals("SUCCESS", result.get("code"));
+        verify(productOrderService).handlePaymentSuccess("PO202608290001", "wx_tx2");
+        verify(membershipService, never()).processPaymentCallback(any(), any(), any());
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void unknownPrefix_routesToHandlePaymentCallback() throws Exception {
+        String json = "{\"out_trade_no\":\"PKG202608290001\",\"transaction_id\":\"wx_tx3\"}";
+        doReturn(json).when(paymentService).decryptAes256Gcm(anyString(), anyString(), anyString());
+
+        String requestBody = "{\"resource\":{\"ciphertext\":\"abc\",\"associated_data\":\"def\",\"nonce\":\"ghi\"}}";
+
+        when(packageOrderMapper.selectOne(any())).thenReturn(null);
+
+        Map<String, Object> result = paymentService.handleWechatNotify(requestBody, "mock-signature");
+
+        assertEquals("SUCCESS", result.get("code"));
+        verify(membershipService, never()).processPaymentCallback(any(), any(), any());
+        verify(productOrderService, never()).handlePaymentSuccess(any(), any());
+    }
+
+    private void setField(Object target, String fieldName, Object value) throws Exception {
+        Field field = target.getClass().getDeclaredField(fieldName);
+        field.setAccessible(true);
+        field.set(target, value);
+    }
+}

+ 0 - 1
cfc-frontend/pages/membership/pay.vue

@@ -236,7 +236,6 @@ export default {
           self.payWechat(data.wechatPayParams)
         } else {
           // testMode/trial:模拟支付成功
-          self.orderNo = data.orderNo
           uni.showToast({ title: '支付成功,开通中…', icon: 'none' })
           setTimeout(function() {
             self.paying = false