瀏覽代碼

feat(s8): add operator binding config APIs

Add /api/aidop/s8/config endpoints to manage the EmployeeMaster.SysUserId
mapping introduced for task-flow auth, so the binding can be maintained
inside the S8 config page instead of through SQL.

GET  /operator-bindings   list with BOUND/UNBOUND filter and keyword
POST /operator-bindings   bind employee → sys user, reject if the sys
                           user is already bound to a different employee
DELETE /operator-bindings/{id}  unbind (set SysUserId to null)
GET  /sys-users           tenant-scoped read-only sys user picker that
                           projects only id/account/realName/status and
                           marks already-bound rows for the front-end to
                           disable

The endpoints touch only EmployeeMaster.SysUserId; SysUser, SysRole and
SysUserRole are read-only and untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YY968XX 3 周之前
父節點
當前提交
b0b35b9284

+ 46 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8ConfigBindingsController.cs

@@ -0,0 +1,46 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8;
+using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
+using Admin.NET.Plugin.AiDOP.Service.S8;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
+
+/// <summary>
+/// S8 配置页:操作员(员工)↔ 系统账号绑定 / 可绑账号查询。
+/// 入口收口在 /aidop/s8/config/roles,与 S8 配置中心同源。
+/// </summary>
+[ApiController]
+[Route("api/aidop/s8/config")]
+[NonUnify]
+public class AdoS8ConfigBindingsController : ControllerBase
+{
+    private readonly S8OperatorBindingService _svc;
+
+    public AdoS8ConfigBindingsController(S8OperatorBindingService svc) => _svc = svc;
+
+    [HttpGet("operator-bindings")]
+    public async Task<IActionResult> ListAsync(
+        [FromQuery] long? factoryRefId = null,
+        [FromQuery] string? bindStatus = null,
+        [FromQuery] string? keyword = null)
+        => Ok(await _svc.ListAsync(factoryRefId, bindStatus, keyword));
+
+    [HttpPost("operator-bindings")]
+    public async Task<IActionResult> BindAsync([FromBody] AdoS8OperatorBindingCreateDto body)
+    {
+        try { return Ok(await _svc.BindAsync(body)); }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
+
+    [HttpDelete("operator-bindings/{employeeId:long}")]
+    public async Task<IActionResult> UnbindAsync(long employeeId)
+    {
+        try { await _svc.UnbindAsync(employeeId); return Ok(new { employeeId }); }
+        catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
+    }
+
+    [HttpGet("sys-users")]
+    public async Task<IActionResult> SysUsersAsync(
+        [FromQuery] string? keyword = null,
+        [FromQuery] long? excludeEmployeeId = null)
+        => Ok(await _svc.ListSysUsersAsync(keyword, excludeEmployeeId));
+}

+ 28 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8OperatorBindingDtos.cs

@@ -0,0 +1,28 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.S8;
+
+public class AdoS8OperatorBindingRowDto
+{
+    public long EmployeeId { get; set; }
+    public string EmpCode { get; set; } = string.Empty;
+    public string? Name { get; set; }
+    public long FactoryRefId { get; set; }
+    public long? SysUserId { get; set; }
+    public string? SysUserName { get; set; }
+    public string BindStatus { get; set; } = "UNBOUND";
+}
+
+public class AdoS8OperatorBindingCreateDto
+{
+    public long EmployeeId { get; set; }
+    public long SysUserId { get; set; }
+    public long FactoryRefId { get; set; }
+}
+
+public class AdoS8ConfigSysUserRowDto
+{
+    public long Id { get; set; }
+    public string? Account { get; set; }
+    public string? RealName { get; set; }
+    public int Status { get; set; }
+    public long? AlreadyBoundEmployeeId { get; set; }
+}

+ 154 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8OperatorBindingService.cs

@@ -0,0 +1,154 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
+using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
+
+namespace Admin.NET.Plugin.AiDOP.Service.S8;
+
+/// <summary>
+/// S8 配置页:操作员(员工)↔ 系统账号绑定。
+/// 仅维护 EmployeeMaster.SysUserId 的 1:1 弱关联;不动 SysUser/SysRole/SysUserRole。
+/// </summary>
+public class S8OperatorBindingService : ITransient
+{
+    private readonly SqlSugarRepository<AdoS0EmployeeMaster> _empRep;
+    private readonly SqlSugarRepository<SysUser> _sysUserRep;
+    private readonly UserManager _userManager;
+
+    public S8OperatorBindingService(
+        SqlSugarRepository<AdoS0EmployeeMaster> empRep,
+        SqlSugarRepository<SysUser> sysUserRep,
+        UserManager userManager)
+    {
+        _empRep = empRep;
+        _sysUserRep = sysUserRep;
+        _userManager = userManager;
+    }
+
+    public async Task<List<AdoS8OperatorBindingRowDto>> ListAsync(long? factoryRefId, string? bindStatus, string? keyword)
+    {
+        var emps = await _empRep.AsQueryable()
+            .WhereIF(factoryRefId.HasValue, x => x.FactoryRefId == factoryRefId!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(keyword),
+                x => x.Employee.Contains(keyword!) || (x.Name != null && x.Name.Contains(keyword!)))
+            .Take(500)
+            .Select(x => new { x.Id, x.Name, x.Employee, x.FactoryRefId, x.SysUserId })
+            .ToListAsync();
+
+        var sysUserIds = emps.Where(e => e.SysUserId.HasValue && e.SysUserId.Value > 0)
+            .Select(e => e.SysUserId!.Value).Distinct().ToList();
+        var userMap = sysUserIds.Count == 0
+            ? new Dictionary<long, string?>()
+            : (await _sysUserRep.AsQueryable()
+                .Where(u => sysUserIds.Contains(u.Id) && u.TenantId == _userManager.TenantId)
+                .Select(u => new { u.Id, u.RealName, u.Account })
+                .ToListAsync())
+              .ToDictionary(u => u.Id, u => string.IsNullOrWhiteSpace(u.RealName) ? u.Account : u.RealName);
+
+        var rows = emps.Select(e =>
+        {
+            var bound = e.SysUserId.HasValue && e.SysUserId.Value > 0 && userMap.ContainsKey(e.SysUserId.Value);
+            return new AdoS8OperatorBindingRowDto
+            {
+                EmployeeId = e.Id,
+                EmpCode = e.Employee,
+                Name = e.Name,
+                FactoryRefId = e.FactoryRefId,
+                SysUserId = e.SysUserId,
+                SysUserName = bound ? userMap[e.SysUserId!.Value] : null,
+                BindStatus = bound ? "BOUND" : "UNBOUND"
+            };
+        });
+
+        if (!string.IsNullOrWhiteSpace(bindStatus) && (bindStatus == "BOUND" || bindStatus == "UNBOUND"))
+            rows = rows.Where(r => r.BindStatus == bindStatus);
+
+        return rows
+            .OrderBy(r => r.BindStatus == "BOUND" ? 0 : 1)
+            .ThenBy(r => r.EmpCode)
+            .ToList();
+    }
+
+    public async Task<List<AdoS8ConfigSysUserRowDto>> ListSysUsersAsync(string? keyword, long? excludeEmployeeId)
+    {
+        var tenantId = _userManager.TenantId;
+        var users = await _sysUserRep.AsQueryable()
+            .Where(u => u.TenantId == tenantId)
+            .WhereIF(!string.IsNullOrWhiteSpace(keyword),
+                u => u.Account.Contains(keyword!) || u.RealName.Contains(keyword!))
+            .Take(200)
+            .Select(u => new { u.Id, u.Account, u.RealName, u.Status })
+            .ToListAsync();
+
+        if (users.Count == 0) return new();
+
+        // 标注哪些 sysUser 已被其它 employee 绑定(用于前端禁选/隐藏);
+        // 当前 employee 自己绑定的 sysUser 在 alreadyBoundEmployeeId == excludeEmployeeId 时会被前端放行。
+        var ids = users.Select(u => u.Id).ToList();
+        var bound = await _empRep.AsQueryable()
+            .Where(e => e.SysUserId.HasValue && ids.Contains(e.SysUserId!.Value))
+            .Select(e => new { e.Id, e.SysUserId })
+            .ToListAsync();
+        var boundMap = bound.GroupBy(b => b.SysUserId!.Value)
+            .ToDictionary(g => g.Key, g => g.First().Id);
+
+        return users.Select(u => new AdoS8ConfigSysUserRowDto
+        {
+            Id = u.Id,
+            Account = u.Account,
+            RealName = u.RealName,
+            Status = (int)u.Status,
+            AlreadyBoundEmployeeId = boundMap.TryGetValue(u.Id, out var empId) ? empId : null
+        }).ToList();
+    }
+
+    public async Task<AdoS8OperatorBindingRowDto> BindAsync(AdoS8OperatorBindingCreateDto dto)
+    {
+        if (dto.EmployeeId <= 0) throw new S8BizException("员工 ID 不能为空");
+        if (dto.SysUserId <= 0) throw new S8BizException("系统账号 ID 不能为空");
+
+        var emp = await _empRep.GetFirstAsync(x => x.Id == dto.EmployeeId)
+            ?? throw new S8BizException("员工不存在");
+
+        var tenantId = _userManager.TenantId;
+        var user = await _sysUserRep.GetFirstAsync(x => x.Id == dto.SysUserId && x.TenantId == tenantId)
+            ?? throw new S8BizException("系统账号不存在或不在当前租户");
+
+        // 同一 sysUser 不允许绑定到另一个 employee
+        var conflict = await _empRep.GetFirstAsync(x => x.SysUserId == dto.SysUserId && x.Id != dto.EmployeeId);
+        if (conflict != null)
+            throw new S8BizException($"系统账号 {user.Account} 已绑定至其它员工 {conflict.Name ?? conflict.Employee}");
+
+        emp.SysUserId = dto.SysUserId;
+        emp.UpdateTime = DateTime.Now;
+        emp.UpdateUser = _userManager.Account;
+        await _empRep.AsUpdateable(emp)
+            .UpdateColumns(it => new { it.SysUserId, it.UpdateTime, it.UpdateUser })
+            .ExecuteCommandAsync();
+
+        return new AdoS8OperatorBindingRowDto
+        {
+            EmployeeId = emp.Id,
+            EmpCode = emp.Employee,
+            Name = emp.Name,
+            FactoryRefId = emp.FactoryRefId,
+            SysUserId = dto.SysUserId,
+            SysUserName = string.IsNullOrWhiteSpace(user.RealName) ? user.Account : user.RealName,
+            BindStatus = "BOUND"
+        };
+    }
+
+    public async Task UnbindAsync(long employeeId)
+    {
+        if (employeeId <= 0) throw new S8BizException("员工 ID 不能为空");
+        var emp = await _empRep.GetFirstAsync(x => x.Id == employeeId)
+            ?? throw new S8BizException("员工不存在");
+        if (!emp.SysUserId.HasValue) return;
+
+        emp.SysUserId = null;
+        emp.UpdateTime = DateTime.Now;
+        emp.UpdateUser = _userManager.Account;
+        await _empRep.AsUpdateable(emp)
+            .UpdateColumns(it => new { it.SysUserId, it.UpdateTime, it.UpdateUser })
+            .ExecuteCommandAsync();
+    }
+}