From 864037580c3e4d9f8c756b66ef40464d23262e6c Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 20 五月 2026 17:53:42 +0800
Subject: [PATCH] feat(home): 添加生产看板功能并优化订单查询性能

---
 doc/20260520_首页生产看板性能优化前端变更文档.md                                     |  107 ++++++++
 src/main/java/com/ruoyi/home/controller/HomeController.java          |  335 +++++++++++++++++++++++++++
 doc/20260520_首页生产看板前端联调文档.md                                         |  160 +++++++++++++
 src/main/resources/mapper/production/ProductionOrderMapper.xml       |   57 ++++
 src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java |   13 +
 5 files changed, 670 insertions(+), 2 deletions(-)

diff --git "a/doc/20260520_\351\246\226\351\241\265\347\224\237\344\272\247\347\234\213\346\235\277\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260520_\351\246\226\351\241\265\347\224\237\344\272\247\347\234\213\346\235\277\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..aaa6055
--- /dev/null
+++ "b/doc/20260520_\351\246\226\351\241\265\347\224\237\344\272\247\347\234\213\346\235\277\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,160 @@
+# 棣栭〉鐢熶骇鐪嬫澘鍓嶇鑱旇皟鏂囨。
+
+鏇存柊鏃堕棿锛�2026-05-20  
+妯″潡锛歚/home`锛堥椤碉級
+
+## 1. 鎺ュ彛娓呭崟
+
+1. `GET /home/productionOverview`锛氱敓浜ф�昏  
+2. `GET /home/productionRealtimeBoard`锛氱敓浜у疄鏃剁湅鏉�  
+3. `GET /home/productionOrderProgress`锛氱敓浜ц鍗曡繘搴�  
+4. `GET /home/todayProductionPlan`锛氫粖鏃ョ敓浜ц鍒�
+
+鎵�鏈夋帴鍙g粺涓�杩斿洖 `AjaxResult`锛�
+
+```json
+{
+  "code": 200,
+  "msg": "鎿嶄綔鎴愬姛",
+  "data": {}
+}
+```
+
+## 2. 鐢熶骇鎬昏
+
+### 2.1 璇锋眰
+
+```http
+GET /home/productionOverview
+```
+
+### 2.2 杩斿洖 `data`
+
+```json
+{
+  "totalOutput": 1280.00,
+  "totalScrap": 25.00,
+  "yieldRate": 98.08
+}
+```
+
+瀛楁璇存槑锛�
+
+- `totalOutput`锛氱疮璁′骇鍑猴紙浠讹紝鍚堟牸鏁帮級
+- `totalScrap`锛氱疮璁℃姤搴燂紙浠讹級
+- `yieldRate`锛氳壇鐜囷紙0-100锛屽墠绔睍绀烘椂鍙嫾鎺� `%`锛�
+
+## 3. 鐢熶骇瀹炴椂鐪嬫澘
+
+### 3.1 璇锋眰
+
+```http
+GET /home/productionRealtimeBoard
+```
+
+### 3.2 杩斿洖 `data`
+
+```json
+{
+  "deviceOee": {
+    "value": 74.00,
+    "compareYesterday": 2.50
+  },
+  "orderAchievementRate": {
+    "value": 81.30,
+    "compareYesterday": -1.20
+  },
+  "defectRate": {
+    "value": 1.40,
+    "compareYesterday": 0.30
+  }
+}
+```
+
+瀛楁璇存槑锛�
+
+- `value`锛氬綋鏃ユ寚鏍囧�硷紙0-100锛�
+- `compareYesterday`锛氳緝鏄ㄦ棩鍙樺寲鍊硷紙鍙鍙礋锛涘墠绔寜姝h礋鍐冲畾绠ご鏂瑰悜鍜岄鑹诧級
+
+## 4. 鐢熶骇璁㈠崟杩涘害
+
+### 4.1 璇锋眰
+
+```http
+GET /home/productionOrderProgress?tab=all&pageNum=1&pageSize=10
+```
+
+鍙傛暟锛�
+
+- `tab`锛歚all` / `inProgress` / `completed` / `paused`
+- `pageNum`锛氶〉鐮侊紙榛樿 `1`锛�
+- `pageSize`锛氭瘡椤垫潯鏁帮紙榛樿 `10`锛屾渶澶� `50`锛�
+
+### 4.2 杩斿洖 `data`
+
+```json
+{
+  "tab": "all",
+  "total": 24,
+  "pageNum": 1,
+  "pageSize": 10,
+  "inProgressCount": 6,
+  "completedCount": 12,
+  "pausedCount": 2,
+  "records": [
+    {
+      "orderNo": "MO-20260518-001",
+      "productName": "鏅鸿兘鎺у埗鍣�",
+      "plannedQuantity": 1000.00,
+      "completedQuantity": 860.00,
+      "completionRate": 86.00,
+      "dueDate": "2026-05-20",
+      "status": 2,
+      "statusLabel": "杩涜涓�"
+    }
+  ]
+}
+```
+
+瀛楁璇存槑锛�
+
+- `completionRate`锛氬畬鎴愮巼锛�0-100锛�
+- `status`锛氬悗绔姸鎬佺爜锛坄1`寰呭紑濮嬶紝`2`杩涜涓紝`3`宸插畬鎴愶紝`4`宸叉殏鍋滐級
+- `statusLabel`锛氱姸鎬佷腑鏂囧睍绀哄��
+
+## 5. 浠婃棩鐢熶骇璁″垝
+
+### 5.1 璇锋眰
+
+```http
+GET /home/todayProductionPlan?limit=4
+```
+
+鍙傛暟锛�
+
+- `limit`锛氳繑鍥炴潯鏁帮紙榛樿 `4`锛屾渶澶� `20`锛�
+
+### 5.2 杩斿洖 `data`
+
+```json
+{
+  "total": 9,
+  "records": [
+    {
+      "orderNo": "MO-20260518-004",
+      "productName": "缁撴瀯浠禔",
+      "plannedQuantity": 1200.00,
+      "dueDate": "2026-05-15",
+      "status": 2,
+      "statusLabel": "杩涜涓�"
+    }
+  ]
+}
+```
+
+## 6. 鍓嶇灞曠ず绾﹀畾
+
+- 鐧惧垎姣斿瓧娈电粺涓�鏄暟鍊硷紙濡� `74.00`锛夛紝鍓嶇鑷鎷兼帴 `%`銆�
+- 鎵�鏈夋暟鍊间繚鐣欎袱浣嶅皬鏁般��
+- `dueDate` 鍙兘涓� `null`锛屽墠绔渶鍏滃簳灞曠ず锛堝 `--`锛夈��
+- `compareYesterday` 姝h礋閮藉彲鑳藉嚭鐜帮紝寤鸿鎸� `>0` 涓婂崌銆乣<0` 涓嬮檷銆乣=0` 鎸佸钩澶勭悊銆�
diff --git "a/doc/20260520_\351\246\226\351\241\265\347\224\237\344\272\247\347\234\213\346\235\277\346\200\247\350\203\275\344\274\230\345\214\226\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md" "b/doc/20260520_\351\246\226\351\241\265\347\224\237\344\272\247\347\234\213\346\235\277\346\200\247\350\203\275\344\274\230\345\214\226\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..730dbf1
--- /dev/null
+++ "b/doc/20260520_\351\246\226\351\241\265\347\224\237\344\272\247\347\234\213\346\235\277\346\200\247\350\203\275\344\274\230\345\214\226\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md"
@@ -0,0 +1,107 @@
+# 棣栭〉鐢熶骇鐪嬫澘鎬ц兘浼樺寲鍓嶇鍙樻洿鏂囨。
+
+鏇存柊鏃堕棿锛�2026-05-20  
+閫傜敤椤甸潰锛氶椤�  
+娑夊強鍖哄潡锛�
+
+1. 鐢熶骇璁㈠崟杩涘害
+2. 浠婃棩鐢熶骇璁″垝
+
+## 1. 鏈浼樺寲鐩爣
+
+閽堝澶ф暟鎹噺鍦烘櫙锛堣鍗曟暟閲忓銆佺敓浜у巻鍙查暱锛変紭鍖栨煡璇㈡�ц兘锛岄檷浣庨椤垫帴鍙e搷搴旀椂闂村拰鍐呭瓨鍗犵敤銆�
+
+## 2. 娑夊強鎺ュ彛
+
+1. `GET /home/productionOrderProgress`
+2. `GET /home/todayProductionPlan`
+
+## 3. 鍓嶇鏄惁闇�瑕佹敼浠g爜
+
+缁撹锛�**鏃犲己鍒舵敼鍔紝鎺ュ彛鍏ュ弬涓庤繑鍥炵粨鏋勪繚鎸佸吋瀹�**銆�  
+浣犵幇鏈夐〉闈㈠彲浠ョ洿鎺ヨ仈璋冿紝涓嶉渶瑕佹敼瀛楁鏄犲皠銆�
+
+## 4. 鎺ュ彛璇存槑锛堜繚鎸佷笉鍙橈級
+
+### 4.1 鐢熶骇璁㈠崟杩涘害
+
+璇锋眰锛�
+
+```http
+GET /home/productionOrderProgress?tab=all&pageNum=1&pageSize=10
+```
+
+鍙傛暟锛�
+
+- `tab`锛歚all` / `inProgress` / `completed` / `paused`
+- `pageNum`锛氶〉鐮侊紝榛樿 `1`
+- `pageSize`锛氭瘡椤垫潯鏁帮紝榛樿 `10`锛屾渶澶� `50`
+
+杩斿洖 `data`锛堢粨鏋勪笉鍙橈級锛�
+
+```json
+{
+  "tab": "all",
+  "total": 1200,
+  "pageNum": 1,
+  "pageSize": 10,
+  "inProgressCount": 180,
+  "completedCount": 900,
+  "pausedCount": 20,
+  "records": [
+    {
+      "orderNo": "MO-20260518-001",
+      "productName": "鏅鸿兘鎺у埗鍣�",
+      "plannedQuantity": 1000.00,
+      "completedQuantity": 860.00,
+      "completionRate": 86.00,
+      "dueDate": "2026-05-20",
+      "status": 2,
+      "statusLabel": "杩涜涓�"
+    }
+  ]
+}
+```
+
+### 4.2 浠婃棩鐢熶骇璁″垝
+
+璇锋眰锛�
+
+```http
+GET /home/todayProductionPlan?limit=4
+```
+
+鍙傛暟锛�
+
+- `limit`锛氳繑鍥炴潯鏁帮紝榛樿 `4`锛屾渶澶� `20`
+
+杩斿洖 `data`锛堢粨鏋勪笉鍙橈級锛�
+
+```json
+{
+  "total": 230,
+  "records": [
+    {
+      "orderNo": "MO-20260518-004",
+      "productName": "缁撴瀯浠禔",
+      "plannedQuantity": 1200.00,
+      "dueDate": "2026-05-15",
+      "status": 2,
+      "statusLabel": "杩涜涓�"
+    }
+  ]
+}
+```
+
+## 5. 鍚庣浼樺寲鐐癸紙渚涘墠绔煡鎮夛級
+
+1. 璁㈠崟杩涘害涓庝粖鏃ヨ鍒掓敼涓鸿交閲� SQL锛屼粎鏌ヨ棣栭〉蹇呴渶瀛楁銆�  
+2. 鍘绘帀浜嗛椤垫煡璇㈣矾寰勪腑涓嶅繀瑕佺殑澶у叧鑱斻�佸浘鐗囧~鍏呭拰瀵硅薄瑁呴厤銆�  
+3. 鐘舵�佺粺璁℃敼涓烘暟鎹簱鑱氬悎璁℃暟锛屼笉鍐嶉�愭潯鎷夊彇璁$畻銆�  
+4. 鍒嗛〉涓庢潯鏁颁笂闄愪繚鐣欙紙`pageSize <= 50`, `limit <= 20`锛夈��
+
+## 6. 鍓嶇寤鸿
+
+1. 鍒囨崲 `tab` 鏃朵繚鐣欑幇鏈夎皟鐢ㄦ柟寮忓嵆鍙��  
+2. `dueDate` 鍙兘涓虹┖锛岀户缁寜 `--` 鍏滃簳灞曠ず銆�  
+3. 鐧惧垎姣斿瓧娈典粛涓烘暟鍊硷紝鍓嶇缁х画杩藉姞 `%` 灞曠ず銆�
diff --git a/src/main/java/com/ruoyi/home/controller/HomeController.java b/src/main/java/com/ruoyi/home/controller/HomeController.java
index 8ef87a5..cbe236d 100644
--- a/src/main/java/com/ruoyi/home/controller/HomeController.java
+++ b/src/main/java/com/ruoyi/home/controller/HomeController.java
@@ -1,6 +1,10 @@
 package com.ruoyi.home.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.ruoyi.approve.pojo.ApproveProcess;
+import com.ruoyi.device.mapper.DeviceLedgerMapper;
+import com.ruoyi.device.mapper.DeviceRepairMapper;
+import com.ruoyi.device.pojo.DeviceRepair;
 import com.ruoyi.dto.MapDto;
 import com.ruoyi.framework.aspectj.lang.annotation.Log;
 import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
@@ -9,17 +13,29 @@
 import com.ruoyi.home.annotation.DefaultType;
 import com.ruoyi.home.dto.*;
 import com.ruoyi.home.service.HomeService;
-import io.swagger.v3.oas.annotations.Operation;
+import com.ruoyi.production.mapper.ProductionOrderMapper;
+import com.ruoyi.production.mapper.ProductionProductOutputMapper;
+import com.ruoyi.production.pojo.ProductionOrder;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
 import lombok.AllArgsConstructor;
 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.math.BigDecimal;
+import java.math.RoundingMode;
 import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * @author :yys
@@ -29,9 +45,19 @@
 @Tag(name = "棣栭〉缁熻")
 @RequestMapping("/home")
 @AllArgsConstructor
-public class HomeController extends BaseController {
+    public class HomeController extends BaseController {
 
     private final HomeService homeService;
+    private final ProductionOrderMapper productionOrderMapper;
+    private final ProductionProductOutputMapper productionProductOutputMapper;
+    private final DeviceLedgerMapper deviceLedgerMapper;
+    private final DeviceRepairMapper deviceRepairMapper;
+
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final Integer ORDER_STATUS_WAIT = 1;
+    private static final Integer ORDER_STATUS_RUNNING = 2;
+    private static final Integer ORDER_STATUS_COMPLETED = 3;
+    private static final Integer ORDER_STATUS_PAUSED = 4;
 
     /********************************************************鍩虹绫�*****************************************************/
     @GetMapping("/todos")
@@ -179,6 +205,117 @@
     }
 
     /********************************************************璐ㄩ噺绫�*****************************************************/
+    @GetMapping("/productionOverview")
+    @Operation(summary = "Production Overview")
+    public R productionOverview() {
+        LocalDate today = LocalDate.now();
+        Map<String, BigDecimal> totalStats = loadOutputStats(LocalDate.of(2000, 1, 1), today.plusDays(1));
+        BigDecimal totalOutput = totalStats.get("quantity");
+        BigDecimal totalScrap = totalStats.get("scrapQty");
+        BigDecimal yieldRate = calcRate(totalOutput, totalOutput.add(totalScrap));
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("totalOutput", scale(totalOutput));
+        result.put("totalScrap", scale(totalScrap));
+        result.put("yieldRate", scale(yieldRate));
+        return R.ok(result);
+    }
+
+    @GetMapping("/productionRealtimeBoard")
+    @Operation(summary = "Production Realtime Board")
+    public R productionRealtimeBoard() {
+        LocalDate today = LocalDate.now();
+        LocalDate yesterday = today.minusDays(1);
+
+        BigDecimal todayDeviceOee = calcDeviceOee(today);
+        BigDecimal yesterdayDeviceOee = calcDeviceOee(yesterday);
+
+        BigDecimal todayOrderAchievementRate = calcOrderAchievementRate(today);
+        BigDecimal yesterdayOrderAchievementRate = calcOrderAchievementRate(yesterday);
+
+        BigDecimal todayDefectRate = calcDefectRate(today);
+        BigDecimal yesterdayDefectRate = calcDefectRate(yesterday);
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("deviceOee", buildRealtimeMetric(todayDeviceOee, todayDeviceOee.subtract(yesterdayDeviceOee)));
+        result.put("orderAchievementRate", buildRealtimeMetric(todayOrderAchievementRate, todayOrderAchievementRate.subtract(yesterdayOrderAchievementRate)));
+        result.put("defectRate", buildRealtimeMetric(todayDefectRate, todayDefectRate.subtract(yesterdayDefectRate)));
+        return R.ok(result);
+    }
+
+    @GetMapping("/productionOrderProgress")
+    @Operation(summary = "Production Order Progress")
+    public R productionOrderProgress(@RequestParam(defaultValue = "all") String tab,
+                                              @RequestParam(defaultValue = "1") Long pageNum,
+                                              @RequestParam(defaultValue = "10") Long pageSize) {
+        long safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
+        long safePageSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 50);
+        Integer status = resolveOrderStatus(tab);
+        long offset = (safePageNum - 1) * safePageSize;
+        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeOrderProgressPage(status, offset, safePageSize);
+        List<Map<String, Object>> records = new ArrayList<>();
+        if (rawRows != null) {
+            for (Map<String, Object> rawRow : rawRows) {
+                records.add(buildOrderProgressRow(rawRow));
+            }
+        }
+
+        long inProgressCount = 0L;
+        long completedCount = 0L;
+        long pausedCount = 0L;
+        List<Map<String, Object>> statusCountRows = productionOrderMapper.countHomeOrderProgressByStatus();
+        if (statusCountRows != null) {
+            for (Map<String, Object> countRow : statusCountRows) {
+                Integer statusKey = toInteger(countRow.get("status"));
+                long cnt = toLong(countRow.get("cnt"));
+                if (Objects.equals(statusKey, ORDER_STATUS_RUNNING)) {
+                    inProgressCount = cnt;
+                } else if (Objects.equals(statusKey, ORDER_STATUS_COMPLETED)) {
+                    completedCount = cnt;
+                } else if (Objects.equals(statusKey, ORDER_STATUS_PAUSED)) {
+                    pausedCount = cnt;
+                }
+            }
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("tab", tab);
+        result.put("total", toLong(productionOrderMapper.countHomeOrderProgress(status)));
+        result.put("pageNum", safePageNum);
+        result.put("pageSize", safePageSize);
+        result.put("inProgressCount", inProgressCount);
+        result.put("completedCount", completedCount);
+        result.put("pausedCount", pausedCount);
+        result.put("records", records);
+        return R.ok(result);
+    }
+
+    @GetMapping("/todayProductionPlan")
+    @Operation(summary = "Today Production Plan")
+    public R todayProductionPlan(@RequestParam(defaultValue = "4") Long limit) {
+        long safeLimit = limit == null || limit < 1 ? 4 : Math.min(limit, 20);
+        List<Map<String, Object>> records = new ArrayList<>();
+        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeTodayProductionPlan(safeLimit);
+        if (rawRows != null) {
+            for (Map<String, Object> rawRow : rawRows) {
+                Map<String, Object> row = new LinkedHashMap<>();
+                Integer rowStatus = toInteger(rawRow.get("status"));
+                row.put("orderNo", rawRow.get("orderNo"));
+                row.put("productName", rawRow.get("productName"));
+                row.put("plannedQuantity", scale(toBigDecimal(rawRow.get("plannedQuantity"))));
+                row.put("dueDate", rawRow.get("dueDate"));
+                row.put("status", rowStatus);
+                row.put("statusLabel", mapOrderStatusLabel(rowStatus));
+                records.add(row);
+            }
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("total", toLong(productionOrderMapper.countHomeTodayProductionPlan()));
+        result.put("records", records);
+        return R.ok(result);
+    }
+
     @GetMapping("/rawMaterialDetection")
     @Operation(summary = "鍘熸潗鏂欐娴�")
     public R rawMaterialDetection(@DefaultType Integer type){
@@ -321,4 +458,198 @@
         return R.ok(list);
     }
 
+    private Map<String, Object> buildOrderProgressRow(Map<String, Object> rawRow) {
+        Map<String, Object> row = new LinkedHashMap<>();
+        Integer rowStatus = toInteger(rawRow.get("status"));
+        row.put("orderNo", rawRow.get("orderNo"));
+        row.put("productName", rawRow.get("productName"));
+        row.put("plannedQuantity", scale(toBigDecimal(rawRow.get("plannedQuantity"))));
+        row.put("completedQuantity", scale(toBigDecimal(rawRow.get("completedQuantity"))));
+        row.put("completionRate", scale(toBigDecimal(rawRow.get("completionRate"))));
+        row.put("dueDate", rawRow.get("dueDate"));
+        row.put("status", rowStatus);
+        row.put("statusLabel", mapOrderStatusLabel(rowStatus));
+        return row;
+    }
+
+    private Integer resolveOrderStatus(String tab) {
+        if (tab == null) {
+            return null;
+        }
+        String normalized = tab.trim().toLowerCase();
+        if ("inprogress".equals(normalized)) {
+            return ORDER_STATUS_RUNNING;
+        }
+        if ("completed".equals(normalized)) {
+            return ORDER_STATUS_COMPLETED;
+        }
+        if ("paused".equals(normalized)) {
+            return ORDER_STATUS_PAUSED;
+        }
+        return null;
+    }
+
+    private String mapOrderStatusLabel(Integer status) {
+        if (Objects.equals(status, ORDER_STATUS_WAIT)) {
+            return "寰呭紑濮�";
+        }
+        if (Objects.equals(status, ORDER_STATUS_RUNNING)) {
+            return "杩涜涓�";
+        }
+        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
+            return "宸插畬鎴�";
+        }
+        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
+            return "宸叉殏鍋�";
+        }
+        return "鏈煡";
+    }
+
+    private Map<String, BigDecimal> loadOutputStats(LocalDate startDate, LocalDate endDateExclusive) {
+        String start = startDate.atStartOfDay().format(DATE_TIME_FORMATTER);
+        String end = endDateExclusive.atStartOfDay().format(DATE_TIME_FORMATTER);
+
+        BigDecimal quantity = BigDecimal.ZERO;
+        BigDecimal scrapQty = BigDecimal.ZERO;
+        List<Map<String, Object>> rows = productionProductOutputMapper.selectDailyOutputStats(start, end);
+        if (rows != null) {
+            for (Map<String, Object> row : rows) {
+                quantity = quantity.add(toBigDecimal(row.get("quantity")));
+                scrapQty = scrapQty.add(toBigDecimal(row.get("scrapQty")));
+            }
+        }
+
+        Map<String, BigDecimal> stats = new LinkedHashMap<>();
+        stats.put("quantity", quantity);
+        stats.put("scrapQty", scrapQty);
+        return stats;
+    }
+
+    private BigDecimal calcDeviceOee(LocalDate day) {
+        long totalDeviceCount = deviceLedgerMapper.selectCount(new LambdaQueryWrapper<>());
+        if (totalDeviceCount <= 0) {
+            return BigDecimal.ZERO;
+        }
+
+        Date start = Date.from(day.atStartOfDay(ZoneId.systemDefault()).toInstant());
+        Date end = Date.from(day.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+        List<DeviceRepair> repairList = deviceRepairMapper.selectList(new LambdaQueryWrapper<DeviceRepair>()
+                .select(DeviceRepair::getDeviceLedgerId)
+                .ge(DeviceRepair::getRepairTime, start)
+                .lt(DeviceRepair::getRepairTime, end)
+                .in(DeviceRepair::getStatus, 0, 3));
+
+        long repairingDeviceCount = repairList == null ? 0 : repairList.stream()
+                .map(DeviceRepair::getDeviceLedgerId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .count();
+
+        long availableDeviceCount = Math.max(totalDeviceCount - repairingDeviceCount, 0);
+        return new BigDecimal(availableDeviceCount)
+                .multiply(new BigDecimal("100"))
+                .divide(new BigDecimal(totalDeviceCount), 2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal calcOrderAchievementRate(LocalDate day) {
+        List<ProductionOrder> orderList = productionOrderMapper.selectList(new LambdaQueryWrapper<ProductionOrder>()
+                .select(ProductionOrder::getQuantity, ProductionOrder::getCompleteQuantity)
+                .ge(ProductionOrder::getCreateTime, day.atStartOfDay())
+                .lt(ProductionOrder::getCreateTime, day.plusDays(1).atStartOfDay())
+                .ne(ProductionOrder::getStatus, ORDER_STATUS_PAUSED));
+
+        BigDecimal totalQuantity = BigDecimal.ZERO;
+        BigDecimal totalCompleteQuantity = BigDecimal.ZERO;
+        if (orderList != null) {
+            for (ProductionOrder order : orderList) {
+                totalQuantity = totalQuantity.add(zeroIfNull(order.getQuantity()));
+                totalCompleteQuantity = totalCompleteQuantity.add(zeroIfNull(order.getCompleteQuantity()));
+            }
+        }
+        return calcRate(totalCompleteQuantity, totalQuantity);
+    }
+
+    private BigDecimal calcDefectRate(LocalDate day) {
+        Map<String, BigDecimal> stats = loadOutputStats(day, day.plusDays(1));
+        BigDecimal quantity = stats.get("quantity");
+        BigDecimal scrapQty = stats.get("scrapQty");
+        return calcRate(scrapQty, quantity.add(scrapQty));
+    }
+
+    private Map<String, Object> buildRealtimeMetric(BigDecimal value, BigDecimal change) {
+        Map<String, Object> metric = new LinkedHashMap<>();
+        metric.put("value", scale(value));
+        metric.put("compareYesterday", scale(change));
+        return metric;
+    }
+
+    private BigDecimal calcRate(BigDecimal numerator, BigDecimal denominator) {
+        if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return zeroIfNull(numerator)
+                .multiply(new BigDecimal("100"))
+                .divide(denominator, 2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal toBigDecimal(Object value) {
+        if (value == null) {
+            return BigDecimal.ZERO;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return BigDecimal.valueOf(((Number) value).doubleValue());
+        }
+        try {
+            return new BigDecimal(String.valueOf(value));
+        } catch (Exception ex) {
+            return BigDecimal.ZERO;
+        }
+    }
+
+    private Integer toInteger(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Integer) {
+            return (Integer) value;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        try {
+            return Integer.valueOf(String.valueOf(value));
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    private long toLong(Object value) {
+        if (value == null) {
+            return 0L;
+        }
+        if (value instanceof Long) {
+            return (Long) value;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        try {
+            return Long.parseLong(String.valueOf(value));
+        } catch (Exception ex) {
+            return 0L;
+        }
+    }
+
+    private BigDecimal zeroIfNull(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
+    }
+
+    private BigDecimal scale(BigDecimal value) {
+        return zeroIfNull(value).setScale(2, RoundingMode.HALF_UP);
+    }
+
 }
diff --git a/src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java b/src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java
index e52df0c..fcc9172 100644
--- a/src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java
+++ b/src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java
@@ -12,6 +12,7 @@
 
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 /**
  * <p>
@@ -39,4 +40,16 @@
 
     Integer countPending(@Param("startDate") String startDate, @Param("endDate") String endDate);
 
+    List<Map<String, Object>> selectHomeOrderProgressPage(@Param("status") Integer status,
+                                                          @Param("offset") Long offset,
+                                                          @Param("size") Long size);
+
+    Long countHomeOrderProgress(@Param("status") Integer status);
+
+    List<Map<String, Object>> countHomeOrderProgressByStatus();
+
+    List<Map<String, Object>> selectHomeTodayProductionPlan(@Param("size") Long size);
+
+    Long countHomeTodayProductionPlan();
+
 }
diff --git a/src/main/resources/mapper/production/ProductionOrderMapper.xml b/src/main/resources/mapper/production/ProductionOrderMapper.xml
index 1a304ef..95679a7 100644
--- a/src/main/resources/mapper/production/ProductionOrderMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOrderMapper.xml
@@ -212,4 +212,61 @@
           and ifnull(complete_quantity, 0) &lt; quantity
     </select>
 
+    <select id="selectHomeOrderProgressPage" resultType="java.util.Map">
+        select po.nps_no as orderNo,
+               p.product_name as productName,
+               ifnull(po.quantity, 0) as plannedQuantity,
+               ifnull(po.complete_quantity, 0) as completedQuantity,
+               round(ifnull(po.complete_quantity, 0) / nullif(po.quantity, 0) * 100, 2) as completionRate,
+               po.plan_complete_time as dueDate,
+               po.status as status
+        from production_order po
+                 left join product_model pm on po.product_model_id = pm.id
+                 left join product p on pm.product_id = p.id
+        <where>
+            <if test="status != null">
+                and po.status = #{status}
+            </if>
+        </where>
+        order by po.id desc
+        limit #{offset}, #{size}
+    </select>
+
+    <select id="countHomeOrderProgress" resultType="java.lang.Long">
+        select count(1)
+        from production_order po
+        <where>
+            <if test="status != null">
+                and po.status = #{status}
+            </if>
+        </where>
+    </select>
+
+    <select id="countHomeOrderProgressByStatus" resultType="java.util.Map">
+        select po.status as status, count(1) as cnt
+        from production_order po
+        where po.status in (2, 3, 4)
+        group by po.status
+    </select>
+
+    <select id="selectHomeTodayProductionPlan" resultType="java.util.Map">
+        select po.nps_no as orderNo,
+               p.product_name as productName,
+               ifnull(po.quantity, 0) as plannedQuantity,
+               po.plan_complete_time as dueDate,
+               po.status as status
+        from production_order po
+                 left join product_model pm on po.product_model_id = pm.id
+                 left join product p on pm.product_id = p.id
+        where po.status in (1, 2)
+        order by case when po.status = 2 then 0 else 1 end, po.id desc
+        limit #{size}
+    </select>
+
+    <select id="countHomeTodayProductionPlan" resultType="java.lang.Long">
+        select count(1)
+        from production_order po
+        where po.status in (1, 2)
+    </select>
+
 </mapper>

--
Gitblit v1.9.3