|
|
@@ -0,0 +1,130 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.dto.ParsedReportPayload;
|
|
|
+import com.etotem.cfc.entity.FoodRecommendIndex;
|
|
|
+import com.etotem.cfc.entity.HealthReport;
|
|
|
+import com.etotem.cfc.entity.IndicatorValue;
|
|
|
+import com.etotem.cfc.mapper.IndicatorValueMapper;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.sql.Date;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 归一化管道 —— 统一指标层「单一写者」。
|
|
|
+ * 报告入库/编辑流程照旧写 health_indicators(不动现有消费者),
|
|
|
+ * 本管道在同流程/同事务内将 指标+菌属+食材 幂等写入 indicator_values。
|
|
|
+ * 唯一键 (source_type, source_id, definition_id) 保证重解析只覆盖更新。
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class NormalizeIndicatorPipeline {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(NormalizeIndicatorPipeline.class);
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private IndicatorMappingService mappingService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private IndicatorValueMapper valueMapper;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 报告指标 + 菌属归一化写入。
|
|
|
+ * 调用点:insertReport()(subjectId 块后)/ updateReportFromPayload()(步骤 5 后)。
|
|
|
+ * 无 subject 的报告不入时序(无法归属观测对象)。
|
|
|
+ *
|
|
|
+ * @param report 已入库/已更新的报告(reportDate 须为真实日期或 null)
|
|
|
+ * @param payload 解析载荷(gutFlora 从 payload 读;indicators 从 payload 读,与 health_indicators 同源)
|
|
|
+ */
|
|
|
+ public void processReport(HealthReport report, ParsedReportPayload.Payload payload) {
|
|
|
+ if (report == null || report.getSubjectId() == null || report.getId() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Long subjectId = report.getSubjectId();
|
|
|
+ Long familyId = report.getFamilyId();
|
|
|
+ Long reportId = report.getId();
|
|
|
+ String sourceType = report.getReportType();
|
|
|
+ Date reportDate = report.getReportDate() != null
|
|
|
+ ? new Date(report.getReportDate().getTime()) : null;
|
|
|
+
|
|
|
+ int written = 0;
|
|
|
+ if (payload != null && payload.getIndicators() != null) {
|
|
|
+ for (ParsedReportPayload.Indicator ind : payload.getIndicators()) {
|
|
|
+ if (ind.getIndicatorName() == null || ind.getIndicatorName().trim().isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Long defId = mappingService.resolve(sourceType, ind.getIndicatorName().trim());
|
|
|
+ if (defId == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ IndicatorValue v = baseValue(sourceType, reportId, defId, subjectId, familyId, reportDate);
|
|
|
+ v.setValue(ind.getIndicatorValue());
|
|
|
+ v.setNumericValue(mappingService.parseNumericValue(ind.getIndicatorValue()));
|
|
|
+ v.setUnit(ind.getUnit());
|
|
|
+ v.setStatus(ind.getStatus());
|
|
|
+ valueMapper.upsertMapping(v);
|
|
|
+ written++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (payload != null && payload.getGutFlora() != null) {
|
|
|
+ for (ParsedReportPayload.Flora flora : payload.getGutFlora()) {
|
|
|
+ if (flora.getBacteriaName() == null || flora.getBacteriaName().trim().isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Long defId = mappingService.resolve(sourceType, flora.getBacteriaName().trim());
|
|
|
+ if (defId == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ IndicatorValue v = baseValue(sourceType, reportId, defId, subjectId, familyId, reportDate);
|
|
|
+ v.setValue(flora.getBacteriaValue());
|
|
|
+ v.setNumericValue(mappingService.parseNumericValue(flora.getBacteriaValue()));
|
|
|
+ v.setUnit("%");
|
|
|
+ v.setStatus(flora.getStatus());
|
|
|
+ valueMapper.upsertMapping(v);
|
|
|
+ written++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("归一化管道 reportId={}, subjectId={}, type={},写入 {} 条观测", reportId, subjectId, sourceType, written);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 食材推荐指数全量快照(钩子 3:FoodRecommendService.recalculateForUser 尾部调用)。
|
|
|
+ * source_type=food_recommend,source_id=food_recommend_idx.id,幂等覆盖。
|
|
|
+ */
|
|
|
+ public void snapshotFoodIndices(Long userId, List<FoodRecommendIndex> indices) {
|
|
|
+ if (userId == null || indices == null || indices.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (FoodRecommendIndex idx : indices) {
|
|
|
+ if (idx.getFoodName() == null || idx.getFoodName().trim().isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Long defId = mappingService.resolve("food_recommend", idx.getFoodName().trim());
|
|
|
+ if (defId == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ IndicatorValue v = baseValue("food_recommend", idx.getId(), defId, userId, null, null);
|
|
|
+ v.setValue(idx.getIndexScore() != null ? idx.getIndexScore().toString() : null);
|
|
|
+ v.setNumericValue(idx.getIndexScore() != null
|
|
|
+ ? BigDecimal.valueOf(idx.getIndexScore()) : null);
|
|
|
+ v.setReportId(idx.getHealthReportId());
|
|
|
+ valueMapper.upsertMapping(v);
|
|
|
+ }
|
|
|
+ log.info("食材指数快照 userId={}, 共 {} 条", userId, indices.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ private IndicatorValue baseValue(String sourceType, Long sourceId, Long defId,
|
|
|
+ Long subjectId, Long familyId, Date reportDate) {
|
|
|
+ IndicatorValue v = new IndicatorValue();
|
|
|
+ v.setSourceType(sourceType);
|
|
|
+ v.setSourceId(sourceId);
|
|
|
+ v.setDefinitionId(defId);
|
|
|
+ v.setSubjectId(subjectId);
|
|
|
+ v.setFamilyId(familyId);
|
|
|
+ v.setReportDate(reportDate);
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+}
|