tjx
2025-10-15 9057d0f6f3a46b93d62d0b71c7f4f03eca41f3a9
11111
已修改3个文件
已删除1个文件
已添加2个文件
1115 ■■■■■ 文件已修改
MES.Service/Dto/service/SpiAoiDto.cs 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
MES.Service/service/QC/SpiAoiService.cs 444 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
MESApplication/Controllers/QC/SpiAoiController.cs 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
MESApplication/Docs/Aoi_API.md 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
MESApplication/Docs/SpiAoi_API.md 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
MESApplication/Docs/Spi_API.md 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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、Fail等)
    ///     å¨´å¬­ç˜¯ç¼æ’´ç‰(濡傦細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>
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>
    ///     å°†ä¸»è¡¨DTO转换为实体
    /// </summary>
    /// <param name="dto">主表DTO</param>
    /// <returns>主表实体</returns>
    private MesSpiAoiHeader ConvertHeaderDtoToEntity(SpiAoiHeaderDto dto)
    {
        return new MesSpiAoiHeader
@@ -335,18 +399,50 @@
    }
    /// <summary>
    ///     å°†å­è¡¨DTO列表转换为实体列表
    ///     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
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;
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`
## ç»Ÿä¸€è¿”回模型(ResponseResult)
- `status`:整数,`0` è¡¨ç¤ºæˆåŠŸï¼Œ`1` è¡¨ç¤ºå¤±è´¥
- `message`:字符串,返回说明或错误信息
- `data`:对象,业务数据载体
- `TotalCount`:整数,仅分页接口使用,表示总记录数
###  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`:OK æ¿æ•°ï¼ˆæ£€æµ‹ä¸º 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`,再结合消息中心记录和服务端日志定位问题。
- å»ºè®®åœ¨æ‰¹é‡è°ƒç”¨å‰è‡ªè¡ŒåŽ»é‡æ¡ç æˆ–å…³é”®å­—æ®µï¼Œå‡å°‘å›žæ»šæ¦‚çŽ‡ã€‚
MESApplication/Docs/SpiAoi_API.md
ÎļþÒÑɾ³ý
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`
## ç»Ÿä¸€è¿”回模型(ResponseResult)
- `status`:整数,`0` è¡¨ç¤ºæˆåŠŸï¼Œ`1` è¡¨ç¤ºå¤±è´¥
- `message`:字符串,返回说明或错误信息
- `data`:对象,业务数据载体
- `TotalCount`:整数,仅分页接口使用,表示总记录数
### Spi数据说明
- è®¾å¤‡ / äº§çº¿ï¼š`machineName`、`lineDisplayName`
- ç¼ºé™·ç»Ÿè®¡ï¼ˆint):`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`
- æ¯”例字段(decimal?):`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`,再结合消息中心记录和服务端日志定位问题。
- å»ºè®®åœ¨æ‰¹é‡è°ƒç”¨å‰è‡ªè¡ŒåŽ»é‡æ¡ç æˆ–å…³é”®å­—æ®µï¼Œå‡å°‘å›žæ»šæ¦‚çŽ‡ã€‚