|
@@ -1,5 +1,6 @@
|
|
|
using Admin.NET.Plugin.AiDOP.Dto.S8;
|
|
using Admin.NET.Plugin.AiDOP.Dto.S8;
|
|
|
using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
|
|
using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
|
|
|
|
|
|
|
|
namespace Admin.NET.Plugin.AiDOP.Service.S8;
|
|
namespace Admin.NET.Plugin.AiDOP.Service.S8;
|
|
@@ -7,6 +8,23 @@ namespace Admin.NET.Plugin.AiDOP.Service.S8;
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// S8 配置页:操作员(员工)↔ 系统账号绑定。
|
|
/// S8 配置页:操作员(员工)↔ 系统账号绑定。
|
|
|
/// 仅维护 EmployeeMaster.SysUserId 的 1:1 弱关联;不动 SysUser/SysRole/SysUserRole。
|
|
/// 仅维护 EmployeeMaster.SysUserId 的 1:1 弱关联;不动 SysUser/SysRole/SysUserRole。
|
|
|
|
|
+///
|
|
|
|
|
+/// <para><b>S8-P0-3-MASTERDATA-TRUSTED-SCOPE-1 作用域合同(五个入口统一)</b>:
|
|
|
|
|
+/// 租户与工厂**一律由 <see cref="S8TrustedScopeResolver"/> 从认证身份解析**,
|
|
|
|
|
+/// 客户端传入的 <c>factoryRefId</c> / <c>tenantId</c> / <c>body.FactoryRefId</c>
|
|
|
|
|
+/// **只被视为筛选意图、不承担任何授权作用,且当前实现直接忽略**。</para>
|
|
|
|
|
+///
|
|
|
|
|
+/// <para>修复前(实测可被普通租户账号利用):<c>ListAsync</c> 是
|
|
|
|
|
+/// <c>ClearFilter()</c> + <c>WhereIF(factoryRefId.HasValue, …)</c> —— 谓词**可选**,
|
|
|
|
|
+/// 不传即全库无边界;`GET /config/operator-bindings` 返回 500 行且全部属于其它租户
|
|
|
|
|
+/// (工厂 1000 × 498 / 1329900200002 × 2,本租户 0 行)。
|
|
|
|
|
+/// <c>BindAsync</c> / <c>UnbindAsync</c> / <c>ListSysUsersAsync</c> 则各自用不同口径,
|
|
|
|
|
+/// 出现过「List 用 ClearFilter、Bind 不用」的不一致。</para>
|
|
|
|
|
+///
|
|
|
|
|
+/// <para>修复后:`EmployeeMaster` 的 <c>ClearFilter()</c> 保留(其 tenant_id 属 S0 业务域,
|
|
|
|
|
+/// 与登录 token 的系统租户历史错位,不清会全过滤为 0 行),但**必须**同时带
|
|
|
|
|
+/// <c>FactoryRefId == 可信工厂</c> 这一强制硬边界 —— 与 CURRENT.md 的
|
|
|
|
|
+/// 「ClearFilter 必须同时具备显式业务边界」铁律一致。</para>
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public class S8OperatorBindingService : ITransient
|
|
public class S8OperatorBindingService : ITransient
|
|
|
{
|
|
{
|
|
@@ -24,12 +42,10 @@ public class S8OperatorBindingService : ITransient
|
|
|
_userManager = userManager;
|
|
_userManager = userManager;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public async Task<List<AdoS8OperatorBindingRowDto>> ListAsync(long? factoryRefId, string? bindStatus, string? keyword)
|
|
|
|
|
|
|
+ /// <summary>按可信作用域列出本工厂员工及其绑定状态。factoryRefId 不再由调用方决定。</summary>
|
|
|
|
|
+ public async Task<List<AdoS8OperatorBindingRowDto>> ListAsync(S8TrustedScope scope, string? bindStatus, string? keyword)
|
|
|
{
|
|
{
|
|
|
- // ClearFilter:EmployeeMaster.tenant_id 属 S0 域租户,与登录 token TenantId 不一致(同 BUG-S8-EMPLOYEES-TENANT-FILTER-001 第 3 处)。
|
|
|
|
|
- // 安全边界:保留 factoryRefId 显式过滤;仅放开租户全局过滤器;不影响 SysUser 查询。
|
|
|
|
|
- var emps = await _empRep.AsQueryable().ClearFilter()
|
|
|
|
|
- .WhereIF(factoryRefId.HasValue, x => x.FactoryRefId == factoryRefId!.Value)
|
|
|
|
|
|
|
+ var emps = await ScopedEmployees(scope)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
|
|
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
|
|
|
x => x.Employee.Contains(keyword!) || (x.Name != null && x.Name.Contains(keyword!)))
|
|
x => x.Employee.Contains(keyword!) || (x.Name != null && x.Name.Contains(keyword!)))
|
|
|
.Take(500)
|
|
.Take(500)
|
|
@@ -41,7 +57,7 @@ public class S8OperatorBindingService : ITransient
|
|
|
var userMap = sysUserIds.Count == 0
|
|
var userMap = sysUserIds.Count == 0
|
|
|
? new Dictionary<long, string?>()
|
|
? new Dictionary<long, string?>()
|
|
|
: (await _sysUserRep.AsQueryable()
|
|
: (await _sysUserRep.AsQueryable()
|
|
|
- .Where(u => sysUserIds.Contains(u.Id) && u.TenantId == _userManager.TenantId)
|
|
|
|
|
|
|
+ .Where(u => sysUserIds.Contains(u.Id) && u.TenantId == scope.TenantId)
|
|
|
.Select(u => new { u.Id, u.RealName, u.Account })
|
|
.Select(u => new { u.Id, u.RealName, u.Account })
|
|
|
.ToListAsync())
|
|
.ToListAsync())
|
|
|
.ToDictionary(u => u.Id, u => string.IsNullOrWhiteSpace(u.RealName) ? u.Account : u.RealName);
|
|
.ToDictionary(u => u.Id, u => string.IsNullOrWhiteSpace(u.RealName) ? u.Account : u.RealName);
|
|
@@ -70,11 +86,10 @@ public class S8OperatorBindingService : ITransient
|
|
|
.ToList();
|
|
.ToList();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public async Task<List<AdoS8ConfigSysUserRowDto>> ListSysUsersAsync(string? keyword, long? excludeEmployeeId)
|
|
|
|
|
|
|
+ public async Task<List<AdoS8ConfigSysUserRowDto>> ListSysUsersAsync(S8TrustedScope scope, string? keyword, long? excludeEmployeeId)
|
|
|
{
|
|
{
|
|
|
- var tenantId = _userManager.TenantId;
|
|
|
|
|
var users = await _sysUserRep.AsQueryable()
|
|
var users = await _sysUserRep.AsQueryable()
|
|
|
- .Where(u => u.TenantId == tenantId)
|
|
|
|
|
|
|
+ .Where(u => u.TenantId == scope.TenantId)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
|
|
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
|
|
|
u => u.Account.Contains(keyword!) || u.RealName.Contains(keyword!))
|
|
u => u.Account.Contains(keyword!) || u.RealName.Contains(keyword!))
|
|
|
.Take(200)
|
|
.Take(200)
|
|
@@ -85,8 +100,9 @@ public class S8OperatorBindingService : ITransient
|
|
|
|
|
|
|
|
// 标注哪些 sysUser 已被其它 employee 绑定(用于前端禁选/隐藏);
|
|
// 标注哪些 sysUser 已被其它 employee 绑定(用于前端禁选/隐藏);
|
|
|
// 当前 employee 自己绑定的 sysUser 在 alreadyBoundEmployeeId == excludeEmployeeId 时会被前端放行。
|
|
// 当前 employee 自己绑定的 sysUser 在 alreadyBoundEmployeeId == excludeEmployeeId 时会被前端放行。
|
|
|
|
|
+ // S8-P0-3:占用查询必须限定在可信工厂内,否则会把其它租户的占用情况泄漏出去。
|
|
|
var ids = users.Select(u => u.Id).ToList();
|
|
var ids = users.Select(u => u.Id).ToList();
|
|
|
- var bound = await _empRep.AsQueryable()
|
|
|
|
|
|
|
+ var bound = await ScopedEmployees(scope)
|
|
|
.Where(e => e.SysUserId.HasValue && ids.Contains(e.SysUserId!.Value))
|
|
.Where(e => e.SysUserId.HasValue && ids.Contains(e.SysUserId!.Value))
|
|
|
.Select(e => new { e.Id, e.SysUserId })
|
|
.Select(e => new { e.Id, e.SysUserId })
|
|
|
.ToListAsync();
|
|
.ToListAsync();
|
|
@@ -103,20 +119,23 @@ public class S8OperatorBindingService : ITransient
|
|
|
}).ToList();
|
|
}).ToList();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public async Task<AdoS8OperatorBindingRowDto> BindAsync(AdoS8OperatorBindingCreateDto dto)
|
|
|
|
|
|
|
+ public async Task<AdoS8OperatorBindingRowDto> BindAsync(S8TrustedScope scope, AdoS8OperatorBindingCreateDto dto)
|
|
|
{
|
|
{
|
|
|
if (dto.EmployeeId <= 0) throw new S8BizException("员工 ID 不能为空");
|
|
if (dto.EmployeeId <= 0) throw new S8BizException("员工 ID 不能为空");
|
|
|
if (dto.SysUserId <= 0) throw new S8BizException("系统账号 ID 不能为空");
|
|
if (dto.SysUserId <= 0) throw new S8BizException("系统账号 ID 不能为空");
|
|
|
|
|
|
|
|
- var emp = await _empRep.GetFirstAsync(x => x.Id == dto.EmployeeId)
|
|
|
|
|
|
|
+ // S8-P0-3:员工必须属于可信工厂。跨工厂 / 跨租户的 employeeId 一律按「不存在」处理,
|
|
|
|
|
+ // 文案与本租户内真实不存在的情况完全一致,避免被用来探测他租户员工是否存在。
|
|
|
|
|
+ var emp = await ScopedEmployee(scope, dto.EmployeeId)
|
|
|
?? throw new S8BizException("员工不存在");
|
|
?? throw new S8BizException("员工不存在");
|
|
|
|
|
|
|
|
- var tenantId = _userManager.TenantId;
|
|
|
|
|
- var user = await _sysUserRep.GetFirstAsync(x => x.Id == dto.SysUserId && x.TenantId == tenantId)
|
|
|
|
|
|
|
+ var user = await _sysUserRep.GetFirstAsync(x => x.Id == dto.SysUserId && x.TenantId == scope.TenantId)
|
|
|
?? throw new S8BizException("系统账号不存在或不在当前租户");
|
|
?? throw new S8BizException("系统账号不存在或不在当前租户");
|
|
|
|
|
|
|
|
- // 同一 sysUser 不允许绑定到另一个 employee
|
|
|
|
|
- var conflict = await _empRep.GetFirstAsync(x => x.SysUserId == dto.SysUserId && x.Id != dto.EmployeeId);
|
|
|
|
|
|
|
+ // 同一 sysUser 不允许绑定到另一个 employee(冲突检测同样限定在可信工厂内)
|
|
|
|
|
+ var conflict = await ScopedEmployees(scope)
|
|
|
|
|
+ .Where(x => x.SysUserId == dto.SysUserId && x.Id != dto.EmployeeId)
|
|
|
|
|
+ .FirstAsync();
|
|
|
if (conflict != null)
|
|
if (conflict != null)
|
|
|
throw new S8BizException($"系统账号 {user.Account} 已绑定至其它员工 {conflict.Name ?? conflict.Employee}");
|
|
throw new S8BizException($"系统账号 {user.Account} 已绑定至其它员工 {conflict.Name ?? conflict.Employee}");
|
|
|
|
|
|
|
@@ -139,10 +158,10 @@ public class S8OperatorBindingService : ITransient
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public async Task UnbindAsync(long employeeId)
|
|
|
|
|
|
|
+ public async Task UnbindAsync(S8TrustedScope scope, long employeeId)
|
|
|
{
|
|
{
|
|
|
if (employeeId <= 0) throw new S8BizException("员工 ID 不能为空");
|
|
if (employeeId <= 0) throw new S8BizException("员工 ID 不能为空");
|
|
|
- var emp = await _empRep.GetFirstAsync(x => x.Id == employeeId)
|
|
|
|
|
|
|
+ var emp = await ScopedEmployee(scope, employeeId)
|
|
|
?? throw new S8BizException("员工不存在");
|
|
?? throw new S8BizException("员工不存在");
|
|
|
if (!emp.SysUserId.HasValue) return;
|
|
if (!emp.SysUserId.HasValue) return;
|
|
|
|
|
|
|
@@ -153,4 +172,16 @@ public class S8OperatorBindingService : ITransient
|
|
|
.UpdateColumns(it => new { it.SysUserId, it.UpdateTime, it.UpdateUser })
|
|
.UpdateColumns(it => new { it.SysUserId, it.UpdateTime, it.UpdateUser })
|
|
|
.ExecuteCommandAsync();
|
|
.ExecuteCommandAsync();
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// S8-P0-3:EmployeeMaster 的唯一合法访问口径。
|
|
|
|
|
+ /// ClearFilter 是必要的(EmployeeMaster.tenant_id 属 S0 业务域,与登录 token 的系统租户历史错位),
|
|
|
|
|
+ /// 但**必须**同时带 FactoryRefId 强制硬边界 —— 谓词不可选、不接受调用方覆盖。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private ISugarQueryable<AdoS0EmployeeMaster> ScopedEmployees(S8TrustedScope scope) =>
|
|
|
|
|
+ _empRep.AsQueryable().ClearFilter()
|
|
|
|
|
+ .Where(x => x.FactoryRefId == scope.FactoryId);
|
|
|
|
|
+
|
|
|
|
|
+ private async Task<AdoS0EmployeeMaster?> ScopedEmployee(S8TrustedScope scope, long employeeId) =>
|
|
|
|
|
+ await ScopedEmployees(scope).Where(x => x.Id == employeeId).FirstAsync();
|
|
|
}
|
|
}
|