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; /// /// 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)。 /// [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; } /// 审核流状态(只读,供前端按钮控制)。 [DisplayName("过程检验单审核状态")] [HttpGet("state")] public async Task GetState([FromQuery] long billId) { await LoadBillAsync(billId); return await BuildStateAsync(billId); } /// 提交审核(仅 COMPLETED 可提交;幂等;发起流程并自动通过 N1→N2)。 [DisplayName("提交审核")] [HttpPost("submit-review")] public async Task 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); } /// 主管审核通过(N2):Approve→GW_RESULT,按 overall_result 自动分流(FAIL→N3 / PASS→end)。不改写质量结果。 [DisplayName("主管审核通过")] [HttpPost("supervisor-approve")] public async Task 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); } /// 主管退回(N2→N1,意见必填)。质量结果不改写。 [DisplayName("主管退回")] [HttpPost("supervisor-return")] public async Task 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); } /// QE 质量处置(N3):落处置记录(不改写 overall_result),Approve→end。一单一有效处置。 [DisplayName("质量处置")] [HttpPost("qe-disposition")] public async Task 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().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; } } /// 加载 Phase 1C 单据 + 结果(严格租户隔离)。 private async Task LoadBillAsync(long billId) { if (billId <= 0) throw Oops.Oh("检验单 id 非法"); var tid = ResolveTenantOrThrow(); var row = await _db.Ado.SqlQuerySingleAsync( """ 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 { new("@id", billId), new("@tid", tid) }); if (row == null) throw Oops.Oh("检验单不存在"); return row; } private async Task GetLatestInstanceAsync(long bizId) => await _db.Queryable().ClearFilter() .Where(x => x.BizType == S6ProcessInspectionFlowConst.BizType && x.BizId == bizId) .OrderByDescending(x => x.Id).FirstAsync(); private async Task GetMyPendingTaskAsync(long instanceId) { var userId = _userManager.UserId; return await _db.Queryable().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 HasRoleAsync(long roleId) { var userId = _userManager.UserId; return await _db.Queryable().ClearFilter().AnyAsync(x => x.UserId == userId && x.RoleId == roleId); } private async Task BuildStateAsync(long billId) { var tid = ResolveTenantOrThrow(); var output = new S6ReviewStateOutput { BillId = billId }; var bill = await _db.Ado.SqlQuerySingleAsync( """ 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 { new("@id", billId), new("@tid", tid) }); var completed = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase); output.OverallResult = bill?.OverallResult; output.ResultStatus = bill?.ResultStatus; output.QualityText = QualityTextOf(bill?.OverallResult); output.DispositionRequired = string.Equals(bill?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0; output.Disposition = await LoadDispositionAsync(billId, tid); output.HasDisposition = output.Disposition != null; var inst = await GetLatestInstanceAsync(billId); if (inst == null) { output.FlowStatus = "NotStarted"; output.CanSubmitReview = completed; output.DisplayStatus = completed ? "待提交审核" : "检验中"; output.Message = completed ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核"; return output; } output.InstanceId = inst.Id; output.FlowStatus = inst.Status.ToString(); output.History = await LoadHistoryAsync(inst.Id, tid); if (inst.Status != FlowInstanceStatusEnum.Running) { output.IsCompleted = true; output.DisplayStatus = inst.Status == FlowInstanceStatusEnum.Approved ? "已关闭" : inst.Status == FlowInstanceStatusEnum.Rejected ? "已驳回" : inst.Status == FlowInstanceStatusEnum.Cancelled ? "已撤回" : "已结束"; output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "审核已闭环(CLOSED)" : $"流程已结束({inst.Status})"; output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled && completed; return output; } output.CurrentNodeCode = inst.CurrentNodeId; output.DisplayStatus = inst.CurrentNodeId switch { S6ProcessInspectionFlowConst.NodeSupervisor => "主管审核中", S6ProcessInspectionFlowConst.NodeDisposition => "QE质量处置中", _ => "审核中", }; var myTask = await GetMyPendingTaskAsync(inst.Id); if (myTask != null) { output.CurrentNodeName = NodeNameOf(myTask.NodeId) ?? 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.CurrentNodeName = NodeNameOf(inst.CurrentNodeId); output.CurrentAssigneeRole = inst.CurrentNodeId switch { S6ProcessInspectionFlowConst.NodeDisposition => S6ProcessInspectionFlowConst.RoleQualityEngineerCode, S6ProcessInspectionFlowConst.NodeSupervisor => S6ProcessInspectionFlowConst.RoleSupervisorCode, _ => null, }; output.Message = "流程进行中(当前用户无待办)"; } return output; } // ── Phase 1E 展示辅助(业务中文映射;后端拥有语义)── private static string QualityTextOf(string? overallResult) => (overallResult ?? "").ToUpperInvariant() switch { "PASS" => "合格", "FAIL" => "不合格", "PENDING" or "IN_PROGRESS" => "检验中", "INVALID" => "无效", _ => "—", }; private static string? NodeNameOf(string? nodeId) => nodeId switch { S6ProcessInspectionFlowConst.NodeSubmit => "提交审核", S6ProcessInspectionFlowConst.NodeSupervisor => "检验主管审核", S6ProcessInspectionFlowConst.NodeDisposition => "质量处置", _ => null, }; private static string DispositionTypeTextOf(string? t) => (t ?? "").ToUpperInvariant() switch { "REWORK" => "返工", "CONCESSION" => "让步接收", "SCRAP" => "报废", "OTHER" => "其它", _ => t ?? "", }; private static string ActionTextOf(int action) => action switch { 1 => "提交", 2 => "通过", 3 => "驳回", 5 => "撤回", 6 => "退回", 9 => "取消", _ => "操作", }; private async Task LoadDispositionAsync(long billId, long tid) { var d = await _db.Queryable().ClearFilter() .Where(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1) .OrderByDescending(x => x.Id).FirstAsync(); if (d == null) return null; return new S6DispositionView { DispositionType = d.DispositionType, DispositionTypeText = DispositionTypeTextOf(d.DispositionType), DispositionQty = d.DispositionQty, DispositionOpinion = d.DispositionOpinion, ResponsibilityDept = d.ResponsibilityDept, ResponsibleUser = d.ResponsibleUser, OperatorName = d.SubmittedByName, SubmittedAt = d.SubmittedAt, }; } private sealed class FlowLogRow { public int Action { get; set; } public string? NodeId { get; set; } public string? OperatorName { get; set; } public string? Comment { get; set; } public DateTime? CreateTime { get; set; } } private async Task> LoadHistoryAsync(long instanceId, long tid) { // 复用正式 ApprovalFlowLog(不造第二套 history)。租户隔离由实例归属保证(instanceId 已按 BizType+BizId 定位)。 var rows = await _db.Ado.SqlQueryAsync( "SELECT Action AS Action, NodeId AS NodeId, OperatorName AS OperatorName, Comment AS Comment, CreateTime AS CreateTime FROM ApprovalFlowLog WHERE InstanceId=@iid ORDER BY Id", new List { new("@iid", instanceId) }); return rows.Select(r => new S6ReviewHistoryItem { Time = r.CreateTime, OperatorName = r.OperatorName, NodeName = NodeNameOf(r.NodeId) ?? r.NodeId, ActionText = ActionTextOf(r.Action), Comment = r.Comment, }).ToList(); } }