namespace Admin.NET.Plugin.AiDOP.Order; /// /// 合同评审服务 📋 /// 路由前缀:/api/Order/contract/... /// 流程:意见评审(法律事务部→技术售前组→综合主计划→试验站)→意见反馈→二次评审→领导意见→合同盖章 /// [ApiDescriptionSettings(Order = 250, Description = "合同评审")] [Route("api/Order")] [AllowAnonymous] [NonUnify] public class ContractReviewService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly SqlSugarRepository _reviewRep; private readonly SqlSugarRepository _flowRep; private readonly UserManager _userManager; // 流程节点模板(stageNo, stageName, department, deptNo, seq) private static readonly (int StageNo, string StageName, string Department, string DeptNo, int Seq)[] FlowTemplate = { (1, "意见评审", "法律事务部", "DEPT_LAW", 1), (1, "意见评审", "技术售前组", "DEPT_TECH", 2), (1, "意见评审", "综合主计划", "DEPT_PLAN", 3), (1, "意见评审", "试验站", "DEPT_TEST", 4), (2, "意见反馈", "申请部门", "", 1), (3, "二次评审", "评审委员会", "", 1), (4, "领导意见", "领导审批", "", 1), (5, "合同盖章", "合同管理部", "", 1), }; private static readonly string[] StageNames = { "", "意见评审", "意见反馈", "二次评审", "领导意见", "合同盖章" }; public ContractReviewService( ISqlSugarClient db, SqlSugarRepository reviewRep, SqlSugarRepository flowRep, UserManager userManager) { _db = db; _reviewRep = reviewRep; _flowRep = flowRep; _userManager = userManager; } // ══════════════════════════════════════════════════════════════ // 列表 GET /api/Order/contract/list // ══════════════════════════════════════════════════════════════ /// 获取合同评审分页列表 📋 [DisplayName("获取合同评审列表")] [HttpGet("contract/list")] public async Task GetContractReviewList([FromQuery] ContractReviewListInput input) { var pars = new List(); var conditions = new List { "1=1" }; if (!string.IsNullOrWhiteSpace(input.BillNo)) { conditions.Add("BillNo LIKE @BillNo"); pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Title)) { conditions.Add("Title LIKE @Title"); pars.Add(new SugarParameter("@Title", $"%{input.Title.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.CustomerName)) { conditions.Add("CustomerName LIKE @CustomerName"); pars.Add(new SugarParameter("@CustomerName", $"%{input.CustomerName.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.FlowStatus)) { conditions.Add("FlowStatus = @FlowStatus"); pars.Add(new SugarParameter("@FlowStatus", input.FlowStatus.Trim())); } var whereClause = "WHERE " + string.Join(" AND ", conditions); var baseSql = $"SELECT * FROM ado_contract_review {whereClause}"; var offset = (input.Page - 1) * input.PageSize; var total = await _db.Ado.GetIntAsync($"SELECT COUNT(*) FROM ({baseSql}) AS t", pars); var list = await _db.Ado.SqlQueryAsync( $"SELECT * FROM ({baseSql}) AS t ORDER BY t.RecID DESC LIMIT {input.PageSize} OFFSET {offset}", pars); return new { total, page = input.Page, pageSize = input.PageSize, list }; } // ══════════════════════════════════════════════════════════════ // 详情 GET /api/Order/contract/{id} // ══════════════════════════════════════════════════════════════ /// 获取合同评审详情(含流程节点)📋 [DisplayName("获取合同评审详情")] [HttpGet("contract/{id}")] public async Task GetContractReviewDetail(int id) { var review = await _reviewRep.GetFirstAsync(u => u.RecID == id) ?? throw Oops.Oh("合同评审记录不存在"); var flows = await _flowRep.GetListAsync(u => u.ReviewRecID == id); return new { recID = review.RecID, billNo = review.BillNo, title = review.Title, customerName = review.CustomerName, customerNo = review.CustomerNo, salesCompany = review.SalesCompany, salesArea = review.SalesArea, projectCode = review.ProjectCode, crmNo = review.CrmNo, responsibleAccount = review.ResponsibleAccount, responsibleName = review.ResponsibleName, projectStartDate = review.ProjectStartDate?.ToString("yyyy-MM-dd"), currentStage = review.CurrentStage ?? 0, currentDept = review.CurrentDept, projectStatus = review.ProjectStatus, winRate = review.WinRate, expectedOrderMonth = review.ExpectedOrderMonth?.ToString("yyyy-MM"), expectedDeliveryDate = review.ExpectedDeliveryDate?.ToString("yyyy-MM-dd"), projectRequirement = review.ProjectRequirement, remark = review.Remark, flowStatus = review.FlowStatus ?? "draft", createUser = review.CreateUser, createTime = review.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"), updateTime = review.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"), flows = flows.OrderBy(f => f.StageNo).ThenBy(f => f.Seq).Select(f => new { recID = f.RecID, reviewRecID = f.ReviewRecID, reviewBillNo = f.ReviewBillNo, stageNo = f.StageNo, stageName = f.StageName, department = f.Department, deptNo = f.DeptNo, seq = f.Seq, reviewerAccount = f.ReviewerAccount, reviewerName = f.ReviewerName, opinion = f.Opinion, startTime = f.StartTime?.ToString("yyyy-MM-dd HH:mm:ss"), completeTime = f.CompleteTime?.ToString("yyyy-MM-dd HH:mm:ss"), actualDays = f.ActualDays, nodeStatus = f.NodeStatus ?? "pending", }) }; } // ══════════════════════════════════════════════════════════════ // 保存(新增/编辑)POST /api/Order/contract/save // ══════════════════════════════════════════════════════════════ /// 保存合同评审(草稿新增或编辑)📋 [DisplayName("保存合同评审")] [ApiDescriptionSettings(Name = "SaveContractReview"), HttpPost("contract/save")] public async Task SaveContractReview([FromBody] ContractReviewSaveInput input) { var now = DateTime.Now; var user = _userManager.Account ?? "system"; if (input.RecID is null or 0) { // ── 新增:参照 SysJobService.AddJobDetail ── var month = now.ToString("yyyyMM"); var maxSeq = await _db.Ado.GetIntAsync( $"SELECT IFNULL(MAX(CAST(SUBSTRING(BillNo, 9) AS UNSIGNED)), 0) FROM ado_contract_review WHERE BillNo LIKE 'CR{month}%'"); var billNo = $"CR{month}{(maxSeq + 1):D4}"; var entity = new ContractReview { BillNo = billNo, Title = input.Title, CustomerName = input.CustomerName, CustomerNo = input.CustomerNo, SalesCompany = input.SalesCompany, SalesArea = input.SalesArea, ProjectCode = input.ProjectCode, CrmNo = input.CrmNo, ResponsibleAccount = input.ResponsibleAccount, ResponsibleName = input.ResponsibleName, ProjectStartDate = string.IsNullOrWhiteSpace(input.ProjectStartDate) ? null : DateTime.Parse(input.ProjectStartDate), ProjectStatus = input.ProjectStatus, WinRate = input.WinRate, ExpectedOrderMonth = string.IsNullOrWhiteSpace(input.ExpectedOrderMonth) ? null : DateTime.Parse(input.ExpectedOrderMonth + "-01"), ExpectedDeliveryDate = string.IsNullOrWhiteSpace(input.ExpectedDeliveryDate) ? null : DateTime.Parse(input.ExpectedDeliveryDate), ProjectRequirement = input.ProjectRequirement, Remark = input.Remark, FlowStatus = "draft", CurrentStage = 0, CreateUser = user, CreateTime = now, IsActive = 1, }; await _reviewRep.InsertAsync(entity); // 取刚插入行的 RecID var newId = await _db.Ado.GetIntAsync( "SELECT MAX(RecID) FROM ado_contract_review WHERE BillNo = @BillNo", new SugarParameter("@BillNo", billNo)); return new { id = newId, billNo, message = "新增成功" }; } else { // ── 编辑:参照 SysJobService.UpdateJobDetail ── var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID.Value) ?? throw Oops.Oh("合同评审记录不存在"); if (entity.FlowStatus == "reviewing") throw Oops.Oh("审批中的合同评审不允许编辑,如需修改请先驳回"); entity.Title = input.Title; entity.CustomerName = input.CustomerName; entity.CustomerNo = input.CustomerNo; entity.SalesCompany = input.SalesCompany; entity.SalesArea = input.SalesArea; entity.ProjectCode = input.ProjectCode; entity.CrmNo = input.CrmNo; entity.ResponsibleAccount = input.ResponsibleAccount; entity.ResponsibleName = input.ResponsibleName; entity.ProjectStartDate = string.IsNullOrWhiteSpace(input.ProjectStartDate) ? null : DateTime.Parse(input.ProjectStartDate); entity.ProjectStatus = input.ProjectStatus; entity.WinRate = input.WinRate; entity.ExpectedOrderMonth = string.IsNullOrWhiteSpace(input.ExpectedOrderMonth) ? null : DateTime.Parse(input.ExpectedOrderMonth + "-01"); entity.ExpectedDeliveryDate = string.IsNullOrWhiteSpace(input.ExpectedDeliveryDate) ? null : DateTime.Parse(input.ExpectedDeliveryDate); entity.ProjectRequirement = input.ProjectRequirement; entity.Remark = input.Remark; entity.UpdateUser = user; entity.UpdateTime = now; await _reviewRep.UpdateAsync(entity); return new { id = entity.RecID, billNo = entity.BillNo, message = "编辑成功" }; } } // ══════════════════════════════════════════════════════════════ // 删除 POST /api/Order/contract/delete // ══════════════════════════════════════════════════════════════ /// 删除合同评审(审批中不允许删除)📋 [DisplayName("删除合同评审")] [ApiDescriptionSettings(Name = "DeleteContractReview"), HttpPost("contract/delete")] public async Task DeleteContractReview([FromBody] ContractReviewDeleteInput input) { // 参照 SysJobService.DeleteJobDetail:先查再删,同时删关联数据 var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID) ?? throw Oops.Oh("合同评审记录不存在"); if (entity.FlowStatus == "reviewing") throw Oops.Oh("审批中的合同评审不允许删除"); await _flowRep.DeleteAsync(u => u.ReviewRecID == input.RecID); await _reviewRep.DeleteAsync(u => u.RecID == input.RecID); return new { message = "删除成功" }; } // ══════════════════════════════════════════════════════════════ // 提交审批 POST /api/Order/contract/submit // ══════════════════════════════════════════════════════════════ /// 提交合同评审(初始化全部流程节点,启动第一个节点)📋 [DisplayName("提交合同评审")] [ApiDescriptionSettings(Name = "SubmitContractReview"), HttpPost("contract/submit")] public async Task SubmitContractReview([FromBody] ContractReviewSubmitInput input) { var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID) ?? throw Oops.Oh("合同评审记录不存在"); if (entity.FlowStatus == "reviewing") throw Oops.Oh("该合同评审已在审批中"); if (entity.FlowStatus == "completed") throw Oops.Oh("该合同评审已完成,无需再次提交"); var now = DateTime.Now; // 清除旧流程节点(驳回后重新提交时重置所有节点) await _flowRep.DeleteAsync(u => u.ReviewRecID == input.RecID); // 初始化全部流程节点(共8个) foreach (var t in FlowTemplate) { var flow = new ContractReviewFlow { ReviewRecID = entity.RecID, ReviewBillNo = entity.BillNo, StageNo = t.StageNo, StageName = t.StageName, Department = t.Department, DeptNo = t.DeptNo, Seq = t.Seq, NodeStatus = "pending", }; await _flowRep.InsertAsync(flow); } // 启动第一个节点(意见评审-法律事务部) var firstNode = await _flowRep.GetFirstAsync(u => u.ReviewRecID == entity.RecID && u.StageNo == 1 && u.Seq == 1); if (firstNode != null) { firstNode.NodeStatus = "reviewing"; firstNode.StartTime = now; await _flowRep.UpdateAsync(firstNode); } entity.FlowStatus = "reviewing"; entity.CurrentStage = 1; entity.CurrentDept = FlowTemplate[0].Department; entity.UpdateUser = _userManager.Account ?? "system"; entity.UpdateTime = now; await _reviewRep.UpdateAsync(entity); return new { message = "提交成功,流程已启动", currentDept = entity.CurrentDept }; } // ══════════════════════════════════════════════════════════════ // 审批通过 POST /api/Order/contract/approve // ══════════════════════════════════════════════════════════════ /// 审批通过当前节点,自动推进到下一节点 📋 [DisplayName("合同评审审批通过")] [ApiDescriptionSettings(Name = "ApproveContractReview"), HttpPost("contract/approve")] public async Task ApproveContractReview([FromBody] ContractReviewApproveInput input) { var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.ReviewRecID) ?? throw Oops.Oh("合同评审记录不存在"); if (entity.FlowStatus != "reviewing") throw Oops.Oh("当前合同评审不在审批中"); var now = DateTime.Now; var account = string.IsNullOrWhiteSpace(input.ReviewerAccount) ? (_userManager.Account ?? "system") : input.ReviewerAccount; var name = string.IsNullOrWhiteSpace(input.ReviewerName) ? account : input.ReviewerName; // 找当前审批中节点 var currentNode = await _flowRep.GetFirstAsync(u => u.ReviewRecID == input.ReviewRecID && u.NodeStatus == "reviewing") ?? throw Oops.Oh("未找到待审批节点,请刷新后重试"); // 完成当前节点 - 参照 SysJobService.UpdateJobDetail currentNode.ReviewerAccount = account; currentNode.ReviewerName = name; currentNode.Opinion = input.Opinion; currentNode.CompleteTime = now; currentNode.NodeStatus = "approved"; if (currentNode.StartTime.HasValue) currentNode.ActualDays = Math.Round((decimal)(now - currentNode.StartTime.Value).TotalDays, 2); await _flowRep.UpdateAsync(currentNode); // 查找下一节点:先找同一stage内的下一seq var nextNode = await _flowRep.GetFirstAsync(u => u.ReviewRecID == input.ReviewRecID && u.StageNo == currentNode.StageNo && u.Seq == (currentNode.Seq ?? 1) + 1); // 若同stage无下一节点,找下一stage的seq=1 if (nextNode == null) { nextNode = await _flowRep.GetFirstAsync(u => u.ReviewRecID == input.ReviewRecID && u.StageNo == currentNode.StageNo + 1 && u.Seq == 1); } string message; if (nextNode != null) { // 启动下一节点 nextNode.NodeStatus = "reviewing"; nextNode.StartTime = now; await _flowRep.UpdateAsync(nextNode); entity.CurrentStage = nextNode.StageNo; entity.CurrentDept = nextNode.Department; message = $"审批通过,已推进至【{nextNode.StageName} - {nextNode.Department}】"; } else { // 所有节点均通过,流程完成 entity.FlowStatus = "completed"; entity.CurrentStage = 5; entity.CurrentDept = "已完成"; message = "审批完成!所有环节均已通过"; } entity.UpdateUser = account; entity.UpdateTime = now; await _reviewRep.UpdateAsync(entity); return new { message }; } // ══════════════════════════════════════════════════════════════ // 审批驳回 POST /api/Order/contract/reject // ══════════════════════════════════════════════════════════════ /// 驳回当前节点,合同评审退回草稿 📋 [DisplayName("合同评审审批驳回")] [ApiDescriptionSettings(Name = "RejectContractReview"), HttpPost("contract/reject")] public async Task RejectContractReview([FromBody] ContractReviewRejectInput input) { var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.ReviewRecID) ?? throw Oops.Oh("合同评审记录不存在"); if (entity.FlowStatus != "reviewing") throw Oops.Oh("当前合同评审不在审批中"); var now = DateTime.Now; var account = string.IsNullOrWhiteSpace(input.ReviewerAccount) ? (_userManager.Account ?? "system") : input.ReviewerAccount; var name = string.IsNullOrWhiteSpace(input.ReviewerName) ? account : input.ReviewerName; // 完成当前节点(驳回状态) var currentNode = await _flowRep.GetFirstAsync(u => u.ReviewRecID == input.ReviewRecID && u.NodeStatus == "reviewing"); if (currentNode != null) { currentNode.ReviewerAccount = account; currentNode.ReviewerName = name; currentNode.Opinion = input.Opinion; currentNode.CompleteTime = now; currentNode.NodeStatus = "rejected"; if (currentNode.StartTime.HasValue) currentNode.ActualDays = Math.Round((decimal)(now - currentNode.StartTime.Value).TotalDays, 2); await _flowRep.UpdateAsync(currentNode); } entity.FlowStatus = "rejected"; entity.UpdateUser = account; entity.UpdateTime = now; await _reviewRep.UpdateAsync(entity); return new { message = "已驳回,请修改后重新提交" }; } // ══════════════════════════════════════════════════════════════ // 更新节点审批人 POST /api/Order/contract/flow/update // ══════════════════════════════════════════════════════════════ /// 更新流程节点的审批人账号/姓名(在提交前预填)📋 [DisplayName("更新合同评审节点审批人")] [ApiDescriptionSettings(Name = "UpdateContractReviewFlowNode"), HttpPost("contract/flow/update")] public async Task UpdateFlowNode([FromBody] ContractReviewFlowUpdateInput input) { var node = await _flowRep.GetFirstAsync(u => u.RecID == input.FlowRecID) ?? throw Oops.Oh("流程节点不存在"); if (!string.IsNullOrWhiteSpace(input.ReviewerAccount)) node.ReviewerAccount = input.ReviewerAccount; if (!string.IsNullOrWhiteSpace(input.ReviewerName)) node.ReviewerName = input.ReviewerName; if (!string.IsNullOrWhiteSpace(input.Opinion)) node.Opinion = input.Opinion; await _flowRep.UpdateAsync(node); return new { message = "更新成功" }; } // ──────────────── 内部查询结果映射类 ──────────────── private sealed class ContractReviewListRow { public int RecID { get; set; } public string? BillNo { get; set; } public string? Title { get; set; } public string? CustomerName { get; set; } public string? SalesCompany { get; set; } public int? CurrentStage { get; set; } public string? CurrentDept { get; set; } public string? FlowStatus { get; set; } public string? ResponsibleAccount { get; set; } public string? ResponsibleName { get; set; } public DateTime? CreateTime { get; set; } public DateTime? UpdateTime { get; set; } } }