using Microsoft.AspNetCore.Http; using OpenAuth.App.BaseApp.Base; using OpenAuth.App.Interface; using OpenAuth.Repository; using OpenAuth.Repository.Domain; using OpenAuth.Repository.Domain.workflow; using SqlSugar; namespace workflow; using Microsoft.AspNetCore.Http; using OpenAuth.App.BaseApp.Base; using OpenAuth.App.Interface; using OpenAuth.Repository; using OpenAuth.Repository.Domain; using SqlSugar; using System; using System.Collections.Generic; using System.IO; using System.Linq; /// /// 完整工作流引擎(适配:区县→执法监督科→5个审核科会签→汇总归档/退回) /// public class WorkflowEngineApp : SqlSugarBaseApp { private readonly ISqlSugarClient _sqlSugar; // 配置:5个审核科角色ID(需与sys_role表对应,自行修改为实际值) private readonly List _auditDeptRoleIds = new() { 1, // 政策法规科 2, // 详细规划科 3, // 空间规划科 4, // 开发利用科 5 // 工程科 }; // 配置:执法监督科角色ID(需与sys_role表对应,自行修改为实际值) private const long _supervisionDeptRoleId = 6; public WorkflowEngineApp(ISugarUnitOfWork unitWork, ISimpleClient repository, IAuth auth) : base(unitWork, repository, auth) { _sqlSugar = Repository.AsSugarClient(); } #region 1. 发起流程(区县提交→流转至执法监督科) public long InitiateFlow(long userId, string userName, InitiateFlowRequest request) { // 参数校验 if (string.IsNullOrEmpty(request.FlowCode) || string.IsNullOrEmpty(request.BusinessNo)) throw new Exception("流程编码和业务编号不能为空"); if (userId <= 0 || string.IsNullOrEmpty(userName)) throw new Exception("发起人ID和姓名不能为空"); var instanceId = 0L; try { // 开启事务(基于UnitWork,保障数据一致性) UnitWork.Db.Ado.BeginTran(); // 步骤1:查询启用的流程模板(违法建设认定流程) var template = _sqlSugar.Queryable() .Where(t => t.FlowCode == request.FlowCode && t.IsEnabled == true) .First(); if (template == null) throw new Exception($"流程模板【{request.FlowCode}】不存在或未启用"); // 步骤2:查询流程开始节点(区县提交) var startNode = _sqlSugar.Queryable() .Where(n => n.TemplateId == template.TemplateId && n.NodeType == "Start" && n.NodeName == "区县提交") .First(); if (startNode == null) throw new Exception("流程开始节点【区县提交】不存在,请配置节点"); // 步骤3:插入流程实例,返回自增主键 var flowInstance = new ZyFlowInstance { TemplateId = template.TemplateId, FlowCode = template.FlowCode, BusinessNo = request.BusinessNo, Status = "Submitted", // 已提交 CurrentNodeId = startNode.NodeId, InitiatorId = userId, InitiatorName = userName, CreateTime = DateTime.Now }; instanceId = _sqlSugar.Insertable(flowInstance).ExecuteReturnIdentity(); // 步骤4:插入开始节点工作项(直接标记为已完成) var startWorkitem = new ZyFlowWorkitem { InstanceId = instanceId, NodeId = startNode.NodeId, NodeName = startNode.NodeName, HandlerId = userId, HandlerName = userName, Status = "Done", // 已完成 ReceiveTime = DateTime.Now, HandleTime = DateTime.Now, Comment = "区县提交业务材料,流程发起成功" }; _sqlSugar.Insertable(startWorkitem).ExecuteCommand(); // 步骤5:保存流程变量(标题、附件路径等) var attachmentPaths = SaveAttachments(request.Attachments); var flowVariables = new List { new ZyFlowVariable { InstanceId = instanceId, VarKey = "Title", VarValue = request.Title }, new ZyFlowVariable { InstanceId = instanceId, VarKey = "AttachmentPaths", VarValue = attachmentPaths }, new ZyFlowVariable { InstanceId = instanceId, VarKey = "SubmitTime", VarValue = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") } }; _sqlSugar.Insertable(flowVariables).ExecuteCommand(); // 步骤6:核心流转:从区县提交节点流转至执法监督科节点 FlowToNextNode(instanceId, startNode, userId, userName, "区县提交完成,流转至执法监督科"); // 提交事务 UnitWork.Db.Ado.CommitTran(); } catch (Exception ex) { // 回滚事务 UnitWork.Db.Ado.RollbackTran(); throw new Exception($"发起流程失败:{ex.Message}", ex); } return instanceId; } #endregion #region 2. 处理工作项(执法监督科转发/审核科会签/汇总处理) public bool HandleWorkitem(long userId, string userName, HandleWorkitemRequest request) { // 参数校验 if (request.WorkitemId <= 0) throw new Exception("工作项ID无效"); if (userId <= 0 || string.IsNullOrEmpty(userName)) throw new Exception("处理人ID和姓名不能为空"); try { // 开启事务 UnitWork.Db.Ado.BeginTran(); // 步骤1:查询待处理的工作项(仅待办状态可处理) var workitem = _sqlSugar.Queryable() .Where(w => w.WorkitemId == request.WorkitemId && w.Status == "ToDo") .First(); if (workitem == null) throw new Exception("工作项不存在、已处理或状态异常"); // 步骤2:查询关联的流程实例和当前节点 var flowInstance = _sqlSugar.Queryable() .Where(i => i.InstanceId == workitem.InstanceId) .First(); var currentNode = _sqlSugar.Queryable() .Where(n => n.NodeId == workitem.NodeId) .First(); if (flowInstance == null || currentNode == null) throw new Exception("流程实例或流程节点不存在"); // 步骤3:更新当前工作项为已完成 workitem.Status = "Done"; workitem.HandleTime = DateTime.Now; workitem.Comment = string.IsNullOrEmpty(request.Comment) ? "处理完成" : request.Comment; _sqlSugar.Updateable(workitem).ExecuteCommand(); // 步骤4:按节点类型分支处理核心逻辑 switch (currentNode.NodeType) { // 普通节点:执法监督科转发 case "Common" when currentNode.NodeName == "执法监督科转发": FlowToNextNode(flowInstance.InstanceId, currentNode, userId, userName, workitem.Comment); break; // 并行节点:5个审核科会签 case "Parallel" when _auditDeptRoleIds.Contains(currentNode.RoleId): ProcessParallelAudit(flowInstance.InstanceId, currentNode.NodeId, userId, userName, request); break; // 分支节点:汇总(归档/退回区县) case "Branch" when currentNode.NodeName == "汇总": ProcessSummaryNode(flowInstance, currentNode, userId, userName); break; // 结束节点:流程归档完成 case "End": CompleteFlowInstance(flowInstance); break; } // 提交事务 UnitWork.Db.Ado.CommitTran(); return true; } catch (Exception ex) { // 回滚事务 UnitWork.Db.Ado.RollbackTran(); throw new Exception($"处理工作项失败:{ex.Message}", ex); } } #endregion #region 3. 流程查询(我的待办/我的已办) /// /// 查询我的待办工作项 /// public List QueryMyToDo(long userId) { if (userId <= 0) return new List(); // 步骤1:查询当前用户的待办工作项 var toDoWorkitems = _sqlSugar.Queryable() .Where(w => w.HandlerId == userId && w.Status == "ToDo") .ToList(); // 步骤2:组装返回结果 var queryResults = new List(); foreach (var workitem in toDoWorkitems) { var flowInstance = _sqlSugar.Queryable() .Where(i => i.InstanceId == workitem.InstanceId) .First(); if (flowInstance == null) continue; // 查询流程标题 var flowTitle = _sqlSugar.Queryable() .Where(v => v.InstanceId == flowInstance.InstanceId && v.VarKey == "Title") .First()?.VarValue ?? string.Empty; queryResults.Add(new FlowQueryResult { InstanceId = flowInstance.InstanceId, BusinessNo = flowInstance.BusinessNo, Title = flowTitle, NodeName = workitem.NodeName, Status = flowInstance.Status, CreateTime = flowInstance.CreateTime, InitiatorName = flowInstance.InitiatorName }); } return queryResults; } /// /// 查询我的已办工作项 /// public List QueryMyDone(long userId) { if (userId <= 0) return new List(); // 步骤1:查询当前用户已完成的工作项对应的流程实例ID(去重) var doneInstanceIds = _sqlSugar.Queryable() .Where(w => w.HandlerId == userId && w.Status == "Done") .Select(w => w.InstanceId) .Distinct() .ToList(); // 步骤2:组装返回结果 var queryResults = new List(); foreach (var instanceId in doneInstanceIds) { var flowInstance = _sqlSugar.Queryable() .Where(i => i.InstanceId == instanceId) .First(); if (flowInstance == null) continue; // 查询流程标题 var flowTitle = _sqlSugar.Queryable() .Where(v => v.InstanceId == flowInstance.InstanceId && v.VarKey == "Title") .First()?.VarValue ?? string.Empty; // 查询最新的已完成工作项 var lastWorkitem = _sqlSugar.Queryable() .Where(w => w.InstanceId == instanceId && w.Status == "Done") .OrderByDescending(w => w.HandleTime) .First(); queryResults.Add(new FlowQueryResult { InstanceId = flowInstance.InstanceId, BusinessNo = flowInstance.BusinessNo, Title = flowTitle, NodeName = lastWorkitem?.NodeName ?? string.Empty, Status = flowInstance.Status, CreateTime = flowInstance.CreateTime, InitiatorName = flowInstance.InitiatorName }); } return queryResults; } #endregion #region 4. 核心辅助方法(流转/会签/汇总/附件等) /// /// 流程节点流转核心方法 /// private void FlowToNextNode(long instanceId, ZyFlowNode currentNode, long userId, string userName, string comment) { // 解析下一节点IDs var nextNodeIds = currentNode.NextNodeIds?.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(long.Parse) .ToList() ?? new List(); if (!nextNodeIds.Any()) return; // 查询流程实例 var flowInstance = _sqlSugar.Queryable() .Where(i => i.InstanceId == instanceId) .First(); if (flowInstance == null) throw new Exception("流程实例不存在"); // 遍历处理每个下一节点 foreach (var nextNodeId in nextNodeIds) { var nextNode = _sqlSugar.Queryable() .Where(n => n.NodeId == nextNodeId) .First(); if (nextNode == null) continue; // 更新流程实例当前节点和状态 flowInstance.CurrentNodeId = nextNode.NodeId; flowInstance.Status = GetFlowStatusByNodeType(nextNode.NodeType); _sqlSugar.Updateable(flowInstance).ExecuteCommand(); // 并行节点(5个审核科):为每个审核科创建工作项 if (nextNode.NodeType == "Parallel") { foreach (var roleId in _auditDeptRoleIds) { CreateAuditDeptWorkitem(instanceId, nextNode, roleId); } } // 普通节点/分支节点:创建单个工作项 else { var nextWorkitem = new ZyFlowWorkitem { InstanceId = instanceId, NodeId = nextNode.NodeId, NodeName = nextNode.NodeName, HandlerId = userId, HandlerName = userName, Status = "ToDo", ReceiveTime = DateTime.Now, Comment = comment }; _sqlSugar.Insertable(nextWorkitem).ExecuteCommand(); } } } /// /// 处理并行会签(保存结果+判断是否全部完成) /// private void ProcessParallelAudit(long instanceId, long nodeId, long userId, string userName, HandleWorkitemRequest request) { // 校验会签结果 if (string.IsNullOrEmpty(request.AuditResult)) throw new Exception("会签需选择审核结果(Pass/Reject)"); // 步骤1:获取当前用户所属科室名称 var deptName = GetUserDeptName(userId); // 步骤2:保存会签记录 var parallelAudit = new ZyFlowParallelAudit { InstanceId = instanceId, NodeId = nodeId, DeptName = deptName, AuditResult = request.AuditResult, AuditComment = request.Comment, AuditorId = userId, AuditorName = userName, AuditTime = DateTime.Now }; _sqlSugar.Insertable(parallelAudit).ExecuteCommand(); // 步骤3:判断是否所有审核科都已完成会签 if (IsAllAuditCompleted(instanceId)) { // 步骤4:流转至汇总节点 var parallelNode = _sqlSugar.Queryable() .Where(n => n.NodeId == nodeId && n.NodeType == "Parallel") .First(); FlowToNextNode(instanceId, parallelNode, userId, userName, "所有审核科会签完成,流转至汇总节点"); } } /// /// 处理汇总节点(归档/退回区县) /// private void ProcessSummaryNode(ZyFlowInstance flowInstance, ZyFlowNode summaryNode, long userId, string userName) { // 步骤1:判断是否全部审核通过 var isAllPass = _sqlSugar.Queryable() .Where(a => a.InstanceId == flowInstance.InstanceId) .Any(a=> a.AuditResult != "Pass"); // todo // 步骤2:解析汇总节点下一节点(归档/退回) var nextNodeIds = summaryNode.NextNodeIds?.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(long.Parse) .ToList() ?? new List(); if (nextNodeIds.Count < 2) throw new Exception("汇总节点需配置2个后续节点(归档/退回区县)"); // 步骤3:确定目标节点(通过→归档,不通过→退回) var targetNodeId = !isAllPass ? nextNodeIds[0] : nextNodeIds[1]; var targetNode = _sqlSugar.Queryable() .Where(n => n.NodeId == targetNodeId) .First(); if (targetNode == null) return; // 步骤4:创建目标节点工作项 var summaryWorkitem = new ZyFlowWorkitem { InstanceId = flowInstance.InstanceId, NodeId = targetNode.NodeId, NodeName = targetNode.NodeName, HandlerId = isAllPass ? null : flowInstance.InitiatorId, HandlerName = isAllPass ? "系统自动处理" : flowInstance.InitiatorName, Status = isAllPass ? "Done" : "ToDo", ReceiveTime = DateTime.Now, HandleTime = isAllPass ? DateTime.Now : null, Comment = isAllPass ? "所有审核科通过,流程归档完成" : "存在审核不通过项,退回区县修改" }; _sqlSugar.Insertable(summaryWorkitem).ExecuteCommand(); // 步骤5:更新流程实例最终状态 flowInstance.CurrentNodeId = targetNode.NodeId; flowInstance.Status = isAllPass ? "Completed" : "Rejected"; flowInstance.FinishTime = isAllPass ? DateTime.Now : null; _sqlSugar.Updateable(flowInstance).ExecuteCommand(); } /// /// 完成流程实例(结束节点) /// private void CompleteFlowInstance(ZyFlowInstance flowInstance) { flowInstance.Status = "Completed"; flowInstance.FinishTime = DateTime.Now; _sqlSugar.Updateable(flowInstance).ExecuteCommand(); } /// /// 为审核科创建工作项 /// private void CreateAuditDeptWorkitem(long instanceId, ZyFlowNode parallelNode, long roleId) { // 获取科室用户信息 var (userId, userName, deptName) = GetRoleFirstUserInfo(roleId); if (userId <= 0 || string.IsNullOrEmpty(userName)) throw new Exception($"审核科角色【{roleId}】未配置有效用户"); // 创建工作项 var workitem = new ZyFlowWorkitem { InstanceId = instanceId, NodeId = parallelNode.NodeId, NodeName = $"{parallelNode.NodeName}({deptName})", HandlerId = userId, HandlerName = userName, Status = "ToDo", ReceiveTime = DateTime.Now, Comment = "请完成违法建设认定相关审核工作" }; _sqlSugar.Insertable(workitem).ExecuteCommand(); } /// /// 保存附件并返回路径 /// private string SaveAttachments(List attachments) { if (attachments == null || !attachments.Any()) return string.Empty; // 定义附件存储目录 var uploadDir = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/Attachments/FlowAttachments"); if (!Directory.Exists(uploadDir)) Directory.CreateDirectory(uploadDir); var attachmentPaths = new List(); foreach (var file in attachments) { if (file.Length <= 0) continue; // 生成唯一文件名 var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; var filePath = Path.Combine(uploadDir, fileName); // 保存文件 using (var stream = new FileStream(filePath, FileMode.Create)) { file.CopyTo(stream); } // 保存相对路径(用于前端访问) attachmentPaths.Add($"/Attachments/FlowAttachments/{fileName}"); } return string.Join(",", attachmentPaths); } /// /// 根据节点类型获取流程状态 /// private string GetFlowStatusByNodeType(string nodeType) { return nodeType switch { "Common" => "Forwarded", // 已转发 "Parallel" => "Auditing", // 审核中 "Branch" => "Summarized", // 已汇总 "End" => "Completed", // 已完成 _ => "Submitted" // 已提交 }; } /// /// 判断所有审核科是否完成会签 /// private bool IsAllAuditCompleted(long instanceId) { var auditResults = _sqlSugar.Queryable() .Where(a => a.InstanceId == instanceId) .ToList(); // 条件:会签记录数=5个审核科 且 无待审核状态 return auditResults.Count == _auditDeptRoleIds.Count && !auditResults.Any(a => a.AuditResult == "Pending"); } /// /// 获取用户所属科室名称 /// private string GetUserDeptName(long userId) { var deptName = _sqlSugar.Queryable((uo, o) => new JoinQueryInfos( JoinType.Inner, uo.OrgId == o.Id)) .Where((uo, o) => uo.UserId == userId) .Select((uo, o) => o.Name) .First(); return deptName ?? "未知科室"; } /// /// 获取角色下第一个用户ID /// private long GetRoleFirstUserId(long roleId) { var userInfo = GetRoleFirstUserInfo(roleId); return userInfo.userId; } /// /// 获取角色下第一个用户姓名 /// private string GetRoleFirstUserName(long roleId) { var userInfo = GetRoleFirstUserInfo(roleId); return userInfo.userName; } /// /// 获取角色下第一个用户的完整信息 /// private (long userId, string userName, string deptName) GetRoleFirstUserInfo(long roleId) { var sql = @" SELECT u.""Id"" as UserId, u.""Name"" as UserName, o.""Name"" as DeptName FROM sys_user u INNER JOIN sys_userrole r ON u.""Id"" = r.""UserId"" INNER JOIN sys_userorg uo ON u.""Id"" = uo.""UserId"" INNER JOIN sys_org o ON uo.""OrgId"" = o.""Id"" WHERE r.""RoleId"" = @RoleId LIMIT 1"; var userInfo = _sqlSugar.SqlQueryable(sql) .AddParameters(new { RoleId = roleId }) .First(); return (userInfo.UserId, userInfo.UserName ?? string.Empty, userInfo.DeptName ?? string.Empty); } #endregion }