using MES.Service.DB;
|
using MES.Service.Dto.service;
|
using MES.Service.Modes;
|
using MES.Service.util;
|
|
namespace MES.Service.service.QC;
|
|
/// <summary>
|
/// SPI/AOI检测数据服务
|
/// </summary>
|
public class SpiAoiService
|
{
|
/// <summary>
|
/// 上传SPI/AOI检测数据
|
/// </summary>
|
/// <param name="request">上传请求DTO</param>
|
/// <returns>上传响应DTO</returns>
|
public SpiAoiUploadResponse UploadSpiAoiData(SpiAoiUploadRequest request)
|
{
|
// 1. 基本校验
|
ValidateRequest(request);
|
|
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;
|
}
|
catch (Exception ex)
|
{
|
throw new Exception($"上传SPI/AOI检测数据失败: {ex.Message}", ex);
|
}
|
}
|
|
/// <summary>
|
/// 根据条码查询SPI/AOI检测数据
|
/// </summary>
|
/// <param name="boardBarcode">条码</param>
|
/// <returns>检测数据(主表+子表)</returns>
|
public (MesSpiAoiHeader header, List<MesSpiAoiDetail> details) GetByBarcode(string boardBarcode)
|
{
|
try
|
{
|
var db = SqlSugarHelper.GetInstance();
|
|
var header = db.Queryable<MesSpiAoiHeader>()
|
.Where(x => x.BoardBarcode == boardBarcode)
|
.First();
|
|
if (header == null)
|
{
|
return (null, null);
|
}
|
|
var details = db.Queryable<MesSpiAoiDetail>()
|
.Where(x => x.HeaderId == header.Id)
|
.ToList();
|
|
return (header, details);
|
}
|
catch (Exception 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)
|
{
|
try
|
{
|
var db = SqlSugarHelper.GetInstance();
|
|
var header = db.Queryable<MesSpiAoiHeader>()
|
.Where(x => x.Id == headerId)
|
.First();
|
|
if (header == null)
|
{
|
return (null, null);
|
}
|
|
var details = db.Queryable<MesSpiAoiDetail>()
|
.Where(x => x.HeaderId == headerId)
|
.ToList();
|
|
return (header, details);
|
}
|
catch (Exception ex)
|
{
|
throw new Exception($"查询SPI/AOI检测数据失败: {ex.Message}", ex);
|
}
|
}
|
|
/// <summary>
|
/// 分页查询SPI/AOI检测数据
|
/// </summary>
|
/// <param name="boardBarcode">条码(可选)</param>
|
/// <param name="workOrder">工单(可选)</param>
|
/// <param name="surface">板面(可选)</param>
|
/// <param name="startDate">开始日期(可选)</param>
|
/// <param name="endDate">结束日期(可选)</param>
|
/// <param name="pageIndex">页码</param>
|
/// <param name="pageSize">页大小</param>
|
/// <returns>分页数据</returns>
|
public (List<MesSpiAoiHeader> 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<MesSpiAoiHeader>()
|
.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 私有方法
|
|
/// <summary>
|
/// 校验请求参数
|
/// </summary>
|
/// <param name="request">请求DTO</param>
|
private void ValidateRequest(SpiAoiUploadRequest request)
|
{
|
if (request == null)
|
{
|
throw new Exception("请求参数不能为空");
|
}
|
|
if (request.Header == null)
|
{
|
throw new Exception("header 不能为空");
|
}
|
|
if (request.Details == null || request.Details.Count == 0)
|
{
|
throw new Exception("details 不能为空");
|
}
|
|
// 校验必填字段
|
if (StringUtil.IsNullOrEmpty(request.Header.TestDate))
|
{
|
throw new Exception("testDate 不能为空");
|
}
|
|
if (StringUtil.IsNullOrEmpty(request.Header.TestTime))
|
{
|
throw new Exception("testTime 不能为空");
|
}
|
|
if (StringUtil.IsNullOrEmpty(request.Header.TestResult))
|
{
|
throw new Exception("testResult 不能为空");
|
}
|
|
if (StringUtil.IsNullOrEmpty(request.Header.BoardBarcode))
|
{
|
throw new Exception("boardBarcode 不能为空");
|
}
|
|
if (StringUtil.IsNullOrEmpty(request.Header.Surface))
|
{
|
throw new Exception("surface 不能为空");
|
}
|
|
// 校验枚举值
|
if (request.Header.Surface != "T" && request.Header.Surface != "B")
|
{
|
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");
|
}
|
}
|
}
|
|
/// <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
|
{
|
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
|
};
|
}
|
|
/// <summary>
|
/// 将子表DTO列表转换为实体列表
|
/// </summary>
|
/// <param name="dtoList">子表DTO列表</param>
|
/// <param name="headerId">主表ID</param>
|
/// <returns>子表实体列表</returns>
|
private List<MesSpiAoiDetail> ConvertDetailDtoListToEntity(
|
List<SpiAoiDetailDto> dtoList, decimal headerId)
|
{
|
var now = DateTime.Now;
|
return dtoList.Select(dto => new MesSpiAoiDetail
|
{
|
HeaderId = headerId,
|
OffsetCount = dto.OffsetCount,
|
MissingCount = dto.MissingCount,
|
ReverseCount = dto.ReverseCount,
|
LiftedCount = dto.LiftedCount,
|
FloatHighCount = dto.FloatHighCount,
|
TombstoneCount = dto.TombstoneCount,
|
FlipCount = dto.FlipCount,
|
WrongPartCount = dto.WrongPartCount,
|
LeadLiftCount = dto.LeadLiftCount,
|
ColdJointCount = dto.ColdJointCount,
|
NoSolderCount = dto.NoSolderCount,
|
InsufficientSolderCount = dto.InsufficientSolderCount,
|
ExcessSolderCount = dto.ExcessSolderCount,
|
BridgeCount = dto.BridgeCount,
|
CopperExposureCount = dto.CopperExposureCount,
|
SpikeCount = dto.SpikeCount,
|
ForeignMatterCount = dto.ForeignMatterCount,
|
GlueOverflowCount = dto.GlueOverflowCount,
|
PinOffsetCount = dto.PinOffsetCount,
|
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
|
}).ToList();
|
}
|
|
#endregion
|
}
|