using Admin.NET.Core.Service; using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.Controllers.S0; /// /// S0 自有的**当前租户只读组织查询**(P1-2)。 /// /// 目的:让 S0 页面不再依赖平台 /api/sysOrg/list。 /// 平台该路由已登记为按钮权限 sysOrg:list(SysMenu Id=1300200030201), /// 普通 S0 业务角色为了填一个隐藏的公司/工厂归属字段而被迫持有平台机构管理权限, /// 并因菜单祖先补全在侧边栏暴露「系统管理 / 机构管理」入口。 /// 本路由 s0:common:orgs 未登记为按钮权限,走 JwtHandler「未登记即放行」分支, /// 普通业务角色无需任何平台机构权限即可读取本租户组织。 /// /// 边界(只读 + 只当前租户 + 最小字段): /// - 租户来自 (JWT claim),不接受 payload; /// - 复用 ,与平台口径**完全一致**(含 GetUserOrgIdList /// 数据权限收窄与 SysOrg 全局租户过滤),不新写一套查询逻辑、不放宽可见范围; /// - 只返回 id / pid / name / code / type 五个字段,不暴露机构管理的任何写能力; /// - 额外对结果再做一次当前租户断言,作为纵深防御。 /// /// 返回一律为**平铺列表**:平台 GetList 在不带 Type 条件时返回树,本接口统一拍平, /// 使调用方(loadOrgList)无需再关心树/平铺两种形态。 /// [ApiController] [Route("api/s0/common/orgs")] [NonUnify] public class AdoS0OrgsController : ControllerBase { private readonly SysOrgService _sysOrgService; public AdoS0OrgsController(SysOrgService sysOrgService) { _sysOrgService = sysOrgService; } /// /// 查询当前租户的组织列表。 为 SysOrg.Type(S0 约定 201=公司、501=工厂, /// 501 沿用 S0 legacy 兼容语义,见 AdoS0OrgScope 注释);不传则返回本租户全部组织。 /// [HttpGet] public async Task GetListAsync([FromQuery] string? type) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var raw = await _sysOrgService.GetList(new OrgInput { Id = 0, Type = type }); var flat = new List(); Flatten(raw, flat); var result = flat .Where(x => x.TenantId == tenantId) // 纵深防御:平台侧已按租户过滤,这里再断言一次 .GroupBy(x => x.Id) .Select(g => g.First()) .OrderBy(x => x.OrderNo).ThenBy(x => x.Id) .Select(x => new { id = x.Id.ToString(), // 雪花 Id 超 JS number 精度,一律以 string 下发 pid = x.Pid.ToString(), name = x.Name, code = x.Code, type = x.Type, }) .ToList(); return Ok(result); } /// 把可能的树形结果拍平(平台 GetList 不带条件时返回树,子节点在 Children 内)。 private static void Flatten(List? nodes, List sink) { if (nodes == null) return; foreach (var n in nodes) { sink.Add(n); Flatten(n.Children, sink); } } }