|
|
@@ -0,0 +1,309 @@
|
|
|
+using Admin.NET.Plugin.AiDOP.Entity;
|
|
|
+using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
|
|
|
+using Admin.NET.Plugin.ApprovalFlow;
|
|
|
+using Admin.NET.Plugin.ApprovalFlow.Service;
|
|
|
+using Yitter.IdGenerator;
|
|
|
+
|
|
|
+namespace Admin.NET.Plugin.AiDOP.Manufacturing;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// S6 Phase 1D 过程检验单(Phase 1C bill)审核流程服务(桥接:Phase 1C Complete → ApprovalFlow)。
|
|
|
+///
|
|
|
+/// business_id = ado_s6_process_inspection_bill.id(BizType=S6_PROCESS_INSPECTION)。复用 ApprovalFlow 引擎,不改核心。
|
|
|
+/// 提交门槛:结果 status=COMPLETED 才能提交审核;幂等:一单最多一个 active flow。
|
|
|
+/// N1_SUBMIT=发起人本人(提交即自动通过)→ N2_SUP_REVIEW 主管审核 → GW_RESULT(overall_result=FAIL→N3 / PASS→end)
|
|
|
+/// → N3_QE_DISPOSITION 质量工程师处置 → end。质量结果 overall_result 全程只读、审批不改写(§20 硬门)。
|
|
|
+/// 处置落 ado_s6_ipqc_quality_disposition(biz_type 区分,复用不建重复表)。严格租户隔离(ResolveTenantOrThrow)。
|
|
|
+/// </summary>
|
|
|
+[ApiDescriptionSettings(Order = 305, Description = "过程检验单审核")]
|
|
|
+[Route("api/S6ProcessInspectionReview")]
|
|
|
+[NonUnify]
|
|
|
+public class S6ProcessInspectionReviewService : IDynamicApiController, ITransient
|
|
|
+{
|
|
|
+ private const long MainTenantId = 1300000000001L;
|
|
|
+
|
|
|
+ private readonly ISqlSugarClient _db;
|
|
|
+ private readonly FlowEngineService _flowEngine;
|
|
|
+ private readonly UserManager _userManager;
|
|
|
+
|
|
|
+ public S6ProcessInspectionReviewService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
|
|
|
+ {
|
|
|
+ _db = db;
|
|
|
+ _flowEngine = flowEngine;
|
|
|
+ _userManager = userManager;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long ResolveTenantOrThrow()
|
|
|
+ {
|
|
|
+ var tid = _userManager.TenantId;
|
|
|
+ if (tid <= 0) throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
|
|
|
+ if (_userManager.SuperAdmin && tid == MainTenantId) throw Oops.Oh("超级管理员操作前必须选择目标租户");
|
|
|
+ return tid;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>审核流状态(只读,供前端按钮控制)。</summary>
|
|
|
+ [DisplayName("过程检验单审核状态")]
|
|
|
+ [HttpGet("state")]
|
|
|
+ public async Task<S6ReviewStateOutput> GetState([FromQuery] long billId)
|
|
|
+ {
|
|
|
+ await LoadBillAsync(billId);
|
|
|
+ return await BuildStateAsync(billId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>提交审核(仅 COMPLETED 可提交;幂等;发起流程并自动通过 N1→N2)。</summary>
|
|
|
+ [DisplayName("提交审核")]
|
|
|
+ [HttpPost("submit-review")]
|
|
|
+ public async Task<S6ReviewStateOutput> SubmitReview([FromBody] S6SubmitReviewInput input)
|
|
|
+ {
|
|
|
+ var bill = await LoadBillAsync(input.BillId);
|
|
|
+ if (!string.Equals(bill.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase))
|
|
|
+ throw Oops.Oh("INSPECTION_NOT_COMPLETED:检验未完成,无法提交审核");
|
|
|
+
|
|
|
+ var inst = await GetLatestInstanceAsync(input.BillId);
|
|
|
+ if (inst != null && inst.Status == FlowInstanceStatusEnum.Running)
|
|
|
+ throw Oops.Oh("该检验单已在审核中,请勿重复提交");
|
|
|
+ if (inst != null && inst.Status != FlowInstanceStatusEnum.Running && inst.Status != FlowInstanceStatusEnum.Rejected && inst.Status != FlowInstanceStatusEnum.Cancelled)
|
|
|
+ throw Oops.Oh("该检验单审核流程已结束");
|
|
|
+
|
|
|
+ var tran = await _db.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ var instanceId = await _flowEngine.StartFlow(new StartFlowInput
|
|
|
+ {
|
|
|
+ BizType = S6ProcessInspectionFlowConst.BizType,
|
|
|
+ BizId = input.BillId,
|
|
|
+ BizNo = bill.WorkOrderNo,
|
|
|
+ Title = $"过程检验单 {bill.WorkOrderNo}",
|
|
|
+ Comment = input.Comment,
|
|
|
+ });
|
|
|
+ // N1_SUBMIT=发起人本人,取自己的 N1 待办并自动通过 → N2
|
|
|
+ var myTask = await GetMyPendingTaskAsync(instanceId);
|
|
|
+ if (myTask == null || myTask.NodeId != S6ProcessInspectionFlowConst.NodeSubmit)
|
|
|
+ throw Oops.Oh("提交审核节点异常,未取到发起人任务");
|
|
|
+ await _flowEngine.Approve(myTask.Id, input.Comment);
|
|
|
+ });
|
|
|
+ if (!tran.IsSuccess) throw tran.ErrorException;
|
|
|
+
|
|
|
+ return await BuildStateAsync(input.BillId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>主管审核通过(N2):Approve→GW_RESULT,按 overall_result 自动分流(FAIL→N3 / PASS→end)。不改写质量结果。</summary>
|
|
|
+ [DisplayName("主管审核通过")]
|
|
|
+ [HttpPost("supervisor-approve")]
|
|
|
+ public async Task<S6ReviewStateOutput> SupervisorApprove([FromBody] S6ReviewApproveInput input)
|
|
|
+ {
|
|
|
+ await LoadBillAsync(input.BillId);
|
|
|
+ var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
|
|
|
+ var tran = await _db.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ await _flowEngine.Approve(myTask.Id, input.Comment);
|
|
|
+ });
|
|
|
+ if (!tran.IsSuccess) throw tran.ErrorException;
|
|
|
+ return await BuildStateAsync(input.BillId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>主管退回(N2→N1,意见必填)。质量结果不改写。</summary>
|
|
|
+ [DisplayName("主管退回")]
|
|
|
+ [HttpPost("supervisor-return")]
|
|
|
+ public async Task<S6ReviewStateOutput> SupervisorReturn([FromBody] S6ReviewReturnInput input)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(input.Comment)) throw Oops.Oh("退回必须填写意见");
|
|
|
+ await LoadBillAsync(input.BillId);
|
|
|
+ var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
|
|
|
+ var tran = await _db.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ await _flowEngine.ReturnToPrev(myTask.Id, input.Comment);
|
|
|
+ });
|
|
|
+ if (!tran.IsSuccess) throw tran.ErrorException;
|
|
|
+ return await BuildStateAsync(input.BillId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>QE 质量处置(N3):落处置记录(不改写 overall_result),Approve→end。一单一有效处置。</summary>
|
|
|
+ [DisplayName("质量处置")]
|
|
|
+ [HttpPost("qe-disposition")]
|
|
|
+ public async Task<S6ReviewStateOutput> QeDisposition([FromBody] S6QeDispositionInput input)
|
|
|
+ {
|
|
|
+ var bill = await LoadBillAsync(input.BillId);
|
|
|
+ var type = (input.DispositionType ?? "").Trim().ToUpperInvariant();
|
|
|
+ if (type != S6ProcessInspectionFlowConst.DispTypeRework && type != S6ProcessInspectionFlowConst.DispTypeConcession
|
|
|
+ && type != S6ProcessInspectionFlowConst.DispTypeScrap && type != S6ProcessInspectionFlowConst.DispTypeOther)
|
|
|
+ throw Oops.Oh("处置类型非法(REWORK/CONCESSION/SCRAP/OTHER)");
|
|
|
+ if (string.IsNullOrWhiteSpace(input.DispositionOpinion)) throw Oops.Oh("处置意见必填");
|
|
|
+
|
|
|
+ var inst = await GetLatestInstanceAsync(input.BillId);
|
|
|
+ if (inst == null || inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
|
|
|
+ var myTask = await GetMyPendingTaskAsync(inst.Id);
|
|
|
+ if (myTask == null) throw Oops.Oh("当前用户没有待处理的处置任务(需质量工程师角色成员)");
|
|
|
+ if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeDisposition) throw Oops.Oh("当前不在质量处置节点");
|
|
|
+
|
|
|
+ var tid = ResolveTenantOrThrow();
|
|
|
+ // 一单一有效处置(status=1 且未软删)+ InspectionId 唯一约束
|
|
|
+ var existed = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
|
|
|
+ .AnyAsync(x => x.InspectionId == input.BillId && x.TenantId == tid && x.Status == 1);
|
|
|
+ if (existed) throw Oops.Oh("该检验单已存在有效质量处置记录");
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var opName = _userManager.RealName ?? _userManager.Account;
|
|
|
+ var tran = await _db.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ await _db.Insertable(new AdoS6IpqcQualityDisposition
|
|
|
+ {
|
|
|
+ Id = YitIdHelper.NextId(),
|
|
|
+ TenantId = tid,
|
|
|
+ InspectionId = input.BillId,
|
|
|
+ InspectionNo = bill.WorkOrderNo,
|
|
|
+ BizType = S6ProcessInspectionFlowConst.BizType,
|
|
|
+ FlowInstanceId = inst.Id,
|
|
|
+ DispositionNo = $"S6PIDISP-{bill.WorkOrderNo}-{now:yyyyMMddHHmmss}",
|
|
|
+ DispositionType = type,
|
|
|
+ DispositionQty = input.DispositionQty,
|
|
|
+ ReworkQty = input.ReworkQty,
|
|
|
+ ConcessionQty = input.ConcessionQty,
|
|
|
+ ScrapQty = input.ScrapQty,
|
|
|
+ DispositionOpinion = input.DispositionOpinion,
|
|
|
+ ResponsibilityDept = input.ResponsibilityDept,
|
|
|
+ ResponsibleUser = input.ResponsibleUser,
|
|
|
+ Status = 1,
|
|
|
+ SubmittedBy = _userManager.UserId,
|
|
|
+ SubmittedByName = opName,
|
|
|
+ SubmittedAt = now,
|
|
|
+ RowVersion = 1,
|
|
|
+ CreateTime = now,
|
|
|
+ }).ExecuteCommandAsync();
|
|
|
+ await _flowEngine.Approve(myTask.Id, input.Comment);
|
|
|
+ });
|
|
|
+ if (!tran.IsSuccess) throw tran.ErrorException;
|
|
|
+ return await BuildStateAsync(input.BillId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ───────────────────────── helpers ─────────────────────────
|
|
|
+
|
|
|
+ private sealed class BillRow
|
|
|
+ {
|
|
|
+ public long Id { get; set; }
|
|
|
+ public string? WorkOrderNo { get; set; }
|
|
|
+ public string? OverallResult { get; set; }
|
|
|
+ public string? ResultStatus { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>加载 Phase 1C 单据 + 结果(严格租户隔离)。</summary>
|
|
|
+ private async Task<BillRow> LoadBillAsync(long billId)
|
|
|
+ {
|
|
|
+ if (billId <= 0) throw Oops.Oh("检验单 id 非法");
|
|
|
+ var tid = ResolveTenantOrThrow();
|
|
|
+ var row = await _db.Ado.SqlQuerySingleAsync<BillRow>(
|
|
|
+ """
|
|
|
+ SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
|
|
|
+ FROM ado_s6_process_inspection_bill b
|
|
|
+ LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
|
|
|
+ WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
|
|
|
+ """,
|
|
|
+ new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
|
|
|
+ if (row == null) throw Oops.Oh("检验单不存在");
|
|
|
+ return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<ApprovalFlowInstance?> GetLatestInstanceAsync(long bizId)
|
|
|
+ => await _db.Queryable<ApprovalFlowInstance>().ClearFilter()
|
|
|
+ .Where(x => x.BizType == S6ProcessInspectionFlowConst.BizType && x.BizId == bizId)
|
|
|
+ .OrderByDescending(x => x.Id).FirstAsync();
|
|
|
+
|
|
|
+ private async Task<ApprovalFlowTask?> GetMyPendingTaskAsync(long instanceId)
|
|
|
+ {
|
|
|
+ var userId = _userManager.UserId;
|
|
|
+ return await _db.Queryable<ApprovalFlowTask>().ClearFilter()
|
|
|
+ .Where(x => x.InstanceId == instanceId && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending)
|
|
|
+ .FirstAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<(ApprovalFlowInstance inst, ApprovalFlowTask task)> RequireSupervisorTaskAsync(long bizId)
|
|
|
+ {
|
|
|
+ var inst = await GetLatestInstanceAsync(bizId);
|
|
|
+ if (inst == null) throw Oops.Oh("该检验单尚未发起审核");
|
|
|
+ if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
|
|
|
+ var myTask = await GetMyPendingTaskAsync(inst.Id);
|
|
|
+ if (myTask == null) throw Oops.Oh("当前用户没有待处理的审核任务(需检验主管角色成员)");
|
|
|
+ if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeSupervisor) throw Oops.Oh("当前不在主管审核节点");
|
|
|
+ return (inst, myTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<bool> HasRoleAsync(long roleId)
|
|
|
+ {
|
|
|
+ var userId = _userManager.UserId;
|
|
|
+ return await _db.Queryable<SysUserRole>().ClearFilter().AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<S6ReviewStateOutput> BuildStateAsync(long billId)
|
|
|
+ {
|
|
|
+ var tid = ResolveTenantOrThrow();
|
|
|
+ var output = new S6ReviewStateOutput { BillId = billId };
|
|
|
+ var bill = await _db.Ado.SqlQuerySingleAsync<BillRow>(
|
|
|
+ """
|
|
|
+ SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
|
|
|
+ FROM ado_s6_process_inspection_bill b
|
|
|
+ LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
|
|
|
+ WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
|
|
|
+ """,
|
|
|
+ new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
|
|
|
+ output.OverallResult = bill?.OverallResult;
|
|
|
+ output.ResultStatus = bill?.ResultStatus;
|
|
|
+ output.DispositionRequired = string.Equals(bill?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
|
|
+ output.HasDisposition = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
|
|
|
+ .AnyAsync(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1);
|
|
|
+
|
|
|
+ var inst = await GetLatestInstanceAsync(billId);
|
|
|
+ if (inst == null)
|
|
|
+ {
|
|
|
+ output.FlowStatus = "NotStarted";
|
|
|
+ output.CanSubmitReview = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
|
|
|
+ output.Message = output.CanSubmitReview ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核";
|
|
|
+ return output;
|
|
|
+ }
|
|
|
+
|
|
|
+ output.InstanceId = inst.Id;
|
|
|
+ output.FlowStatus = inst.Status.ToString();
|
|
|
+ if (inst.Status != FlowInstanceStatusEnum.Running)
|
|
|
+ {
|
|
|
+ output.IsCompleted = true;
|
|
|
+ output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "审核已闭环(CLOSED)" : $"流程已结束({inst.Status})";
|
|
|
+ // 已结束且非通过(驳回/撤回)→ 允许重新提交
|
|
|
+ output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled
|
|
|
+ && string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
|
|
|
+ return output;
|
|
|
+ }
|
|
|
+
|
|
|
+ output.CurrentNodeCode = inst.CurrentNodeId;
|
|
|
+ var myTask = await GetMyPendingTaskAsync(inst.Id);
|
|
|
+ if (myTask != null)
|
|
|
+ {
|
|
|
+ output.CurrentNodeName = myTask.NodeName;
|
|
|
+ if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeSupervisor)
|
|
|
+ {
|
|
|
+ output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleSupervisorCode;
|
|
|
+ output.CanSupervisorApprove = true;
|
|
|
+ output.CanSupervisorReturn = true;
|
|
|
+ output.Message = "待当前用户主管审核";
|
|
|
+ }
|
|
|
+ else if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeDisposition)
|
|
|
+ {
|
|
|
+ output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleQualityEngineerCode;
|
|
|
+ output.CanSubmitDisposition = true;
|
|
|
+ output.Message = "待当前用户提交质量处置";
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ output.Message = "流程进行中";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ output.CurrentAssigneeRole = inst.CurrentNodeId switch
|
|
|
+ {
|
|
|
+ S6ProcessInspectionFlowConst.NodeDisposition => S6ProcessInspectionFlowConst.RoleQualityEngineerCode,
|
|
|
+ S6ProcessInspectionFlowConst.NodeSupervisor => S6ProcessInspectionFlowConst.RoleSupervisorCode,
|
|
|
+ _ => null,
|
|
|
+ };
|
|
|
+ output.Message = "流程进行中(当前用户无待办)";
|
|
|
+ }
|
|
|
+ return output;
|
|
|
+ }
|
|
|
+}
|