Skip to content

Commit 169eb8f

Browse files
committed
feat: 角色权限管理 + 多租户 + 配置文件数据库支持(需求26/27/28)
需求26 - 角色管理 + 菜单权限: - 新增 RoleEntity / RoleMenuEntity 实体 - RoleController: 角色CRUD + 菜单权限分配(checkbox tree) - 登录返回 roleCode 和 menuKeys - Layout.vue 根据 menuKeys 动态过滤侧边栏菜单 - 超级管理员(roleCode=admin)显示所有菜单 需求27 - 系统数据库配置文件支持: - appsettings.json 增加 Database 配置节 - 支持 sqlite / sqlserver / mysql / postgresql 四种数据库 - 每种数据库可独立配置 Host/Port/UserId/Password 等 - 优先级: 环境变量 > appsettings.json > 默认值 - 保留环境变量 DB_TYPE / DB_CONNECTION_STRING 向后兼容 需求28 - 多租户实现: - 新增 TenantEntity 实体 + TenantController CRUD - UserEntity 增加 RoleId / TenantId 外键 - 登录时校验租户状态(禁用租户无法登录) - ITenantAccessor 中间件从 JWT Claims 提取 TenantId - 用户管理支持绑定角色和租户 前端新增页面: - RoleManage.vue: 角色列表 + 新增/编辑弹窗(含菜单权限树) - TenantManage.vue: 租户列表 + 新增/编辑弹窗(含状态开关) - UserManage.vue: 增加角色/租户列 + 新增弹窗角色/租户下拉选择 - Login.vue: 登录时保存 roleCode/menuKeys - router: 增加 /system/role 和 /system/tenant 路由
1 parent 034a9f7 commit 169eb8f

21 files changed

Lines changed: 1033 additions & 94 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using Juggle.Domain.Entities;
2+
using Juggle.Infrastructure.Persistence;
3+
using Juggle.Application.Models.Request;
4+
using Juggle.Application.Models.Response;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.EntityFrameworkCore;
8+
9+
namespace Juggle.Api.Controllers.Api;
10+
11+
[ApiController]
12+
[Route("api/role")]
13+
public class RoleController : ControllerBase
14+
{
15+
private readonly JuggleDbContext _db;
16+
17+
public RoleController(JuggleDbContext db) => _db = db;
18+
19+
/// <summary>角色分页列表</summary>
20+
[HttpPost("page"), Authorize]
21+
public async Task<ApiResult> Page([FromBody] PageRequest req)
22+
{
23+
var query = _db.Roles.Where(r => r.Deleted == 0);
24+
if (!string.IsNullOrEmpty(req.Keyword))
25+
query = query.Where(r => r.RoleName!.Contains(req.Keyword) || (r.RoleCode != null && r.RoleCode.Contains(req.Keyword)));
26+
27+
var total = await query.CountAsync();
28+
var records = await query.OrderByDescending(r => r.Id)
29+
.Skip((req.PageNum - 1) * req.PageSize)
30+
.Take(req.PageSize)
31+
.ToListAsync();
32+
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 });
47+
}
48+
49+
/// <summary>所有角色(下拉选择用)</summary>
50+
[HttpGet("all"), Authorize]
51+
public async Task<ApiResult> All()
52+
{
53+
var list = await _db.Roles
54+
.Where(r => r.Deleted == 0)
55+
.OrderBy(r => r.Id)
56+
.Select(r => new { r.Id, r.RoleName, r.RoleCode })
57+
.ToListAsync();
58+
return ApiResult.Success(list);
59+
}
60+
61+
/// <summary>角色详情(含菜单权限)</summary>
62+
[HttpGet("detail/{id}"), Authorize]
63+
public async Task<ApiResult> Detail(long id)
64+
{
65+
var role = await _db.Roles.FindAsync(id);
66+
if (role == null || role.Deleted == 1) return ApiResult.Fail("角色不存在");
67+
68+
var menuKeys = await _db.RoleMenus
69+
.Where(rm => rm.RoleId == id && rm.Deleted == 0)
70+
.Select(rm => rm.MenuKey)
71+
.ToListAsync();
72+
73+
return ApiResult.Success(new
74+
{
75+
role.Id, role.RoleName, role.RoleCode, role.Remark, role.CreatedAt,
76+
MenuKeys = menuKeys
77+
});
78+
}
79+
80+
/// <summary>新增角色</summary>
81+
[HttpPost("add"), Authorize]
82+
public async Task<ApiResult> Add([FromBody] RoleAddRequest req)
83+
{
84+
if (string.IsNullOrWhiteSpace(req.RoleName))
85+
return ApiResult.Fail("角色名称不能为空");
86+
87+
if (!string.IsNullOrWhiteSpace(req.RoleCode))
88+
{
89+
var exists = await _db.Roles.AnyAsync(r => r.RoleCode == req.RoleCode && r.Deleted == 0);
90+
if (exists) return ApiResult.Fail("角色编码已存在");
91+
}
92+
93+
var role = new RoleEntity
94+
{
95+
RoleName = req.RoleName,
96+
RoleCode = req.RoleCode,
97+
Remark = req.Remark,
98+
Deleted = 0,
99+
CreatedAt = DateTime.Now.ToString("o")
100+
};
101+
_db.Roles.Add(role);
102+
await _db.SaveChangesAsync();
103+
104+
// 保存菜单权限
105+
if (req.MenuKeys.Count > 0)
106+
{
107+
_db.RoleMenus.AddRange(req.MenuKeys.Select(key => new RoleMenuEntity
108+
{
109+
RoleId = role.Id,
110+
MenuKey = key,
111+
Deleted = 0,
112+
CreatedAt = DateTime.Now.ToString("o")
113+
}));
114+
await _db.SaveChangesAsync();
115+
}
116+
117+
return ApiResult.Success(new { id = role.Id });
118+
}
119+
120+
/// <summary>更新角色</summary>
121+
[HttpPut("update"), Authorize]
122+
public async Task<ApiResult> Update([FromBody] RoleUpdateRequest req)
123+
{
124+
var role = await _db.Roles.FindAsync(req.Id);
125+
if (role == null || role.Deleted == 1) return ApiResult.Fail("角色不存在");
126+
if (role.Id == 1) return ApiResult.Fail("不能修改超级管理员角色");
127+
128+
role.RoleName = req.RoleName;
129+
role.RoleCode = req.RoleCode;
130+
role.Remark = req.Remark;
131+
role.UpdatedAt = DateTime.Now.ToString("o");
132+
133+
// 先删除旧权限
134+
var oldMenus = await _db.RoleMenus
135+
.Where(rm => rm.RoleId == req.Id && rm.Deleted == 0)
136+
.ToListAsync();
137+
foreach (var m in oldMenus) m.Deleted = 1;
138+
_db.RoleMenus.UpdateRange(oldMenus);
139+
140+
// 添加新权限
141+
if (req.MenuKeys.Count > 0)
142+
{
143+
_db.RoleMenus.AddRange(req.MenuKeys.Select(key => new RoleMenuEntity
144+
{
145+
RoleId = req.Id,
146+
MenuKey = key,
147+
Deleted = 0,
148+
CreatedAt = DateTime.Now.ToString("o")
149+
}));
150+
}
151+
152+
await _db.SaveChangesAsync();
153+
return ApiResult.Success();
154+
}
155+
156+
/// <summary>删除角色</summary>
157+
[HttpDelete("delete/{id}"), Authorize]
158+
public async Task<ApiResult> Delete(long id)
159+
{
160+
if (id == 1) return ApiResult.Fail("不能删除超级管理员角色");
161+
var role = await _db.Roles.FindAsync(id);
162+
if (role == null || role.Deleted == 1) return ApiResult.Fail("角色不存在");
163+
164+
// 检查是否有用户使用该角色
165+
var hasUsers = await _db.Users.AnyAsync(u => u.RoleId == id && u.Deleted == 0);
166+
if (hasUsers) return ApiResult.Fail("该角色下还有用户,不能删除");
167+
168+
role.Deleted = 1;
169+
role.UpdatedAt = DateTime.Now.ToString("o");
170+
171+
// 同时删除角色菜单
172+
var menus = await _db.RoleMenus.Where(rm => rm.RoleId == id && rm.Deleted == 0).ToListAsync();
173+
foreach (var m in menus) m.Deleted = 1;
174+
_db.RoleMenus.UpdateRange(menus);
175+
176+
await _db.SaveChangesAsync();
177+
return ApiResult.Success();
178+
}
179+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using Juggle.Domain.Entities;
2+
using Juggle.Infrastructure.Persistence;
3+
using Juggle.Application.Models.Request;
4+
using Juggle.Application.Models.Response;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.EntityFrameworkCore;
8+
9+
namespace Juggle.Api.Controllers.Api;
10+
11+
[ApiController]
12+
[Route("api/tenant")]
13+
public class TenantController : ControllerBase
14+
{
15+
private readonly JuggleDbContext _db;
16+
17+
public TenantController(JuggleDbContext db) => _db = db;
18+
19+
/// <summary>租户分页列表</summary>
20+
[HttpPost("page"), Authorize]
21+
public async Task<ApiResult> Page([FromBody] PageRequest req)
22+
{
23+
var query = _db.Tenants.Where(t => t.Deleted == 0);
24+
if (!string.IsNullOrEmpty(req.Keyword))
25+
query = query.Where(t => t.TenantName!.Contains(req.Keyword) || (t.TenantCode != null && t.TenantCode.Contains(req.Keyword)));
26+
27+
var total = await query.CountAsync();
28+
var records = await query.OrderByDescending(t => t.Id)
29+
.Skip((req.PageNum - 1) * req.PageSize)
30+
.Take(req.PageSize)
31+
.Select(t => new
32+
{
33+
t.Id, t.TenantName, t.TenantCode, t.Status, t.Remark, t.CreatedAt, t.UpdatedAt,
34+
UserCount = _db.Users.Count(u => u.TenantId == t.Id && u.Deleted == 0)
35+
})
36+
.ToListAsync();
37+
38+
return ApiResult.Success(new { total, records });
39+
}
40+
41+
/// <summary>所有租户(下拉选择用)</summary>
42+
[HttpGet("all"), Authorize]
43+
public async Task<ApiResult> All()
44+
{
45+
var list = await _db.Tenants
46+
.Where(t => t.Deleted == 0 && t.Status == 1)
47+
.OrderBy(t => t.Id)
48+
.Select(t => new { t.Id, t.TenantName, t.TenantCode })
49+
.ToListAsync();
50+
return ApiResult.Success(list);
51+
}
52+
53+
/// <summary>新增租户</summary>
54+
[HttpPost("add"), Authorize]
55+
public async Task<ApiResult> Add([FromBody] TenantAddRequest req)
56+
{
57+
if (string.IsNullOrWhiteSpace(req.TenantName))
58+
return ApiResult.Fail("租户名称不能为空");
59+
60+
if (!string.IsNullOrWhiteSpace(req.TenantCode))
61+
{
62+
var exists = await _db.Tenants.AnyAsync(t => t.TenantCode == req.TenantCode && t.Deleted == 0);
63+
if (exists) return ApiResult.Fail("租户编码已存在");
64+
}
65+
66+
var tenant = new TenantEntity
67+
{
68+
TenantName = req.TenantName,
69+
TenantCode = req.TenantCode,
70+
Remark = req.Remark,
71+
Status = 1,
72+
Deleted = 0,
73+
CreatedAt = DateTime.Now.ToString("o")
74+
};
75+
_db.Tenants.Add(tenant);
76+
await _db.SaveChangesAsync();
77+
return ApiResult.Success(new { id = tenant.Id });
78+
}
79+
80+
/// <summary>更新租户</summary>
81+
[HttpPut("update"), Authorize]
82+
public async Task<ApiResult> Update([FromBody] TenantUpdateRequest req)
83+
{
84+
var tenant = await _db.Tenants.FindAsync(req.Id);
85+
if (tenant == null || tenant.Deleted == 1) return ApiResult.Fail("租户不存在");
86+
if (tenant.Id == 1) return ApiResult.Fail("不能修改默认租户");
87+
88+
tenant.TenantName = req.TenantName;
89+
tenant.TenantCode = req.TenantCode;
90+
tenant.Remark = req.Remark;
91+
tenant.Status = req.Status;
92+
tenant.UpdatedAt = DateTime.Now.ToString("o");
93+
94+
await _db.SaveChangesAsync();
95+
return ApiResult.Success();
96+
}
97+
98+
/// <summary>删除租户</summary>
99+
[HttpDelete("delete/{id}"), Authorize]
100+
public async Task<ApiResult> Delete(long id)
101+
{
102+
if (id == 1) return ApiResult.Fail("不能删除默认租户");
103+
var tenant = await _db.Tenants.FindAsync(id);
104+
if (tenant == null || tenant.Deleted == 1) return ApiResult.Fail("租户不存在");
105+
106+
var hasUsers = await _db.Users.AnyAsync(u => u.TenantId == id && u.Deleted == 0);
107+
if (hasUsers) return ApiResult.Fail("该租户下还有用户,不能删除");
108+
109+
tenant.Deleted = 1;
110+
tenant.UpdatedAt = DateTime.Now.ToString("o");
111+
await _db.SaveChangesAsync();
112+
return ApiResult.Success();
113+
}
114+
}

Juggle.Api/Controllers/Api/UserController.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,35 @@ public async Task<ApiResult> Login([FromBody] LoginRequest req)
3434
if (user == null)
3535
return ApiResult.Fail("用户名或密码错误", 401);
3636

37+
// 检查租户是否禁用
38+
if (user.TenantId.HasValue)
39+
{
40+
var tenant = await _db.Tenants.FindAsync(user.TenantId.Value);
41+
if (tenant == null || tenant.Deleted == 1 || tenant.Status == 0)
42+
return ApiResult.Fail("租户已被禁用,无法登录", 403);
43+
}
44+
3745
var tokenStr = _jwtService.GenerateToken(user);
38-
return ApiResult.Success(new { token = tokenStr, userName = user.UserName });
46+
47+
// 查询菜单权限
48+
List<string> menuKeys = new();
49+
if (user.RoleId.HasValue && user.RoleId != 1)
50+
{
51+
// 非超级管理员,查询角色菜单
52+
menuKeys = await _db.RoleMenus
53+
.Where(rm => rm.RoleId == user.RoleId && rm.Deleted == 0)
54+
.Select(rm => rm.MenuKey)
55+
.ToListAsync();
56+
}
57+
// 超级管理员返回空列表,前端根据 roleCode="admin" 显示全部菜单
58+
59+
return ApiResult.Success(new
60+
{
61+
token = tokenStr,
62+
userName = user.UserName,
63+
roleCode = user.RoleId == 1 ? "admin" : "",
64+
menuKeys
65+
});
3966
}
4067

4168
/// <summary>用户分页列表(需登录)</summary>
@@ -49,7 +76,12 @@ public async Task<ApiResult> Page([FromBody] PageRequest req)
4976
var records = await query.OrderByDescending(u => u.Id)
5077
.Skip((req.PageNum - 1) * req.PageSize)
5178
.Take(req.PageSize)
52-
.Select(u => new { u.Id, u.UserName, u.CreatedAt, u.UpdatedAt })
79+
.Select(u => new
80+
{
81+
u.Id, u.UserName, u.RoleId, u.TenantId, u.CreatedAt, u.UpdatedAt,
82+
RoleName = _db.Roles.Where(r => r.Id == u.RoleId && r.Deleted == 0).Select(r => r.RoleName).FirstOrDefault(),
83+
TenantName = _db.Tenants.Where(t => t.Id == u.TenantId && t.Deleted == 0).Select(t => t.TenantName).FirstOrDefault()
84+
})
5385
.ToListAsync();
5486
return ApiResult.Success(new { total, records });
5587
}
@@ -69,6 +101,8 @@ public async Task<ApiResult> Add([FromBody] UserAddRequest req)
69101
{
70102
UserName = req.UserName,
71103
Password = Md5Helper.Encrypt(req.Password),
104+
RoleId = req.RoleId,
105+
TenantId = req.TenantId,
72106
Deleted = 0,
73107
CreatedAt = DateTime.Now.ToString("o")
74108
};

0 commit comments

Comments
 (0)