|
|
@@ -1,24 +1,44 @@
|
|
|
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<AdoS0QmsFqcInspectionSpec> _rep;
|
|
|
- private readonly SqlSugarRepository<AdoS0QmsFqcInspectionSpecEntry> _entryRep;
|
|
|
+ private readonly SqlSugarRepository<AdoS0QmsFinishedInspectionSpec> _rep;
|
|
|
+ private readonly SqlSugarRepository<AdoS0QmsFinishedInspectionSpecEntry> _entryRep;
|
|
|
+ private readonly FqcSpecMaterialMapService _specMap;
|
|
|
|
|
|
public AdoS0QmsFqcInspectionSpecsController(
|
|
|
- SqlSugarRepository<AdoS0QmsFqcInspectionSpec> rep,
|
|
|
- SqlSugarRepository<AdoS0QmsFqcInspectionSpecEntry> entryRep)
|
|
|
+ SqlSugarRepository<AdoS0QmsFinishedInspectionSpec> rep,
|
|
|
+ SqlSugarRepository<AdoS0QmsFinishedInspectionSpecEntry> entryRep,
|
|
|
+ FqcSpecMaterialMapService specMap)
|
|
|
{
|
|
|
_rep = rep;
|
|
|
_entryRep = entryRep;
|
|
|
+ _specMap = specMap;
|
|
|
}
|
|
|
|
|
|
[HttpGet]
|
|
|
@@ -29,7 +49,8 @@ public class AdoS0QmsFqcInspectionSpecsController : ControllerBase
|
|
|
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.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();
|
|
|
@@ -42,45 +63,51 @@ public class AdoS0QmsFqcInspectionSpecsController : ControllerBase
|
|
|
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();
|
|
|
+ var items = await QueryEntriesAsync(id, tenantId);
|
|
|
return Ok(new { master, items });
|
|
|
}
|
|
|
|
|
|
[HttpPost]
|
|
|
- public async Task<IActionResult> CreateAsync([FromBody] AdoS0QmsFqcInspectionSpecUpsertDto dto)
|
|
|
+ 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);
|
|
|
+ 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 AdoS0QmsFqcInspectionSpec();
|
|
|
+ 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();
|
|
|
- return await GetDetailAsync(master.Id);
|
|
|
+ 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] AdoS0QmsFqcInspectionSpecUpsertDto dto)
|
|
|
+ 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 _entryRep.AsQueryable().Where(x => x.MasterId == id).ToListAsync();
|
|
|
+ var existingRows = await QueryEntriesAsync(id, tenantId);
|
|
|
var items = AdoS0QmsSpecAggregateGuard.ForUpdateWrite(dto.Items);
|
|
|
var guardError = AdoS0QmsSpecAggregateGuard.ValidateUpdate(
|
|
|
[
|
|
|
@@ -89,7 +116,8 @@ public class AdoS0QmsFqcInspectionSpecsController : ControllerBase
|
|
|
("版本", master.VersionNo, dto.VersionNo)
|
|
|
],
|
|
|
AdoS0QmsSpecAggregateGuard.CountMeaningful(existingRows),
|
|
|
- AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items).Count);
|
|
|
+ AdoS0QmsSpecAggregateGuard.Meaningful(dto.Items).Count)
|
|
|
+ ?? AdoS0QmsSpecAggregateGuard.ValidateFinishedEntries(items);
|
|
|
if (guardError != null) return AdoS0ApiErrors.InvalidRequest(guardError);
|
|
|
var db = _rep.Context;
|
|
|
await db.Ado.BeginTranAsync();
|
|
|
@@ -97,84 +125,122 @@ public class AdoS0QmsFqcInspectionSpecsController : ControllerBase
|
|
|
{
|
|
|
ApplyFqcSpec(master, dto);
|
|
|
await _rep.AsUpdateable(master).ExecuteCommandAsync();
|
|
|
- await _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
|
|
|
+ await DeleteEntriesAsync(id, tenantId);
|
|
|
await SyncFqcEntriesAsync(id, items, tenantId);
|
|
|
await db.Ado.CommitTranAsync();
|
|
|
- return await GetDetailAsync(id);
|
|
|
}
|
|
|
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 _entryRep.AsDeleteable().Where(x => x.MasterId == id).ExecuteCommandAsync();
|
|
|
+ await DeleteEntriesAsync(id, tenantId);
|
|
|
await _rep.DeleteAsync(master);
|
|
|
await db.Ado.CommitTranAsync();
|
|
|
- return Ok(new { message = "删除成功" });
|
|
|
}
|
|
|
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(AdoS0QmsFqcInspectionSpecUpsertDto dto) =>
|
|
|
+ private static IReadOnlyList<(string, string?)> FqcRequiredFields(AdoS0QmsFinishedInspectionSpecUpsertDto dto) =>
|
|
|
[
|
|
|
("文件编号", dto.FileNumber),
|
|
|
("物料编码", dto.MaterialCode),
|
|
|
("版本", dto.VersionNo),
|
|
|
];
|
|
|
|
|
|
-
|
|
|
- private static void ApplyFqcSpec(AdoS0QmsFqcInspectionSpec entity, AdoS0QmsFqcInspectionSpecUpsertDto dto)
|
|
|
+ private static void ApplyFqcSpec(AdoS0QmsFinishedInspectionSpec entity, AdoS0QmsFinishedInspectionSpecUpsertDto dto)
|
|
|
{
|
|
|
- entity.ApplicableModel = NullIfWhiteSpace(dto.ApplicableModel);
|
|
|
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.Attachment = NullIfWhiteSpace(dto.Attachment);
|
|
|
entity.MaterialCode = NullIfWhiteSpace(dto.MaterialCode);
|
|
|
- entity.Attachment2 = NullIfWhiteSpace(dto.Attachment2);
|
|
|
- entity.Version = dto.Version;
|
|
|
+ entity.Attachment = NullIfWhiteSpace(dto.Attachment);
|
|
|
}
|
|
|
|
|
|
- private async Task SyncFqcEntriesAsync(long masterId, IEnumerable<AdoS0QmsFqcInspectionSpecEntryDto> items, long tenantId)
|
|
|
+ private async Task SyncFqcEntriesAsync(long masterId, IEnumerable<AdoS0QmsFinishedInspectionSpecEntryDto> items, long tenantId)
|
|
|
{
|
|
|
foreach (var item in items)
|
|
|
{
|
|
|
- var entity = new AdoS0QmsFqcInspectionSpecEntry
|
|
|
+ var entity = new AdoS0QmsFinishedInspectionSpecEntry
|
|
|
{
|
|
|
TenantId = tenantId,
|
|
|
MasterId = masterId,
|
|
|
- OperationCode = NullIfWhiteSpace(item.OperationCode),
|
|
|
- OperationName = NullIfWhiteSpace(item.OperationName),
|
|
|
+ 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),
|
|
|
- InspectionSpec = NullIfWhiteSpace(item.InspectionSpec),
|
|
|
- ImageCategory = NullIfWhiteSpace(item.ImageCategory),
|
|
|
- InspectionFrequency = NullIfWhiteSpace(item.InspectionFrequency),
|
|
|
+ Instrument = NullIfWhiteSpace(item.Instrument),
|
|
|
+ InspectionClause = NullIfWhiteSpace(item.InspectionClause),
|
|
|
TechnicalStandard = NullIfWhiteSpace(item.TechnicalStandard),
|
|
|
- PeelingForce = item.PeelingForce,
|
|
|
- UpperLimit = NullIfWhiteSpace(item.UpperLimit),
|
|
|
- LowerLimit = NullIfWhiteSpace(item.LowerLimit)
|
|
|
+ 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]
|