|
|
@@ -35,7 +35,9 @@ import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
+import java.math.BigDecimal;
|
|
|
import java.util.Map;
|
|
|
+import com.etotem.cfc.entity.MemberIndicatorCurrent;
|
|
|
import java.util.Set;
|
|
|
import com.etotem.cfc.util.SortUtil;
|
|
|
|
|
|
@@ -112,6 +114,10 @@ public class HealthReportService {
|
|
|
@Resource
|
|
|
private FoodMapper foodMapper;
|
|
|
|
|
|
+ @Resource
|
|
|
+ @org.springframework.context.annotation.Lazy
|
|
|
+ private MemberIndicatorCurrentService memberIndicatorCurrentService;
|
|
|
+
|
|
|
@Resource
|
|
|
private com.etotem.cfc.service.NormalizeIndicatorPipeline normalizeIndicatorPipeline;
|
|
|
|
|
|
@@ -2149,7 +2155,9 @@ public class HealthReportService {
|
|
|
List<Map<String, Object>> result = new java.util.ArrayList<>();
|
|
|
for (IndicatorDefinition def : defs) {
|
|
|
LambdaQueryWrapper<IndicatorValue> w = new LambdaQueryWrapper<>();
|
|
|
- w.eq(IndicatorValue::getSourceId, memberId)
|
|
|
+ // 口径:写入端 source_id=报告ID(保留每报告快照),subject_id=成员ID;
|
|
|
+ // 读取端按 subject_id=memberId 查最新观测值
|
|
|
+ w.eq(IndicatorValue::getSubjectId, memberId)
|
|
|
.eq(IndicatorValue::getDefinitionId, def.getId())
|
|
|
.orderByDesc(IndicatorValue::getRecordedAt)
|
|
|
.last("LIMIT 1");
|
|
|
@@ -2213,4 +2221,129 @@ public class HealthReportService {
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
-}
|
|
|
+ /**
|
|
|
+ * 刷新成员指标当前值 diff(member_indicator_current)
|
|
|
+ * 逻辑:
|
|
|
+ * 1. 查该成员最近 2 份按 report_date desc 的确认报告(status=active)
|
|
|
+ * 2. 取第 1 份(最新)与第 2 份(次新)中同名指标
|
|
|
+ * 3. 通过 IndicatorDefinitionMapper 拿 domain/category
|
|
|
+ * 4. 用 numeric 做 delta/trend 计算(字符串 fallback)
|
|
|
+ * 5. upsert 写入 member_indicator_current
|
|
|
+ * 幂等:同一 member_id+definition_id 用 upsert 覆盖
|
|
|
+ */
|
|
|
+ /**
|
|
|
+ * 回填重载:按成员自动找最近 2 份已确认报告并刷新 member_indicator_current
|
|
|
+ * 供 admin 端点 / 启动任务使用;无 latestReportId 也可调用(自动解析)
|
|
|
+ */
|
|
|
+ public void rebuildForMember(Long subjectId) {
|
|
|
+ if (subjectId == null) return;
|
|
|
+ List<HealthReport> recent = healthReportMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<HealthReport>()
|
|
|
+ .eq(HealthReport::getSubjectId, subjectId)
|
|
|
+ .eq(HealthReport::getStatus, "active")
|
|
|
+ .orderByDesc(HealthReport::getReportDate)
|
|
|
+ .orderByDesc(HealthReport::getCreatedAt)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (recent.isEmpty()) {
|
|
|
+ log.info("rebuildForMember: 成员{}无已确认报告,跳过", subjectId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ refreshMemberIndicatorCurrent(subjectId, recent.get(0).getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ public void refreshMemberIndicatorCurrent(Long subjectId, Long latestReportId) {
|
|
|
+ if (subjectId == null || latestReportId == null) return;
|
|
|
+ try {
|
|
|
+ // 取该成员最近 2 份报告 (按 report_date desc, 失败则 fallback 到 created_at)
|
|
|
+ List<HealthReport> recent = healthReportMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<HealthReport>()
|
|
|
+ .eq(HealthReport::getSubjectId, subjectId)
|
|
|
+ .eq(HealthReport::getStatus, "active")
|
|
|
+ .orderByDesc(HealthReport::getReportDate)
|
|
|
+ .orderByDesc(HealthReport::getCreatedAt)
|
|
|
+ .last("LIMIT 2"));
|
|
|
+ if (recent.isEmpty()) return;
|
|
|
+ HealthReport latest = recent.get(0);
|
|
|
+ HealthReport previous = recent.size() > 1 ? recent.get(1) : null;
|
|
|
+
|
|
|
+ // 最新报告的指标
|
|
|
+ List<HealthIndicator> latestInds = getReportIndicators(latest.getId());
|
|
|
+ if (latestInds.isEmpty()) return;
|
|
|
+
|
|
|
+ // 次新指标的 name→HealthIndicator 映射
|
|
|
+ java.util.Map<String, HealthIndicator> prevMap = new java.util.HashMap<>();
|
|
|
+ if (previous != null) {
|
|
|
+ for (HealthIndicator pi : getReportIndicators(previous.getId())) {
|
|
|
+ if (pi.getIndicatorName() != null) {
|
|
|
+ prevMap.put(pi.getIndicatorName().trim(), pi);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ int written = 0;
|
|
|
+ for (HealthIndicator li : latestInds) {
|
|
|
+ if (li.getIndicatorName() == null) continue;
|
|
|
+ String rawName = li.getIndicatorName().trim();
|
|
|
+
|
|
|
+ // 通过 mapper 找 definition(domain/category 来源)
|
|
|
+ com.etotem.cfc.entity.IndicatorDefinition def = indicatorDefinitionMapper.selectOne(
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<
|
|
|
+ com.etotem.cfc.entity.IndicatorDefinition>()
|
|
|
+ .eq(com.etotem.cfc.entity.IndicatorDefinition::getName, rawName)
|
|
|
+ .eq(com.etotem.cfc.entity.IndicatorDefinition::getStatus, 1)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (def == null) continue;
|
|
|
+
|
|
|
+ MemberIndicatorCurrent item = new MemberIndicatorCurrent();
|
|
|
+ item.setMemberId(subjectId);
|
|
|
+ item.setDefinitionId(def.getId());
|
|
|
+ item.setDomain(def.getDomain());
|
|
|
+ item.setCategory(def.getCategory());
|
|
|
+ item.setName(rawName);
|
|
|
+ item.setValue(li.getIndicatorValue());
|
|
|
+ item.setLatestReportId(latest.getId());
|
|
|
+ item.setReportDate(latest.getReportDate() != null
|
|
|
+ ? new java.sql.Date(latest.getReportDate().getTime()) : null);
|
|
|
+
|
|
|
+ // previous 指标(同名)
|
|
|
+ HealthIndicator pi = prevMap.get(rawName);
|
|
|
+ if (pi != null) {
|
|
|
+ item.setPreviousValue(pi.getIndicatorValue());
|
|
|
+ item.setPreviousReportId(previous.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // numeric 计算
|
|
|
+ BigDecimal currentNum = parseNumericString(li.getIndicatorValue());
|
|
|
+ BigDecimal previousNum = pi != null ? parseNumericString(pi.getIndicatorValue()) : null;
|
|
|
+ item.setNumericValue(currentNum);
|
|
|
+ item.setPreviousNumeric(previousNum);
|
|
|
+ if (currentNum != null && previousNum != null) {
|
|
|
+ item.setDelta(currentNum.subtract(previousNum));
|
|
|
+ int cmp = currentNum.compareTo(previousNum);
|
|
|
+ item.setTrend(cmp > 0 ? "up" : (cmp < 0 ? "down" : "flat"));
|
|
|
+ } else {
|
|
|
+ item.setTrend(pi != null ? "na" : "new");
|
|
|
+ }
|
|
|
+ item.setRecordedAt(new Date());
|
|
|
+ memberIndicatorCurrentService.upsert(item);
|
|
|
+ written++;
|
|
|
+ }
|
|
|
+ log.info("迁移378 补写完成: member={} latestReport={} prevReport={} written={}",
|
|
|
+ subjectId, latest.getId(),
|
|
|
+ previous != null ? previous.getId() : "none", written);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移378 补写失败 member={} report={}: {}", subjectId, latestReportId, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 安全解析指标值为 BigDecimal(失败返回 null,避免整块报告因单条脏数据卡死)*/
|
|
|
+ private BigDecimal parseNumericString(String s) {
|
|
|
+ if (s == null || s.trim().isEmpty()) return null;
|
|
|
+ try {
|
|
|
+ return new BigDecimal(s.trim().replaceAll("[^0-9.eE+-]", ""));
|
|
|
+ } catch (Exception ex) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|