Merge branch 'master' of http://192.168.195.14:3000/outsource/mi-assessment
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
d6db9996be
|
|
@ -24,10 +24,15 @@ public class CreateAdminUserRequest
|
|||
public string Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 真实姓名
|
||||
/// 真实姓名(为空时自动转 null)
|
||||
/// </summary>
|
||||
[MaxLength(50, ErrorMessage = "真实姓名最多50个字符")]
|
||||
public string? RealName { get; set; }
|
||||
public string? RealName
|
||||
{
|
||||
get => _realName;
|
||||
set => _realName = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
private string? _realName;
|
||||
|
||||
/// <summary>
|
||||
/// 头像URL
|
||||
|
|
@ -36,17 +41,27 @@ public class CreateAdminUserRequest
|
|||
public string? Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱
|
||||
/// 邮箱(为空时跳过格式验证)
|
||||
/// </summary>
|
||||
[MaxLength(100, ErrorMessage = "邮箱最多100个字符")]
|
||||
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
|
||||
public string? Email { get; set; }
|
||||
public string? Email
|
||||
{
|
||||
get => _email;
|
||||
set => _email = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
private string? _email;
|
||||
|
||||
/// <summary>
|
||||
/// 手机号
|
||||
/// 手机号(为空时自动转 null)
|
||||
/// </summary>
|
||||
[MaxLength(20, ErrorMessage = "手机号最多20个字符")]
|
||||
public string? Phone { get; set; }
|
||||
public string? Phone
|
||||
{
|
||||
get => _phone;
|
||||
set => _phone = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
private string? _phone;
|
||||
|
||||
/// <summary>
|
||||
/// 部门ID
|
||||
|
|
|
|||
|
|
@ -8,10 +8,15 @@ namespace MiAssessment.Admin.Models.AdminUser;
|
|||
public class UpdateAdminUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 真实姓名
|
||||
/// 真实姓名(为空时自动转 null)
|
||||
/// </summary>
|
||||
[MaxLength(50, ErrorMessage = "真实姓名最多50个字符")]
|
||||
public string? RealName { get; set; }
|
||||
public string? RealName
|
||||
{
|
||||
get => _realName;
|
||||
set => _realName = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
private string? _realName;
|
||||
|
||||
/// <summary>
|
||||
/// 头像URL
|
||||
|
|
@ -20,17 +25,27 @@ public class UpdateAdminUserRequest
|
|||
public string? Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱
|
||||
/// 邮箱(为空时跳过格式验证)
|
||||
/// </summary>
|
||||
[MaxLength(100, ErrorMessage = "邮箱最多100个字符")]
|
||||
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
|
||||
public string? Email { get; set; }
|
||||
public string? Email
|
||||
{
|
||||
get => _email;
|
||||
set => _email = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
private string? _email;
|
||||
|
||||
/// <summary>
|
||||
/// 手机号
|
||||
/// 手机号(为空时自动转 null)
|
||||
/// </summary>
|
||||
[MaxLength(20, ErrorMessage = "手机号最多20个字符")]
|
||||
public string? Phone { get; set; }
|
||||
public string? Phone
|
||||
{
|
||||
get => _phone;
|
||||
set => _phone = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
private string? _phone;
|
||||
|
||||
/// <summary>
|
||||
/// 部门ID
|
||||
|
|
|
|||
|
|
@ -15,15 +15,17 @@ public class AdminUserService : IAdminUserService
|
|||
private readonly AdminDbContext _dbContext;
|
||||
private readonly ILogger<AdminUserService> _logger;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IPermissionService _permissionService;
|
||||
|
||||
// 超级管理员角色编码
|
||||
private const string SuperAdminRoleCode = "super_admin";
|
||||
|
||||
public AdminUserService(AdminDbContext dbContext, ILogger<AdminUserService> logger, IAuthService authService)
|
||||
public AdminUserService(AdminDbContext dbContext, ILogger<AdminUserService> logger, IAuthService authService, IPermissionService permissionService)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
_authService = authService;
|
||||
_permissionService = permissionService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -310,6 +312,7 @@ public class AdminUserService : IAdminUserService
|
|||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
_permissionService.InvalidateCache(userId);
|
||||
_logger.LogInformation("管理员 {UserId} 分配角色成功,角色数量: {Count}", userId, roleIds.Count);
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +356,10 @@ public class AdminUserService : IAdminUserService
|
|||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
// 清除用户权限缓存,使新菜单权限立即生效
|
||||
_permissionService.InvalidateCache(userId);
|
||||
|
||||
_logger.LogInformation("管理员 {UserId} 分配用户专属菜单成功,菜单数量: {Count}", userId, menuIds.Count);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -212,14 +212,71 @@ public class PermissionService : IPermissionService
|
|||
|
||||
var roleIds = userRoles.Select(ur => ur.RoleId).ToList();
|
||||
|
||||
// 获取角色关联的权限
|
||||
var permissions = await _dbContext.RolePermissions
|
||||
// 1. 获取角色关联的权限(permissions 表)
|
||||
var rolePermissions = await _dbContext.RolePermissions
|
||||
.Where(rp => roleIds.Contains(rp.RoleId))
|
||||
.Include(rp => rp.Permission)
|
||||
.Select(rp => rp.Permission.Code)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
return permissions;
|
||||
// 2. 收集角色关联的菜单 ID
|
||||
var roleMenuIds = await _dbContext.RoleMenus
|
||||
.Where(rm => roleIds.Contains(rm.RoleId))
|
||||
.Select(rm => rm.MenuId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// 3. 收集用户专属菜单 ID
|
||||
var userMenuIds = await _dbContext.AdminUserMenus
|
||||
.Where(um => um.AdminUserId == adminUserId)
|
||||
.Select(um => um.MenuId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// 4. 合并所有菜单 ID,并递归查找所有子菜单
|
||||
var allMenuIds = roleMenuIds.Concat(userMenuIds).Distinct().ToHashSet();
|
||||
var allChildMenuIds = await GetAllChildMenuIdsAsync(allMenuIds);
|
||||
allMenuIds.UnionWith(allChildMenuIds);
|
||||
|
||||
// 5. 从所有菜单中提取权限标识
|
||||
var menuPermissions = await _dbContext.Set<Entities.Menu>()
|
||||
.Where(m => allMenuIds.Contains(m.Id) && m.Permission != null && m.Permission != "")
|
||||
.Select(m => m.Permission!)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// 合并所有权限
|
||||
var allPermissions = rolePermissions
|
||||
.Concat(menuPermissions)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return allPermissions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归获取所有子菜单 ID
|
||||
/// </summary>
|
||||
private async Task<HashSet<long>> GetAllChildMenuIdsAsync(HashSet<long> parentIds)
|
||||
{
|
||||
var result = new HashSet<long>();
|
||||
var currentParentIds = parentIds;
|
||||
|
||||
while (currentParentIds.Count > 0)
|
||||
{
|
||||
var childIds = await _dbContext.Set<Entities.Menu>()
|
||||
.Where(m => currentParentIds.Contains(m.ParentId))
|
||||
.Select(m => m.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var newIds = childIds.Where(id => !result.Contains(id) && !parentIds.Contains(id)).ToHashSet();
|
||||
if (newIds.Count == 0) break;
|
||||
|
||||
result.UnionWith(newIds);
|
||||
currentParentIds = newIds;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,28 +2,29 @@ import { request, type ApiResponse, type PagedResult } from '@/utils/request'
|
|||
|
||||
export interface OperationLog {
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
module: string
|
||||
action: string
|
||||
method: string
|
||||
url: string
|
||||
params: string | null
|
||||
adminUserId: number | null
|
||||
username: string | null
|
||||
module: string | null
|
||||
action: string | null
|
||||
method: string | null
|
||||
url: string | null
|
||||
ip: string | null
|
||||
userAgent: string | null
|
||||
duration: number
|
||||
requestData: string | null
|
||||
responseData: string | null
|
||||
status: number
|
||||
errorMessage: string | null
|
||||
errorMsg: string | null
|
||||
duration: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface OperationLogQuery {
|
||||
keyword?: string
|
||||
module?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
page: number
|
||||
pageSize: number
|
||||
username?: string
|
||||
module?: string
|
||||
status?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
// 获取操作日志列表
|
||||
|
|
@ -35,7 +36,7 @@ export function getOperationLogList(params: OperationLogQuery): Promise<ApiRespo
|
|||
})
|
||||
}
|
||||
|
||||
// 获取操作日志详情
|
||||
// 获取日志详情
|
||||
export function getOperationLog(id: number): Promise<ApiResponse<OperationLog>> {
|
||||
return request({
|
||||
url: `/admin/logs/${id}`,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export { businessRoutes, type RouteMeta, getBusinessPermissions, filterRoutesByP
|
|||
const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
redirect: '/dashboard' // 默认值,登录后会被动态路由覆盖
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
|
|
@ -60,6 +60,23 @@ const router = createRouter({
|
|||
// 白名单路由
|
||||
const whiteList = ['/login', '/404']
|
||||
|
||||
/**
|
||||
* 递归获取用户有权限的第一个可访问页面路径
|
||||
*/
|
||||
function getFirstAccessiblePath(routes: RouteRecordRaw[], parentPath = ''): string | null {
|
||||
for (const route of routes) {
|
||||
const fullPath = parentPath ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') : route.path
|
||||
// 有 component 且不是 Layout 的就是叶子页面
|
||||
if (route.children && route.children.length > 0) {
|
||||
const childPath = getFirstAccessiblePath(route.children, fullPath)
|
||||
if (childPath) return childPath
|
||||
} else if (route.component) {
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 标记动态路由是否已加载
|
||||
let dynamicRoutesLoaded = false
|
||||
|
||||
|
|
@ -94,6 +111,11 @@ router.beforeEach(async (to, _from, next) => {
|
|||
accessRoutes.forEach(route => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
// 动态设置首页重定向到用户有权限的第一个页面
|
||||
const firstRoute = getFirstAccessiblePath(accessRoutes)
|
||||
if (firstRoute) {
|
||||
router.addRoute({ path: '/', redirect: firstRoute })
|
||||
}
|
||||
// 添加 404 兜底路由
|
||||
router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
|
||||
// 标记动态路由已加载
|
||||
|
|
|
|||
|
|
@ -123,6 +123,11 @@ async function handle401Error(error: any): Promise<any> {
|
|||
|
||||
// Don't retry if already retried or if it's a refresh request
|
||||
if (originalConfig._retry || isWhiteListUrl(originalConfig.url)) {
|
||||
// 白名单接口(如登录)的 401 错误,直接显示后端返回的错误信息
|
||||
const msg = error.response?.data?.message
|
||||
if (msg) {
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,6 @@
|
|||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="checkedMenuIds"
|
||||
:check-strictly="true"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="menuDialogVisible = false">取消</el-button>
|
||||
|
|
@ -304,6 +303,25 @@ const passwordRules: FormRules = {
|
|||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤出叶子节点 ID,用于树组件回显(非 check-strictly 模式)
|
||||
*/
|
||||
function filterLeafMenuIds(tree: MenuTree[], checkedIds: number[]): number[] {
|
||||
const result: number[] = []
|
||||
const idSet = new Set(checkedIds)
|
||||
function walk(nodes: MenuTree[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.children && node.children.length > 0) {
|
||||
walk(node.children)
|
||||
} else if (idSet.has(node.id)) {
|
||||
result.push(node.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(tree)
|
||||
return result
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
|
|
@ -372,11 +390,18 @@ const handleSubmit = async () => {
|
|||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
// 非必填字段:空字符串转 null,避免后端格式验证失败
|
||||
const submitData = {
|
||||
...formData,
|
||||
realName: formData.realName || null,
|
||||
email: formData.email || null,
|
||||
phone: formData.phone || null
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await updateAdminUser(formData.id, formData)
|
||||
await updateAdminUser(submitData.id, submitData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createAdminUser(formData)
|
||||
await createAdminUser(submitData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
|
|
@ -446,14 +471,20 @@ const handleSubmitDepartment = async () => {
|
|||
const handleAssignMenu = async (row: AdminUser) => {
|
||||
currentUserId.value = row.id
|
||||
const res = await getUserMenus(row.id)
|
||||
checkedMenuIds.value = res.data
|
||||
// 去掉 check-strictly 后,回显时只设置叶子节点 ID,避免父节点导致子节点全选
|
||||
const allIds: number[] = res.data
|
||||
const leafIds = filterLeafMenuIds(menuTree.value, allIds)
|
||||
checkedMenuIds.value = leafIds
|
||||
menuDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmitMenus = async () => {
|
||||
menuSubmitLoading.value = true
|
||||
try {
|
||||
const menuIds = menuTreeRef.value?.getCheckedKeys(false) as number[]
|
||||
const menuIds = [
|
||||
...(menuTreeRef.value?.getCheckedKeys(false) as number[]),
|
||||
...(menuTreeRef.value?.getHalfCheckedKeys() as number[])
|
||||
]
|
||||
await assignUserMenus({ userId: currentUserId.value, menuIds })
|
||||
ElMessage.success('分配成功')
|
||||
menuDialogVisible.value = false
|
||||
|
|
|
|||
1
temp_menu29.txt
Normal file
1
temp_menu29.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"code":0,"message":"success","data":{"id":29,"parentId":28,"name":"用户列表","path":"/user/list","component":"business/user/index","icon":"peoples","menuType":2,"permission":"user:view","sortOrder":1,"status":1,"isExternal":false,"isCache":true,"createdAt":"2026-02-03T17:32:15.877","updatedAt":"2026-04-19T20:22:50.107"}}
|
||||
1
temp_menus.json
Normal file
1
temp_menus.json
Normal file
File diff suppressed because one or more lines are too long
1
temp_test_menus.txt
Normal file
1
temp_test_menus.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"code":0,"message":"success","data":[28,29,30,31,32,34,35,36,37]}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name" : "学业邑规划",
|
||||
"appid" : "__UNI__1BAACAB",
|
||||
"appid" : "__UNI__A612028",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.1",
|
||||
"versionCode" : 101,
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@
|
|||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 进行中测评弹窗 -->
|
||||
<view v-if="showPendingPopup" class="popup-mask" @click="handleDismissPending">
|
||||
<view class="popup-container pending-popup" @click.stop>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user