Просмотр исходного кода

feat(attribution): S-cfc-119 guhe.p3.lowSideAggregate T15 跨案例聚合落地(契约 4 RED -> 6 GREEN)

按2026-11-08浠艾福平台程序修改计划 §三-P3/T15:低侧口径并入既有 P3 聚合口径。
- 新增 AttributionCaseAggregateService:按「条件致病菌低侧判读」口径聚合跨案例、聚类「同策略」(aggregateLowSideStrategy),可检索「同条件致病菌低侧」应对方案;低侧机制恒 associationOnly=true(关联非因果)
- 新增 AttributionCaseStrategy 实体 + AttributionCaseStrategyMapper + attribution_case_strategies 承载表(schema.sql 同步 + DatabaseInitializer 迁移464)
- ConditionalPathogenLowSideRules 暴露 LOW_SIDE_READING_KEY(§2.2 KB 子键锚点)
E2E Test Bot 3 дней назад
Родитель
Сommit
f1a0d9908e

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3488,6 +3488,42 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         // 迁移463: service_cost_config 补 §7.5 规格列 service_name/enabled(S-cfc-68 team.admin.manage 成本价管理端维护载体)
         ensureColumn("service_cost_config", "service_name", "VARCHAR(64) NULL COMMENT '服务名称'");
         ensureColumn("service_cost_config", "enabled", "TINYINT DEFAULT 1 COMMENT '是否启用: 1=启用 0=停用'");
+
+        // 迁移464: 创建 P3 跨案例聚合承载表 attribution_case_strategies(T15 跨案例聚合,S-cfc-119 guhe.p3.lowSideAggregate)
+        createAttributionCaseStrategyTable();
+    }
+
+    /**
+     * 迁移464: 创建 P3 跨案例聚合承载表(T15 跨案例聚合,S-cfc-119 guhe.p3.lowSideAggregate)。
+     *
+     * <p>承载跨案例「同条件致病菌低侧」应对方案:低侧口径并入既有 P3 聚合口径;
+     * 同 {@code strategy_code}(同条件致病菌低侧)聚类为同应对方案(T15 验收语义)。
+     * {@code association_only} 恒 1(§五 边界声明:低侧机制表述为「关联非因果」)。</p>
+     */
+    private void createAttributionCaseStrategyTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS attribution_case_strategies ("
+                    + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "report_id BIGINT NULL COMMENT '来源归因报告ID(attribution_reports.id)', "
+                    + "member_id BIGINT NOT NULL COMMENT '家庭成员ID(case归属)', "
+                    + "bacteria_code VARCHAR(64) NULL COMMENT '条件致病菌指标码(如大肠杆菌/escherichia)', "
+                    + "bacteria_name VARCHAR(100) NULL COMMENT '条件致病菌显示名', "
+                    + "reading_key VARCHAR(100) NOT NULL DEFAULT '条件致病菌低侧判读补充(人工)' COMMENT '判读口径键名(§2.2 KB新增子键)', "
+                    + "reading_level VARCHAR(16) NOT NULL DEFAULT 'low' COMMENT '判读分组: low=低侧(仅低侧口径聚合)/normal/high', "
+                    + "strategy_code VARCHAR(64) NOT NULL COMMENT '策略码: 同条件致病菌低侧→同策略聚类键(T15验收语义)', "
+                    + "strategy_name VARCHAR(200) NULL COMMENT '应对方案名(关联非因果表述)', "
+                    + "strategy_detail TEXT NULL COMMENT '应对方案详情(可空)', "
+                    + "association_only TINYINT(1) NOT NULL DEFAULT 1 COMMENT '关联非因果口径: 低侧机制全部=1(§五边界声明)', "
+                    + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
+                    + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+                    + "INDEX idx_member (member_id), "
+                    + "INDEX idx_reading (reading_level, strategy_code), "
+                    + "INDEX idx_bacteria (bacteria_code)"
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='P3跨案例聚合承载(同条件致病菌低侧→同策略)'");
+            log.info("迁移464: attribution_case_strategies 表已就绪");
+        } catch (Exception e) {
+            log.warn("迁移464: attribution_case_strategies 创建失败: {}", e.getMessage());
+        }
     }
 
     /**

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/constant/ConditionalPathogenLowSideRules.java

@@ -33,6 +33,9 @@ public final class ConditionalPathogenLowSideRules {
     public static final String GROUP_CODE = "L4-1";
     public static final String GROUP_TITLE = "条件致病菌低侧判读(四机制 · 关联非因果 · U 型双向)";
 
+    /** 判读口径键名(§2.2 KB 新增子键「条件致病菌低侧判读补充(人工)」,P3 跨案例聚合口径的事实锚点,T15) */
+    public static final String LOW_SIDE_READING_KEY = "条件致病菌低侧判读补充(人工)";
+
     /** 大肠杆菌 / 埃希氏菌属(16S 属/种级)匹配键 */
     private static final List<String> ECOLI_KEYS = Collections.unmodifiableList(Arrays.asList(
             "大肠杆菌", "埃希氏", "escherichia", "ecoli"));

+ 94 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/AttributionCaseStrategy.java

@@ -0,0 +1,94 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * P3 跨案例聚合承载实体(T15 跨案例聚合,能力键 {@code guhe.p3.lowSideAggregate})。
+ *
+ * <p>沉淀跨案例「同条件致病菌低侧」应对方案:低侧口径并入既有 P3 聚合口径,
+ * 每行记录一次跨案例低侧判读观察(成员 + 条件致病菌 + 判读口径 + 同策略码),
+ * 同 {@code strategyCode}(同条件致病菌低侧)聚类为同应对方案。
+ *
+ * <p>低侧机制全部以「关联非因果」表述({@code associationOnly=true}),
+ * 不构成健康评估/诊断/干预建议(§五 边界声明)。判读口径事实锚点为知识库
+ * 子键 {@link #LOW_SIDE_READING_KEY}(§2.2 KB 新增「条件致病菌低侧判读补充(人工)」)。
+ *
+ * @author 浠艾福平台程序修改 T15(P3 沉淀与协同层·跨案例聚合)
+ */
+@Data
+@TableName("attribution_case_strategies")
+public class AttributionCaseStrategy implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 判读分组:低侧(能力键仅聚合低侧判读案例) */
+    public static final String READING_LOW = "low";
+
+    /** 判读分组:正常 */
+    public static final String READING_NORMAL = "normal";
+
+    /** 判读分组:高侧 */
+    public static final String READING_HIGH = "high";
+
+    /** 判读口径键名(§2.2 KB 新增子键,跨案例聚合口径的事实锚点) */
+    public static final String LOW_SIDE_READING_KEY = "条件致病菌低侧判读补充(人工)";
+
+    /** 默认策略码(无明确策略时归入低侧观察组) */
+    public static final String DEFAULT_STRATEGY_CODE = "low_side_observation";
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 来源归因报告ID(attribution_reports.id,跨案例来源) */
+    @TableField("report_id")
+    private Long reportId;
+
+    /** 家庭成员ID(case 归属) */
+    @TableField("member_id")
+    private Long memberId;
+
+    /** 条件致病菌指标码(如 大肠杆菌 / escherichia) */
+    @TableField("bacteria_code")
+    private String bacteriaCode;
+
+    /** 条件致病菌显示名 */
+    @TableField("bacteria_name")
+    private String bacteriaName;
+
+    /** 判读口径键名(默认「条件致病菌低侧判读补充(人工)」) */
+    @TableField("reading_key")
+    private String readingKey;
+
+    /** 判读分组:low=低侧 / normal / high(能力键仅聚合低侧) */
+    @TableField("reading_level")
+    private String readingLevel;
+
+    /** 策略码:同条件致病菌低侧 → 同策略聚类键(T15 验收语义) */
+    @TableField("strategy_code")
+    private String strategyCode;
+
+    /** 应对方案名(关联非因果表述) */
+    @TableField("strategy_name")
+    private String strategyName;
+
+    /** 应对方案详情(可空) */
+    @TableField("strategy_detail")
+    private String strategyDetail;
+
+    /** 关联非因果口径:低侧机制全部 = true(§五 边界声明) */
+    @TableField("association_only")
+    private Boolean associationOnly;
+
+    @TableField("created_at")
+    private Date createdAt;
+
+    @TableField("updated_at")
+    private Date updatedAt;
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/AttributionCaseStrategyMapper.java

@@ -0,0 +1,17 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.AttributionCaseStrategy;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * P3 跨案例聚合承载表 Mapper(T15,能力键 {@code guhe.p3.lowSideAggregate})。
+ *
+ * <p>沿用现有 Mapper 空接口范式(MyBatis-Plus BaseMapper 基础 CRUD 无需 XML),
+ * 承载 `attribution_case_strategies` 表:沉淀跨案例「同条件致病菌低侧」应对方案。
+ *
+ * @author 浠艾福平台程序修改 T15(P3 沉淀与协同层·跨案例聚合)
+ */
+@Mapper
+public interface AttributionCaseStrategyMapper extends BaseMapper<AttributionCaseStrategy> {
+}

+ 119 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/attribution/AttributionCaseAggregateService.java

@@ -0,0 +1,119 @@
+package com.etotem.cfc.service.attribution;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.constant.ConditionalPathogenLowSideRules;
+import com.etotem.cfc.entity.AttributionCaseStrategy;
+import com.etotem.cfc.mapper.AttributionCaseStrategyMapper;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * P3 跨案例聚合服务(T15 跨案例聚合,能力键 {@code guhe.p3.lowSideAggregate})。
+ *
+ * <p>低侧口径并入既有 P3 聚合口径(§三-P3 §T15,2026-11-08 浠艾福平台程序修改计划):
+ * 按「条件致病菌低侧判读」判读口径聚合跨案例归因资产,聚类为「同策略」——
+ * 同条件致病菌低侧(相同 {@code strategyCode})→ 同一应对方案组,
+ * 可检索「同条件致病菌低侧」应对方案。跨案例可按低侧判读口径聚合同策略(T15 验收)。
+ *
+ * <p>判读口径事实锚点为知识库子键「条件致病菌低侧判读补充(人工)」
+ * ({@link ConditionalPathogenLowSideRules#LOW_SIDE_READING_KEY},§2.2 KB)。
+ * 低侧机制全部以「关联非因果」表述({@link AttributionCaseStrategy#getAssociationOnly()} 恒 true),
+ * 不构成健康评估/诊断/干预建议(§五 边界声明);⛔ 不产生「缺 X → 买含 X 菌株产品」桥接。
+ *
+ * <p>本能力属后端内部归因资产层(P3 沉淀与协同),无用户权限维度。
+ *
+ * @author 浠艾福平台程序修改 T15(P3 沉淀与协同层·跨案例聚合)
+ */
+@Component
+public class AttributionCaseAggregateService {
+
+    /** 低侧判读分组码(能力键仅按低侧口径聚合) */
+    public static final String READING_LOW = AttributionCaseStrategy.READING_LOW;
+
+    /** 默认策略码(无明确策略时归入低侧观察组) */
+    public static final String DEFAULT_STRATEGY_CODE = AttributionCaseStrategy.DEFAULT_STRATEGY_CODE;
+
+    @Resource
+    private AttributionCaseStrategyMapper attributionCaseStrategyMapper;
+
+    /**
+     * 按「条件致病菌低侧判读」口径聚合跨案例,聚类为「同策略」(T15 验收)。
+     *
+     * <p>仅聚合 {@code readingLevel=low} 的低侧判读案例(其余判读分组不并入低侧口径);
+     * 同一 {@code strategyCode}(同条件致病菌低侧)归入同一应对方案组。
+     *
+     * @param cases 跨案例判读资产(可含非低侧行,低侧口径自动过滤;null 视为空集)
+     * @return 策略码 → 同策略案例列表 的分组视图(LinkedHashMap 保持插入序)
+     */
+    public Map<String, List<AttributionCaseStrategy>> aggregateLowSideStrategy(
+            List<AttributionCaseStrategy> cases) {
+        Map<String, List<AttributionCaseStrategy>> grouped =
+                new LinkedHashMap<String, List<AttributionCaseStrategy>>();
+        if (cases == null) {
+            return grouped;
+        }
+        for (AttributionCaseStrategy c : cases) {
+            if (c == null || !READING_LOW.equalsIgnoreCase(c.getReadingLevel())) {
+                continue; // 低侧口径:仅聚合低侧判读案例
+            }
+            String key = c.getStrategyCode() == null || c.getStrategyCode().trim().isEmpty()
+                    ? DEFAULT_STRATEGY_CODE : c.getStrategyCode();
+            List<AttributionCaseStrategy> bucket = grouped.get(key);
+            if (bucket == null) {
+                bucket = new ArrayList<AttributionCaseStrategy>();
+                grouped.put(key, bucket);
+            }
+            bucket.add(c);
+        }
+        return grouped;
+    }
+
+    /**
+     * 沉淀一条跨案例低侧判读应对方案(T15 数据层写入)。
+     *
+     * <p>缺省兜底:{@code readingLevel=low}、{@code readingKey=条件致病菌低侧判读补充(人工)}、
+     * {@code associationOnly=true}(低侧机制全部「关联非因果」)。
+     *
+     * @param strategyCase 跨案例低侧判读资产(null 忽略)
+     * @return 影响行数(MyBatis-Plus insert 返回值)
+     */
+    public int recordCase(AttributionCaseStrategy strategyCase) {
+        if (strategyCase == null) {
+            return 0;
+        }
+        if (strategyCase.getReadingLevel() == null || strategyCase.getReadingLevel().trim().isEmpty()) {
+            strategyCase.setReadingLevel(READING_LOW);
+        }
+        if (strategyCase.getReadingKey() == null || strategyCase.getReadingKey().trim().isEmpty()) {
+            strategyCase.setReadingKey(ConditionalPathogenLowSideRules.LOW_SIDE_READING_KEY);
+        }
+        if (strategyCase.getAssociationOnly() == null) {
+            strategyCase.setAssociationOnly(Boolean.TRUE);
+        }
+        if (strategyCase.getCreatedAt() == null) {
+            strategyCase.setCreatedAt(new Date());
+        }
+        if (strategyCase.getUpdatedAt() == null) {
+            strategyCase.setUpdatedAt(new Date());
+        }
+        return attributionCaseStrategyMapper.insert(strategyCase);
+    }
+
+    /**
+     * 检索全部低侧判读案例(「同条件致病菌低侧」应对方案的跨案例检索入口,T15)。
+     *
+     * @return 低侧判读案例列表(按 id 倒序)
+     */
+    public List<AttributionCaseStrategy> listLowSideCases() {
+        return attributionCaseStrategyMapper.selectList(
+                new LambdaQueryWrapper<AttributionCaseStrategy>()
+                        .eq(AttributionCaseStrategy::getReadingLevel, READING_LOW)
+                        .orderByDesc(AttributionCaseStrategy::getId));
+    }
+}

+ 20 - 0
cfc-backend/src/main/resources/schema.sql

@@ -5224,6 +5224,26 @@ CREATE TABLE IF NOT EXISTS attribution_reports (
     INDEX idx_plan (plan_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭归因分析报告(阶段二核心闭环)';
 
+-- P3 跨案例聚合承载表(T15 跨案例聚合,S-cfc-119 guhe.p3.lowSideAggregate:沉淀跨案例「同条件致病菌低侧」应对方案)
+CREATE TABLE IF NOT EXISTS attribution_case_strategies (
+    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
+    report_id        BIGINT NULL COMMENT '来源归因报告ID(attribution_reports.id)',
+    member_id        BIGINT NOT NULL COMMENT '家庭成员ID(case归属)',
+    bacteria_code    VARCHAR(64) NULL COMMENT '条件致病菌指标码(如大肠杆菌/escherichia)',
+    bacteria_name    VARCHAR(100) NULL COMMENT '条件致病菌显示名',
+    reading_key      VARCHAR(100) NOT NULL DEFAULT '条件致病菌低侧判读补充(人工)' COMMENT '判读口径键名(§2.2 KB新增子键)',
+    reading_level    VARCHAR(16) NOT NULL DEFAULT 'low' COMMENT '判读分组: low=低侧(仅低侧口径聚合)/normal/high',
+    strategy_code    VARCHAR(64) NOT NULL COMMENT '策略码: 同条件致病菌低侧→同策略聚类键(T15验收语义)',
+    strategy_name    VARCHAR(200) NULL COMMENT '应对方案名(关联非因果表述)',
+    strategy_detail  TEXT NULL COMMENT '应对方案详情(可空)',
+    association_only TINYINT(1) NOT NULL DEFAULT 1 COMMENT '关联非因果口径: 低侧机制全部=1(§五边界声明)',
+    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_member (member_id),
+    INDEX idx_reading (reading_level, strategy_code),
+    INDEX idx_bacteria (bacteria_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='P3跨案例聚合承载(同条件致病菌低侧→同策略)';
+
 -- 定期调研模板
 CREATE TABLE IF NOT EXISTS survey_templates (
     id            BIGINT AUTO_INCREMENT PRIMARY KEY,