|
@@ -0,0 +1,191 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.entity.*;
|
|
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.mockito.Mock;
|
|
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 回归测试:resolveMemberViewToUserId — 成员视角(family_members.id 语义)解析为 users.id。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 背景(家庭72 报告问题3):成长页「饮食推荐」以 family_members.id=62 查食材(/api/health/foods?memberId=62)。
|
|
|
|
|
+ * users.id=62("用户2712")与 family_members.id=62(书宁宁, user_id=34)数值重叠。
|
|
|
|
|
+ * 旧逻辑(resolveFoodIndexUserId)为防 user→member 误归一,优先查 users 表 → 误判 62 为 users.id
|
|
|
|
|
+ * → food_recommend_idx 查 user_id=62 无数据(书宁宁食材实际挂 user_id=34)→ 成长页看不到食材。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 修复:成员视角新增 resolveMemberViewToUserId(语义明确为 family_members.id),始终优先按
|
|
|
|
|
+ * family_members 反查其登录用户 users.id,规避 user 视角的 users 优先启发式。
|
|
|
|
|
+ */
|
|
|
|
|
+class FoodRecommendMemberViewResolutionTest {
|
|
|
|
|
+
|
|
|
|
|
+ @Mock private FoodRecommendIndexMapper foodRecommendIndexMapper;
|
|
|
|
|
+ @Mock private FoodRecommendIdxLogMapper foodRecommendIdxLogMapper;
|
|
|
|
|
+ @Mock private ReportFoodSuitabilityMapper reportFoodSuitabilityMapper;
|
|
|
|
|
+ @Mock private FamilyMemberMapper familyMemberMapper;
|
|
|
|
|
+ @Mock private UserMapper userMapper;
|
|
|
|
|
+ @Mock private HealthReportService healthReportService;
|
|
|
|
|
+ @Mock private FoodService foodService;
|
|
|
|
|
+ @Mock private FamilyFoodOverviewService familyFoodOverviewService;
|
|
|
|
|
+
|
|
|
|
|
+ private FoodRecommendService service;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setup() throws Exception {
|
|
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
|
|
+ service = new FoodRecommendService();
|
|
|
|
|
+ setField(service, "foodRecommendIndexMapper", foodRecommendIndexMapper);
|
|
|
|
|
+ setField(service, "foodRecommendIdxLogMapper", foodRecommendIdxLogMapper);
|
|
|
|
|
+ setField(service, "reportFoodSuitabilityMapper", reportFoodSuitabilityMapper);
|
|
|
|
|
+ setField(service, "familyMemberMapper", familyMemberMapper);
|
|
|
|
|
+ setField(service, "userMapper", userMapper);
|
|
|
|
|
+ setField(service, "healthReportService", healthReportService);
|
|
|
|
|
+ setField(service, "foodService", foodService);
|
|
|
|
|
+ setField(service, "familyFoodOverviewService", familyFoodOverviewService);
|
|
|
|
|
+ installLambdaColumnCache();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void setField(Object target, String fieldName, Object value) throws Exception {
|
|
|
|
|
+ java.lang.reflect.Field f = FoodRecommendService.class.getDeclaredField(fieldName);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FamilyMember member(Long id, Long userId, Long familyId) {
|
|
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
|
|
+ m.setId(id);
|
|
|
|
|
+ m.setUserId(userId);
|
|
|
|
|
+ m.setFamilyId(familyId);
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private User memberUser(Long id) {
|
|
|
|
|
+ User u = new User();
|
|
|
|
|
+ u.setId(id);
|
|
|
|
|
+ u.setNickname("用户" + id);
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void memberView_familyMember62_resolvesToLoginUserId34() {
|
|
|
|
|
+ // 家庭72 书宁宁:family_members.id=62, user_id=34;users.id=62 是无关用户"用户2712"
|
|
|
|
|
+ when(familyMemberMapper.selectById(62L))
|
|
|
|
|
+ .thenReturn(member(62L, 34L, 72L));
|
|
|
|
|
+
|
|
|
|
|
+ Long resolved = service.resolveMemberViewToUserId(62L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(34L, resolved, "成员视角 family_members.id=62 应解析到其登录用户 users.id=34,"
|
|
|
|
|
+ + "而非数值重叠的无关用户 users.id=62");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void memberView_cfMember84_resolvesToLoginUserId27() {
|
|
|
|
|
+ // 家庭72 CF:family_members.id=84, user_id=27
|
|
|
|
|
+ when(familyMemberMapper.selectById(84L))
|
|
|
|
|
+ .thenReturn(member(84L, 27L, 72L));
|
|
|
|
|
+
|
|
|
|
|
+ Long resolved = service.resolveMemberViewToUserId(84L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(27L, resolved);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void memberView_notAMemberId_returnsAsIs() {
|
|
|
|
|
+ // 非成员卡(例如前端直接传 users.id)→ 原样返回
|
|
|
|
|
+ when(familyMemberMapper.selectById(999L)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ Long resolved = service.resolveMemberViewToUserId(999L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(999L, resolved);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void memberView_memberWithoutLoginUser_keepsMemberId() {
|
|
|
|
|
+ // 无登录账号的档案成员(user_id=null)→ 保留 member id(与 resolveFoodIndexUserId 语义一致)
|
|
|
|
|
+ when(familyMemberMapper.selectById(23L))
|
|
|
|
|
+ .thenReturn(member(23L, null, 72L));
|
|
|
|
|
+
|
|
|
|
|
+ Long resolved = service.resolveMemberViewToUserId(23L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(23L, resolved);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void memberView_null_returnsNull() {
|
|
|
|
|
+ assertNull(service.resolveMemberViewToUserId(null));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === recalculateForUser 写侧 writeKey 权威化(家庭72 报告63 案例) ===
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void recalculateForUser_reportSubjectId_authoritativeWriteKeyOverridesCollision() {
|
|
|
|
|
+ // 报告63:subject_id=84(CF 成员卡),user_id=27;users.id=84 是无关用户"用户1905"
|
|
|
|
|
+ // 旧逻辑 writeKey=resolveFoodIndexUserId(84)=84(users 优先碰撞误判);
|
|
|
|
|
+ // 权威化后 writeKey=resolveMemberViewToUserId(84)=27(CF 真实登录用户),食材应写入 user_id=27
|
|
|
|
|
+ HealthReport report = new HealthReport();
|
|
|
|
|
+ report.setId(63L);
|
|
|
|
|
+ report.setUserId(27L);
|
|
|
|
|
+ report.setSubjectId(84L);
|
|
|
|
|
+ report.setStatus("active");
|
|
|
|
|
+
|
|
|
|
|
+ when(familyMemberMapper.selectById(84L)).thenReturn(member(84L, 27L, 72L));
|
|
|
|
|
+ when(userMapper.selectById(84L)).thenReturn(memberUser(84L));
|
|
|
|
|
+ when(healthReportService.getLatestReport(84L)).thenReturn(report);
|
|
|
|
|
+
|
|
|
|
|
+ // 报告有食材:food id=10 score=5
|
|
|
|
|
+ Food food = new Food();
|
|
|
|
|
+ food.setId(10L);
|
|
|
|
|
+ food.setName("黄豆");
|
|
|
|
|
+ food.setScore(5);
|
|
|
|
|
+ when(foodService.listByReportId(63L)).thenReturn(java.util.Collections.singletonList(food));
|
|
|
|
|
+
|
|
|
|
|
+ // 已有索引为空 → 走 insert 分支
|
|
|
|
|
+ when(foodRecommendIndexMapper.selectList(any())).thenReturn(new java.util.ArrayList<>());
|
|
|
|
|
+ // refreshFamilyOverviews 内部反查成员家庭:返回空列表即可
|
|
|
|
|
+ when(familyMemberMapper.selectList(any())).thenReturn(new java.util.ArrayList<>());
|
|
|
|
|
+
|
|
|
|
|
+ service.recalculateForUser(84L);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证写入的 FoodRecommendIndex.userId = 27(CF 登录用户),而非碰撞误判的 84
|
|
|
|
|
+ // (1个食材且无已有索引 → 恰好 1 次 index insert;另有 1 次 idx-log insert,属不同 mapper)
|
|
|
|
|
+ org.mockito.ArgumentCaptor<FoodRecommendIndex> insCap =
|
|
|
|
|
+ org.mockito.ArgumentCaptor.forClass(FoodRecommendIndex.class);
|
|
|
|
|
+ verify(foodRecommendIndexMapper).insert(insCap.capture());
|
|
|
|
|
+ assertEquals(27L, insCap.getValue().getUserId(),
|
|
|
|
|
+ "食材写入键应为成员84的登录用户 users.id=27,而非数值重叠的无关用户 users.id=84");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void installLambdaColumnCache() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ java.lang.reflect.Field f = com.baomidou.mybatisplus.core.toolkit.LambdaUtils.class.getDeclaredField("COLUMN_CACHE_MAP");
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache>> cache =
|
|
|
|
|
+ (Map<String, Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache>>) f.get(null);
|
|
|
|
|
+ if (!cache.containsKey(FoodRecommendIndex.class.getName())) {
|
|
|
|
|
+ Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache> cols = new HashMap<>();
|
|
|
|
|
+ cols.put("USERID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("user_id", "user_id", null));
|
|
|
|
|
+ cols.put("FOODID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("food_id", "food_id", null));
|
|
|
|
|
+ cols.put("FOODNAME", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("food_name", "food_name", null));
|
|
|
|
|
+ cols.put("INDEXSCORE", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("index_score", "index_score", null));
|
|
|
|
|
+ cols.put("HEALTHREPORTID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("health_report_id", "health_report_id", null));
|
|
|
|
|
+ cols.put("CALCULATEDAT", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("calculated_at", "calculated_at", null));
|
|
|
|
|
+ cols.put("CREATEDAT", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("created_at", "created_at", null));
|
|
|
|
|
+ cache.put(FoodRecommendIndex.class.getName(), cols);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!cache.containsKey(com.etotem.cfc.entity.FamilyMember.class.getName())) {
|
|
|
|
|
+ Map<String, com.baomidou.mybatisplus.core.toolkit.support.ColumnCache> cols = new HashMap<>();
|
|
|
|
|
+ cols.put("USERID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("user_id", "user_id", null));
|
|
|
|
|
+ cols.put("FAMILYID", new com.baomidou.mybatisplus.core.toolkit.support.ColumnCache("family_id", "family_id", null));
|
|
|
|
|
+ cache.put(com.etotem.cfc.entity.FamilyMember.class.getName(), cols);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|