|
@@ -0,0 +1,262 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.entity.FamilyMembership;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyMembershipMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.MemberUpgradeRecordMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.MembershipLevelMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.mockito.Mock;
|
|
|
|
|
+import org.mockito.Mockito;
|
|
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
|
|
+
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.lang.reflect.Method;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.Mockito.doReturn;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * MembershipService 单元测试(纯单元测试,无 SpringBootTest,避免 MySQL 连接)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 测试范围:
|
|
|
|
|
+ * - getMemberLevel: 会员判定取个人/家庭身份中更高级别(含有效期、FREE/BASIC 平级 tie-break)
|
|
|
|
|
+ * - resolveEffectiveLevel: 级别比较核心逻辑
|
|
|
|
|
+ * - syncMembershipToFamilyMembers: 家庭同步只升不降,不覆盖更高级别个人身份
|
|
|
|
|
+ * - downgradeFamilyMembers: 降级时保留高于家庭历史最高级别的个人身份
|
|
|
|
|
+ *
|
|
|
|
|
+ * 说明:level_code → level_order 的映射属于配置数据而非被测逻辑,通过 spy stub
|
|
|
|
|
+ * getLevelOrder 提供(与生产 membership_levels 数据一致:FREE=1 BASIC=1 FAMILY=2
|
|
|
|
|
+ * PROVIDER=3 PREMIUM=4 LIFETIME=5)。
|
|
|
|
|
+ */
|
|
|
|
|
+public class MembershipServiceTest {
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private FamilyMembershipMapper membershipMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private MembershipLevelMapper levelMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private MemberUpgradeRecordMapper memberUpgradeRecordMapper;
|
|
|
|
|
+
|
|
|
|
|
+ private MembershipService service;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ public void setUp() {
|
|
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
|
|
+ MembershipService raw = new MembershipService();
|
|
|
|
|
+ setField(raw, "userMapper", userMapper);
|
|
|
|
|
+ setField(raw, "membershipMapper", membershipMapper);
|
|
|
|
|
+ setField(raw, "levelMapper", levelMapper);
|
|
|
|
|
+ setField(raw, "memberUpgradeRecordMapper", memberUpgradeRecordMapper);
|
|
|
|
|
+ service = Mockito.spy(raw);
|
|
|
|
|
+ doReturn(1).when(service).getLevelOrder("FREE");
|
|
|
|
|
+ doReturn(1).when(service).getLevelOrder("BASIC");
|
|
|
|
|
+ doReturn(2).when(service).getLevelOrder("FAMILY");
|
|
|
|
|
+ doReturn(3).when(service).getLevelOrder("PROVIDER");
|
|
|
|
|
+ doReturn(4).when(service).getLevelOrder("PREMIUM");
|
|
|
|
|
+ doReturn(5).when(service).getLevelOrder("LIFETIME");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void setField(Object target, String fieldName, Object value) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(fieldName);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, value);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private User user(Long id, String level, Date expire, Long familyId) {
|
|
|
|
|
+ User u = new User();
|
|
|
|
|
+ u.setId(id);
|
|
|
|
|
+ u.setMemberLevel(level);
|
|
|
|
|
+ u.setMemberExpireTime(expire);
|
|
|
|
|
+ u.setFamilyId(familyId);
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FamilyMembership activeMembership(String code) {
|
|
|
|
|
+ FamilyMembership fm = new FamilyMembership();
|
|
|
|
|
+ fm.setLevelCode(code);
|
|
|
|
|
+ fm.setPaymentStatus("paid");
|
|
|
|
|
+ fm.setEndDate(future());
|
|
|
|
|
+ return fm;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FamilyMembership membership(String code, Date end) {
|
|
|
|
|
+ FamilyMembership fm = new FamilyMembership();
|
|
|
|
|
+ fm.setLevelCode(code);
|
|
|
|
|
+ fm.setEndDate(end);
|
|
|
|
|
+ return fm;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date future() {
|
|
|
|
|
+ return new Date(System.currentTimeMillis() + 86_400_000L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date past() {
|
|
|
|
|
+ return new Date(System.currentTimeMillis() - 86_400_000L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ── getMemberLevel ─────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_nullUser_returnsFree() {
|
|
|
|
|
+ when(userMapper.selectById(any())).thenReturn(null);
|
|
|
|
|
+ assertEquals("FREE", service.getMemberLevel(999L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalFamily_noFamilyRecord_returnsFamily() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "FAMILY", future(), null));
|
|
|
|
|
+ assertEquals("FAMILY", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalFamily_expired_returnsFree() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "FAMILY", past(), null));
|
|
|
|
|
+ assertEquals("FREE", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalFree_familyPremiumActive_returnsPremium() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "FREE", null, 10L));
|
|
|
|
|
+ when(membershipMapper.selectOne(any())).thenReturn(activeMembership("PREMIUM"));
|
|
|
|
|
+ assertEquals("PREMIUM", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalPremium_familyFamily_returnsPremium() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "PREMIUM", future(), 10L));
|
|
|
|
|
+ when(membershipMapper.selectOne(any())).thenReturn(activeMembership("FAMILY"));
|
|
|
|
|
+ assertEquals("PREMIUM", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalFamily_familyPremium_returnsPremium() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "FAMILY", future(), 10L));
|
|
|
|
|
+ when(membershipMapper.selectOne(any())).thenReturn(activeMembership("PREMIUM"));
|
|
|
|
|
+ assertEquals("PREMIUM", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalBasic_tieBreakKeepsBasic() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "BASIC", future(), 10L));
|
|
|
|
|
+ when(membershipMapper.selectOne(any())).thenReturn(null);
|
|
|
|
|
+ assertEquals("BASIC", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalFree_familyBasic_tieBreakReturnsBasic() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "FREE", null, 10L));
|
|
|
|
|
+ when(membershipMapper.selectOne(any())).thenReturn(activeMembership("BASIC"));
|
|
|
|
|
+ assertEquals("BASIC", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void getMemberLevel_personalLifetime_noFamily_returnsLifetime() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "LIFETIME", null, null));
|
|
|
|
|
+ assertEquals("LIFETIME", service.getMemberLevel(1L));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ── resolveEffectiveLevel ──────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void resolveEffectiveLevel_higherFamilyWins() {
|
|
|
|
|
+ assertEquals("FAMILY", service.resolveEffectiveLevel("FREE", "FAMILY"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void resolveEffectiveLevel_higherPersonalWins() {
|
|
|
|
|
+ assertEquals("PREMIUM", service.resolveEffectiveLevel("PREMIUM", "FAMILY"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void resolveEffectiveLevel_tie_prefersNonFree() {
|
|
|
|
|
+ assertEquals("BASIC", service.resolveEffectiveLevel("FREE", "BASIC"));
|
|
|
|
|
+ assertEquals("BASIC", service.resolveEffectiveLevel("BASIC", "FREE"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void resolveEffectiveLevel_nullInputs_returnsFree() {
|
|
|
|
|
+ assertEquals("FREE", service.resolveEffectiveLevel(null, null));
|
|
|
|
|
+ assertEquals("FREE", service.resolveEffectiveLevel("FREE", null));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ── syncMembershipToFamilyMembers ──────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void syncMembership_neverOverwritesHigherPersonalLevel() {
|
|
|
|
|
+ User premium = user(1L, "PREMIUM", future(), 10L);
|
|
|
|
|
+ when(userMapper.selectList(any())).thenReturn(Collections.singletonList(premium));
|
|
|
|
|
+ callSync("FAMILY");
|
|
|
|
|
+ verify(userMapper, never()).updateById(premium);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void syncMembership_writesWhenFamilyLevelHigher() {
|
|
|
|
|
+ User freeUser = user(1L, "FREE", null, 10L);
|
|
|
|
|
+ when(userMapper.selectList(any())).thenReturn(Collections.singletonList(freeUser));
|
|
|
|
|
+ callSync("FAMILY");
|
|
|
|
|
+ verify(userMapper).updateById(freeUser);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void callSync(String levelCode) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Method m = MembershipService.class.getDeclaredMethod(
|
|
|
|
|
+ "syncMembershipToFamilyMembers", Long.class, String.class, Date.class, String.class, Long.class);
|
|
|
|
|
+ m.setAccessible(true);
|
|
|
|
|
+ m.invoke(service, 10L, levelCode, future(), "pay", 100L);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ── downgradeFamilyMembers ─────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void downgradeFamilyMembers_keepsHigherPersonalLevel() {
|
|
|
|
|
+ User lifetime = user(1L, "LIFETIME", null, 10L);
|
|
|
|
|
+ User family = user(2L, "FAMILY", future(), 10L);
|
|
|
|
|
+ when(userMapper.selectList(any())).thenReturn(Arrays.asList(lifetime, family));
|
|
|
|
|
+ when(membershipMapper.selectList(any())).thenReturn(
|
|
|
|
|
+ Collections.singletonList(membership("FAMILY", past())));
|
|
|
|
|
+ callDowngrade();
|
|
|
|
|
+ verify(userMapper, never()).updateById(lifetime);
|
|
|
|
|
+ verify(userMapper).updateById(family);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void downgradeFamilyMembers_noFamilyHistory_keepsAllPaidMembers() {
|
|
|
|
|
+ User premium = user(1L, "PREMIUM", future(), 10L);
|
|
|
|
|
+ when(userMapper.selectList(any())).thenReturn(Collections.singletonList(premium));
|
|
|
|
|
+ when(membershipMapper.selectList(any())).thenReturn(Collections.emptyList());
|
|
|
|
|
+ callDowngrade();
|
|
|
|
|
+ verify(userMapper, never()).updateById(premium);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void callDowngrade() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Method m = MembershipService.class.getDeclaredMethod("downgradeFamilyMembers", Long.class);
|
|
|
|
|
+ m.setAccessible(true);
|
|
|
|
|
+ m.invoke(service, 10L);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|