|
|
@@ -0,0 +1,364 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.Family;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.FamilyMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberAttributesMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberLogMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMembershipMapper;
|
|
|
+import com.etotem.cfc.mapper.FiveDimensionScoreMapper;
|
|
|
+import com.etotem.cfc.mapper.HealthReportMapper;
|
|
|
+import com.etotem.cfc.mapper.MembershipLevelMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.service.CouponService;
|
|
|
+import com.etotem.cfc.service.FamilyFoodOverviewService;
|
|
|
+import com.etotem.cfc.service.FamilyMemberService;
|
|
|
+import com.etotem.cfc.service.FamilyRelationshipService;
|
|
|
+import com.etotem.cfc.service.MembershipService;
|
|
|
+import com.etotem.cfc.service.OnboardingService;
|
|
|
+import com.etotem.cfc.service.ReportMemberPermissionService;
|
|
|
+import com.etotem.cfc.service.SystemConfigService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:能力键 {@code family.familyMembers.removeMember}(移除/退出家庭成员后的会员身份重算)。
|
|
|
+ *
|
|
|
+ * <p><b>溯源·需求</b>:{@code tests/user-scenarios/应用场景总览.md} SC-P-06(移除/删除成员,业务域 A 家庭与成员管理)。
|
|
|
+ * <p><b>溯源·设计</b>:{@code docs/superpowers/api/API_REFERENCE.md} §4.2
|
|
|
+ * {@code POST /api/family/member/kick}(移除成员)/ {@code POST /api/family/member/leave}(主动退出家庭),
|
|
|
+ * 均已合入 origin/cfclub。</p>
|
|
|
+ *
|
|
|
+ * <p><b>用户实测缺陷</b>:用户被从家庭会员家庭移除后未恢复免费身份——
|
|
|
+ * {@code users.member_level / member_expire_time} 缓存残留(仍在 FAMILY),
|
|
|
+ * {@code MembershipService.getMemberLevel} 取 personal 与 family 高者仍返回 FAMILY。
|
|
|
+ * <b>期望</b>:kick/leave 后被踢用户如无个人独立更高等级会员则恢复 FREE。</p>
|
|
|
+ *
|
|
|
+ * <p><b>实现链现状(缺陷定位)</b>:
|
|
|
+ * <blockquote>
|
|
|
+ * {@code FamilyMemberService.kickMember()} 仅调用 {@code membershipService.reconcileFamilyMembership(oldFamilyId)}
|
|
|
+ * (会先迁移被踢用户 familyId → 初始家庭,再对原家庭查全部成员)——被踢用户已迁出,不在
|
|
|
+ * {@code downgradeFamilyMembers} / {@code syncMembershipToFamilyMembers}(只升不降)的查询范围,
|
|
|
+ * 其 {@code users.member_level} 缓存原样残留;{@code leaveFamily()} 同样只核原家庭。
|
|
|
+ * {@code MembershipService.getMemberLevel(userId)} 把缓存值当「个人维度」参与取高,
|
|
|
+ * 新家庭无有效会员(family=FREE)时仍返回缓存的 FAMILY。
|
|
|
+ * </blockquote></p>
|
|
|
+ *
|
|
|
+ * <p><b>修复方向(GREEN 升级指引)</b>:kick/leave 对被踢用户按「个人维度」重算会员缓存——
|
|
|
+ * 仅清除源自家庭会员的缓存(等级 + 到期时间),保留更高等级独立个人会员;若无独立更高等级则恢复 FREE。
|
|
|
+ * 本测试以「结果契约」断言(被踢用户落库的 member_level / getMemberLevel 返回值),不预设具体方法名,
|
|
|
+ * 实现即可自主选择改造路径(新方法或对新家庭维度的重核),只要结果符合契约即转绿。</p>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期</b>:本分支 src/main 未落地该重算逻辑——「恢复 FREE」「getMemberLevel 重归 FREE」
|
|
|
+ * 类用例应失败(红,4 例);「保留更高个人会员」「权限守卫」类用例当前已具备应为绿(用于锁定
|
|
|
+ * GREEN 不得误清个人高等级会员、不得放开权限规则)。纯 Mockito / 反射,无 Spring / 无 DB / 无外部服务。</p>
|
|
|
+ */
|
|
|
+class FamilyRemoveMemberContractTest {
|
|
|
+
|
|
|
+ private static final String LEVEL_FREE = "FREE";
|
|
|
+ private static final String LEVEL_FAMILY = "FAMILY";
|
|
|
+ private static final String LEVEL_LIFETIME = "LIFETIME";
|
|
|
+
|
|
|
+ private static final Long ADMIN_USER_ID = 1L;
|
|
|
+ private static final Long KICKED_USER_ID = 200L;
|
|
|
+ private static final Long OLD_FAMILY_ID = 100L;
|
|
|
+ private static final Long OWN_FAMILY_ID = 300L;
|
|
|
+ private static final Long MEMBER_ID = 105L;
|
|
|
+
|
|
|
+ // ---------------- FamilyMemberService 被测对象及其依赖 ----------------
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Mock
|
|
|
+ private UserMapper userMapper;
|
|
|
+ @Mock
|
|
|
+ private FamilyMapper familyMapper;
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberAttributesMapper familyMemberAttributesMapper;
|
|
|
+ @Mock
|
|
|
+ private OnboardingService onboardingService;
|
|
|
+ @Mock
|
|
|
+ private SystemConfigService systemConfigService;
|
|
|
+ @Mock
|
|
|
+ private FamilyRelationshipService familyRelationshipService;
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberLogMapper familyMemberLogMapper;
|
|
|
+ @Mock
|
|
|
+ private CouponService couponService;
|
|
|
+ @Mock
|
|
|
+ private FiveDimensionScoreMapper fiveDimensionScoreMapper;
|
|
|
+ @Mock
|
|
|
+ private HealthReportMapper healthReportMapper;
|
|
|
+ @Mock
|
|
|
+ private MembershipService membershipService;
|
|
|
+ @Mock
|
|
|
+ private FamilyFoodOverviewService familyFoodOverviewService;
|
|
|
+ @Mock
|
|
|
+ private ReportMemberPermissionService reportMemberPermissionService;
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private FamilyMemberService familyMemberService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 正常流:踢出微信用户(kick) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-1:踢出微信成员后,其家庭派生会员缓存应重算为 FREE(无独立更高等级时恢复免费身份)")
|
|
|
+ void kick微信用户_家庭派生会员缓存应重算为FREE() {
|
|
|
+ // 操作人 = 家庭创建者(管理员)
|
|
|
+ User admin = user(ADMIN_USER_ID, OLD_FAMILY_ID, "o_admin_001", LEVEL_FREE, null);
|
|
|
+ when(userMapper.selectById(ADMIN_USER_ID)).thenReturn(admin);
|
|
|
+ when(familyMapper.selectById(OLD_FAMILY_ID)).thenReturn(family(OLD_FAMILY_ID, ADMIN_USER_ID));
|
|
|
+ when(familyMemberMapper.selectById(MEMBER_ID))
|
|
|
+ .thenReturn(member(MEMBER_ID, OLD_FAMILY_ID, KICKED_USER_ID, "小李"));
|
|
|
+
|
|
|
+ // 被踢用户:微信登录用户,会员缓存为家庭派生(FAMILY + 未到期),且无独立更高等级个人会员
|
|
|
+ User kicked = user(KICKED_USER_ID, OLD_FAMILY_ID, "o_kicked_001", LEVEL_FAMILY, future());
|
|
|
+ when(userMapper.selectById(KICKED_USER_ID)).thenReturn(kicked);
|
|
|
+ // 被踢用户已有自己的初始家庭(资产随成员迁入)
|
|
|
+ when(familyMapper.selectOne(any())).thenReturn(family(OWN_FAMILY_ID, KICKED_USER_ID));
|
|
|
+
|
|
|
+ familyMemberService.kickMember(ADMIN_USER_ID, MEMBER_ID);
|
|
|
+
|
|
|
+ ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
|
|
+ verify(userMapper).updateById(captor.capture());
|
|
|
+ User persisted = captor.getValue();
|
|
|
+
|
|
|
+ assertEquals(LEVEL_FREE, persisted.getMemberLevel(),
|
|
|
+ "[family.familyMembers.removeMember] 踢出微信成员后,其 users.member_level 缓存应重算为 FREE"
|
|
|
+ + "(仅清除源自家庭会员的缓存、恢复免费身份)——当前实现未落地:kickMember 只重核原家庭"
|
|
|
+ + "(reconcileFamilyMembership(oldFamilyId)),被踢用户已迁出其家庭,缓存仍残留="
|
|
|
+ + persisted.getMemberLevel() + ",期望=FREE(SC-P-06 / API §4.2 member/kick)。");
|
|
|
+ assertNull(persisted.getMemberExpireTime(),
|
|
|
+ "[family.familyMembers.removeMember] 被踢用户无独立更高等级会员时,member_expire_time 缓存应一并清除。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-2:踢出持有独立更高等级个人会员(LIFETIME)的用户,LIFETIME 应保留不被误清")
|
|
|
+ void kick用户_独立LIFETIME个人会员应保留() {
|
|
|
+ User admin = user(ADMIN_USER_ID, OLD_FAMILY_ID, "o_admin_001", LEVEL_FREE, null);
|
|
|
+ when(userMapper.selectById(ADMIN_USER_ID)).thenReturn(admin);
|
|
|
+ when(familyMapper.selectById(OLD_FAMILY_ID)).thenReturn(family(OLD_FAMILY_ID, ADMIN_USER_ID));
|
|
|
+ when(familyMemberMapper.selectById(MEMBER_ID))
|
|
|
+ .thenReturn(member(MEMBER_ID, OLD_FAMILY_ID, KICKED_USER_ID, "小李"));
|
|
|
+
|
|
|
+ // 被踢用户自己购买了 LIFETIME(levelOrder 高于家庭派生的 FAMILY),属个人高等级会员
|
|
|
+ User kicked = user(KICKED_USER_ID, OLD_FAMILY_ID, "o_kicked_001", LEVEL_LIFETIME, future());
|
|
|
+ when(userMapper.selectById(KICKED_USER_ID)).thenReturn(kicked);
|
|
|
+ when(familyMapper.selectOne(any())).thenReturn(family(OWN_FAMILY_ID, KICKED_USER_ID));
|
|
|
+
|
|
|
+ familyMemberService.kickMember(ADMIN_USER_ID, MEMBER_ID);
|
|
|
+
|
|
|
+ ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
|
|
+ verify(userMapper).updateById(captor.capture());
|
|
|
+ User persisted = captor.getValue();
|
|
|
+
|
|
|
+ // 契约:仅清除「源自家庭会员」的缓存,更高等级独立个人会员必须保留(GREEN 不得一刀切清空)
|
|
|
+ assertEquals(LEVEL_LIFETIME, persisted.getMemberLevel(),
|
|
|
+ "[family.familyMembers.removeMember] 被踢用户独立 LIFETIME 个人会员应保留(仅清除家庭派生缓存)。");
|
|
|
+ assertNotNull(persisted.getMemberExpireTime(),
|
|
|
+ "[family.familyMembers.removeMember] 个人会员到期时间不应被清除。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 边界流:踢出非微信用户(回收箱) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-1:踢出非微信用户进回收箱,其家庭派生会员缓存也应重算为 FREE")
|
|
|
+ void kick非微信用户进回收箱_家庭派生会员缓存应重算为FREE() {
|
|
|
+ User admin = user(ADMIN_USER_ID, OLD_FAMILY_ID, "o_admin_001", LEVEL_FREE, null);
|
|
|
+ when(userMapper.selectById(ADMIN_USER_ID)).thenReturn(admin);
|
|
|
+ when(familyMapper.selectById(OLD_FAMILY_ID)).thenReturn(family(OLD_FAMILY_ID, ADMIN_USER_ID));
|
|
|
+ when(familyMemberMapper.selectById(MEMBER_ID))
|
|
|
+ .thenReturn(member(MEMBER_ID, OLD_FAMILY_ID, KICKED_USER_ID, "小李"));
|
|
|
+
|
|
|
+ // 非微信用户(openid 为 phone: 前缀),会员缓存为家庭派生 FAMILY
|
|
|
+ User kicked = user(KICKED_USER_ID, OLD_FAMILY_ID, "phone:13800138000", LEVEL_FAMILY, future());
|
|
|
+ when(userMapper.selectById(KICKED_USER_ID)).thenReturn(kicked);
|
|
|
+
|
|
|
+ familyMemberService.kickMember(ADMIN_USER_ID, MEMBER_ID);
|
|
|
+
|
|
|
+ ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
|
|
+ verify(userMapper).updateById(captor.capture());
|
|
|
+ User persisted = captor.getValue();
|
|
|
+
|
|
|
+ assertEquals(LEVEL_FREE, persisted.getMemberLevel(),
|
|
|
+ "[family.familyMembers.removeMember] 踢出非微信用户进回收箱(familyId 置空)时,"
|
|
|
+ + "其家庭派生会员缓存(users.member_level)也应重算为 FREE——当前仍残留="
|
|
|
+ + persisted.getMemberLevel() + ",期望=FREE。");
|
|
|
+ assertNull(persisted.getFamilyId(), "非微信用户被踢后 familyId 应置空(进回收箱)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 正常流:主动退出家庭(leave) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-3:主动退出家庭后,其家庭派生会员缓存应重算为 FREE(恢复免费身份)")
|
|
|
+ void leave微信用户_家庭派生会员缓存应重算为FREE() {
|
|
|
+ User leaver = user(KICKED_USER_ID, OLD_FAMILY_ID, "o_kicked_001", LEVEL_FAMILY, future());
|
|
|
+ when(userMapper.selectById(KICKED_USER_ID)).thenReturn(leaver);
|
|
|
+ when(familyMapper.selectById(OLD_FAMILY_ID)).thenReturn(family(OLD_FAMILY_ID, ADMIN_USER_ID));
|
|
|
+ when(familyMemberMapper.selectOne(any()))
|
|
|
+ .thenReturn(member(MEMBER_ID, OLD_FAMILY_ID, KICKED_USER_ID, "小李"));
|
|
|
+ when(familyMapper.selectOne(any())).thenReturn(family(OWN_FAMILY_ID, KICKED_USER_ID));
|
|
|
+
|
|
|
+ familyMemberService.leaveFamily(KICKED_USER_ID);
|
|
|
+
|
|
|
+ ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
|
|
+ verify(userMapper).updateById(captor.capture());
|
|
|
+ User persisted = captor.getValue();
|
|
|
+
|
|
|
+ assertEquals(LEVEL_FREE, persisted.getMemberLevel(),
|
|
|
+ "[family.familyMembers.removeMember] 主动退出家庭后,其 users.member_level 缓存应重算为 FREE"
|
|
|
+ + "(当前仅重核原家庭 reconcileFamilyMembership(oldFamilyId),退出者已不在范围,缓存仍残留="
|
|
|
+ + persisted.getMemberLevel() + ",期望=FREE)(API §4.2 member/leave)。");
|
|
|
+ assertNull(persisted.getMemberExpireTime(),
|
|
|
+ "[family.familyMembers.removeMember] 退出用户无独立更高等级会员时,member_expire_time 缓存应一并清除。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-2:主动退出家庭后,独立 LIFETIME 个人会员应保留")
|
|
|
+ void leave用户_独立LIFETIME个人会员应保留() {
|
|
|
+ User leaver = user(KICKED_USER_ID, OLD_FAMILY_ID, "o_kicked_001", LEVEL_LIFETIME, future());
|
|
|
+ when(userMapper.selectById(KICKED_USER_ID)).thenReturn(leaver);
|
|
|
+ when(familyMapper.selectById(OLD_FAMILY_ID)).thenReturn(family(OLD_FAMILY_ID, ADMIN_USER_ID));
|
|
|
+ when(familyMemberMapper.selectOne(any()))
|
|
|
+ .thenReturn(member(MEMBER_ID, OLD_FAMILY_ID, KICKED_USER_ID, "小李"));
|
|
|
+ when(familyMapper.selectOne(any())).thenReturn(family(OWN_FAMILY_ID, KICKED_USER_ID));
|
|
|
+
|
|
|
+ familyMemberService.leaveFamily(KICKED_USER_ID);
|
|
|
+
|
|
|
+ ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
|
|
+ verify(userMapper).updateById(captor.capture());
|
|
|
+ User persisted = captor.getValue();
|
|
|
+
|
|
|
+ assertEquals(LEVEL_LIFETIME, persisted.getMemberLevel(),
|
|
|
+ "[family.familyMembers.removeMember] 退出用户独立 LIFETIME 个人会员应保留。");
|
|
|
+ assertNotNull(persisted.getMemberExpireTime(),
|
|
|
+ "[family.familyMembers.removeMember] 个人会员到期时间不应被清除。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 用户可见症状:getMemberLevel 应重归个人维度 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("用户症状:被移除用户(缓存残留 FAMILY + 新家庭无会员)getMemberLevel 应返回 FREE,而非 FAMILY")
|
|
|
+ void 被移除用户_getMemberLevel应重归FREE() {
|
|
|
+ // 真实 MembershipService + 3 个 mock mapper(无 Spring / 无 DB)
|
|
|
+ MembershipService membership = new MembershipService();
|
|
|
+ UserMapper um = mock(UserMapper.class);
|
|
|
+ FamilyMembershipMapper fmm = mock(FamilyMembershipMapper.class);
|
|
|
+ MembershipLevelMapper lmm = mock(MembershipLevelMapper.class);
|
|
|
+ setPrivateField(membership, "userMapper", um);
|
|
|
+ setPrivateField(membership, "membershipMapper", fmm);
|
|
|
+ setPrivateField(membership, "levelMapper", lmm);
|
|
|
+
|
|
|
+ // 被移除用户:已回到自己的初始家庭(familyId=300,无有效家庭会员),
|
|
|
+ // 但 users.member_level 仍残留家庭派生的 FAMILY(本次移出动作未重算缓存的缺陷现场)
|
|
|
+ User kicked = user(KICKED_USER_ID, OWN_FAMILY_ID, "o_kicked_001", LEVEL_FAMILY, future());
|
|
|
+ when(um.selectById(KICKED_USER_ID)).thenReturn(kicked);
|
|
|
+ when(fmm.selectOne(any())).thenReturn(null); // 新家庭无有效会员 → family 维度 FREE
|
|
|
+ when(lmm.selectOne(any())).thenReturn(null); // 未知等级按最低阶处理
|
|
|
+
|
|
|
+ assertEquals(LEVEL_FREE, membership.getMemberLevel(KICKED_USER_ID),
|
|
|
+ "[family.familyMembers.removeMember] 被移出家庭的用户(无独立更高等级个人会员)"
|
|
|
+ + "getMemberLevel 应返回 FREE——用户实测缺陷:缓存残留导致仍返回 FAMILY(实际="
|
|
|
+ + membership.getMemberLevel(KICKED_USER_ID) + ",期望=FREE)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 权限流:踢出成员权限守卫(当前已具备,样式锚点) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流-1:踢出成员仅家庭创建者(管理员)可操作,非管理员拒绝")
|
|
|
+ void kick非创建者_拒绝() {
|
|
|
+ User operator = user(ADMIN_USER_ID, OLD_FAMILY_ID, "o_admin_001", LEVEL_FREE, null);
|
|
|
+ when(userMapper.selectById(ADMIN_USER_ID)).thenReturn(operator);
|
|
|
+ // 家庭创建者不是操作人
|
|
|
+ when(familyMapper.selectById(OLD_FAMILY_ID)).thenReturn(family(OLD_FAMILY_ID, 999L));
|
|
|
+
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
+ () -> familyMemberService.kickMember(ADMIN_USER_ID, MEMBER_ID));
|
|
|
+ assertEquals("只有家庭管理员才能踢出成员", ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流-2:不能踢出家庭创建者本人(管理员需先转让)")
|
|
|
+ void kick创建者本人_拒绝() {
|
|
|
+ User operator = user(ADMIN_USER_ID, OLD_FAMILY_ID, "o_admin_001", LEVEL_FREE, null);
|
|
|
+ when(userMapper.selectById(ADMIN_USER_ID)).thenReturn(operator);
|
|
|
+ when(familyMapper.selectById(OLD_FAMILY_ID)).thenReturn(family(OLD_FAMILY_ID, ADMIN_USER_ID));
|
|
|
+ // 目标成员就是家庭创建者本人
|
|
|
+ when(familyMemberMapper.selectById(MEMBER_ID))
|
|
|
+ .thenReturn(member(MEMBER_ID, OLD_FAMILY_ID, ADMIN_USER_ID, "管理员"));
|
|
|
+
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
+ () -> familyMemberService.kickMember(ADMIN_USER_ID, MEMBER_ID));
|
|
|
+ assertEquals("不能踢出家庭创建者,请先转让管理员权限", ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 辅助构造 ====================
|
|
|
+
|
|
|
+ private User user(Long id, Long familyId, String openid, String memberLevel, Date memberExpireTime) {
|
|
|
+ User u = new User();
|
|
|
+ u.setId(id);
|
|
|
+ u.setFamilyId(familyId);
|
|
|
+ u.setOpenid(openid);
|
|
|
+ u.setMemberLevel(memberLevel);
|
|
|
+ u.setMemberExpireTime(memberExpireTime);
|
|
|
+ u.setNickname("成员" + id);
|
|
|
+ return u;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Family family(Long id, Long creatorId) {
|
|
|
+ Family f = new Family();
|
|
|
+ f.setId(id);
|
|
|
+ f.setName("契约测试家庭" + id);
|
|
|
+ f.setCreatorId(creatorId);
|
|
|
+ return f;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FamilyMember member(Long id, Long familyId, Long userId, String nickname) {
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
+ m.setId(id);
|
|
|
+ m.setFamilyId(familyId);
|
|
|
+ m.setUserId(userId);
|
|
|
+ m.setNickname(nickname);
|
|
|
+ m.setStatus("active");
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 一年后到期(家庭会员未过期,缓存残留场景) */
|
|
|
+ private Date future() {
|
|
|
+ return new Date(System.currentTimeMillis() + 365L * 24 * 3600 * 1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void setPrivateField(Object target, String name, Object value) {
|
|
|
+ try {
|
|
|
+ Field f = target.getClass().getDeclaredField(name);
|
|
|
+ f.setAccessible(true);
|
|
|
+ f.set(target, value);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new IllegalStateException("反射注入失败: " + name, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|