|
@@ -34,8 +34,11 @@ import java.util.Comparator;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.HashSet;
|
|
import java.util.HashSet;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.math.BigDecimal;
|
|
import java.math.BigDecimal;
|
|
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
import com.etotem.cfc.entity.MemberIndicatorCurrent;
|
|
import com.etotem.cfc.entity.MemberIndicatorCurrent;
|
|
|
import java.util.Set;
|
|
import java.util.Set;
|
|
@@ -121,6 +124,12 @@ public class HealthReportService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private com.etotem.cfc.service.NormalizeIndicatorPipeline normalizeIndicatorPipeline;
|
|
private com.etotem.cfc.service.NormalizeIndicatorPipeline normalizeIndicatorPipeline;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private com.etotem.cfc.service.IndicatorMappingService mappingService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private com.etotem.cfc.service.IndicatorChangeService changeService;
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private ReportDateExtractor reportDateExtractor;
|
|
private ReportDateExtractor reportDateExtractor;
|
|
|
|
|
|
|
@@ -393,6 +402,297 @@ public class HealthReportService {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取报告指标(含上一期对照)—— §4.1 新增接口 /api/health/report/indicators。
|
|
|
|
|
+ * 严禁 N+1:批量调 IndicatorChangeService.batchResolve 一次性解析全部 previous,
|
|
|
|
|
+ * 再组装 summary 与 items。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param reportId 报告 ID
|
|
|
|
|
+ * @param withPrevious true=计算 previous/change;false=仅单期(编辑模式等场景用)
|
|
|
|
|
+ * @return 含 report 信息、summary 统计、items(每项含 previous/change)
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> getReportIndicatorsWithPrevious(Long reportId, Boolean withPrevious) {
|
|
|
|
|
+ HealthReport report = healthReportMapper.selectById(reportId);
|
|
|
|
|
+ if (report == null) {
|
|
|
|
|
+ Map<String, Object> err = new LinkedHashMap<>();
|
|
|
|
|
+ err.put("code", 500);
|
|
|
|
|
+ err.put("message", "报告不存在");
|
|
|
|
|
+ return err;
|
|
|
|
|
+ }
|
|
|
|
|
+ return getReportIndicatorsWithPrevious(report, null, withPrevious);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取报告指标(含上一期对照)—— 复用已加载的 report / indicators,避免重复查询。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>SQL 预算(验收标准第 6 条:≤ 5 次)</b>:
|
|
|
|
|
+ * <ol>
|
|
|
|
|
+ * <li>health_indicators(本报告指标行)</li>
|
|
|
|
|
+ * <li>indicator_mapping_rules(批量解析 raw_name → definition_id,1 次)</li>
|
|
|
|
|
+ * <li>indicator_definitions(selectBatchIds,1 次)</li>
|
|
|
|
|
+ * <li>health_reports(olderReportCount,1 次)</li>
|
|
|
|
|
+ * <li>indicator_values(batchResolve 上一期,1 次)</li>
|
|
|
|
|
+ * </ol>
|
|
|
|
|
+ * 指标数从 1 条涨到 42 条时,查询数恒定为 5。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param report 已加载的报告(控制器做权限校验时已查过,直接复用)
|
|
|
|
|
+ * @param indicators 已加载的本报告指标行;null 时内部再查一次
|
|
|
|
|
+ * @param withPrevious true=计算 previous/change
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> getReportIndicatorsWithPrevious(HealthReport report,
|
|
|
|
|
+ List<HealthIndicator> indicators,
|
|
|
|
|
+ Boolean withPrevious) {
|
|
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
|
|
+ if (report == null) {
|
|
|
|
|
+ data.put("code", 500);
|
|
|
|
|
+ data.put("message", "报告不存在");
|
|
|
|
|
+ return data;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data.put("reportId", report.getId());
|
|
|
|
|
+ data.put("reportType", report.getReportType());
|
|
|
|
|
+ data.put("subjectId", report.getSubjectId());
|
|
|
|
|
+ data.put("reportDate", report.getReportDate() != null
|
|
|
|
|
+ ? new SimpleDateFormat("yyyy-MM-dd").format(report.getReportDate()) : null);
|
|
|
|
|
+ data.put("dateSource", report.getReportDate() == null ? "unknown" : "real");
|
|
|
|
|
+
|
|
|
|
|
+ if (indicators == null) {
|
|
|
|
|
+ indicators = getReportIndicators(report.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ int totalCount = indicators.size();
|
|
|
|
|
+ int abnormalCount = 0;
|
|
|
|
|
+ int changedCount = 0;
|
|
|
|
|
+ int comparableCount = 0;
|
|
|
|
|
+ int firstRecordCount = 0;
|
|
|
|
|
+ int pendingCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (!indicators.isEmpty() && Boolean.TRUE.equals(withPrevious)) {
|
|
|
|
|
+ // ---- 1) 按 (subjectId, sourceType, reportDate) 批量取上一期 ----
|
|
|
|
|
+ Long subjectId = report.getSubjectId();
|
|
|
|
|
+ String sourceType = report.getReportType();
|
|
|
|
|
+ java.sql.Date currentReportDate = report.getReportDate() != null
|
|
|
|
|
+ ? new java.sql.Date(report.getReportDate().getTime()) : null;
|
|
|
|
|
+
|
|
|
|
|
+ // olderReportCount:同 subject+sourceType 且 report_date < current 的报告份数
|
|
|
|
|
+ long olderReportCount = 0;
|
|
|
|
|
+ if (subjectId != null && currentReportDate != null) {
|
|
|
|
|
+ olderReportCount = healthReportMapper.selectList(new LambdaQueryWrapper<HealthReport>()
|
|
|
|
|
+ .eq(HealthReport::getSubjectId, subjectId)
|
|
|
|
|
+ .eq(HealthReport::getStatus, "active")
|
|
|
|
|
+ .isNotNull(HealthReport::getReportDate)
|
|
|
|
|
+ .eq(HealthReport::getReportType, sourceType)
|
|
|
|
|
+ .lt(HealthReport::getReportDate, currentReportDate)
|
|
|
|
|
+ .select(HealthReport::getReportDate))
|
|
|
|
|
+ .size();
|
|
|
|
|
+ }
|
|
|
|
|
+ data.put("olderReportCount", olderReportCount);
|
|
|
|
|
+
|
|
|
|
|
+ // 2) 解析 payload 获取指标 definitionId 映射(同 NormalizeIndicatorPipeline 逻辑)
|
|
|
|
|
+ // 批量解析:1 次查规则表 + 内存 exact/fuzzy,避免逐指标查表(N+1)
|
|
|
|
|
+ Map<String, Long> nameToDefId = new HashMap<>();
|
|
|
|
|
+ List<String> rawNames = new ArrayList<>();
|
|
|
|
|
+ ParsedReportPayload.Payload payload = null;
|
|
|
|
|
+ if (report.getPayloadJson() != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ payload = objectMapper.readValue(report.getPayloadJson(), ParsedReportPayload.Payload.class);
|
|
|
|
|
+ if (payload != null && payload.getIndicators() != null) {
|
|
|
|
|
+ for (ParsedReportPayload.Indicator ind : payload.getIndicators()) {
|
|
|
|
|
+ if (ind.getIndicatorName() != null) {
|
|
|
|
|
+ rawNames.add(ind.getIndicatorName().trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("解析 payload 失败 reportId={}: {}", report.getId(), e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (payload != null && payload.getGutFlora() != null) {
|
|
|
|
|
+ for (ParsedReportPayload.Flora flora : payload.getGutFlora()) {
|
|
|
|
|
+ if (flora.getBacteriaName() != null) {
|
|
|
|
|
+ rawNames.add(flora.getBacteriaName().trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 报告行上的指标名也要参与解析(payload 可能缺失,指标行仍存在)
|
|
|
|
|
+ for (HealthIndicator hi : indicators) {
|
|
|
|
|
+ if (hi.getIndicatorName() != null) {
|
|
|
|
|
+ rawNames.add(hi.getIndicatorName().trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<String, IndicatorMappingService.ResolvedMapping> resolvedMap = mappingService.resolveBatch(sourceType, rawNames);
|
|
|
|
|
+ for (IndicatorMappingService.ResolvedMapping rm : resolvedMap.values()) {
|
|
|
|
|
+ if (rm.getDefinitionId() != null) {
|
|
|
|
|
+ nameToDefId.put(rm.getRawName(), rm.getDefinitionId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ List<Long> defIds = new ArrayList<>(new LinkedHashSet<>(nameToDefId.values()));
|
|
|
|
|
+
|
|
|
|
|
+ // 2b) 已在 resolveBatch 的联合查询中随规则一并带出 definition 字段,无需再 selectBatchIds(省 1 次查询)
|
|
|
|
|
+ Map<Long, IndicatorDefinition> defMap = new HashMap<>();
|
|
|
|
|
+ for (IndicatorMappingService.ResolvedMapping rm : resolvedMap.values()) {
|
|
|
|
|
+ if (rm.getDefId() != null) {
|
|
|
|
|
+ IndicatorDefinition d = new IndicatorDefinition();
|
|
|
|
|
+ d.setId(rm.getDefId());
|
|
|
|
|
+ d.setName(rm.getDefName());
|
|
|
|
|
+ d.setGoodDirection(rm.getDefGoodDirection());
|
|
|
|
|
+ d.setValueType(rm.getDefValueType());
|
|
|
|
|
+ d.setUnit(rm.getDefUnit());
|
|
|
|
|
+ d.setRefRange(rm.getDefRefRange());
|
|
|
|
|
+ d.setStatus(rm.getDefStatus());
|
|
|
|
|
+ defMap.put(rm.getDefId(), d);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3) 批量解析上一期
|
|
|
|
|
+ Map<Long, IndicatorValue> previousMap = Collections.emptyMap();
|
|
|
|
|
+ if (subjectId != null && currentReportDate != null && !defIds.isEmpty()) {
|
|
|
|
|
+ previousMap = changeService.batchResolve(subjectId, sourceType, defIds, currentReportDate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4) 组装 items
|
|
|
|
|
+ List<Map<String, Object>> items = new ArrayList<>();
|
|
|
|
|
+ for (HealthIndicator hi : indicators) {
|
|
|
|
|
+ String name = hi.getIndicatorName();
|
|
|
|
|
+ Long defId = nameToDefId.get(name != null ? name.trim() : "");
|
|
|
|
|
+ IndicatorDefinition def = defId != null ? defMap.get(defId) : null;
|
|
|
|
|
+
|
|
|
|
|
+ // Q17:status=0 的 definition 剔除(不降级为"指标"+id)
|
|
|
|
|
+ if (def != null && def.getStatus() != null && def.getStatus() != 1) {
|
|
|
|
|
+ def = null;
|
|
|
|
|
+ defId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 组装 item(未命中字典的指标 definitionId=null,仍要出现在 items 中)
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("indicatorId", hi.getId());
|
|
|
|
|
+ item.put("definitionId", defId);
|
|
|
|
|
+ item.put("category", hi.getCategory());
|
|
|
|
|
+ item.put("indicatorName", hi.getIndicatorName());
|
|
|
|
|
+ item.put("indicatorValue", hi.getIndicatorValue());
|
|
|
|
|
+ item.put("numericValue", hi.getIndicatorValue() != null ? parseNumeric(hi.getIndicatorValue()) : null);
|
|
|
|
|
+ item.put("unit", hi.getUnit());
|
|
|
|
|
+ item.put("refRange", hi.getRefRange());
|
|
|
|
|
+ item.put("status", hi.getStatus());
|
|
|
|
|
+ item.put("goodDirection", def != null ? def.getGoodDirection() : null);
|
|
|
|
|
+ item.put("valueType", def != null ? def.getValueType() : null);
|
|
|
|
|
+ item.put("pointsCount", 1); // 单报告视角下每指标只有 1 期,时间轴视图另有计算
|
|
|
|
|
+
|
|
|
|
|
+ if (def == null) {
|
|
|
|
|
+ // 未命中字典:previous/change 恒为 null(前端按"无对比"渲染)
|
|
|
|
|
+ item.put("previous", null);
|
|
|
|
|
+ item.put("change", null);
|
|
|
|
|
+ items.add(item);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // previous
|
|
|
|
|
+ IndicatorValue prev = previousMap.get(defId);
|
|
|
|
|
+ // change
|
|
|
|
|
+ IndicatorChangeService.ChangeResult cr;
|
|
|
|
|
+ if (prev == null) {
|
|
|
|
|
+ cr = changeService.computeChange(def, null, buildValueFromHi(hi), olderReportCount > 0 ? (int) olderReportCount : 0);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ IndicatorValue after = buildValueFromHi(hi);
|
|
|
|
|
+ after.setSourceType(sourceType);
|
|
|
|
|
+ cr = changeService.computeChange(def, prev, after);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 统计
|
|
|
|
|
+ if (hi.getStatus() != null && !"正常".equals(hi.getStatus())) {
|
|
|
|
|
+ abnormalCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cr.getTrendCode().equals(IndicatorChangeService.TREND_IMPROVED)
|
|
|
|
|
+ || cr.getTrendCode().equals(IndicatorChangeService.TREND_WORSENED)) {
|
|
|
|
|
+ changedCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cr.isComparable()) {
|
|
|
|
|
+ comparableCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cr.getTrendCode().equals(IndicatorChangeService.TREND_FIRST_RECORD)) {
|
|
|
|
|
+ firstRecordCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cr.getTrendCode().equals(IndicatorChangeService.TREND_PENDING_BACKFILL)) {
|
|
|
|
|
+ pendingCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // previous
|
|
|
|
|
+ if (prev != null) {
|
|
|
|
|
+ Map<String, Object> prevMap = new LinkedHashMap<>();
|
|
|
|
|
+ prevMap.put("reportId", prev.getReportId());
|
|
|
|
|
+ prevMap.put("reportDate", prev.getReportDate() != null ? new SimpleDateFormat("yyyy-MM-dd").format(prev.getReportDate()) : null);
|
|
|
|
|
+ prevMap.put("dateSource", prev.getDateSource());
|
|
|
|
|
+ prevMap.put("value", prev.getValue());
|
|
|
|
|
+ prevMap.put("numericValue", prev.getNumericValue());
|
|
|
|
|
+ prevMap.put("unit", prev.getUnit());
|
|
|
|
|
+ prevMap.put("status", prev.getStatus());
|
|
|
|
|
+ item.put("previous", prevMap);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ item.put("previous", null);
|
|
|
|
|
+ }
|
|
|
|
|
+ item.put("change", cr.toMap());
|
|
|
|
|
+ items.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // summary
|
|
|
|
|
+ Map<String, Object> summary = new LinkedHashMap<>();
|
|
|
|
|
+ summary.put("totalCount", totalCount);
|
|
|
|
|
+ summary.put("abnormalCount", abnormalCount);
|
|
|
|
|
+ summary.put("abnormalDelta", 0); // 单报告视角无法直接算,需拿上一期 report 的异常数对比(预留字段)
|
|
|
|
|
+ summary.put("changedCount", changedCount);
|
|
|
|
|
+ summary.put("comparableCount", comparableCount);
|
|
|
|
|
+ summary.put("firstRecordCount", firstRecordCount);
|
|
|
|
|
+ summary.put("pendingCount", pendingCount);
|
|
|
|
|
+ summary.put("dataIncomplete", pendingCount > 0);
|
|
|
|
|
+ data.put("summary", summary);
|
|
|
|
|
+ data.put("items", items);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // withPrevious=false:仅单期
|
|
|
|
|
+ List<Map<String, Object>> items = new ArrayList<>();
|
|
|
|
|
+ for (HealthIndicator hi : indicators) {
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("indicatorId", hi.getId());
|
|
|
|
|
+ item.put("category", hi.getCategory());
|
|
|
|
|
+ item.put("indicatorName", hi.getIndicatorName());
|
|
|
|
|
+ item.put("indicatorValue", hi.getIndicatorValue());
|
|
|
|
|
+ item.put("unit", hi.getUnit());
|
|
|
|
|
+ item.put("refRange", hi.getRefRange());
|
|
|
|
|
+ item.put("status", hi.getStatus());
|
|
|
|
|
+ items.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ data.put("items", items);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return data;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private IndicatorValue buildValueFromHi(HealthIndicator hi) {
|
|
|
|
|
+ IndicatorValue v = new IndicatorValue();
|
|
|
|
|
+ v.setValue(hi.getIndicatorValue());
|
|
|
|
|
+ v.setNumericValue(parseNumeric(hi.getIndicatorValue()));
|
|
|
|
|
+ v.setUnit(hi.getUnit());
|
|
|
|
|
+ v.setStatus(hi.getStatus());
|
|
|
|
|
+ return v;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 把指标原始文本值解析为 BigDecimal(去掉 %/~/>/< 与空格后尝试转换)。
|
|
|
|
|
+ * 与 IndicatorMappingService.parseNumericValue 语义一致,本处复用以避免重复正则。
|
|
|
|
|
+ */
|
|
|
|
|
+ private BigDecimal parseNumeric(String raw) {
|
|
|
|
|
+ if (raw == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String cleaned = raw.replaceAll("[^0-9.-]", "");
|
|
|
|
|
+ if (cleaned.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new BigDecimal(cleaned);
|
|
|
|
|
+ } catch (NumberFormatException ignore) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 计算孩子身体维度得分 (0-100)
|
|
* 计算孩子身体维度得分 (0-100)
|
|
|
* <p>
|
|
* <p>
|