| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- using Admin.NET.Plugin.AiDOP.Dto.S0.Quality;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Quality;
- using Admin.NET.Plugin.AiDOP.FinishedWarehouse;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using static Admin.NET.Plugin.AiDOP.Controllers.S0.Quality.AdoS0QmsControllerHelpers;
- namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Quality;
- /// <summary>
- /// FQC(成品)检验规范维护 —— FUNC-S0-052。
- ///
- /// 【本轮变更:底层表切到 S7 运行链路】原实现写 `qms_fqcjygf` / `qms_fqcjygfzb`,该表**零下游消费者**,
- /// 维护出来的检规 S7 报检根本读不到。现改写 `qms_ccjygf` / `qms_ccjygfzb`——它才是
- /// `FqcApplyService` 生成检验单时经桥表解析并快照的表。见 `S7:D-002`(唯一事实源)。
- /// **路由、FUNC 权限码、菜单结构全部不变**,只换底层实体与字段集。
- ///
- /// 【写后必须同步桥表】`ado_s7_fqc_spec_material_map` 是「物料 → 检规」的唯一解析入口;
- /// 不同步则新建的检规对 S7 完全不可见。Create / Update / Delete 三条写路径都调
- /// `FqcSpecMaterialMapService.SyncSpecAsync`(单检规粒度,人工绑定行受保护)。
- ///
- /// 【租户】`qms_ccjygf(zb)` 实体不实现 `ITenantIdFilter`,全局过滤器不生效;
- /// 六条路径全部显式 `tenant_id` 谓词,**明细查询/删除也必须带 TenantId**——目标表存在
- /// 52 个 phantom glid / 444 行无主明细,只按 MasterId 过滤存在误删无主行的实际风险。
- /// </summary>
- [ApiController]
- [Route("api/s0/quality/fqc-inspection-specs")]
- [NonUnify]
- public class AdoS0QmsFqcInspectionSpecsController : ControllerBase
- {
- private readonly SqlSugarRepository<AdoS0QmsFinishedInspectionSpec> _rep;
- private readonly SqlSugarRepository<AdoS0QmsFinishedInspectionSpecEntry> _entryRep;
- private readonly FqcSpecMaterialMapService _specMap;
- public AdoS0QmsFqcInspectionSpecsController(
- SqlSugarRepository<AdoS0QmsFinishedInspectionSpec> rep,
- SqlSugarRepository<AdoS0QmsFinishedInspectionSpecEntry> entryRep,
- FqcSpecMaterialMapService specMap)
- {
- _rep = rep;
- _entryRep = entryRep;
- _specMap = specMap;
- }
- [HttpGet]
- public async Task<IActionResult> 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.ProductName != null && x.ProductName.Contains(q.Keyword!))
- || (x.ProductModel != null && x.ProductModel.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<IActionResult> 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 QueryEntriesAsync(id, tenantId);
- return Ok(new { master, items });
- }
- [HttpPost]
- public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsFinishedInspectionSpecUpsertDto dto)
- {
- if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
- var items = AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items);
- var guardError = AdoS0QmsSpecAggregateGuard.ValidateCreate(FqcRequiredFields(dto), items.Count)
- ?? AdoS0QmsSpecAggregateGuard.ValidateFinishedEntries(items);
- if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError);
- var db = _rep.Context;
- await db.Ado.BeginTranAsync();
- long newId;
- try
- {
- var master = new AdoS0QmsFinishedInspectionSpec();
- ApplyFqcSpec(master, dto);
- master.TenantId = tenantId;
- await _rep.AsInsertable(master).ExecuteReturnEntityAsync();
- await SyncFqcEntriesAsync(master.Id, items, tenantId);
- await db.Ado.CommitTranAsync();
- newId = master.Id;
- }
- catch
- {
- await db.Ado.RollbackTranAsync();
- throw;
- }
- // 桥表同步放在主事务之外:同步失败不应回滚已保存的检规(用户可在维护页重试),
- // 但必须在返回前完成,否则前端拿到「保存成功」而 S7 仍解析不到。
- await _specMap.SyncSpecAsync(tenantId, newId);
- return await GetDetailAsync(newId);
- }
- [HttpPut("{id:long}")]
- public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsFinishedInspectionSpecUpsertDto 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 QueryEntriesAsync(id, tenantId);
- 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)
- ?? AdoS0QmsSpecAggregateGuard.ValidateFinishedEntries(items);
- if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError);
- var db = _rep.Context;
- await db.Ado.BeginTranAsync();
- try
- {
- ApplyFqcSpec(master, dto);
- await _rep.AsUpdateable(master).ExecuteCommandAsync();
- await DeleteEntriesAsync(id, tenantId);
- await SyncFqcEntriesAsync(id, items, tenantId);
- await db.Ado.CommitTranAsync();
- }
- catch
- {
- await db.Ado.RollbackTranAsync();
- throw;
- }
- // wlbm / sxrq / bb 任一变更都会改变解析结果与生效日期排序,一律重同步(判断成本高于直接跑)
- await _specMap.SyncSpecAsync(tenantId, id);
- return await GetDetailAsync(id);
- }
- /// <summary>
- /// 删除检规。**已被检验单引用则拒绝**——`qms_qcpp_inspbill.jygfid` 是 varchar 无 FK,
- /// 物理删除会静默产生悬空指针、断掉质量记录的受控文件追溯链。
- /// </summary>
- [HttpDelete("{id:long}")]
- public async Task<IActionResult> 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 referenced = await _rep.Context.Ado.GetIntAsync(
- "SELECT COUNT(1) FROM qms_qcpp_inspbill WHERE tenant_id=@t AND jygfid=@j",
- new SugarParameter("@t", tenantId), new SugarParameter("@j", id.ToString()));
- if (referenced > 0)
- return AdoS0ApiErrors.InvalidRequest($"该检规已被 {referenced} 张检验单引用,不能删除");
- var db = _rep.Context;
- await db.Ado.BeginTranAsync();
- try
- {
- await DeleteEntriesAsync(id, tenantId);
- await _rep.DeleteAsync(master);
- await db.Ado.CommitTranAsync();
- }
- catch
- {
- await db.Ado.RollbackTranAsync();
- throw;
- }
- // 主表已不存在 → SyncSpec 只清自动映射行;人工绑定行保留待人工处置(不静默删人工痕迹)
- await _specMap.SyncSpecAsync(tenantId, id);
- return Ok(new { message = "删除成功" });
- }
- // ==================== 内部 ====================
- /// <summary>明细查询:MasterId + TenantId 双条件(目标表存在无主明细,不能只按 MasterId)。</summary>
- private Task<List<AdoS0QmsFinishedInspectionSpecEntry>> QueryEntriesAsync(long masterId, long tenantId)
- => _entryRep.AsQueryable()
- .Where(x => x.MasterId == masterId && x.TenantId == tenantId)
- .OrderBy(x => x.SeqNo).OrderBy(x => x.Id).ToListAsync();
- /// <summary>明细删除:同上,必须带 TenantId。</summary>
- private Task<int> DeleteEntriesAsync(long masterId, long tenantId)
- => _entryRep.AsDeleteable()
- .Where(x => x.MasterId == masterId && x.TenantId == tenantId).ExecuteCommandAsync();
- /// <summary>FQC 检规必填集(UAT-S0-10 拍板;本轮不含「适用型号」,缺业务依据)。</summary>
- private static IReadOnlyList<(string, string?)> FqcRequiredFields(AdoS0QmsFinishedInspectionSpecUpsertDto dto) =>
- [
- ("文件编号", dto.FileNumber),
- ("物料编码", dto.MaterialCode),
- ("版本", dto.VersionNo),
- ];
- private static void ApplyFqcSpec(AdoS0QmsFinishedInspectionSpec entity, AdoS0QmsFinishedInspectionSpecUpsertDto dto)
- {
- entity.FileNumber = NullIfWhiteSpace(dto.FileNumber);
- entity.VersionNo = NullIfWhiteSpace(dto.VersionNo);
- entity.ProductName = NullIfWhiteSpace(dto.ProductName);
- entity.ProductModel = NullIfWhiteSpace(dto.ProductModel);
- entity.EffectiveDate = NullIfWhiteSpace(dto.EffectiveDate);
- entity.MaterialCode = NullIfWhiteSpace(dto.MaterialCode);
- entity.Attachment = NullIfWhiteSpace(dto.Attachment);
- }
- private async Task SyncFqcEntriesAsync(long masterId, IEnumerable<AdoS0QmsFinishedInspectionSpecEntryDto> items, long tenantId)
- {
- foreach (var item in items)
- {
- var entity = new AdoS0QmsFinishedInspectionSpecEntry
- {
- TenantId = tenantId,
- MasterId = masterId,
- SeqNo = NullIfWhiteSpace(item.SeqNo),
- InspectionItem = NullIfWhiteSpace(item.InspectionItem),
- ResultType = NormalizeResultType(item.ResultType),
- UpperLimit = NullIfWhiteSpace(item.UpperLimit),
- LowerLimit = NullIfWhiteSpace(item.LowerLimit),
- TechnicalRequirement = NullIfWhiteSpace(item.TechnicalRequirement),
- InspectionMethod = NullIfWhiteSpace(item.InspectionMethod),
- Instrument = NullIfWhiteSpace(item.Instrument),
- InspectionClause = NullIfWhiteSpace(item.InspectionClause),
- TechnicalStandard = NullIfWhiteSpace(item.TechnicalStandard),
- SamplingPlan = NullIfWhiteSpace(item.SamplingPlan),
- Record = NullIfWhiteSpace(item.Record),
- SizeSpecifications = NullIfWhiteSpace(item.SizeSpecifications),
- Remarks = NullIfWhiteSpace(item.Remarks)
- };
- // 非数值型不承载上下限:避免脏残留在生成检验单时被当成数值判定条件快照出去
- if (entity.ResultType == "NON_NUMERIC") { entity.UpperLimit = null; entity.LowerLimit = null; }
- await _entryRep.AsInsertable(entity).ExecuteCommandAsync();
- }
- }
- /// <summary>录入类型归一:大小写不敏感,空/未知一律落 NULL(=未设置,运行期退化为人工判定)。</summary>
- internal static string? NormalizeResultType(string? value)
- {
- var v = (value ?? string.Empty).Trim().ToUpperInvariant();
- return v is "NUMERIC" or "NON_NUMERIC" ? v : null;
- }
- }
- [ApiController]
- [Route("api/s0/quality/oqc-inspection-specs")]
- [NonUnify]
- public class AdoS0QmsOqcInspectionSpecsController : ControllerBase
- {
- private readonly SqlSugarRepository<AdoS0QmsOqcInspectionSpec> _rep;
- private readonly SqlSugarRepository<AdoS0QmsOqcInspectionSpecEntry> _entryRep;
- public AdoS0QmsOqcInspectionSpecsController(
- SqlSugarRepository<AdoS0QmsOqcInspectionSpec> rep,
- SqlSugarRepository<AdoS0QmsOqcInspectionSpecEntry> entryRep)
- {
- _rep = rep;
- _entryRep = entryRep;
- }
- [HttpGet]
- public async Task<IActionResult> 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<IActionResult> 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<IActionResult> CreateAsync([FromBody] AdoS0QmsOqcInspectionSpecUpsertDto dto)
- {
- if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
- var items = AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items);
- var guardError = AdoS0QmsSpecAggregateGuard.ValidateCreate(OqcRequiredFields(dto), items.Count);
- if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError);
- var db = _rep.Context;
- await db.Ado.BeginTranAsync();
- try
- {
- var master = new AdoS0QmsOqcInspectionSpec();
- ApplyOqcSpec(master, dto);
- master.TenantId = tenantId;
- await _rep.AsInsertable(master).ExecuteReturnEntityAsync();
- await SyncOqcEntriesAsync(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<IActionResult> UpdateAsync(long id, [FromBody] AdoS0QmsOqcInspectionSpecUpsertDto 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
- {
- ApplyOqcSpec(master, dto);
- await _rep.AsUpdateable(master).ExecuteCommandAsync();
- await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
- await SyncOqcEntriesAsync(id, items, tenantId);
- await db.Ado.CommitTranAsync();
- return await GetDetailAsync(id);
- }
- catch
- {
- await db.Ado.RollbackTranAsync();
- throw;
- }
- }
- [HttpDelete("{id:long}")]
- public async Task<IActionResult> 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;
- }
- }
- /// <summary>OQC 检规必填集(UAT-S0-10 拍板;本轮不含「适用型号」,缺业务依据)。</summary>
- private static IReadOnlyList<(string, string?)> OqcRequiredFields(AdoS0QmsOqcInspectionSpecUpsertDto dto) =>
- [
- ("文件编号", dto.FileNumber),
- ("物料编码", dto.MaterialCode),
- ("版本", dto.VersionNo),
- ];
- private static void ApplyOqcSpec(AdoS0QmsOqcInspectionSpec entity, AdoS0QmsOqcInspectionSpecUpsertDto 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 SyncOqcEntriesAsync(long masterId, IEnumerable<AdoS0QmsOqcInspectionSpecEntryDto> items, long tenantId)
- {
- foreach (var item in items)
- {
- var entity = new AdoS0QmsOqcInspectionSpecEntry
- {
- 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();
- }
- }
- }
|