ContractReviewService.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. namespace Admin.NET.Plugin.AiDOP.Order;
  2. /// <summary>
  3. /// 合同评审服务 📋
  4. /// 路由前缀:/api/Order/contract/...
  5. /// 审批:由审批流插件(BizType=CONTRACT_REVIEW)+ 前端 ApprovalPanel 发起与办理;本服务仅保留列表/详情/保存/删除。
  6. ///
  7. /// 【租户安全边界】需认证访问(已移除类级 <c>[AllowAnonymous]</c>);租户一律经
  8. /// <c>AidopTenantScope.ResolveOrThrow</c> 从认证后 JWT 解析,无有效租户即拒绝,
  9. /// 不读前端 tenantId、无默认回退(原实现:类级匿名 + 直接取 <c>_userManager.TenantId</c>,
  10. /// 匿名时 TenantId=0 使租户过滤失效/查不到数据)。
  11. /// </summary>
  12. [ApiDescriptionSettings(Order = 250, Description = "合同评审")]
  13. [Route("api/Order")]
  14. [NonUnify]
  15. public class ContractReviewService : IDynamicApiController, ITransient
  16. {
  17. private readonly ISqlSugarClient _db;
  18. private readonly SqlSugarRepository<ContractReview> _reviewRep;
  19. private readonly SqlSugarRepository<ContractReviewFlow> _flowRep;
  20. private readonly UserManager _userManager;
  21. public ContractReviewService(
  22. ISqlSugarClient db,
  23. SqlSugarRepository<ContractReview> reviewRep,
  24. SqlSugarRepository<ContractReviewFlow> flowRep,
  25. UserManager userManager)
  26. {
  27. _db = db;
  28. _reviewRep = reviewRep;
  29. _flowRep = flowRep;
  30. _userManager = userManager;
  31. }
  32. // ══════════════════════════════════════════════════════════════
  33. // 列表 GET /api/Order/contract/list
  34. // ══════════════════════════════════════════════════════════════
  35. /// <summary>获取合同评审分页列表 📋</summary>
  36. [DisplayName("获取合同评审列表")]
  37. [HttpGet("contract/list")]
  38. public async Task<object> GetContractReviewList([FromQuery] ContractReviewListInput input)
  39. {
  40. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  41. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  42. var conditions = new List<string> { "tenant_id = @TenantId" };
  43. if (!string.IsNullOrWhiteSpace(input.BillNo))
  44. {
  45. conditions.Add("BillNo LIKE @BillNo");
  46. pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
  47. }
  48. if (!string.IsNullOrWhiteSpace(input.Title))
  49. {
  50. conditions.Add("Title LIKE @Title");
  51. pars.Add(new SugarParameter("@Title", $"%{input.Title.Trim()}%"));
  52. }
  53. if (!string.IsNullOrWhiteSpace(input.CustomerName))
  54. {
  55. conditions.Add("CustomerName LIKE @CustomerName");
  56. pars.Add(new SugarParameter("@CustomerName", $"%{input.CustomerName.Trim()}%"));
  57. }
  58. if (!string.IsNullOrWhiteSpace(input.FlowStatus))
  59. {
  60. conditions.Add("FlowStatus = @FlowStatus");
  61. pars.Add(new SugarParameter("@FlowStatus", input.FlowStatus.Trim()));
  62. }
  63. var whereClause = "WHERE " + string.Join(" AND ", conditions);
  64. var baseSql = $"SELECT * FROM ado_contract_review {whereClause}";
  65. var offset = (input.Page - 1) * input.PageSize;
  66. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  67. var list = await _db.Ado.SqlQueryAsync<ContractReviewListRow>(
  68. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.RecID DESC LIMIT {input.PageSize} OFFSET {offset}",
  69. pars);
  70. return new { total, page = input.Page, pageSize = input.PageSize, list };
  71. }
  72. // ══════════════════════════════════════════════════════════════
  73. // 详情 GET /api/Order/contract/{id}
  74. // ══════════════════════════════════════════════════════════════
  75. /// <summary>获取合同评审详情(含流程节点)📋</summary>
  76. [DisplayName("获取合同评审详情")]
  77. [HttpGet("contract/{id}")]
  78. public async Task<object> GetContractReviewDetail(int id)
  79. {
  80. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  81. var review = await _reviewRep.GetFirstAsync(u => u.RecID == id && u.TenantId == tenantId)
  82. ?? throw Oops.Oh("合同评审记录不存在");
  83. var flows = await _flowRep.GetListAsync(u => u.ReviewRecID == id && u.TenantId == tenantId);
  84. return new
  85. {
  86. recID = review.RecID,
  87. billNo = review.BillNo,
  88. title = review.Title,
  89. customerName = review.CustomerName,
  90. customerNo = review.CustomerNo,
  91. salesCompany = review.SalesCompany,
  92. salesArea = review.SalesArea,
  93. projectCode = review.ProjectCode,
  94. crmNo = review.CrmNo,
  95. responsibleAccount = review.ResponsibleAccount,
  96. responsibleName = review.ResponsibleName,
  97. projectStartDate = review.ProjectStartDate?.ToString("yyyy-MM-dd"),
  98. currentStage = review.CurrentStage ?? 0,
  99. currentDept = review.CurrentDept,
  100. projectStatus = review.ProjectStatus,
  101. winRate = review.WinRate,
  102. expectedOrderMonth = review.ExpectedOrderMonth?.ToString("yyyy-MM"),
  103. expectedDeliveryDate = review.ExpectedDeliveryDate?.ToString("yyyy-MM-dd"),
  104. projectRequirement = review.ProjectRequirement,
  105. remark = review.Remark,
  106. flowStatus = review.FlowStatus ?? "draft",
  107. createUser = review.CreateUser,
  108. createTime = review.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  109. updateTime = review.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  110. flows = flows.OrderBy(f => f.StageNo).ThenBy(f => f.Seq).Select(f => new
  111. {
  112. recID = f.RecID,
  113. reviewRecID = f.ReviewRecID,
  114. reviewBillNo = f.ReviewBillNo,
  115. stageNo = f.StageNo,
  116. stageName = f.StageName,
  117. department = f.Department,
  118. deptNo = f.DeptNo,
  119. seq = f.Seq,
  120. reviewerAccount = f.ReviewerAccount,
  121. reviewerName = f.ReviewerName,
  122. opinion = f.Opinion,
  123. startTime = f.StartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  124. completeTime = f.CompleteTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  125. actualDays = f.ActualDays,
  126. nodeStatus = f.NodeStatus ?? "pending",
  127. })
  128. };
  129. }
  130. // ══════════════════════════════════════════════════════════════
  131. // 保存(新增/编辑)POST /api/Order/contract/save
  132. // ══════════════════════════════════════════════════════════════
  133. /// <summary>保存合同评审(草稿新增或编辑)📋</summary>
  134. [DisplayName("保存合同评审")]
  135. [ApiDescriptionSettings(Name = "SaveContractReview"), HttpPost("contract/save")]
  136. public async Task<object> SaveContractReview([FromBody] ContractReviewSaveInput input)
  137. {
  138. // 必填校验
  139. if (string.IsNullOrWhiteSpace(input.ProjectCode))
  140. throw Oops.Oh("合同编号不能为空");
  141. if (string.IsNullOrWhiteSpace(input.ProjectStartDate))
  142. throw Oops.Oh("合同开始时间不能为空");
  143. if (string.IsNullOrWhiteSpace(input.SalesCompany))
  144. throw Oops.Oh("销售公司不能为空");
  145. if (string.IsNullOrWhiteSpace(input.CustomerName))
  146. throw Oops.Oh("客户名称不能为空");
  147. if (string.IsNullOrWhiteSpace(input.Title))
  148. throw Oops.Oh("合同标题不能为空");
  149. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  150. // 合同编号唯一性校验
  151. var existingId = input.RecID ?? 0;
  152. var codeExists = await _reviewRep.IsAnyAsync(u =>
  153. u.ProjectCode == input.ProjectCode.Trim()
  154. && u.TenantId == tenantId
  155. && u.RecID != existingId);
  156. if (codeExists)
  157. throw Oops.Oh($"合同编号「{input.ProjectCode.Trim()}」已存在,请使用其他编号");
  158. var now = DateTime.Now;
  159. var user = _userManager.Account ?? "system";
  160. if (input.RecID is null or 0)
  161. {
  162. // ── 新增:参照 SysJobService.AddJobDetail ──
  163. var month = now.ToString("yyyyMM");
  164. var maxSeq = await _db.Ado.GetIntAsync(
  165. $"SELECT IFNULL(MAX(CAST(SUBSTRING(BillNo, 9) AS UNSIGNED)), 0) FROM ado_contract_review WHERE BillNo LIKE 'CR{month}%' AND tenant_id = @TenantId",
  166. new SugarParameter("@TenantId", tenantId));
  167. var billNo = $"CR{month}{(maxSeq + 1):D4}";
  168. var entity = new ContractReview
  169. {
  170. BillNo = billNo,
  171. Title = input.Title,
  172. CustomerName = input.CustomerName,
  173. CustomerNo = input.CustomerNo,
  174. SalesCompany = input.SalesCompany,
  175. SalesArea = input.SalesArea,
  176. ProjectCode = input.ProjectCode,
  177. CrmNo = input.CrmNo,
  178. ResponsibleAccount = input.ResponsibleAccount,
  179. ResponsibleName = input.ResponsibleName,
  180. ProjectStartDate = string.IsNullOrWhiteSpace(input.ProjectStartDate) ? null : DateTime.Parse(input.ProjectStartDate),
  181. ProjectStatus = input.ProjectStatus,
  182. WinRate = input.WinRate,
  183. ExpectedOrderMonth = string.IsNullOrWhiteSpace(input.ExpectedOrderMonth) ? null : DateTime.Parse(input.ExpectedOrderMonth + "-01"),
  184. ExpectedDeliveryDate = string.IsNullOrWhiteSpace(input.ExpectedDeliveryDate) ? null : DateTime.Parse(input.ExpectedDeliveryDate),
  185. ProjectRequirement = input.ProjectRequirement,
  186. Remark = input.Remark,
  187. FlowStatus = "draft",
  188. CurrentStage = 0,
  189. CreateUser = user,
  190. CreateTime = now,
  191. IsActive = 1,
  192. TenantId = tenantId,
  193. };
  194. await _reviewRep.InsertAsync(entity);
  195. // 取刚插入行的 RecID
  196. var newId = await _db.Ado.GetIntAsync(
  197. "SELECT MAX(RecID) FROM ado_contract_review WHERE BillNo = @BillNo",
  198. new SugarParameter("@BillNo", billNo));
  199. return new { id = newId, billNo, message = "新增成功" };
  200. }
  201. else
  202. {
  203. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  204. var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID.Value && u.TenantId == tenantId)
  205. ?? throw Oops.Oh("合同评审记录不存在");
  206. if (entity.FlowStatus == "reviewing")
  207. throw Oops.Oh("审批中的合同评审不允许编辑,如需修改请先驳回");
  208. entity.Title = input.Title;
  209. entity.CustomerName = input.CustomerName;
  210. entity.CustomerNo = input.CustomerNo;
  211. entity.SalesCompany = input.SalesCompany;
  212. entity.SalesArea = input.SalesArea;
  213. entity.ProjectCode = input.ProjectCode;
  214. entity.CrmNo = input.CrmNo;
  215. entity.ResponsibleAccount = input.ResponsibleAccount;
  216. entity.ResponsibleName = input.ResponsibleName;
  217. entity.ProjectStartDate = string.IsNullOrWhiteSpace(input.ProjectStartDate) ? null : DateTime.Parse(input.ProjectStartDate);
  218. entity.ProjectStatus = input.ProjectStatus;
  219. entity.WinRate = input.WinRate;
  220. entity.ExpectedOrderMonth = string.IsNullOrWhiteSpace(input.ExpectedOrderMonth) ? null : DateTime.Parse(input.ExpectedOrderMonth + "-01");
  221. entity.ExpectedDeliveryDate = string.IsNullOrWhiteSpace(input.ExpectedDeliveryDate) ? null : DateTime.Parse(input.ExpectedDeliveryDate);
  222. entity.ProjectRequirement = input.ProjectRequirement;
  223. entity.Remark = input.Remark;
  224. entity.UpdateUser = user;
  225. entity.UpdateTime = now;
  226. await _reviewRep.UpdateAsync(entity);
  227. return new { id = entity.RecID, billNo = entity.BillNo, message = "编辑成功" };
  228. }
  229. }
  230. // ══════════════════════════════════════════════════════════════
  231. // 删除 POST /api/Order/contract/delete
  232. // ══════════════════════════════════════════════════════════════
  233. /// <summary>删除合同评审(审批中不允许删除)📋</summary>
  234. [DisplayName("删除合同评审")]
  235. [ApiDescriptionSettings(Name = "DeleteContractReview"), HttpPost("contract/delete")]
  236. public async Task<object> DeleteContractReview([FromBody] ContractReviewDeleteInput input)
  237. {
  238. // 参照 SysJobService.DeleteJobDetail:先查再删,同时删关联数据
  239. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  240. var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID && u.TenantId == tenantId)
  241. ?? throw Oops.Oh("合同评审记录不存在");
  242. if (entity.FlowStatus == "reviewing")
  243. throw Oops.Oh("审批中的合同评审不允许删除");
  244. await _flowRep.DeleteAsync(u => u.ReviewRecID == input.RecID);
  245. await _reviewRep.DeleteAsync(u => u.RecID == input.RecID);
  246. return new { message = "删除成功" };
  247. }
  248. // ══════════════════════════════════════════════════════════════
  249. // 合同编号唯一性校验 GET /api/Order/contract/check-project-code
  250. // ══════════════════════════════════════════════════════════════
  251. /// <summary>校验合同编号唯一性 📋</summary>
  252. [DisplayName("校验合同编号唯一性")]
  253. [HttpGet("contract/check-project-code")]
  254. public async Task<object> CheckProjectCode([FromQuery] string projectCode, [FromQuery] int excludeId = 0)
  255. {
  256. if (string.IsNullOrWhiteSpace(projectCode))
  257. return new { exists = false };
  258. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  259. var exists = await _reviewRep.IsAnyAsync(u =>
  260. u.ProjectCode == projectCode.Trim()
  261. && u.TenantId == tenantId
  262. && u.RecID != excludeId);
  263. return new { exists };
  264. }
  265. // ══════════════════════════════════════════════════════════════
  266. // 合同负责人下拉框 GET /api/Order/contract/responsible-users
  267. // ══════════════════════════════════════════════════════════════
  268. /// <summary>获取合同负责人下拉选项(当前租户用户列表)📋</summary>
  269. [DisplayName("获取合同负责人下拉选项")]
  270. [HttpGet("contract/responsible-users")]
  271. public async Task<List<object>> GetResponsibleUsers([FromQuery] string? keyword)
  272. {
  273. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  274. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  275. var conditions = new List<string> { "TenantId = @TenantId" };
  276. if (!string.IsNullOrWhiteSpace(keyword))
  277. {
  278. conditions.Add("(Account LIKE @Keyword OR RealName LIKE @Keyword)");
  279. pars.Add(new SugarParameter("@Keyword", $"%{keyword.Trim()}%"));
  280. }
  281. var whereClause = "WHERE " + string.Join(" AND ", conditions);
  282. var sql = $"SELECT Account, RealName FROM SysUser {whereClause} ORDER BY Account LIMIT 100";
  283. var rows = await _db.Ado.SqlQueryAsync<ResponsibleUserRow>(sql, pars);
  284. return rows.Select(r => (object)new { value = r.Account, label = r.RealName }).ToList();
  285. }
  286. // ──────────────── 内部查询结果映射类 ────────────────
  287. private sealed class ResponsibleUserRow
  288. {
  289. public string? Account { get; set; }
  290. public string? RealName { get; set; }
  291. }
  292. private sealed class ContractReviewListRow
  293. {
  294. public int RecID { get; set; }
  295. public string? BillNo { get; set; }
  296. public string? Title { get; set; }
  297. public string? CustomerName { get; set; }
  298. public string? SalesCompany { get; set; }
  299. public int? CurrentStage { get; set; }
  300. public string? CurrentDept { get; set; }
  301. public string? FlowStatus { get; set; }
  302. public string? ResponsibleAccount { get; set; }
  303. public string? ResponsibleName { get; set; }
  304. public DateTime? CreateTime { get; set; }
  305. public DateTime? UpdateTime { get; set; }
  306. }
  307. }