S8OperatorBindingService.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. using Admin.NET.Plugin.AiDOP.Dto.S8;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  5. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  6. /// <summary>
  7. /// S8 配置页:操作员(员工)↔ 系统账号绑定。
  8. /// 仅维护 EmployeeMaster.SysUserId 的 1:1 弱关联;不动 SysUser/SysRole/SysUserRole。
  9. ///
  10. /// <para><b>S8-P0-3-MASTERDATA-TRUSTED-SCOPE-1 作用域合同(五个入口统一)</b>:
  11. /// 租户与工厂**一律由 <see cref="S8TrustedScopeResolver"/> 从认证身份解析**,
  12. /// 客户端传入的 <c>factoryRefId</c> / <c>tenantId</c> / <c>body.FactoryRefId</c>
  13. /// **只被视为筛选意图、不承担任何授权作用,且当前实现直接忽略**。</para>
  14. ///
  15. /// <para>修复前(实测可被普通租户账号利用):<c>ListAsync</c> 是
  16. /// <c>ClearFilter()</c> + <c>WhereIF(factoryRefId.HasValue, …)</c> —— 谓词**可选**,
  17. /// 不传即全库无边界;`GET /config/operator-bindings` 返回 500 行且全部属于其它租户
  18. /// (工厂 1000 × 498 / 1329900200002 × 2,本租户 0 行)。
  19. /// <c>BindAsync</c> / <c>UnbindAsync</c> / <c>ListSysUsersAsync</c> 则各自用不同口径,
  20. /// 出现过「List 用 ClearFilter、Bind 不用」的不一致。</para>
  21. ///
  22. /// <para>修复后:`EmployeeMaster` 的 <c>ClearFilter()</c> 保留(其 tenant_id 属 S0 业务域,
  23. /// 与登录 token 的系统租户历史错位,不清会全过滤为 0 行),但**必须**同时带
  24. /// <c>FactoryRefId == 可信工厂</c> 这一强制硬边界 —— 与 CURRENT.md 的
  25. /// 「ClearFilter 必须同时具备显式业务边界」铁律一致。</para>
  26. /// </summary>
  27. public class S8OperatorBindingService : ITransient
  28. {
  29. private readonly SqlSugarRepository<AdoS0EmployeeMaster> _empRep;
  30. private readonly SqlSugarRepository<SysUser> _sysUserRep;
  31. private readonly UserManager _userManager;
  32. public S8OperatorBindingService(
  33. SqlSugarRepository<AdoS0EmployeeMaster> empRep,
  34. SqlSugarRepository<SysUser> sysUserRep,
  35. UserManager userManager)
  36. {
  37. _empRep = empRep;
  38. _sysUserRep = sysUserRep;
  39. _userManager = userManager;
  40. }
  41. /// <summary>按可信作用域列出本工厂员工及其绑定状态。factoryRefId 不再由调用方决定。</summary>
  42. public async Task<List<AdoS8OperatorBindingRowDto>> ListAsync(S8TrustedScope scope, string? bindStatus, string? keyword)
  43. {
  44. var emps = await ScopedEmployees(scope)
  45. .WhereIF(!string.IsNullOrWhiteSpace(keyword),
  46. x => x.Employee.Contains(keyword!) || (x.Name != null && x.Name.Contains(keyword!)))
  47. .Take(500)
  48. .Select(x => new { x.Id, x.Name, x.Employee, x.FactoryRefId, x.SysUserId })
  49. .ToListAsync();
  50. var sysUserIds = emps.Where(e => e.SysUserId.HasValue && e.SysUserId.Value > 0)
  51. .Select(e => e.SysUserId!.Value).Distinct().ToList();
  52. var userMap = sysUserIds.Count == 0
  53. ? new Dictionary<long, string?>()
  54. : (await _sysUserRep.AsQueryable()
  55. .Where(u => sysUserIds.Contains(u.Id) && u.TenantId == scope.TenantId)
  56. .Select(u => new { u.Id, u.RealName, u.Account })
  57. .ToListAsync())
  58. .ToDictionary(u => u.Id, u => string.IsNullOrWhiteSpace(u.RealName) ? u.Account : u.RealName);
  59. var rows = emps.Select(e =>
  60. {
  61. var bound = e.SysUserId.HasValue && e.SysUserId.Value > 0 && userMap.ContainsKey(e.SysUserId.Value);
  62. return new AdoS8OperatorBindingRowDto
  63. {
  64. EmployeeId = e.Id,
  65. EmpCode = e.Employee,
  66. Name = e.Name,
  67. FactoryRefId = e.FactoryRefId,
  68. SysUserId = e.SysUserId,
  69. SysUserName = bound ? userMap[e.SysUserId!.Value] : null,
  70. BindStatus = bound ? "BOUND" : "UNBOUND"
  71. };
  72. });
  73. if (!string.IsNullOrWhiteSpace(bindStatus) && (bindStatus == "BOUND" || bindStatus == "UNBOUND"))
  74. rows = rows.Where(r => r.BindStatus == bindStatus);
  75. return rows
  76. .OrderBy(r => r.BindStatus == "BOUND" ? 0 : 1)
  77. .ThenBy(r => r.EmpCode)
  78. .ToList();
  79. }
  80. public async Task<List<AdoS8ConfigSysUserRowDto>> ListSysUsersAsync(S8TrustedScope scope, string? keyword, long? excludeEmployeeId)
  81. {
  82. var users = await _sysUserRep.AsQueryable()
  83. .Where(u => u.TenantId == scope.TenantId)
  84. .WhereIF(!string.IsNullOrWhiteSpace(keyword),
  85. u => u.Account.Contains(keyword!) || u.RealName.Contains(keyword!))
  86. .Take(200)
  87. .Select(u => new { u.Id, u.Account, u.RealName, u.Status })
  88. .ToListAsync();
  89. if (users.Count == 0) return new();
  90. // 标注哪些 sysUser 已被其它 employee 绑定(用于前端禁选/隐藏);
  91. // 当前 employee 自己绑定的 sysUser 在 alreadyBoundEmployeeId == excludeEmployeeId 时会被前端放行。
  92. // S8-P0-3:占用查询必须限定在可信工厂内,否则会把其它租户的占用情况泄漏出去。
  93. var ids = users.Select(u => u.Id).ToList();
  94. var bound = await ScopedEmployees(scope)
  95. .Where(e => e.SysUserId.HasValue && ids.Contains(e.SysUserId!.Value))
  96. .Select(e => new { e.Id, e.SysUserId })
  97. .ToListAsync();
  98. var boundMap = bound.GroupBy(b => b.SysUserId!.Value)
  99. .ToDictionary(g => g.Key, g => g.First().Id);
  100. return users.Select(u => new AdoS8ConfigSysUserRowDto
  101. {
  102. Id = u.Id,
  103. Account = u.Account,
  104. RealName = u.RealName,
  105. Status = (int)u.Status,
  106. AlreadyBoundEmployeeId = boundMap.TryGetValue(u.Id, out var empId) ? empId : null
  107. }).ToList();
  108. }
  109. public async Task<AdoS8OperatorBindingRowDto> BindAsync(S8TrustedScope scope, AdoS8OperatorBindingCreateDto dto)
  110. {
  111. if (dto.EmployeeId <= 0) throw new S8BizException("员工 ID 不能为空");
  112. if (dto.SysUserId <= 0) throw new S8BizException("系统账号 ID 不能为空");
  113. // S8-P0-3:员工必须属于可信工厂。跨工厂 / 跨租户的 employeeId 一律按「不存在」处理,
  114. // 文案与本租户内真实不存在的情况完全一致,避免被用来探测他租户员工是否存在。
  115. var emp = await ScopedEmployee(scope, dto.EmployeeId)
  116. ?? throw new S8BizException("员工不存在");
  117. var user = await _sysUserRep.GetFirstAsync(x => x.Id == dto.SysUserId && x.TenantId == scope.TenantId)
  118. ?? throw new S8BizException("系统账号不存在或不在当前租户");
  119. // 同一 sysUser 不允许绑定到另一个 employee(冲突检测同样限定在可信工厂内)
  120. var conflict = await ScopedEmployees(scope)
  121. .Where(x => x.SysUserId == dto.SysUserId && x.Id != dto.EmployeeId)
  122. .FirstAsync();
  123. if (conflict != null)
  124. throw new S8BizException($"系统账号 {user.Account} 已绑定至其它员工 {conflict.Name ?? conflict.Employee}");
  125. emp.SysUserId = dto.SysUserId;
  126. emp.UpdateTime = DateTime.Now;
  127. emp.UpdateUser = _userManager.Account;
  128. await _empRep.AsUpdateable(emp)
  129. .UpdateColumns(it => new { it.SysUserId, it.UpdateTime, it.UpdateUser })
  130. .ExecuteCommandAsync();
  131. return new AdoS8OperatorBindingRowDto
  132. {
  133. EmployeeId = emp.Id,
  134. EmpCode = emp.Employee,
  135. Name = emp.Name,
  136. FactoryRefId = emp.FactoryRefId,
  137. SysUserId = dto.SysUserId,
  138. SysUserName = string.IsNullOrWhiteSpace(user.RealName) ? user.Account : user.RealName,
  139. BindStatus = "BOUND"
  140. };
  141. }
  142. public async Task UnbindAsync(S8TrustedScope scope, long employeeId)
  143. {
  144. if (employeeId <= 0) throw new S8BizException("员工 ID 不能为空");
  145. var emp = await ScopedEmployee(scope, employeeId)
  146. ?? throw new S8BizException("员工不存在");
  147. if (!emp.SysUserId.HasValue) return;
  148. emp.SysUserId = null;
  149. emp.UpdateTime = DateTime.Now;
  150. emp.UpdateUser = _userManager.Account;
  151. await _empRep.AsUpdateable(emp)
  152. .UpdateColumns(it => new { it.SysUserId, it.UpdateTime, it.UpdateUser })
  153. .ExecuteCommandAsync();
  154. }
  155. /// <summary>
  156. /// S8-P0-3:EmployeeMaster 的唯一合法访问口径。
  157. /// ClearFilter 是必要的(EmployeeMaster.tenant_id 属 S0 业务域,与登录 token 的系统租户历史错位),
  158. /// 但**必须**同时带 FactoryRefId 强制硬边界 —— 谓词不可选、不接受调用方覆盖。
  159. /// </summary>
  160. private ISugarQueryable<AdoS0EmployeeMaster> ScopedEmployees(S8TrustedScope scope) =>
  161. _empRep.AsQueryable().ClearFilter()
  162. .Where(x => x.FactoryRefId == scope.FactoryId);
  163. private async Task<AdoS0EmployeeMaster?> ScopedEmployee(S8TrustedScope scope, long employeeId) =>
  164. await ScopedEmployees(scope).Where(x => x.Id == employeeId).FirstAsync();
  165. }