ContractReviewService.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. namespace Admin.NET.Plugin.AiDOP.Order;
  2. /// <summary>
  3. /// 合同评审服务 📋
  4. /// 路由前缀:/api/Order/contract/...
  5. /// 流程:意见评审(法律事务部→技术售前组→综合主计划→试验站)→意见反馈→二次评审→领导意见→合同盖章
  6. /// </summary>
  7. [ApiDescriptionSettings(Order = 250, Description = "合同评审")]
  8. [Route("api/Order")]
  9. [AllowAnonymous]
  10. [NonUnify]
  11. public class ContractReviewService : IDynamicApiController, ITransient
  12. {
  13. private readonly ISqlSugarClient _db;
  14. private readonly SqlSugarRepository<ContractReview> _reviewRep;
  15. private readonly SqlSugarRepository<ContractReviewFlow> _flowRep;
  16. private readonly UserManager _userManager;
  17. // 流程节点模板(stageNo, stageName, department, deptNo, seq)
  18. private static readonly (int StageNo, string StageName, string Department, string DeptNo, int Seq)[] FlowTemplate =
  19. {
  20. (1, "意见评审", "法律事务部", "DEPT_LAW", 1),
  21. (1, "意见评审", "技术售前组", "DEPT_TECH", 2),
  22. (1, "意见评审", "综合主计划", "DEPT_PLAN", 3),
  23. (1, "意见评审", "试验站", "DEPT_TEST", 4),
  24. (2, "意见反馈", "申请部门", "", 1),
  25. (3, "二次评审", "评审委员会", "", 1),
  26. (4, "领导意见", "领导审批", "", 1),
  27. (5, "合同盖章", "合同管理部", "", 1),
  28. };
  29. private static readonly string[] StageNames = { "", "意见评审", "意见反馈", "二次评审", "领导意见", "合同盖章" };
  30. public ContractReviewService(
  31. ISqlSugarClient db,
  32. SqlSugarRepository<ContractReview> reviewRep,
  33. SqlSugarRepository<ContractReviewFlow> flowRep,
  34. UserManager userManager)
  35. {
  36. _db = db;
  37. _reviewRep = reviewRep;
  38. _flowRep = flowRep;
  39. _userManager = userManager;
  40. }
  41. // ══════════════════════════════════════════════════════════════
  42. // 列表 GET /api/Order/contract/list
  43. // ══════════════════════════════════════════════════════════════
  44. /// <summary>获取合同评审分页列表 📋</summary>
  45. [DisplayName("获取合同评审列表")]
  46. [HttpGet("contract/list")]
  47. public async Task<object> GetContractReviewList([FromQuery] ContractReviewListInput input)
  48. {
  49. var pars = new List<SugarParameter>();
  50. var conditions = new List<string> { "1=1" };
  51. if (!string.IsNullOrWhiteSpace(input.BillNo))
  52. {
  53. conditions.Add("BillNo LIKE @BillNo");
  54. pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
  55. }
  56. if (!string.IsNullOrWhiteSpace(input.Title))
  57. {
  58. conditions.Add("Title LIKE @Title");
  59. pars.Add(new SugarParameter("@Title", $"%{input.Title.Trim()}%"));
  60. }
  61. if (!string.IsNullOrWhiteSpace(input.CustomerName))
  62. {
  63. conditions.Add("CustomerName LIKE @CustomerName");
  64. pars.Add(new SugarParameter("@CustomerName", $"%{input.CustomerName.Trim()}%"));
  65. }
  66. if (!string.IsNullOrWhiteSpace(input.FlowStatus))
  67. {
  68. conditions.Add("FlowStatus = @FlowStatus");
  69. pars.Add(new SugarParameter("@FlowStatus", input.FlowStatus.Trim()));
  70. }
  71. var whereClause = "WHERE " + string.Join(" AND ", conditions);
  72. var baseSql = $"SELECT * FROM ado_contract_review {whereClause}";
  73. var offset = (input.Page - 1) * input.PageSize;
  74. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  75. var list = await _db.Ado.SqlQueryAsync<ContractReviewListRow>(
  76. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.RecID DESC LIMIT {input.PageSize} OFFSET {offset}",
  77. pars);
  78. return new { total, page = input.Page, pageSize = input.PageSize, list };
  79. }
  80. // ══════════════════════════════════════════════════════════════
  81. // 详情 GET /api/Order/contract/{id}
  82. // ══════════════════════════════════════════════════════════════
  83. /// <summary>获取合同评审详情(含流程节点)📋</summary>
  84. [DisplayName("获取合同评审详情")]
  85. [HttpGet("contract/{id}")]
  86. public async Task<object> GetContractReviewDetail(int id)
  87. {
  88. var review = await _reviewRep.GetFirstAsync(u => u.RecID == id)
  89. ?? throw Oops.Oh("合同评审记录不存在");
  90. var flows = await _flowRep.GetListAsync(u => u.ReviewRecID == id);
  91. return new
  92. {
  93. recID = review.RecID,
  94. billNo = review.BillNo,
  95. title = review.Title,
  96. customerName = review.CustomerName,
  97. customerNo = review.CustomerNo,
  98. salesCompany = review.SalesCompany,
  99. salesArea = review.SalesArea,
  100. projectCode = review.ProjectCode,
  101. crmNo = review.CrmNo,
  102. responsibleAccount = review.ResponsibleAccount,
  103. responsibleName = review.ResponsibleName,
  104. projectStartDate = review.ProjectStartDate?.ToString("yyyy-MM-dd"),
  105. currentStage = review.CurrentStage ?? 0,
  106. currentDept = review.CurrentDept,
  107. projectStatus = review.ProjectStatus,
  108. winRate = review.WinRate,
  109. expectedOrderMonth = review.ExpectedOrderMonth?.ToString("yyyy-MM"),
  110. expectedDeliveryDate = review.ExpectedDeliveryDate?.ToString("yyyy-MM-dd"),
  111. projectRequirement = review.ProjectRequirement,
  112. remark = review.Remark,
  113. flowStatus = review.FlowStatus ?? "draft",
  114. createUser = review.CreateUser,
  115. createTime = review.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  116. updateTime = review.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  117. flows = flows.OrderBy(f => f.StageNo).ThenBy(f => f.Seq).Select(f => new
  118. {
  119. recID = f.RecID,
  120. reviewRecID = f.ReviewRecID,
  121. reviewBillNo = f.ReviewBillNo,
  122. stageNo = f.StageNo,
  123. stageName = f.StageName,
  124. department = f.Department,
  125. deptNo = f.DeptNo,
  126. seq = f.Seq,
  127. reviewerAccount = f.ReviewerAccount,
  128. reviewerName = f.ReviewerName,
  129. opinion = f.Opinion,
  130. startTime = f.StartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  131. completeTime = f.CompleteTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  132. actualDays = f.ActualDays,
  133. nodeStatus = f.NodeStatus ?? "pending",
  134. })
  135. };
  136. }
  137. // ══════════════════════════════════════════════════════════════
  138. // 保存(新增/编辑)POST /api/Order/contract/save
  139. // ══════════════════════════════════════════════════════════════
  140. /// <summary>保存合同评审(草稿新增或编辑)📋</summary>
  141. [DisplayName("保存合同评审")]
  142. [ApiDescriptionSettings(Name = "SaveContractReview"), HttpPost("contract/save")]
  143. public async Task<object> SaveContractReview([FromBody] ContractReviewSaveInput input)
  144. {
  145. var now = DateTime.Now;
  146. var user = _userManager.Account ?? "system";
  147. if (input.RecID is null or 0)
  148. {
  149. // ── 新增:参照 SysJobService.AddJobDetail ──
  150. var month = now.ToString("yyyyMM");
  151. var maxSeq = await _db.Ado.GetIntAsync(
  152. $"SELECT IFNULL(MAX(CAST(SUBSTRING(BillNo, 9) AS UNSIGNED)), 0) FROM ado_contract_review WHERE BillNo LIKE 'CR{month}%'");
  153. var billNo = $"CR{month}{(maxSeq + 1):D4}";
  154. var entity = new ContractReview
  155. {
  156. BillNo = billNo,
  157. Title = input.Title,
  158. CustomerName = input.CustomerName,
  159. CustomerNo = input.CustomerNo,
  160. SalesCompany = input.SalesCompany,
  161. SalesArea = input.SalesArea,
  162. ProjectCode = input.ProjectCode,
  163. CrmNo = input.CrmNo,
  164. ResponsibleAccount = input.ResponsibleAccount,
  165. ResponsibleName = input.ResponsibleName,
  166. ProjectStartDate = string.IsNullOrWhiteSpace(input.ProjectStartDate) ? null : DateTime.Parse(input.ProjectStartDate),
  167. ProjectStatus = input.ProjectStatus,
  168. WinRate = input.WinRate,
  169. ExpectedOrderMonth = string.IsNullOrWhiteSpace(input.ExpectedOrderMonth) ? null : DateTime.Parse(input.ExpectedOrderMonth + "-01"),
  170. ExpectedDeliveryDate = string.IsNullOrWhiteSpace(input.ExpectedDeliveryDate) ? null : DateTime.Parse(input.ExpectedDeliveryDate),
  171. ProjectRequirement = input.ProjectRequirement,
  172. Remark = input.Remark,
  173. FlowStatus = "draft",
  174. CurrentStage = 0,
  175. CreateUser = user,
  176. CreateTime = now,
  177. IsActive = 1,
  178. };
  179. await _reviewRep.InsertAsync(entity);
  180. // 取刚插入行的 RecID
  181. var newId = await _db.Ado.GetIntAsync(
  182. "SELECT MAX(RecID) FROM ado_contract_review WHERE BillNo = @BillNo",
  183. new SugarParameter("@BillNo", billNo));
  184. return new { id = newId, billNo, message = "新增成功" };
  185. }
  186. else
  187. {
  188. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  189. var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID.Value)
  190. ?? throw Oops.Oh("合同评审记录不存在");
  191. if (entity.FlowStatus == "reviewing")
  192. throw Oops.Oh("审批中的合同评审不允许编辑,如需修改请先驳回");
  193. entity.Title = input.Title;
  194. entity.CustomerName = input.CustomerName;
  195. entity.CustomerNo = input.CustomerNo;
  196. entity.SalesCompany = input.SalesCompany;
  197. entity.SalesArea = input.SalesArea;
  198. entity.ProjectCode = input.ProjectCode;
  199. entity.CrmNo = input.CrmNo;
  200. entity.ResponsibleAccount = input.ResponsibleAccount;
  201. entity.ResponsibleName = input.ResponsibleName;
  202. entity.ProjectStartDate = string.IsNullOrWhiteSpace(input.ProjectStartDate) ? null : DateTime.Parse(input.ProjectStartDate);
  203. entity.ProjectStatus = input.ProjectStatus;
  204. entity.WinRate = input.WinRate;
  205. entity.ExpectedOrderMonth = string.IsNullOrWhiteSpace(input.ExpectedOrderMonth) ? null : DateTime.Parse(input.ExpectedOrderMonth + "-01");
  206. entity.ExpectedDeliveryDate = string.IsNullOrWhiteSpace(input.ExpectedDeliveryDate) ? null : DateTime.Parse(input.ExpectedDeliveryDate);
  207. entity.ProjectRequirement = input.ProjectRequirement;
  208. entity.Remark = input.Remark;
  209. entity.UpdateUser = user;
  210. entity.UpdateTime = now;
  211. await _reviewRep.UpdateAsync(entity);
  212. return new { id = entity.RecID, billNo = entity.BillNo, message = "编辑成功" };
  213. }
  214. }
  215. // ══════════════════════════════════════════════════════════════
  216. // 删除 POST /api/Order/contract/delete
  217. // ══════════════════════════════════════════════════════════════
  218. /// <summary>删除合同评审(审批中不允许删除)📋</summary>
  219. [DisplayName("删除合同评审")]
  220. [ApiDescriptionSettings(Name = "DeleteContractReview"), HttpPost("contract/delete")]
  221. public async Task<object> DeleteContractReview([FromBody] ContractReviewDeleteInput input)
  222. {
  223. // 参照 SysJobService.DeleteJobDetail:先查再删,同时删关联数据
  224. var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID)
  225. ?? throw Oops.Oh("合同评审记录不存在");
  226. if (entity.FlowStatus == "reviewing")
  227. throw Oops.Oh("审批中的合同评审不允许删除");
  228. await _flowRep.DeleteAsync(u => u.ReviewRecID == input.RecID);
  229. await _reviewRep.DeleteAsync(u => u.RecID == input.RecID);
  230. return new { message = "删除成功" };
  231. }
  232. // ══════════════════════════════════════════════════════════════
  233. // 提交审批 POST /api/Order/contract/submit
  234. // ══════════════════════════════════════════════════════════════
  235. /// <summary>提交合同评审(初始化全部流程节点,启动第一个节点)📋</summary>
  236. [DisplayName("提交合同评审")]
  237. [ApiDescriptionSettings(Name = "SubmitContractReview"), HttpPost("contract/submit")]
  238. public async Task<object> SubmitContractReview([FromBody] ContractReviewSubmitInput input)
  239. {
  240. var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID)
  241. ?? throw Oops.Oh("合同评审记录不存在");
  242. if (entity.FlowStatus == "reviewing")
  243. throw Oops.Oh("该合同评审已在审批中");
  244. if (entity.FlowStatus == "completed")
  245. throw Oops.Oh("该合同评审已完成,无需再次提交");
  246. var now = DateTime.Now;
  247. // 清除旧流程节点(驳回后重新提交时重置所有节点)
  248. await _flowRep.DeleteAsync(u => u.ReviewRecID == input.RecID);
  249. // 初始化全部流程节点(共8个)
  250. foreach (var t in FlowTemplate)
  251. {
  252. var flow = new ContractReviewFlow
  253. {
  254. ReviewRecID = entity.RecID,
  255. ReviewBillNo = entity.BillNo,
  256. StageNo = t.StageNo,
  257. StageName = t.StageName,
  258. Department = t.Department,
  259. DeptNo = t.DeptNo,
  260. Seq = t.Seq,
  261. NodeStatus = "pending",
  262. };
  263. await _flowRep.InsertAsync(flow);
  264. }
  265. // 启动第一个节点(意见评审-法律事务部)
  266. var firstNode = await _flowRep.GetFirstAsync(u =>
  267. u.ReviewRecID == entity.RecID && u.StageNo == 1 && u.Seq == 1);
  268. if (firstNode != null)
  269. {
  270. firstNode.NodeStatus = "reviewing";
  271. firstNode.StartTime = now;
  272. await _flowRep.UpdateAsync(firstNode);
  273. }
  274. entity.FlowStatus = "reviewing";
  275. entity.CurrentStage = 1;
  276. entity.CurrentDept = FlowTemplate[0].Department;
  277. entity.UpdateUser = _userManager.Account ?? "system";
  278. entity.UpdateTime = now;
  279. await _reviewRep.UpdateAsync(entity);
  280. return new { message = "提交成功,流程已启动", currentDept = entity.CurrentDept };
  281. }
  282. // ══════════════════════════════════════════════════════════════
  283. // 审批通过 POST /api/Order/contract/approve
  284. // ══════════════════════════════════════════════════════════════
  285. /// <summary>审批通过当前节点,自动推进到下一节点 📋</summary>
  286. [DisplayName("合同评审审批通过")]
  287. [ApiDescriptionSettings(Name = "ApproveContractReview"), HttpPost("contract/approve")]
  288. public async Task<object> ApproveContractReview([FromBody] ContractReviewApproveInput input)
  289. {
  290. var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.ReviewRecID)
  291. ?? throw Oops.Oh("合同评审记录不存在");
  292. if (entity.FlowStatus != "reviewing")
  293. throw Oops.Oh("当前合同评审不在审批中");
  294. var now = DateTime.Now;
  295. var account = string.IsNullOrWhiteSpace(input.ReviewerAccount) ? (_userManager.Account ?? "system") : input.ReviewerAccount;
  296. var name = string.IsNullOrWhiteSpace(input.ReviewerName) ? account : input.ReviewerName;
  297. // 找当前审批中节点
  298. var currentNode = await _flowRep.GetFirstAsync(u =>
  299. u.ReviewRecID == input.ReviewRecID && u.NodeStatus == "reviewing")
  300. ?? throw Oops.Oh("未找到待审批节点,请刷新后重试");
  301. // 完成当前节点 - 参照 SysJobService.UpdateJobDetail
  302. currentNode.ReviewerAccount = account;
  303. currentNode.ReviewerName = name;
  304. currentNode.Opinion = input.Opinion;
  305. currentNode.CompleteTime = now;
  306. currentNode.NodeStatus = "approved";
  307. if (currentNode.StartTime.HasValue)
  308. currentNode.ActualDays = Math.Round((decimal)(now - currentNode.StartTime.Value).TotalDays, 2);
  309. await _flowRep.UpdateAsync(currentNode);
  310. // 查找下一节点:先找同一stage内的下一seq
  311. var nextNode = await _flowRep.GetFirstAsync(u =>
  312. u.ReviewRecID == input.ReviewRecID
  313. && u.StageNo == currentNode.StageNo
  314. && u.Seq == (currentNode.Seq ?? 1) + 1);
  315. // 若同stage无下一节点,找下一stage的seq=1
  316. if (nextNode == null)
  317. {
  318. nextNode = await _flowRep.GetFirstAsync(u =>
  319. u.ReviewRecID == input.ReviewRecID
  320. && u.StageNo == currentNode.StageNo + 1
  321. && u.Seq == 1);
  322. }
  323. string message;
  324. if (nextNode != null)
  325. {
  326. // 启动下一节点
  327. nextNode.NodeStatus = "reviewing";
  328. nextNode.StartTime = now;
  329. await _flowRep.UpdateAsync(nextNode);
  330. entity.CurrentStage = nextNode.StageNo;
  331. entity.CurrentDept = nextNode.Department;
  332. message = $"审批通过,已推进至【{nextNode.StageName} - {nextNode.Department}】";
  333. }
  334. else
  335. {
  336. // 所有节点均通过,流程完成
  337. entity.FlowStatus = "completed";
  338. entity.CurrentStage = 5;
  339. entity.CurrentDept = "已完成";
  340. message = "审批完成!所有环节均已通过";
  341. }
  342. entity.UpdateUser = account;
  343. entity.UpdateTime = now;
  344. await _reviewRep.UpdateAsync(entity);
  345. return new { message };
  346. }
  347. // ══════════════════════════════════════════════════════════════
  348. // 审批驳回 POST /api/Order/contract/reject
  349. // ══════════════════════════════════════════════════════════════
  350. /// <summary>驳回当前节点,合同评审退回草稿 📋</summary>
  351. [DisplayName("合同评审审批驳回")]
  352. [ApiDescriptionSettings(Name = "RejectContractReview"), HttpPost("contract/reject")]
  353. public async Task<object> RejectContractReview([FromBody] ContractReviewRejectInput input)
  354. {
  355. var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.ReviewRecID)
  356. ?? throw Oops.Oh("合同评审记录不存在");
  357. if (entity.FlowStatus != "reviewing")
  358. throw Oops.Oh("当前合同评审不在审批中");
  359. var now = DateTime.Now;
  360. var account = string.IsNullOrWhiteSpace(input.ReviewerAccount) ? (_userManager.Account ?? "system") : input.ReviewerAccount;
  361. var name = string.IsNullOrWhiteSpace(input.ReviewerName) ? account : input.ReviewerName;
  362. // 完成当前节点(驳回状态)
  363. var currentNode = await _flowRep.GetFirstAsync(u =>
  364. u.ReviewRecID == input.ReviewRecID && u.NodeStatus == "reviewing");
  365. if (currentNode != null)
  366. {
  367. currentNode.ReviewerAccount = account;
  368. currentNode.ReviewerName = name;
  369. currentNode.Opinion = input.Opinion;
  370. currentNode.CompleteTime = now;
  371. currentNode.NodeStatus = "rejected";
  372. if (currentNode.StartTime.HasValue)
  373. currentNode.ActualDays = Math.Round((decimal)(now - currentNode.StartTime.Value).TotalDays, 2);
  374. await _flowRep.UpdateAsync(currentNode);
  375. }
  376. entity.FlowStatus = "rejected";
  377. entity.UpdateUser = account;
  378. entity.UpdateTime = now;
  379. await _reviewRep.UpdateAsync(entity);
  380. return new { message = "已驳回,请修改后重新提交" };
  381. }
  382. // ══════════════════════════════════════════════════════════════
  383. // 更新节点审批人 POST /api/Order/contract/flow/update
  384. // ══════════════════════════════════════════════════════════════
  385. /// <summary>更新流程节点的审批人账号/姓名(在提交前预填)📋</summary>
  386. [DisplayName("更新合同评审节点审批人")]
  387. [ApiDescriptionSettings(Name = "UpdateContractReviewFlowNode"), HttpPost("contract/flow/update")]
  388. public async Task<object> UpdateFlowNode([FromBody] ContractReviewFlowUpdateInput input)
  389. {
  390. var node = await _flowRep.GetFirstAsync(u => u.RecID == input.FlowRecID)
  391. ?? throw Oops.Oh("流程节点不存在");
  392. if (!string.IsNullOrWhiteSpace(input.ReviewerAccount))
  393. node.ReviewerAccount = input.ReviewerAccount;
  394. if (!string.IsNullOrWhiteSpace(input.ReviewerName))
  395. node.ReviewerName = input.ReviewerName;
  396. if (!string.IsNullOrWhiteSpace(input.Opinion))
  397. node.Opinion = input.Opinion;
  398. await _flowRep.UpdateAsync(node);
  399. return new { message = "更新成功" };
  400. }
  401. // ──────────────── 内部查询结果映射类 ────────────────
  402. private sealed class ContractReviewListRow
  403. {
  404. public int RecID { get; set; }
  405. public string? BillNo { get; set; }
  406. public string? Title { get; set; }
  407. public string? CustomerName { get; set; }
  408. public string? SalesCompany { get; set; }
  409. public int? CurrentStage { get; set; }
  410. public string? CurrentDept { get; set; }
  411. public string? FlowStatus { get; set; }
  412. public string? ResponsibleAccount { get; set; }
  413. public string? ResponsibleName { get; set; }
  414. public DateTime? CreateTime { get; set; }
  415. public DateTime? UpdateTime { get; set; }
  416. }
  417. }