|
|
@@ -159,6 +159,14 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
var (parentItem, componentItem, itemErr) = await ResolveItemNumsAsync(dto.ParentMaterialId, dto.ComponentMaterialId, tenantId);
|
|
|
if (itemErr != null) return BadRequest(new { message = itemErr });
|
|
|
|
|
|
+ // DATA LOSS GUARD(见下方 SyncOpsAsync 注释):
|
|
|
+ // 当前 op 子表契约只携带 Op 一个字段,无法构造 ProductItem / Domain / ParentItem /
|
|
|
+ // ComponentItem / IsActive / IsConfirm / CreateUser 这 7 列,创建出来的会是稀疏行。
|
|
|
+ // 在完整子表契约落地前,禁止创建带工序明细的标准 BOM;OpNos 为空则照常创建主表。
|
|
|
+ if (NormalizeOps(dto.OpNos).Count > 0)
|
|
|
+ return AdoS0ApiErrors.InvalidRequest(
|
|
|
+ "当前页面的工序编号字段无法完整描述工序明细(缺少成品项等必填业务列),暂不支持新建时录入工序明细。请先创建标准 BOM 主记录。");
|
|
|
+
|
|
|
var db = _masterRep.Context;
|
|
|
await db.Ado.BeginTranAsync();
|
|
|
try
|
|
|
@@ -197,6 +205,30 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
var (parentItem, componentItem, itemErr) = await ResolveItemNumsAsync(dto.ParentMaterialId, dto.ComponentMaterialId, tenantId);
|
|
|
if (itemErr != null) return BadRequest(new { message = itemErr });
|
|
|
|
|
|
+ // ── DATA LOSS GUARD ──────────────────────────────────────────────────────
|
|
|
+ // 历史工序明细每行携带 ProductItem 等 7 列业务数据,而当前 DTO 只有 OpNos。
|
|
|
+ // 实测:520 个 master 中 96 个各有 4 行、行内 Op 完全相同、仅 ProductItem 不同;
|
|
|
+ // 原实现「删光子行 + 按 Distinct(OpNos) 重建」会把 808 行压成 520 行(净丢 288 行),
|
|
|
+ // 且重建行的 7 列全部为空。
|
|
|
+ //
|
|
|
+ // 在完整子表契约落地前:
|
|
|
+ // 提交的 Op 多重集合 == 现有子行的 Op 多重集合 → 跳过子表同步(主表照常更新)
|
|
|
+ // 不相同 → 拒绝保存,绝不 delete/insert
|
|
|
+ // 必须按【多重集合】比较:existing [303,303,303,303] vs submitted [303] 是「不同」,
|
|
|
+ // 不能因为去重后都是 [303] 就判为相同。
|
|
|
+ var existingOps = await _opRep.ScopedTo(tenantId)
|
|
|
+ .Where(o => o.ProductStructureMasterId == id)
|
|
|
+ .Select(o => o.Op)
|
|
|
+ .ToListAsync();
|
|
|
+ var submittedOps = NormalizeOps(dto.OpNos);
|
|
|
+ if (!existingOps.OrderBy(n => n).SequenceEqual(submittedOps.OrderBy(n => n)))
|
|
|
+ {
|
|
|
+ return AdoS0ApiErrors.InvalidRequest(
|
|
|
+ existingOps.Count > 0
|
|
|
+ ? "该标准 BOM 含历史工序明细,当前页面的工序编号字段无法完整描述这些明细(缺少成品项等业务列),暂不支持修改工序集合。工序编号以外的字段可正常保存。"
|
|
|
+ : "当前页面的工序编号字段无法完整描述工序明细(缺少成品项等必填业务列),暂不支持新增工序明细。");
|
|
|
+ }
|
|
|
+
|
|
|
var db = _masterRep.Context;
|
|
|
await db.Ado.BeginTranAsync();
|
|
|
try
|
|
|
@@ -207,8 +239,7 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
existing.UpdatedAt = DateTime.Now;
|
|
|
await _masterRep.AsUpdateable(existing).ExecuteCommandAsync();
|
|
|
|
|
|
- await _opRep.AsDeleteable().Where(o => o.ProductStructureMasterId == id).ExecuteCommandAsync();
|
|
|
- await SyncOpsAsync(id, dto, existing.ParentMaterialId, existing.ComponentMaterialId, tenantId);
|
|
|
+ // 工序集合与库中完全一致 → 不触碰子表(这正是本 guard 的全部作用)
|
|
|
|
|
|
await db.Ado.CommitTranAsync();
|
|
|
return await GetDetailAsync(id);
|
|
|
@@ -281,6 +312,13 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
m.IsEnabled = dto.IsEnabled;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 归一化提交的工序号:过滤非正数,**保留重复项**(多重集合语义)。
|
|
|
+ /// 不做 Distinct —— 历史数据里同一 master 下多行共享同一个 Op 是合法形态。
|
|
|
+ /// </summary>
|
|
|
+ private static List<int> NormalizeOps(IEnumerable<int>? ops) =>
|
|
|
+ ops?.Where(n => n > 0).ToList() ?? new List<int>();
|
|
|
+
|
|
|
private async Task SyncOpsAsync(long masterId, AdoS0ProductStructureUpsertDto dto, long parentMaterialId, long componentMaterialId, long tenantId)
|
|
|
{
|
|
|
var distinct = dto.OpNos?.Where(n => n > 0).Distinct().OrderBy(n => n).ToList() ?? new List<int>();
|