ソースを参照

feat(大屏 3.3): 新增 BigScreen 聚合统计 - 6张业务卡片+2饼图+1折线图+1柱状图+1预警走马灯 (VO/Service/Controller/SQL菜单)

yangjingjing 2 日 前
コミット
bf85e81129

+ 57 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/BigScreenController.java

@@ -0,0 +1,57 @@
+package org.dromara.risk.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.risk.domain.vo.BigScreenCardVo;
+import org.dromara.risk.domain.vo.BigScreenChartVo;
+import org.dromara.risk.domain.vo.BigScreenWarningStreamVo;
+import org.dromara.risk.service.IBigScreenService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 3.3 大屏 - 聚合统计接口
+ * <p>
+ * 提供 6 张业务卡片 + 5 个图表 + 1 个走马灯的端点
+ * </p>
+ *
+ * @author ruoyi
+ */
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/risk/bigScreen")
+public class BigScreenController {
+
+    private final IBigScreenService bigScreenService;
+
+    /**
+     * ① 6 张业务数据卡片统计
+     */
+    @GetMapping("/cards")
+    public R<BigScreenCardVo> getCardStats() {
+        return R.ok(bigScreenService.getCardStats());
+    }
+
+    /**
+     * ② ③ ④ 图表数据(2 饼图 + 1 折线图 + 1 柱状图)
+     */
+    @GetMapping("/charts")
+    public R<BigScreenChartVo> getChartData() {
+        return R.ok(bigScreenService.getChartData());
+    }
+
+    /**
+     * ⑤ 最新预警动态走马灯
+     *
+     * @param limit 条数限制,默认 10,最大 50
+     */
+    @GetMapping("/warnings")
+    public R<List<BigScreenWarningStreamVo>> getWarningStream(
+            @RequestParam(defaultValue = "10") int limit) {
+        return R.ok(bigScreenService.getWarningStream(limit));
+    }
+}

+ 75 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/BigScreenCardVo.java

@@ -0,0 +1,75 @@
+package org.dromara.risk.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 3.3 大屏 - 6 张业务数据卡片
+ * <p>
+ * 包含:
+ * <ol>
+ *   <li>安全隐患卡片: 隐患总数 / 已闭环 / 待处理</li>
+ *   <li>危险源卡片: 危险源总数 / 本月新增</li>
+ *   <li>作业上报卡片: 上报总数 / 进行中</li>
+ *   <li>审计工作卡片: 人员异动总数 / 离职 / 调任</li>
+ *   <li>特种设备卡片: 设备总数 / 运行中 / 维护中</li>
+ *   <li>访厂审核卡片: 访厂总数 / 通过 / 不通过</li>
+ * </ol>
+ * </p>
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = BigScreenCardVo.class)
+public class BigScreenCardVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    // ====== 1. 安全隐患卡片 ======
+    /** 隐患总数(全量历史累计) */
+    private Integer dangerTotal;
+    /** 已闭环处理(绿色) */
+    private Integer dangerHandled;
+    /** 待处理(红色高亮) */
+    private Integer dangerPending;
+
+    // ====== 2. 危险源卡片 ======
+    /** 危险源总数(全量登记在册) */
+    private Integer dangerSourceTotal;
+    /** 本月新增(自然月内,红色高亮) */
+    private Integer dangerSourceMonthNew;
+
+    // ====== 3. 作业上报卡片 ======
+    /** 作业上报总条数(历史累计) */
+    private Integer workReportTotal;
+    /** 作业进行中(品牌主色) */
+    private Integer workReportOngoing;
+
+    // ====== 4. 审计工作卡片 ======
+    /** 人员异动总人次(指定周期) */
+    private Integer personnelChangeTotal;
+    /** 离职人员(红色高亮) */
+    private Integer personnelLeave;
+    /** 调任岗位人员(品牌主色) */
+    private Integer personnelTransfer;
+
+    // ====== 5. 特种设备卡片 ======
+    /** 设备总数(全量登记) */
+    private Integer specialEquipmentTotal;
+    /** 运行中(绿色) */
+    private Integer specialEquipmentRunning;
+    /** 维护保养中(黄色) */
+    private Integer specialEquipmentMaintaining;
+
+    // ====== 6. 访厂审核卡片 ======
+    /** 访厂总次数(历史累计) */
+    private Integer factoryVisitTotal;
+    /** 审核通过(绿色) */
+    private Integer factoryVisitPassed;
+    /** 审核不通过(红色高亮) */
+    private Integer factoryVisitRejected;
+}

+ 54 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/BigScreenChartVo.java

@@ -0,0 +1,54 @@
+package org.dromara.risk.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 3.3 大屏 - 5 个图表数据
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = BigScreenChartVo.class)
+public class BigScreenChartVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    // ====== ② 饼图 1: 人员岗位占比 ======
+    /** 饼图数据项 { name, value } */
+    private List<NameValue> personnelDistribution;
+
+    // ====== ② 饼图 2: 事故类型分布 ======
+    /** 饼图数据项 { name, value } */
+    private List<NameValue> accidentDistribution;
+
+    // ====== ③ 折线图: 巡检/抽检近 30 天趋势 ======
+    /** x 轴日期 ['2026-05-22', ...] */
+    private List<String> trendDates;
+    /** 品控巡检(纵轴 1) */
+    private List<Integer> qualityInspectionCount;
+    /** 风控抽检(纵轴 2) */
+    private List<Integer> riskInspectionCount;
+
+    // ====== ④ 柱状图: 客诉月度统计 ======
+    /** x 轴月份 ['2025-07', '2025-08', ...] */
+    private List<String> complaintMonths;
+    /** 当月客诉数 */
+    private List<Integer> complaintCounts;
+
+    /**
+     * 通用名值对 (饼图项)
+     */
+    @Data
+    public static class NameValue implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+        private String name;
+        private Integer value;
+    }
+}

+ 44 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/BigScreenWarningStreamVo.java

@@ -0,0 +1,44 @@
+package org.dromara.risk.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 3.3 大屏 - 最新预警动态走马灯单项
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = BigScreenWarningStreamVo.class)
+public class BigScreenWarningStreamVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 警示编号 */
+    private String warningCode;
+
+    /** 标题 */
+    private String title;
+
+    /** 警示类型 */
+    private String warningType;
+
+    /** 警示等级(决定状态点颜色) */
+    private String warningLevel;
+
+    /** 状态点颜色(前端映射: 红/黄/蓝) */
+    private String levelColor;
+
+    /** 发布时间 */
+    private String releaseTime;
+
+    /** 关联门店/位置 */
+    private String storeName;
+}

+ 35 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IBigScreenService.java

@@ -0,0 +1,35 @@
+package org.dromara.risk.service;
+
+import org.dromara.risk.domain.vo.BigScreenCardVo;
+import org.dromara.risk.domain.vo.BigScreenChartVo;
+import org.dromara.risk.domain.vo.BigScreenWarningStreamVo;
+
+import java.util.List;
+
+/**
+ * 3.3 大屏 - 统计 Service
+ * <p>
+ * 提供 6 张业务卡片 + 5 个图表 + 1 个走马灯的聚合统计
+ * </p>
+ *
+ * @author ruoyi
+ */
+public interface IBigScreenService {
+
+    /**
+     * 6 张业务数据卡片统计
+     */
+    BigScreenCardVo getCardStats();
+
+    /**
+     * 5 个图表数据
+     */
+    BigScreenChartVo getChartData();
+
+    /**
+     * 最新预警动态走马灯(取最新 N 条)
+     *
+     * @param limit 条数限制
+     */
+    List<BigScreenWarningStreamVo> getWarningStream(int limit);
+}

+ 415 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/BigScreenServiceImpl.java

@@ -0,0 +1,415 @@
+package org.dromara.risk.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.risk.domain.*;
+import org.dromara.risk.domain.vo.BigScreenCardVo;
+import org.dromara.risk.domain.vo.BigScreenChartVo;
+import org.dromara.risk.domain.vo.BigScreenWarningStreamVo;
+import org.dromara.risk.mapper.*;
+import org.dromara.risk.service.IBigScreenService;
+import org.springframework.stereotype.Service;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * 3.3 大屏 - 聚合统计 Service 实现
+ * <p>
+ * 数据来源:
+ * <ul>
+ *   <li>隐患: risk_hidden_danger</li>
+ *   <li>危险源: risk_danger_source</li>
+ *   <li>作业上报: risk_work_report</li>
+ *   <li>人员异动: risk_personnel_change / 人员: risk_personnel</li>
+ *   <li>特种设备: risk_special_equipment</li>
+ *   <li>访厂审核: risk_factory_visit</li>
+ *   <li>事故: risk_accident</li>
+ *   <li>预警: risk_warning</li>
+ *   <li>品控巡检: risk_inspection_task (biz_type='quality')</li>
+ *   <li>风控抽检: risk_supervision_inspection / 监督抽检记录</li>
+ *   <li>客诉: risk_supervision_complaint</li>
+ * </ul>
+ * </p>
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class BigScreenServiceImpl implements IBigScreenService {
+
+    private final RiskHiddenDangerMapper hiddenDangerMapper;
+    private final RiskDangerSourceMapper dangerSourceMapper;
+    private final RiskWorkReportMapper workReportMapper;
+    private final RiskPersonnelChangeMapper personnelChangeMapper;
+    private final RiskPersonnelMapper personnelMapper;
+    private final RiskSpecialEquipmentMapper specialEquipmentMapper;
+    private final RiskFactoryVisitMapper factoryVisitMapper;
+    private final RiskAccidentMapper accidentMapper;
+    private final RiskWarningMapper warningMapper;
+    private final RiskInspectionTaskMapper inspectionTaskMapper;
+    private final RiskSupervisionInspectionMapper supervisionInspectionMapper;
+    private final RiskSupervisionComplaintMapper supervisionComplaintMapper;
+
+    // ============================================================
+    // ① 6 张业务数据卡片
+    // ============================================================
+
+    @Override
+    public BigScreenCardVo getCardStats() {
+        BigScreenCardVo vo = new BigScreenCardVo();
+
+        // ====== 1. 安全隐患卡片 ======
+        // 隐患总数 / 已闭环 / 待处理
+        try {
+            int dangerTotal = safeCount(hiddenDangerMapper);
+            int dangerHandled = safeCount(hiddenDangerMapper, "handle_status", "ne", "待处理");
+            int dangerPending = safeCount(hiddenDangerMapper, "handle_status", "eq", "待处理");
+            vo.setDangerTotal(dangerTotal);
+            vo.setDangerHandled(dangerHandled);
+            vo.setDangerPending(dangerPending);
+        } catch (Exception e) {
+            log.warn("[BigScreen] 隐患统计失败", e);
+        }
+
+        // ====== 2. 危险源卡片 ======
+        // 危险源总数 / 本月新增
+        try {
+            int total = safeCount(dangerSourceMapper);
+            vo.setDangerSourceTotal(total);
+            // 本月新增 = report_time 在本月 1 号至当前时间
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            Calendar c = Calendar.getInstance();
+            c.set(Calendar.DAY_OF_MONTH, 1);
+            c.set(Calendar.HOUR_OF_DAY, 0);
+            c.set(Calendar.MINUTE, 0);
+            c.set(Calendar.SECOND, 0);
+            String monthStart = sdf.format(c.getTime());
+            int monthNew = safeCount(dangerSourceMapper, "report_time", "ge", monthStart);
+            vo.setDangerSourceMonthNew(monthNew);
+        } catch (Exception e) {
+            log.warn("[BigScreen] 危险源统计失败", e);
+        }
+
+        // ====== 3. 作业上报卡片 ======
+        // 上报总条数 / 进行中(审核通过且当前时间在工期内)
+        try {
+            int total = safeCount(workReportMapper);
+            vo.setWorkReportTotal(total);
+            // 进行中: 审核通过 + 当前时间在 work_start_time ~ work_end_time 之间
+            QueryWrapper<RiskWorkReport> lqw = new QueryWrapper<>();
+            lqw.eq("audit_status", "审核通过");
+            lqw.le("work_start_time", new Date());
+            lqw.ge("work_end_time", new Date());
+            int ongoing = Math.toIntExact(workReportMapper.selectCount(lqw));
+            vo.setWorkReportOngoing(ongoing);
+        } catch (Exception e) {
+            log.warn("[BigScreen] 作业上报统计失败", e);
+        }
+
+        // ====== 4. 审计工作卡片 ======
+        // 人员异动总人次 / 离职 / 调任
+        try {
+            int total = safeCount(personnelChangeMapper);
+            vo.setPersonnelChangeTotal(total);
+            int leave = safeCount(personnelChangeMapper, "change_type", "eq", "离职");
+            int transfer = safeCount(personnelChangeMapper, "change_type", "eq", "调任");
+            vo.setPersonnelLeave(leave);
+            vo.setPersonnelTransfer(transfer);
+        } catch (Exception e) {
+            log.warn("[BigScreen] 审计工作统计失败", e);
+        }
+
+        // ====== 5. 特种设备卡片 ======
+        // 设备总数 / 运行中 / 维护保养中
+        try {
+            int total = safeCount(specialEquipmentMapper);
+            vo.setSpecialEquipmentTotal(total);
+            int running = safeCount(specialEquipmentMapper, "use_status", "eq", "在用");
+            // 维护中包含: 维护中 / 检修中 / 维修中(多状态)
+            int maintaining = safeCount(specialEquipmentMapper, "use_status", "in",
+                "维护中", "检修中", "维修中");
+            vo.setSpecialEquipmentRunning(running);
+            vo.setSpecialEquipmentMaintaining(maintaining);
+        } catch (Exception e) {
+            log.warn("[BigScreen] 特种设备统计失败", e);
+        }
+
+        // ====== 6. 访厂审核卡片 ======
+        // 访厂总次数 / 通过 / 不通过
+        try {
+            int total = safeCount(factoryVisitMapper);
+            int passed = safeCount(factoryVisitMapper, "audit_result", "eq", "通过");
+            int rejected = safeCount(factoryVisitMapper, "audit_result", "eq", "不通过");
+            vo.setFactoryVisitTotal(total);
+            vo.setFactoryVisitPassed(passed);
+            vo.setFactoryVisitRejected(rejected);
+        } catch (Exception e) {
+            log.warn("[BigScreen] 访厂审核统计失败", e);
+        }
+
+        return vo;
+    }
+
+    // ============================================================
+    // ② ③ ④ 图表数据
+    // ============================================================
+
+    @Override
+    public BigScreenChartVo getChartData() {
+        BigScreenChartVo vo = new BigScreenChartVo();
+        vo.setPersonnelDistribution(buildPersonnelDistribution());
+        vo.setAccidentDistribution(buildAccidentDistribution());
+        buildInspectionTrend(vo);
+        buildComplaintMonthly(vo);
+        return vo;
+    }
+
+    /**
+     * 人员岗位占比(按 position 字段聚合)
+     */
+    private List<BigScreenChartVo.NameValue> buildPersonnelDistribution() {
+        try {
+            // 按 position 分组统计(去重 + 仅统计未删除)
+            QueryWrapper<RiskPersonnel> lqw = new QueryWrapper<>();
+            lqw.select("position as name, COUNT(*) as value");
+            lqw.eq("del_flag", "0");
+            lqw.isNotNull("position");
+            lqw.ne("position", "");
+            lqw.groupBy("position");
+            lqw.orderByDesc("value");
+            List<Map<String, Object>> rows = personnelMapper.selectMaps(lqw);
+            List<BigScreenChartVo.NameValue> result = new ArrayList<>();
+            for (Map<String, Object> row : rows) {
+                result.add(new BigScreenChartVo.NameValue() {{
+                    setName(toStr(row.get("name")));
+                    setValue(toInt(row.get("value")));
+                }});
+            }
+            return result;
+        } catch (Exception e) {
+            log.warn("[BigScreen] 人员岗位占比统计失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 事故类型分布
+     */
+    private List<BigScreenChartVo.NameValue> buildAccidentDistribution() {
+        try {
+            QueryWrapper<RiskAccident> lqw = new QueryWrapper<>();
+            lqw.select("accident_type as name, COUNT(*) as value");
+            lqw.eq("del_flag", "0");
+            lqw.isNotNull("accident_type");
+            lqw.ne("accident_type", "");
+            lqw.groupBy("accident_type");
+            lqw.orderByDesc("value");
+            List<Map<String, Object>> rows = accidentMapper.selectMaps(lqw);
+            List<BigScreenChartVo.NameValue> result = new ArrayList<>();
+            for (Map<String, Object> row : rows) {
+                result.add(new BigScreenChartVo.NameValue() {{
+                    setName(toStr(row.get("name")));
+                    setValue(toInt(row.get("value")));
+                }});
+            }
+            return result;
+        } catch (Exception e) {
+            log.warn("[BigScreen] 事故类型分布统计失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 巡检/抽检近 30 天趋势
+     * - 品控巡检: risk_inspection_task 中 biz_type='quality' 且 create_time 在日期
+     * - 风控抽检: risk_supervision_inspection 中 create_time 在日期
+     */
+    private void buildInspectionTrend(BigScreenChartVo vo) {
+        List<String> dates = new ArrayList<>();
+        List<Integer> qualityCounts = new ArrayList<>();
+        List<Integer> riskCounts = new ArrayList<>();
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Calendar c = Calendar.getInstance();
+            // 从 29 天前到今天(共 30 天)
+            for (int i = 29; i >= 0; i--) {
+                Calendar day = Calendar.getInstance();
+                day.add(Calendar.DAY_OF_YEAR, -i);
+                String dayStr = sdf.format(day.getTime());
+                dates.add(dayStr);
+                // 一天的起止
+                Calendar start = (Calendar) day.clone();
+                start.set(Calendar.HOUR_OF_DAY, 0);
+                start.set(Calendar.MINUTE, 0);
+                start.set(Calendar.SECOND, 0);
+                Calendar end = (Calendar) day.clone();
+                end.set(Calendar.HOUR_OF_DAY, 23);
+                end.set(Calendar.MINUTE, 59);
+                end.set(Calendar.SECOND, 59);
+                // 品控巡检
+                QueryWrapper<RiskInspectionTask> qualityQw = new QueryWrapper<>();
+                qualityQw.eq("biz_type", "quality");
+                qualityQw.between("create_time", start.getTime(), end.getTime());
+                int qualityCount = Math.toIntExact(inspectionTaskMapper.selectCount(qualityQw));
+                qualityCounts.add(qualityCount);
+                // 风控抽检
+                QueryWrapper<RiskSupervisionInspection> riskQw = new QueryWrapper<>();
+                riskQw.between("create_time", start.getTime(), end.getTime());
+                int riskCount = Math.toIntExact(supervisionInspectionMapper.selectCount(riskQw));
+                riskCounts.add(riskCount);
+            }
+        } catch (Exception e) {
+            log.warn("[BigScreen] 巡检/抽检近 30 天趋势统计失败", e);
+        }
+        vo.setTrendDates(dates);
+        vo.setQualityInspectionCount(qualityCounts);
+        vo.setRiskInspectionCount(riskCounts);
+    }
+
+    /**
+     * 客诉月度统计(近 12 个月)
+     */
+    private void buildComplaintMonthly(BigScreenChartVo vo) {
+        List<String> months = new ArrayList<>();
+        List<Integer> counts = new ArrayList<>();
+        try {
+            SimpleDateFormat monthFmt = new SimpleDateFormat("yyyy-MM");
+            SimpleDateFormat dayFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            Calendar c = Calendar.getInstance();
+            for (int i = 11; i >= 0; i--) {
+                Calendar m = Calendar.getInstance();
+                m.add(Calendar.MONTH, -i);
+                String monthStr = monthFmt.format(m.getTime());
+                months.add(monthStr);
+                // 月初到下月初
+                Calendar start = (Calendar) m.clone();
+                start.set(Calendar.DAY_OF_MONTH, 1);
+                start.set(Calendar.HOUR_OF_DAY, 0);
+                start.set(Calendar.MINUTE, 0);
+                start.set(Calendar.SECOND, 0);
+                Calendar end = (Calendar) start.clone();
+                end.add(Calendar.MONTH, 1);
+                QueryWrapper<RiskSupervisionComplaint> lqw = new QueryWrapper<>();
+                lqw.between("create_time", dayFmt.format(start.getTime()),
+                    dayFmt.format(end.getTime()));
+                int count = Math.toIntExact(supervisionComplaintMapper.selectCount(lqw));
+                counts.add(count);
+            }
+        } catch (Exception e) {
+            log.warn("[BigScreen] 客诉月度统计失败", e);
+        }
+        vo.setComplaintMonths(months);
+        vo.setComplaintCounts(counts);
+    }
+
+    // ============================================================
+    // ⑤ 滚动动态 - 最新预警
+    // ============================================================
+
+    @Override
+    public List<BigScreenWarningStreamVo> getWarningStream(int limit) {
+        try {
+            LambdaQueryWrapper<RiskWarning> lqw = new LambdaQueryWrapper<>();
+            lqw.eq(RiskWarning::getDelFlag, "0");
+            lqw.orderByDesc(RiskWarning::getReleaseTime);
+            lqw.last("LIMIT " + Math.max(1, Math.min(limit, 50)));
+            List<RiskWarning> list = warningMapper.selectList(lqw);
+            List<BigScreenWarningStreamVo> result = new ArrayList<>();
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            for (RiskWarning w : list) {
+                BigScreenWarningStreamVo vo = new BigScreenWarningStreamVo();
+                vo.setId(w.getId());
+                vo.setWarningCode(w.getWarningCode());
+                vo.setTitle(w.getWarningTitle());
+                vo.setWarningType(w.getWarningType());
+                vo.setWarningLevel(w.getWarningLevel());
+                vo.setLevelColor(mapLevelColor(w.getWarningLevel()));
+                vo.setReleaseTime(w.getReleaseTime() == null ? "" : sdf.format(w.getReleaseTime()));
+                vo.setStoreName(w.getStoreName());
+                result.add(vo);
+            }
+            return result;
+        } catch (Exception e) {
+            log.warn("[BigScreen] 预警走马灯统计失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 预警等级 → 状态点颜色
+     * - 重大/红色: 紧急
+     * - 较大/黄色: 警告
+     * - 一般/蓝色: 提示
+     */
+    private String mapLevelColor(String level) {
+        if (level == null) return "blue";
+        if (level.contains("重大") || level.contains("高") || level.contains("紧急") || level.contains("红")) {
+            return "red";
+        }
+        if (level.contains("较大") || level.contains("中") || level.contains("黄")) {
+            return "yellow";
+        }
+        return "blue";
+    }
+
+    // ============================================================
+    // 工具方法
+    // ============================================================
+
+    /**
+     * 通用 count 工具:支持 eq/ne/in 条件
+     * <p>使用 raw type 接受任意 BaseMapper</p>
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private int safeCount(com.baomidou.mybatisplus.core.mapper.BaseMapper mapper, String field, String op, Object... vals) {
+        try {
+            QueryWrapper lqw = new QueryWrapper();
+            if (field != null && op != null && vals != null && vals.length > 0) {
+                switch (op) {
+                    case "eq" -> lqw.eq(field, vals[0]);
+                    case "ne" -> lqw.ne(field, vals[0]);
+                    case "in" -> lqw.in(field, vals);
+                    case "ge" -> lqw.ge(field, vals[0]);
+                    case "le" -> lqw.le(field, vals[0]);
+                    default -> {}
+                }
+            }
+            return Math.toIntExact(mapper.selectCount(lqw));
+        } catch (Exception e) {
+            log.warn("[BigScreen] safeCount 失败: field={}, op={}, vals={}", field, op, vals, e);
+            return 0;
+        }
+    }
+
+    /**
+     * 重载:仅 count 全部
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private int safeCount(com.baomidou.mybatisplus.core.mapper.BaseMapper mapper) {
+        try {
+            return Math.toIntExact(mapper.selectCount(new QueryWrapper()));
+        } catch (Exception e) {
+            log.warn("[BigScreen] safeCount(全量) 失败", e);
+            return 0;
+        }
+    }
+
+    private static String toStr(Object o) {
+        return o == null ? "" : o.toString();
+    }
+
+    private static int toInt(Object o) {
+        if (o == null) return 0;
+        if (o instanceof Number) return ((Number) o).intValue();
+        try {
+            return Integer.parseInt(o.toString());
+        } catch (Exception ignored) {
+            return 0;
+        }
+    }
+}

+ 6 - 0
sql/risk_menu.sql

@@ -183,6 +183,12 @@ VALUES ('文件上传', @risk_parent_id, 11, '#', '', 1, 0, 'F', '0', '0', 'risk
 INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, remark)
 VALUES ('文件删除', @risk_parent_id, 12, '#', '', 1, 0, 'F', '0', '0', 'risk:file:remove', '#', 103, 1, NOW(), '文件删除');
 
+-- ============================================
+-- 13. 风控大屏 (3.3 大屏菜单)
+-- ============================================
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, remark)
+VALUES ('风控大屏', @risk_parent_id, 0, 'bigScreen', 'risk/bigScreen/index', 1, 0, 'C', '0', '0', 'risk:bigScreen:view', 'data-board', 103, 1, NOW(), '风控品控管理平台大屏');
+
 -- ============================================
 -- 13. 给超级管理员角色授权所有风控平台权限
 -- ============================================