Procházet zdrojové kódy

新增
1、新增九防安全报表

liangwenxuan před 2 týdny
rodič
revize
dc1ef6c654
11 změnil soubory, kde provedl 1774 přidání a 1 odebrání
  1. 38 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/NineRiskBoardController.java
  2. 37 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/riskboard/NineRiskBoardReqVO.java
  3. 478 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/riskboard/NineRiskBoardRespVO.java
  4. 52 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/NineRiskBoardMapper.java
  5. 80 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/bo/NineRiskAssessFlatBO.java
  6. 61 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/enums/biz/NineRiskItemEnum.java
  7. 69 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/enums/biz/RiskLevelEnum.java
  8. 18 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NineRiskBoardService.java
  9. 811 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NineRiskBoardServiceImpl.java
  10. 130 0
      yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/NineRiskBoardMapper.xml
  11. 0 1
      yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/RefundSettlementOrderItemMapper.xml

+ 38 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/NineRiskBoardController.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardRespVO;
+import cn.iocoder.yudao.module.system.service.biz.NineRiskBoardService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/**
+ * 九防安全风险综合看板。
+ *
+ * <p>按楼栋、楼层过滤,均不传时表示查询全部。</p>
+ */
+@Tag(name = "管理后台 - 九防安全风险综合看板")
+@RestController
+@RequestMapping("/nine-risk-board")
+@Validated
+public class NineRiskBoardController {
+
+    @Resource
+    private NineRiskBoardService nineRiskBoardService;
+
+    @GetMapping("/get")
+    @Operation(summary = "获取九防安全风险综合看板数据",
+            description = "一次性返回核心指标、风险分布、月度趋势、热力图、高风险名单等 12 项统计")
+    public CommonResult<NineRiskBoardRespVO> getRiskBoard(NineRiskBoardReqVO reqVO) {
+        return success(nineRiskBoardService.getRiskBoard(reqVO));
+    }
+}

+ 37 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/riskboard/NineRiskBoardReqVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 九防安全风险综合看板 - 查询请求 VO
+ *
+ * <p>楼栋、楼层均为可选过滤条件,不传即代表查询全部。</p>
+ */
+@Schema(description = "管理后台 - 九防安全风险综合看板 Request VO")
+@Data
+public class NineRiskBoardReqVO implements Serializable {
+
+    @Schema(description = "楼栋id,不传表示全部", example = "1")
+    private Long buildId;
+
+    @Schema(description = "楼层id,不传表示全部", example = "2")
+    private Long floorId;
+
+    @Schema(description = "机构id数组,不传则取当前租户")
+    private Long[] tenantIds;
+
+    /**
+     * 风险等级月度趋势的统计月份数,默认近 6 个月
+     */
+    @Schema(description = "月度趋势统计月份数,默认 6", example = "6")
+    private Integer trendMonths;
+
+    /**
+     * 判定为「多项高风险」的阈值,默认 2 项及以上
+     */
+    @Schema(description = "多项高风险阈值,默认 2", example = "2")
+    private Integer multiHighRiskThreshold;
+}

+ 478 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/riskboard/NineRiskBoardRespVO.java

@@ -0,0 +1,478 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 九防安全风险综合看板 - 响应 VO
+ *
+ * <p>一次请求返回看板全部 12 个模块的数据。</p>
+ */
+@Schema(description = "管理后台 - 九防安全风险综合看板 Response VO")
+@Data
+public class NineRiskBoardRespVO implements Serializable {
+
+    @Schema(description = "1、顶部核心指标")
+    private Overview overview;
+
+    @Schema(description = "2、九防风险等级分布(按高危降序)")
+    private List<ItemRiskDistribution> itemRiskDistributions;
+
+    @Schema(description = "2、风险等级整体占比")
+    private List<RiskLevelRatio> riskLevelRatios;
+
+    @Schema(description = "3、风险等级月度趋势(近 N 个月)")
+    private List<MonthlyTrend> monthlyTrends;
+
+    @Schema(description = "4、九防 × 楼栋 高危人数热力图")
+    private HeatMap highRiskHeatMap;
+
+    @Schema(description = "5、存在高风险的长者列表")
+    private List<HighRiskElder> highRiskElders;
+
+    @Schema(description = "6、九防评估得分分布")
+    private List<ItemScoreDistribution> itemScoreDistributions;
+
+    @Schema(description = "7、知情书签署率")
+    private DisclosureSignRate disclosureSignRate;
+
+    @Schema(description = "8、各护理等级高风险长者人数(按第 5 点数据分组)")
+    private List<NurseLevelHighRiskCount> nurseLevelHighRiskElderCounts;
+
+    @Schema(description = "9、各护理等级下、各九防项风险程度为高的记录数量")
+    private List<NurseLevelItemHighRiskCount> nurseLevelHighRiskRecordCounts;
+
+    @Schema(description = "10、九防评估人工作量统计")
+    private List<AssessorStat> assessorStats;
+
+    @Schema(description = "11、九防高危占比")
+    private HighRiskRatio highRiskRatio;
+
+    @Schema(description = "12、2 项及以上高风险长者列表")
+    private List<MultiHighRiskElder> multiHighRiskElders;
+
+    // ==================================================================
+    // 1、顶部核心指标
+    // ==================================================================
+
+    @Schema(description = "顶部核心指标")
+    @Data
+    public static class Overview implements Serializable {
+
+        @Schema(description = "知情书总数")
+        private Integer disclosureTotal;
+
+        @Schema(description = "高风险总人次(九防中风险程度为高的记录条数)")
+        private Integer highRiskTimes;
+
+        @Schema(description = "高风险人次占比(%),分母为九防评估记录总数")
+        private BigDecimal highRiskTimesRatio;
+
+        @Schema(description = "待签署知情书总数(secondElderSign 为空)")
+        private Integer disclosureUnsignedCount;
+
+        @Schema(description = "待签署知情书占比(%)")
+        private BigDecimal disclosureUnsignedRatio;
+
+        @Schema(description = "评估完成率(%),即 9 项防险综合覆盖率 = 实际评估项数 / (评估人数 × 9)")
+        private BigDecimal assessCompleteRate;
+
+        @Schema(description = "评估人员数(不同评估人个数)")
+        private Integer assessorCount;
+
+        @Schema(description = "被评估长者数(去重)")
+        private Integer assessedElderCount;
+
+        @Schema(description = "九防评估记录总数")
+        private Integer assessRecordTotal;
+    }
+
+    // ==================================================================
+    // 2、风险等级分布 / 占比
+    // ==================================================================
+
+    @Schema(description = "单个九防项的风险等级分布")
+    @Data
+    public static class ItemRiskDistribution implements Serializable {
+
+        @Schema(description = "九防项编码")
+        private String itemCode;
+
+        @Schema(description = "九防项名称")
+        private String itemName;
+
+        @Schema(description = "高风险数量")
+        private Integer highCount;
+
+        @Schema(description = "中风险数量")
+        private Integer middleCount;
+
+        @Schema(description = "低风险数量")
+        private Integer lowCount;
+
+        @Schema(description = "未评定数量")
+        private Integer unknownCount;
+
+        @Schema(description = "该项评估总数")
+        private Integer totalCount;
+
+        @Schema(description = "该项高风险占比(%)")
+        private BigDecimal highRatio;
+    }
+
+    @Schema(description = "风险等级整体占比")
+    @Data
+    public static class RiskLevelRatio implements Serializable {
+
+        @Schema(description = "风险等级编码")
+        private String riskLevel;
+
+        @Schema(description = "风险等级名称")
+        private String riskLevelName;
+
+        @Schema(description = "数量")
+        private Integer count;
+
+        @Schema(description = "占比(%)")
+        private BigDecimal ratio;
+    }
+
+    // ==================================================================
+    // 3、月度趋势
+    // ==================================================================
+
+    @Schema(description = "风险等级月度趋势")
+    @Data
+    public static class MonthlyTrend implements Serializable {
+
+        @Schema(description = "月份,格式 yyyy-MM")
+        private String month;
+
+        @Schema(description = "高风险数量")
+        private Integer highCount;
+
+        @Schema(description = "中风险数量")
+        private Integer middleCount;
+
+        @Schema(description = "低风险数量")
+        private Integer lowCount;
+
+        @Schema(description = "未评定数量")
+        private Integer unknownCount;
+
+        @Schema(description = "当月评估总数")
+        private Integer totalCount;
+
+        @Schema(description = "当月高风险占比(%)")
+        private BigDecimal highRatio;
+    }
+
+    // ==================================================================
+    // 4、热力图
+    // ==================================================================
+
+    @Schema(description = "九防 × 楼栋 高危人数热力图")
+    @Data
+    public static class HeatMap implements Serializable {
+
+        @Schema(description = "X 轴:九防项列表")
+        private List<HeatMapAxis> items;
+
+        @Schema(description = "Y 轴:楼栋列表")
+        private List<HeatMapAxis> builds;
+
+        @Schema(description = "热力值集合")
+        private List<HeatMapCell> cells;
+
+        @Schema(description = "所有单元格中的最大值,便于前端做色阶归一化")
+        private Integer maxValue;
+    }
+
+    @Schema(description = "热力图坐标轴")
+    @Data
+    public static class HeatMapAxis implements Serializable {
+
+        @Schema(description = "编码,九防项为 itemCode,楼栋为 buildId")
+        private String code;
+
+        @Schema(description = "名称")
+        private String name;
+    }
+
+    @Schema(description = "热力图单元格")
+    @Data
+    public static class HeatMapCell implements Serializable {
+
+        @Schema(description = "楼栋id")
+        private Long buildId;
+
+        @Schema(description = "楼栋名称")
+        private String buildName;
+
+        @Schema(description = "九防项编码")
+        private String itemCode;
+
+        @Schema(description = "九防项名称")
+        private String itemName;
+
+        @Schema(description = "高危人数(同一长者同项去重)")
+        private Integer highRiskElderCount;
+    }
+
+    // ==================================================================
+    // 5、高风险长者列表
+    // ==================================================================
+
+    @Schema(description = "存在高风险的长者")
+    @Data
+    public static class HighRiskElder implements Serializable {
+
+        @Schema(description = "长者id")
+        private Long elderId;
+
+        @Schema(description = "长者姓名")
+        private String elderName;
+
+        @Schema(description = "性别")
+        private Integer elderSex;
+
+        @Schema(description = "年龄")
+        private Integer elderAge;
+
+        @Schema(description = "护理等级id")
+        private Long nurseLevelId;
+
+        @Schema(description = "护理等级名称")
+        private String nurseLevelName;
+
+        @Schema(description = "风险程度为高的九防项个数")
+        private Integer highRiskCount;
+    }
+
+    // ==================================================================
+    // 6、得分分布
+    // ==================================================================
+
+    @Schema(description = "九防项评估得分分布")
+    @Data
+    public static class ItemScoreDistribution implements Serializable {
+
+        @Schema(description = "九防项编码")
+        private String itemCode;
+
+        @Schema(description = "九防项名称")
+        private String itemName;
+
+        @Schema(description = "有效评分记录数")
+        private Integer count;
+
+        @Schema(description = "平均分")
+        private BigDecimal avgScore;
+
+        @Schema(description = "最低分")
+        private BigDecimal minScore;
+
+        @Schema(description = "最高分")
+        private BigDecimal maxScore;
+
+        @Schema(description = "各分段人次分布")
+        private List<ScoreBucket> buckets;
+    }
+
+    @Schema(description = "得分分段")
+    @Data
+    public static class ScoreBucket implements Serializable {
+
+        @Schema(description = "分段名称,如 0-20")
+        private String range;
+
+        @Schema(description = "分段下界(含)")
+        private Integer min;
+
+        @Schema(description = "分段上界(不含,最后一段为含)")
+        private Integer max;
+
+        @Schema(description = "人次")
+        private Integer count;
+    }
+
+    // ==================================================================
+    // 7、知情书签署率
+    // ==================================================================
+
+    @Schema(description = "知情书签署率")
+    @Data
+    public static class DisclosureSignRate implements Serializable {
+
+        @Schema(description = "知情书总数")
+        private Integer total;
+
+        @Schema(description = "已签署数(secondElderSign 非空)")
+        private Integer signedCount;
+
+        @Schema(description = "待签署数")
+        private Integer unsignedCount;
+
+        @Schema(description = "签署率(%)")
+        private BigDecimal signRate;
+    }
+
+    // ==================================================================
+    // 8、9、护理等级维度
+    // ==================================================================
+
+    @Schema(description = "护理等级维度的高风险统计")
+    @Data
+    public static class NurseLevelHighRiskCount implements Serializable {
+
+        @Schema(description = "护理等级id")
+        private Long nurseLevelId;
+
+        @Schema(description = "护理等级名称")
+        private String nurseLevelName;
+
+        @Schema(description = "数量")
+        private Integer count;
+
+        @Schema(description = "占比(%)")
+        private BigDecimal ratio;
+    }
+
+    @Schema(description = "护理等级 × 九防项的高风险记录数统计")
+    @Data
+    public static class NurseLevelItemHighRiskCount implements Serializable {
+
+        @Schema(description = "护理等级id")
+        private Long nurseLevelId;
+
+        @Schema(description = "护理等级名称")
+        private String nurseLevelName;
+
+        @Schema(description = "该护理等级下各九防项的高风险记录数明细")
+        private List<ItemHighRiskCount> items;
+
+        @Schema(description = "该护理等级的高风险记录数合计")
+        private Integer totalCount;
+
+        @Schema(description = "该护理等级合计数占全部高风险记录数的占比(%)")
+        private BigDecimal ratio;
+    }
+
+    @Schema(description = "单个九防项的高风险记录数")
+    @Data
+    public static class ItemHighRiskCount implements Serializable {
+
+        @Schema(description = "九防项编码")
+        private String itemCode;
+
+        @Schema(description = "九防项名称")
+        private String itemName;
+
+        @Schema(description = "高风险记录数")
+        private Integer count;
+
+        @Schema(description = "占所在护理等级高风险记录数的占比(%)")
+        private BigDecimal ratio;
+    }
+
+    // ==================================================================
+    // 10、评估人统计
+    // ==================================================================
+
+    @Schema(description = "九防评估人统计")
+    @Data
+    public static class AssessorStat implements Serializable {
+
+        @Schema(description = "评估人")
+        private String assessor;
+
+        @Schema(description = "评估长者数量(去重)")
+        private Integer assessedElderCount;
+
+        @Schema(description = "风险程度为高的长者数量(同一长者多个九防表视为 1 个)")
+        private Integer highRiskElderCount;
+
+        @Schema(description = "评估记录数")
+        private Integer assessRecordCount;
+    }
+
+    // ==================================================================
+    // 11、高危占比
+    // ==================================================================
+
+    @Schema(description = "九防高危占比")
+    @Data
+    public static class HighRiskRatio implements Serializable {
+
+        @Schema(description = "被评估长者总数(去重)")
+        private Integer assessedElderCount;
+
+        @Schema(description = "存在高危的长者数(去重)")
+        private Integer highRiskElderCount;
+
+        @Schema(description = "高危长者占比(%)")
+        private BigDecimal highRiskElderRatio;
+
+        @Schema(description = "九防评估记录总数")
+        private Integer assessRecordTotal;
+
+        @Schema(description = "高危记录数")
+        private Integer highRiskRecordCount;
+
+        @Schema(description = "高危记录占比(%)")
+        private BigDecimal highRiskRecordRatio;
+    }
+
+    // ==================================================================
+    // 12、多项高风险列表
+    // ==================================================================
+
+    @Schema(description = "2 项及以上高风险长者")
+    @Data
+    public static class MultiHighRiskElder implements Serializable {
+
+        @Schema(description = "长者id")
+        private Long elderId;
+
+        @Schema(description = "进行中的合同号")
+        private String contractNumber;
+
+        @Schema(description = "姓名")
+        private String elderName;
+
+        @Schema(description = "性别")
+        private Integer elderSex;
+
+        @Schema(description = "年龄")
+        private Integer elderAge;
+
+        @Schema(description = "楼栋")
+        private String buildName;
+
+        @Schema(description = "楼层")
+        private String floorName;
+
+        @Schema(description = "床号")
+        private String bedName;
+
+        @Schema(description = "护理级别")
+        private String nurseLevelName;
+
+        @Schema(description = "评估人(多个高风险项的评估人去重后拼接)")
+        private String assessor;
+
+        @Schema(description = "高风险项名称,逗号分隔")
+        private String highRiskItems;
+
+        @Schema(description = "高风险项个数")
+        private Integer highRiskCount;
+
+        @Schema(description = "评估日期,取高风险项中的最新评估日期")
+        private LocalDate assessDate;
+    }
+}

+ 52 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/NineRiskBoardMapper.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.system.dal.mysql.biz;
+
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardReqVO;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.NineRiskAssessFlatBO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 九防安全风险综合看板 Mapper。
+ *
+ * <p>整个看板只有两次数据库访问:</p>
+ * <ol>
+ *     <li>{@link #selectAssessFlatList} —— 9 张九防表 UNION ALL 后关联长者档案,
+ *     产出一份「扁平化明细」,看板 12 个模块中除知情书外的所有指标都基于它在内存中聚合;</li>
+ *     <li>{@link #selectDisclosureStat} —— 知情书签署情况统计。</li>
+ * </ol>
+ */
+@Mapper
+public interface NineRiskBoardMapper {
+
+    /**
+     * 查询九防评估扁平化明细(全量历史,含同一长者同一项的多条记录)。
+     *
+     * <p>这是看板的通用数据源,支撑:风险等级分布、整体占比、月度趋势、热力图、
+     * 高风险长者列表、得分分布、护理等级分组、评估人统计、高危占比、多项高风险列表。</p>
+     *
+     * <p><b>注意:</b>同一名长者在同一张九防表中可能存在多条评估记录,此处不做去重,
+     * 因为月度趋势需要完整的历史轨迹;其余「现状类」指标由 Service 层按
+     * 「长者 + 九防项」取最新一条后再统计。结果已按 elderId、itemCode、assessDate、assessId 排序。</p>
+     *
+     * @param reqVO         查询条件(楼栋、楼层、机构)
+     * @param trendStartDay 月度趋势的起始日期;为空则不限制。注意该参数不用于过滤主数据,
+     *                      仅在需要时由调用方决定,是否收缩数据范围
+     * @return 扁平化明细列表
+     */
+    List<NineRiskAssessFlatBO> selectAssessFlatList(@Param("reqVO") NineRiskBoardReqVO reqVO,
+                                                    @Param("trendStartDay") LocalDate trendStartDay);
+
+    /**
+     * 查询知情书统计。
+     *
+     * <p>返回单行,包含 total(知情书总数)与 signedCount(secondElderSign 非空的已签署数)。</p>
+     *
+     * @param reqVO 查询条件(楼栋、楼层、机构)
+     * @return 统计结果
+     */
+    Map<String, Object> selectDisclosureStat(@Param("reqVO") NineRiskBoardReqVO reqVO);
+}

+ 80 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/bo/NineRiskAssessFlatBO.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.system.dal.mysql.biz.bo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 九防评估「扁平化」记录。
+ *
+ * <p>由 9 张九防评估表 UNION ALL 后与长者档案关联得到的查询结果载体,不对应任何单一数据库表,
+ * 是看板所有统计口径的唯一数据来源,一次查询即可支撑风险分布、趋势、热力图、明细列表等全部指标。</p>
+ */
+@Schema(description = "九防评估扁平化记录")
+@Data
+public class NineRiskAssessFlatBO implements Serializable {
+
+    // ==================== 评估记录自身信息 ====================
+
+    @Schema(description = "评估记录id")
+    private Long assessId;
+
+    /**
+     * 九防项编码,取值见 {@code NineRiskItemEnum}
+     */
+    @Schema(description = "九防项编码")
+    private String itemCode;
+
+    @Schema(description = "长者id")
+    private Long elderId;
+
+    @Schema(description = "评估人")
+    private String assessor;
+
+    @Schema(description = "评估日期")
+    private LocalDate assessDate;
+
+    @Schema(description = "风险程度原始值")
+    private String riskLevel;
+
+    @Schema(description = "评估得分")
+    private BigDecimal assessScore;
+
+    // ==================== 长者档案冗余信息 ====================
+
+    @Schema(description = "长者姓名")
+    private String elderName;
+
+    @Schema(description = "长者性别")
+    private Integer elderSex;
+
+    @Schema(description = "长者年龄")
+    private Integer elderAge;
+
+    @Schema(description = "护理等级id")
+    private Long nurseLevelId;
+
+    @Schema(description = "护理等级名称")
+    private String nurseLevelName;
+
+    @Schema(description = "楼栋id")
+    private Long buildId;
+
+    @Schema(description = "楼栋名称")
+    private String buildName;
+
+    @Schema(description = "楼层id")
+    private Long floorId;
+
+    @Schema(description = "楼层名称")
+    private String floorName;
+
+    @Schema(description = "床位名称")
+    private String bedName;
+
+    @Schema(description = "进行中的合同号")
+    private String contractNumber;
+}

+ 61 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/enums/biz/NineRiskItemEnum.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.system.enums.biz;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 九防评估项枚举。
+ *
+ * <p>注意:风险知情书(elderly_risk_disclosure_statement)不属于九防,仅用于知情书签署率统计。</p>
+ */
+@Getter
+@AllArgsConstructor
+public enum NineRiskItemEnum {
+
+    FALL_DOWN("fallDown", "防跌倒", "elderly_fall_down"),
+    PRESSURE_SORES("pressureSores", "防压疮", "elderly_pressure_sores"),
+    FALL_PREVENTION_MEASURES("fallPreventionMeasures", "防坠床", "elderly_fall_prevention_measures"),
+    EMPYROSIS("empyrosis", "防烫伤", "elderly_empyrosis"),
+    WANDER_AWAY("wanderAway", "防走失", "elderly_wander_away"),
+    SUICIDE_RISK("suicideRisk", "防自杀", "elderly_assess_suicide_risk"),
+    ANTI_ENTERTAINMENT("antiEntertainment", "防文娱活动意外", "elderly_anti_entertainment"),
+    SIMPLE_MENTAL_STATE("simpleMentalState", "简易精神状态", "elderly_assess_simple_mental_state"),
+    ASPHYXIATION_BY_CHOKING("asphyxiationByChoking", "防噎食", "elderly_asphyxiation_by_choking"),
+    ;
+
+    /**
+     * 九防项编码
+     */
+    private final String code;
+    /**
+     * 九防项名称
+     */
+    private final String name;
+    /**
+     * 对应的数据库表名
+     */
+    private final String tableName;
+
+    private static final Map<String, NineRiskItemEnum> CODE_MAP =
+            Arrays.stream(values()).collect(Collectors.toMap(NineRiskItemEnum::getCode, e -> e,
+                    (a, b) -> a, LinkedHashMap::new));
+
+    /**
+     * 九防项总数,用于计算评估完成率(综合覆盖率)
+     */
+    public static final int TOTAL_ITEM_COUNT = values().length;
+
+    public static NineRiskItemEnum of(String code) {
+        return CODE_MAP.get(code);
+    }
+
+    public static String nameOf(String code) {
+        NineRiskItemEnum item = of(code);
+        return item == null ? code : item.getName();
+    }
+}

+ 69 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/enums/biz/RiskLevelEnum.java

@@ -0,0 +1,69 @@
+package cn.iocoder.yudao.module.system.enums.biz;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 风险等级枚举。
+ *
+ * <p>数据库中 {@code risk_level} 为自由文本(如「高风险」「高危」「中风险」「低风险」等),
+ * 这里统一做归一化,便于看板按等级聚合与排序。</p>
+ */
+@Getter
+@AllArgsConstructor
+public enum RiskLevelEnum {
+
+    /**
+     * 高风险,排序值最小,保证「按高危排序」时排在最前
+     */
+    HIGH("high", "高风险", 1),
+    MIDDLE("middle", "中风险", 2),
+    LOW("low", "低风险", 3),
+    /**
+     * 无法识别或未填写
+     */
+    UNKNOWN("unknown", "未评定", 4),
+    ;
+
+    private final String code;
+    private final String name;
+    /**
+     * 排序值,越小风险越高
+     */
+    private final int sort;
+
+    /**
+     * 将数据库中的自由文本风险等级归一化为枚举。
+     *
+     * @param rawRiskLevel 原始风险等级文本
+     * @return 归一化后的枚举,识别不了时返回 {@link #UNKNOWN}
+     */
+    public static RiskLevelEnum parse(String rawRiskLevel) {
+        if (rawRiskLevel == null) {
+            return UNKNOWN;
+        }
+        String text = rawRiskLevel.trim();
+        if (text.isEmpty()) {
+            return UNKNOWN;
+        }
+        // 兼容「高」「高风险」「高危」「高度危险」以及英文 high / 数字 3 等写法
+        if (text.startsWith("高") || "high".equalsIgnoreCase(text) || "3".equals(text)) {
+            return HIGH;
+        }
+        if (text.startsWith("中") || "middle".equalsIgnoreCase(text) || "medium".equalsIgnoreCase(text)
+                || "2".equals(text)) {
+            return MIDDLE;
+        }
+        if (text.startsWith("低") || "low".equalsIgnoreCase(text) || "1".equals(text)) {
+            return LOW;
+        }
+        return UNKNOWN;
+    }
+
+    /**
+     * 是否为高风险
+     */
+    public static boolean isHigh(String rawRiskLevel) {
+        return parse(rawRiskLevel) == HIGH;
+    }
+}

+ 18 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NineRiskBoardService.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.system.service.biz;
+
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardRespVO;
+
+/**
+ * 九防安全风险综合看板 Service
+ */
+public interface NineRiskBoardService {
+
+    /**
+     * 获取九防安全风险综合看板数据。
+     *
+     * @param reqVO 查询条件,楼栋 / 楼层为空时表示查询全部
+     * @return 看板全量数据
+     */
+    NineRiskBoardRespVO getRiskBoard(NineRiskBoardReqVO reqVO);
+}

+ 811 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NineRiskBoardServiceImpl.java

@@ -0,0 +1,811 @@
+package cn.iocoder.yudao.module.system.service.biz;
+
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.riskboard.NineRiskBoardRespVO.*;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.NineRiskAssessFlatBO;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.NineRiskBoardMapper;
+import cn.iocoder.yudao.module.system.enums.biz.NineRiskItemEnum;
+import cn.iocoder.yudao.module.system.enums.biz.RiskLevelEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 九防安全风险综合看板 Service 实现。
+ *
+ * <p><b>设计要点</b>:整个看板仅访问数据库 2 次 —— 一次取九防评估扁平化明细,
+ * 一次取知情书统计。其余 12 个模块的指标全部由这份明细在内存中聚合派生,
+ * 避免了「一个指标一条 SQL」带来的重复扫表。</p>
+ */
+@Slf4j
+@Service
+public class NineRiskBoardServiceImpl implements NineRiskBoardService {
+
+    /**
+     * 月度趋势默认统计月份数
+     */
+    private static final int DEFAULT_TREND_MONTHS = 6;
+
+    /**
+     * 多项高风险默认阈值:2 项及以上
+     */
+    private static final int DEFAULT_MULTI_HIGH_RISK_THRESHOLD = 2;
+
+    /**
+     * 得分分布的分段边界,形成 0-20、20-40、40-60、60-80、80-100 五档
+     */
+    private static final int[] SCORE_BUCKET_BOUNDS = {0, 20, 40, 60, 80, 100};
+
+    private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
+
+    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
+
+    @Resource
+    private NineRiskBoardMapper nineRiskBoardMapper;
+
+    @Override
+    public NineRiskBoardRespVO getRiskBoard(NineRiskBoardReqVO reqVO) {
+        if (reqVO == null) {
+            reqVO = new NineRiskBoardReqVO();
+        }
+        int trendMonths = reqVO.getTrendMonths() == null || reqVO.getTrendMonths() <= 0
+                ? DEFAULT_TREND_MONTHS : reqVO.getTrendMonths();
+        int multiThreshold = reqVO.getMultiHighRiskThreshold() == null || reqVO.getMultiHighRiskThreshold() <= 1
+                ? DEFAULT_MULTI_HIGH_RISK_THRESHOLD : reqVO.getMultiHighRiskThreshold();
+
+        // ============ 数据库访问 1/2:九防评估扁平化明细(看板通用数据源)============
+        // 查询全量历史明细,不下推日期条件:月度趋势需要历史轨迹,其余指标在内存中取最新一条
+        List<NineRiskAssessFlatBO> historyList = nineRiskBoardMapper.selectAssessFlatList(reqVO, null);
+        historyList = historyList == null ? Collections.emptyList() : historyList;
+
+        // ============ 数据库访问 2/2:知情书统计 ============
+        Map<String, Object> disclosureStat = nineRiskBoardMapper.selectDisclosureStat(reqVO);
+
+        // 同一长者在同一张九防表可能存在多条评估记录,「现状类」指标一律只认最新的一条
+        List<NineRiskAssessFlatBO> latestList = filterLatestPerElderItem(historyList);
+
+        // ---------- 基于最新明细预计算若干「公共中间结果」,供多个模块复用 ----------
+        Context ctx = buildContext(latestList);
+
+        NineRiskBoardRespVO resp = new NineRiskBoardRespVO();
+        DisclosureSignRate signRate = buildDisclosureSignRate(disclosureStat);
+
+        resp.setDisclosureSignRate(signRate);                                       // 7
+        resp.setOverview(buildOverview(ctx, signRate));                             // 1
+        resp.setItemRiskDistributions(buildItemRiskDistributions(ctx));             // 2-1
+        resp.setRiskLevelRatios(buildRiskLevelRatios(ctx));                         // 2-2
+        // 月度趋势反映的是「每月评估情况的变化」,必须基于全量历史明细,不能去重
+        resp.setMonthlyTrends(buildMonthlyTrends(historyList, trendMonths));        // 3
+        resp.setHighRiskHeatMap(buildHeatMap(ctx));                                 // 4
+        resp.setHighRiskElders(buildHighRiskElders(ctx));                           // 5
+        resp.setItemScoreDistributions(buildItemScoreDistributions(ctx));           // 6
+        resp.setNurseLevelHighRiskElderCounts(buildNurseLevelElderCounts(ctx));     // 8
+        resp.setNurseLevelHighRiskRecordCounts(buildNurseLevelRecordCounts(ctx));   // 9
+        resp.setAssessorStats(buildAssessorStats(ctx));                             // 10
+        resp.setHighRiskRatio(buildHighRiskRatio(ctx));                             // 11
+        resp.setMultiHighRiskElders(buildMultiHighRiskElders(ctx, multiThreshold)); // 12
+        return resp;
+    }
+
+    // ==================================================================
+    // 公共中间结果
+    // ==================================================================
+
+    /**
+     * 看板计算上下文:把明细「一次遍历」拆解成若干可复用的中间结构,
+     * 后续 12 个模块只读这些结构,不再重复遍历原始明细。
+     */
+    private static class Context {
+
+        /**
+         * 九防评估明细,每个「长者 + 九防项」只保留最新一条
+         */
+        List<NineRiskAssessFlatBO> all;
+
+        /**
+         * 高风险明细(risk_level 归一化为「高」),同样只含最新一条
+         */
+        List<NineRiskAssessFlatBO> highList;
+
+        /**
+         * 按九防项分组的明细(最新一条口径)
+         */
+        Map<String, List<NineRiskAssessFlatBO>> byItem;
+
+        /**
+         * 全局风险等级计数
+         */
+        Map<RiskLevelEnum, Integer> riskLevelCount;
+
+        /**
+         * 被评估长者(去重):elderId -> 该长者任一档案行,用于取姓名等冗余信息
+         */
+        Map<Long, NineRiskAssessFlatBO> elderProfileMap;
+
+        /**
+         * 长者 -> 已评估的九防项集合,用于计算综合覆盖率
+         */
+        Map<Long, Set<String>> elderAssessedItems;
+
+        /**
+         * 长者 -> 高风险的九防项集合(同一长者同一项多次评估只算 1 项)
+         */
+        Map<Long, Set<String>> elderHighRiskItems;
+
+        /**
+         * 长者 -> 高风险项中的最新评估日期
+         */
+        Map<Long, LocalDate> elderLatestHighRiskDate;
+
+        /**
+         * 长者 -> 高风险项的评估人集合
+         */
+        Map<Long, Set<String>> elderHighRiskAssessors;
+    }
+
+    /**
+     * 按「长者 + 九防项」保留最新的一条评估记录。
+     *
+     * <p>同一名长者在同一张九防表中可能有多条历史评估记录,看板的「现状类」指标
+     * (风险分布、高危人数、得分、覆盖率、热力图、明细列表等)只应体现最近一次评估结果,
+     * 否则历史记录会被重复计入,导致人次虚高、且同一长者可能同时被算成高危与低危。</p>
+     *
+     * <p>排序规则:评估日期较晚者优先;日期相同(或为空)时取主键 id 较大者,
+     * 保证同日多次录入时仍能稳定拿到最后录入的一条。</p>
+     *
+     * @param historyList 全量历史明细
+     * @return 每个「长者 + 九防项」仅保留一条的最新明细
+     */
+    private List<NineRiskAssessFlatBO> filterLatestPerElderItem(List<NineRiskAssessFlatBO> historyList) {
+        Map<String, NineRiskAssessFlatBO> latestMap = new LinkedHashMap<>();
+        for (NineRiskAssessFlatBO row : historyList) {
+            if (row.getElderId() == null || row.getItemCode() == null) {
+                continue;
+            }
+            String key = row.getElderId() + "#" + row.getItemCode();
+            NineRiskAssessFlatBO exists = latestMap.get(key);
+            if (exists == null || isNewer(row, exists)) {
+                latestMap.put(key, row);
+            }
+        }
+        return new ArrayList<>(latestMap.values());
+    }
+
+    /**
+     * 判断 candidate 是否比 current 更新:先比评估日期,日期无法区分时比主键 id。
+     */
+    private static boolean isNewer(NineRiskAssessFlatBO candidate, NineRiskAssessFlatBO current) {
+        LocalDate candidateDate = candidate.getAssessDate();
+        LocalDate currentDate = current.getAssessDate();
+        if (candidateDate != null && currentDate != null && !candidateDate.isEqual(currentDate)) {
+            return candidateDate.isAfter(currentDate);
+        }
+        // 有日期的优先于没日期的
+        if (candidateDate != null && currentDate == null) {
+            return true;
+        }
+        if (candidateDate == null && currentDate != null) {
+            return false;
+        }
+        // 日期相同或均为空,比较主键
+        Long candidateId = candidate.getAssessId();
+        Long currentId = current.getAssessId();
+        if (candidateId == null) {
+            return false;
+        }
+        return currentId == null || candidateId > currentId;
+    }
+
+    /**
+     * 单次遍历明细,构建全部公共中间结果。
+     */
+    private Context buildContext(List<NineRiskAssessFlatBO> flatList) {
+        Context ctx = new Context();
+        ctx.all = flatList;
+        ctx.highList = new ArrayList<>();
+        ctx.byItem = new LinkedHashMap<>();
+        ctx.riskLevelCount = new EnumMap<>(RiskLevelEnum.class);
+        ctx.elderProfileMap = new LinkedHashMap<>();
+        ctx.elderAssessedItems = new LinkedHashMap<>();
+        ctx.elderHighRiskItems = new LinkedHashMap<>();
+        ctx.elderLatestHighRiskDate = new HashMap<>();
+        ctx.elderHighRiskAssessors = new HashMap<>();
+
+        // 预置九防项,保证没有数据的项也会出现在图表中
+        for (NineRiskItemEnum item : NineRiskItemEnum.values()) {
+            ctx.byItem.put(item.getCode(), new ArrayList<>());
+        }
+        for (RiskLevelEnum level : RiskLevelEnum.values()) {
+            ctx.riskLevelCount.put(level, 0);
+        }
+
+        for (NineRiskAssessFlatBO row : flatList) {
+            String itemCode = row.getItemCode();
+            Long elderId = row.getElderId();
+            RiskLevelEnum level = RiskLevelEnum.parse(row.getRiskLevel());
+
+            ctx.byItem.computeIfAbsent(itemCode, k -> new ArrayList<>()).add(row);
+            ctx.riskLevelCount.merge(level, 1, Integer::sum);
+
+            if (elderId != null) {
+                ctx.elderProfileMap.putIfAbsent(elderId, row);
+                ctx.elderAssessedItems.computeIfAbsent(elderId, k -> new HashSet<>()).add(itemCode);
+            }
+
+            if (level == RiskLevelEnum.HIGH) {
+                ctx.highList.add(row);
+                if (elderId != null) {
+                    ctx.elderHighRiskItems.computeIfAbsent(elderId, k -> new LinkedHashSet<>()).add(itemCode);
+                    // 记录高风险项中的最新评估日期
+                    LocalDate date = row.getAssessDate();
+                    if (date != null) {
+                        ctx.elderLatestHighRiskDate.merge(elderId, date,
+                                (oldVal, newVal) -> newVal.isAfter(oldVal) ? newVal : oldVal);
+                    }
+                    if (isNotBlank(row.getAssessor())) {
+                        ctx.elderHighRiskAssessors.computeIfAbsent(elderId, k -> new LinkedHashSet<>())
+                                .add(row.getAssessor().trim());
+                    }
+                }
+            }
+        }
+        return ctx;
+    }
+
+    // ==================================================================
+    // 1、顶部核心指标
+    // ==================================================================
+
+    private Overview buildOverview(Context ctx, DisclosureSignRate signRate) {
+        Overview vo = new Overview();
+        int recordTotal = ctx.all.size();
+        int highTimes = ctx.highList.size();
+        int elderCount = ctx.elderProfileMap.size();
+
+        vo.setAssessRecordTotal(recordTotal);
+        vo.setAssessedElderCount(elderCount);
+        vo.setHighRiskTimes(highTimes);
+        vo.setHighRiskTimesRatio(ratio(highTimes, recordTotal));
+
+        // 知情书相关
+        vo.setDisclosureTotal(signRate.getTotal());
+        vo.setDisclosureUnsignedCount(signRate.getUnsignedCount());
+        vo.setDisclosureUnsignedRatio(ratio(signRate.getUnsignedCount(), signRate.getTotal()));
+
+        // 评估完成率(9 项防险综合覆盖率)= 实际评估到的项数 / (被评估长者数 × 9)
+        int assessedItemCount = ctx.elderAssessedItems.values().stream().mapToInt(Set::size).sum();
+        vo.setAssessCompleteRate(ratio(assessedItemCount, elderCount * NineRiskItemEnum.TOTAL_ITEM_COUNT));
+
+        // 评估人员数
+        vo.setAssessorCount((int) ctx.all.stream()
+                .map(NineRiskAssessFlatBO::getAssessor)
+                .filter(NineRiskBoardServiceImpl::isNotBlank)
+                .map(String::trim)
+                .distinct()
+                .count());
+        return vo;
+    }
+
+    // ==================================================================
+    // 2、风险等级分布与整体占比
+    // ==================================================================
+
+    /**
+     * 九防各项的风险等级分布,按高危数量降序排列。
+     */
+    private List<ItemRiskDistribution> buildItemRiskDistributions(Context ctx) {
+        List<ItemRiskDistribution> list = new ArrayList<>();
+        for (Map.Entry<String, List<NineRiskAssessFlatBO>> entry : ctx.byItem.entrySet()) {
+            List<NineRiskAssessFlatBO> rows = entry.getValue();
+            ItemRiskDistribution vo = new ItemRiskDistribution();
+            vo.setItemCode(entry.getKey());
+            vo.setItemName(NineRiskItemEnum.nameOf(entry.getKey()));
+
+            int high = 0, middle = 0, low = 0, unknown = 0;
+            for (NineRiskAssessFlatBO row : rows) {
+                switch (RiskLevelEnum.parse(row.getRiskLevel())) {
+                    case HIGH:
+                        high++;
+                        break;
+                    case MIDDLE:
+                        middle++;
+                        break;
+                    case LOW:
+                        low++;
+                        break;
+                    default:
+                        unknown++;
+                }
+            }
+            vo.setHighCount(high);
+            vo.setMiddleCount(middle);
+            vo.setLowCount(low);
+            vo.setUnknownCount(unknown);
+            vo.setTotalCount(rows.size());
+            vo.setHighRatio(ratio(high, rows.size()));
+            list.add(vo);
+        }
+        // 按高危人次降序,高危相同时按总量降序
+        list.sort(Comparator.comparing(ItemRiskDistribution::getHighCount).reversed()
+                .thenComparing(Comparator.comparing(ItemRiskDistribution::getTotalCount).reversed()));
+        return list;
+    }
+
+    private List<RiskLevelRatio> buildRiskLevelRatios(Context ctx) {
+        int total = ctx.all.size();
+        List<RiskLevelRatio> list = new ArrayList<>();
+        for (RiskLevelEnum level : RiskLevelEnum.values()) {
+            int count = ctx.riskLevelCount.getOrDefault(level, 0);
+            RiskLevelRatio vo = new RiskLevelRatio();
+            vo.setRiskLevel(level.getCode());
+            vo.setRiskLevelName(level.getName());
+            vo.setCount(count);
+            vo.setRatio(ratio(count, total));
+            list.add(vo);
+        }
+        return list;
+    }
+
+    // ==================================================================
+    // 3、风险等级月度趋势(近 N 个月)
+    // ==================================================================
+
+    private List<MonthlyTrend> buildMonthlyTrends(List<NineRiskAssessFlatBO> flatList, int trendMonths) {
+        // 先按月份初始化,保证没有数据的月份也返回 0,前端折线图不断点
+        YearMonth current = YearMonth.now();
+        Map<String, MonthlyTrend> monthMap = new LinkedHashMap<>();
+        for (int i = trendMonths - 1; i >= 0; i--) {
+            YearMonth ym = current.minusMonths(i);
+            MonthlyTrend vo = new MonthlyTrend();
+            vo.setMonth(ym.format(MONTH_FORMATTER));
+            vo.setHighCount(0);
+            vo.setMiddleCount(0);
+            vo.setLowCount(0);
+            vo.setUnknownCount(0);
+            vo.setTotalCount(0);
+            monthMap.put(vo.getMonth(), vo);
+        }
+
+        for (NineRiskAssessFlatBO row : flatList) {
+            LocalDate date = row.getAssessDate();
+            if (date == null) {
+                continue;
+            }
+            MonthlyTrend vo = monthMap.get(YearMonth.from(date).format(MONTH_FORMATTER));
+            if (vo == null) {
+                // 落在统计窗口之外
+                continue;
+            }
+            switch (RiskLevelEnum.parse(row.getRiskLevel())) {
+                case HIGH:
+                    vo.setHighCount(vo.getHighCount() + 1);
+                    break;
+                case MIDDLE:
+                    vo.setMiddleCount(vo.getMiddleCount() + 1);
+                    break;
+                case LOW:
+                    vo.setLowCount(vo.getLowCount() + 1);
+                    break;
+                default:
+                    vo.setUnknownCount(vo.getUnknownCount() + 1);
+            }
+            vo.setTotalCount(vo.getTotalCount() + 1);
+        }
+        monthMap.values().forEach(vo -> vo.setHighRatio(ratio(vo.getHighCount(), vo.getTotalCount())));
+        return new ArrayList<>(monthMap.values());
+    }
+
+    // ==================================================================
+    // 4、九防 × 楼栋 高危人数热力图
+    // ==================================================================
+
+    private HeatMap buildHeatMap(Context ctx) {
+        // key = buildId + "#" + itemCode,value = 该格子内去重后的长者集合
+        Map<String, Set<Long>> cellElderMap = new LinkedHashMap<>();
+        Map<Long, String> buildNameMap = new LinkedHashMap<>();
+
+        for (NineRiskAssessFlatBO row : ctx.highList) {
+            Long buildId = row.getBuildId();
+            if (buildId == null) {
+                continue;
+            }
+            buildNameMap.putIfAbsent(buildId, row.getBuildName());
+            cellElderMap.computeIfAbsent(buildId + "#" + row.getItemCode(), k -> new HashSet<>())
+                    .add(row.getElderId());
+        }
+
+        List<HeatMapAxis> itemAxis = Arrays.stream(NineRiskItemEnum.values())
+                .map(item -> {
+                    HeatMapAxis axis = new HeatMapAxis();
+                    axis.setCode(item.getCode());
+                    axis.setName(item.getName());
+                    return axis;
+                }).collect(Collectors.toList());
+
+        List<HeatMapAxis> buildAxis = buildNameMap.entrySet().stream()
+                .map(entry -> {
+                    HeatMapAxis axis = new HeatMapAxis();
+                    axis.setCode(String.valueOf(entry.getKey()));
+                    axis.setName(entry.getValue());
+                    return axis;
+                }).collect(Collectors.toList());
+
+        // 生成完整矩阵,缺失格子补 0
+        List<HeatMapCell> cells = new ArrayList<>();
+        int maxValue = 0;
+        for (Map.Entry<Long, String> build : buildNameMap.entrySet()) {
+            for (NineRiskItemEnum item : NineRiskItemEnum.values()) {
+                Set<Long> elders = cellElderMap.get(build.getKey() + "#" + item.getCode());
+                int count = elders == null ? 0 : elders.size();
+                maxValue = Math.max(maxValue, count);
+
+                HeatMapCell cell = new HeatMapCell();
+                cell.setBuildId(build.getKey());
+                cell.setBuildName(build.getValue());
+                cell.setItemCode(item.getCode());
+                cell.setItemName(item.getName());
+                cell.setHighRiskElderCount(count);
+                cells.add(cell);
+            }
+        }
+
+        HeatMap heatMap = new HeatMap();
+        heatMap.setItems(itemAxis);
+        heatMap.setBuilds(buildAxis);
+        heatMap.setCells(cells);
+        heatMap.setMaxValue(maxValue);
+        return heatMap;
+    }
+
+    // ==================================================================
+    // 5、存在高风险的长者列表
+    // ==================================================================
+
+    private List<HighRiskElder> buildHighRiskElders(Context ctx) {
+        return ctx.elderHighRiskItems.entrySet().stream()
+                .map(entry -> {
+                    NineRiskAssessFlatBO profile = ctx.elderProfileMap.get(entry.getKey());
+                    HighRiskElder vo = new HighRiskElder();
+                    vo.setElderId(entry.getKey());
+                    if (profile != null) {
+                        vo.setElderName(profile.getElderName());
+                        vo.setElderSex(profile.getElderSex());
+                        vo.setElderAge(profile.getElderAge());
+                        vo.setNurseLevelId(profile.getNurseLevelId());
+                        vo.setNurseLevelName(profile.getNurseLevelName());
+                    }
+                    vo.setHighRiskCount(entry.getValue().size());
+                    return vo;
+                })
+                // 高风险项个数降序
+                .sorted(Comparator.comparing(HighRiskElder::getHighRiskCount).reversed())
+                .collect(Collectors.toList());
+    }
+
+    // ==================================================================
+    // 6、九防评估得分分布
+    // ==================================================================
+
+    private List<ItemScoreDistribution> buildItemScoreDistributions(Context ctx) {
+        List<ItemScoreDistribution> list = new ArrayList<>();
+        for (Map.Entry<String, List<NineRiskAssessFlatBO>> entry : ctx.byItem.entrySet()) {
+            ItemScoreDistribution vo = new ItemScoreDistribution();
+            vo.setItemCode(entry.getKey());
+            vo.setItemName(NineRiskItemEnum.nameOf(entry.getKey()));
+
+            List<BigDecimal> scores = entry.getValue().stream()
+                    .map(NineRiskAssessFlatBO::getAssessScore)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+
+            vo.setCount(scores.size());
+            vo.setBuckets(buildScoreBuckets(scores));
+            if (scores.isEmpty()) {
+                vo.setAvgScore(BigDecimal.ZERO);
+                vo.setMinScore(BigDecimal.ZERO);
+                vo.setMaxScore(BigDecimal.ZERO);
+            } else {
+                BigDecimal sum = scores.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+                vo.setAvgScore(sum.divide(BigDecimal.valueOf(scores.size()), 2, RoundingMode.HALF_UP));
+                vo.setMinScore(scores.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO));
+                vo.setMaxScore(scores.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO));
+            }
+            list.add(vo);
+        }
+        return list;
+    }
+
+    private List<ScoreBucket> buildScoreBuckets(List<BigDecimal> scores) {
+        List<ScoreBucket> buckets = new ArrayList<>();
+        for (int i = 0; i < SCORE_BUCKET_BOUNDS.length - 1; i++) {
+            ScoreBucket bucket = new ScoreBucket();
+            int min = SCORE_BUCKET_BOUNDS[i];
+            int max = SCORE_BUCKET_BOUNDS[i + 1];
+            bucket.setMin(min);
+            bucket.setMax(max);
+            bucket.setRange(min + "-" + max);
+            bucket.setCount(0);
+            buckets.add(bucket);
+        }
+        int lastIndex = buckets.size() - 1;
+        for (BigDecimal score : scores) {
+            double value = score.doubleValue();
+            for (int i = 0; i < buckets.size(); i++) {
+                ScoreBucket bucket = buckets.get(i);
+                // 最后一段闭区间,其余左闭右开;超出上界的统一归入最后一段
+                boolean matched = i == lastIndex
+                        ? value >= bucket.getMin()
+                        : value >= bucket.getMin() && value < bucket.getMax();
+                if (matched) {
+                    bucket.setCount(bucket.getCount() + 1);
+                    break;
+                }
+            }
+        }
+        return buckets;
+    }
+
+    // ==================================================================
+    // 7、知情书签署率
+    // ==================================================================
+
+    private DisclosureSignRate buildDisclosureSignRate(Map<String, Object> stat) {
+        int total = toInt(stat == null ? null : stat.get("total"));
+        int signed = toInt(stat == null ? null : stat.get("signedCount"));
+        DisclosureSignRate vo = new DisclosureSignRate();
+        vo.setTotal(total);
+        vo.setSignedCount(signed);
+        vo.setUnsignedCount(Math.max(total - signed, 0));
+        vo.setSignRate(ratio(signed, total));
+        return vo;
+    }
+
+    // ==================================================================
+    // 8、各护理等级「高风险长者人数」(基于第 5 点的长者维度数据分组)
+    // ==================================================================
+
+    private List<NurseLevelHighRiskCount> buildNurseLevelElderCounts(Context ctx) {
+        Map<String, NurseLevelHighRiskCount> map = new LinkedHashMap<>();
+        int total = 0;
+        for (Long elderId : ctx.elderHighRiskItems.keySet()) {
+            NineRiskAssessFlatBO profile = ctx.elderProfileMap.get(elderId);
+            NurseLevelHighRiskCount vo = obtainNurseLevelBucket(map, profile);
+            vo.setCount(vo.getCount() + 1);
+            total++;
+        }
+        return finishNurseLevelCounts(map, total);
+    }
+
+    // ==================================================================
+    // 9、各护理等级下、各九防项「风险程度为高的记录数」
+    // ==================================================================
+
+    /**
+     * 按「护理等级 × 九防项」二维交叉统计高风险记录数。
+     *
+     * <p>每个护理等级下都会输出全部九防项,没有高风险记录的项补 0,
+     * 便于前端直接渲染成矩阵或堆叠柱状图而无需再做补位。</p>
+     */
+    private List<NurseLevelItemHighRiskCount> buildNurseLevelRecordCounts(Context ctx) {
+        // 护理等级 -> (九防项编码 -> 高风险记录数)
+        Map<String, Map<String, Integer>> levelItemCountMap = new LinkedHashMap<>();
+        // 护理等级 -> 等级基本信息(借用任意一条明细承载 id 与名称)
+        Map<String, NineRiskAssessFlatBO> levelProfileMap = new LinkedHashMap<>();
+        for (NineRiskAssessFlatBO row : ctx.highList) {
+            String levelKey = buildNurseLevelKey(row);
+            levelProfileMap.putIfAbsent(levelKey, row);
+            levelItemCountMap.computeIfAbsent(levelKey, k -> new LinkedHashMap<>())
+                    .merge(row.getItemCode(), 1, Integer::sum);
+        }
+
+        int grandTotal = ctx.highList.size();
+        List<NurseLevelItemHighRiskCount> result = new ArrayList<>(levelItemCountMap.size());
+        for (Map.Entry<String, Map<String, Integer>> entry : levelItemCountMap.entrySet()) {
+            NineRiskAssessFlatBO profile = levelProfileMap.get(entry.getKey());
+            Map<String, Integer> itemCountMap = entry.getValue();
+
+            NurseLevelItemHighRiskCount levelVO = new NurseLevelItemHighRiskCount();
+            levelVO.setNurseLevelId(profile == null ? null : profile.getNurseLevelId());
+            levelVO.setNurseLevelName(resolveNurseLevelName(profile));
+
+            // 遍历全部九防项,缺失的补 0,保证各等级返回的项一致
+            List<ItemHighRiskCount> items = new ArrayList<>(NineRiskItemEnum.values().length);
+            int levelTotal = 0;
+            for (NineRiskItemEnum item : NineRiskItemEnum.values()) {
+                int count = itemCountMap.getOrDefault(item.getCode(), 0);
+                levelTotal += count;
+
+                ItemHighRiskCount itemVO = new ItemHighRiskCount();
+                itemVO.setItemCode(item.getCode());
+                itemVO.setItemName(item.getName());
+                itemVO.setCount(count);
+                items.add(itemVO);
+            }
+            // 项占比的分母是所在护理等级的合计数
+            for (ItemHighRiskCount itemVO : items) {
+                itemVO.setRatio(ratio(itemVO.getCount(), levelTotal));
+            }
+            items.sort(Comparator.comparing(ItemHighRiskCount::getCount).reversed());
+
+            levelVO.setItems(items);
+            levelVO.setTotalCount(levelTotal);
+            levelVO.setRatio(ratio(levelTotal, grandTotal));
+            result.add(levelVO);
+        }
+        result.sort(Comparator.comparing(NurseLevelItemHighRiskCount::getTotalCount).reversed());
+        return result;
+    }
+
+    /**
+     * 生成护理等级的分组键:优先用 id,id 为空时退化为名称,避免不同的空等级被合并。
+     */
+    private static String buildNurseLevelKey(NineRiskAssessFlatBO profile) {
+        Long levelId = profile == null ? null : profile.getNurseLevelId();
+        return levelId == null ? "null#" + resolveNurseLevelName(profile) : String.valueOf(levelId);
+    }
+
+    private static String resolveNurseLevelName(NineRiskAssessFlatBO profile) {
+        return profile == null || !isNotBlank(profile.getNurseLevelName())
+                ? "未设置" : profile.getNurseLevelName();
+    }
+
+    private NurseLevelHighRiskCount obtainNurseLevelBucket(Map<String, NurseLevelHighRiskCount> map,
+                                                           NineRiskAssessFlatBO profile) {
+        Long levelId = profile == null ? null : profile.getNurseLevelId();
+        String levelName = resolveNurseLevelName(profile);
+        return map.computeIfAbsent(buildNurseLevelKey(profile), k -> {
+            NurseLevelHighRiskCount vo = new NurseLevelHighRiskCount();
+            vo.setNurseLevelId(levelId);
+            vo.setNurseLevelName(levelName);
+            vo.setCount(0);
+            return vo;
+        });
+    }
+
+    private List<NurseLevelHighRiskCount> finishNurseLevelCounts(Map<String, NurseLevelHighRiskCount> map,
+                                                                 int total) {
+        List<NurseLevelHighRiskCount> list = new ArrayList<>(map.values());
+        list.forEach(vo -> vo.setRatio(ratio(vo.getCount(), total)));
+        list.sort(Comparator.comparing(NurseLevelHighRiskCount::getCount).reversed());
+        return list;
+    }
+
+    // ==================================================================
+    // 10、评估人统计
+    // ==================================================================
+
+    private List<AssessorStat> buildAssessorStats(Context ctx) {
+        // assessor -> 评估过的长者集合 / 高风险长者集合 / 记录数
+        Map<String, Set<Long>> assessedMap = new LinkedHashMap<>();
+        Map<String, Set<Long>> highRiskMap = new LinkedHashMap<>();
+        Map<String, Integer> recordCountMap = new LinkedHashMap<>();
+
+        for (NineRiskAssessFlatBO row : ctx.all) {
+            if (!isNotBlank(row.getAssessor())) {
+                continue;
+            }
+            String assessor = row.getAssessor().trim();
+            recordCountMap.merge(assessor, 1, Integer::sum);
+            if (row.getElderId() != null) {
+                assessedMap.computeIfAbsent(assessor, k -> new HashSet<>()).add(row.getElderId());
+                // 同一长者被同一评估人在多张九防表判为高风险时,只计 1 个
+                if (RiskLevelEnum.isHigh(row.getRiskLevel())) {
+                    highRiskMap.computeIfAbsent(assessor, k -> new HashSet<>()).add(row.getElderId());
+                }
+            }
+        }
+
+        return recordCountMap.entrySet().stream().map(entry -> {
+            String assessor = entry.getKey();
+            AssessorStat vo = new AssessorStat();
+            vo.setAssessor(assessor);
+            vo.setAssessRecordCount(entry.getValue());
+            vo.setAssessedElderCount(assessedMap.getOrDefault(assessor, Collections.emptySet()).size());
+            vo.setHighRiskElderCount(highRiskMap.getOrDefault(assessor, Collections.emptySet()).size());
+            return vo;
+        }).sorted(Comparator.comparing(AssessorStat::getAssessedElderCount).reversed())
+                .collect(Collectors.toList());
+    }
+
+    // ==================================================================
+    // 11、九防高危占比
+    // ==================================================================
+
+    private HighRiskRatio buildHighRiskRatio(Context ctx) {
+        HighRiskRatio vo = new HighRiskRatio();
+        int elderCount = ctx.elderProfileMap.size();
+        int highElderCount = ctx.elderHighRiskItems.size();
+        int recordTotal = ctx.all.size();
+        int highRecordCount = ctx.highList.size();
+
+        vo.setAssessedElderCount(elderCount);
+        vo.setHighRiskElderCount(highElderCount);
+        vo.setHighRiskElderRatio(ratio(highElderCount, elderCount));
+        vo.setAssessRecordTotal(recordTotal);
+        vo.setHighRiskRecordCount(highRecordCount);
+        vo.setHighRiskRecordRatio(ratio(highRecordCount, recordTotal));
+        return vo;
+    }
+
+    // ==================================================================
+    // 12、2 项及以上高风险列表
+    // ==================================================================
+
+    private List<MultiHighRiskElder> buildMultiHighRiskElders(Context ctx, int threshold) {
+        List<MultiHighRiskElder> list = new ArrayList<>();
+        for (Map.Entry<Long, Set<String>> entry : ctx.elderHighRiskItems.entrySet()) {
+            Set<String> items = entry.getValue();
+            if (items.size() < threshold) {
+                continue;
+            }
+            Long elderId = entry.getKey();
+            NineRiskAssessFlatBO profile = ctx.elderProfileMap.get(elderId);
+
+            MultiHighRiskElder vo = new MultiHighRiskElder();
+            vo.setElderId(elderId);
+            if (profile != null) {
+                vo.setContractNumber(profile.getContractNumber());
+                vo.setElderName(profile.getElderName());
+                vo.setElderSex(profile.getElderSex());
+                vo.setElderAge(profile.getElderAge());
+                vo.setBuildName(profile.getBuildName());
+                vo.setFloorName(profile.getFloorName());
+                vo.setBedName(profile.getBedName());
+                vo.setNurseLevelName(profile.getNurseLevelName());
+            }
+            vo.setHighRiskCount(items.size());
+            vo.setHighRiskItems(items.stream().map(NineRiskItemEnum::nameOf)
+                    .collect(Collectors.joining("、")));
+            vo.setAssessor(String.join("、",
+                    ctx.elderHighRiskAssessors.getOrDefault(elderId, Collections.emptySet())));
+            vo.setAssessDate(ctx.elderLatestHighRiskDate.get(elderId));
+            list.add(vo);
+        }
+        list.sort(Comparator.comparing(MultiHighRiskElder::getHighRiskCount).reversed());
+        return list;
+    }
+
+    // ==================================================================
+    // 工具方法
+    // ==================================================================
+
+    /**
+     * 计算百分比,保留 2 位小数;分母为 0 时返回 0。
+     */
+    private static BigDecimal ratio(int part, int total) {
+        if (total <= 0) {
+            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+        }
+        return BigDecimal.valueOf(part)
+                .multiply(HUNDRED)
+                .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
+    }
+
+    private static boolean isNotBlank(String text) {
+        return text != null && !text.trim().isEmpty();
+    }
+
+    private static int toInt(Object value) {
+        if (value == null) {
+            return 0;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        try {
+            return Integer.parseInt(value.toString());
+        } catch (NumberFormatException e) {
+            log.warn("[toInt] 无法解析数值: {}", value);
+            return 0;
+        }
+    }
+}

+ 130 - 0
yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/NineRiskBoardMapper.xml

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.system.dal.mysql.biz.NineRiskBoardMapper">
+
+    <!-- 机构过滤:不传 tenantIds 时交由框架的多租户插件处理 -->
+    <sql id="tenantFilter">
+        <if test="reqVO.tenantIds != null and reqVO.tenantIds.length > 0">
+            AND t.tenant_id IN
+            <foreach item="id" collection="reqVO.tenantIds" open="(" separator="," close=")">
+                #{id}
+            </foreach>
+        </if>
+    </sql>
+
+    <!--
+        九防评估扁平化明细。
+        9 张九防表结构一致(elder_id / assessor / assess_date / risk_level / assess_score / tenant_id),
+        因此可用 UNION ALL 合并为一张「虚拟宽表」,再统一关联 elderly_info 补充长者档案,
+        楼栋、楼层过滤与所有看板指标都基于这一次查询完成。
+    -->
+    <select id="selectAssessFlatList"
+            resultType="cn.iocoder.yudao.module.system.dal.mysql.biz.bo.NineRiskAssessFlatBO">
+        SELECT
+            a.assessId,
+            a.itemCode,
+            a.elderId,
+            a.assessor,
+            a.assessDate,
+            a.riskLevel,
+            a.assessScore,
+            ei.elder_name        AS elderName,
+            ei.elder_sex         AS elderSex,
+            ei.elder_age         AS elderAge,
+            ei.nurse_level_id    AS nurseLevelId,
+            ei.nurse_level_name  AS nurseLevelName,
+            ei.build_id          AS buildId,
+            ei.build_name        AS buildName,
+            ei.floor_id          AS floorId,
+            ei.floor_name        AS floorName,
+            ei.bed_name          AS bedName,
+            ei.contract_number   AS contractNumber
+        FROM (
+            <include refid="unionAllAssess"/>
+        ) a
+        INNER JOIN elderly_info ei ON ei.id = a.elderId AND ei.deleted = 0
+        <where>
+            <!-- 楼栋过滤,不传即全部 -->
+            <if test="reqVO.buildId != null">
+                AND ei.build_id = #{reqVO.buildId}
+            </if>
+            <!-- 楼层过滤,不传即全部 -->
+            <if test="reqVO.floorId != null">
+                AND ei.floor_id = #{reqVO.floorId}
+            </if>
+            <!-- 仅统计在住长者 -->
+            AND ei.in_status = 1
+            <if test="trendStartDay != null">
+                AND (a.assessDate IS NULL OR a.assessDate &gt;= #{trendStartDay})
+            </if>
+        </where>
+        <!--
+            同一长者在同一张九防表可能有多条记录,这里按「长者 + 九防项 + 评估日期 + 主键」排序,
+            便于上层按顺序取到每个「长者 + 九防项」的最新一条;月度趋势仍使用全量历史数据
+        -->
+        ORDER BY a.elderId, a.itemCode, a.assessDate, a.assessId
+    </select>
+
+    <!-- 9 张九防评估表的 UNION ALL;itemCode 与 NineRiskItemEnum 的 code 一一对应 -->
+    <sql id="unionAllAssess">
+            SELECT t.id AS assessId, 'fallDown' AS itemCode, t.elder_id AS elderId, t.assessor AS assessor,
+                   t.assess_date AS assessDate, t.risk_level AS riskLevel, t.assess_score AS assessScore
+            FROM elderly_fall_down t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'pressureSores', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_pressure_sores t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'fallPreventionMeasures', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_fall_prevention_measures t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'empyrosis', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_empyrosis t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'wanderAway', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_wander_away t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'suicideRisk', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_assess_suicide_risk t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'antiEntertainment', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_anti_entertainment t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'simpleMentalState', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_assess_simple_mental_state t WHERE 1 = 1 <include refid="tenantFilter"/>
+        UNION ALL
+            SELECT t.id, 'asphyxiationByChoking', t.elder_id, t.assessor,
+                   t.assess_date, t.risk_level, t.assess_score
+            FROM elderly_asphyxiation_by_choking t WHERE 1 = 1 <include refid="tenantFilter"/>
+    </sql>
+
+    <!--
+        知情书统计。风险知情书不属于九防,单独统计;
+        签署与否以 second_elder_sign 是否为空判断。
+    -->
+    <select id="selectDisclosureStat" resultType="java.util.Map">
+        SELECT
+            COUNT(*) AS total,
+            SUM(CASE WHEN t.second_elder_sign IS NOT NULL AND t.second_elder_sign != ''
+                     THEN 1 ELSE 0 END) AS signedCount
+        FROM elderly_risk_disclosure_statement t
+        INNER JOIN elderly_info ei ON ei.id = t.elder_id AND ei.deleted = 0
+        <where>
+            <if test="reqVO.buildId != null">
+                AND ei.build_id = #{reqVO.buildId}
+            </if>
+            <if test="reqVO.floorId != null">
+                AND ei.floor_id = #{reqVO.floorId}
+            </if>
+            AND ei.in_status = 1
+            <include refid="tenantFilter"/>
+        </where>
+    </select>
+
+</mapper>

+ 0 - 1
yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/RefundSettlementOrderItemMapper.xml

@@ -19,7 +19,6 @@
             <foreach item="elderId" collection="elderIds" open="(" separator="," close=")">
                 #{elderId}
             </foreach>
-            AND rso.status = 1
         GROUP BY
             rso.elder_id, eei.type
     </select>