Skip to content

Commit 67acf0b

Browse files
committed
feat: 多租户权限增强 - 角色归属租户+菜单权限约束+用户编辑+审计日志
1 parent 169eb8f commit 67acf0b

15 files changed

Lines changed: 903 additions & 195 deletions

File tree

Juggle.Api/Controllers/Api/RoleController.cs

Lines changed: 126 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
using Juggle.Infrastructure.Persistence;
33
using Juggle.Application.Models.Request;
44
using Juggle.Application.Models.Response;
5+
using Juggle.Application.Services;
56
using Microsoft.AspNetCore.Authorization;
67
using Microsoft.AspNetCore.Mvc;
78
using Microsoft.EntityFrameworkCore;
9+
using System.Text.Json;
810

911
namespace Juggle.Api.Controllers.Api;
1012

@@ -13,47 +15,72 @@ namespace Juggle.Api.Controllers.Api;
1315
public class RoleController : ControllerBase
1416
{
1517
private readonly JuggleDbContext _db;
18+
private readonly ITenantAccessor _tenant;
1619

17-
public RoleController(JuggleDbContext db) => _db = db;
20+
public RoleController(JuggleDbContext db, ITenantAccessor tenant)
21+
{
22+
_db = db;
23+
_tenant = tenant;
24+
}
1825

1926
/// <summary>角色分页列表</summary>
2027
[HttpPost("page"), Authorize]
2128
public async Task<ApiResult> Page([FromBody] PageRequest req)
2229
{
30+
// 超级管理员看所有角色,其他人员只看本租户角色和全局角色(TenantId=null)
2331
var query = _db.Roles.Where(r => r.Deleted == 0);
32+
if (!_tenant.IsSuperAdmin)
33+
{
34+
// 非超管:显示全局角色(null TenantId)+ 本租户角色
35+
query = query.Where(r => r.TenantId == null || r.TenantId == _tenant.TenantId);
36+
}
37+
2438
if (!string.IsNullOrEmpty(req.Keyword))
2539
query = query.Where(r => r.RoleName!.Contains(req.Keyword) || (r.RoleCode != null && r.RoleCode.Contains(req.Keyword)));
2640

2741
var total = await query.CountAsync();
2842
var records = await query.OrderByDescending(r => r.Id)
2943
.Skip((req.PageNum - 1) * req.PageSize)
3044
.Take(req.PageSize)
45+
.Select(r => new
46+
{
47+
r.Id, r.RoleName, r.RoleCode, r.Remark, r.CreatedAt, r.UpdatedAt,
48+
r.TenantId,
49+
TenantName = _db.Tenants.Where(t => t.Id == r.TenantId && t.Deleted == 0).Select(t => t.TenantName).FirstOrDefault()!,
50+
MenuCount = _db.RoleMenus.Count(rm => rm.RoleId == r.Id && rm.Deleted == 0)
51+
})
3152
.ToListAsync();
3253

33-
// 查每个角色的菜单权限数
34-
var roleIds = records.Select(r => r.Id).ToList();
35-
var menuCounts = await _db.RoleMenus
36-
.Where(rm => roleIds.Contains(rm.RoleId) && rm.Deleted == 0)
37-
.GroupBy(rm => rm.RoleId)
38-
.Select(g => new { RoleId = g.Key, Count = g.Count() })
39-
.ToDictionaryAsync(x => x.RoleId, x => x.Count);
40-
41-
var result = records.Select(r => new
42-
{
43-
r.Id, r.RoleName, r.RoleCode, r.Remark, r.CreatedAt, r.UpdatedAt,
44-
MenuCount = menuCounts.GetValueOrDefault(r.Id, 0)
45-
});
46-
return ApiResult.Success(new { total, records = result });
54+
return ApiResult.Success(new { total, records });
4755
}
4856

49-
/// <summary>所有角色(下拉选择用)</summary>
57+
/// <summary>角色下拉列表(根据权限过滤)</summary>
5058
[HttpGet("all"), Authorize]
5159
public async Task<ApiResult> All()
5260
{
61+
var query = _db.Roles.Where(r => r.Deleted == 0);
62+
if (!_tenant.IsSuperAdmin)
63+
query = query.Where(r => r.TenantId == null || r.TenantId == _tenant.TenantId);
64+
65+
var list = await query.OrderBy(r => r.Id)
66+
.Select(r => new { r.Id, r.RoleName, r.RoleCode, r.TenantId })
67+
.ToListAsync();
68+
return ApiResult.Success(list);
69+
}
70+
71+
/// <summary>根据租户ID获取角色列表(用户编辑时使用)</summary>
72+
[HttpGet("byTenant/{tenantId}"), Authorize]
73+
public async Task<ApiResult> ByTenant(long tenantId)
74+
{
75+
// 超管可获取指定租户下所有角色
76+
// 非超管只能获取本租户的角色
77+
if (!_tenant.IsSuperAdmin && tenantId != _tenant.TenantId)
78+
return ApiResult.Fail("无权访问该租户角色", 403);
79+
5380
var list = await _db.Roles
54-
.Where(r => r.Deleted == 0)
81+
.Where(r => r.Deleted == 0 && (r.TenantId == tenantId || r.TenantId == null))
5582
.OrderBy(r => r.Id)
56-
.Select(r => new { r.Id, r.RoleName, r.RoleCode })
83+
.Select(r => new { r.Id, r.RoleName, r.RoleCode, r.TenantId })
5784
.ToListAsync();
5885
return ApiResult.Success(list);
5986
}
@@ -70,10 +97,24 @@ public async Task<ApiResult> Detail(long id)
7097
.Select(rm => rm.MenuKey)
7198
.ToListAsync();
7299

100+
// 获取角色所属租户的最大菜单权限
101+
List<string> tenantMenuKeys = new();
102+
if (role.TenantId.HasValue)
103+
{
104+
var tenant = await _db.Tenants.FindAsync(role.TenantId.Value);
105+
if (tenant != null)
106+
{
107+
try { tenantMenuKeys = JsonSerializer.Deserialize<List<string>>(tenant.MenuKeys ?? "[]") ?? new(); }
108+
catch { tenantMenuKeys = new(); }
109+
}
110+
}
111+
73112
return ApiResult.Success(new
74113
{
75114
role.Id, role.RoleName, role.RoleCode, role.Remark, role.CreatedAt,
76-
MenuKeys = menuKeys
115+
role.TenantId,
116+
MenuKeys = menuKeys,
117+
TenantMenuKeys = tenantMenuKeys // 角色可分配的最大权限边界
77118
});
78119
}
79120

@@ -84,17 +125,46 @@ public async Task<ApiResult> Add([FromBody] RoleAddRequest req)
84125
if (string.IsNullOrWhiteSpace(req.RoleName))
85126
return ApiResult.Fail("角色名称不能为空");
86127

128+
// 非超管不能设置租户
129+
if (!_tenant.IsSuperAdmin && req.TenantId != null)
130+
return ApiResult.Fail("无权指定租户", 403);
131+
132+
// 非超管只能创建本租户角色或全局角色
133+
if (!_tenant.IsSuperAdmin && req.TenantId != null && req.TenantId != _tenant.TenantId)
134+
return ApiResult.Fail("只能创建本租户的角色", 403);
135+
136+
// 租户编码唯一性
87137
if (!string.IsNullOrWhiteSpace(req.RoleCode))
88138
{
89139
var exists = await _db.Roles.AnyAsync(r => r.RoleCode == req.RoleCode && r.Deleted == 0);
90140
if (exists) return ApiResult.Fail("角色编码已存在");
91141
}
92142

143+
// 如果角色绑定租户,校验菜单权限不超出租户权限
144+
if (req.TenantId.HasValue)
145+
{
146+
var tenant = await _db.Tenants.FindAsync(req.TenantId.Value);
147+
if (tenant != null)
148+
{
149+
List<string> tenantMenuKeys;
150+
try { tenantMenuKeys = JsonSerializer.Deserialize<List<string>>(tenant.MenuKeys ?? "[]") ?? new(); }
151+
catch { tenantMenuKeys = new(); }
152+
153+
if (tenantMenuKeys.Count > 0)
154+
{
155+
var overKeys = req.MenuKeys.Where(k => !tenantMenuKeys.Contains(k)).ToList();
156+
if (overKeys.Count > 0)
157+
return ApiResult.Fail($"角色权限超出租户权限范围:{string.Join(", ", overKeys)}");
158+
}
159+
}
160+
}
161+
93162
var role = new RoleEntity
94163
{
95164
RoleName = req.RoleName,
96165
RoleCode = req.RoleCode,
97166
Remark = req.Remark,
167+
TenantId = req.TenantId,
98168
Deleted = 0,
99169
CreatedAt = DateTime.Now.ToString("o")
100170
};
@@ -106,10 +176,7 @@ public async Task<ApiResult> Add([FromBody] RoleAddRequest req)
106176
{
107177
_db.RoleMenus.AddRange(req.MenuKeys.Select(key => new RoleMenuEntity
108178
{
109-
RoleId = role.Id,
110-
MenuKey = key,
111-
Deleted = 0,
112-
CreatedAt = DateTime.Now.ToString("o")
179+
RoleId = role.Id, MenuKey = key, Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
113180
}));
114181
await _db.SaveChangesAsync();
115182
}
@@ -125,15 +192,41 @@ public async Task<ApiResult> Update([FromBody] RoleUpdateRequest req)
125192
if (role == null || role.Deleted == 1) return ApiResult.Fail("角色不存在");
126193
if (role.Id == 1) return ApiResult.Fail("不能修改超级管理员角色");
127194

195+
// 非超管不能修改租户
196+
if (!_tenant.IsSuperAdmin && req.TenantId != role.TenantId)
197+
return ApiResult.Fail("无权修改角色所属租户", 403);
198+
199+
// 非超管不能把角色移到其他租户
200+
if (!_tenant.IsSuperAdmin && req.TenantId != null && req.TenantId != _tenant.TenantId)
201+
return ApiResult.Fail("无权将角色移至其他租户", 403);
202+
203+
// 如果修改了租户,重新校验权限
204+
if (req.TenantId.HasValue)
205+
{
206+
var tenant = await _db.Tenants.FindAsync(req.TenantId.Value);
207+
if (tenant != null)
208+
{
209+
List<string> tenantMenuKeys;
210+
try { tenantMenuKeys = JsonSerializer.Deserialize<List<string>>(tenant.MenuKeys ?? "[]") ?? new(); }
211+
catch { tenantMenuKeys = new(); }
212+
213+
if (tenantMenuKeys.Count > 0)
214+
{
215+
var overKeys = req.MenuKeys.Where(k => !tenantMenuKeys.Contains(k)).ToList();
216+
if (overKeys.Count > 0)
217+
return ApiResult.Fail($"角色权限超出租户权限范围:{string.Join(", ", overKeys)}");
218+
}
219+
}
220+
}
221+
128222
role.RoleName = req.RoleName;
129223
role.RoleCode = req.RoleCode;
130224
role.Remark = req.Remark;
225+
role.TenantId = req.TenantId;
131226
role.UpdatedAt = DateTime.Now.ToString("o");
132227

133228
// 先删除旧权限
134-
var oldMenus = await _db.RoleMenus
135-
.Where(rm => rm.RoleId == req.Id && rm.Deleted == 0)
136-
.ToListAsync();
229+
var oldMenus = await _db.RoleMenus.Where(rm => rm.RoleId == req.Id && rm.Deleted == 0).ToListAsync();
137230
foreach (var m in oldMenus) m.Deleted = 1;
138231
_db.RoleMenus.UpdateRange(oldMenus);
139232

@@ -142,10 +235,7 @@ public async Task<ApiResult> Update([FromBody] RoleUpdateRequest req)
142235
{
143236
_db.RoleMenus.AddRange(req.MenuKeys.Select(key => new RoleMenuEntity
144237
{
145-
RoleId = req.Id,
146-
MenuKey = key,
147-
Deleted = 0,
148-
CreatedAt = DateTime.Now.ToString("o")
238+
RoleId = req.Id, MenuKey = key, Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
149239
}));
150240
}
151241

@@ -158,17 +248,20 @@ public async Task<ApiResult> Update([FromBody] RoleUpdateRequest req)
158248
public async Task<ApiResult> Delete(long id)
159249
{
160250
if (id == 1) return ApiResult.Fail("不能删除超级管理员角色");
251+
161252
var role = await _db.Roles.FindAsync(id);
162253
if (role == null || role.Deleted == 1) return ApiResult.Fail("角色不存在");
163254

164-
// 检查是否有用户使用该角色
255+
// 非超管只能删除本租户角色
256+
if (!_tenant.IsSuperAdmin && role.TenantId != _tenant.TenantId && role.TenantId != null)
257+
return ApiResult.Fail("无权删除该角色", 403);
258+
165259
var hasUsers = await _db.Users.AnyAsync(u => u.RoleId == id && u.Deleted == 0);
166260
if (hasUsers) return ApiResult.Fail("该角色下还有用户,不能删除");
167261

168-
role.Deleted = 1;
262+
role.Deleted = 1;
169263
role.UpdatedAt = DateTime.Now.ToString("o");
170264

171-
// 同时删除角色菜单
172265
var menus = await _db.RoleMenus.Where(rm => rm.RoleId == id && rm.Deleted == 0).ToListAsync();
173266
foreach (var m in menus) m.Deleted = 1;
174267
_db.RoleMenus.UpdateRange(menus);

0 commit comments

Comments
 (0)