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

---
 /dev/null                                         |  167 --------
 MES.Service/service/QC/SpiAoiService.cs           |  444 ++++++++++++++---------
 MESApplication/Docs/Spi_API.md                    |  140 +++++++
 MESApplication/Docs/Aoi_API.md                    |  134 +++++++
 MESApplication/Controllers/QC/SpiAoiController.cs |  188 +++++++++
 MES.Service/Dto/service/SpiAoiDto.cs              |   42 -
 6 files changed, 733 insertions(+), 382 deletions(-)

diff --git a/MES.Service/Dto/service/SpiAoiDto.cs b/MES.Service/Dto/service/SpiAoiDto.cs
index 602a5eb..1360bc7 100644
--- a/MES.Service/Dto/service/SpiAoiDto.cs
+++ b/MES.Service/Dto/service/SpiAoiDto.cs
@@ -1,22 +1,6 @@
 namespace MES.Service.Dto.service;
 
 /// <summary>
-///     SPI/AOI涓婁紶璇锋眰DTO
-/// </summary>
-public class SpiAoiUploadRequest
-{
-    /// <summary>
-    ///     涓昏〃鏁版嵁
-    /// </summary>
-    public SpiAoiHeaderDto Header { get; set; }
-
-    /// <summary>
-    ///     瀛愯〃鏁版嵁鍒楄〃
-    /// </summary>
-    public List<SpiAoiDetailDto> Details { get; set; }
-}
-
-/// <summary>
 ///     SPI/AOI涓昏〃DTO
 /// </summary>
 public class SpiAoiHeaderDto
@@ -32,7 +16,7 @@
     public string TestTime { get; set; }
 
     /// <summary>
-    ///     娴嬭瘯缁撴灉(濡傦細0:0:1;0銆�0;0;0:1銆丗ail绛�)
+    ///     娴嬭瘯缁撴灉(濡傦細0:0:1;0銆?;0;0:1銆丗ail绛?
     /// </summary>
     public string TestResult { get; set; }
 
@@ -62,7 +46,7 @@
     public string? WorkOrder { get; set; }
 
     /// <summary>
-    ///     鏈虹鍚�
+    ///     鏈虹鍚?
     /// </summary>
     public string? ProductModel { get; set; }
 
@@ -93,10 +77,14 @@
     public string? MachineName { get; set; }
 
     /// <summary>
-    ///     鐢熶骇绾垮悕绉�
+    ///     鐢熶骇绾垮悕绉?
     /// </summary>
     public string? LineDisplayName { get; set; }
 
+    /// <summary>
+    ///     Legacy header identifier (optional).
+    /// </summary>
+    public decimal? HeaderId { get; set; }
     /// <summary>
     ///     鍋忎綅鏁伴噺
     /// </summary>
@@ -208,7 +196,7 @@
     public int PassBoards { get; set; }
 
     /// <summary>
-    ///     鍚堟牸鐜�(%)
+    ///     鍚堟牸鐜?%)
     /// </summary>
     public decimal? PassRate { get; set; }
 
@@ -218,12 +206,12 @@
     public int DefectBoards { get; set; }
 
     /// <summary>
-    ///     涓嶈壇鐜�(%)
+    ///     涓嶈壇鐜?%)
     /// </summary>
     public decimal? DefectRate { get; set; }
 
     /// <summary>
-    ///     涓嶈壇鐜�(PPM)
+    ///     涓嶈壇鐜?PPM)
     /// </summary>
     public int? DefectPpm { get; set; }
 
@@ -244,15 +232,21 @@
 }
 
 /// <summary>
-///     SPI/AOI涓婁紶鍝嶅簲DTO
+///     AOI涓昏〃涓婁紶鍝嶅簲DTO
 /// </summary>
-public class SpiAoiUploadResponse
+public class SpiAoiHeaderUploadResponse
 {
     /// <summary>
     ///     涓昏〃ID
     /// </summary>
     public decimal HeaderId { get; set; }
+}
 
+/// <summary>
+///     SPI鏄庣粏涓婁紶鍝嶅簲DTO
+/// </summary>
+public class SpiAoiDetailUploadResponse
+{
     /// <summary>
     ///     鎻掑叆鐨勫瓙琛ㄨ褰曟暟
     /// </summary>
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
diff --git a/MESApplication/Controllers/QC/SpiAoiController.cs b/MESApplication/Controllers/QC/SpiAoiController.cs
index 1339d59..c9f08c7 100644
--- a/MESApplication/Controllers/QC/SpiAoiController.cs
+++ b/MESApplication/Controllers/QC/SpiAoiController.cs
@@ -1,4 +1,6 @@
+using System.Collections.Generic;
 using System.Dynamic;
+using System.Linq;
 using MES.Service.Dto.service;
 using MES.Service.Modes;
 using MES.Service.service;
@@ -20,34 +22,186 @@
     private readonly MessageCenterManager _manager = new();
     private readonly SpiAoiService _service = new();
 
-    private readonly string METHOD = "POST";
-    private readonly string TableName = "MES_SPI_AOI_HEADER";
-    private readonly string URL = "http://localhost:10054/api/QC/SpiAoi/";
+    private const string METHOD = "POST";
+    private const string HeaderTableName = "MES_SPI_AOI_HEADER";
+    private const string DetailTableName = "MES_SPI_AOI_DETAIL";
+    private const string BaseUrl = "http://localhost:10054/api/QC/SpiAoi/";
 
     /// <summary>
-    ///     涓婁紶SPI/AOI妫�娴嬫暟鎹�
+    ///     Upload AOI header data.
     /// </summary>
-    /// <param name="request">涓婁紶璇锋眰</param>
-    /// <returns>涓婁紶缁撴灉</returns>
-    [HttpPost("Upload")]
-    public ResponseResult Upload([FromBody] SpiAoiUploadRequest request)
+    /// <param name="header">AOI header payload.</param>
+    /// <returns>Upload result.</returns>
+    [HttpPost("UploadAoiHeader")]
+    public ResponseResult UploadAoiHeader(
+        [FromBody] SpiAoiHeaderDto header)
     {
-        var entity = new MessageCenter();
-        entity.TableName = TableName;
-        entity.Url = URL + "Upload";
-        entity.Method = METHOD;
-        entity.Data = JsonConvert.SerializeObject(request);
-        entity.Status = 1;
-        entity.CreateBy = "SPI_AOI_SYSTEM";
+        var entity = new MessageCenter
+        {
+            TableName = HeaderTableName,
+            Url = BaseUrl + "UploadAoiHeader",
+            Method = METHOD,
+            Data = JsonConvert.SerializeObject(header),
+            Status = 1,
+            CreateBy = "SPI_AOI_SYSTEM"
+        };
 
         try
         {
-            var response = _service.UploadSpiAoiData(request);
+            var response = _service.UploadAoiHeader(header);
 
             dynamic resultInfos = new ExpandoObject();
             resultInfos.headerId = response.HeaderId;
+            resultInfos.message = "AOI header upload succeeded";
+
+            entity.Result = 1;
+            entity.DealWith = 1;
+            entity.ResultData = JsonConvert.SerializeObject(response);
+            _manager.save(entity);
+
+            return new ResponseResult
+            {
+                status = 0,
+                message = "OK",
+                data = resultInfos
+            };
+        }
+        catch (Exception ex)
+        {
+            entity.Result = 0;
+            entity.DealWith = 0;
+            entity.ResultData = ex.Message;
+            _manager.save(entity);
+
+            return ResponseResult.ResponseError(ex);
+        }
+    }
+
+    /// <summary>
+    ///     Batch upload AOI header data.
+    /// </summary>
+    /// <param name="headers">AOI header payload collection.</param>
+    /// <returns>Upload result.</returns>
+    [HttpPost("UploadAoiHeaderBatch")]
+    public ResponseResult UploadAoiHeaderBatch(
+        [FromBody] List<SpiAoiHeaderDto> headers)
+    {
+        var entity = new MessageCenter
+        {
+            TableName = HeaderTableName,
+            Url = BaseUrl + "UploadAoiHeaderBatch",
+            Method = METHOD,
+            Data = JsonConvert.SerializeObject(headers),
+            Status = 1,
+            CreateBy = "SPI_AOI_SYSTEM"
+        };
+
+        try
+        {
+            var responses = _service.UploadAoiHeaderBatch(headers);
+            dynamic resultInfos = new ExpandoObject();
+            resultInfos.headerIds = responses.Select(r => r.HeaderId).ToList();
+            resultInfos.message = "AOI header batch upload succeeded";
+
+            entity.Result = 1;
+            entity.DealWith = 1;
+            entity.ResultData = JsonConvert.SerializeObject(responses);
+            _manager.save(entity);
+
+            return new ResponseResult
+            {
+                status = 0,
+                message = "OK",
+                data = resultInfos
+            };
+        }
+        catch (Exception ex)
+        {
+            entity.Result = 0;
+            entity.DealWith = 0;
+            entity.ResultData = ex.Message;
+            _manager.save(entity);
+
+            return ResponseResult.ResponseError(ex);
+        }
+    }
+
+    /// <summary>
+    ///     Upload SPI detail data.
+    /// </summary>
+    /// <param name="request">SPI detail payload.</param>
+    /// <returns>Upload result.</returns>
+    [HttpPost("UploadSpiDetails")]
+    public ResponseResult UploadSpiDetails(
+        [FromBody] SpiAoiDetailDto request)
+    {
+        var entity = new MessageCenter
+        {
+            TableName = DetailTableName,
+            Url = BaseUrl + "UploadSpiDetails",
+            Method = METHOD,
+            Data = JsonConvert.SerializeObject(request),
+            Status = 1,
+            CreateBy = "SPI_AOI_SYSTEM"
+        };
+
+        try
+        {
+            var response = _service.UploadSpiDetails(request);
+
+            dynamic resultInfos = new ExpandoObject();
             resultInfos.detailCount = response.DetailCount;
-            resultInfos.message = "SPI/AOI妫�娴嬫暟鎹笂浼犳垚鍔�";
+            resultInfos.message = "SPI detail upload succeeded";
+
+            entity.Result = 1;
+            entity.DealWith = 1;
+            entity.ResultData = JsonConvert.SerializeObject(response);
+            _manager.save(entity);
+
+            return new ResponseResult
+            {
+                status = 0,
+                message = "OK",
+                data = resultInfos
+            };
+        }
+        catch (Exception ex)
+        {
+            entity.Result = 0;
+            entity.DealWith = 0;
+            entity.ResultData = ex.Message;
+            _manager.save(entity);
+
+            return ResponseResult.ResponseError(ex);
+        }
+    }
+
+    /// <summary>
+    ///     Batch upload SPI detail data.
+    /// </summary>
+    /// <param name="requests">SPI detail payload collection.</param>
+    /// <returns>Upload result.</returns>
+    [HttpPost("UploadSpiDetailsBatch")]
+    public ResponseResult UploadSpiDetailsBatch(
+        [FromBody] List<SpiAoiDetailDto> requests)
+    {
+        var entity = new MessageCenter
+        {
+            TableName = DetailTableName,
+            Url = BaseUrl + "UploadSpiDetailsBatch",
+            Method = METHOD,
+            Data = JsonConvert.SerializeObject(requests),
+            Status = 1,
+            CreateBy = "SPI_AOI_SYSTEM"
+        };
+
+        try
+        {
+            var response = _service.UploadSpiDetailsBatch(requests);
+
+            dynamic resultInfos = new ExpandoObject();
+            resultInfos.detailCount = response.DetailCount;
+            resultInfos.message = "SPI detail batch upload succeeded";
 
             entity.Result = 1;
             entity.DealWith = 1;
diff --git a/MESApplication/Docs/Aoi_API.md b/MESApplication/Docs/Aoi_API.md
new file mode 100644
index 0000000..213d483
--- /dev/null
+++ b/MESApplication/Docs/Aoi_API.md
@@ -0,0 +1,134 @@
+# AOI 鎺ュ彛鏂囨。
+
+## 鍩烘湰淇℃伅
+- 鍩虹璺緞锛歚/api/SpiAoi`锛堢ず渚嬪畬鏁村湴鍧�锛歚http://192.168.1.251:10054/api/SpiAoi`锛�
+- 璇锋眰涓庡搷搴旀牸寮忥細`application/json; charset=utf-8`
+
+## 缁熶竴杩斿洖妯″瀷锛圧esponseResult锛�
+- `status`锛氭暣鏁帮紝`0` 琛ㄧず鎴愬姛锛宍1` 琛ㄧず澶辫触
+- `message`锛氬瓧绗︿覆锛岃繑鍥炶鏄庢垨閿欒淇℃伅
+- `data`锛氬璞★紝涓氬姟鏁版嵁杞戒綋
+- `TotalCount`锛氭暣鏁帮紝浠呭垎椤垫帴鍙d娇鐢紝琛ㄧず鎬昏褰曟暟
+
+
+###  Aoi鏁版嵁璇存槑
+璁惧涓庝骇绾匡細
+- `machineName`锛氬瓧绗︿覆锛屽彲绌恒�傛娴嬫満鍙板悕绉般��
+- `lineDisplayName`锛氬瓧绗︿覆锛屽彲绌恒�備骇绾挎樉绀哄悕绉般��
+
+缂洪櫡璁℃暟锛堝潎涓烘暣鏁帮紝蹇呴』 鈮�0锛夛細
+- `offsetCount`锛氬亸浣嶆暟閲忥紙鍣ㄤ欢璐磋鍋忕Щ锛夈��
+- `missingCount`锛氱己浠舵暟閲忥紙鍣ㄤ欢缂哄け锛夈��
+- `reverseCount`锛氬弽鍚戞暟閲忥紙鍣ㄤ欢鏋佹��/鏂瑰悜鍙嶈锛夈��
+- `liftedCount`锛氱繕璧锋暟閲忥紙鍣ㄤ欢涓�绔垨鏁翠綋缈樿捣锛夈��
+- `floatHighCount`锛氭诞楂樻暟閲忥紙鍣ㄤ欢楂樺害瓒呭樊锛夈��
+- `tombstoneCount`锛氱珛纰戞暟閲忥紙鐗囧紡鍣ㄤ欢涓�绔珫璧凤級銆�
+- `flipCount`锛氱炕杞暟閲忥紙鍣ㄤ欢缈婚潰/缈昏韩锛夈��
+- `wrongPartCount`锛氶敊浠舵暟閲忥紙鍨嬪彿/瑙勬牸閿欒锛夈��
+- `leadLiftCount`锛氱繕鑴氭暟閲忥紙寮曡剼鏈创浼忥級銆�
+- `coldJointCount`锛氳櫄鐒婃暟閲忥紙鍐风剨/鍋囩剨锛夈��
+- `noSolderCount`锛氱┖鐒婃暟閲忥紙鐒婃枡缂哄け锛夈��
+- `insufficientSolderCount`锛氬皯閿℃暟閲忥紙鐒婃枡涓嶈冻锛夈��
+- `excessSolderCount`锛氬閿℃暟閲忥紙鐒婃枡杩囬噺锛夈��
+- `bridgeCount`锛氳繛閿℃暟閲忥紙鐒婇敗妗ヨ繛锛夈��
+- `copperExposureCount`锛氭紡閾滄暟閲忥紙鐒婄洏/绾胯矾閾滅當澶栭湶锛夈��
+- `spikeCount`锛氭媺灏栨暟閲忥紙閿℃枡鎷夊皷锛夈��
+- `foreignMatterCount`锛氬紓鐗╂暟閲忥紙姹℃煋/棰楃矑锛夈��
+- `glueOverflowCount`锛氭孩鑳舵暟閲忥紙鑳舵按澶栨孩锛夈��
+- `pinOffsetCount`锛氬紩鑴氬亸浣嶆暟閲忥紙寮曡剼鏈涓級銆�
+
+浜х嚎缁熻锛堥櫎姣旂巼澶栦负鏁存暟锛屽繀椤� 鈮�0锛夛細
+- `inputBoards`锛氭姇鍏ユ澘鏁帮紙杩涘叆妫�娴嬬殑鏉挎暟閲忥級銆�
+- `okBoards`锛歄K 鏉挎暟锛堟娴嬩负 OK 鐨勬澘鏁伴噺锛夈��
+- `passBoards`锛氶�氳繃鏉挎暟锛堟渶缁堝垽瀹氶�氳繃鐨勬澘鏁伴噺锛夈��
+- `passRate`锛氬皬鏁帮紝鍗曚綅鐧惧垎姣斻�傚悎鏍肩巼锛屼緥濡� `98.00` 琛ㄧず 98%銆�
+- `defectBoards`锛氫笉鑹澘鏁帮紙鏈�缁堝垽瀹氫笉鑹殑鏉挎暟閲忥級銆�
+- `defectRate`锛氬皬鏁帮紝鍗曚綅鐧惧垎姣斻�備笉鑹巼銆�
+- `defectPpm`锛氭暣鏁帮紝鍗曚綅 PPM銆備笉鑹暟锛堢櫨涓囧垎鐜囷級銆�
+- `defectPoints`锛氫笉鑹偣鏁帮紙缂洪櫡鐐瑰悎璁★級銆�
+- `measuredPoints`锛氬疄娴嬬偣鏁帮紙瀹為檯瀹屾垚妫�娴嬬殑鐐规暟锛夈��
+- `pendingPoints`锛氬緟娴嬬偣鏁帮紙鏈畬鎴愭娴嬬殑鐐规暟锛夈��
+
+
+杩斿洖瀛楁绀轰緥锛�
+- `headerId`锛氭暟鎹簱鐢熸垚鐨� ID
+- `createdAt`锛氬垱寤烘椂闂�
+- `updatedAt`锛氭洿鏂版椂闂�
+---
+
+## 鎺ュ彛鍒楄〃
+
+浠ヤ笅涓や釜鎺ュ彛瑕嗙洊鍗曠瑪涓庢壒閲忓叆搴撳満鏅紝鍧囦娇鐢� POST銆�
+
+### 1. UploadAoiHeader 鈥� 涓婁紶 AOI Header锛堝崟绗旓級
+- 璺緞锛歚POST /api/SpiAoi/UploadAoiHeader`
+- 鎻忚堪锛氭彃鍏ヤ竴鏉� AOI Header 璁板綍锛屾潯鐮侀噸澶嶆椂杩斿洖閿欒銆�
+- 璇锋眰绀轰緥
+  ```json
+  {
+    "testDate": "2025-10-10",
+    "testTime": "14:33:21",
+    "testResult": "0;0;0:1",
+    "surface": "T",
+    "totalPoints": 500,
+    "actualDefects": 3,
+    "equipmentModel": "SPI-9000",
+    "workOrder": "WO20251010-01",
+    "productModel": "MODEL-ABC",
+    "boardBarcode": "BC123456789",
+    "smtGroup": "A1",
+    "lineName": "SMT-01"
+  }
+  ```
+- 鎴愬姛鍝嶅簲
+  ```json
+  {
+    "status": 0,
+    "message": "OK",
+    "data": {
+      "headerId": 12001,
+      "message": "AOI header upload succeeded"
+    },
+    "TotalCount": 0
+  }
+  ```
+
+### 2. UploadAoiHeaderBatch 鈥� 鎵归噺涓婁紶 AOI Header
+- 璺緞锛歚POST /api/SpiAoi/UploadAoiHeaderBatch`
+- 鎻忚堪锛氫竴娆℃彃鍏ュ鏉� Header 璁板綍锛涜嫢浠讳綍涓�鏉℃牎楠屾垨鎻掑叆澶辫触锛屾暣鎵瑰洖婊氥��
+- 璇锋眰绀轰緥
+  ```json
+  [
+    {
+      "testDate": "2025-10-10",
+      "testTime": "14:33:21",
+      "testResult": "0;0;0:1",
+      "surface": "T",
+      "boardBarcode": "BC123456789"
+    },
+    {
+      "testDate": "2025-10-10",
+      "testTime": "14:34:00",
+      "testResult": "0;0;0:1",
+      "surface": "B",
+      "boardBarcode": "BC123456790"
+    }
+  ]
+  ```
+- 鎴愬姛鍝嶅簲
+  ```json
+  {
+    "status": 0,
+    "message": "OK",
+    "data": {
+      "headerIds": [12001, 12002],
+      "message": "AOI header batch upload succeeded"
+    },
+    "TotalCount": 0
+  }
+  ```
+  
+## 璋冪敤寤鸿
+- 涓ユ牸鎸夌収瀛楁绫诲瀷涓庨暱搴︽瀯閫犳暟鎹紱鏁板�肩被瀛楁寤鸿浣跨敤鏁村瀷鎴栦繚鐣欏悎閫傚皬鏁颁綅銆�
+- 鍑虹幇閿欒鏃跺厛鏌ョ湅 `message`锛屽啀缁撳悎娑堟伅涓績璁板綍鍜屾湇鍔$鏃ュ織瀹氫綅闂銆�
+- 寤鸿鍦ㄦ壒閲忚皟鐢ㄥ墠鑷鍘婚噸鏉$爜鎴栧叧閿瓧娈碉紝鍑忓皯鍥炴粴姒傜巼銆�
diff --git a/MESApplication/Docs/SpiAoi_API.md b/MESApplication/Docs/SpiAoi_API.md
deleted file mode 100644
index 86ecb6e..0000000
--- a/MESApplication/Docs/SpiAoi_API.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# SPI/AOI 鎺ュ彛鏂囨。
-
-## 鍩烘湰淇℃伅
-- 鍩虹璺緞锛歚/api/SpiAoi`锛堢ず渚嬪畬鏁村湴鍧�锛歚http://192.168.1.251:10054/api/SpiAoi`锛�
-- 璇锋眰涓庡搷搴旀牸寮忥細`application/json; charset=utf-8`
-
-## 缁熶竴杩斿洖妯″瀷锛圧esponseResult锛�
-- `status`锛氭暣鏁帮紝0=鎴愬姛锛�1=澶辫触
-- `message`锛氬瓧绗︿覆锛岀粨鏋滆鏄庢垨閿欒鍘熷洜
-- `data`锛氬璞★紝涓氬姟鏁版嵁杞戒綋
-- `TotalCount`锛氭暣鏁帮紝浠呭垎椤垫帴鍙h繑鍥烇紝鎬昏褰曟暟
-
-## 鏁版嵁妯″瀷涓庣害鏉燂紙閫愬瓧娈垫敞閲婏級
-浠ヤ笅瀛楁璇存槑鍙傝�冧簡 SPIAOI.txt 涓殑鏁版嵁搴撳畾涔変笌娉ㄩ噴锛岃皟鐢ㄦ柟璇蜂弗鏍兼寜绾︽潫鎻愪緵鏁版嵁銆�
-
-### Header锛堜富琛ㄦ憳瑕侊紝涓婁紶鏃跺繀濉級
-- `testDate`锛氬瓧绗︿覆锛屽繀濉�傛祴璇曟棩鏈燂紝鏍煎紡涓� `yyyy-MM-dd`锛屼笌璁惧涓婁紶鏃ユ湡淇濇寔涓�鑷淬��
-- `testTime`锛氬瓧绗︿覆锛屽繀濉�傛祴璇曟椂闂达紝鏍煎紡涓� `HH:mm:ss`锛屼笌璁惧涓婁紶鏃堕棿淇濇寔涓�鑷淬��
-- `testResult`锛氬瓧绗︿覆锛屽繀濉紝闀垮害鈮�12銆傛祴璇曠粨鏋滃師濮嬪瓧绗︿覆锛屼緥濡� `0:0:1;0`锛堥�氳繃锛夈�乣0;0;0:1`锛堝け璐ワ級銆乣Fail`锛堝紓甯革級銆備笉鍋氳В鏋愶紝鍘熸牱鍏ュ簱銆�
-- `surface`锛氬瓧绗︿覆锛屽繀濉�傛娴嬮潰锛屽彇鍊� `T` 琛ㄧず椤堕潰锛圱op锛夛紝`B` 琛ㄧず搴曢潰锛圔ottom锛夈��
-- `totalPoints`锛氭暣鏁帮紝鍙┖锛屸墺0銆傝鍒掓娴嬬偣鏁般��
-- `actualDefects`锛氭暣鏁帮紝鍙┖锛屸墺0銆傚疄闄呬笉鑹偣鏁帮紙缁熻缁撴灉锛夈��
-- `equipmentModel`锛氬瓧绗︿覆锛屽彲绌恒�傝澶囧瀷鍙凤紙濡� SPI/AOI 鏈哄彴鍨嬪彿锛夈��
-- `workOrder`锛氬瓧绗︿覆锛屽彲绌恒�傜敓浜у伐鍗曟垨鎵规鍙枫��
-- `productModel`锛氬瓧绗︿覆锛屽彲绌恒�傛満绉�/浜у搧鍨嬪彿銆�
-- `boardBarcode`锛氬瓧绗︿覆锛屽繀濉紝闀垮害鈮�128銆傛澘浠舵潯鐮侊紝鍏ㄥ眬鍞竴锛涢噸澶嶅皢琚嫆缁濄��
-- `smtGroup`锛氬瓧绗︿覆锛屽彲绌恒�係MT 缁勫埆銆�
-- `lineName`锛氬瓧绗︿覆锛屽彲绌恒�傜嚎鍒悕绉般��
-
-杩斿洖涓撶敤锛堟煡璇㈡帴鍙d細杩斿洖浠ヤ笅瀛楁锛夛細
-- `id`锛氭暟鍊硷紝鏁版嵁搴撲富閿� ID锛岀敱搴忓垪鐢熸垚銆�
-- `createdAt`锛氬瓧绗︿覆锛屽垱寤烘椂闂� 
-- `updatedAt`锛氬瓧绗︿覆锛屾洿鏂版椂闂� 
-
-### Detail锛堝瓙琛ㄧ己闄蜂笌浜х嚎鎸囨爣锛屼笂浼犳椂鑷冲皯 1 鏉★級
-璁惧涓庝骇绾匡細
-- `machineName`锛氬瓧绗︿覆锛屽彲绌恒�傛娴嬫満鍙板悕绉般��
-- `lineDisplayName`锛氬瓧绗︿覆锛屽彲绌恒�備骇绾挎樉绀哄悕绉般��
-
-缂洪櫡璁℃暟锛堝潎涓烘暣鏁帮紝蹇呴』 鈮�0锛夛細
-- `offsetCount`锛氬亸浣嶆暟閲忥紙鍣ㄤ欢璐磋鍋忕Щ锛夈��
-- `missingCount`锛氱己浠舵暟閲忥紙鍣ㄤ欢缂哄け锛夈��
-- `reverseCount`锛氬弽鍚戞暟閲忥紙鍣ㄤ欢鏋佹��/鏂瑰悜鍙嶈锛夈��
-- `liftedCount`锛氱繕璧锋暟閲忥紙鍣ㄤ欢涓�绔垨鏁翠綋缈樿捣锛夈��
-- `floatHighCount`锛氭诞楂樻暟閲忥紙鍣ㄤ欢楂樺害瓒呭樊锛夈��
-- `tombstoneCount`锛氱珛纰戞暟閲忥紙鐗囧紡鍣ㄤ欢涓�绔珫璧凤級銆�
-- `flipCount`锛氱炕杞暟閲忥紙鍣ㄤ欢缈婚潰/缈昏韩锛夈��
-- `wrongPartCount`锛氶敊浠舵暟閲忥紙鍨嬪彿/瑙勬牸閿欒锛夈��
-- `leadLiftCount`锛氱繕鑴氭暟閲忥紙寮曡剼鏈创浼忥級銆�
-- `coldJointCount`锛氳櫄鐒婃暟閲忥紙鍐风剨/鍋囩剨锛夈��
-- `noSolderCount`锛氱┖鐒婃暟閲忥紙鐒婃枡缂哄け锛夈��
-- `insufficientSolderCount`锛氬皯閿℃暟閲忥紙鐒婃枡涓嶈冻锛夈��
-- `excessSolderCount`锛氬閿℃暟閲忥紙鐒婃枡杩囬噺锛夈��
-- `bridgeCount`锛氳繛閿℃暟閲忥紙鐒婇敗妗ヨ繛锛夈��
-- `copperExposureCount`锛氭紡閾滄暟閲忥紙鐒婄洏/绾胯矾閾滅當澶栭湶锛夈��
-- `spikeCount`锛氭媺灏栨暟閲忥紙閿℃枡鎷夊皷锛夈��
-- `foreignMatterCount`锛氬紓鐗╂暟閲忥紙姹℃煋/棰楃矑锛夈��
-- `glueOverflowCount`锛氭孩鑳舵暟閲忥紙鑳舵按澶栨孩锛夈��
-- `pinOffsetCount`锛氬紩鑴氬亸浣嶆暟閲忥紙寮曡剼鏈涓級銆�
-
-浜х嚎缁熻锛堥櫎姣旂巼澶栦负鏁存暟锛屽繀椤� 鈮�0锛夛細
-- `inputBoards`锛氭姇鍏ユ澘鏁帮紙杩涘叆妫�娴嬬殑鏉挎暟閲忥級銆�
-- `okBoards`锛歄K 鏉挎暟锛堟娴嬩负 OK 鐨勬澘鏁伴噺锛夈��
-- `passBoards`锛氶�氳繃鏉挎暟锛堟渶缁堝垽瀹氶�氳繃鐨勬澘鏁伴噺锛夈��
-- `passRate`锛氬皬鏁帮紝鍗曚綅鐧惧垎姣斻�傚悎鏍肩巼锛屼緥濡� `98.00` 琛ㄧず 98%銆�
-- `defectBoards`锛氫笉鑹澘鏁帮紙鏈�缁堝垽瀹氫笉鑹殑鏉挎暟閲忥級銆�
-- `defectRate`锛氬皬鏁帮紝鍗曚綅鐧惧垎姣斻�備笉鑹巼銆�
-- `defectPpm`锛氭暣鏁帮紝鍗曚綅 PPM銆備笉鑹暟锛堢櫨涓囧垎鐜囷級銆�
-- `defectPoints`锛氫笉鑹偣鏁帮紙缂洪櫡鐐瑰悎璁★級銆�
-- `measuredPoints`锛氬疄娴嬬偣鏁帮紙瀹為檯瀹屾垚妫�娴嬬殑鐐规暟锛夈��
-- `pendingPoints`锛氬緟娴嬬偣鏁帮紙鏈畬鎴愭娴嬬殑鐐规暟锛夈��
-
-涓氬姟鏍¢獙鎻愮ず锛堟彁绀轰笉闃绘柇锛夛細
-- `passBoards` 涓嶅簲澶т簬 `inputBoards`銆�
-- 寤鸿 `defectBoards = inputBoards - passBoards`锛屽亸宸粎鎻愮ず銆�
-- 褰� `inputBoards > 0` 涓旀彁渚� `passRate` 鏃讹紝寤鸿涓庤绠楀�� `passBoards/inputBoards*100` 鍋忓樊 鈮� 1.0銆�
-
-绯荤粺璁板綍锛氭瘡娆¤皟鐢ㄤ細鍦ㄦ秷鎭腑蹇冭褰曚竴鏉℃祦姘达紙璁板綍鐩爣琛ㄥ悕涓� `MES_SPI_AOI_HEADER`銆佹帴鍙� URL銆佹柟娉曘�佽姹備笌澶勭悊缁撴灉锛夛紝鐢ㄤ簬瀹¤杩借釜銆�
-
----
-
-## 鎺ュ彛鍒楄〃
-
-### 1. 涓婁紶 SPI/AOI 鏁版嵁
-- 璺緞涓庢柟娉曪細`POST /api/SpiAoi/Upload`
-- 鐢ㄩ�旓細鏂板涓�绗斾富琛ㄤ笌澶氱瑪瀛愯〃鏁版嵁锛涜嫢 `boardBarcode` 宸插瓨鍦ㄥ垯澶辫触
-- 璇锋眰绀轰緥锛�
-```json
-{
-  "header": {
-    "testDate": "2025-10-10",
-    "testTime": "14:33:21",
-    "testResult": "0;0;0:1",
-    "surface": "T",
-    "totalPoints": 500,
-    "actualDefects": 3,
-    "equipmentModel": "SPI-9000",
-    "workOrder": "WO20251010-01",
-    "productModel": "MODEL-ABC",
-    "boardBarcode": "BC123456789",
-    "smtGroup": "A1",
-    "lineName": "SMT-01"
-  },
-  "details": [
-    {
-      "machineName": "AOI-01",
-      "lineDisplayName": "SMT-01",
-      "offsetCount": 1,
-      "missingCount": 0,
-      "reverseCount": 0,
-      "liftedCount": 0,
-      "floatHighCount": 0,
-      "tombstoneCount": 0,
-      "flipCount": 0,
-      "wrongPartCount": 0,
-      "leadLiftCount": 0,
-      "coldJointCount": 0,
-      "noSolderCount": 0,
-      "insufficientSolderCount": 1,
-      "excessSolderCount": 0,
-      "bridgeCount": 1,
-      "copperExposureCount": 0,
-      "spikeCount": 0,
-      "foreignMatterCount": 0,
-      "glueOverflowCount": 0,
-      "pinOffsetCount": 0,
-      "inputBoards": 100,
-      "okBoards": 98,
-      "passBoards": 98,
-      "passRate": 98.00,
-      "defectBoards": 2,
-      "defectRate": 2.00,
-      "defectPpm": 20000,
-      "defectPoints": 3,
-      "measuredPoints": 500,
-      "pendingPoints": 0
-    }
-  ]
-}
-```
-- 鎴愬姛鍝嶅簲绀轰緥锛�
-```json
-{
-  "status": 0,
-  "message": "OK",
-  "data": {
-    "headerId": 12001,
-    "detailCount": 1,
-    "message": "SPI/AOI妫�娴嬫暟鎹笂浼犳垚鍔�"
-  },
-  "TotalCount": 0
-}
-```
-- 澶辫触鍝嶅簲绀轰緥锛堟潯鐮侀噸澶嶏級锛�
-```json
-{
-  "status": 1,
-  "message": "涓婁紶SPI/AOI妫�娴嬫暟鎹け璐�: 鏉$爜 BC123456789 宸插瓨鍦紝涓嶅厑璁搁噸澶嶄笂浼�",
-  "data": "涓婁紶SPI/AOI妫�娴嬫暟鎹け璐�: 鏉$爜 BC123456789 宸插瓨鍦紝涓嶅厑璁搁噸澶嶄笂浼�",
-  "TotalCount": 0
-}
-```
- 
-
-## 璋冪敤寤鸿
-- 涓ユ牸鎸夊瓧娈电害鏉熺粍缁囨暟鎹紱鏁板�煎瓧娈典娇鐢ㄦ暣鏁版垨灏忔暟鐨勬纭被鍨�
-- 鍙戠敓閿欒鏃惰鍙� `message` 渚夸簬瀹氫綅闂锛涗繚鐣欒姹備笌鍝嶅簲浣滀负杩借釜渚濇嵁
diff --git a/MESApplication/Docs/Spi_API.md b/MESApplication/Docs/Spi_API.md
new file mode 100644
index 0000000..63a2088
--- /dev/null
+++ b/MESApplication/Docs/Spi_API.md
@@ -0,0 +1,140 @@
+# SPI 鎺ュ彛鏂囨。
+
+## 鍩烘湰淇℃伅
+- 鍩虹璺緞锛歚/api/SpiAoi`锛堢ず渚嬪畬鏁村湴鍧�锛歚http://192.168.1.251:10054/api/SpiAoi`锛�
+- 璇锋眰涓庡搷搴旀牸寮忥細`application/json; charset=utf-8`
+
+## 缁熶竴杩斿洖妯″瀷锛圧esponseResult锛�
+- `status`锛氭暣鏁帮紝`0` 琛ㄧず鎴愬姛锛宍1` 琛ㄧず澶辫触
+- `message`锛氬瓧绗︿覆锛岃繑鍥炶鏄庢垨閿欒淇℃伅
+- `data`锛氬璞★紝涓氬姟鏁版嵁杞戒綋
+- `TotalCount`锛氭暣鏁帮紝浠呭垎椤垫帴鍙d娇鐢紝琛ㄧず鎬昏褰曟暟
+
+ 
+### Spi鏁版嵁璇存槑
+- 璁惧 / 浜х嚎锛歚machineName`銆乣lineDisplayName`
+- 缂洪櫡缁熻锛坕nt锛夛細`offsetCount`銆乣missingCount`銆乣reverseCount`銆乣liftedCount`銆乣floatHighCount`銆乣tombstoneCount`銆乣flipCount`銆乣wrongPartCount`銆乣leadLiftCount`銆乣coldJointCount`銆乣noSolderCount`銆乣insufficientSolderCount`銆乣excessSolderCount`銆乣bridgeCount`銆乣copperExposureCount`銆乣spikeCount`銆乣foreignMatterCount`銆乣glueOverflowCount`銆乣pinOffsetCount`
+- 鐢熶骇缁熻锛歚inputBoards`銆乣okBoards`銆乣passBoards`銆乣defectBoards`銆乣defectPoints`銆乣measuredPoints`銆乣pendingPoints`
+- 姣斾緥瀛楁锛坉ecimal?锛夛細`passRate`銆乣defectRate`
+- 鍏跺畠锛歚defectPpm` *(int?)*銆乣headerId` *(decimal?)*
+ 
+鏃ュ織璁板綍锛氭瘡娆¤皟鐢ㄤ細鍐欏叆娑堟伅涓績锛坄MES_SPI_AOI_HEADER` / `MES_SPI_AOI_DETAIL`锛夛紝淇濆瓨璇锋眰浣撱�佺粨鏋滅瓑淇℃伅澶囩敤銆�
+
+---
+
+## 鎺ュ彛鍒楄〃
+
+浠ヤ笅涓や釜鎺ュ彛瑕嗙洊鍗曠瑪涓庢壒閲忓叆搴撳満鏅紝鍧囦娇鐢� POST銆�
+ 
+### 1. UploadSpiDetails 鈥� 涓婁紶 SPI鏄庣粏锛堝崟绗旓級
+- 璺緞锛歚POST /api/SpiAoi/UploadSpiDetails`
+- 鎻忚堪锛氭彃鍏ヤ竴鏉℃槑缁嗚褰曪紝闇�鎸囧畾 `headerId`锛堟垨鐢辨暟鎹簱瑙﹀彂鍣ㄨ嚜鍔ㄧ淮鎶わ級銆�
+- 璇锋眰绀轰緥
+  ```json
+  {
+    "headerId": 12001,
+    "machineName": "AOI-01",
+    "lineDisplayName": "SMT-01",
+    "offsetCount": 1,
+    "missingCount": 0,
+    "reverseCount": 0,
+    "liftedCount": 0,
+    "floatHighCount": 0,
+    "tombstoneCount": 0,
+    "flipCount": 0,
+    "wrongPartCount": 0,
+    "leadLiftCount": 0,
+    "coldJointCount": 0,
+    "noSolderCount": 0,
+    "insufficientSolderCount": 1,
+    "excessSolderCount": 0,
+    "bridgeCount": 1,
+    "copperExposureCount": 0,
+    "spikeCount": 0,
+    "foreignMatterCount": 0,
+    "glueOverflowCount": 0,
+    "pinOffsetCount": 0,
+    "inputBoards": 100,
+    "okBoards": 98,
+    "passBoards": 98,
+    "passRate": 98.00,
+    "defectBoards": 2,
+    "defectRate": 2.00,
+    "defectPpm": 20000,
+    "defectPoints": 3,
+    "measuredPoints": 500,
+    "pendingPoints": 0
+  }
+  ```
+- 鎴愬姛鍝嶅簲
+  ```json
+  {
+    "status": 0,
+    "message": "OK",
+    "data": {
+      "detailCount": 1,
+      "message": "SPI detail upload succeeded"
+    },
+    "TotalCount": 0
+  }
+  ```
+
+### 2. UploadSpiDetailsBatch 鈥� 鎵归噺涓婁紶 SPI鏄庣粏
+- 璺緞锛歚POST /api/SpiAoi/UploadSpiDetailsBatch`
+- 鎻忚堪锛氭壒閲忓啓鍏ュ鏉℃槑缁嗭紝浣跨敤鍚屼竴鏁版嵁搴撲簨鍔′繚璇佸師瀛愭�с��
+- 璇锋眰绀轰緥
+  ```json
+  [
+    {
+      "headerId": 12001,
+      "machineName": "AOI-01",
+      "lineDisplayName": "SMT-01",
+      "offsetCount": 1,
+      "missingCount": 0,
+      "reverseCount": 0,
+      "liftedCount": 0,
+      "floatHighCount": 0,
+      "tombstoneCount": 0,
+      "flipCount": 0,
+      "wrongPartCount": 0,
+      "leadLiftCount": 0,
+      "coldJointCount": 0,
+      "noSolderCount": 0,
+      "insufficientSolderCount": 1,
+      "excessSolderCount": 0,
+      "bridgeCount": 1,
+      "copperExposureCount": 0,
+      "spikeCount": 0,
+      "foreignMatterCount": 0,
+      "glueOverflowCount": 0,
+      "pinOffsetCount": 0,
+      "inputBoards": 100,
+      "okBoards": 98,
+      "passBoards": 98,
+      "passRate": 98.00,
+      "defectBoards": 2,
+      "defectRate": 2.00,
+      "defectPpm": 20000,
+      "defectPoints": 3,
+      "measuredPoints": 500,
+      "pendingPoints": 0
+    }
+  ]
+  ```
+- 鎴愬姛鍝嶅簲
+  ```json
+  {
+    "status": 0,
+    "message": "OK",
+    "data": {
+      "detailCount": 5,
+      "message": "SPI detail batch upload succeeded"
+    },
+    "TotalCount": 0
+  }
+  ```
+
+## 璋冪敤寤鸿
+- 涓ユ牸鎸夌収瀛楁绫诲瀷涓庨暱搴︽瀯閫犳暟鎹紱鏁板�肩被瀛楁寤鸿浣跨敤鏁村瀷鎴栦繚鐣欏悎閫傚皬鏁颁綅銆�
+- 鍑虹幇閿欒鏃跺厛鏌ョ湅 `message`锛屽啀缁撳悎娑堟伅涓績璁板綍鍜屾湇鍔$鏃ュ織瀹氫綅闂銆�
+- 寤鸿鍦ㄦ壒閲忚皟鐢ㄥ墠鑷鍘婚噸鏉$爜鎴栧叧閿瓧娈碉紝鍑忓皯鍥炴粴姒傜巼銆�

--
Gitblit v1.9.3