|
|
@@ -3,6 +3,8 @@ package com.etotem.cfc.service;
|
|
|
import com.etotem.cfc.entity.*;
|
|
|
import com.etotem.cfc.mapper.*;
|
|
|
import com.etotem.cfc.service.HealthReportService;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.LambdaUtils;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.support.ColumnCache;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
import org.mockito.ArgumentCaptor;
|
|
|
@@ -44,6 +46,9 @@ public class FoodRecommendServiceTest {
|
|
|
@Mock
|
|
|
private FamilyMemberMapper familyMemberMapper;
|
|
|
|
|
|
+ @Mock
|
|
|
+ private UserMapper userMapper;
|
|
|
+
|
|
|
@Mock
|
|
|
private HealthReportService healthReportService;
|
|
|
|
|
|
@@ -63,9 +68,35 @@ public class FoodRecommendServiceTest {
|
|
|
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();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * MP 3.5.3.1 纯单测环境(无 MyBatis TableInfo 初始化)下,LambdaQueryWrapper 的列名缓存
|
|
|
+ * (LambdaUtils.COLUMN_CACHE_MAP)为空:tryInitCache 经 getColumnMap 拿不到缓存时
|
|
|
+ * Assert.notNull 抛 MybatisPlusException(can not find lambda cache for this entity)。
|
|
|
+ * 这里直接向缓存预置 FamilyMember 列名映射(外层 key = 实体类全名;内层 key =
|
|
|
+ * LambdaUtils.formatKey(property) = 字段名转大写),使 getCustomSqlSegment 能解析反查键。
|
|
|
+ */
|
|
|
+ private static void installLambdaColumnCache() {
|
|
|
+ try {
|
|
|
+ java.lang.reflect.Field f = LambdaUtils.class.getDeclaredField("COLUMN_CACHE_MAP");
|
|
|
+ f.setAccessible(true);
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Map<String, ColumnCache>> cache =
|
|
|
+ (Map<String, Map<String, ColumnCache>>) f.get(null);
|
|
|
+ if (!cache.containsKey(FamilyMember.class.getName())) {
|
|
|
+ Map<String, ColumnCache> cols = new HashMap<>();
|
|
|
+ cols.put("USERID", new ColumnCache("user_id", "user_id", null));
|
|
|
+ cache.put(FamilyMember.class.getName(), cols);
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 缓存注入失败时走原有断言路径
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
|
|
@@ -262,6 +293,8 @@ public class FoodRecommendServiceTest {
|
|
|
(com.baomidou.mybatisplus.core.conditions.AbstractWrapper<FamilyMember, ?, ?>) wrapperCaptor.getValue();
|
|
|
// MP 3.5.x 的 paramNameValuePairs 在 getSqlSegment()/getCustomSqlSegment() 时才惰性填充(eq() 时不写),
|
|
|
// 必须先强制生成 SQL 段,再读取参数值,否则拿到的 map 为空。
|
|
|
+ // 纯单测环境无 MyBatis TableInfo 初始化,列名缓存由 setup() 中 installLambdaColumnCache() 预置,
|
|
|
+ // 保证 getCustomSqlSegment() 能解析 user_id 列而不抛 can not find lambda cache。
|
|
|
aw.getCustomSqlSegment();
|
|
|
java.util.Map<String, Object> params = aw.getParamNameValuePairs();
|
|
|
assertTrue(params.values().contains(81330L), "反查应使用归一化 users.id 键");
|
|
|
@@ -281,6 +314,63 @@ public class FoodRecommendServiceTest {
|
|
|
assertEquals(81330L, result.get(0).getUserId());
|
|
|
}
|
|
|
|
|
|
+ // === T28-FIX AC-09 P0 加固:resolveFoodIndexUserId 数值碰撞防护 ===
|
|
|
+ // users 表与 family_members 表各自独立自增,id 数值空间高度重叠。若前端传的是 users.id(如 userId=500),
|
|
|
+ // 而 family_members.id=500 恰有记录(memberId=500, user_id=100),旧逻辑只查 family_members 会误判为
|
|
|
+ // memberId=500 → 返回 100 → 读到成员 100 的数据而非用户 500 自己的数据。加固后必须优先判定 users 表。
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void resolveFoodIndexUserId_usersIdNotMember_returnedAsIs() {
|
|
|
+ // 模拟数值碰撞:users.id=500 存在,family_members.id=500 也存在(memberId=500, user_id=100)。
|
|
|
+ // 加固后优先走 users 表,返回原值 500L(旧逻辑会误判为 memberId=500 → 返回 100)。
|
|
|
+ User user = new User();
|
|
|
+ user.setId(500L);
|
|
|
+ when(userMapper.selectById(500L)).thenReturn(user);
|
|
|
+ when(familyMemberMapper.selectById(500L)).thenReturn(mockMember(500L, 100L));
|
|
|
+
|
|
|
+ Long resolved = service.resolveFoodIndexUserId(500L);
|
|
|
+
|
|
|
+ assertEquals(500L, resolved);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void resolveFoodIndexUserId_alreadyInUsers_noMember() {
|
|
|
+ // users.id 命中,family_members.id 无记录 → 返回原值(已是 users.id,幂等)
|
|
|
+ User user = new User();
|
|
|
+ user.setId(500L);
|
|
|
+ when(userMapper.selectById(500L)).thenReturn(user);
|
|
|
+ when(familyMemberMapper.selectById(500L)).thenReturn(null);
|
|
|
+
|
|
|
+ Long resolved = service.resolveFoodIndexUserId(500L);
|
|
|
+
|
|
|
+ assertEquals(500L, resolved);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void recalculateForUser_usersIdInput_keepsUserId() {
|
|
|
+ // 前端传 users.id=500:users 表命中,且无 family_members.id=500 记录 → 写入键 = 500(不被误归一为 100)
|
|
|
+ User user = new User();
|
|
|
+ user.setId(500L);
|
|
|
+ when(userMapper.selectById(500L)).thenReturn(user);
|
|
|
+ when(familyMemberMapper.selectById(500L)).thenReturn(null);
|
|
|
+ HealthReport report = mockReport(1L, 1L);
|
|
|
+ when(healthReportService.getLatestReport(500L)).thenReturn(report);
|
|
|
+ Food food = new Food();
|
|
|
+ food.setId(100L);
|
|
|
+ food.setName("食材A");
|
|
|
+ food.setScore(-25);
|
|
|
+ when(foodService.listByReportId(1L)).thenReturn(Collections.singletonList(food));
|
|
|
+ when(foodRecommendIndexMapper.selectList(any())).thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ service.recalculateForUser(500L);
|
|
|
+
|
|
|
+ ArgumentCaptor<FoodRecommendIndex> idxCaptor = ArgumentCaptor.forClass(FoodRecommendIndex.class);
|
|
|
+ verify(foodRecommendIndexMapper).insert(idxCaptor.capture());
|
|
|
+ // 关键断言:写入键保持 users.id=500,绝不被 family_members.id 碰撞误归一为 100
|
|
|
+ assertEquals(500L, idxCaptor.getValue().getUserId());
|
|
|
+ assertEquals(-25, idxCaptor.getValue().getIndexScore());
|
|
|
+ }
|
|
|
+
|
|
|
// === getIndexByUser ===
|
|
|
|
|
|
@Test
|