Ver Fonte

fix(membership): /plans 返回 levelOrder,修复免费用户付费页显示 ¥0.00

dbd94c59b 起升级页按接口 levelOrder 比较等级高低,但 getPricingPlans()
手工组装 Map 时遗漏 levelOrder,所有档位 levelOrder=undefined:
免费用户 applyDefaultLevel 找不到更高档位,selectedLevel 回落 FREE,
下一步按 FREE 取价=0 分,付费页显示 ¥0.00,下单被后端以
「该会员方案暂未开放购买」拒绝。

- MembershipController.getPricingPlans() 补 plan.put("levelOrder", ...)
- 新增契约单测 MembershipControllerPlansLevelOrderTest 锁定该字段
Sisyphus Agent há 1 semana atrás
pai
commit
a9f283d258

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java

@@ -349,6 +349,9 @@ public class MembershipController {
             plan.put("levelName", level.getLevelName());
             plan.put("levelDesc", level.getLevelDesc());
             plan.put("recommended", "FAMILY".equals(level.getLevelCode()));
+            // 升级页按接口 levelOrder 比较等级高低(前端不再维护等级排序),缺失会导致
+            // 免费用户无法定位下一档而回落 FREE,付费页按 FREE 取价显示 ¥0.00。
+            plan.put("levelOrder", level.getLevelOrder());
             plan.put("yearly", level.getPriceYearly());
             plan.put("monthly", level.getPriceMonthly());
             plan.put("quarterly", level.getPriceQuarterly());

+ 85 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/MembershipControllerPlansLevelOrderTest.java

@@ -0,0 +1,85 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.dto.MembershipLevelDTO;
+import com.etotem.cfc.service.MembershipService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Arrays;
+import java.util.List;
+
+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.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * 契约级单测:POST /api/membership/plans 必须返回 levelOrder。
+ *
+ * <p>背景(2026-11-05 付费页显示 ¥0.00):升级页 upgrade.vue 自 dbd94c59b
+ * 起按接口返回的 {@code levelOrder} 比较等级高低({@code getLevelOrder})。
+ * 但 {@code getPricingPlans()} 手工组装 Map 时遗漏了 levelOrder,导致所有档位
+ * levelOrder 均为 undefined/0:免费用户 applyDefaultLevel 无法找到更高档位,
+ * selectedLevel 回落为 FREE,下一步进入 pay 页时按 FREE 取价 = 0 分,
+ * 付费页显示 ¥0.00,下单被后端以「该会员方案暂未开放购买」拒绝。</p>
+ *
+ * <p>测试隔离:MockMvc standalone + mock MembershipService,无 Spring 上下文 / 无 DB。</p>
+ */
+class MembershipControllerPlansLevelOrderTest {
+
+    private static final String URL = "/api/membership/plans";
+
+    @Mock
+    private MembershipService membershipService;
+
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        MembershipController controller = new MembershipController();
+        ReflectionTestUtils.setField(controller, "membershipService", membershipService);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    private MembershipLevelDTO level(String code, int levelOrder, int monthly, int yearly) {
+        MembershipLevelDTO dto = new MembershipLevelDTO();
+        dto.setLevelCode(code);
+        dto.setLevelName(code + "-name");
+        dto.setLevelOrder(levelOrder);
+        dto.setPriceMonthly(monthly);
+        dto.setPriceQuarterly(0);
+        dto.setPriceYearly(yearly);
+        return dto;
+    }
+
+    @Test
+    void plans_返回每个可购档位的levelOrder() throws Exception {
+        List<MembershipLevelDTO> levels = Arrays.asList(
+                level("FREE", 1, 0, 0),
+                level("FAMILY", 3, 19800, 198000),
+                level("PREMIUM", 4, 131400, 1316800),
+                level("BASIC", 2, 990, 0),
+                level("LIFETIME", 5, 0, 0),
+                level("PROVIDER", 1, 13140, 131400)
+        );
+        when(membershipService.getAllLevels()).thenReturn(levels);
+
+        mockMvc.perform(post(URL).contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                // LIFETIME / PROVIDER 不可购,仍应被过滤
+                .andExpect(jsonPath("$.data.length()").value(4))
+                // 免费用户升级页依赖该字段判断下一档;缺失会导致选档回落 FREE、付费页显示 0
+                .andExpect(jsonPath("$.data[0].levelOrder").value(1))   // FREE
+                .andExpect(jsonPath("$.data[1].levelOrder").value(3))   // FAMILY
+                .andExpect(jsonPath("$.data[2].levelOrder").value(4))   // PREMIUM
+                .andExpect(jsonPath("$.data[3].levelOrder").value(2));  // BASIC
+    }
+}