S8TaskFlowService.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  4. using Admin.NET.Plugin.ApprovalFlow;
  5. using Admin.NET.Plugin.ApprovalFlow.Service;
  6. using Microsoft.Extensions.Logging;
  7. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  8. public class S8TaskFlowService : ITransient
  9. {
  10. private readonly SqlSugarRepository<AdoS8Exception> _rep;
  11. private readonly SqlSugarRepository<AdoS8ExceptionTimeline> _timelineRep;
  12. private readonly SqlSugarRepository<AdoS0EmployeeMaster> _employeeRep;
  13. private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
  14. private readonly SqlSugarRepository<ApprovalFlowInstance> _flowInstanceRep;
  15. private readonly SqlSugarRepository<ApprovalFlowTask> _flowTaskRep;
  16. private readonly FlowEngineService _flowEngine;
  17. private readonly UserManager _userManager;
  18. private readonly ILogger<S8TaskFlowService> _logger;
  19. public S8TaskFlowService(
  20. SqlSugarRepository<AdoS8Exception> rep,
  21. SqlSugarRepository<AdoS8ExceptionTimeline> timelineRep,
  22. SqlSugarRepository<AdoS0EmployeeMaster> employeeRep,
  23. SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
  24. SqlSugarRepository<ApprovalFlowInstance> flowInstanceRep,
  25. SqlSugarRepository<ApprovalFlowTask> flowTaskRep,
  26. FlowEngineService flowEngine,
  27. UserManager userManager,
  28. ILogger<S8TaskFlowService> logger)
  29. {
  30. _rep = rep;
  31. _timelineRep = timelineRep;
  32. _employeeRep = employeeRep;
  33. _deptRep = deptRep;
  34. _flowInstanceRep = flowInstanceRep;
  35. _flowTaskRep = flowTaskRep;
  36. _flowEngine = flowEngine;
  37. _userManager = userManager;
  38. _logger = logger;
  39. }
  40. public async Task<AdoS8Exception> ClaimAsync(long id, long tenantId, long factoryId, long assigneeId, string? remark)
  41. {
  42. if (assigneeId <= 0) throw new S8BizException("认领需指定处理人 AssigneeId");
  43. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  44. if (!S8StatusRules.IsAllowedTransition(e.Status, "ASSIGNED"))
  45. throw new S8BizException($"状态 {e.Status} 不可认领");
  46. var fromStatus = e.Status;
  47. e.Status = "ASSIGNED";
  48. e.AssigneeId = assigneeId;
  49. e.AssignedAt = DateTime.Now;
  50. e.UpdatedAt = DateTime.Now;
  51. // S8-ASSIGNEE-RESP-DEPT-SYNC-1(P0-B-2):派单同步处理部门 = 处理人所属部门 RecID。
  52. // 兜底策略:解析失败保留原 ResponsibleDeptId,仅 warn;避免误覆盖既有归属。
  53. var resolvedDeptId = await ResolveEmployeeResponsibleDeptIdAsync(assigneeId, factoryId);
  54. if (resolvedDeptId.HasValue)
  55. {
  56. e.ResponsibleDeptId = resolvedDeptId.Value;
  57. }
  58. else
  59. {
  60. _logger.LogWarning(
  61. "s8_claim_dept_unresolved exceptionId={Id} assigneeId={AssigneeId} factoryId={FactoryId}",
  62. e.Id, assigneeId, factoryId);
  63. }
  64. await _rep.AsTenant().UseTranAsync(async () =>
  65. {
  66. await _rep.UpdateAsync(e);
  67. await InsertTimelineAsync(e.Id, "CLAIM", "认领", fromStatus, "ASSIGNED", assigneeId, null, remark);
  68. }, ex => throw ex);
  69. // 注:认领仅承接异常,不视为审批流完成。审批流的"通过"在"开始处理"那一步触发。
  70. return e;
  71. }
  72. public async Task<AdoS8Exception> TransferAsync(long id, long tenantId, long factoryId, long newAssigneeId, string? remark)
  73. {
  74. var allowedStatuses = new HashSet<string> { "ASSIGNED", "IN_PROGRESS" };
  75. if (newAssigneeId <= 0) throw new S8BizException("转派目标无效");
  76. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  77. if (e.ActiveFlowInstanceId.HasValue)
  78. throw new S8BizException("审批进行中不可转派");
  79. if (e.Status == "ESCALATED")
  80. throw new S8BizException("升级审批中不可转派");
  81. if (S8StatusRules.IsTerminal(e.Status))
  82. throw new S8BizException("已关闭不可转派");
  83. if (!allowedStatuses.Contains(e.Status))
  84. throw new S8BizException($"状态 {e.Status} 不可转派");
  85. e.AssigneeId = newAssigneeId;
  86. e.UpdatedAt = DateTime.Now;
  87. // S8-ASSIGNEE-RESP-DEPT-SYNC-1(P0-B-2):转派同步处理部门 = 新处理人所属部门 RecID。
  88. // 兜底策略:解析失败保留原 ResponsibleDeptId,仅 warn;避免误覆盖既有归属。
  89. var resolvedDeptId = await ResolveEmployeeResponsibleDeptIdAsync(newAssigneeId, factoryId);
  90. if (resolvedDeptId.HasValue)
  91. {
  92. e.ResponsibleDeptId = resolvedDeptId.Value;
  93. }
  94. else
  95. {
  96. _logger.LogWarning(
  97. "s8_transfer_dept_unresolved exceptionId={Id} newAssigneeId={AssigneeId} factoryId={FactoryId}",
  98. e.Id, newAssigneeId, factoryId);
  99. }
  100. await _rep.AsTenant().UseTranAsync(async () =>
  101. {
  102. await _rep.UpdateAsync(e);
  103. await InsertTimelineAsync(e.Id, "TRANSFER", "转派", e.Status, e.Status, newAssigneeId, null, remark);
  104. }, ex => throw ex);
  105. // 双线合一:S8 转派 = TB001 任务转办给新处理人。
  106. await TryTransferIntakeOnTransferAsync(e.Id, newAssigneeId, remark);
  107. return e;
  108. }
  109. private async Task TryTransferIntakeOnTransferAsync(long exceptionId, long newAssigneeRecId, string? remark)
  110. {
  111. try
  112. {
  113. var instance = await _flowInstanceRep.AsQueryable()
  114. .Where(x => x.BizType == "EXCEPTION_REPORT"
  115. && x.BizId == exceptionId
  116. && x.Status == FlowInstanceStatusEnum.Running)
  117. .FirstAsync();
  118. if (instance == null) return;
  119. var currentUserId = _userManager.UserId;
  120. var task = await _flowTaskRep.AsQueryable()
  121. .Where(x => x.InstanceId == instance.Id
  122. && x.AssigneeId == currentUserId
  123. && x.Status == FlowTaskStatusEnum.Pending)
  124. .FirstAsync();
  125. if (task == null) return;
  126. // newAssigneeRecId 是 EmployeeMaster.RecID;FlowEngine.Transfer 要 SysUser.UserId。
  127. var targetSysUserId = await _employeeRep.AsQueryable().ClearFilter()
  128. .Where(x => x.Id == newAssigneeRecId && x.SysUserId != null)
  129. .Select(x => x.SysUserId)
  130. .FirstAsync();
  131. if (targetSysUserId == null || targetSysUserId == 0)
  132. {
  133. _logger.LogWarning(
  134. "S8 转派联动审批流跳过:员工 {RecId} 未绑定 SysUser,TB001 任务保留原 assignee",
  135. newAssigneeRecId);
  136. return;
  137. }
  138. await _flowEngine.Transfer(task.Id, targetSysUserId.Value, remark ?? "S8 转派(双线合一自动转办)");
  139. }
  140. catch (Exception ex)
  141. {
  142. _logger.LogWarning(ex,
  143. "S8 转派时自动转办 TB001 任务失败 exceptionId={Id} newAssigneeRecId={AssigneeId}",
  144. exceptionId, newAssigneeRecId);
  145. }
  146. }
  147. public async Task<AdoS8Exception> StartProgressAsync(long id, long tenantId, long factoryId, string? remark)
  148. {
  149. var currentUserId = GetCurrentUserId();
  150. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  151. if (!S8StatusRules.IsAllowedTransition(e.Status, "IN_PROGRESS"))
  152. throw new S8BizException($"状态 {e.Status} 不可开始处理");
  153. var fromStatus = e.Status;
  154. e.Status = "IN_PROGRESS";
  155. e.UpdatedAt = DateTime.Now;
  156. await _rep.AsTenant().UseTranAsync(async () =>
  157. {
  158. await _rep.UpdateAsync(e);
  159. await InsertTimelineAsync(e.Id, "START_PROGRESS", "开始处理", fromStatus, "IN_PROGRESS", currentUserId, null, remark);
  160. }, ex => throw ex);
  161. // 双线合一:开始处理 = TB001 异常提报审批通过。
  162. // 当前用户必须是 TB001 task 的 AssigneeId,FlowEngine 强校验。
  163. await TryApproveIntakeOnStartProgressAsync(e.Id, currentUserId);
  164. return e;
  165. }
  166. private async Task TryApproveIntakeOnStartProgressAsync(long exceptionId, long currentUserId)
  167. {
  168. try
  169. {
  170. var instance = await _flowInstanceRep.AsQueryable()
  171. .Where(x => x.BizType == "EXCEPTION_REPORT"
  172. && x.BizId == exceptionId
  173. && x.Status == FlowInstanceStatusEnum.Running)
  174. .FirstAsync();
  175. if (instance == null) return;
  176. var task = await _flowTaskRep.AsQueryable()
  177. .Where(x => x.InstanceId == instance.Id
  178. && x.AssigneeId == currentUserId
  179. && x.Status == FlowTaskStatusEnum.Pending)
  180. .FirstAsync();
  181. if (task == null) return;
  182. await _flowEngine.Approve(task.Id, "S8 已开始处理(双线合一自动同意)");
  183. }
  184. catch (Exception ex)
  185. {
  186. _logger.LogWarning(ex,
  187. "S8 开始处理时自动同意 TB001 任务失败 exceptionId={Id} userId={UserId}",
  188. exceptionId, currentUserId);
  189. }
  190. }
  191. public async Task<AdoS8Exception> UpgradeAsync(long id, long tenantId, long factoryId, string? remark)
  192. {
  193. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  194. if (e.ActiveFlowInstanceId.HasValue)
  195. throw new S8BizException("该异常已有进行中的审批流程,请等待审批完成");
  196. if (!S8StatusRules.IsAllowedTransition(e.Status, "ESCALATED"))
  197. throw new S8BizException($"状态 {e.Status} 不可升级");
  198. await _flowEngine.StartFlow(new StartFlowInput
  199. {
  200. BizType = "EXCEPTION_ESCALATION",
  201. BizId = e.Id,
  202. Title = $"异常升级审批 - {e.ExceptionCode}",
  203. Comment = remark,
  204. BizData = new Dictionary<string, object>
  205. {
  206. ["severity"] = e.Severity,
  207. ["sceneCode"] = e.SceneCode,
  208. ["priorityLevel"] = e.PriorityLevel,
  209. }
  210. });
  211. // 状态和时间线由 ExceptionEscalationBizHandler.OnFlowStarted 回调更新
  212. return await LoadAsync(id, tenantId, factoryId) ?? e;
  213. }
  214. public async Task<AdoS8Exception> RejectAsync(long id, long tenantId, long factoryId, string? remark)
  215. {
  216. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  217. if (!S8StatusRules.IsAllowedTransition(e.Status, "REJECTED"))
  218. throw new S8BizException($"状态 {e.Status} 不可驳回");
  219. var from = e.Status;
  220. e.Status = "REJECTED";
  221. e.UpdatedAt = DateTime.Now;
  222. await _rep.AsTenant().UseTranAsync(async () =>
  223. {
  224. await _rep.UpdateAsync(e);
  225. await InsertTimelineAsync(e.Id, "REJECT", "驳回", from, "REJECTED", null, null, remark);
  226. }, ex => throw ex);
  227. // 双线合一:S8 驳回 = TB001 流程整体拒绝(取消所有 pending 任务、Instance 终止)。
  228. await TryRejectIntakeOnRejectAsync(e.Id, remark);
  229. return e;
  230. }
  231. private async Task TryRejectIntakeOnRejectAsync(long exceptionId, string? remark)
  232. {
  233. try
  234. {
  235. var instance = await _flowInstanceRep.AsQueryable()
  236. .Where(x => x.BizType == "EXCEPTION_REPORT"
  237. && x.BizId == exceptionId
  238. && x.Status == FlowInstanceStatusEnum.Running)
  239. .FirstAsync();
  240. if (instance == null) return;
  241. var currentUserId = _userManager.UserId;
  242. var task = await _flowTaskRep.AsQueryable()
  243. .Where(x => x.InstanceId == instance.Id
  244. && x.AssigneeId == currentUserId
  245. && x.Status == FlowTaskStatusEnum.Pending)
  246. .FirstAsync();
  247. if (task == null) return;
  248. await _flowEngine.Reject(task.Id, remark ?? "S8 已驳回(双线合一自动拒绝)");
  249. }
  250. catch (Exception ex)
  251. {
  252. _logger.LogWarning(ex,
  253. "S8 驳回时自动拒绝 TB001 流程失败 exceptionId={Id}",
  254. exceptionId);
  255. }
  256. }
  257. public async Task<AdoS8Exception> SubmitVerificationAsync(
  258. long id, long tenantId, long factoryId,
  259. long verifierId, string? remark)
  260. {
  261. var currentUserId = GetCurrentUserId();
  262. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  263. await EnsureCurrentUserIsOperatorAsync(e.AssigneeId, currentUserId,
  264. "只有当前处理人才能提交复检(或当前账号未绑定员工主数据)");
  265. if (verifierId <= 0)
  266. throw new S8BizException("请选择检验人");
  267. if (!S8StatusRules.IsAllowedTransition(e.Status, "PENDING_VERIFICATION"))
  268. throw new S8BizException($"状态 {e.Status} 不可提交复检");
  269. var from = e.Status;
  270. e.Status = "PENDING_VERIFICATION";
  271. e.VerifierId = verifierId;
  272. e.VerificationAssignedAt = DateTime.Now;
  273. e.UpdatedAt = DateTime.Now;
  274. await _rep.AsTenant().UseTranAsync(async () =>
  275. {
  276. await _rep.UpdateAsync(e);
  277. await InsertTimelineAsync(e.Id, "VERIFY_SUBMITTED", "提交复检", from, "PENDING_VERIFICATION",
  278. currentUserId, null, remark);
  279. }, ex => throw ex);
  280. // 双线合一:提交复检 = 启动 EXCEPTION_CLOSURE 流程,指派检验人。
  281. // 该流程定义复用,作为复检/关闭确认通用审批载体;handler 只做 ActiveFlowInstanceId 维护。
  282. await TryStartVerificationFlowAsync(e, verifierId, remark);
  283. return await LoadAsync(id, tenantId, factoryId) ?? e;
  284. }
  285. private async Task TryStartVerificationFlowAsync(AdoS8Exception e, long verifierRecId, string? remark)
  286. {
  287. try
  288. {
  289. await _flowEngine.StartFlow(new StartFlowInput
  290. {
  291. BizType = "EXCEPTION_CLOSURE",
  292. BizId = e.Id,
  293. BizNo = e.ExceptionCode,
  294. Title = $"异常复检 - {e.ExceptionCode}",
  295. Comment = remark,
  296. BizData = new Dictionary<string, object>
  297. {
  298. ["sceneCode"] = e.SceneCode ?? string.Empty,
  299. ["verifierRecId"] = verifierRecId,
  300. }
  301. });
  302. }
  303. catch (Exception ex)
  304. {
  305. _logger.LogWarning(ex,
  306. "S8 提交复检时启动 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} verifierRecId={VerifierId}",
  307. e.Id, verifierRecId);
  308. }
  309. }
  310. public async Task<AdoS8Exception> ApproveVerificationAsync(
  311. long id, long tenantId, long factoryId,
  312. string? remark)
  313. {
  314. var currentUserId = GetCurrentUserId();
  315. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  316. await EnsureCurrentUserIsOperatorAsync(e.VerifierId, currentUserId,
  317. "只有指定检验人才能检验通过(或当前账号未绑定员工主数据)");
  318. if (!S8StatusRules.IsAllowedTransition(e.Status, "CLOSED"))
  319. throw new S8BizException($"状态 {e.Status} 不可检验通过");
  320. var from = e.Status;
  321. e.Status = "CLOSED";
  322. e.VerifiedAt = DateTime.Now;
  323. e.VerificationResult = "APPROVED";
  324. e.VerificationRemark = remark;
  325. e.ClosedAt = DateTime.Now;
  326. e.UpdatedAt = DateTime.Now;
  327. await _rep.AsTenant().UseTranAsync(async () =>
  328. {
  329. await _rep.UpdateAsync(e);
  330. await InsertTimelineAsync(e.Id, "VERIFY_APPROVED", "检验通过", from, "CLOSED",
  331. currentUserId, null, remark);
  332. }, ex => throw ex);
  333. // 双线合一:检验通过 = EXCEPTION_CLOSURE 复检流程审批通过。
  334. await TryApproveVerificationFlowAsync(e.Id, currentUserId);
  335. return e;
  336. }
  337. private async Task TryApproveVerificationFlowAsync(long exceptionId, long currentUserId)
  338. {
  339. try
  340. {
  341. var instance = await _flowInstanceRep.AsQueryable()
  342. .Where(x => x.BizType == "EXCEPTION_CLOSURE"
  343. && x.BizId == exceptionId
  344. && x.Status == FlowInstanceStatusEnum.Running)
  345. .FirstAsync();
  346. if (instance == null) return;
  347. var task = await _flowTaskRep.AsQueryable()
  348. .Where(x => x.InstanceId == instance.Id
  349. && x.AssigneeId == currentUserId
  350. && x.Status == FlowTaskStatusEnum.Pending)
  351. .FirstAsync();
  352. if (task == null) return;
  353. await _flowEngine.Approve(task.Id, "S8 检验通过(双线合一自动同意)");
  354. }
  355. catch (Exception ex)
  356. {
  357. _logger.LogWarning(ex,
  358. "S8 检验通过时自动同意 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} userId={UserId}",
  359. exceptionId, currentUserId);
  360. }
  361. }
  362. public async Task<AdoS8Exception> RejectVerificationAsync(
  363. long id, long tenantId, long factoryId,
  364. string remark)
  365. {
  366. var currentUserId = GetCurrentUserId();
  367. var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
  368. await EnsureCurrentUserIsOperatorAsync(e.VerifierId, currentUserId,
  369. "只有指定检验人才能检验退回(或当前账号未绑定员工主数据)");
  370. if (!S8StatusRules.IsAllowedTransition(e.Status, "IN_PROGRESS"))
  371. throw new S8BizException($"状态 {e.Status} 不可检验退回");
  372. if (string.IsNullOrWhiteSpace(remark))
  373. throw new S8BizException("检验退回必须填写退回原因");
  374. var from = e.Status;
  375. e.Status = "IN_PROGRESS";
  376. e.VerifiedAt = DateTime.Now;
  377. e.VerificationResult = "REJECTED";
  378. e.VerificationRemark = remark;
  379. e.UpdatedAt = DateTime.Now;
  380. await _rep.AsTenant().UseTranAsync(async () =>
  381. {
  382. await _rep.UpdateAsync(e);
  383. await InsertTimelineAsync(e.Id, "VERIFY_REJECTED", "检验退回", from, "IN_PROGRESS",
  384. currentUserId, null, remark);
  385. }, ex => throw ex);
  386. // 双线合一:检验退回 = EXCEPTION_CLOSURE 复检流程整体拒绝。
  387. await TryRejectVerificationFlowAsync(e.Id, currentUserId, remark);
  388. return e;
  389. }
  390. private async Task TryRejectVerificationFlowAsync(long exceptionId, long currentUserId, string? remark)
  391. {
  392. try
  393. {
  394. var instance = await _flowInstanceRep.AsQueryable()
  395. .Where(x => x.BizType == "EXCEPTION_CLOSURE"
  396. && x.BizId == exceptionId
  397. && x.Status == FlowInstanceStatusEnum.Running)
  398. .FirstAsync();
  399. if (instance == null) return;
  400. var task = await _flowTaskRep.AsQueryable()
  401. .Where(x => x.InstanceId == instance.Id
  402. && x.AssigneeId == currentUserId
  403. && x.Status == FlowTaskStatusEnum.Pending)
  404. .FirstAsync();
  405. if (task == null) return;
  406. await _flowEngine.Reject(task.Id, remark ?? "S8 检验退回(双线合一自动拒绝)");
  407. }
  408. catch (Exception ex)
  409. {
  410. _logger.LogWarning(ex,
  411. "S8 检验退回时自动拒绝 EXCEPTION_CLOSURE 流程失败 exceptionId={Id} userId={UserId}",
  412. exceptionId, currentUserId);
  413. }
  414. }
  415. public async Task CommentAsync(long id, string? remark)
  416. {
  417. var e = await _rep.GetFirstAsync(x => x.Id == id && !x.IsDeleted)
  418. ?? throw new S8BizException("异常不存在");
  419. await InsertTimelineAsync(e.Id, "COMMENT", "补充说明", e.Status, e.Status, null, null, remark);
  420. }
  421. private Task<AdoS8Exception?> LoadAsync(long id, long tenantId, long factoryId) =>
  422. _rep.GetFirstAsync(x => x.Id == id && x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted);
  423. // 统一复用框架登录上下文,避免业务身份继续信任前端传参。
  424. private long GetCurrentUserId()
  425. {
  426. var currentUserId = _userManager.UserId;
  427. if (currentUserId <= 0)
  428. throw new S8BizException("未获取到当前登录用户");
  429. return currentUserId;
  430. }
  431. // S8-ASSIGNEE-RESP-DEPT-SYNC-1(P0-B-2):处理人 RecID → 所属部门 RecID。
  432. // 链路:EmployeeMaster.Department(codename) + FactoryRefId → DepartmentMaster.Department + FactoryRefId → RecID。
  433. // ClearFilter 同口径(S8MasterDataAdapter / GetEmployeeSysUserIdAsync),factoryId 做硬边界。
  434. // 解析失败返回 null —— 调用方按"保留原值 + warn"处理,绝不覆盖既有 ResponsibleDeptId。
  435. private async Task<long?> ResolveEmployeeResponsibleDeptIdAsync(long assigneeId, long factoryId)
  436. {
  437. if (assigneeId <= 0 || factoryId <= 0) return null;
  438. var emp = await _employeeRep.AsQueryable().ClearFilter()
  439. .Where(x => x.Id == assigneeId && x.FactoryRefId == factoryId)
  440. .Select(x => new { x.Department })
  441. .FirstAsync();
  442. if (emp == null || string.IsNullOrWhiteSpace(emp.Department)) return null;
  443. var deptId = await _deptRep.AsQueryable().ClearFilter()
  444. .Where(x => x.Department == emp.Department && x.FactoryRefId == factoryId && x.IsActive)
  445. .Select(x => (long?)x.Id)
  446. .FirstAsync();
  447. return deptId;
  448. }
  449. // 把异常上的处理人/检验人(employeeId) 经 EmployeeMaster.SysUserId 解析到系统账号 ID。
  450. // EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位,必须 ClearFilter 跳过多租户全局 filter;
  451. // 通过 employee.Id 主键精确查询作为安全边界,无跨租户泄漏风险。
  452. private async Task<long?> GetEmployeeSysUserIdAsync(long? employeeId)
  453. {
  454. if (!employeeId.HasValue || employeeId.Value <= 0) return null;
  455. var emp = await _employeeRep.AsQueryable().ClearFilter()
  456. .Where(x => x.Id == employeeId.Value)
  457. .FirstAsync();
  458. return emp?.SysUserId;
  459. }
  460. // 鉴权统一入口:要求当前登录用户必须是 employeeId 解析后的 SysUserId。
  461. private async Task EnsureCurrentUserIsOperatorAsync(long? employeeId, long currentUserId, string failMessage)
  462. {
  463. var ownerSysUserId = await GetEmployeeSysUserIdAsync(employeeId);
  464. if (ownerSysUserId != currentUserId)
  465. throw new S8BizException(failMessage);
  466. }
  467. private async Task InsertTimelineAsync(long exceptionId, string code, string label, string? from, string? to,
  468. long? operatorId, string? operatorName, string? remark) =>
  469. await _timelineRep.InsertAsync(new AdoS8ExceptionTimeline
  470. {
  471. ExceptionId = exceptionId,
  472. ActionCode = code,
  473. ActionLabel = label,
  474. FromStatus = from,
  475. ToStatus = to,
  476. OperatorId = operatorId,
  477. OperatorName = operatorName,
  478. ActionRemark = remark,
  479. CreatedAt = DateTime.Now
  480. });
  481. }