liu
昨天 00e4d71bd9d504ed25339595690ebe074146b7e7
feat(mes): 新增管理驾驶舱与统计分析模块

整合生产/质检/库存/出库/追溯五大维度核心指标统计接口,
提供可视化驾驶舱页面,支持 Excel 多sheet报表导出与打印存档。

Co-Authored-By: Claude <noreply@anthropic.com>
已添加20个文件
1659 ■■■■■ 文件已修改
docs/management_cockpit_module.md 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_dashboard_module_init.sql 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_dashboard_test_data.sql 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/MesDashboardController.java 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardDailyCountRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardGenericExcelVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardIndicatorAvgRespVO.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardInventoryRespVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardNameCountRespVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardOutboundRespVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardOverviewExcelVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardProductionExcelVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardProductionTrendRespVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardQualityRespVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardSummaryRespVO.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardTraceRespVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dashboard/MesDashboardMapper.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dashboard/MesDashboardService.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dashboard/MesDashboardServiceImpl.java 404 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/dashboard/MesDashboardMapper.xml 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/management_cockpit_module.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
# ç®¡ç†é©¾é©¶èˆ±ä¸Žç»Ÿè®¡åˆ†æžæ¨¡å—
## 1. æ¨¡å—概述
对应招标需求书第 12 æ¡ï¼š**整合生产、质检、库存、出库、追溯五大维度核心指标,提供可视化图表展示,支持 Excel/PDF æ ¼å¼æŠ¥è¡¨å¯¼å‡ºå’Œæ‰“印存档。**
- äº”大维度:
  - **生产**:生产批次、袋码(一袋一码)、托盘码、今日扫码入库袋数、生产趋势;
  - **质检**:种子四项指标(纯度/净度/发芽率/水分)检验结果分布、均值与合格率;
  - **库存**:按仓库/品种/库存状态(暂存库/合格库/已出库)分布、低库存预警数;
  - **出库**:出库按日趋势、按经销商出库 TOP20;
  - **追溯**:消费者扫码趋势、省份分布、渠道分布、窜货预警汇总。
- å¯¼å‡ºèƒ½åŠ›ï¼š
  - **Excel**:后端 EasyExcel(fastexcel)生成多 sheet å·¥ä½œç°¿ï¼Œå…± 6 ä¸ª sheet(驾驶舱总览 / ç”Ÿäº§ç»Ÿè®¡ / è´¨æ£€ç»Ÿè®¡ / åº“存统计 / å‡ºåº“统计 / è¿½æº¯ç»Ÿè®¡ï¼‰ï¼›
  - **PDF**:前端 `html2canvas` + `jspdf` å°†é©¾é©¶èˆ±æŠ¥è¡¨åŒºåŸŸç”Ÿæˆä¸ºå¯ç›´æŽ¥ä¸‹è½½çš„ PDF æ–‡ä»¶ï¼›
  - **打印**:前端 `vue3-print-nb` æ‰“印报表区域用于存档。
## 2. æ•°æ®æºè¦æ±‚(重要)
**种子业务数据表位于 `mom-thzy` åº“**(含生产批次 `mes_pro_batch`、袋码 `mes_pro_bag_code`、托盘 `mes_pro_pallet`、种子质检 `mes_qc_seed_quality`、扫码事件 `mes_trace_consumer_scan_event` ç­‰å…¨éƒ¨ä¸šåŠ¡è¡¨ï¼‰ã€‚
> âœ… å·²å¤„理:`yudao-server/src/main/resources/application-local.yaml` ä¸»æ•°æ®æºå·²æŒ‡å›ž `jdbc:mysql://192.168.197.128:3306/mom-thzy`(root / sugaozhixing.cn)。驾驶舱及一袋一码等种子业务功能**必须连接 `mom-thzy` åº“**才能正常统计,请勿再切回 `ruoyi-vue-pro`(该库不含种子业务表)。
若目标库缺少种子业务表,需先执行 `sql/mysql/` ä¸‹çš„初始化脚本:
- `mes_pro_batch_bag_code_init.sql`(生产批次/袋码/托盘)
- `mes_seed_quality_init.sql`(种子质检)
- `mes_trace_consumer_scan_event.sql`(消费者扫码事件)
- `mes_trace_anti_cross_region_init.sql`(窜货预警)
- `mes_pro_scan_inbound_init.sql`(生产线扫码入库)
## 3. åŽç«¯å®žçް
后端位于 `yudao-module-mes`,新增包:
| æ–‡ä»¶ | è¯´æ˜Ž |
|---|---|
| `controller/admin/dashboard/MesDashboardController.java` | `/mes/dashboard` ç»Ÿè®¡æŽ¥å£ + Excel å¤š sheet å¯¼å‡º |
| `controller/admin/dashboard/vo/*.java` | ç»Ÿè®¡å“åº” VO ä¸Ž Excel å¯¼å‡º VO |
| `service/dashboard/MesDashboardService(.java|Impl)` | äº”维度统计聚合(含按日补零) |
| `dal/mysql/dashboard/MesDashboardMapper.java` | MyBatis Mapper æŽ¥å£ |
| `resources/mapper/dashboard/MesDashboardMapper.xml` | æ‰‹å†™ç»Ÿè®¡ SQL |
接口清单(权限 `mes:dashboard:query` / `mes:dashboard:export`):
| æŽ¥å£ | è¯´æ˜Ž |
|---|---|
| `GET /mes/dashboard/summary` | äº”维度 KPI æ±‡æ€» |
| `GET /mes/dashboard/production-trend?days=` | ç”Ÿäº§è¶‹åŠ¿ |
| `GET /mes/dashboard/quality-statistics` | è´¨æ£€ç»´åº¦ç»Ÿè®¡ |
| `GET /mes/dashboard/inventory-statistics` | åº“存维度统计 |
| `GET /mes/dashboard/outbound-statistics?days=` | å‡ºåº“维度统计 |
| `GET /mes/dashboard/trace-statistics?days=` | è¿½æº¯ç»´åº¦ç»Ÿè®¡ï¼ˆå¤ç”¨æ‰«ç ç»Ÿè®¡ä¸Žçªœè´§é¢„警服务) |
| `GET /mes/dashboard/export-excel` | Excel å¤š sheet æŠ¥è¡¨å¯¼å‡º |
设计要点:
- è¿½æº¯ç»´åº¦**复用** `MesTraceConsumerScanStatisticsService`、`MesTraceCrossRegionService`,避免重复 SQL;
- åº“存低预警数**复用** `MesWmStockWarningService.getCurrentWarnings()`;
- ç©ºæ•°æ®è¡¨ï¼ˆå¦‚质检/库存/出库尚无数据)返回 0 æˆ–空数组,前端图表正常渲染;Excel å¯¼å‡ºç©º sheet è‡ªåŠ¨è¡¥ã€Œæš‚æ— æ•°æ®ã€è¡Œï¼Œä¿è¯è¡¨å¤´å¯æŽ¨æ–­ã€‚
## 4. å‰ç«¯å®žçް
前端位于 `mom-pro2-before`,新增:
| æ–‡ä»¶ | è¯´æ˜Ž |
|---|---|
| `src/api/mes/management-cockpit/index.ts` | é©¾é©¶èˆ± API(`requestClient`) |
| `src/views/management-cockpit/index.vue` | é©¾é©¶èˆ±ä¸»é¡µé¢ï¼ˆå·¥å…·æ  + æŠ¥è¡¨åŒºåŸŸï¼‰ |
| `src/views/management-cockpit/chart-options.ts` | ECharts é…ç½®å·¥åŽ‚ |
| `src/views/management-cockpit/modules/*.vue` | KPI å¡ + ç”Ÿäº§/质检/库存/出库/追溯图表组件 |
页面能力:
- **刷新**:整体刷新全部维度数据;
- **导出 Excel**:`requestClient.download` ä¸‹è½½åŽç«¯å¤š sheet å·¥ä½œç°¿ï¼›
- **导出 PDF**:`html2canvas` æˆªå–报表区域 + `jspdf` ç”Ÿæˆå¯ä¸‹è½½ PDF;
- **打印**:`vue3-print-nb` æ‰“印报表区域,用于存档。
新增依赖(根 `package.json`):`html2canvas`、`jspdf`。
## 5. èœå•与权限
执行 `sql/mysql/mes_dashboard_module_init.sql`(幂等)注册:
- é¡¶çº§ç›®å½•「统计分析」(id=602700,已存在时复用);
- é¡µé¢ã€Œç®¡ç†é©¾é©¶èˆ±ã€ï¼ˆid=602701,`component = management-cockpit/index`);
- æŒ‰é’®æƒé™ `mes:dashboard:query`、`mes:dashboard:export`。
菜单由后端 `system_menu` åŠ¨æ€ä¸‹å‘ï¼Œå‰ç«¯ `component` å­—段映射到 `src/views/management-cockpit/index.vue`。
## 6. éªŒè¯
1. åŽç«¯ç¼–译:`mvn compile -pl yudao-module-mes -am -q`
2. å‰ç«¯ç±»åž‹æ£€æŸ¥ï¼š`pnpm typecheck`(本次新增文件无类型错误)
3. æ‰§è¡Œ `mes_dashboard_module_init.sql` åŽç™»å½•系统,菜单出现「统计分析 â†’ ç®¡ç†é©¾é©¶èˆ±ã€
4. æ ¸å¯¹äº”大维度图表数据与 `mom-thzy` åº“实际数据一致
5. åˆ†åˆ«éªŒè¯ Excel ä¸‹è½½ã€PDF ä¸‹è½½ã€æ‰“印三个功能
sql/mysql/mes_dashboard_module_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
-- =============================================
-- ç®¡ç†é©¾é©¶èˆ±ä¸Žç»Ÿè®¡åˆ†æžæ¨¡å— åˆå§‹åŒ–脚本
-- åŒ…含:「管理驾驶舱」页面及按钮权限(查询/导出)
-- è¯´æ˜Žï¼šé¡¶çº§ç›®å½•「统计分析」(id=602700) å·²å­˜åœ¨ï¼Œæœ¬è„šæœ¬ä»…åœ¨å…¶ä¸‹æŒ‚è½½é¡µé¢ã€‚
-- é€‚用库:mom-thzy(种子业务主库,含生产/质检/库存/出库/追溯全部业务表)
-- æ³¨æ„ï¼šè‹¥ç›®æ ‡åº“缺少种子业务表,需先执行以下脚本:
--   mes_pro_batch_bag_code_init.sql      ï¼ˆç”Ÿäº§æ‰¹æ¬¡/袋码/托盘)
--   mes_seed_quality_init.sql            ï¼ˆç§å­è´¨æ£€ï¼‰
--   mes_trace_consumer_scan_event.sql    ï¼ˆæ¶ˆè´¹è€…扫码事件)
--   mes_trace_anti_cross_region_init.sql ï¼ˆçªœè´§é¢„警)
--   mes_pro_scan_inbound_init.sql        ï¼ˆç”Ÿäº§çº¿æ‰«ç å…¥åº“)
-- MySQL 8.4 é€‚用
-- =============================================
SET NAMES utf8mb4;
-- ----------------------------
-- 1. é¡µé¢ï¼šç®¡ç†é©¾é©¶èˆ±ï¼ˆæŒ‚到「统计分析」目录 602700 ä¸‹ï¼‰
-- ----------------------------
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name,
                         status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 602701, '管理驾驶舱', '', 2, 1, 602700, 'management-cockpit', 'ep:monitor', 'management-cockpit/index', 'ManagementCockpit',
       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM system_menu
    WHERE (id = 602701 OR (parent_id = 602700 AND path = 'management-cockpit' AND deleted = 0)) AND deleted = 0
)
  AND EXISTS (SELECT 1 FROM system_menu WHERE id = 602700 AND deleted = 0);
-- ----------------------------
-- 2. æŒ‰é’®æƒé™ï¼šæŸ¥è¯¢ / å¯¼å‡º
-- ----------------------------
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name,
                         status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 602702, '驾驶舱查询', 'mes:dashboard:query', 3, 1, 602701, '', '', '', '',
       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM system_menu
    WHERE (id = 602702 OR (parent_id = 602701 AND permission = 'mes:dashboard:query' AND deleted = 0)) AND deleted = 0
);
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name,
                         status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 602703, '驾驶舱导出', 'mes:dashboard:export', 3, 2, 602701, '', '', '', '',
       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM system_menu
    WHERE (id = 602703 OR (parent_id = 602701 AND permission = 'mes:dashboard:export' AND deleted = 0)) AND deleted = 0
);
sql/mysql/mes_dashboard_test_data.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
-- =============================================
-- ç®¡ç†é©¾é©¶èˆ± æµ‹è¯•数据脚本
-- ç›®æ ‡åº“:mom-thzy
-- ä½œç”¨ï¼šä¸ºè´¨æ£€ / åº“å­˜ / å‡ºåº“ / çªœè´§é¢„è­¦ å››ä¸ªå½“前为空的维度补充演示数据
-- å…³è”已有基础数据:仓库(产品库/物料库/虚拟线边仓库)、品种(mdm_item#1)、
--                  ç»é”€å•†(crm_customer#2/4/6)、批次(1/102)、袋码(0001~0007)
-- æ•°æ®æ—¥æœŸï¼š2026-08-28 ~ 2026-09-01
-- æ³¨æ„ï¼šid ä½¿ç”¨ 1001+ æ®µé¿å…ä¸Žæ—¢æœ‰æ•°æ®å†²çªï¼›é‡å¤æ‰§è¡Œå‰è¯·å…ˆæ¸…理或备份
-- =============================================
SET NAMES utf8mb4;
-- ----------------------------
-- 1. ç§å­è´¨æ£€ä¸»è¡¨ï¼ˆ7 æ¡ï¼š4 åˆæ ¼ / 2 ä¸åˆæ ¼ / 1 å¤„理中)
-- ----------------------------
INSERT INTO `mes_qc_seed_quality` (id, code, item_id, batch_code, result, reason, inspector_id, inspected_at, creator, create_time, updater, update_time, deleted) VALUES
(1001, 'PC20260819twoITEM_001-0001', 1, 'PC20260819twoITEM_001', 1, NULL, 1, '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1002, 'PC20260819twoITEM_001-0002', 1, 'PC20260819twoITEM_001', 1, NULL, 111, '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', b'0'),
(1003, 'PC20260819twoITEM_001-0003', 1, 'PC20260819twoITEM_001', 2, '发芽率低于标准值', 1, '2026-08-29 14:30:00', '1', '2026-08-29 14:30:00', '1', '2026-08-29 14:30:00', b'0'),
(1004, 'PC20260819twoITEM_001-0004', 1, 'PC20260819twoITEM_001', 1, NULL, 111, '2026-08-30 09:20:00', '1', '2026-08-30 09:20:00', '1', '2026-08-30 09:20:00', b'0'),
(1005, 'PC20260819twoITEM_001-0005', 1, 'PC20260819twoITEM_001', 0, NULL, 1, NULL, '1', '2026-08-30 11:00:00', '1', '2026-08-30 11:00:00', b'0'),
(1006, 'PC20260819twoITEM_001-0006', 1, 'PC20260819twoITEM_001', 1, NULL, 1, '2026-08-31 15:00:00', '1', '2026-08-31 15:00:00', '1', '2026-08-31 15:00:00', b'0'),
(1007, 'PC20260819twoITEM_001-0007', 1, 'PC20260819twoITEM_001', 2, '水分含量超标', 111, '2026-09-01 09:10:00', '1', '2026-09-01 09:10:00', '1', '2026-09-01 09:10:00', b'0');
-- ----------------------------
-- 2. ç§å­è´¨æ£€æŒ‡æ ‡ç»“果(每条质检 4 é¡¹ï¼šçº¯åº¦/净度/发芽率/水分)
--    æ ‡å‡†åŒºé—´ï¼šçº¯åº¦[96,100] å‡€åº¦[98,100] å‘芽率[85,100] æ°´åˆ†[0,13]
-- ----------------------------
INSERT INTO `mes_qc_seed_quality_indicator` (id, quality_id, indicator_code, value, min_value, max_value, creator, create_time, updater, update_time, deleted) VALUES
(1001, 1001, 'purity', 98.500000, 96, 100, '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1002, 1001, 'cleanliness', 99.200000, 98, 100, '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1003, 1001, 'germination_rate', 92.000000, 85, 100, '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1004, 1001, 'moisture', 10.500000, 0, 13, '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1005, 1002, 'purity', 98.100000, 96, 100, '1', '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', b'0'),
(1006, 1002, 'cleanliness', 99.000000, 98, 100, '1', '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', b'0'),
(1007, 1002, 'germination_rate', 93.500000, 85, 100, '1', '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', b'0'),
(1008, 1002, 'moisture', 10.200000, 0, 13, '1', '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', b'0'),
(1009, 1003, 'purity', 98.200000, 96, 100, '1', '2026-08-29 14:30:00', '1', '2026-08-29 14:30:00', b'0'),
(1010, 1003, 'cleanliness', 99.000000, 98, 100, '1', '2026-08-29 14:30:00', '1', '2026-08-29 14:30:00', b'0'),
(1011, 1003, 'germination_rate', 80.000000, 85, 100, '1', '2026-08-29 14:30:00', '1', '2026-08-29 14:30:00', b'0'),
(1012, 1003, 'moisture', 11.200000, 0, 13, '1', '2026-08-29 14:30:00', '1', '2026-08-29 14:30:00', b'0'),
(1013, 1004, 'purity', 98.800000, 96, 100, '1', '2026-08-30 09:20:00', '1', '2026-08-30 09:20:00', b'0'),
(1014, 1004, 'cleanliness', 99.400000, 98, 100, '1', '2026-08-30 09:20:00', '1', '2026-08-30 09:20:00', b'0'),
(1015, 1004, 'germination_rate', 95.000000, 85, 100, '1', '2026-08-30 09:20:00', '1', '2026-08-30 09:20:00', b'0'),
(1016, 1004, 'moisture', 9.800000, 0, 13, '1', '2026-08-30 09:20:00', '1', '2026-08-30 09:20:00', b'0'),
(1017, 1005, 'purity', 98.000000, 96, 100, '1', '2026-08-30 11:00:00', '1', '2026-08-30 11:00:00', b'0'),
(1018, 1005, 'cleanliness', 98.800000, 98, 100, '1', '2026-08-30 11:00:00', '1', '2026-08-30 11:00:00', b'0'),
(1019, 1005, 'germination_rate', 90.000000, 85, 100, '1', '2026-08-30 11:00:00', '1', '2026-08-30 11:00:00', b'0'),
(1020, 1005, 'moisture', 11.000000, 0, 13, '1', '2026-08-30 11:00:00', '1', '2026-08-30 11:00:00', b'0'),
(1021, 1006, 'purity', 98.600000, 96, 100, '1', '2026-08-31 15:00:00', '1', '2026-08-31 15:00:00', b'0'),
(1022, 1006, 'cleanliness', 99.100000, 98, 100, '1', '2026-08-31 15:00:00', '1', '2026-08-31 15:00:00', b'0'),
(1023, 1006, 'germination_rate', 91.500000, 85, 100, '1', '2026-08-31 15:00:00', '1', '2026-08-31 15:00:00', b'0'),
(1024, 1006, 'moisture', 10.100000, 0, 13, '1', '2026-08-31 15:00:00', '1', '2026-08-31 15:00:00', b'0'),
(1025, 1007, 'purity', 97.500000, 96, 100, '1', '2026-09-01 09:10:00', '1', '2026-09-01 09:10:00', b'0'),
(1026, 1007, 'cleanliness', 98.800000, 98, 100, '1', '2026-09-01 09:10:00', '1', '2026-09-01 09:10:00', b'0'),
(1027, 1007, 'germination_rate', 90.000000, 85, 100, '1', '2026-09-01 09:10:00', '1', '2026-09-01 09:10:00', b'0'),
(1028, 1007, 'moisture', 14.500000, 0, 13, '1', '2026-09-01 09:10:00', '1', '2026-09-01 09:10:00', b'0');
-- ----------------------------
-- 3. åº“存(4 æ¡ï¼šåˆæ ¼åº“2 / æš‚存库2,分布于产品库/物料库/虚拟线边仓库)
--    åº“存状态:10-暂存库 20-合格库 30-已出库
-- ----------------------------
INSERT INTO `mes_wm_material_stock` (id, item_type_id, item_id, batch_id, batch_code, warehouse_id, location_id, area_id, vendor_id, quantity, frozen_quantity, reserved_quantity, in_transit_quantity, stock_state, erp_time, receipt_time, frozen, remark, creator, create_time, updater, update_time, deleted) VALUES
(1001, NULL, 1, 1, 'PC20260819twoITEM_001', 1, NULL, NULL, NULL, 1000.000000, 0, 0, 0, 20, '2026-08-20 08:00:00', '2026-08-20 08:00:00', b'0', '驾驶舱演示数据', '1', '2026-08-20 08:00:00', '1', '2026-08-20 08:00:00', b'0'),
(1002, NULL, 1, 102, 'PC20260828twoITEM_001', 1, NULL, NULL, NULL, 500.000000, 0, 0, 0, 20, '2026-08-28 09:00:00', '2026-08-28 09:00:00', b'0', '驾驶舱演示数据', '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1003, NULL, 1, 1, 'PC20260819twoITEM_001', 2, NULL, NULL, NULL, 800.000000, 0, 0, 0, 10, '2026-08-20 08:30:00', '2026-08-20 08:30:00', b'0', '驾驶舱演示数据', '1', '2026-08-20 08:30:00', '1', '2026-08-20 08:30:00', b'0'),
(1004, NULL, 1, 102, 'PC20260828twoITEM_001', 3, NULL, NULL, NULL, 300.000000, 0, 0, 0, 10, '2026-08-28 10:00:00', '2026-08-28 10:00:00', b'0', '驾驶舱演示数据', '1', '2026-08-28 10:00:00', '1', '2026-08-28 10:00:00', b'0');
-- ----------------------------
-- 4. é”€å”®å‡ºåº“单(4 å¼ ï¼Œ3 å·²å®Œæˆ + 1 å¾…执行出库)
--    å‡ºåº“状态:0草稿 1待检测 2待拣货 3待执行出库 4已完成
-- ----------------------------
INSERT INTO `mes_wm_product_sales` (id, code, name, client_id, sale_order_id, notice_id, sales_order_code, sales_date, contact_name, contact_telephone, contact_address, carrier, shipping_number, status, remark, creator, create_time, updater, update_time, deleted) VALUES
(1001, 'XSCK20260828001', '销售出库单-南通客户', 2, NULL, NULL, NULL, '2026-08-28 09:00:00', '张经理', '13800000001', '江苏省南通市', '顺丰', 'SF1001', 4, '驾驶舱演示数据', '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1002, 'XSCK20260829001', '销售出库单-客户沟通', 4, NULL, NULL, NULL, '2026-08-29 10:00:00', '李经理', '13800000002', '新疆阿克苏', '中通', 'ZT2001', 4, '驾驶舱演示数据', '1', '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', b'0'),
(1003, 'XSCK20260831001', '销售出库单-高级', 6, NULL, NULL, NULL, '2026-08-31 11:00:00', '王经理', '13800000003', '新疆喀什', '圆通', 'YT3001', 3, '驾驶舱演示数据', '1', '2026-08-31 11:00:00', '1', '2026-08-31 11:00:00', b'0'),
(1004, 'XSCK20260901001', '销售出库单-南通客户', 2, NULL, NULL, NULL, '2026-09-01 09:30:00', '张经理', '13800000001', '江苏省南通市', '顺丰', 'SF1004', 4, '驾驶舱演示数据', '1', '2026-09-01 09:30:00', '1', '2026-09-01 09:30:00', b'0');
-- ----------------------------
-- 5. é”€å”®å‡ºåº“单行(对应上面 4 å¼ å‡ºåº“单)
-- ----------------------------
INSERT INTO `mes_wm_product_sales_line` (id, sales_id, sale_order_item_id, notice_line_id, item_id, batch_id, batch_code, material_stock_id, quantity, oqc_check_flag, oqc_id, quality_status, remark, creator, create_time, updater, update_time, deleted) VALUES
(1001, 1001, NULL, NULL, 1, 1, 'PC20260819twoITEM_001', NULL, 200.000000, b'0', NULL, NULL, '驾驶舱演示数据', '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1002, 1001, NULL, NULL, 1, 1, 'PC20260819twoITEM_001', NULL, 150.000000, b'0', NULL, NULL, '驾驶舱演示数据', '1', '2026-08-28 09:00:00', '1', '2026-08-28 09:00:00', b'0'),
(1003, 1002, NULL, NULL, 1, 102, 'PC20260828twoITEM_001', NULL, 180.000000, b'0', NULL, NULL, '驾驶舱演示数据', '1', '2026-08-29 10:00:00', '1', '2026-08-29 10:00:00', b'0'),
(1004, 1003, NULL, NULL, 1, 1, 'PC20260819twoITEM_001', NULL, 220.000000, b'0', NULL, NULL, '驾驶舱演示数据', '1', '2026-08-31 11:00:00', '1', '2026-08-31 11:00:00', b'0'),
(1005, 1004, NULL, NULL, 1, 102, 'PC20260828twoITEM_001', NULL, 160.000000, b'0', NULL, NULL, '驾驶舱演示数据', '1', '2026-09-01 09:30:00', '1', '2026-09-01 09:30:00', b'0');
-- ----------------------------
-- 6. çªœè´§é¢„警(3 æ¡ï¼šè·¨çœå¾…处理 / è·¨å¸‚已处理 / è·¨çœè¯¯æŠ¥ï¼‰
--    çŠ¶æ€ï¼š10-待处理 20-已处理 30-误报;类型:CROSS_PROVINCE è·¨çœ / CROSS_CITY è·¨å¸‚
-- ----------------------------
INSERT INTO `mes_trace_cross_region_warning` (id, warning_no, bag_code_id, bag_code, pallet_code_id, pallet_code, batch_id, batch_code, item_id, item_code, item_name, sales_id, sales_code, client_id, client_name, dealer_province, dealer_city, scan_province, scan_city, warning_type, scan_event_id, status, handle_user_id, handle_time, handle_remark, event_time, create_time, update_time, creator, updater, deleted) VALUES
(1001, 'WARN20260901001', 1, 'PC20260819twoITEM_001-0001', NULL, NULL, 1, 'PC20260819twoITEM_001', 1, '001', '测试', 1001, 'XSCK20260828001', 2, '南通客户', '新疆', '阿克苏', '河南', '郑州', 'CROSS_PROVINCE', NULL, 10, NULL, NULL, NULL, '2026-08-30 12:00:00', '2026-08-30 12:00:00', '2026-08-30 12:00:00', '1', '1', b'0'),
(1002, 'WARN20260901002', NULL, NULL, NULL, NULL, 102, 'PC20260828twoITEM_001', 1, '001', '测试', 1002, 'XSCK20260829001', 4, '客户沟通', '新疆', '喀什', '新疆', '乌鲁木齐', 'CROSS_CITY', NULL, 20, 1, '2026-09-01 08:00:00', '已核实处理', '2026-08-31 18:00:00', '2026-08-31 18:00:00', '2026-09-01 08:00:00', '1', '1', b'0'),
(1003, 'WARN20260901003', NULL, NULL, NULL, NULL, 1, 'PC20260819twoITEM_001', 1, '001', '测试', 1004, 'XSCK20260901001', 2, '南通客户', '新疆', '阿克苏', '甘肃', '兰州', 'CROSS_PROVINCE', NULL, 30, 1, '2026-09-01 10:00:00', '误报', '2026-09-01 09:00:00', '2026-09-01 09:00:00', '2026-09-01 10:00:00', '1', '1', b'0');
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/MesDashboardController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard;
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.FastExcelFactory;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardDailyCountRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardGenericExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardIndicatorAvgRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardInventoryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardNameCountRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardOutboundRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardOverviewExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardProductionExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardProductionTrendRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardQualityRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardTraceRespVO;
import cn.iocoder.yudao.module.mes.service.dashboard.MesDashboardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±ä¸Žç»Ÿè®¡åˆ†æž
 * <p>
 * æ•´åˆç”Ÿäº§ã€è´¨æ£€ã€åº“存、出库、追溯五大维度核心指标,提供可视化图表数据与 Excel æŠ¥è¡¨å¯¼å‡ºã€‚
 *
 * @author è¶…级管理员
 */
@Tag(name = "管理后台 - MES ç®¡ç†é©¾é©¶èˆ±")
@RestController
@RequestMapping("/mes/dashboard")
@Validated
public class MesDashboardController {
    @Resource
    private MesDashboardService dashboardService;
    @GetMapping("/summary")
    @Operation(summary = "获得管理驾驶舱五维度汇总统计")
    @PreAuthorize("@ss.hasPermission('mes:dashboard:query')")
    public CommonResult<MesDashboardSummaryRespVO> getSummary() {
        return success(dashboardService.getSummary());
    }
    @GetMapping("/production-trend")
    @Operation(summary = "获得生产趋势")
    @Parameter(name = "days", description = "天数", example = "30")
    @PreAuthorize("@ss.hasPermission('mes:dashboard:query')")
    public CommonResult<List<MesDashboardProductionTrendRespVO>> getProductionTrend(
            @RequestParam(value = "days", defaultValue = "30") @Min(1) @Max(365) Integer days) {
        return success(dashboardService.getProductionTrend(days));
    }
    @GetMapping("/quality-statistics")
    @Operation(summary = "获得质检维度统计")
    @PreAuthorize("@ss.hasPermission('mes:dashboard:query')")
    public CommonResult<MesDashboardQualityRespVO> getQualityStatistics() {
        return success(dashboardService.getQualityStatistics());
    }
    @GetMapping("/inventory-statistics")
    @Operation(summary = "获得库存维度统计")
    @PreAuthorize("@ss.hasPermission('mes:dashboard:query')")
    public CommonResult<MesDashboardInventoryRespVO> getInventoryStatistics() {
        return success(dashboardService.getInventoryStatistics());
    }
    @GetMapping("/outbound-statistics")
    @Operation(summary = "获得出库维度统计")
    @Parameter(name = "days", description = "天数", example = "30")
    @PreAuthorize("@ss.hasPermission('mes:dashboard:query')")
    public CommonResult<MesDashboardOutboundRespVO> getOutboundStatistics(
            @RequestParam(value = "days", defaultValue = "30") @Min(1) @Max(365) Integer days) {
        return success(dashboardService.getOutboundStatistics(days));
    }
    @GetMapping("/trace-statistics")
    @Operation(summary = "获得追溯维度统计")
    @Parameter(name = "days", description = "天数", example = "30")
    @PreAuthorize("@ss.hasPermission('mes:dashboard:query')")
    public CommonResult<MesDashboardTraceRespVO> getTraceStatistics(
            @RequestParam(value = "days", defaultValue = "30") @Min(1) @Max(365) Integer days) {
        return success(dashboardService.getTraceStatistics(days));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出管理驾驶舱 Excel æŠ¥è¡¨")
    @ApiAccessLog(operateType = EXPORT)
    @PreAuthorize("@ss.hasPermission('mes:dashboard:export')")
    public void exportExcel(HttpServletResponse response) throws IOException {
        // 1. ç»„装各维度报表数据
        MesDashboardSummaryRespVO summary = dashboardService.getSummary();
        List<MesDashboardProductionTrendRespVO> productionTrend = dashboardService.getProductionTrend(30);
        MesDashboardQualityRespVO quality = dashboardService.getQualityStatistics();
        MesDashboardInventoryRespVO inventory = dashboardService.getInventoryStatistics();
        MesDashboardOutboundRespVO outbound = dashboardService.getOutboundStatistics(30);
        MesDashboardTraceRespVO trace = dashboardService.getTraceStatistics(30);
        // 2. å¤š sheet å†™å…¥
        ExcelWriter excelWriter = FastExcelFactory.write(response.getOutputStream())
                .autoCloseStream(false)
                .build();
        try {
            excelWriter.write(buildOverviewRows(summary), FastExcelFactory.writerSheet("驾驶舱总览").build());
            excelWriter.write(buildProductionRows(summary, productionTrend), FastExcelFactory.writerSheet("生产统计").build());
            excelWriter.write(buildQualityRows(quality), FastExcelFactory.writerSheet("质检统计").build());
            excelWriter.write(buildInventoryRows(inventory), FastExcelFactory.writerSheet("库存统计").build());
            excelWriter.write(buildOutboundRows(outbound), FastExcelFactory.writerSheet("出库统计").build());
            excelWriter.write(buildTraceRows(trace), FastExcelFactory.writerSheet("追溯统计").build());
        } finally {
            excelWriter.finish();
            excelWriter.close();
        }
        response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8("管理驾驶舱统计报表.xls"));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }
    // ========== Excel è¡Œç»„装 ==========
    private List<MesDashboardOverviewExcelVO> buildOverviewRows(MesDashboardSummaryRespVO summary) {
        List<MesDashboardOverviewExcelVO> rows = new ArrayList<>();
        addOverview(rows, "批次总数", summary.getBatchTotal());
        addOverview(rows, "生产中批次", summary.getBatchProducing());
        addOverview(rows, "已完成批次", summary.getBatchFinished());
        addOverview(rows, "已作废批次", summary.getBatchCanceled());
        addOverview(rows, "袋码总数", summary.getBagCodeTotal());
        addOverview(rows, "已导出印刷袋码", summary.getBagCodePrinted());
        addOverview(rows, "已作废袋码", summary.getBagCodeCanceled());
        addOverview(rows, "托盘码总数", summary.getPalletTotal());
        addOverview(rows, "今日扫码入库袋数", summary.getTodayInboundBagCount());
        addOverview(rows, "检验记录总数", summary.getQualityTotal());
        addOverview(rows, "合格检验记录", summary.getQualityPass());
        addOverview(rows, "不合格检验记录", summary.getQualityFail());
        addOverview(rows, "检验合格率(%)", summary.getQualityPassRate());
        addOverview(rows, "当前库存总量", summary.getStockTotal());
        addOverview(rows, "暂存库存量", summary.getStockTemporary());
        addOverview(rows, "合格库存量", summary.getStockQualified());
        addOverview(rows, "低库存预警数", summary.getLowStockCount());
        addOverview(rows, "出库单总数", summary.getOutboundOrderTotal());
        addOverview(rows, "出库总量", summary.getOutboundQuantityTotal());
        addOverview(rows, "今日出库量", summary.getTodayOutboundQuantity());
        addOverview(rows, "扫码总数", summary.getScanTotal());
        addOverview(rows, "今日扫码数", summary.getTodayScanCount());
        addOverview(rows, "窜货预警总数", summary.getCrossRegionWarningTotal());
        addOverview(rows, "待处理窜货预警", summary.getCrossRegionPendingCount());
        return rows;
    }
    private List<MesDashboardProductionExcelVO> buildProductionRows(MesDashboardSummaryRespVO summary,
                                                                    List<MesDashboardProductionTrendRespVO> trend) {
        List<MesDashboardProductionExcelVO> rows = new ArrayList<>();
        addProduction(rows, "批次", "草稿", summary.getBatchDraft());
        addProduction(rows, "批次", "生产中", summary.getBatchProducing());
        addProduction(rows, "批次", "已完成", summary.getBatchFinished());
        addProduction(rows, "批次", "已作废", summary.getBatchCanceled());
        addProduction(rows, "袋码", "已生成", summary.getBagCodeGenerated());
        addProduction(rows, "袋码", "已导出印刷", summary.getBagCodePrinted());
        addProduction(rows, "袋码", "已作废", summary.getBagCodeCanceled());
        addProduction(rows, "托盘", "总数", summary.getPalletTotal());
        addProduction(rows, "入库", "今日扫码入库袋数", summary.getTodayInboundBagCount());
        for (MesDashboardProductionTrendRespVO item : trend) {
            addProduction(rows, "趋势-" + item.getDate(), "新增批次", item.getBatchCount());
            addProduction(rows, "趋势-" + item.getDate(), "新增袋码", item.getBagCodeCount());
            addProduction(rows, "趋势-" + item.getDate(), "新增托盘", item.getPalletCount());
            addProduction(rows, "趋势-" + item.getDate(), "入库袋数", item.getInboundBagQuantity() == null ? 0L
                    : item.getInboundBagQuantity().longValue());
        }
        return rows;
    }
    private List<MesDashboardGenericExcelVO> buildQualityRows(MesDashboardQualityRespVO quality) {
        List<MesDashboardGenericExcelVO> rows = new ArrayList<>();
        for (MesDashboardNameCountRespVO item : quality.getResultDistribution()) {
            addGeneric(rows, "检验结果", item.getName(), item.getCount());
        }
        for (MesDashboardIndicatorAvgRespVO item : quality.getIndicatorList()) {
            addGeneric(rows, "指标均值", item.getIndicatorName() + "(平均值)", item.getAverageValue());
            addGeneric(rows, "指标均值", item.getIndicatorName() + "(合格率%)", item.getPassRate());
        }
        addEmptyGuard(rows);
        return rows;
    }
    private List<MesDashboardGenericExcelVO> buildInventoryRows(MesDashboardInventoryRespVO inventory) {
        List<MesDashboardGenericExcelVO> rows = new ArrayList<>();
        for (MesDashboardNameCountRespVO item : inventory.getWarehouseDistribution()) {
            addGeneric(rows, "按仓库", item.getName(), item.getCount());
        }
        for (MesDashboardNameCountRespVO item : inventory.getItemDistribution()) {
            addGeneric(rows, "按品种", item.getName(), item.getCount());
        }
        for (MesDashboardNameCountRespVO item : inventory.getStateDistribution()) {
            addGeneric(rows, "按状态", item.getName(), item.getCount());
        }
        addEmptyGuard(rows);
        return rows;
    }
    private List<MesDashboardGenericExcelVO> buildOutboundRows(MesDashboardOutboundRespVO outbound) {
        List<MesDashboardGenericExcelVO> rows = new ArrayList<>();
        for (MesDashboardDailyCountRespVO item : outbound.getTrend()) {
            addGeneric(rows, "出库趋势", item.getDate(), item.getCount());
        }
        for (MesDashboardNameCountRespVO item : outbound.getDealerDistribution()) {
            addGeneric(rows, "按经销商", item.getName(), item.getCount());
        }
        addEmptyGuard(rows);
        return rows;
    }
    private List<MesDashboardGenericExcelVO> buildTraceRows(MesDashboardTraceRespVO trace) {
        List<MesDashboardGenericExcelVO> rows = new ArrayList<>();
        for (MesDashboardDailyCountRespVO item : trace.getScanTrend()) {
            addGeneric(rows, "扫码趋势", item.getDate(), item.getCount());
        }
        for (MesDashboardNameCountRespVO item : trace.getProvinceDistribution()) {
            addGeneric(rows, "扫码省份", item.getName(), item.getCount());
        }
        for (MesDashboardNameCountRespVO item : trace.getChannelDistribution()) {
            addGeneric(rows, "扫码渠道", item.getName(), item.getCount());
        }
        if (trace.getCrossRegionSummary() != null) {
            addGeneric(rows, "窜货预警", "总数", trace.getCrossRegionSummary().getTotal());
            addGeneric(rows, "窜货预警", "待处理", trace.getCrossRegionSummary().getPending());
            addGeneric(rows, "窜货预警", "已处理", trace.getCrossRegionSummary().getHandled());
            addGeneric(rows, "窜货预警", "误报", trace.getCrossRegionSummary().getFalseAlarm());
            addGeneric(rows, "窜货预警", "跨省", trace.getCrossRegionSummary().getCrossProvince());
            addGeneric(rows, "窜货预警", "跨市", trace.getCrossRegionSummary().getCrossCity());
        }
        addEmptyGuard(rows);
        return rows;
    }
    /**
     * ç©ºæ•°æ®å…œåº•:保证每个 sheet è‡³å°‘有一行,避免 FastExcel æ— æ³•推断表头
     */
    private void addEmptyGuard(List<MesDashboardGenericExcelVO> rows) {
        if (rows.isEmpty()) {
            rows.add(new MesDashboardGenericExcelVO("提示", "暂无数据", "0"));
        }
    }
    private void addOverview(List<MesDashboardOverviewExcelVO> rows, String indicator, Object value) {
        rows.add(new MesDashboardOverviewExcelVO(indicator, value == null ? "0" : value.toString()));
    }
    private void addProduction(List<MesDashboardProductionExcelVO> rows, String category, String status, Object count) {
        rows.add(new MesDashboardProductionExcelVO(category, status, count == null ? 0L : ((Number) count).longValue()));
    }
    private void addGeneric(List<MesDashboardGenericExcelVO> rows, String category, String name, Object value) {
        rows.add(new MesDashboardGenericExcelVO(category, name, value == null ? "0" : value.toString()));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardDailyCountRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±æŒ‰æ—¥æ•°é‡è¶‹åŠ¿é¡¹ Response VO
 * <p>
 * ç”¨äºŽå‡ºåº“按日趋势、追溯扫码按日趋势等。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±æŒ‰æ—¥æ•°é‡è¶‹åŠ¿é¡¹ Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MesDashboardDailyCountRespVO {
    @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08-13")
    private String date;
    @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    private BigDecimal count;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardGenericExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±é€šç”¨å¯¼å‡º VO(类别-名称-数量 ä¸‰åˆ—)
 * <p>
 * ç”¨äºŽè´¨æ£€/库存/出库/追溯维度的分布表导出。
 *
 * @author è¶…级管理员
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ExcelIgnoreUnannotated
public class MesDashboardGenericExcelVO {
    @ExcelProperty("类别")
    private String category;
    @ExcelProperty("名称")
    private String name;
    @ExcelProperty("数值")
    private String value;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardIndicatorAvgRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±è´¨æ£€æŒ‡æ ‡å‡å€¼ Response VO
 * <p>
 * ç§å­å››é¡¹æŒ‡æ ‡ï¼šçº¯åº¦ã€å‡€åº¦ã€å‘芽率、水分
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±è´¨æ£€æŒ‡æ ‡å‡å€¼ Response VO")
@Data
public class MesDashboardIndicatorAvgRespVO {
    @Schema(description = "指标编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PURITY")
    private String indicatorCode;
    @Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "纯度")
    private String indicatorName;
    @Schema(description = "检验次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    private Long checkCount;
    @Schema(description = "指标平均值", requiredMode = Schema.RequiredMode.REQUIRED, example = "98.500000")
    private BigDecimal averageValue;
    @Schema(description = "指标合格率(%),落在标准区间内的占比", requiredMode = Schema.RequiredMode.REQUIRED, example = "90.00")
    private BigDecimal passRate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardInventoryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±åº“存维度统计 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±åº“存维度统计 Response VO")
@Data
public class MesDashboardInventoryRespVO {
    @Schema(description = "按仓库分布", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardNameCountRespVO> warehouseDistribution;
    @Schema(description = "按品种分布", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardNameCountRespVO> itemDistribution;
    @Schema(description = "按库存状态分布(暂存库/合格库/已出库)", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardNameCountRespVO> stateDistribution;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardNameCountRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±é€šç”¨åç§°-数量分布项 Response VO
 * <p>
 * ç”¨äºŽè´¨æ£€ç»“果分布、库存按仓库/品种/状态分布、出库经销商分布、追溯省份/渠道分布等。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±é€šç”¨åç§°-数量分布项 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MesDashboardNameCountRespVO {
    @Schema(description = "名称(仓库名/品种名/状态名/经销商名/省份等)", requiredMode = Schema.RequiredMode.REQUIRED, example = "合格库")
    private String name;
    @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    private Long count;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardOutboundRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±å‡ºåº“维度统计 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±å‡ºåº“维度统计 Response VO")
@Data
public class MesDashboardOutboundRespVO {
    @Schema(description = "出库按日趋势", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardDailyCountRespVO> trend;
    @Schema(description = "按经销商出库分布(TOP 20)", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardNameCountRespVO> dealerDistribution;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardOverviewExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±æ€»è§ˆå¯¼å‡º VO(指标-数值 ä¸¤åˆ—)
 *
 * @author è¶…级管理员
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ExcelIgnoreUnannotated
public class MesDashboardOverviewExcelVO {
    @ExcelProperty("指标")
    private String indicator;
    @ExcelProperty("数值")
    private String value;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardProductionExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±ç”Ÿäº§ç»´åº¦å¯¼å‡º VO(类别-状态-数量 ä¸‰åˆ—)
 *
 * @author è¶…级管理员
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ExcelIgnoreUnannotated
public class MesDashboardProductionExcelVO {
    @ExcelProperty("类别")
    private String category;
    @ExcelProperty("状态")
    private String statusName;
    @ExcelProperty("数量")
    private Long count;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardProductionTrendRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±ç”Ÿäº§è¶‹åŠ¿ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±ç”Ÿäº§è¶‹åŠ¿ Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MesDashboardProductionTrendRespVO {
    @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08-13")
    private String date;
    @Schema(description = "当日新增批次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long batchCount;
    @Schema(description = "当日新增袋码数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    private Long bagCodeCount;
    @Schema(description = "当日新增托盘码数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
    private Long palletCount;
    @Schema(description = "当日扫码入库袋数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    private BigDecimal inboundBagQuantity;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardQualityRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±è´¨æ£€ç»´åº¦ç»Ÿè®¡ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±è´¨æ£€ç»´åº¦ç»Ÿè®¡ Response VO")
@Data
public class MesDashboardQualityRespVO {
    @Schema(description = "检验结果分布(处理中/合格/不合格)", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardNameCountRespVO> resultDistribution;
    @Schema(description = "四项指标均值与合格率", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardIndicatorAvgRespVO> indicatorList;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardSummaryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±äº”维度汇总统计 Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±æ±‡æ€»ç»Ÿè®¡ Response VO")
@Data
public class MesDashboardSummaryRespVO {
    // ========== ç”Ÿäº§ç»´åº¦ ==========
    @Schema(description = "批次总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "103")
    private Long batchTotal;
    @Schema(description = "草稿批次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long batchDraft;
    @Schema(description = "生产中批次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long batchProducing;
    @Schema(description = "已完成批次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long batchFinished;
    @Schema(description = "已作废批次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long batchCanceled;
    @Schema(description = "袋码总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1101")
    private Long bagCodeTotal;
    @Schema(description = "已生成袋码数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long bagCodeGenerated;
    @Schema(description = "已导出印刷袋码数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long bagCodePrinted;
    @Schema(description = "已作废袋码数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long bagCodeCanceled;
    @Schema(description = "托盘码总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
    private Long palletTotal;
    @Schema(description = "今日扫码入库袋数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long todayInboundBagCount;
    // ========== è´¨æ£€ç»´åº¦ ==========
    @Schema(description = "检验记录总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long qualityTotal;
    @Schema(description = "处理中检验记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long qualityPending;
    @Schema(description = "合格检验记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long qualityPass;
    @Schema(description = "不合格检验记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long qualityFail;
    @Schema(description = "检验合格率(%),如 95.50", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
    private BigDecimal qualityPassRate;
    // ========== åº“存维度 ==========
    @Schema(description = "当前库存总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private BigDecimal stockTotal;
    @Schema(description = "暂存库存量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private BigDecimal stockTemporary;
    @Schema(description = "合格库存量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private BigDecimal stockQualified;
    @Schema(description = "已出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private BigDecimal stockOutbound;
    @Schema(description = "当前低库存预警记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long lowStockCount;
    // ========== å‡ºåº“维度 ==========
    @Schema(description = "出库单总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long outboundOrderTotal;
    @Schema(description = "出库总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private BigDecimal outboundQuantityTotal;
    @Schema(description = "今日出库量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private BigDecimal todayOutboundQuantity;
    // ========== è¿½æº¯ç»´åº¦ ==========
    @Schema(description = "扫码总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "7")
    private Long scanTotal;
    @Schema(description = "今日扫码数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long todayScanCount;
    @Schema(description = "扫码成功数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
    private Long scanSuccessCount;
    @Schema(description = "窜货预警总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long crossRegionWarningTotal;
    @Schema(description = "待处理窜货预警数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long crossRegionPendingCount;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dashboard/vo/MesDashboardTraceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo;
import cn.iocoder.yudao.module.mes.controller.admin.trace.crossregion.vo.MesTraceCrossRegionSummaryRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * ç®¡ç†åŽå° - ç®¡ç†é©¾é©¶èˆ±è¿½æº¯ç»´åº¦ç»Ÿè®¡ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç®¡ç†é©¾é©¶èˆ±è¿½æº¯ç»´åº¦ç»Ÿè®¡ Response VO")
@Data
public class MesDashboardTraceRespVO {
    @Schema(description = "消费者扫码按日趋势", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardDailyCountRespVO> scanTrend;
    @Schema(description = "扫码省份分布", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardNameCountRespVO> provinceDistribution;
    @Schema(description = "扫码渠道分布(微信/支付宝/浏览器)", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<MesDashboardNameCountRespVO> channelDistribution;
    @Schema(description = "窜货预警汇总", requiredMode = Schema.RequiredMode.REQUIRED)
    private MesTraceCrossRegionSummaryRespVO crossRegionSummary;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dashboard/MesDashboardMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.mes.dal.mysql.dashboard;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†é©¾é©¶èˆ±ç»Ÿè®¡ Mapper
 * <p>
 * æ‰‹å†™ SQL èšåˆç”Ÿäº§ã€è´¨æ£€ã€åº“存、出库、追溯五大维度核心指标。
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesDashboardMapper {
    // ========== ç”Ÿäº§ç»´åº¦ ==========
    /** æŒ‰çŠ¶æ€ç»Ÿè®¡æ‰¹æ¬¡æ•°é‡ï¼Œè¿”å›ž [{ "status": 10, "count": 5 }] */
    List<Map<String, Object>> selectBatchCountGroupByStatus();
    /** æŒ‰çŠ¶æ€ç»Ÿè®¡è¢‹ç æ•°é‡ï¼Œè¿”å›ž [{ "status": 10, "count": 5 }] */
    List<Map<String, Object>> selectBagCodeCountGroupByStatus();
    /** ç»Ÿè®¡æ‰˜ç›˜ç æ€»æ•° */
    Long selectPalletTotal();
    /** ç»Ÿè®¡ä»Šæ—¥æ‰«ç å…¥åº“袋数(扫码成功事件数量之和) */
    Long selectTodayInboundBagCount(@Param("beginTime") LocalDateTime beginTime);
    /** æŒ‰å¤©èšåˆæ–°å¢žæ‰¹æ¬¡æ•°ï¼Œè¿”回 [{ "date": "2026-08-13", "count": 1 }] */
    List<Map<String, Object>> selectDailyBatchCount(@Param("beginTime") LocalDateTime beginTime,
                                                    @Param("endTime") LocalDateTime endTime);
    /** æŒ‰å¤©èšåˆæ–°å¢žè¢‹ç æ•° */
    List<Map<String, Object>> selectDailyBagCodeCount(@Param("beginTime") LocalDateTime beginTime,
                                                      @Param("endTime") LocalDateTime endTime);
    /** æŒ‰å¤©èšåˆæ–°å¢žæ‰˜ç›˜ç æ•° */
    List<Map<String, Object>> selectDailyPalletCount(@Param("beginTime") LocalDateTime beginTime,
                                                     @Param("endTime") LocalDateTime endTime);
    /** æŒ‰å¤©èšåˆæ‰«ç å…¥åº“袋数(扫码成功事件数量之和),返回 [{ "date": "...", "quantity": 100 }] */
    List<Map<String, Object>> selectDailyInboundBagQuantity(@Param("beginTime") LocalDateTime beginTime,
                                                            @Param("endTime") LocalDateTime endTime);
    // ========== è´¨æ£€ç»´åº¦ ==========
    /** æŒ‰æ£€éªŒç»“果统计数量,返回 [{ "result": 1, "count": 5 }] */
    List<Map<String, Object>> selectQualityCountGroupByResult();
    /** æŒ‡æ ‡å‡å€¼ä¸Žåˆæ ¼çŽ‡ï¼Œè¿”å›ž [{ "indicatorCode", "checkCount", "averageValue", "passRate" }] */
    List<Map<String, Object>> selectIndicatorStatistics();
    // ========== åº“存维度 ==========
    /** æŒ‰åº“存状态汇总数量,返回 [{ "stockState": 10, "quantity": 100 }] */
    List<Map<String, Object>> selectStockGroupByState();
    /** æŒ‰ä»“库汇总数量,返回 [{ "name": "仓库A", "quantity": 100 }] */
    List<Map<String, Object>> selectStockGroupByWarehouse();
    /** æŒ‰å“ç§æ±‡æ€»æ•°é‡ï¼Œè¿”回 [{ "name": "棉花", "quantity": 100 }] */
    List<Map<String, Object>> selectStockGroupByItem();
    // ========== å‡ºåº“维度 ==========
    /** å‡ºåº“汇总(单数/总量/今日出库量),返回单行 Map:{ orderTotal, quantityTotal, todayQuantity } */
    Map<String, Object> selectOutboundSummary(@Param("beginTime") LocalDateTime beginTime);
    /** æŒ‰å¤©èšåˆå‡ºåº“量,返回 [{ "date": "...", "quantity": 100 }] */
    List<Map<String, Object>> selectOutboundDailyQuantity(@Param("beginTime") LocalDateTime beginTime,
                                                          @Param("endTime") LocalDateTime endTime);
    /** æŒ‰ç»é”€å•†æ±‡æ€»å‡ºåº“量(TOP 20),返回 [{ "name": "经销商A", "quantity": 100 }] */
    List<Map<String, Object>> selectOutboundGroupByClient();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dashboard/MesDashboardService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.mes.service.dashboard;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardInventoryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardOutboundRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardProductionTrendRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardQualityRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardTraceRespVO;
import java.util.List;
/**
 * ç®¡ç†é©¾é©¶èˆ±ç»Ÿè®¡ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesDashboardService {
    /**
     * èŽ·å¾—ç®¡ç†é©¾é©¶èˆ±äº”ç»´åº¦æ±‡æ€»ç»Ÿè®¡
     *
     * @return æ±‡æ€»ç»Ÿè®¡
     */
    MesDashboardSummaryRespVO getSummary();
    /**
     * èŽ·å¾—ç”Ÿäº§è¶‹åŠ¿ï¼ˆè¿‘ N å¤©ï¼ŒæŒ‰æ—¥ï¼‰
     *
     * @param days å¤©æ•°
     * @return ç”Ÿäº§è¶‹åŠ¿
     */
    List<MesDashboardProductionTrendRespVO> getProductionTrend(Integer days);
    /**
     * èŽ·å¾—è´¨æ£€ç»´åº¦ç»Ÿè®¡
     *
     * @return è´¨æ£€ç»Ÿè®¡
     */
    MesDashboardQualityRespVO getQualityStatistics();
    /**
     * èŽ·å¾—åº“å­˜ç»´åº¦ç»Ÿè®¡
     *
     * @return åº“存统计
     */
    MesDashboardInventoryRespVO getInventoryStatistics();
    /**
     * èŽ·å¾—å‡ºåº“ç»´åº¦ç»Ÿè®¡ï¼ˆè¿‘ N å¤©ï¼‰
     *
     * @param days å¤©æ•°
     * @return å‡ºåº“统计
     */
    MesDashboardOutboundRespVO getOutboundStatistics(Integer days);
    /**
     * èŽ·å¾—è¿½æº¯ç»´åº¦ç»Ÿè®¡ï¼ˆè¿‘ N å¤©ï¼‰
     *
     * @param days å¤©æ•°
     * @return è¿½æº¯ç»Ÿè®¡
     */
    MesDashboardTraceRespVO getTraceStatistics(Integer days);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dashboard/MesDashboardServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,404 @@
package cn.iocoder.yudao.module.mes.service.dashboard;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardDailyCountRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardIndicatorAvgRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardInventoryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardNameCountRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardOutboundRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardProductionTrendRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardQualityRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dashboard.vo.MesDashboardTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanDistributionReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanDistributionRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanTrendRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.trace.crossregion.vo.MesTraceCrossRegionSummaryRespVO;
import cn.iocoder.yudao.module.mes.dal.mysql.dashboard.MesDashboardMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.service.trace.consumer.MesTraceConsumerScanStatisticsService;
import cn.iocoder.yudao.module.mes.service.trace.crossregion.MesTraceCrossRegionService;
import cn.iocoder.yudao.module.mes.service.wm.warning.MesWmStockWarningService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†é©¾é©¶èˆ±ç»Ÿè®¡ Service å®žçŽ°ç±»
 * <p>
 * æ•´åˆç”Ÿäº§ã€è´¨æ£€ã€åº“存、出库、追溯五大维度核心指标。
 *
 * @author è¶…级管理员
 */
@Service
public class MesDashboardServiceImpl implements MesDashboardService {
    /** æŒ‡æ ‡ç¼–码 -> æŒ‡æ ‡åç§° */
    private static final Map<String, String> INDICATOR_NAME_MAP = Map.of(
            "purity", "纯度",
            "cleanliness", "净度",
            "germination_rate", "发芽率",
            "moisture", "水分");
    @Resource
    private MesDashboardMapper dashboardMapper;
    @Resource
    private MesTraceConsumerScanStatisticsService traceScanStatisticsService;
    @Resource
    private MesTraceCrossRegionService traceCrossRegionService;
    @Resource
    private MesWmStockWarningService stockWarningService;
    @Override
    public MesDashboardSummaryRespVO getSummary() {
        MesDashboardSummaryRespVO summary = new MesDashboardSummaryRespVO();
        // 1. ç”Ÿäº§ç»´åº¦
        List<Map<String, Object>> batchStats = dashboardMapper.selectBatchCountGroupByStatus();
        summary.setBatchTotal(getTotalCount(batchStats));
        summary.setBatchDraft(getCountByStatus(batchStats, MesProBatchStatusEnum.DRAFT.getStatus()));
        summary.setBatchProducing(getCountByStatus(batchStats, MesProBatchStatusEnum.PRODUCING.getStatus()));
        summary.setBatchFinished(getCountByStatus(batchStats, MesProBatchStatusEnum.FINISHED.getStatus()));
        summary.setBatchCanceled(getCountByStatus(batchStats, MesProBatchStatusEnum.CANCELED.getStatus()));
        List<Map<String, Object>> bagCodeStats = dashboardMapper.selectBagCodeCountGroupByStatus();
        summary.setBagCodeTotal(getTotalCount(bagCodeStats));
        summary.setBagCodeGenerated(getCountByStatus(bagCodeStats, MesProBagCodeStatusEnum.GENERATED.getStatus()));
        summary.setBagCodePrinted(getCountByStatus(bagCodeStats, MesProBagCodeStatusEnum.PRINTED.getStatus()));
        summary.setBagCodeCanceled(getCountByStatus(bagCodeStats, MesProBagCodeStatusEnum.CANCELED.getStatus()));
        summary.setPalletTotal(getLong(dashboardMapper.selectPalletTotal()));
        summary.setTodayInboundBagCount(getLong(dashboardMapper.selectTodayInboundBagCount(LocalDateTimeUtils.getToday())));
        // 2. è´¨æ£€ç»´åº¦
        List<Map<String, Object>> qualityStats = dashboardMapper.selectQualityCountGroupByResult();
        long qualityPending = getCountByResult(qualityStats, 0);
        long qualityPass = getCountByResult(qualityStats, 1);
        long qualityFail = getCountByResult(qualityStats, 2);
        summary.setQualityTotal(qualityPending + qualityPass + qualityFail);
        summary.setQualityPending(qualityPending);
        summary.setQualityPass(qualityPass);
        summary.setQualityFail(qualityFail);
        summary.setQualityPassRate(calcPercent(qualityPass, qualityPass + qualityFail));
        // 3. åº“存维度
        List<Map<String, Object>> stockStateStats = dashboardMapper.selectStockGroupByState();
        BigDecimal stockTemporary = getStockQuantityByState(stockStateStats, MesWmStockStateEnum.TEMP.getState());
        BigDecimal stockQualified = getStockQuantityByState(stockStateStats, MesWmStockStateEnum.QUALIFIED.getState());
        BigDecimal stockOutbound = getStockQuantityByState(stockStateStats, MesWmStockStateEnum.OUT.getState());
        summary.setStockTemporary(stockTemporary);
        summary.setStockQualified(stockQualified);
        summary.setStockOutbound(stockOutbound);
        summary.setStockTotal(stockTemporary.add(stockQualified).add(stockOutbound));
        summary.setLowStockCount((long) stockWarningService.getCurrentWarnings().size());
        // 4. å‡ºåº“维度
        Map<String, Object> outboundSummary = dashboardMapper.selectOutboundSummary(LocalDateTimeUtils.getToday());
        summary.setOutboundOrderTotal(getLong(outboundSummary == null ? null : outboundSummary.get("orderTotal")));
        summary.setOutboundQuantityTotal(getDecimal(outboundSummary == null ? null : outboundSummary.get("quantityTotal")));
        summary.setTodayOutboundQuantity(getDecimal(outboundSummary == null ? null : outboundSummary.get("todayQuantity")));
        // 5. è¿½æº¯ç»´åº¦
        MesTraceConsumerScanSummaryRespVO scanAll = traceScanStatisticsService.getSummary(null, null);
        MesTraceConsumerScanSummaryRespVO scanToday = traceScanStatisticsService.getSummary(LocalDateTimeUtils.getToday(), null);
        MesTraceCrossRegionSummaryRespVO crossRegion = traceCrossRegionService.getWarningSummary(null, null);
        summary.setScanTotal(scanAll == null ? 0L : getLong(scanAll.getTotal()));
        summary.setScanSuccessCount(scanAll == null ? 0L : getLong(scanAll.getSuccess()));
        summary.setTodayScanCount(scanToday == null ? 0L : getLong(scanToday.getTotal()));
        summary.setCrossRegionWarningTotal(crossRegion == null ? 0L : getLong(crossRegion.getTotal()));
        summary.setCrossRegionPendingCount(crossRegion == null ? 0L : getLong(crossRegion.getPending()));
        return summary;
    }
    @Override
    public List<MesDashboardProductionTrendRespVO> getProductionTrend(Integer days) {
        LocalDate endDate = LocalDate.now().plusDays(1);
        LocalDate startDate = endDate.minusDays(days);
        LocalDateTime beginTime = startDate.atStartOfDay();
        LocalDateTime endTime = endDate.atStartOfDay();
        Map<String, Map<String, Object>> batchMap = toDateMap(dashboardMapper.selectDailyBatchCount(beginTime, endTime));
        Map<String, Map<String, Object>> bagCodeMap = toDateMap(dashboardMapper.selectDailyBagCodeCount(beginTime, endTime));
        Map<String, Map<String, Object>> palletMap = toDateMap(dashboardMapper.selectDailyPalletCount(beginTime, endTime));
        Map<String, Map<String, Object>> inboundMap = toDateMap(dashboardMapper.selectDailyInboundBagQuantity(beginTime, endTime));
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        List<MesDashboardProductionTrendRespVO> result = new ArrayList<>();
        for (LocalDate d = startDate; d.isBefore(endDate); d = d.plusDays(1)) {
            String dateStr = d.format(fmt);
            result.add(new MesDashboardProductionTrendRespVO(
                    dateStr,
                    getLongFromMap(batchMap.get(dateStr), "count"),
                    getLongFromMap(bagCodeMap.get(dateStr), "count"),
                    getLongFromMap(palletMap.get(dateStr), "count"),
                    getDecimalFromMap(inboundMap.get(dateStr), "quantity")));
        }
        return result;
    }
    @Override
    public MesDashboardQualityRespVO getQualityStatistics() {
        MesDashboardQualityRespVO vo = new MesDashboardQualityRespVO();
        // 1. æ£€éªŒç»“果分布
        List<MesDashboardNameCountRespVO> resultDistribution = new ArrayList<>();
        for (Map<String, Object> stat : dashboardMapper.selectQualityCountGroupByResult()) {
            int result = ((Number) stat.get("result")).intValue();
            resultDistribution.add(new MesDashboardNameCountRespVO(qualityResultName(result), getLong(stat.get("count"))));
        }
        vo.setResultDistribution(resultDistribution);
        // 2. å››é¡¹æŒ‡æ ‡å‡å€¼ä¸Žåˆæ ¼çއ
        List<MesDashboardIndicatorAvgRespVO> indicatorList = new ArrayList<>();
        for (Map<String, Object> stat : dashboardMapper.selectIndicatorStatistics()) {
            MesDashboardIndicatorAvgRespVO item = new MesDashboardIndicatorAvgRespVO();
            item.setIndicatorCode((String) stat.get("indicatorCode"));
            item.setIndicatorName(indicatorName(item.getIndicatorCode()));
            item.setCheckCount(getLong(stat.get("checkCount")));
            item.setAverageValue(getDecimal(stat.get("averageValue")));
            item.setPassRate(getDecimal(stat.get("passRate")));
            indicatorList.add(item);
        }
        vo.setIndicatorList(indicatorList);
        return vo;
    }
    @Override
    public MesDashboardInventoryRespVO getInventoryStatistics() {
        MesDashboardInventoryRespVO vo = new MesDashboardInventoryRespVO();
        vo.setWarehouseDistribution(toNameCountList(dashboardMapper.selectStockGroupByWarehouse()));
        vo.setItemDistribution(toNameCountList(dashboardMapper.selectStockGroupByItem()));
        // åº“存状态分布
        List<MesDashboardNameCountRespVO> stateDistribution = new ArrayList<>();
        for (Map<String, Object> stat : dashboardMapper.selectStockGroupByState()) {
            int stockState = ((Number) stat.get("stockState")).intValue();
            stateDistribution.add(new MesDashboardNameCountRespVO(stockStateName(stockState),
                    getDecimal(stat.get("quantity")).longValue()));
        }
        vo.setStateDistribution(stateDistribution);
        return vo;
    }
    @Override
    public MesDashboardOutboundRespVO getOutboundStatistics(Integer days) {
        LocalDate endDate = LocalDate.now().plusDays(1);
        LocalDate startDate = endDate.minusDays(days);
        LocalDateTime beginTime = startDate.atStartOfDay();
        LocalDateTime endTime = endDate.atStartOfDay();
        MesDashboardOutboundRespVO vo = new MesDashboardOutboundRespVO();
        // å‡ºåº“按日趋势(日补零)
        Map<String, Map<String, Object>> trendMap = toDateMap(dashboardMapper.selectOutboundDailyQuantity(beginTime, endTime));
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        List<MesDashboardDailyCountRespVO> trend = new ArrayList<>();
        for (LocalDate d = startDate; d.isBefore(endDate); d = d.plusDays(1)) {
            String dateStr = d.format(fmt);
            trend.add(new MesDashboardDailyCountRespVO(dateStr, getDecimalFromMap(trendMap.get(dateStr), "quantity")));
        }
        vo.setTrend(trend);
        // ç»é”€å•†å‡ºåº“分布
        vo.setDealerDistribution(toNameCountList(dashboardMapper.selectOutboundGroupByClient()));
        return vo;
    }
    @Override
    public MesDashboardTraceRespVO getTraceStatistics(Integer days) {
        LocalDate endDate = LocalDate.now().plusDays(1);
        LocalDate startDate = endDate.minusDays(days);
        LocalDateTime beginTime = startDate.atStartOfDay();
        LocalDateTime endTime = endDate.atStartOfDay();
        MesDashboardTraceRespVO vo = new MesDashboardTraceRespVO();
        // 1. æ‰«ç æŒ‰æ—¥è¶‹åŠ¿
        List<MesTraceConsumerScanTrendRespVO> trend = traceScanStatisticsService.getTrend(beginTime, endTime);
        Map<String, Long> trendMap = new LinkedHashMap<>();
        if (trend != null) {
            for (MesTraceConsumerScanTrendRespVO item : trend) {
                trendMap.put(item.getDate(), getLong(item.getTotal()));
            }
        }
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        List<MesDashboardDailyCountRespVO> scanTrend = new ArrayList<>();
        for (LocalDate d = startDate; d.isBefore(endDate); d = d.plusDays(1)) {
            String dateStr = d.format(fmt);
            scanTrend.add(new MesDashboardDailyCountRespVO(dateStr, new BigDecimal(trendMap.getOrDefault(dateStr, 0L))));
        }
        vo.setScanTrend(scanTrend);
        // 2. çœä»½åˆ†å¸ƒ
        vo.setProvinceDistribution(toDistribution(traceScanStatisticsService.getDistribution(
                buildDistributionReqVO("province", beginTime, endTime))));
        // 3. æ¸ é“分布
        vo.setChannelDistribution(toDistribution(traceScanStatisticsService.getDistribution(
                buildDistributionReqVO("channel", beginTime, endTime))));
        // 4. çªœè´§é¢„警汇总
        vo.setCrossRegionSummary(traceCrossRegionService.getWarningSummary(null, null));
        return vo;
    }
    // ========== å·¥å…·æ–¹æ³• ==========
    private MesTraceConsumerScanDistributionReqVO buildDistributionReqVO(String groupBy, LocalDateTime beginTime, LocalDateTime endTime) {
        MesTraceConsumerScanDistributionReqVO reqVO = new MesTraceConsumerScanDistributionReqVO();
        reqVO.setGroupBy(groupBy);
        reqVO.setStartTime(beginTime);
        reqVO.setEndTime(endTime);
        return reqVO;
    }
    private List<MesDashboardNameCountRespVO> toDistribution(List<MesTraceConsumerScanDistributionRespVO> distribution) {
        List<MesDashboardNameCountRespVO> result = new ArrayList<>();
        if (distribution == null) {
            return result;
        }
        for (MesTraceConsumerScanDistributionRespVO item : distribution) {
            result.add(new MesDashboardNameCountRespVO(item.getName(), getLong(item.getCount())));
        }
        return result;
    }
    private List<MesDashboardNameCountRespVO> toNameCountList(List<Map<String, Object>> stats) {
        List<MesDashboardNameCountRespVO> result = new ArrayList<>();
        if (stats == null) {
            return result;
        }
        for (Map<String, Object> stat : stats) {
            result.add(new MesDashboardNameCountRespVO((String) stat.get("name"), getDecimal(stat.get("quantity")).longValue()));
        }
        return result;
    }
    private Map<String, Map<String, Object>> toDateMap(List<Map<String, Object>> stats) {
        Map<String, Map<String, Object>> map = new LinkedHashMap<>();
        if (stats == null) {
            return map;
        }
        for (Map<String, Object> stat : stats) {
            map.put((String) stat.get("date"), stat);
        }
        return map;
    }
    private Long getCountByStatus(List<Map<String, Object>> stats, Integer status) {
        if (stats == null) {
            return 0L;
        }
        for (Map<String, Object> stat : stats) {
            if (((Number) stat.get("status")).intValue() == status) {
                return getLong(stat.get("count"));
            }
        }
        return 0L;
    }
    private Long getCountByResult(List<Map<String, Object>> stats, Integer result) {
        if (stats == null) {
            return 0L;
        }
        for (Map<String, Object> stat : stats) {
            if (((Number) stat.get("result")).intValue() == result) {
                return getLong(stat.get("count"));
            }
        }
        return 0L;
    }
    private BigDecimal getStockQuantityByState(List<Map<String, Object>> stats, Integer stockState) {
        if (stats == null) {
            return BigDecimal.ZERO;
        }
        for (Map<String, Object> stat : stats) {
            if (((Number) stat.get("stockState")).intValue() == stockState) {
                return getDecimal(stat.get("quantity"));
            }
        }
        return BigDecimal.ZERO;
    }
    private Long getTotalCount(List<Map<String, Object>> stats) {
        long total = 0L;
        if (stats != null) {
            for (Map<String, Object> stat : stats) {
                total += getLong(stat.get("count"));
            }
        }
        return total;
    }
    private Long getLongFromMap(Map<String, Object> map, String key) {
        return map == null ? 0L : getLong(map.get(key));
    }
    private BigDecimal getDecimalFromMap(Map<String, Object> map, String key) {
        return map == null ? BigDecimal.ZERO : getDecimal(map.get(key));
    }
    private Long getLong(Object obj) {
        if (obj == null) {
            return 0L;
        }
        if (obj instanceof Number) {
            return ((Number) obj).longValue();
        }
        return Long.valueOf(obj.toString());
    }
    private BigDecimal getDecimal(Object obj) {
        if (obj == null) {
            return BigDecimal.ZERO;
        }
        if (obj instanceof BigDecimal) {
            return (BigDecimal) obj;
        }
        if (obj instanceof Number) {
            return BigDecimal.valueOf(((Number) obj).doubleValue());
        }
        return new BigDecimal(obj.toString());
    }
    private BigDecimal calcPercent(long pass, long total) {
        if (total == 0) {
            return BigDecimal.ZERO;
        }
        return BigDecimal.valueOf(pass)
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
    }
    private String indicatorName(String code) {
        return code == null ? "未知指标" : INDICATOR_NAME_MAP.getOrDefault(code, code);
    }
    private String qualityResultName(int result) {
        return switch (result) {
            case 0 -> "处理中";
            case 1 -> "合格";
            case 2 -> "不合格";
            default -> "未知";
        };
    }
    private String stockStateName(int stockState) {
        return switch (stockState) {
            case 10 -> "暂存库";
            case 20 -> "合格库";
            case 30 -> "已出库";
            default -> "未知状态";
        };
    }
}
yudao-module-mes/src/main/resources/mapper/dashboard/MesDashboardMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
<?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.mes.dal.mysql.dashboard.MesDashboardMapper">
    <!-- ========== ç”Ÿäº§ç»´åº¦ ========== -->
    <!-- æŒ‰çŠ¶æ€ç»Ÿè®¡æ‰¹æ¬¡æ•°é‡ -->
    <select id="selectBatchCountGroupByStatus" resultType="java.util.Map">
        SELECT status, COUNT(*) AS count
        FROM mes_pro_batch
        WHERE deleted = 0
        GROUP BY status
    </select>
    <!-- æŒ‰çŠ¶æ€ç»Ÿè®¡è¢‹ç æ•°é‡ -->
    <select id="selectBagCodeCountGroupByStatus" resultType="java.util.Map">
        SELECT status, COUNT(*) AS count
        FROM mes_pro_bag_code
        WHERE deleted = 0
        GROUP BY status
    </select>
    <!-- ç»Ÿè®¡æ‰˜ç›˜ç æ€»æ•° -->
    <select id="selectPalletTotal" resultType="java.lang.Long">
        SELECT COUNT(*)
        FROM mes_pro_pallet
        WHERE deleted = 0
    </select>
    <!-- ç»Ÿè®¡ä»Šæ—¥æ‰«ç å…¥åº“袋数(扫码成功事件数量之和) -->
    <select id="selectTodayInboundBagCount" resultType="java.lang.Long">
        SELECT IFNULL(ROUND(SUM(quantity)), 0)
        FROM mes_pro_scan_event
        WHERE status = 'SUCCESS'
          AND event_time &gt;= #{beginTime}
    </select>
    <!-- æŒ‰å¤©èšåˆæ–°å¢žæ‰¹æ¬¡æ•° -->
    <select id="selectDailyBatchCount" resultType="java.util.Map">
        SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS date, COUNT(*) AS count
        FROM mes_pro_batch
        WHERE deleted = 0
          AND create_time &gt;= #{beginTime}
          <if test="endTime != null">
              AND create_time &lt; #{endTime}
          </if>
        GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
        ORDER BY date
    </select>
    <!-- æŒ‰å¤©èšåˆæ–°å¢žè¢‹ç æ•° -->
    <select id="selectDailyBagCodeCount" resultType="java.util.Map">
        SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS date, COUNT(*) AS count
        FROM mes_pro_bag_code
        WHERE deleted = 0
          AND create_time &gt;= #{beginTime}
          <if test="endTime != null">
              AND create_time &lt; #{endTime}
          </if>
        GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
        ORDER BY date
    </select>
    <!-- æŒ‰å¤©èšåˆæ–°å¢žæ‰˜ç›˜ç æ•° -->
    <select id="selectDailyPalletCount" resultType="java.util.Map">
        SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS date, COUNT(*) AS count
        FROM mes_pro_pallet
        WHERE deleted = 0
          AND create_time &gt;= #{beginTime}
          <if test="endTime != null">
              AND create_time &lt; #{endTime}
          </if>
        GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
        ORDER BY date
    </select>
    <!-- æŒ‰å¤©èšåˆæ‰«ç å…¥åº“袋数(扫码成功事件数量之和) -->
    <select id="selectDailyInboundBagQuantity" resultType="java.util.Map">
        SELECT DATE_FORMAT(event_time, '%Y-%m-%d') AS date,
               IFNULL(SUM(quantity), 0)              AS quantity
        FROM mes_pro_scan_event
        WHERE status = 'SUCCESS'
          AND event_time &gt;= #{beginTime}
          <if test="endTime != null">
              AND event_time &lt; #{endTime}
          </if>
        GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d')
        ORDER BY date
    </select>
    <!-- ========== è´¨æ£€ç»´åº¦ ========== -->
    <!-- æŒ‰æ£€éªŒç»“果统计数量(0处理中 1合格 2不合格) -->
    <select id="selectQualityCountGroupByResult" resultType="java.util.Map">
        SELECT result, COUNT(*) AS count
        FROM mes_qc_seed_quality
        WHERE deleted = 0
        GROUP BY result
    </select>
    <!-- æŒ‡æ ‡å‡å€¼ä¸Žåˆæ ¼çŽ‡ï¼ˆå€¼è½åœ¨æ ‡å‡†åŒºé—´ [min_value, max_value] å†…视为合格) -->
    <select id="selectIndicatorStatistics" resultType="java.util.Map">
        SELECT indicator_code,
               COUNT(*)                                                                        AS checkCount,
               ROUND(AVG(value), 6)                                                           AS averageValue,
               IFNULL(ROUND(SUM(CASE WHEN value &gt;= min_value AND value &lt;= max_value THEN 1 ELSE 0 END)
                            / COUNT(*) * 100, 2), 0)                                          AS passRate
        FROM mes_qc_seed_quality_indicator
        WHERE deleted = 0
        GROUP BY indicator_code
        ORDER BY indicator_code
    </select>
    <!-- ========== åº“存维度 ========== -->
    <!-- æŒ‰åº“存状态汇总数量(10暂存库 20合格库 30已出库) -->
    <select id="selectStockGroupByState" resultType="java.util.Map">
        SELECT IFNULL(stock_state, -1) AS stockState,
               IFNULL(SUM(quantity), 0) AS quantity
        FROM mes_wm_material_stock
        WHERE deleted = 0
        GROUP BY stock_state
    </select>
    <!-- æŒ‰ä»“库汇总数量 -->
    <select id="selectStockGroupByWarehouse" resultType="java.util.Map">
        SELECT IFNULL(w.name, '未知仓库') AS name,
               IFNULL(SUM(s.quantity), 0) AS quantity
        FROM mes_wm_material_stock s
                 LEFT JOIN mes_wm_warehouse w ON w.id = s.warehouse_id AND w.deleted = 0
        WHERE s.deleted = 0
        GROUP BY s.warehouse_id
        ORDER BY quantity DESC
    </select>
    <!-- æŒ‰å“ç§æ±‡æ€»æ•°é‡ -->
    <select id="selectStockGroupByItem" resultType="java.util.Map">
        SELECT IFNULL(i.name, '未知品种') AS name,
               IFNULL(SUM(s.quantity), 0) AS quantity
        FROM mes_wm_material_stock s
                 LEFT JOIN mes_md_item i ON i.id = s.item_id AND i.deleted = 0
        WHERE s.deleted = 0
        GROUP BY s.item_id
        ORDER BY quantity DESC
    </select>
    <!-- ========== å‡ºåº“维度 ========== -->
    <!-- å‡ºåº“汇总(单数/总量/今日出库量) -->
    <select id="selectOutboundSummary" resultType="java.util.Map">
        SELECT COUNT(DISTINCT s.id)                    AS orderTotal,
               IFNULL(SUM(l.quantity), 0)              AS quantityTotal,
               IFNULL(SUM(CASE WHEN s.sales_date &gt;= #{beginTime} THEN l.quantity ELSE 0 END), 0) AS todayQuantity
        FROM mes_wm_product_sales s
                 LEFT JOIN mes_wm_product_sales_line l ON l.sales_id = s.id AND l.deleted = 0
        WHERE s.deleted = 0
    </select>
    <!-- æŒ‰å¤©èšåˆå‡ºåº“量 -->
    <select id="selectOutboundDailyQuantity" resultType="java.util.Map">
        SELECT DATE_FORMAT(s.sales_date, '%Y-%m-%d') AS date,
               IFNULL(SUM(l.quantity), 0)              AS quantity
        FROM mes_wm_product_sales s
                 LEFT JOIN mes_wm_product_sales_line l ON l.sales_id = s.id AND l.deleted = 0
        WHERE s.deleted = 0
          AND s.sales_date &gt;= #{beginTime}
          <if test="endTime != null">
              AND s.sales_date &lt; #{endTime}
          </if>
        GROUP BY DATE_FORMAT(s.sales_date, '%Y-%m-%d')
        ORDER BY date
    </select>
    <!-- æŒ‰ç»é”€å•†æ±‡æ€»å‡ºåº“量(TOP 20) -->
    <select id="selectOutboundGroupByClient" resultType="java.util.Map">
        SELECT COALESCE(NULLIF(c.name, ''), CONCAT('经销商#', s.client_id), CONCAT('客户ID:', s.client_id)) AS name,
               IFNULL(SUM(l.quantity), 0) AS quantity
        FROM mes_wm_product_sales s
                 LEFT JOIN mes_wm_product_sales_line l ON l.sales_id = s.id AND l.deleted = 0
                 LEFT JOIN crm_customer c ON c.id = s.client_id AND c.deleted = 0
        WHERE s.deleted = 0
        GROUP BY s.client_id, c.name
        ORDER BY quantity DESC
        LIMIT 20
    </select>
</mapper>