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 过程检验单(IPQC)检验员+主管流程服务(v2:含整批判定驱动的质量处置分流)。 /// /// N1_INSPECT 检验员录入 → 提交 → N2_SUP_REVIEW 主管整批审核: /// 整批合格(Qualified=true) → GW_RESULT(disposition_required=0) → end; /// 整批不合格(Qualified=false) → GW_RESULT(disposition_required=1) → N3_QUALITY_DISPOSITION(见 IpqcInspectionDispositionService)。 /// 退回:N2→N1。复用 ApprovalFlow 引擎(StartFlow/Approve/ReturnToPrev + IFlowBizHandler),不改核心。 /// 整批判定 jgpd 写 qms_gcjyd 白名单列(可空、不硬编码枚举);disposition_required 落扩展表作网关判据(与 jgpd 整批判定一致)。 /// 项目级 pd(qms_gcjydzb)仅明细展示辅助,不驱动整批流程。不回写 qms_gcjyd.status;不写库存/生产/仓储。 /// 正常鉴权(非 AllowAnonymous);权限以当前用户 pending task 归属为准。 /// [ApiDescriptionSettings(Order = 312, Description = "过程检验单流程")] [Route("api/IpqcInspectionFlow")] [NonUnify] public class IpqcInspectionFlowService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly FlowEngineService _flowEngine; private readonly UserManager _userManager; public IpqcInspectionFlowService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager) { _db = db; _flowEngine = flowEngine; _userManager = userManager; } /// /// 检验单流程状态(只读,供前端按钮控制)。 /// [DisplayName("过程检验单流程状态")] [HttpGet("state")] public async Task GetState([FromQuery] long id) { await EnsureBillExistsAsync(id); return await BuildStateAsync(id); } /// /// 检验员提交检验结果(写 qms_gcjyd 白名单列 + 推进 N1→N2)。 /// [DisplayName("检验员提交检验结果")] [HttpPost("submit-inspection")] public async Task SubmitInspection([FromBody] IpqcSubmitInspectionInput input) { var billNo = await EnsureBillExistsAsync(input.Id); // 参数校验(不合格数不得为负、不得大于样本量;合格数不得为负) if (input.Jyhgsl < 0) throw Oops.Oh("检验合格数量不能为负"); if (input.Jybhgsl < 0) throw Oops.Oh("检验不合格数量不能为负"); if (input.Ybl is < 0) throw Oops.Oh("样本量不能为负"); if (input.Ybl is > 0 && input.Jybhgsl > input.Ybl.Value) throw Oops.Oh("不合格数量不能大于样本量(检验数)"); var inst = await GetLatestInstanceAsync(input.Id); if (inst != null && inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单流程已结束,无法再提交检验结果"); var now = DateTime.Now; var inspectorName = _userManager.RealName ?? _userManager.Account; var tran = await _db.AsTenant().UseTranAsync(async () => { // 无运行中实例 → 起流程(自动生成 N1 任务给检验员角色成员) long instanceId; if (inst == null) { instanceId = await _flowEngine.StartFlow(new StartFlowInput { BizType = IpqcInspectionFlowConst.BizType, BizId = input.Id, BizNo = billNo, Title = $"过程检验单 {billNo}", Comment = input.Comment, }); } else { instanceId = inst.Id; } var myTask = await GetMyPendingTaskAsync(instanceId); if (myTask == null) throw Oops.Oh("当前用户没有待处理的检验任务(需检验员角色成员)"); if (myTask.NodeId != IpqcInspectionFlowConst.NodeInspect) throw Oops.Oh("当前不在检验录入节点,无法提交检验结果"); // 写检验结果(仅 qms_gcjyd 白名单列 jgpd/jyhgsl/jybhgsl/ybl/jyr/bz;jgpd 可空不强写) await _db.Ado.ExecuteCommandAsync( "UPDATE qms_gcjyd SET jgpd=@jgpd, jyhgsl=@jyhgsl, jybhgsl=@jybhgsl, ybl=@ybl, jyr=@jyr, bz=@bz WHERE id=@id", new List { new("@jgpd", input.Jgpd.HasValue ? input.Jgpd.Value : (object?)null), new("@jyhgsl", input.Jyhgsl), new("@jybhgsl", input.Jybhgsl), new("@ybl", input.Ybl.HasValue ? input.Ybl.Value : (object?)null), new("@jyr", inspectorName), new("@bz", input.Bz), new("@id", input.Id), }); // 落轻量扩展表(提交人/提交时间/业务状态,不回写 qms_gcjyd.status) await UpsertFlowStateAsync(input.Id, billNo, instanceId, IpqcInspectionFlowConst.StatusPendingReview, IpqcInspectionFlowConst.NodeSupervisor, now, isSubmit: true, inspectorName: inspectorName); // 推进 N1 → N2 await _flowEngine.Approve(myTask.Id, input.Comment); }); if (!tran.IsSuccess) throw tran.ErrorException; return await BuildStateAsync(input.Id); } /// /// 检验主管通过(整批判定):Qualified=true 整批合格→end;Qualified=false 整批不合格→N3 质量处置。 /// 写整批 jgpd(可空、不硬编码枚举)+ disposition_required(网关判据),再 Approve→GW_RESULT 分流。 /// [DisplayName("检验主管通过")] [HttpPost("supervisor-approve")] public async Task SupervisorApprove([FromBody] IpqcSupervisorApproveInput input) { var billNo = await EnsureBillExistsAsync(input.Id); var (inst, myTask) = await RequireSupervisorTaskAsync(input.Id); var now = DateTime.Now; var supervisorName = _userManager.RealName ?? _userManager.Account; var dispositionRequired = input.Qualified ? 0 : 1; var businessStatus = input.Qualified ? IpqcInspectionFlowConst.StatusApproved : IpqcInspectionFlowConst.StatusPendingDisposition; var nextNode = input.Qualified ? null : IpqcInspectionFlowConst.NodeDisposition; var tran = await _db.AsTenant().UseTranAsync(async () => { // 整批判定 jgpd 写 qms_gcjyd 白名单列(可空,不硬编码枚举;由前端传测试/已确认合法值) if (input.Jgpd.HasValue) await _db.Ado.ExecuteCommandAsync( "UPDATE qms_gcjyd SET jgpd=@jgpd WHERE id=@id", new List { new("@jgpd", input.Jgpd.Value), new("@id", input.Id) }); // 先写 disposition_required(GW_RESULT 网关判据,Approve 时 GetBizData 读取)再推进 await UpsertFlowStateAsync(input.Id, billNo, inst.Id, businessStatus, nextNode, now, isSubmit: false, reviewResult: input.Qualified ? "APPROVED" : "TO_DISPOSITION", supervisorName: supervisorName, dispositionRequired: dispositionRequired); await _flowEngine.Approve(myTask.Id, input.Comment); }); if (!tran.IsSuccess) throw tran.ErrorException; return await BuildStateAsync(input.Id); } /// /// 检验主管退回(N2 → 回 N1,意见必填)。 /// [DisplayName("检验主管退回")] [HttpPost("supervisor-return")] public async Task SupervisorReturn([FromBody] IpqcSupervisorReturnInput input) { if (string.IsNullOrWhiteSpace(input.Comment)) throw Oops.Oh("退回必须填写意见"); var billNo = await EnsureBillExistsAsync(input.Id); var (inst, myTask) = await RequireSupervisorTaskAsync(input.Id); var now = DateTime.Now; var supervisorName = _userManager.RealName ?? _userManager.Account; var tran = await _db.AsTenant().UseTranAsync(async () => { await UpsertFlowStateAsync(input.Id, billNo, inst.Id, IpqcInspectionFlowConst.StatusReturned, IpqcInspectionFlowConst.NodeInspect, now, isSubmit: false, reviewResult: "RETURNED", supervisorName: supervisorName, returnReason: input.Comment); await _flowEngine.ReturnToPrev(myTask.Id, input.Comment); }); if (!tran.IsSuccess) throw tran.ErrorException; return await BuildStateAsync(input.Id); } // ───────────────────────── helpers ───────────────────────── /// 校验检验单存在并返回 djbh(无 djbh 用 id 兜底)。 private async Task EnsureBillExistsAsync(long id) { if (id <= 0) throw Oops.Oh("检验单 id 非法"); var rows = await _db.Ado.SqlQueryAsync( "SELECT djbh FROM qms_gcjyd WHERE id=@id LIMIT 1", new List { new("@id", id) }); if (rows.Count == 0) throw Oops.Oh("检验单不存在"); return rows[0] ?? id.ToString(); } /// 取该检验单最新流程实例(BizType+BizId,跨数据范围)。 private async Task GetLatestInstanceAsync(long bizId) { return await _db.Queryable() .ClearFilter() .Where(x => x.BizType == IpqcInspectionFlowConst.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(); } /// 校验并返回主管 N2 待办任务。 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 != IpqcInspectionFlowConst.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); } /// Upsert 流程状态扩展表(不回写 qms_gcjyd.status)。dispositionRequired 非空时更新网关判据。 private async Task UpsertFlowStateAsync(long inspectionId, string billNo, long instanceId, string businessStatus, string? currentNode, DateTime now, bool isSubmit, string? inspectorName = null, string? supervisorName = null, string? reviewResult = null, string? returnReason = null, int? dispositionRequired = null) { var existing = await _db.Queryable() .ClearFilter() .Where(x => x.InspectionId == inspectionId) .FirstAsync(); if (existing == null) { var row = new AdoS6IpqcInspectionFlowState { Id = YitIdHelper.NextId(), TenantId = _userManager.TenantId, InspectionId = inspectionId, InspectionNo = billNo, BizType = IpqcInspectionFlowConst.BizType, FlowInstanceId = instanceId, BusinessStatus = businessStatus, CurrentNode = currentNode, DispositionRequired = dispositionRequired, RowVersion = 1, CreateTime = now, }; if (isSubmit) { row.InspectorUserId = _userManager.UserId; row.InspectorName = inspectorName; row.SubmittedAt = now; } else { row.SupervisorUserId = _userManager.UserId; row.SupervisorName = supervisorName; row.ReviewedAt = now; row.ReviewResult = reviewResult; row.ReturnReason = returnReason; } await _db.Insertable(row).ExecuteCommandAsync(); return; } existing.FlowInstanceId = instanceId; existing.BusinessStatus = businessStatus; existing.CurrentNode = currentNode; if (dispositionRequired.HasValue) existing.DispositionRequired = dispositionRequired; existing.RowVersion = (existing.RowVersion ?? 0) + 1; existing.UpdateTime = now; if (isSubmit) { existing.InspectorUserId = _userManager.UserId; existing.InspectorName = inspectorName; existing.SubmittedAt = now; } else { existing.SupervisorUserId = _userManager.UserId; existing.SupervisorName = supervisorName; existing.ReviewedAt = now; existing.ReviewResult = reviewResult; existing.ReturnReason = returnReason; } await _db.Updateable(existing).ExecuteCommandAsync(); } /// 组装流程状态输出。 private async Task BuildStateAsync(long bizId) { var output = new IpqcFlowStateOutput { BizId = bizId }; var inst = await GetLatestInstanceAsync(bizId); var state = await _db.Queryable() .ClearFilter() .Where(x => x.InspectionId == bizId) .FirstAsync(); if (state != null) { output.BusinessStatus = state.BusinessStatus; output.InspectorName = state.InspectorName; output.SupervisorName = state.SupervisorName; output.DispositionRequired = state.DispositionRequired; } output.HasDisposition = await _db.Queryable() .ClearFilter().AnyAsync(x => x.InspectionId == bizId && x.Status == 1); if (inst == null) { output.FlowStatus = "NotStarted"; output.CanSubmitResult = await HasRoleAsync(IpqcInspectionFlowConst.RoleInspectorId); output.Message = output.CanSubmitResult ? "待检验员录入结果" : "尚未发起流程"; return output; } output.InstanceId = inst.Id; output.FlowStatus = inst.Status.ToString(); output.UpdatedAt = inst.EndTime ?? inst.StartTime; if (inst.Status != FlowInstanceStatusEnum.Running) { output.IsCompleted = true; output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "流程已完成(主管审核通过)" : $"流程已结束({inst.Status})"; return output; } output.CurrentNodeCode = inst.CurrentNodeId; var myTask = await GetMyPendingTaskAsync(inst.Id); if (myTask != null) { output.CurrentNodeName = myTask.NodeName; if (myTask.NodeId == IpqcInspectionFlowConst.NodeInspect) { output.CurrentAssigneeRole = IpqcInspectionFlowConst.RoleInspectorCode; output.CanSubmitResult = true; output.Message = "待当前用户录入检验结果"; } else if (myTask.NodeId == IpqcInspectionFlowConst.NodeSupervisor) { output.CurrentAssigneeRole = IpqcInspectionFlowConst.RoleSupervisorCode; output.CanSupervisorApprove = true; output.CanSupervisorReturn = true; output.Message = "待当前用户主管审核"; } else if (myTask.NodeId == IpqcInspectionFlowConst.NodeDisposition) { output.CurrentAssigneeRole = IpqcInspectionFlowConst.RoleQualityEngineerCode; output.CanSubmitDisposition = true; output.Message = "待当前用户提交质量处置"; } } else { output.CurrentAssigneeRole = inst.CurrentNodeId switch { IpqcInspectionFlowConst.NodeInspect => IpqcInspectionFlowConst.RoleInspectorCode, IpqcInspectionFlowConst.NodeDisposition => IpqcInspectionFlowConst.RoleQualityEngineerCode, _ => IpqcInspectionFlowConst.RoleSupervisorCode, }; output.Message = "流程进行中(当前用户无待办)"; } return output; } }