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
}