using System.Linq; using MES.Service.DB; using MES.Service.Dto.service; using MES.Service.Modes; using MES.Service.util; namespace MES.Service.service.QC; /// /// SPI/AOI检测数据服务 /// public class SpiAoiService { /// /// Upload AOI header data. /// /// Header DTO. /// Header upload response. public SpiAoiHeaderUploadResponse UploadAoiHeader(SpiAoiHeaderDto header) { if (header == null) { throw new Exception("header cannot be null"); } try { return UploadAoiHeaderBatchInternal( new List { header })[0]; } catch (Exception ex) { throw new Exception($"Failed to upload AOI header: {ex.Message}", ex); } } /// /// Upload multiple AOI header records within a single transaction. /// /// Header DTO collection. /// Header upload responses. public List UploadAoiHeaderBatch( List 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 UploadAoiHeaderBatchInternal( List 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(); SqlSugarHelper.UseTransactionWithOracle(db => { var boardBarcodes = headerList.Select(h => h.BoardBarcode) .ToList(); if (boardBarcodes.Any()) { var existingBarcodes = db.Queryable() .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; } /// /// Upload SPI detail data. /// /// Detail upload DTO. /// Detail upload response. public SpiAoiDetailUploadResponse UploadSpiDetails(SpiAoiDetailDto request) { if (request == null) { throw new Exception("request cannot be null"); } try { return UploadSpiDetailsInternal( new List { request }); } catch (Exception ex) { throw new Exception($"Failed to upload SPI details: {ex.Message}", ex); } } /// /// Upload multiple SPI detail records within a single transaction. /// /// Detail DTO collection. /// Aggregated upload response. public SpiAoiDetailUploadResponse UploadSpiDetailsBatch( List 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 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 }; } /// /// Retrieve SPI/AOI data by board barcode. /// /// Board barcode. /// Header and detail tuple. public (MesSpiAoiHeader header, List details) GetByBarcode( string boardBarcode) { try { var db = SqlSugarHelper.GetInstance(); var header = db.Queryable() .Where(x => x.BoardBarcode == boardBarcode) .First(); if (header == null) { return (null, null); } var details = db.Queryable() .Where(x => x.HeaderId == header.Id) .ToList(); return (header, details); } catch (Exception ex) { throw new Exception($"查询SPI/AOI检测数据失�? {ex.Message}", ex); } } public (MesSpiAoiHeader header, List details) GetById( decimal headerId) { try { var db = SqlSugarHelper.GetInstance(); var header = db.Queryable() .Where(x => x.Id == headerId) .First(); if (header == null) { return (null, null); } var details = db.Queryable() .Where(x => x.HeaderId == headerId) .ToList(); return (header, details); } catch (Exception ex) { throw new Exception($"查询SPI/AOI检测数据失�? {ex.Message}", ex); } } /// /// 分页查询SPI/AOI检测数�? /// /// 条码(可选) /// 工单(可选) /// 板面(可选) /// 开始日�?可选) /// 结束日期(可选) /// 页码 /// 页大�?/param> /// 分页数据 public (List items, int totalCount) GetPage( string boardBarcode = null, string workOrder = null, string surface = null, DateTime? startDate = null, DateTime? endDate = null, int pageIndex = 1, int pageSize = 20) { try { var db = SqlSugarHelper.GetInstance(); var totalCount = 0; var data = db.Queryable() .WhereIF(StringUtil.IsNotNullOrEmpty(boardBarcode), x => x.BoardBarcode.Contains(boardBarcode)) .WhereIF(StringUtil.IsNotNullOrEmpty(workOrder), x => x.WorkOrder.Contains(workOrder)) .WhereIF(StringUtil.IsNotNullOrEmpty(surface), x => x.Surface == surface) .WhereIF(startDate.HasValue, x => x.TestDate >= startDate.Value) .WhereIF(endDate.HasValue, x => x.TestDate <= endDate.Value) .OrderBy(x => x.CreatedAt, SqlSugar.OrderByType.Desc) .ToPageList(pageIndex, pageSize, ref totalCount); return (data, totalCount); } catch (Exception ex) { throw new Exception($"分页查询SPI/AOI检测数据失�? {ex.Message}", ex); } } #region 私有方法 /// /// Validate AOI header payload. /// /// Header DTO. private void ValidateHeader(SpiAoiHeaderDto header) { if (header == null) { throw new Exception("header cannot be null"); } if (StringUtil.IsNullOrEmpty(header.TestDate)) { throw new Exception("testDate is required"); } if (StringUtil.IsNullOrEmpty(header.TestTime)) { throw new Exception("testTime is required"); } if (StringUtil.IsNullOrEmpty(header.TestResult)) { throw new Exception("testResult is required"); } if (StringUtil.IsNullOrEmpty(header.BoardBarcode)) { throw new Exception("boardBarcode is required"); } if (StringUtil.IsNullOrEmpty(header.Surface)) { throw new Exception("surface is required"); } if (header.Surface != "T" && header.Surface != "B") { throw new Exception("surface must be T or B"); } if (header.BoardBarcode.Length > 128) { throw new Exception("boardBarcode cannot exceed 128 characters"); } if (!StringUtil.IsNullOrEmpty(header.TestResult) && header.TestResult.Length > 12) { throw new Exception("testResult cannot exceed 12 characters"); } } private MesSpiAoiHeader ConvertHeaderDtoToEntity(SpiAoiHeaderDto dto) { return new MesSpiAoiHeader { TestDate = DateTime.Parse(dto.TestDate), TestTime = dto.TestTime, TestResult = dto.TestResult, Surface = dto.Surface, TotalPoints = dto.TotalPoints, ActualDefects = dto.ActualDefects, EquipmentModel = dto.EquipmentModel, WorkOrder = dto.WorkOrder, ProductModel = dto.ProductModel, BoardBarcode = dto.BoardBarcode, SmtGroup = dto.SmtGroup, LineName = dto.LineName }; } /// /// Perform non-blocking SPI detail checks (warnings only). /// /// Detail DTO list. private void ValidateDetailData(List 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 new MesSpiAoiDetail { HeaderId = dto.HeaderId ?? 0, AreaOverflowCount = dto.AreaOverflowCount, AreaUnderflowCount = dto.AreaUnderflowCount, ExceedingHeightCount = dto.ExceedingHeightCount, InsufficientHeightCount = dto.InsufficientHeightCount, XDeviationCount = dto.XDeviationCount, YDeviationCount = dto.YDeviationCount, CollapseCount = dto.CollapseCount, SolderPullTipCount = dto.SolderPullTipCount, AbnormalityCount = dto.AbnormalityCount, LineDisplayName = dto.LineDisplayName, MachineName = dto.MachineName, InputBoards = dto.InputBoards, OkBoards = dto.OkBoards, PassBoards = dto.PassBoards, PassRate = dto.PassRate, DefectBoards = dto.DefectBoards, DefectRate = dto.DefectRate, DefectPpm = dto.DefectPpm, DefectPoints = dto.DefectPoints, MeasuredPoints = dto.MeasuredPoints, PendingPoints = dto.PendingPoints, CreatedAt = now, UpdatedAt = now }; } #endregion }