|
@@ -47,14 +47,48 @@ public class FoodRecommendService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private FamilyFoodOverviewService familyFoodOverviewService;
|
|
private FamilyFoodOverviewService familyFoodOverviewService;
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 把外部传入的 subjectId 归一为 food_recommend_idx 的统一写入/读取键(T28 方向1:写侧统一为 users.id)。
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 报告确认链路把被检测人归一为 {@code family_members.id}(memberId,见
|
|
|
|
|
+ * {@link HealthReportService#resolveSubjectToMemberId}),而家庭总览读侧按
|
|
|
|
|
+ * {@code family_members.user_id}(即 users.id)关联 food_recommend_idx —— 若把 memberId 原样写入
|
|
|
|
|
+ * {@code food_recommend_idx.user_id},两条键不唯一对齐,家庭总览重算链路失效(AC-09 P0)。
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 规则:
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>命中 {@code family_members.id} 且该成员有登录账号(user_id 非空)→ 返回其 {@code users.id}(登录成员统一键);</li>
|
|
|
|
|
+ * <li>命中 {@code family_members.id} 但 user_id 为空(纯档案成员,无登录账号)→ 原样返回 memberId
|
|
|
|
|
+ * (历史口径:无 users.id 可挂,且家庭总览/按家庭查询天然排除此类成员,不会参与 min 计算);</li>
|
|
|
|
|
+ * <li>未命中 {@code family_members.id} → 视为已是 {@code users.id},原样返回(幂等,兼容已按 users.id 写入的历史数据)。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ */
|
|
|
|
|
+ public Long resolveFoodIndexUserId(Long subjectId) {
|
|
|
|
|
+ if (subjectId == null) return null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ FamilyMember m = familyMemberMapper.selectById(subjectId);
|
|
|
|
|
+ if (m != null) {
|
|
|
|
|
+ return m.getUserId() != null ? m.getUserId() : subjectId;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("subjectId 归一为 food_recommend_idx 键失败 subjectId={}: {}", subjectId, e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ return subjectId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Transactional
|
|
@Transactional
|
|
|
public void recalculateForUser(Long userId) {
|
|
public void recalculateForUser(Long userId) {
|
|
|
|
|
+ // T28 方向1:入参可能是 family_members.id(报告确认链路 subjectId,经 resolveSubjectToMemberId 归一)
|
|
|
|
|
+ // 或 users.id;food_recommend_idx 的写入键统一归一为成员真实登录用户 users.id,
|
|
|
|
|
+ // 与家庭总览读侧(family_members.user_id 关联)对齐。读侧 getLatestReport / snapshotFoodIndices 的
|
|
|
|
|
+ // subject 语义仍用原始入参(health_reports.subject_id 本就是 memberId)。
|
|
|
|
|
+ Long writeKey = resolveFoodIndexUserId(userId);
|
|
|
HealthReport report = healthReportService.getLatestReport(userId);
|
|
HealthReport report = healthReportService.getLatestReport(userId);
|
|
|
if (report == null) {
|
|
if (report == null) {
|
|
|
foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
|
- .eq(FoodRecommendIndex::getUserId, userId));
|
|
|
|
|
|
|
+ .eq(FoodRecommendIndex::getUserId, writeKey));
|
|
|
log.info("用户{}无最新健康报告,已清除推荐指数", userId);
|
|
log.info("用户{}无最新健康报告,已清除推荐指数", userId);
|
|
|
- refreshFamilyOverviews(userId);
|
|
|
|
|
|
|
+ refreshFamilyOverviews(writeKey);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -67,9 +101,9 @@ public class FoodRecommendService {
|
|
|
.eq(ReportFoodSuitability::getReportId, report.getId()));
|
|
.eq(ReportFoodSuitability::getReportId, report.getId()));
|
|
|
if (fallback.isEmpty()) {
|
|
if (fallback.isEmpty()) {
|
|
|
foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
|
- .eq(FoodRecommendIndex::getUserId, userId));
|
|
|
|
|
|
|
+ .eq(FoodRecommendIndex::getUserId, writeKey));
|
|
|
log.info("报告{}无食材推荐数据,已清除用户{}的推荐指数", report.getId(), userId);
|
|
log.info("报告{}无食材推荐数据,已清除用户{}的推荐指数", report.getId(), userId);
|
|
|
- refreshFamilyOverviews(userId);
|
|
|
|
|
|
|
+ refreshFamilyOverviews(writeKey);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
// 回退:将 report_food_suitability 映射为 Food 列表后继续处理
|
|
// 回退:将 report_food_suitability 映射为 Food 列表后继续处理
|
|
@@ -86,7 +120,7 @@ public class FoodRecommendService {
|
|
|
|
|
|
|
|
List<FoodRecommendIndex> existingIndices = foodRecommendIndexMapper.selectList(
|
|
List<FoodRecommendIndex> existingIndices = foodRecommendIndexMapper.selectList(
|
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
|
- .eq(FoodRecommendIndex::getUserId, userId)
|
|
|
|
|
|
|
+ .eq(FoodRecommendIndex::getUserId, writeKey)
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
Map<Long, FoodRecommendIndex> existingMap = existingIndices.stream()
|
|
Map<Long, FoodRecommendIndex> existingMap = existingIndices.stream()
|
|
@@ -111,7 +145,7 @@ public class FoodRecommendService {
|
|
|
foodRecommendIndexMapper.updateById(existing);
|
|
foodRecommendIndexMapper.updateById(existing);
|
|
|
|
|
|
|
|
FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
|
|
FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
|
|
|
- logEntry.setUserId(userId);
|
|
|
|
|
|
|
+ logEntry.setUserId(writeKey);
|
|
|
logEntry.setFoodId(foodId);
|
|
logEntry.setFoodId(foodId);
|
|
|
logEntry.setFoodName(foodName);
|
|
logEntry.setFoodName(foodName);
|
|
|
logEntry.setPreviousIndex(oldIndex);
|
|
logEntry.setPreviousIndex(oldIndex);
|
|
@@ -125,7 +159,7 @@ public class FoodRecommendService {
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
FoodRecommendIndex index = new FoodRecommendIndex();
|
|
FoodRecommendIndex index = new FoodRecommendIndex();
|
|
|
- index.setUserId(userId);
|
|
|
|
|
|
|
+ index.setUserId(writeKey);
|
|
|
index.setFoodId(foodId);
|
|
index.setFoodId(foodId);
|
|
|
index.setFoodName(foodName);
|
|
index.setFoodName(foodName);
|
|
|
index.setIndexScore(newIndex);
|
|
index.setIndexScore(newIndex);
|
|
@@ -135,7 +169,7 @@ public class FoodRecommendService {
|
|
|
foodRecommendIndexMapper.insert(index);
|
|
foodRecommendIndexMapper.insert(index);
|
|
|
|
|
|
|
|
FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
|
|
FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
|
|
|
- logEntry.setUserId(userId);
|
|
|
|
|
|
|
+ logEntry.setUserId(writeKey);
|
|
|
logEntry.setFoodId(foodId);
|
|
logEntry.setFoodId(foodId);
|
|
|
logEntry.setFoodName(foodName);
|
|
logEntry.setFoodName(foodName);
|
|
|
logEntry.setPreviousIndex(null);
|
|
logEntry.setPreviousIndex(null);
|
|
@@ -153,7 +187,7 @@ public class FoodRecommendService {
|
|
|
foodRecommendIndexMapper.deleteById(removed.getId());
|
|
foodRecommendIndexMapper.deleteById(removed.getId());
|
|
|
|
|
|
|
|
FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
|
|
FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
|
|
|
- logEntry.setUserId(userId);
|
|
|
|
|
|
|
+ logEntry.setUserId(writeKey);
|
|
|
logEntry.setFoodId(removed.getFoodId());
|
|
logEntry.setFoodId(removed.getFoodId());
|
|
|
logEntry.setFoodName(removed.getFoodName());
|
|
logEntry.setFoodName(removed.getFoodName());
|
|
|
logEntry.setPreviousIndex(removed.getIndexScore());
|
|
logEntry.setPreviousIndex(removed.getIndexScore());
|
|
@@ -170,7 +204,9 @@ public class FoodRecommendService {
|
|
|
try {
|
|
try {
|
|
|
List<FoodRecommendIndex> finalIndices = foodRecommendIndexMapper.selectList(
|
|
List<FoodRecommendIndex> finalIndices = foodRecommendIndexMapper.selectList(
|
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
|
- .eq(FoodRecommendIndex::getUserId, userId));
|
|
|
|
|
|
|
+ .eq(FoodRecommendIndex::getUserId, writeKey));
|
|
|
|
|
+ // snapshotFoodIndices 的 subject 语义 = 被检测成员(memberId),保持原始入参;
|
|
|
|
|
+ // finalIndices 已按 writeKey 归一化键查询,内容正确,此处不做二次归一化。
|
|
|
normalizeIndicatorPipeline.snapshotFoodIndices(userId, finalIndices);
|
|
normalizeIndicatorPipeline.snapshotFoodIndices(userId, finalIndices);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
log.warn("食材指数归一化快照失败 userId={}: {}", userId, e.getMessage());
|
|
log.warn("食材指数归一化快照失败 userId={}: {}", userId, e.getMessage());
|
|
@@ -178,7 +214,7 @@ public class FoodRecommendService {
|
|
|
|
|
|
|
|
// 家庭食材总览重算(T28):最新指数已全量覆盖写入,刷新所属家庭的持久化最小值总览。
|
|
// 家庭食材总览重算(T28):最新指数已全量覆盖写入,刷新所属家庭的持久化最小值总览。
|
|
|
// try/catch + 只 warn,绝不因总览重算失败让报告确认入库主流程回滚(与归一化管道处理方式一致)
|
|
// try/catch + 只 warn,绝不因总览重算失败让报告确认入库主流程回滚(与归一化管道处理方式一致)
|
|
|
- refreshFamilyOverviews(userId);
|
|
|
|
|
|
|
+ refreshFamilyOverviews(writeKey);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -212,9 +248,11 @@ public class FoodRecommendService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public List<FoodRecommendIndex> getIndexByUser(Long userId) {
|
|
public List<FoodRecommendIndex> getIndexByUser(Long userId) {
|
|
|
|
|
+ // T28 方向1:读侧归一化,把可能传入的 memberId 解析为 food_recommend_idx 统一键(users.id)
|
|
|
|
|
+ Long resolved = resolveFoodIndexUserId(userId);
|
|
|
return foodRecommendIndexMapper.selectList(
|
|
return foodRecommendIndexMapper.selectList(
|
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
|
- .eq(FoodRecommendIndex::getUserId, userId)
|
|
|
|
|
|
|
+ .eq(FoodRecommendIndex::getUserId, resolved)
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -285,9 +323,11 @@ public class FoodRecommendService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public List<FoodRecommendIdxLog> getIndexHistory(Long userId, Long foodId) {
|
|
public List<FoodRecommendIdxLog> getIndexHistory(Long userId, Long foodId) {
|
|
|
|
|
+ // T28 方向1:读侧归一化,把可能传入的 memberId 解析为统一键(users.id),日志键与 food_recommend_idx 一致
|
|
|
|
|
+ Long resolved = resolveFoodIndexUserId(userId);
|
|
|
return foodRecommendIdxLogMapper.selectList(
|
|
return foodRecommendIdxLogMapper.selectList(
|
|
|
new LambdaQueryWrapper<FoodRecommendIdxLog>()
|
|
new LambdaQueryWrapper<FoodRecommendIdxLog>()
|
|
|
- .eq(FoodRecommendIdxLog::getUserId, userId)
|
|
|
|
|
|
|
+ .eq(FoodRecommendIdxLog::getUserId, resolved)
|
|
|
.eq(FoodRecommendIdxLog::getFoodId, foodId)
|
|
.eq(FoodRecommendIdxLog::getFoodId, foodId)
|
|
|
.orderByDesc(FoodRecommendIdxLog::getChangedAt)
|
|
.orderByDesc(FoodRecommendIdxLog::getChangedAt)
|
|
|
);
|
|
);
|
|
@@ -319,9 +359,11 @@ public class FoodRecommendService {
|
|
|
* @param queryUserId 查询目标,传memberId则按成员查,否则退回userId
|
|
* @param queryUserId 查询目标,传memberId则按成员查,否则退回userId
|
|
|
*/
|
|
*/
|
|
|
public List<FoodRecommendIndex> getCautionFoods(Long queryUserId) {
|
|
public List<FoodRecommendIndex> getCautionFoods(Long queryUserId) {
|
|
|
|
|
+ // T28 方向1:读侧归一化,把可能传入的 memberId 解析为统一键(users.id)
|
|
|
|
|
+ Long resolved = resolveFoodIndexUserId(queryUserId);
|
|
|
return foodRecommendIndexMapper.selectList(
|
|
return foodRecommendIndexMapper.selectList(
|
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
new LambdaQueryWrapper<FoodRecommendIndex>()
|
|
|
- .eq(FoodRecommendIndex::getUserId, queryUserId)
|
|
|
|
|
|
|
+ .eq(FoodRecommendIndex::getUserId, resolved)
|
|
|
.lt(FoodRecommendIndex::getIndexScore, 0)
|
|
.lt(FoodRecommendIndex::getIndexScore, 0)
|
|
|
.orderByAsc(FoodRecommendIndex::getIndexScore)
|
|
.orderByAsc(FoodRecommendIndex::getIndexScore)
|
|
|
);
|
|
);
|
|
@@ -376,11 +418,13 @@ public class FoodRecommendService {
|
|
|
* 每个元素: { foodId, foodName, currentScore, trend: [reportDate, score], changeDir: "up"|"down"|"flat" }
|
|
* 每个元素: { foodId, foodName, currentScore, trend: [reportDate, score], changeDir: "up"|"down"|"flat" }
|
|
|
*/
|
|
*/
|
|
|
public List<Map<String, Object>> getTrend(Long userId, List<Long> foodIds) {
|
|
public List<Map<String, Object>> getTrend(Long userId, List<Long> foodIds) {
|
|
|
|
|
+ // T28 方向1:读侧归一化,把可能传入的 memberId 解析为统一键(users.id),日志键与 food_recommend_idx 一致
|
|
|
|
|
+ Long resolved = resolveFoodIndexUserId(userId);
|
|
|
List<FoodRecommendIdxLog> logs;
|
|
List<FoodRecommendIdxLog> logs;
|
|
|
if (foodIds != null && !foodIds.isEmpty()) {
|
|
if (foodIds != null && !foodIds.isEmpty()) {
|
|
|
logs = foodRecommendIdxLogMapper.selectList(
|
|
logs = foodRecommendIdxLogMapper.selectList(
|
|
|
new LambdaQueryWrapper<FoodRecommendIdxLog>()
|
|
new LambdaQueryWrapper<FoodRecommendIdxLog>()
|
|
|
- .eq(FoodRecommendIdxLog::getUserId, userId)
|
|
|
|
|
|
|
+ .eq(FoodRecommendIdxLog::getUserId, resolved)
|
|
|
.in(FoodRecommendIdxLog::getFoodId, foodIds)
|
|
.in(FoodRecommendIdxLog::getFoodId, foodIds)
|
|
|
.orderByDesc(FoodRecommendIdxLog::getChangedAt)
|
|
.orderByDesc(FoodRecommendIdxLog::getChangedAt)
|
|
|
.last("LIMIT 60")
|
|
.last("LIMIT 60")
|
|
@@ -388,7 +432,7 @@ public class FoodRecommendService {
|
|
|
} else {
|
|
} else {
|
|
|
logs = foodRecommendIdxLogMapper.selectList(
|
|
logs = foodRecommendIdxLogMapper.selectList(
|
|
|
new LambdaQueryWrapper<FoodRecommendIdxLog>()
|
|
new LambdaQueryWrapper<FoodRecommendIdxLog>()
|
|
|
- .eq(FoodRecommendIdxLog::getUserId, userId)
|
|
|
|
|
|
|
+ .eq(FoodRecommendIdxLog::getUserId, resolved)
|
|
|
.orderByDesc(FoodRecommendIdxLog::getChangedAt)
|
|
.orderByDesc(FoodRecommendIdxLog::getChangedAt)
|
|
|
.last("LIMIT 120")
|
|
.last("LIMIT 120")
|
|
|
);
|
|
);
|