using Admin.NET.Plugin.AiDOP.Dto.S0.Quality; using Admin.NET.Plugin.AiDOP.Entity.S0.Quality; using Admin.NET.Plugin.AiDOP.Infrastructure; using static Admin.NET.Plugin.AiDOP.Controllers.S0.Quality.AdoS0QmsControllerHelpers; namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Quality; [ApiController] [Route("api/s0/quality/inspection-bases")] [NonUnify] public class AdoS0QmsInspectionBasesController : ControllerBase { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _entryRep; public AdoS0QmsInspectionBasesController( SqlSugarRepository rep, SqlSugarRepository entryRep) { _rep = rep; _entryRep = entryRep; } [HttpGet] public async Task GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize); var query = _rep.AsQueryable().Where(x => x.TenantId == tenantId) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => (x.Number != null && x.Number.Contains(q.Keyword!)) || (x.Name != null && x.Name.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync(); return Ok(new { total, page = q.Page, pageSize = q.PageSize, list }); } [HttpGet("{id:long}")] public async Task GetDetailAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync(); return Ok(new { master, items }); } [HttpGet("options")] public async Task GetOptionsAsync() { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; return Ok(await _rep.AsQueryable().Where(x => x.TenantId == tenantId) .OrderBy(x => x.Number) .Select(x => new AdoS0QualitySimpleOptionDto { Value = x.Id, Label = (x.Number ?? "") + " / " + (x.Name ?? "") }) .ToListAsync()); } [HttpPost] public async Task CreateAsync([FromBody] AdoS0QmsInspectionBasisUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var db = _rep.Context; await db.Ado.BeginTranAsync(); try { var master = new AdoS0QmsInspectionBasis(); ApplyInspectionBasis(master, dto, true); master.TenantId = tenantId; await _rep.AsInsertable(master).ExecuteReturnEntityAsync(); await SyncInspectionBasisEntriesAsync(master.Id, dto.Items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(master.Id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0QmsInspectionBasisUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { ApplyInspectionBasis(master, dto, false); await _rep.AsUpdateable(master).ExecuteCommandAsync(); await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await SyncInspectionBasisEntriesAsync(id, dto.Items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpDelete("{id:long}")] public async Task DeleteAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await _rep.DeleteAsync(master); await db.Ado.CommitTranAsync(); return Ok(new { message = "删除成功" }); } catch { await db.Ado.RollbackTranAsync(); throw; } } private static void ApplyInspectionBasis(AdoS0QmsInspectionBasis entity, AdoS0QmsInspectionBasisUpsertDto dto, bool isCreate) { entity.Number = dto.Number.Trim(); entity.Name = dto.Name.Trim(); entity.ControlStrategy = NullIfWhiteSpace(dto.ControlStrategy); entity.CreateOrgId = dto.CreateOrgId; entity.UseOrgId = dto.UseOrgId; entity.Comment = NullIfWhiteSpace(dto.Comment); entity.Status = NullIfWhiteSpace(dto.Status); entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus); if (isCreate) entity.CreateTime = DateTime.Now; entity.ModifyTime = DateTime.Now; } private async Task SyncInspectionBasisEntriesAsync(long masterId, IEnumerable items, long tenantId) { var seq = 1L; foreach (var item in items) { var entity = new AdoS0QmsInspectionBasisEntry { TenantId = tenantId, MasterId = masterId, Seq = item.Seq ?? seq++, DocumentNumber = NullIfWhiteSpace(item.DocumentNumber), DocumentName = NullIfWhiteSpace(item.DocumentName), Attachment = NullIfWhiteSpace(item.Attachment) }; await _entryRep.AsInsertable(entity).ExecuteCommandAsync(); } } } [ApiController] [Route("api/s0/quality/inspection-standards")] [NonUnify] public class AdoS0QmsInspectionStandardsController : ControllerBase { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _entryRep; public AdoS0QmsInspectionStandardsController( SqlSugarRepository rep, SqlSugarRepository entryRep) { _rep = rep; _entryRep = entryRep; } [HttpGet] public async Task GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize); var query = _rep.AsQueryable().Where(x => x.TenantId == tenantId) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => (x.Number != null && x.Number.Contains(q.Keyword!)) || (x.Name != null && x.Name.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync(); return Ok(new { total, page = q.Page, pageSize = q.PageSize, list }); } [HttpGet("{id:long}")] public async Task GetDetailAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync(); return Ok(new { master, items }); } [HttpGet("options")] public async Task GetOptionsAsync() { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; return Ok(await _rep.AsQueryable().Where(x => x.TenantId == tenantId) .OrderBy(x => x.Number) .Select(x => new AdoS0QualitySimpleOptionDto { Value = x.Id, Label = (x.Number ?? "") + " / " + (x.Name ?? "") }) .ToListAsync()); } [HttpPost] public async Task CreateAsync([FromBody] AdoS0QmsInspectionStandardUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var db = _rep.Context; await db.Ado.BeginTranAsync(); try { var master = new AdoS0QmsInspectionStandard(); ApplyInspectionStandard(master, dto); master.TenantId = tenantId; await _rep.AsInsertable(master).ExecuteReturnEntityAsync(); await SyncInspectionStandardEntriesAsync(master.Id, dto.Items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(master.Id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0QmsInspectionStandardUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { ApplyInspectionStandard(master, dto); await _rep.AsUpdateable(master).ExecuteCommandAsync(); await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await SyncInspectionStandardEntriesAsync(id, dto.Items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpDelete("{id:long}")] public async Task DeleteAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await _rep.DeleteAsync(master); await db.Ado.CommitTranAsync(); return Ok(new { message = "删除成功" }); } catch { await db.Ado.RollbackTranAsync(); throw; } } private static void ApplyInspectionStandard(AdoS0QmsInspectionStandard entity, AdoS0QmsInspectionStandardUpsertDto dto) { entity.Number = dto.Number.Trim(); entity.Name = dto.Name.Trim(); entity.Comment = NullIfWhiteSpace(dto.Comment); entity.ControlStrategy = NullIfWhiteSpace(dto.ControlStrategy); entity.CreateOrgId = dto.CreateOrgId; entity.UseOrgId = dto.UseOrgId; entity.Status = NullIfWhiteSpace(dto.Status); entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus); } private async Task SyncInspectionStandardEntriesAsync(long masterId, IEnumerable items, long tenantId) { var seq = 1L; foreach (var item in items) { var entity = new AdoS0QmsInspectionStandardEntry { TenantId = tenantId, MasterId = masterId, Seq = item.Seq ?? seq++, CheckItems = NullIfWhiteSpace(item.CheckItems), CheckContent = NullIfWhiteSpace(item.CheckContent), NormType = NullIfWhiteSpace(item.NormType), SpecValue = NullIfWhiteSpace(item.SpecValue), TopValue = item.TopValue, DownValue = item.DownValue, CheckBasisId = item.CheckBasisId, CheckMethodId = item.CheckMethodId, CheckFrequencyId = item.CheckFrequencyId, CheckInstructId = item.CheckInstructId, Unit = NullIfWhiteSpace(item.Unit), KeyQuality = item.KeyQuality }; await _entryRep.AsInsertable(entity).ExecuteCommandAsync(); } } } [ApiController] [Route("api/s0/quality/inspection-plans")] [NonUnify] public class AdoS0QmsInspectionPlansController : ControllerBase { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _entryRep; public AdoS0QmsInspectionPlansController( SqlSugarRepository rep, SqlSugarRepository entryRep) { _rep = rep; _entryRep = entryRep; } [HttpGet] public async Task GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize); var query = _rep.AsQueryable().Where(x => x.TenantId == tenantId) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => (x.Number != null && x.Number.Contains(q.Keyword!)) || (x.Name != null && x.Name.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query.OrderBy(x => x.Number).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync(); return Ok(new { total, page = q.Page, pageSize = q.PageSize, list }); } [HttpGet("{id:long}")] public async Task GetDetailAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync(); return Ok(new { master, items }); } [HttpPost] public async Task CreateAsync([FromBody] AdoS0QmsInspectionPlanUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var db = _rep.Context; await db.Ado.BeginTranAsync(); try { var master = new AdoS0QmsInspectionPlan(); ApplyInspectionPlan(master, dto, true); master.TenantId = tenantId; await _rep.AsInsertable(master).ExecuteReturnEntityAsync(); await SyncInspectionPlanEntriesAsync(master.Id, dto.Items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(master.Id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0QmsInspectionPlanUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { ApplyInspectionPlan(master, dto, false); await _rep.AsUpdateable(master).ExecuteCommandAsync(); await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await SyncInspectionPlanEntriesAsync(id, dto.Items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpDelete("{id:long}")] public async Task DeleteAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await _rep.DeleteAsync(master); await db.Ado.CommitTranAsync(); return Ok(new { message = "删除成功" }); } catch { await db.Ado.RollbackTranAsync(); throw; } } private static void ApplyInspectionPlan(AdoS0QmsInspectionPlan entity, AdoS0QmsInspectionPlanUpsertDto dto, bool isCreate) { entity.Number = dto.Number.Trim(); entity.Name = dto.Name.Trim(); entity.BizTypeId = NullIfWhiteSpace(dto.BizTypeId); entity.Comment = NullIfWhiteSpace(dto.Comment); entity.ControlStrategy = NullIfWhiteSpace(dto.ControlStrategy); entity.CreateOrgId = dto.CreateOrgId; entity.UseOrgId = dto.UseOrgId; entity.Status = NullIfWhiteSpace(dto.Status); entity.EnableStatus = NullIfWhiteSpace(dto.EnableStatus); if (isCreate) entity.CreateTime = DateTime.Now; entity.ModifyTime = DateTime.Now; } private async Task SyncInspectionPlanEntriesAsync(long masterId, IEnumerable items, long tenantId) { var seq = 1; foreach (var item in items) { var entity = new AdoS0QmsInspectionPlanEntry { TenantId = tenantId, MasterId = masterId, Seq = item.Seq ?? seq++, SetupType = NullIfWhiteSpace(item.SetupType), MaterialCode = NullIfWhiteSpace(item.MaterialCode), MaterialName = NullIfWhiteSpace(item.MaterialName), MaterialTypeId = item.MaterialTypeId, SupplierId = NullIfWhiteSpace(item.SupplierId), SamplingSchemeId = item.SamplingSchemeId, InspectionStandardId = item.InspectionStandardId, InspectOrgId = item.InspectOrgId, InspectUserId = item.InspectUserId, QRouteId = item.QRouteId, OperationNo = NullIfWhiteSpace(item.OperationNo), OperationId = item.OperationId, InspectionFrequencyId = item.InspectionFrequencyId, ProcessSeq = NullIfWhiteSpace(item.ProcessSeq), InspectionType = item.InspectionType }; await _entryRep.AsInsertable(entity).ExecuteCommandAsync(); } } } [ApiController] [Route("api/s0/quality/raw-inspection-specs")] [NonUnify] public class AdoS0QmsRawInspectionSpecsController : ControllerBase { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _entryRep; /// /// S5 IQC 检规↔物料桥表同步(B-1)。原材料检规是 S5 IQC 的检规源, /// 主表 wlbm 变化后必须同步派生映射,否则 B-2 Resolver 会读到过期关系。 /// 与 S7 在 AdoS0QualityFqcOqcControllers 中的 FqcSpecMaterialMapService 用法同构。 /// private readonly MaterialWarehouse.S5IqcSpecMaterialMapService _iqcSpecMap; public AdoS0QmsRawInspectionSpecsController( SqlSugarRepository rep, SqlSugarRepository entryRep, MaterialWarehouse.S5IqcSpecMaterialMapService iqcSpecMap) { _rep = rep; _entryRep = entryRep; _iqcSpecMap = iqcSpecMap; } [HttpGet] public async Task GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize); var query = _rep.AsQueryable().Where(x => x.TenantId == tenantId) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => (x.FileNumber != null && x.FileNumber.Contains(q.Keyword!)) || (x.RawMaterialName != null && x.RawMaterialName.Contains(q.Keyword!)) || (x.MaterialCode != null && x.MaterialCode.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query.OrderByDescending(x => x.Id).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync(); return Ok(new { total, page = q.Page, pageSize = q.PageSize, list }); } [HttpGet("{id:long}")] public Task GetDetailAsync(long id) => GetDetailAsync(id, null); /// /// 详情。 仅在 Create/Update 后回传 S5 IQC 桥表同步告警, /// 正常读取恒为 null(响应形状对既有前端向后兼容,仅新增可选字段)。 /// [NonAction] private async Task GetDetailAsync(long id, string? specMapWarning) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Seq).OrderBy(x => x.Id).ToListAsync(); return Ok(new { master, items, specMapWarning }); } /// /// 同步 S5 IQC 检规物料桥表;失败**不抛**(业务写已提交,桥表是派生数据), /// 以告警字符串回传调用方,并可通过 POST /api/S5IqcSpecMap/sync/{specId} 幂等重试。 /// [NonAction] private async Task TrySyncIqcSpecMapAsync(long tenantId, long specId) { try { await _iqcSpecMap.SyncSpecAsync(tenantId, specId); return null; } catch (Exception ex) { return $"检规已保存,但 IQC 检规物料映射同步失败,请调用 POST /api/S5IqcSpecMap/sync/{specId} 重试:{ex.Message}"; } } [HttpPost] public async Task CreateAsync([FromBody] AdoS0QmsRawInspectionSpecUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var items = AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items); var guardError = AdoS0QmsSpecAggregateGuard.ValidateCreate(RawRequiredFields(dto), items.Count); if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { var master = new AdoS0QmsRawInspectionSpec(); ApplyRawInspectionSpec(master, dto); master.TenantId = tenantId; await _rep.AsInsertable(master).ExecuteReturnEntityAsync(); await SyncRawInspectionSpecEntriesAsync(master.Id, items, tenantId); await db.Ado.CommitTranAsync(); // 业务写已提交后再同步 S5 IQC 桥表(同构 S7 AdoS0QualityFqcOqcControllers)。 // 桥表是派生数据,其失败不得回滚已成功的检规保存;失败以 specMapWarning 显式回传,可人工 resync。 var mapWarn = await TrySyncIqcSpecMapAsync(tenantId, master.Id); return await GetDetailAsync(master.Id, mapWarn); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0QmsRawInspectionSpecUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); // 写入集保留历史已持久化的明细行(含空行),只丢弃本次新补的空占位; // 「原有明细数」按 meaningful 计,避免全空明细的历史单被自己的空行锁死。 var existingRows = await _entryRep.AsQueryable().Where(x => x.MasterId == id).ToListAsync(); var items = AdoS0QmsSpecAggregateGuard.ForUpdateWrite(dto.Items); var guardError = AdoS0QmsSpecAggregateGuard.ValidateUpdate( [ ("文件编号", master.FileNumber, dto.FileNumber), ("物料编码", master.MaterialCode, dto.MaterialCode), ("原材料名称", master.RawMaterialName, dto.RawMaterialName), ("版本", master.VersionNo, dto.VersionNo) ], AdoS0QmsSpecAggregateGuard.CountMeaningful(existingRows), AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items).Count); if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { ApplyRawInspectionSpec(master, dto); await _rep.AsUpdateable(master).ExecuteCommandAsync(); await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await SyncRawInspectionSpecEntriesAsync(id, items, tenantId); await db.Ado.CommitTranAsync(); // wlbm/wjbh/bb 可能已变更 → 重建该检规的自动派生映射(人工绑定行不受影响) var mapWarn = await TrySyncIqcSpecMapAsync(tenantId, id); return await GetDetailAsync(id, mapWarn); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpDelete("{id:long}")] public async Task DeleteAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await _rep.DeleteAsync(master); await db.Ado.CommitTranAsync(); // 检规已删除 → 桥表整条清理(含人工绑定行:指向已删检规的人工绑定属悬挂引用) string? mapWarn = null; try { await _iqcSpecMap.PurgeSpecAsync(tenantId, id); } catch (Exception ex) { mapWarn = $"检规已删除,但 IQC 检规物料映射清理失败,请调用 POST /api/S5IqcSpecMap/sync/{id} 重试:{ex.Message}"; } return Ok(new { message = "删除成功", specMapWarning = mapWarn }); } catch { await db.Ado.RollbackTranAsync(); throw; } } /// 原材料检规必填集(UAT-S0-10 拍板)。与 Excel 导入入口共用同一守卫,口径单一。 private static IReadOnlyList<(string, string?)> RawRequiredFields(AdoS0QmsRawInspectionSpecUpsertDto dto) => [ ("文件编号", dto.FileNumber), ("物料编码", dto.MaterialCode), ("原材料名称", dto.RawMaterialName), ("版本", dto.VersionNo), ]; private static void ApplyRawInspectionSpec(AdoS0QmsRawInspectionSpec entity, AdoS0QmsRawInspectionSpecUpsertDto dto) { entity.FileNumber = NullIfWhiteSpace(dto.FileNumber); entity.VersionNo = NullIfWhiteSpace(dto.VersionNo); entity.DrawingNo = NullIfWhiteSpace(dto.DrawingNo); entity.RawMaterialName = NullIfWhiteSpace(dto.RawMaterialName); entity.MaterialCode = NullIfWhiteSpace(dto.MaterialCode); entity.EffectiveDate = NullIfWhiteSpace(dto.EffectiveDate); entity.DrawingVersion = NullIfWhiteSpace(dto.DrawingVersion); entity.MaterialGrade = NullIfWhiteSpace(dto.MaterialGrade); entity.CavityOrMold = NullIfWhiteSpace(dto.CavityOrMold); entity.Attachment = NullIfWhiteSpace(dto.Attachment); entity.FileName = NullIfWhiteSpace(dto.FileName); entity.Title = NullIfWhiteSpace(dto.Title); } private async Task SyncRawInspectionSpecEntriesAsync(long masterId, IEnumerable items, long tenantId) { var seq = 1; foreach (var item in items) { var entity = new AdoS0QmsRawInspectionSpecEntry { TenantId = tenantId, MasterId = masterId, Seq = item.Seq ?? seq++, InspectionItem = NullIfWhiteSpace(item.InspectionItem), InspectionStandard = NullIfWhiteSpace(item.InspectionStandard), InspectionMethod = NullIfWhiteSpace(item.InspectionMethod), ImageCategory = NullIfWhiteSpace(item.ImageCategory), SamplingScheme = NullIfWhiteSpace(item.SamplingScheme), Remark = NullIfWhiteSpace(item.Remark), Attachment = NullIfWhiteSpace(item.Attachment), UpperLimit = NullIfWhiteSpace(item.UpperLimit), LowerLimit = NullIfWhiteSpace(item.LowerLimit) }; await _entryRep.AsInsertable(entity).ExecuteCommandAsync(); } } } [ApiController] [Route("api/s0/quality/process-inspection-specs")] [NonUnify] public class AdoS0QmsProcessInspectionSpecsController : ControllerBase { private readonly SqlSugarRepository _rep; private readonly SqlSugarRepository _entryRep; public AdoS0QmsProcessInspectionSpecsController( SqlSugarRepository rep, SqlSugarRepository entryRep) { _rep = rep; _entryRep = entryRep; } [HttpGet] public async Task GetPagedAsync([FromQuery] AdoS0QualityPagedQueryDto q) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize); var query = _rep.AsQueryable().Where(x => x.TenantId == tenantId) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => (x.FileNumber != null && x.FileNumber.Contains(q.Keyword!)) || (x.ApplicableModel != null && x.ApplicableModel.Contains(q.Keyword!)) || (x.MaterialCode != null && x.MaterialCode.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query.OrderByDescending(x => x.Id).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync(); return Ok(new { total, page = q.Page, pageSize = q.PageSize, list }); } [HttpGet("{id:long}")] public async Task GetDetailAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var items = await _entryRep.AsQueryable().Where(x => x.MasterId == id).OrderBy(x => x.Id).ToListAsync(); return Ok(new { master, items }); } [HttpPost] public async Task CreateAsync([FromBody] AdoS0QmsProcessInspectionSpecUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var items = AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items); var guardError = AdoS0QmsSpecAggregateGuard.ValidateCreate(ProcessRequiredFields(dto), items.Count); if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { var master = new AdoS0QmsProcessInspectionSpec(); ApplyProcessInspectionSpec(master, dto); master.TenantId = tenantId; await _rep.AsInsertable(master).ExecuteReturnEntityAsync(); await SyncProcessInspectionSpecEntriesAsync(master.Id, items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(master.Id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0QmsProcessInspectionSpecUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); // 写入集保留历史已持久化的明细行(含空行),只丢弃本次新补的空占位; // 「原有明细数」按 meaningful 计,避免全空明细的历史单被自己的空行锁死。 var existingRows = await _entryRep.AsQueryable().Where(x => x.MasterId == id).ToListAsync(); var items = AdoS0QmsSpecAggregateGuard.ForUpdateWrite(dto.Items); var guardError = AdoS0QmsSpecAggregateGuard.ValidateUpdate( [ ("文件编号", master.FileNumber, dto.FileNumber), ("物料编码", master.MaterialCode, dto.MaterialCode), ("版本", master.VersionNo, dto.VersionNo) ], AdoS0QmsSpecAggregateGuard.CountMeaningful(existingRows), AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items).Count); if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { ApplyProcessInspectionSpec(master, dto); await _rep.AsUpdateable(master).ExecuteCommandAsync(); await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await SyncProcessInspectionSpecEntriesAsync(id, items, tenantId); await db.Ado.CommitTranAsync(); return await GetDetailAsync(id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpDelete("{id:long}")] public async Task DeleteAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var master = await _rep.ByIdScopedByColumnAsync(id, tenantId); if (master == null) return NotFound(); var db = _rep.Context; await db.Ado.BeginTranAsync(); try { await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync(); await _rep.DeleteAsync(master); await db.Ado.CommitTranAsync(); return Ok(new { message = "删除成功" }); } catch { await db.Ado.RollbackTranAsync(); throw; } } /// 过程检规必填集(UAT-S0-10 拍板;本轮不含「适用型号」,缺业务依据)。 private static IReadOnlyList<(string, string?)> ProcessRequiredFields(AdoS0QmsProcessInspectionSpecUpsertDto dto) => [ ("文件编号", dto.FileNumber), ("物料编码", dto.MaterialCode), ("版本", dto.VersionNo), ]; private static void ApplyProcessInspectionSpec(AdoS0QmsProcessInspectionSpec entity, AdoS0QmsProcessInspectionSpecUpsertDto dto) { entity.ApplicableModel = NullIfWhiteSpace(dto.ApplicableModel); entity.FileNumber = NullIfWhiteSpace(dto.FileNumber); entity.VersionNo = NullIfWhiteSpace(dto.VersionNo); entity.EffectiveDate = NullIfWhiteSpace(dto.EffectiveDate); entity.Attachment = NullIfWhiteSpace(dto.Attachment); entity.MaterialCode = NullIfWhiteSpace(dto.MaterialCode); entity.Attachment2 = NullIfWhiteSpace(dto.Attachment2); entity.Version = dto.Version; } private async Task SyncProcessInspectionSpecEntriesAsync(long masterId, IEnumerable items, long tenantId) { foreach (var item in items) { var entity = new AdoS0QmsProcessInspectionSpecEntry { TenantId = tenantId, MasterId = masterId, OperationCode = NullIfWhiteSpace(item.OperationCode), OperationName = NullIfWhiteSpace(item.OperationName), InspectionItem = NullIfWhiteSpace(item.InspectionItem), InspectionMethod = NullIfWhiteSpace(item.InspectionMethod), InspectionSpec = NullIfWhiteSpace(item.InspectionSpec), ImageCategory = NullIfWhiteSpace(item.ImageCategory), InspectionFrequency = NullIfWhiteSpace(item.InspectionFrequency), TechnicalStandard = NullIfWhiteSpace(item.TechnicalStandard), PeelingForce = item.PeelingForce, UpperLimit = NullIfWhiteSpace(item.UpperLimit), LowerLimit = NullIfWhiteSpace(item.LowerLimit) }; await _entryRep.AsInsertable(entity).ExecuteCommandAsync(); } } }