From 9057d0f6f3a46b93d62d0b71c7f4f03eca41f3a9 Mon Sep 17 00:00:00 2001
From: tjx <t2856754968@163.com>
Date: 星期三, 15 十月 2025 16:43:14 +0800
Subject: [PATCH] 11111

---
 MES.Service/service/QC/SpiAoiService.cs |  444 +++++++++++++++++++++++++++++++++---------------------
 1 files changed, 270 insertions(+), 174 deletions(-)

diff --git a/MES.Service/service/QC/SpiAoiService.cs b/MES.Service/service/QC/SpiAoiService.cs
index 7a2aad8..d5e7669 100644
--- a/MES.Service/service/QC/SpiAoiService.cs
+++ b/MES.Service/service/QC/SpiAoiService.cs
@@ -1,3 +1,4 @@
+using System.Linq;
 using MES.Service.DB;
 using MES.Service.Dto.service;
 using MES.Service.Modes;
@@ -11,78 +12,216 @@
 public class SpiAoiService
 {
     /// <summary>
-    ///     涓婁紶SPI/AOI妫�娴嬫暟鎹�
+    ///     Upload AOI header data.
     /// </summary>
-    /// <param name="request">涓婁紶璇锋眰DTO</param>
-    /// <returns>涓婁紶鍝嶅簲DTO</returns>
-    public SpiAoiUploadResponse UploadSpiAoiData(SpiAoiUploadRequest request)
+    /// <param name="header">Header DTO.</param>
+    /// <returns>Header upload response.</returns>
+    public SpiAoiHeaderUploadResponse UploadAoiHeader(SpiAoiHeaderDto header)
     {
-        // 1. 鍩烘湰鏍¢獙
-        ValidateRequest(request);
+        if (header == null)
+        {
+            throw new Exception("header cannot be null");
+        }
 
         try
         {
-            SpiAoiUploadResponse response = null;
-
-            SqlSugarHelper.UseTransactionWithOracle(db =>
-            {
-                // 2. 妫�鏌ユ潯鐮佹槸鍚﹀凡瀛樺湪
-                var existingHeader = db.Queryable<MesSpiAoiHeader>()
-                    .Where(x => x.BoardBarcode == request.Header.BoardBarcode)
-                    .First();
-
-                if (existingHeader != null)
-                {
-                    throw new Exception($"鏉$爜 {request.Header.BoardBarcode} 宸插瓨鍦紝涓嶅厑璁搁噸澶嶄笂浼�");
-                }
-
-                // 3. 杞崲骞舵彃鍏ヤ富琛ㄦ暟鎹�
-                var header = ConvertHeaderDtoToEntity(request.Header);
-                header.CreatedAt = DateTime.Now;
-                header.UpdatedAt = DateTime.Now;
-
-                var headerId = db.Insertable(header).ExecuteReturnIdentity();
-
-                // 4. 杞崲骞舵彃鍏ュ瓙琛ㄦ暟鎹�
-                var detailCount = 0;
-                if (request.Details != null && request.Details.Count > 0)
-                {
-                    var details = ConvertDetailDtoListToEntity(request.Details, headerId);
-
-                    // 鏁版嵁鏍¢獙(璁板綍璀﹀憡浣嗕笉闃绘柇)
-                    ValidateDetailData(request.Details);
-
-                    detailCount = db.Insertable(details).ExecuteCommand();
-                }
-
-                response = new SpiAoiUploadResponse
-                {
-                    HeaderId = headerId,
-                    DetailCount = detailCount
-                };
-
-                return 1; // 杩斿洖鍙楀奖鍝嶇殑琛屾暟渚涗簨鍔″垽鏂�
-            });
-
-            if (response == null)
-            {
-                throw new Exception("涓婁紶澶辫触");
-            }
-
-            return response;
+            return UploadAoiHeaderBatchInternal(
+                new List<SpiAoiHeaderDto> { header })[0];
         }
         catch (Exception ex)
         {
-            throw new Exception($"涓婁紶SPI/AOI妫�娴嬫暟鎹け璐�: {ex.Message}", ex);
+            throw new Exception($"Failed to upload AOI header: {ex.Message}",
+                ex);
         }
     }
 
     /// <summary>
-    ///     鏍规嵁鏉$爜鏌ヨSPI/AOI妫�娴嬫暟鎹�
+    ///     Upload multiple AOI header records within a single transaction.
     /// </summary>
-    /// <param name="boardBarcode">鏉$爜</param>
-    /// <returns>妫�娴嬫暟鎹�(涓昏〃+瀛愯〃)</returns>
-    public (MesSpiAoiHeader header, List<MesSpiAoiDetail> details) GetByBarcode(string boardBarcode)
+    /// <param name="headers">Header DTO collection.</param>
+    /// <returns>Header upload responses.</returns>
+    public List<SpiAoiHeaderUploadResponse> UploadAoiHeaderBatch(
+        List<SpiAoiHeaderDto> headers)
+    {
+        if (headers == null)
+        {
+            throw new Exception("headers cannot be null");
+        }
+
+        try
+        {
+            return UploadAoiHeaderBatchInternal(headers);
+        }
+        catch (Exception ex)
+        {
+            throw new Exception(
+                $"Failed to upload AOI header batch: {ex.Message}", ex);
+        }
+    }
+
+    private List<SpiAoiHeaderUploadResponse> UploadAoiHeaderBatchInternal(
+        List<SpiAoiHeaderDto> headers)
+    {
+        var headerList = headers.ToList();
+        if (headerList.Count == 0)
+        {
+            throw new Exception("headers cannot be empty");
+        }
+
+        foreach (var header in headerList)
+        {
+            ValidateHeader(header);
+        }
+
+        var duplicateBarcodes = headerList
+            .GroupBy(h => h.BoardBarcode)
+            .Where(g => g.Count() > 1)
+            .Select(g => g.Key)
+            .ToList();
+
+        if (duplicateBarcodes.Any())
+        {
+            throw new Exception(
+                $"Duplicate board barcodes in request: {string.Join(", ",
+                    duplicateBarcodes)}.");
+        }
+
+        var responses = new List<SpiAoiHeaderUploadResponse>();
+
+        SqlSugarHelper.UseTransactionWithOracle(db =>
+        {
+            var boardBarcodes = headerList.Select(h => h.BoardBarcode)
+                .ToList();
+
+            if (boardBarcodes.Any())
+            {
+                var existingBarcodes = db.Queryable<MesSpiAoiHeader>()
+                    .Where(x => boardBarcodes.Contains(x.BoardBarcode))
+                    .Select(x => x.BoardBarcode)
+                    .ToList();
+
+                if (existingBarcodes.Any())
+                {
+                    throw new Exception(
+                        $"Board barcodes {string.Join(", ", existingBarcodes)} already exist; duplicate AOI uploads are not allowed.");
+                }
+            }
+
+            foreach (var headerDto in headerList)
+            {
+                var entity = ConvertHeaderDtoToEntity(headerDto);
+                var timestamp = DateTime.Now;
+                entity.CreatedAt = timestamp;
+                entity.UpdatedAt = timestamp;
+
+                var headerId = db.Insertable(entity).ExecuteReturnIdentity();
+                responses.Add(new SpiAoiHeaderUploadResponse
+                {
+                    HeaderId = headerId
+                });
+            }
+
+            return responses.Count;
+        });
+
+        if (responses.Count == 0)
+        {
+            throw new Exception("AOI upload returned no result.");
+        }
+
+        return responses;
+    }
+
+    /// <summary>
+    ///     Upload SPI detail data.
+    /// </summary>
+    /// <param name="request">Detail upload DTO.</param>
+    /// <returns>Detail upload response.</returns>
+    public SpiAoiDetailUploadResponse UploadSpiDetails(SpiAoiDetailDto request)
+    {
+        if (request == null)
+        {
+            throw new Exception("request cannot be null");
+        }
+
+        try
+        {
+            return UploadSpiDetailsInternal(
+                new List<SpiAoiDetailDto> { request });
+        }
+        catch (Exception ex)
+        {
+            throw new Exception($"Failed to upload SPI details: {ex.Message}",
+                ex);
+        }
+    }
+
+    /// <summary>
+    ///     Upload multiple SPI detail records within a single transaction.
+    /// </summary>
+    /// <param name="requests">Detail DTO collection.</param>
+    /// <returns>Aggregated upload response.</returns>
+    public SpiAoiDetailUploadResponse UploadSpiDetailsBatch(
+        List<SpiAoiDetailDto> requests)
+    {
+        if (requests == null)
+        {
+            throw new Exception("details cannot be null");
+        }
+
+        try
+        {
+            return UploadSpiDetailsInternal(requests);
+        }
+        catch (Exception ex)
+        {
+            throw new Exception(
+                $"Failed to upload SPI detail batch: {ex.Message}", ex);
+        }
+    }
+
+    private SpiAoiDetailUploadResponse UploadSpiDetailsInternal(
+        List<SpiAoiDetailDto> requests)
+    {
+        var detailList = requests.ToList();
+        if (detailList.Count == 0)
+        {
+            throw new Exception("details cannot be empty");
+        }
+
+        ValidateDetailData(detailList);
+
+        var affectedRows = SqlSugarHelper.UseTransactionWithOracle(db =>
+        {
+            var total = 0;
+            foreach (var detail in detailList)
+            {
+                var detailEntity = ConvertDetailDtoToEntity(detail);
+                total += db.Insertable(detailEntity).ExecuteCommand();
+            }
+
+            return total;
+        });
+
+        if (affectedRows <= 0)
+        {
+            throw new Exception(
+                "SPI detail insert returned no affected rows.");
+        }
+
+        return new SpiAoiDetailUploadResponse
+        {
+            DetailCount = affectedRows
+        };
+    }
+
+    /// <summary>
+    ///     Retrieve SPI/AOI data by board barcode.
+    /// </summary>
+    /// <param name="boardBarcode">Board barcode.</param>
+    /// <returns>Header and detail tuple.</returns>
+    public (MesSpiAoiHeader header, List<MesSpiAoiDetail> details) GetByBarcode(
+        string boardBarcode)
     {
         try
         {
@@ -105,16 +244,12 @@
         }
         catch (Exception ex)
         {
-            throw new Exception($"鏌ヨSPI/AOI妫�娴嬫暟鎹け璐�: {ex.Message}", ex);
+            throw new Exception($"鏌ヨSPI/AOI妫�娴嬫暟鎹け锟�? {ex.Message}", ex);
         }
     }
 
-    /// <summary>
-    ///     鏍规嵁ID鏌ヨSPI/AOI妫�娴嬫暟鎹�
-    /// </summary>
-    /// <param name="headerId">涓昏〃ID</param>
-    /// <returns>妫�娴嬫暟鎹�(涓昏〃+瀛愯〃)</returns>
-    public (MesSpiAoiHeader header, List<MesSpiAoiDetail> details) GetById(decimal headerId)
+    public (MesSpiAoiHeader header, List<MesSpiAoiDetail> details) GetById(
+        decimal headerId)
     {
         try
         {
@@ -137,20 +272,20 @@
         }
         catch (Exception ex)
         {
-            throw new Exception($"鏌ヨSPI/AOI妫�娴嬫暟鎹け璐�: {ex.Message}", ex);
+            throw new Exception($"鏌ヨSPI/AOI妫�娴嬫暟鎹け锟�? {ex.Message}", ex);
         }
     }
 
     /// <summary>
-    ///     鍒嗛〉鏌ヨSPI/AOI妫�娴嬫暟鎹�
+    ///     鍒嗛〉鏌ヨSPI/AOI妫�娴嬫暟锟�?
     /// </summary>
     /// <param name="boardBarcode">鏉$爜(鍙��)</param>
     /// <param name="workOrder">宸ュ崟(鍙��)</param>
     /// <param name="surface">鏉块潰(鍙��)</param>
-    /// <param name="startDate">寮�濮嬫棩鏈�(鍙��)</param>
+    /// <param name="startDate">寮�濮嬫棩锟�?鍙��)</param>
     /// <param name="endDate">缁撴潫鏃ユ湡(鍙��)</param>
     /// <param name="pageIndex">椤电爜</param>
-    /// <param name="pageSize">椤靛ぇ灏�</param>
+    /// <param name="pageSize">椤靛ぇ锟�?/param>
     /// <returns>鍒嗛〉鏁版嵁</returns>
     public (List<MesSpiAoiHeader> items, int totalCount) GetPage(
         string boardBarcode = null,
@@ -184,137 +319,66 @@
         }
         catch (Exception ex)
         {
-            throw new Exception($"鍒嗛〉鏌ヨSPI/AOI妫�娴嬫暟鎹け璐�: {ex.Message}", ex);
+            throw new Exception($"鍒嗛〉鏌ヨSPI/AOI妫�娴嬫暟鎹け锟�? {ex.Message}", ex);
         }
     }
 
     #region 绉佹湁鏂规硶
 
     /// <summary>
-    ///     鏍¢獙璇锋眰鍙傛暟
+    ///     Validate AOI header payload.
     /// </summary>
-    /// <param name="request">璇锋眰DTO</param>
-    private void ValidateRequest(SpiAoiUploadRequest request)
+    /// <param name="header">Header DTO.</param>
+    private void ValidateHeader(SpiAoiHeaderDto header)
     {
-        if (request == null)
+        if (header == null)
         {
-            throw new Exception("璇锋眰鍙傛暟涓嶈兘涓虹┖");
+            throw new Exception("header cannot be null");
         }
 
-        if (request.Header == null)
+        if (StringUtil.IsNullOrEmpty(header.TestDate))
         {
-            throw new Exception("header 涓嶈兘涓虹┖");
+            throw new Exception("testDate is required");
         }
 
-        if (request.Details == null || request.Details.Count == 0)
+        if (StringUtil.IsNullOrEmpty(header.TestTime))
         {
-            throw new Exception("details 涓嶈兘涓虹┖");
+            throw new Exception("testTime is required");
         }
 
-        // 鏍¢獙蹇呭~瀛楁
-        if (StringUtil.IsNullOrEmpty(request.Header.TestDate))
+        if (StringUtil.IsNullOrEmpty(header.TestResult))
         {
-            throw new Exception("testDate 涓嶈兘涓虹┖");
+            throw new Exception("testResult is required");
         }
 
-        if (StringUtil.IsNullOrEmpty(request.Header.TestTime))
+        if (StringUtil.IsNullOrEmpty(header.BoardBarcode))
         {
-            throw new Exception("testTime 涓嶈兘涓虹┖");
+            throw new Exception("boardBarcode is required");
         }
 
-        if (StringUtil.IsNullOrEmpty(request.Header.TestResult))
+        if (StringUtil.IsNullOrEmpty(header.Surface))
         {
-            throw new Exception("testResult 涓嶈兘涓虹┖");
+            throw new Exception("surface is required");
         }
 
-        if (StringUtil.IsNullOrEmpty(request.Header.BoardBarcode))
+        if (header.Surface != "T" && header.Surface != "B")
         {
-            throw new Exception("boardBarcode 涓嶈兘涓虹┖");
+            throw new Exception("surface must be T or B");
         }
 
-        if (StringUtil.IsNullOrEmpty(request.Header.Surface))
+        if (header.BoardBarcode.Length > 128)
         {
-            throw new Exception("surface 涓嶈兘涓虹┖");
+            throw new Exception("boardBarcode cannot exceed 128 characters");
         }
 
-        // 鏍¢獙鏋氫妇鍊�
-        if (request.Header.Surface != "T" && request.Header.Surface != "B")
+        if (!StringUtil.IsNullOrEmpty(header.TestResult) &&
+            header.TestResult.Length > 12)
         {
-            throw new Exception("surface 蹇呴』涓� T 鎴� B");
-        }
-
-        // 鏍¢獙瀛楃涓查暱搴�
-        if (request.Header.BoardBarcode.Length > 128)
-        {
-            throw new Exception("boardBarcode 闀垮害涓嶈兘瓒呰繃 128 瀛楃");
-        }
-
-        if (request.Header.TestResult.Length > 12)
-        {
-            throw new Exception("testResult 闀垮害涓嶈兘瓒呰繃 12 瀛楃");
-        }
-
-        // 鏍¢獙鏁板�奸潪璐�
-        foreach (var detail in request.Details)
-        {
-            if (detail.OffsetCount < 0 || detail.MissingCount < 0 ||
-                detail.ReverseCount < 0 || detail.LiftedCount < 0 ||
-                detail.FloatHighCount < 0 || detail.TombstoneCount < 0 ||
-                detail.FlipCount < 0 || detail.WrongPartCount < 0 ||
-                detail.LeadLiftCount < 0 || detail.ColdJointCount < 0 ||
-                detail.NoSolderCount < 0 || detail.InsufficientSolderCount < 0 ||
-                detail.ExcessSolderCount < 0 || detail.BridgeCount < 0 ||
-                detail.CopperExposureCount < 0 || detail.SpikeCount < 0 ||
-                detail.ForeignMatterCount < 0 || detail.GlueOverflowCount < 0 ||
-                detail.PinOffsetCount < 0 || detail.InputBoards < 0 ||
-                detail.OkBoards < 0 || detail.PassBoards < 0 ||
-                detail.DefectBoards < 0 || detail.DefectPoints < 0 ||
-                detail.MeasuredPoints < 0 || detail.PendingPoints < 0)
-            {
-                throw new Exception("鎵�鏈夎鏁板瓧娈靛繀椤� >= 0");
-            }
+            throw new Exception("testResult cannot exceed 12 characters");
         }
     }
 
-    /// <summary>
-    ///     鏍¢獙瀛愯〃鏁版嵁(璁板綍璀﹀憡浣嗕笉闃绘柇)
-    /// </summary>
-    /// <param name="details">瀛愯〃DTO鍒楄〃</param>
-    private void ValidateDetailData(List<SpiAoiDetailDto> details)
-    {
-        foreach (var detail in details)
-        {
-            // 鏍¢獙 passBoards <= inputBoards
-            if (detail.PassBoards > detail.InputBoards)
-            {
-                Console.WriteLine($"[璀﹀憡] passBoards({detail.PassBoards}) 澶т簬 inputBoards({detail.InputBoards})");
-            }
-
-            // 鏍¢獙 defectBoards = inputBoards - passBoards
-            var expectedDefectBoards = detail.InputBoards - detail.PassBoards;
-            if (Math.Abs(detail.DefectBoards - expectedDefectBoards) > 0)
-            {
-                Console.WriteLine($"[璀﹀憡] defectBoards({detail.DefectBoards}) 涓庤绠楀��({expectedDefectBoards})涓嶄竴鑷�");
-            }
-
-            // 鏍¢獙 passRate 鍋忓樊鍦� 卤1.0 浠ュ唴
-            if (detail.InputBoards > 0 && detail.PassRate.HasValue)
-            {
-                var expectedPassRate = (decimal)detail.PassBoards / detail.InputBoards * 100;
-                var deviation = Math.Abs(detail.PassRate.Value - expectedPassRate);
-                if (deviation > 1.0m)
-                {
-                    Console.WriteLine($"[璀﹀憡] passRate({detail.PassRate}) 涓庤绠楀��({expectedPassRate:F2})鍋忓樊瓒呰繃1.0");
-                }
-            }
-        }
-    }
-
-    /// <summary>
-    ///     灏嗕富琛―TO杞崲涓哄疄浣�
-    /// </summary>
-    /// <param name="dto">涓昏〃DTO</param>
-    /// <returns>涓昏〃瀹炰綋</returns>
+    
     private MesSpiAoiHeader ConvertHeaderDtoToEntity(SpiAoiHeaderDto dto)
     {
         return new MesSpiAoiHeader
@@ -335,18 +399,50 @@
     }
 
     /// <summary>
-    ///     灏嗗瓙琛―TO鍒楄〃杞崲涓哄疄浣撳垪琛�
+    ///     Perform non-blocking SPI detail checks (warnings only).
     /// </summary>
-    /// <param name="dtoList">瀛愯〃DTO鍒楄〃</param>
-    /// <param name="headerId">涓昏〃ID</param>
-    /// <returns>瀛愯〃瀹炰綋鍒楄〃</returns>
-    private List<MesSpiAoiDetail> ConvertDetailDtoListToEntity(
-        List<SpiAoiDetailDto> dtoList, decimal headerId)
+    /// <param name="details">Detail DTO list.</param>
+    private void ValidateDetailData(List<SpiAoiDetailDto> details)
+    {
+        foreach (var detail in details)
+        {
+            // Validate passBoards <= inputBoards
+            if (detail.PassBoards > detail.InputBoards)
+            {
+                Console.WriteLine(
+                    $"[Warning] passBoards({detail.PassBoards}) is greater than inputBoards({detail.InputBoards}).");
+            }
+
+            // Validate defectBoards = inputBoards - passBoards
+            var expectedDefectBoards = detail.InputBoards - detail.PassBoards;
+            if (Math.Abs(detail.DefectBoards - expectedDefectBoards) > 0)
+            {
+                Console.WriteLine(
+                    $"[Warning] defectBoards({detail.DefectBoards}) does not match the expected value ({expectedDefectBoards}).");
+            }
+
+            // Validate passRate deviation stays within +/- 1.0
+            if (detail.InputBoards > 0 && detail.PassRate.HasValue)
+            {
+                var expectedPassRate = (decimal)detail.PassBoards /
+                    detail.InputBoards * 100;
+                var deviation =
+                    Math.Abs(detail.PassRate.Value - expectedPassRate);
+                if (deviation > 1.0m)
+                {
+                    Console.WriteLine(
+                        $"[Warning] passRate({detail.PassRate}) deviates from the expected value ({expectedPassRate:F2}) by more than 1.0.");
+                }
+            }
+        }
+    }
+
+    private MesSpiAoiDetail ConvertDetailDtoToEntity(SpiAoiDetailDto dto)
     {
         var now = DateTime.Now;
-        return dtoList.Select(dto => new MesSpiAoiDetail
+        return new MesSpiAoiDetail
         {
-            HeaderId = headerId,
+            HeaderId = dto.HeaderId ?? 0,
             OffsetCount = dto.OffsetCount,
             MissingCount = dto.MissingCount,
             ReverseCount = dto.ReverseCount,
@@ -380,7 +476,7 @@
             PendingPoints = dto.PendingPoints,
             CreatedAt = now,
             UpdatedAt = now
-        }).ToList();
+        };
     }
 
     #endregion

--
Gitblit v1.9.3