|
|
@@ -1,5 +1,7 @@
|
|
|
using System.ComponentModel;
|
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
+using Admin.NET.Plugin.ApprovalFlow;
|
|
|
+using Admin.NET.Plugin.ApprovalFlow.Service;
|
|
|
|
|
|
namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
|
|
|
|
|
|
@@ -17,12 +19,14 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
|
|
|
private readonly ISqlSugarClient _db;
|
|
|
private readonly UserManager _userManager;
|
|
|
private readonly FqcApplyService _fqcApply;
|
|
|
+ private readonly FlowEngineService _flowEngine;
|
|
|
|
|
|
- public FqcTaskEntryService(ISqlSugarClient db, UserManager userManager, FqcApplyService fqcApply)
|
|
|
+ public FqcTaskEntryService(ISqlSugarClient db, UserManager userManager, FqcApplyService fqcApply, FlowEngineService flowEngine)
|
|
|
{
|
|
|
_db = db;
|
|
|
_userManager = userManager;
|
|
|
_fqcApply = fqcApply;
|
|
|
+ _flowEngine = flowEngine;
|
|
|
}
|
|
|
|
|
|
private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
@@ -151,6 +155,7 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
|
|
|
{
|
|
|
var ids = NormalizeIds(input?.Ids);
|
|
|
var tenantId = ResolveTenantOrThrow();
|
|
|
+ await EnsureNotLockedAsync(ids, tenantId); // G4:起流后冻结负责人
|
|
|
var owner = _userManager.UserId.ToString();
|
|
|
var n = await _db.Ado.ExecuteCommandAsync(
|
|
|
$"UPDATE qms_fqcbj SET jyfzr = @Owner WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})",
|
|
|
@@ -167,6 +172,7 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
|
|
|
var ids = NormalizeIds(input?.Ids);
|
|
|
if (input == null || input.InspectorUserId <= 0) throw Oops.Oh("请指定检验负责人");
|
|
|
var tenantId = ResolveTenantOrThrow();
|
|
|
+ await EnsureNotLockedAsync(ids, tenantId); // G4:起流后冻结负责人
|
|
|
var owner = input.InspectorUserId.ToString();
|
|
|
var n = await _db.Ado.ExecuteCommandAsync(
|
|
|
$"UPDATE qms_fqcbj SET jyfzr = @Owner WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})",
|
|
|
@@ -225,9 +231,45 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
|
|
|
"SELECT id AS Id, FBILLNO AS BillNo FROM qms_qcpp_inspbill WHERE lydjbh=@no AND hid=@Id AND tenant_id=@t LIMIT 1",
|
|
|
new SugarParameter("@no", apply.BillNo), new SugarParameter("@Id", input.EntryId), new SugarParameter("@t", tenantId));
|
|
|
if (bill == null) throw Oops.Oh("检验单生成后未能回读,请刷新重试");
|
|
|
+
|
|
|
+ // EnsureFlow:建单后幂等起流(无实例则起、有则复用)。检验单已 DB 幂等,Generate 可安全重调;
|
|
|
+ // 即便"建单成功、起流失败",重调 Generate 会走到这里再次补流 → 无永久孤儿单。起流失败向上抛,暴露真实原因。
|
|
|
+ await EnsureFlowAsync(bill.Id, bill.BillNo);
|
|
|
+
|
|
|
return new FqcTaskGenerateOutput { InspBillId = bill.Id, InspBillNo = bill.BillNo ?? "" };
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 幂等起流:BizType+BizId 已有实例则复用,无则 StartFlow。发起人=当前用户(认领门保证==jyfzr),
|
|
|
+ /// N1(ApproverType=Initiator) 待办自然落发起人=认领人。幂等等级 B(代码级 check-then-start),
|
|
|
+ /// 与 submit-result 的 inst?.Id ?? StartFlow 同范式;ApprovalFlow 核心(BizType,BizId)无 DB 唯一约束,
|
|
|
+ /// 极端并发 race 仅登记风险(本批不改核心表)。
|
|
|
+ /// </summary>
|
|
|
+ private async Task EnsureFlowAsync(long inspBillId, string? billNo)
|
|
|
+ {
|
|
|
+ var hasInstance = await _db.Queryable<ApprovalFlowInstance>()
|
|
|
+ .ClearFilter()
|
|
|
+ .Where(x => x.BizType == FqcInspBillFlowConst.BizType && x.BizId == inspBillId)
|
|
|
+ .AnyAsync();
|
|
|
+ if (hasInstance) return; // 复用已有实例,不重复起流
|
|
|
+
|
|
|
+ // StartFlow 引擎内非原子(Instance Insert 在前、N1 Task Insert 在后,无内部事务)→ 裸调可能留下
|
|
|
+ // "实例有、N1 无"的半启动、永久卡死。用事务包裹(镜像 FqcInspBillFlowService.SubmitResult 的既有范式),
|
|
|
+ // 保证"实例 + N1 任务"要么全成、要么全回滚:N1 建失败 → 实例回滚 → 退化为可恢复 Scenario B(Bill 有 Flow 无),
|
|
|
+ // 由下一次 Generate 经本方法补起。失败明确上抛(不吞错、不在本次请求重试 StartFlow)。
|
|
|
+ var tran = await _db.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ await _flowEngine.StartFlow(new StartFlowInput
|
|
|
+ {
|
|
|
+ BizType = FqcInspBillFlowConst.BizType,
|
|
|
+ BizId = inspBillId,
|
|
|
+ BizNo = billNo,
|
|
|
+ Title = $"FQC检验单 {billNo}",
|
|
|
+ });
|
|
|
+ });
|
|
|
+ if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("FQC 检验单工作流启动失败");
|
|
|
+ }
|
|
|
+
|
|
|
private static List<long> NormalizeIds(List<long>? ids)
|
|
|
{
|
|
|
var list = (ids ?? new List<long>()).Where(x => x > 0).Distinct().ToList();
|
|
|
@@ -236,6 +278,20 @@ public class FqcTaskEntryService : IDynamicApiController, ITransient
|
|
|
return list;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// G4 负责人冻结:选中报检若有任一已生成检验单(=已起流),禁止再改 jyfzr(Claim/Assign),
|
|
|
+ /// 防止 jyfzr 与 N1 待办 Assignee 分叉产生孤儿。以检验单存在性判定(生成即起流,二者等价)。
|
|
|
+ /// </summary>
|
|
|
+ private async Task EnsureNotLockedAsync(List<long> ids, long tenantId)
|
|
|
+ {
|
|
|
+ var locked = await _db.Ado.GetIntAsync(
|
|
|
+ $@"SELECT COUNT(1) FROM qms_qcpp_inspbill b
|
|
|
+ JOIN qms_fqcbj q ON q.FBILLNO=b.lydjbh AND q.id=b.hid AND q.tenant_id=b.tenant_id
|
|
|
+ WHERE q.tenant_id=@t AND q.id IN ({string.Join(",", ids)})",
|
|
|
+ new SugarParameter("@t", tenantId));
|
|
|
+ if (locked > 0) throw Oops.Oh("检验单已生成,检验负责人已锁定,不允许重新认领或调配。");
|
|
|
+ }
|
|
|
+
|
|
|
private sealed class FqcTaskApplyRow
|
|
|
{
|
|
|
public long Id { get; set; }
|