serv\web- 多租户修改

This commit is contained in:
2025-10-29 19:08:22 +08:00
parent c5c134fbb3
commit 82b6f14e64
86 changed files with 4446 additions and 2730 deletions

View File

@@ -38,7 +38,7 @@ CREATE TABLE `tb_achievement` (
KEY `idx_condition_type` (`condition_type`),
KEY `idx_deleted` (`deleted`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='成就定义表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='成就定义表';
-- =============================================
-- 2. 用户成就表 (tb_user_achievement)
@@ -56,7 +56,7 @@ CREATE TABLE `tb_user_achievement` (
KEY `idx_user_id` (`user_id`),
KEY `idx_achievement_id` (`achievement_id`),
KEY `idx_obtain_time` (`obtain_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户成就表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户成就表';
-- =============================================
-- 3. 用户成就进度表 (tb_user_achievement_progress)
@@ -80,7 +80,7 @@ CREATE TABLE `tb_user_achievement_progress` (
KEY `idx_achievement_id` (`achievement_id`),
KEY `idx_completed` (`completed`),
KEY `idx_last_update_time` (`last_update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户成就进度表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户成就进度表';
-- =============================================
-- 初始化成就数据
@@ -93,14 +93,6 @@ CREATE TABLE `tb_user_achievement_progress` (
-- 学习时长类成就
-- =============================================
INSERT INTO `tb_achievement` (`id`, `achievement_id`, `name`,`icon`, `description`, `type`, `level`, `condition_type`, `condition_value`, `points`, `order_num`, `deleted`) VALUES
('ACH001', 'learning_time_l1', '初学者', 'v1-icon.svg', '累计学习时长达到10小时', 1, 1, 1, 10*60*60, 10, 1, 0),
('ACH002', 'learning_time_l2', '勤学者', 'v2-icon.svg', '累计学习时长达到50小时', 1, 2, 1, 50*60*60, 50, 2, 0),
('ACH003', 'learning_time_l3', '学习达人', 'v3-icon.svg', '累计学习时长达到100小时', 1, 3, 1, 100*60*60, 100, 3, 0),
('ACH004', 'learning_time_l4', '学习狂人', 'v4-icon.svg', '累计学习时长达到500小时', 1, 4, 1, 500*60*60, 500, 4, 0),
('ACH005', 'learning_time_l5', '学习大师', 'v5-icon.svg', '累计学习时长达到1000小时', 1, 5, 1, 1000*60*60, 1000, 5, 0),
('ACH006', 'learning_time_l6', '学习宗师', 'v6-icon.svg', '累计学习时长达到2000小时', 1, 6, 1, 2000*60*60, 2000, 6, 0);
-- -- =============================================
-- -- 课程完成类成就
-- -- =============================================

View File

@@ -56,4 +56,4 @@ CREATE TABLE `tb_crontab_log` (
KEY `idx_execute_status` (`execute_status`),
KEY `idx_start_time` (`start_time`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时任务执行日志表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='定时任务执行日志表';

View File

@@ -6,6 +6,7 @@ CREATE TABLE `tb_sys_dept` (
`dept_id` VARCHAR(50) NOT NULL COMMENT '部门ID',
`name` VARCHAR(100) NOT NULL COMMENT '部门名称',
`parent_id` VARCHAR(50) DEFAULT NULL COMMENT '父部门ID',
`dept_path` VARCHAR(500) DEFAULT NULL COMMENT '部门路径,格式:/root_department/dept_001/,用于快速判断父子关系',
`description` VARCHAR(255) DEFAULT NULL COMMENT '部门描述',
`creator` VARCHAR(50) DEFAULT NULL COMMENT '创建者',
`updater` VARCHAR(50) DEFAULT NULL COMMENT '更新者',
@@ -15,8 +16,9 @@ CREATE TABLE `tb_sys_dept` (
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dept_id` (`dept_id`),
KEY `idx_dept_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
KEY `idx_dept_parent` (`parent_id`),
KEY `idx_dept_path` (`dept_path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='部门表';
-- 角色表
@@ -34,7 +36,7 @@ CREATE TABLE `tb_sys_role` (
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 部门-角色关联
@@ -51,7 +53,7 @@ CREATE TABLE `tb_sys_dept_role` (
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dept_role` (`dept_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 用户-角色关联
@@ -69,7 +71,7 @@ CREATE TABLE `tb_sys_user_dept_role` (
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_dept_role` (`user_id`, `dept_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 模块表
@@ -92,7 +94,7 @@ CREATE TABLE `tb_sys_module` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_module_id` (`module_id`),
UNIQUE KEY `uk_module_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 权限表
DROP TABLE IF EXISTS `tb_sys_permission`;
@@ -112,7 +114,7 @@ CREATE TABLE `tb_sys_permission` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_permission_id` (`permission_id`),
KEY `idx_permission_module` (`module_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 角色-权限关联
@@ -129,7 +131,7 @@ CREATE TABLE `tb_sys_role_permission` (
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 菜单表
@@ -154,7 +156,7 @@ CREATE TABLE `tb_sys_menu` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_menu_id` (`menu_id`),
KEY `idx_menu_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DROP TABLE IF EXISTS `tb_sys_menu_permission`;
@@ -170,4 +172,4 @@ CREATE TABLE `tb_sys_menu_permission` (
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_menu_permission` (`menu_id`, `permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@@ -3,7 +3,7 @@ use school_news;
DROP TABLE IF EXISTS `tb_resource_permission`;
CREATE TABLE `tb_resource_permission` (
`id` VARCHAR(50) NOT NULL COMMENT '权限ID',
`resource_type` INT(4) NOT NULL COMMENT '资源类型1资源/新闻 2课程 3课程章节 4学习任务)',
`resource_type` INT(4) NOT NULL COMMENT '资源类型1新闻 2课程 3学习任务 4部门 5角色 6成就 7定时任务 8轮播图 9标签',
`resource_id` VARCHAR(50) NOT NULL COMMENT '资源ID',
`dept_id` VARCHAR(50) DEFAULT NULL COMMENT '部门IDNULL表示不限制部门',
`role_id` VARCHAR(50) DEFAULT NULL COMMENT '角色IDNULL表示不限制角色',
@@ -25,30 +25,45 @@ CREATE TABLE `tb_resource_permission` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='统一资源权限控制表';
-- 说明:
-- 1. resource_type 资源类型说明:
-- 1 - 资源/新闻
-- 2 - 课程
-- 3 - 课程章节
-- 4 - 学习任务
-- 可根据实际业务扩展
-- 1. resource_type 资源类型说明对应ResourceType枚举
-- 1 - NEWS (新闻/资源)
-- 2 - COURSE (课程)
-- 3 - TASK (学习任务)
-- 4 - DEPT (部门)
-- 5 - ROLE (角色)
-- 6 - ACHIEVEMENT (成就)
-- 7 - CRONTAB_TASK (定时任务)
-- 8 - BANNER (轮播图)
-- 9 - TAG (标签)
-- 注意:这些值必须与 common-core/enums/ResourceType.java 中的枚举定义完全一致
--
-- 2. dept_id 和 role_id 组合使用:
-- - 都为NULL不限制,所有人可访问
-- - 都为NULL超级管理员权限,所有人可访问
-- - dept_id有值role_id为NULL该部门所有人可访问
-- - dept_id为NULLrole_id有值该角色所有人可访问
-- - 都有值:该部门的该角色可访问
-- - dept_id为NULLrole_id有值该角色所有人可访问(跨部门)
-- - 都有值:该部门的该角色可访问(精确控制)
--
-- 3. 权限说明:
-- - can_read查看权限浏览、阅读
-- - can_read查看权限浏览、阅读、查询
-- - can_write编辑权限修改、删除
-- - can_execute执行权限发布、审核操作)
-- - can_execute执行权限发布、审核、执行等高级操作)
--
-- 4. 查询示例
-- 4. 权限创建逻辑
-- - root_department的superadmin创建资源时为所有部门和角色创建权限
-- - 普通用户创建资源时:为父部门管理员+子部门所有角色创建权限
-- - 始终为root_department的superadmin创建全权限记录
--
-- 5. 查询示例:
-- 查询用户对某资源的权限:
-- SELECT * FROM tb_resource_permission
-- WHERE resource_type = 1
-- AND resource_id = 'xxx'
-- AND (dept_id IS NULL OR dept_id = '用户部门')
-- AND (role_id IS NULL OR role_id IN ('用户角色列表'))
-- AND (
-- (dept_id IS NULL AND role_id IS NULL) -- 超级权限
-- OR (dept_id = '用户部门' AND role_id IS NULL) -- 部门权限
-- OR (role_id = '用户角色' AND dept_id IS NULL) -- 角色权限
-- OR (dept_id = '用户部门' AND role_id = '用户角色') -- 精确权限
-- )
-- AND can_read = 1
-- AND deleted = 0;

View File

@@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS `tb_sys_file` (
INDEX `idx_storage_type` (`storage_type`),
INDEX `idx_deleted` (`deleted`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件上传记录表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文件上传记录表';

View File

@@ -17,7 +17,7 @@ CREATE TABLE `tb_sys_user` (
UNIQUE KEY `uk_user_username` (`username`),
UNIQUE KEY `uk_user_email` (`email`),
KEY `idx_user_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 推荐:把默认 admin 密码替换为已哈希的值
@@ -40,7 +40,7 @@ CREATE TABLE `tb_sys_user_info` (
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_info_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DROP TABLE IF EXISTS `tb_sys_login_log`;
@@ -61,5 +61,5 @@ CREATE TABLE `tb_sys_login_log` (
PRIMARY KEY (`id`),
index `idx_user_id` (`user_id`) USING BTREE,
index `idx_login_time` (`login_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@@ -45,11 +45,72 @@ INSERT INTO `tb_sys_config` (id, config_key, config_value, config_type, config_g
('12', 'system.resource.auto_publish_time', '08:00', 'string', 'resource', '自动发布时间', 0, '1', now()),
('13', 'system.ai.enabled', 'true', 'boolean', 'ai', '是否启用智能体', 0, '1', now());
-- 注意默认superadmin用户已在 initMenuData.sql 中创建,此处无需重复创建
-- 插入默认用户数据
INSERT INTO `tb_sys_user` (id, username, password, email, status) VALUES
('1', 'superadmin', '$2a$10$/Bo2SXboVUpYfR6EA.y8puYQaMGBcuNYFY/EkQRY3w27IH56EuEcS', '3223905473@qq.com', 0);
-- =====================================================
-- 初始化资源权限数据
-- =====================================================
-- 插入默认用户信息数据
INSERT INTO `tb_sys_user_info` (id, user_id, full_name, avatar) VALUES
('1', '1', '管理员', 'default');
INSERT INTO `tb_achievement` (`id`, `achievement_id`, `name`,`icon`, `description`, `type`, `level`, `condition_type`, `condition_value`, `points`, `order_num`, `deleted`) VALUES
('ACH001', 'learning_time_l1', '初学者', 'v1-icon.svg', '累计学习时长达到10小时', 1, 1, 1, 10*60*60, 10, 1, 0),
('ACH002', 'learning_time_l2', '勤学者', 'v2-icon.svg', '累计学习时长达到50小时', 1, 2, 1, 50*60*60, 50, 2, 0),
('ACH003', 'learning_time_l3', '学习达人', 'v3-icon.svg', '累计学习时长达到100小时', 1, 3, 1, 100*60*60, 100, 3, 0),
('ACH004', 'learning_time_l4', '学习狂人', 'v4-icon.svg', '累计学习时长达到500小时', 1, 4, 1, 500*60*60, 500, 4, 0),
('ACH005', 'learning_time_l5', '学习大师', 'v5-icon.svg', '累计学习时长达到1000小时', 1, 5, 1, 1000*60*60, 1000, 5, 0),
('ACH006', 'learning_time_l6', '学习宗师', 'v6-icon.svg', '累计学习时长达到2000小时', 1, 6, 1, 2000*60*60, 2000, 6, 0);
INSERT INTO `tb_resource_permission` (id, resource_type, resource_id, dept_id, role_id, can_read, can_write, can_execute, creator, create_time) VALUES
('perm_achievement_001', 6, 'learning_time_l1', NULL, NULL, 1, 0, 0, '1', now()),
('perm_achievement_002', 6, 'learning_time_l2', NULL, NULL, 1, 0, 0, '1', now()),
('perm_achievement_003', 6, 'learning_time_l3', NULL, NULL, 1, 0, 0, '1', now()),
('perm_achievement_004', 6, 'learning_time_l4', NULL, NULL, 1, 0, 0, '1', now()),
('perm_achievement_005', 6, 'learning_time_l5', NULL, NULL, 1, 0, 0, '1', now()),
('perm_achievement_006', 6, 'learning_time_l6', NULL, NULL, 1, 0, 0, '1', now());
-- 为默认标签创建超级管理员权限(文章标签)
INSERT INTO `tb_resource_permission` (id, resource_type, resource_id, dept_id, role_id, can_read, can_write, can_execute, creator, create_time) VALUES
-- 文章标签权限resource_type=9TAG
('perm_tag_001', 9, 'tag_article_001', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_002', 9, 'tag_article_002', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_003', 9, 'tag_article_003', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_004', 9, 'tag_article_004', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_005', 9, 'tag_article_005', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_006', 9, 'tag_article_006', NULL, NULL, 1, 1, 1, '1', now()),
-- 课程标签权限resource_type=9TAG
('perm_tag_101', 9, 'tag_course_001', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_102', 9, 'tag_course_002', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_103', 9, 'tag_course_003', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_104', 9, 'tag_course_004', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_105', 9, 'tag_course_005', NULL, NULL, 1, 1, 1, '1', now()),
-- 学习任务标签权限resource_type=9TAG
('perm_tag_201', 9, 'tag_task_001', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_202', 9, 'tag_task_002', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_203', 9, 'tag_task_003', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_204', 9, 'tag_task_004', NULL, NULL, 1, 1, 1, '1', now()),
('perm_tag_205', 9, 'tag_task_005', NULL, NULL, 1, 1, 1, '1', now());
-- 为默认部门创建权限resource_type=4DEPT
INSERT INTO `tb_resource_permission` (id, resource_type, resource_id, dept_id, role_id, can_read, can_write, can_execute, creator, create_time) VALUES
('perm_dept_001', 4, 'root_department', NULL, NULL, 1, 1, 1, '1', now()),
('perm_dept_002', 4, 'default_department', NULL, NULL, 1, 1, 1, '1', now());
-- 为默认角色创建权限resource_type=5ROLE
INSERT INTO `tb_resource_permission` (id, resource_type, resource_id, dept_id, role_id, can_read, can_write, can_execute, creator, create_time) VALUES
('perm_role_001', 5, 'superadmin', NULL, NULL, 1, 1, 1, '1', now()),
('perm_role_002', 5, 'admin', NULL, NULL, 1, 1, 1, '1', now()),
('perm_role_003', 5, 'freedom', NULL, NULL, 1, 1, 1, '1', now());
-- 说明:
-- 1. 这些初始权限都是超级管理员权限dept_id和role_id都为NULL
-- 2. 所有权限都是全权限can_read=1, can_write=1, can_execute=1
-- 3. 后续创建的资源会自动根据创建者的部门和角色创建相应的权限
-- 4. resource_type说明
-- 1-NEWS, 2-COURSE, 3-TASK, 4-DEPT, 5-ROLE, 6-ACHIEVEMENT, 7-CRONTAB_TASK, 8-BANNER, 9-TAG
-- 5. 权限创建规则:
-- - root_department的superadmin创建资源为所有部门和角色创建权限
-- - 普通用户创建资源:为父部门管理员+子部门角色创建权限
-- - 所有资源都会为root_department的superadmin创建全权限

View File

@@ -1,13 +1,26 @@
use school_news;
-- 插入部门数据
INSERT INTO `tb_sys_dept` (id,dept_id,name, description) VALUES ('1','root_department', '超级部门', '系统超级部门');
INSERT INTO `tb_sys_dept` (id,dept_id,name, parent_id, description) VALUES ('2','default_department', '默认部门', 'root_department', '系统默认创建的部门');
-- 插入默认超级管理员用户必须最先创建因为后续数据的creator都需要引用此用户
INSERT INTO `tb_sys_user` (id, username, password, email, status, create_time) VALUES
('1', 'superadmin', '$2a$10$/Bo2SXboVUpYfR6EA.y8puYQaMGBcuNYFY/EkQRY3w27IH56EuEcS', 'superadmin@example.com', 1, now());
-- 插入默认用户信息数据
INSERT INTO `tb_sys_user_info` (id, user_id, full_name, avatar, create_time) VALUES
('1', '1', '超级管理员', 'default', now());
-- 插入部门数据包含dept_path字段
INSERT INTO `tb_sys_dept` (id, dept_id, name, parent_id, dept_path, description, creator, create_time) VALUES
('1', 'root_department', '超级部门', NULL, '/root_department/', '系统超级部门', '1', now());
INSERT INTO `tb_sys_dept` (id, dept_id, name, parent_id, dept_path, description, creator, create_time) VALUES
('2', 'default_department', '默认部门', 'root_department', '/root_department/default_department/', '系统默认创建的部门', '1', now());
-- 插入角色数据
INSERT INTO `tb_sys_role` (id,role_id, name, description) VALUES ('1','superadmin', '超级管理员', '超级管理员角色');
INSERT INTO `tb_sys_role` (id,role_id, name, description) VALUES ('2','admin', '管理员', '管理员角色');
INSERT INTO `tb_sys_role` (id,role_id, name, description) VALUES ('3','freedom', '自由角色', '自由角色');
INSERT INTO `tb_sys_role` (id,role_id, name, description, creator, create_time) VALUES
('1','superadmin', '超级管理员', '超级管理员角色', '1', now());
INSERT INTO `tb_sys_role` (id,role_id, name, description, creator, create_time) VALUES
('2','admin', '管理员', '管理员角色', '1', now());
INSERT INTO `tb_sys_role` (id,role_id, name, description, creator, create_time) VALUES
('3','freedom', '自由角色', '自由角色', '1', now());
-- 插入部门-角色关联数据
INSERT INTO `tb_sys_dept_role` (id, dept_id, role_id, creator, create_time) VALUES
@@ -15,8 +28,9 @@ INSERT INTO `tb_sys_dept_role` (id, dept_id, role_id, creator, create_time) VALU
('2', 'default_department', 'admin', '1', now()),
('3', 'default_department', 'freedom', '1', now());
-- 插入用户-角色关联数据
INSERT INTO `tb_sys_user_dept_role` (id, user_id, dept_id, role_id, creator, create_time) VALUES ('1', '1', 'root_department', 'superadmin', '1', now());
-- 插入用户-部门-角色关联数据root_department的superadmin
INSERT INTO `tb_sys_user_dept_role` (id, user_id, dept_id, role_id, creator, create_time) VALUES
('1', '1', 'root_department', 'superadmin', '1', now());
-- 插入模块数据
INSERT INTO `tb_sys_module` (id, module_id, name, code, description, icon, order_num, status, creator, create_time) VALUES
@@ -72,87 +86,62 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat
('18', 'admin', 'perm_crontab_execute', '1', now());
-- 插入前端菜单数据
INSERT INTO `tb_sys_menu` (id, menu_id, name, parent_id, url, component, icon, order_num, type, layout, creator, create_time) VALUES
('100', 'menu_home', '首页', NULL, '/home', 'user/home/HomeView', 'el-icon-house', 1, 1, 'NavigationLayout', '1', now()),
-- 资源中心
('200', 'menu_resource_center', '资源中心', NULL, '/resource-center', 'user/resource-center/ResourceCenterView', 'el-icon-folder-opened', 2, 1, 'NavigationLayout', '1', now()),
-- 学习计划
('300', 'menu_study_plan', '学习计划', NULL, '/study-plan', 'user/study-plan/StudyPlanView', 'el-icon-reading', 3, 1, 'NavigationLayout', '1', now()),
('301', 'menu_study_tasks', '学习任务', 'menu_study_plan', '/study-plan/tasks', 'user/study-plan/StudyTasksView', 'el-icon-s-order', 1, 1, 'NavigationLayout', '1', now()),
('302', 'menu_course_center', '课程中心', 'menu_study_plan', '/study-plan/course', 'user/study-plan/CourseCenterView', 'el-icon-video-play', 2, 1, 'NavigationLayout', '1', now()),
('303', 'menu_task_detail', '任务详情', 'menu_study_plan', '/study-plan/task-detail', 'user/study-plan/LearningTaskDetailView', 'el-icon-document', 3, 3, 'NavigationLayout', '1', now()),
('304', 'menu_course_detail', '课程详情', 'menu_study_plan', '/study-plan/course-detail', 'user/study-plan/CourseDetailView', 'el-icon-video-play', 4, 3, 'NavigationLayout', '1', now()),
('305', 'menu_course_study', '课程学习', 'menu_study_plan', '/study-plan/course-study', 'user/study-plan/CourseStudyView', 'el-icon-video-play', 5, 3, 'NavigationLayout', '1', now()),
('400', 'menu_user_dropdown', '用户下拉菜单', NULL, '', '', 'el-icon-user', 4, 0, 'NavigationLayout', '1', now()),
-- 个人中心
('401', 'menu_user_center', '个人中心', 'menu_user_dropdown', '/user-center', 'user/user-center/UserCenterView', 'el-icon-user', 4, 1, 'NavigationLayout', '1', now()),
('402', 'menu_learning_records', '学习记录', 'menu_user_center', '/user-center/learning-records', 'user/user-center/LearningRecordsView', 'el-icon-document', 1, 0, 'NavigationLayout', '1', now()),
('403', 'menu_my_favorites', '我的收藏', 'menu_user_center', '/user-center/favorites', 'user/user-center/MyFavoritesView', 'el-icon-star-on', 2, 0, 'NavigationLayout', '1', now()),
('404', 'menu_my_achievements', '我的成就', 'menu_user_center', '/user-center/achievements', 'user/user-center/MyAchievementsView', 'el-icon-trophy', 3, 0, 'NavigationLayout', '1', now()),
-- 账号中心
('500', 'menu_profile', '账号中心', 'menu_user_dropdown', '/profile', 'user/profile/ProfileView', 'el-icon-user-solid', 5, 1, 'NavigationLayout', '1', now()),
('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'user/profile/PersonalInfoView', 'el-icon-user', 1, 0, 'NavigationLayout', '1', now()),
('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'user/profile/AccountSettingsView', 'el-icon-setting', 2, 0, 'NavigationLayout', '1', now()),
-- 智能体模块
('600', 'menu_ai_assistant', '智能体模块', NULL, '/ai-assistant', 'user/ai-assistant/AIAssistantView', 'el-icon-cpu', 6, 1, 'NavigationLayout', '1', now());
-- 插入后端管理菜单数据 (type=0 侧边栏菜单)
INSERT INTO `tb_sys_menu` (id, menu_id, name, parent_id, url, component, icon, order_num, type, layout, creator, create_time) VALUES
-- 系统总览
('1000', 'menu_admin_overview', '系统总览', NULL, '/admin/overview', 'admin/overview/SystemOverviewView', 'el-icon-data-analysis', 1, 0, 'SidebarLayout', '1', now()),
-- 用户管理
('2000', 'menu_sys_manage', '系统管理', NULL, '', '', 'el-icon-user', 2, 0, 'SidebarLayout', '1', now()),
('2001', 'menu_admin_user', '用户管理', 'menu_sys_manage', '/admin/manage/system/user', 'admin/manage/system/UserManageView', 'el-icon-user', 1, 0, 'SidebarLayout', '1', now()),
('2002', 'menu_admin_dept', '部门管理', 'menu_sys_manage', '/admin/manage/system/dept', 'admin/manage/system/DeptManageView', 'el-icon-office-building', 2, 0, 'SidebarLayout', '1', now()),
('2003', 'menu_admin_role', '角色管理', 'menu_sys_manage', '/admin/manage/system/role', 'admin/manage/system/RoleManageView', 'el-icon-user-solid', 3, 0, 'SidebarLayout', '1', now()),
('2005', 'menu_admin_menu', '菜单管理', 'menu_sys_manage', '/admin/manage/system/menu', 'admin/manage/system/MenuManageView', 'el-icon-menu', 4, 0, 'SidebarLayout', '1', now()),
('2006', 'menu_admin_module', '模块权限管理', 'menu_sys_manage', '/admin/manage/system/module-permission', 'admin/manage/system/ModulePermissionManageView', 'el-icon-s-grid', 5, 0, 'SidebarLayout', '1', now()),
-- 资源管理
('3000', 'menu_admin_resource_manage', '资源管理', NULL, '', '', 'el-icon-folder', 3, 0, 'SidebarLayout', '1', now()),
('3001', 'menu_admin_resource', '资源管理', 'menu_admin_resource_manage', '/admin/manage/resource/resource', 'admin/manage/resource/ResourceManagementView', 'el-icon-folder', 1, 0, 'SidebarLayout', '1', now()),
('3002', 'menu_admin_article', '文章管理', 'menu_admin_resource_manage', '/admin/manage/resource/article', 'admin/manage/resource/ArticleManagementView', 'el-icon-document', 2, 0, 'SidebarLayout', '1', now()),
('3003', 'menu_admin_data_records', '数据记录', 'menu_admin_resource_manage', '/admin/manage/resource/data-records', 'admin/manage/resource/DataRecordsView', 'el-icon-data-line', 3, 0, 'SidebarLayout', '1', now()),
-- 文章相关
('3010', 'menu_article_add', '文章添加', 'menu_admin_article', '/article/add', 'public/article/ArticleAddView', 'el-icon-plus', 1, 3, 'SidebarLayout', '1', now()),
('3011', 'menu_article_show', '文章展示', 'menu_admin_article', '/article/show', 'public/article/ArticleShowView', 'el-icon-document', 2, 3, 'SidebarLayout', '1', now()),
-- 运营管理
('4000', 'menu_admin_content_manage', '运营管理', NULL, '', '', 'el-icon-s-operation', 4, 0, 'SidebarLayout', '1', now()),
('4001', 'menu_admin_banner', 'Banner管理', 'menu_admin_content_manage', '/admin/manage/content/banner', 'admin/manage/content/BannerManagementView', 'el-icon-picture', 1, 0, 'SidebarLayout', '1', now()),
('4002', 'menu_admin_tag', '标签管理', 'menu_admin_content_manage', '/admin/manage/content/tag', 'admin/manage/content/TagManagementView', 'el-icon-price-tag', 2, 0, 'SidebarLayout', '1', now()),
('4003', 'menu_admin_column', '栏目管理', 'menu_admin_content_manage', '/admin/manage/content/column', 'admin/manage/content/ColumnManagementView', 'el-icon-menu', 3, 0, 'SidebarLayout', '1', now()),
('4004', 'menu_admin_content', '内容管理', 'menu_admin_content_manage', '/admin/manage/content/content', 'admin/manage/content/ContentManagementView', 'el-icon-document', 4, 0, 'SidebarLayout', '1', now()),
-- 学习管理
('5000', 'menu_admin_study_manage', '学习管理', NULL, '', '', 'el-icon-reading', 5, 0, 'SidebarLayout', '1', now()),
('5002', 'menu_admin_task_manage', '任务管理', 'menu_admin_study_manage', '/admin/manage/study/task-manage', 'admin/manage/study/TaskManageView', 'el-icon-s-order', 2, 0, 'SidebarLayout', '1', now()),
('5003', 'menu_admin_study_records', '学习记录', 'menu_admin_study_manage', '/admin/manage/study/study-records', 'admin/manage/study/StudyRecordsView', 'el-icon-document', 3, 0, 'SidebarLayout', '1', now()),
('5004', 'menu_admin_course_manage', '课程管理', 'menu_admin_study_manage', '/admin/manage/study/course', 'admin/manage/study/CourseManagementView', 'el-icon-video-play', 4, 0, 'SidebarLayout', '1', now()),
('5005', 'menu_admin_achievement_manage', '成就管理', 'menu_admin_study_manage', '/admin/manage/study/achievement', 'admin/manage/achievement/AchievementManagementView', 'el-icon-trophy', 5, 0, 'SidebarLayout', '1', now()),
-- 智能体管理
('6000', 'menu_admin_ai_manage', '智能体管理', NULL, '', '', 'el-icon-cpu', 6, 0, 'SidebarLayout', '1', now()),
('6001', 'menu_admin_ai', 'AI管理', 'menu_admin_ai_manage', '/admin/manage/ai/ai', 'admin/manage/ai/AIManagementView', 'el-icon-cpu', 1, 0, 'SidebarLayout', '1', now()),
('6002', 'menu_admin_ai_config', 'AI配置', 'menu_admin_ai_manage', '/admin/manage/ai/config', 'admin/manage/ai/AIConfigView', 'el-icon-setting', 2, 0, 'SidebarLayout', '1', now()),
('6003', 'menu_admin_knowledge', '知识库管理', 'menu_admin_ai_manage', '/admin/manage/ai/knowledge', 'admin/manage/ai/KnowledgeManagementView', 'el-icon-collection', 3, 0, 'SidebarLayout', '1', now()),
-- 系统日志
('7000', 'menu_admin_logs_manage', '系统日志', NULL, '', '', 'el-icon-document', 7, 0, 'SidebarLayout', '1', now()),
('7001', 'menu_admin_system_logs', '系统日志', 'menu_admin_logs_manage', '/admin/manage/logs/system', 'admin/manage/logs/SystemLogsView', 'el-icon-document', 1, 0, 'SidebarLayout', '1', now()),
('7002', 'menu_admin_login_logs', '登录日志', 'menu_admin_logs_manage', '/admin/manage/logs/login', 'admin/manage/logs/LoginLogsView', 'el-icon-key', 2, 0, 'SidebarLayout', '1', now()),
('7003', 'menu_admin_operation_logs', '操作日志', 'menu_admin_logs_manage', '/admin/manage/logs/operation', 'admin/manage/logs/OperationLogsView', 'el-icon-s-operation', 3, 0, 'SidebarLayout', '1', now()),
('7004', 'menu_admin_system_config', '系统配置', 'menu_admin_logs_manage', '/admin/manage/logs/config', 'admin/manage/logs/SystemConfigView', 'el-icon-setting', 4, 0, 'SidebarLayout', '1', now()),
-- 定时任务管理
('8000', 'menu_admin_crontab_manage', '定时任务管理', NULL, '', '', 'el-icon-alarm-clock', 8, 0, 'SidebarLayout', '1', now()),
('8001', 'menu_admin_crontab_task', '任务管理', 'menu_admin_crontab_manage', '/admin/manage/crontab/task', 'admin/manage/crontab/TaskManagementView', 'el-icon-s-order', 1, 0, 'SidebarLayout', '1', now()),
('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', 'el-icon-document', 2, 0, 'SidebarLayout', '1', now()),
('8003', 'menu_admin_news_crawler', '新闻爬虫配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/news-crawler', 'admin/manage/crontab/NewsCrawlerView', 'el-icon-share', 3, 0, 'SidebarLayout', '1', now());
INSERT INTO `tb_sys_menu` VALUES
-- 用户前端菜单 (100-699)
('100', 'menu_home', '首页', NULL, '/home', 'user/home/HomeView', NULL, 1, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('200', 'menu_resource_center', '资源中心', NULL, '/resource-center', 'user/resource-center/ResourceCenterView', NULL, 2, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('300', 'menu_study_plan', '学习计划', NULL, '/study-plan', 'user/study-plan/StudyPlanView', NULL, 3, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('301', 'menu_study_tasks', '学习任务', 'menu_study_plan', '/study-plan/tasks', 'user/study-plan/StudyTasksView', NULL, 1, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('302', 'menu_course_center', '课程中心', 'menu_study_plan', '/study-plan/course', 'user/study-plan/CourseCenterView', NULL, 2, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('303', 'menu_task_detail', '任务详情', 'menu_study_plan', '/study-plan/task-detail', 'user/study-plan/LearningTaskDetailView', NULL, 3, 3, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('304', 'menu_course_detail', '课程详情', 'menu_study_plan', '/study-plan/course-detail', 'user/study-plan/CourseDetailView', NULL, 4, 3, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('305', 'menu_course_study', '课程学习', 'menu_study_plan', '/study-plan/course-study', 'user/study-plan/CourseStudyView', NULL, 5, 3, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('400', 'menu_user_dropdown', '用户下拉菜单', NULL, '', '', NULL, 4, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('401', 'menu_user_center', '个人中心', 'menu_user_dropdown', '/user-center', 'user/user-center/UserCenterView', NULL, 4, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('402', 'menu_learning_records', '学习记录', 'menu_user_center', '/user-center/learning-records', 'user/user-center/LearningRecordsView', NULL, 1, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('403', 'menu_my_favorites', '我的收藏', 'menu_user_center', '/user-center/favorites', 'user/user-center/MyFavoritesView', NULL, 2, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('404', 'menu_my_achievements', '我的成就', 'menu_user_center', '/user-center/achievements', 'user/user-center/MyAchievementsView', NULL, 3, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('500', 'menu_profile', '账号中心', 'menu_user_dropdown', '/profile', 'user/profile/ProfileView', NULL, 5, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'user/profile/PersonalInfoView', NULL, 1, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'user/profile/AccountSettingsView', NULL, 2, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('600', 'menu_ai_assistant', '智能体模块', NULL, '/ai-assistant', 'user/ai-assistant/AIAssistantView', NULL, 6, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
-- 管理后台菜单 (1000-8999)
('1000', 'menu_admin_overview', '系统总览', NULL, '/admin/overview', 'admin/overview/SystemOverviewView', 'admin/overview.svg', 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:32', NULL, 0),
('2000', 'menu_sys_manage', '系统管理', NULL, '', '', 'admin/settings.svg', 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:35', NULL, 0),
('2001', 'menu_admin_user', '用户管理', 'menu_sys_manage', '/admin/manage/system/user', 'admin/manage/system/UserManageView', 'admin/usermange.svg', 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:38', NULL, 0),
('2002', 'menu_admin_dept', '部门管理', 'menu_sys_manage', '/admin/manage/system/dept', 'admin/manage/system/DeptManageView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('2003', 'menu_admin_role', '角色管理', 'menu_sys_manage', '/admin/manage/system/role', 'admin/manage/system/RoleManageView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('2005', 'menu_admin_menu', '菜单管理', 'menu_sys_manage', '/admin/manage/system/menu', 'admin/manage/system/MenuManageView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('2006', 'menu_admin_module', '模块权限管理', 'menu_sys_manage', '/admin/manage/system/module-permission', 'admin/manage/system/ModulePermissionManageView', NULL, 5, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('3000', 'menu_admin_resource_manage', '资源管理', NULL, '', '', 'admin/resource.svg', 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:40', NULL, 0),
('3001', 'menu_admin_resource', '数据采集', 'menu_admin_resource_manage', '/admin/manage/resource/resource', 'admin/manage/resource/ResourceManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('3002', 'menu_admin_article', '文章管理', 'menu_admin_resource_manage', '/admin/manage/resource/article', 'admin/manage/resource/ArticleManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('3003', 'menu_admin_data_records', '数据记录', 'menu_admin_resource_manage', '/admin/manage/resource/data-records', 'admin/manage/resource/DataRecordsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('3010', 'menu_article_add', '文章添加', 'menu_admin_article', '/article/add', 'public/article/ArticleAddView', NULL, 1, 3, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('3011', 'menu_article_show', '文章展示', 'menu_admin_article', '/article/show', 'public/article/ArticleShowView', NULL, 2, 3, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('4000', 'menu_admin_content_manage', '运营管理', NULL, '', '', 'admin/maintain.svg', 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:42', NULL, 0),
('4001', 'menu_admin_banner', 'Banner管理', 'menu_admin_content_manage', '/admin/manage/content/banner', 'admin/manage/content/BannerManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('4002', 'menu_admin_tag', '标签管理', 'menu_admin_content_manage', '/admin/manage/content/tag', 'admin/manage/content/TagManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('4003', 'menu_admin_column', '栏目管理', 'menu_admin_content_manage', '/admin/manage/content/column', 'admin/manage/content/ColumnManagementView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('4004', 'menu_admin_content', '内容管理', 'menu_admin_content_manage', '/admin/manage/content/content', 'admin/manage/content/ContentManagementView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('5000', 'menu_admin_study_manage', '学习管理', NULL, '', '', 'admin/study.svg', 5, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:46', NULL, 0),
('5002', 'menu_admin_task_manage', '任务管理', 'menu_admin_study_manage', '/admin/manage/study/task-manage', 'admin/manage/study/TaskManageView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('5003', 'menu_admin_study_records', '学习记录', 'menu_admin_study_manage', '/admin/manage/study/study-records', 'admin/manage/study/StudyRecordsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('5004', 'menu_admin_course_manage', '课程管理', 'menu_admin_study_manage', '/admin/manage/study/course', 'admin/manage/study/CourseManagementView', 'admin/course.svg', 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:48', NULL, 0),
('5005', 'menu_admin_achievement_manage', '成就管理', 'menu_admin_study_manage', '/admin/manage/study/achievement', 'admin/manage/achievement/AchievementManagementView', NULL, 5, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('6000', 'menu_admin_ai_manage', '智能体管理', NULL, '', '', 'admin/agent.svg', 6, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:50', NULL, 0),
('6001', 'menu_admin_ai', 'AI管理', 'menu_admin_ai_manage', '/admin/manage/ai/ai', 'admin/manage/ai/AIManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('6002', 'menu_admin_ai_config', 'AI配置', 'menu_admin_ai_manage', '/admin/manage/ai/config', 'admin/manage/ai/AIConfigView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('6003', 'menu_admin_knowledge', '知识库管理', 'menu_admin_ai_manage', '/admin/manage/ai/knowledge', 'admin/manage/ai/KnowledgeManagementView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('7000', 'menu_admin_logs_manage', '系统日志', NULL, '', '', 'admin/logs.svg', 7, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:53', NULL, 0),
('7001', 'menu_admin_system_logs', '系统日志', 'menu_admin_logs_manage', '/admin/manage/logs/system', 'admin/manage/logs/SystemLogsView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('7002', 'menu_admin_login_logs', '登录日志', 'menu_admin_logs_manage', '/admin/manage/logs/login', 'admin/manage/logs/LoginLogsView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('7003', 'menu_admin_operation_logs', '操作日志', 'menu_admin_logs_manage', '/admin/manage/logs/operation', 'admin/manage/logs/OperationLogsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('7004', 'menu_admin_system_config', '系统配置', 'menu_admin_logs_manage', '/admin/manage/logs/config', 'admin/manage/logs/SystemConfigView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('8000', 'menu_admin_crontab_manage', '定时任务管理', NULL, '', '', NULL, 8, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('8001', 'menu_admin_crontab_task', '任务管理', 'menu_admin_crontab_manage', '/admin/manage/crontab/task', 'admin/manage/crontab/TaskManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('8003', 'menu_admin_news_crawler', '新闻爬虫配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/news-crawler', 'admin/manage/crontab/NewsCrawlerView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0);
-- 插入菜单权限关联数据
INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, create_time) VALUES
-- 前端菜单权限关联

View File

@@ -5,6 +5,8 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.usercenter.TbAchievement;
import org.xyzh.common.vo.AchievementVO;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.List;
@@ -21,9 +23,10 @@ public interface AchievementMapper extends BaseMapper<TbAchievement> {
/**
* @description 查询成就列表
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return List<TbAchievement> 成就列表
*/
List<TbAchievement> selectAchievements(@Param("filter") TbAchievement filter);
List<TbAchievement> selectAchievements(@Param("filter") TbAchievement filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 根据成就ID查询成就信息
@@ -108,15 +111,30 @@ public interface AchievementMapper extends BaseMapper<TbAchievement> {
* @description 分页查询成就
* @param filter 过滤条件
* @param pageParam 分页参数
* @param userDeptRoles 用户部门角色列表
* @return List<TbAchievement> 成就列表
*/
List<TbAchievement> selectAchievementsPage(@Param("filter") TbAchievement filter, @Param("pageParam") PageParam pageParam);
List<TbAchievement> selectAchievementsPage(@Param("filter") TbAchievement filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 统计成就总数
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return long 总数
*/
long countAchievements(@Param("filter") TbAchievement filter);
long countAchievements(@Param("filter") TbAchievement filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 联表查询用户成就列表(包含进度信息)- 带权限过滤
* @param userId 用户ID
* @param type 成就类型(可选)
* @param userDeptRoles 用户部门角色列表
* @return List<AchievementVO> 用户成就列表(包含进度)
*/
List<AchievementVO> selectUserAchievementsWithProgress(
@Param("userId") String userId,
@Param("type") Integer type,
@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles
);
}

View File

@@ -23,6 +23,9 @@ import org.xyzh.common.dto.usercenter.TbUserAchievementProgress;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.vo.AchievementVO;
import org.xyzh.system.utils.LoginUtil;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
import java.util.*;
import java.util.stream.Collectors;
@@ -51,6 +54,9 @@ public class ACHAchievementServiceImpl implements AchievementService {
@Autowired
private List<AchievementChecker> checkers;
@Autowired
private ResourcePermissionService resourcePermissionService;
// ==================== 成就定义管理 ====================
@Override
@@ -84,6 +90,21 @@ public class ACHAchievementServiceImpl implements AchievementService {
// 插入数据库
int result = achievementMapper.insertAchievement(achievement);
if (result > 0) {
// 创建成就资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
// 使用用户的第一个部门角色创建权限
resourcePermissionService.createResourcePermission(
ResourceType.ACHIEVEMENT.getCode(),
achievement.getAchievementID(),
userDeptRoles.get(0)
);
logger.info("创建成就权限成功: {}", achievement.getAchievementID());
}
} catch (Exception e) {
logger.error("创建成就权限异常,但不影响成就创建: {}", e.getMessage(), e);
}
resultDomain.success("创建成就成功", achievement);
} else {
resultDomain.fail("创建成就失败");
@@ -176,7 +197,9 @@ public class ACHAchievementServiceImpl implements AchievementService {
public ResultDomain<TbAchievement> getAllAchievements(TbAchievement filter) {
ResultDomain<TbAchievement> resultDomain = new ResultDomain<>();
try {
List<TbAchievement> list = achievementMapper.selectAchievements(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbAchievement> list = achievementMapper.selectAchievements(filter, userDeptRoles);
resultDomain.success("获取成就列表成功", list);
return resultDomain;
@@ -196,8 +219,10 @@ public class ACHAchievementServiceImpl implements AchievementService {
filter.setDeleted(false);
}
List<TbAchievement> list = achievementMapper.selectAchievementsPage(filter, pageParam);
long total = achievementMapper.countAchievements(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbAchievement> list = achievementMapper.selectAchievementsPage(filter, pageParam, userDeptRoles);
long total = achievementMapper.countAchievements(filter, userDeptRoles);
pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
@@ -286,87 +311,13 @@ public class ACHAchievementServiceImpl implements AchievementService {
String userID = user.getID();
// 1. 获取所有成就列表根据type过滤)
List<TbAchievement> allAchievements;
if (type != null) {
allAchievements = achievementMapper.selectByType(type);
} else {
TbAchievement filter = new TbAchievement();
filter.setDeleted(false);
allAchievements = achievementMapper.selectAchievements(filter);
}
// 2. 获取用户已获得的成就列表
List<TbUserAchievement> userAchievements = type != null
? userAchievementMapper.selectByUserIdAndType(userID, type)
: userAchievementMapper.selectByUserId(userID);
// 转换为Mapkey为achievementID
Map<String, TbUserAchievement> userAchievementMap = userAchievements.stream()
.collect(Collectors.toMap(TbUserAchievement::getAchievementID, ua -> ua));
// 3. 获取用户的成就进度列表
List<TbUserAchievementProgress> progressList = progressMapper.selectByUserId(userID);
// 转换为Mapkey为achievementID
Map<String, TbUserAchievementProgress> progressMap = progressList.stream()
.collect(Collectors.toMap(TbUserAchievementProgress::getAchievementID, p -> p));
// 4. 组装AchievementVO列表
List<AchievementVO> achievementVOList = new ArrayList<>();
for (TbAchievement achievement : allAchievements) {
AchievementVO vo = new AchievementVO();
// 复制成就基本信息
vo.setID(achievement.getID());
vo.setAchievementID(achievement.getAchievementID());
vo.setName(achievement.getName());
vo.setDescription(achievement.getDescription());
vo.setType(achievement.getType());
vo.setLevel(achievement.getLevel());
vo.setIcon(achievement.getIcon());
vo.setPoints(achievement.getPoints());
vo.setConditionType(achievement.getConditionType());
vo.setConditionValue(achievement.getConditionValue());
vo.setOrderNum(achievement.getOrderNum());
vo.setCreator(achievement.getCreator());
vo.setUpdater(achievement.getUpdater());
vo.setCreateTime(achievement.getCreateTime());
vo.setUpdateTime(achievement.getUpdateTime());
vo.setDeleted(achievement.getDeleted());
// 设置用户ID
vo.setUserID(userID);
// 填充用户成就信息(如果已获得)
TbUserAchievement userAchievement = userAchievementMap.get(achievement.getAchievementID());
if (userAchievement != null) {
vo.setUserAchievementID(userAchievement.getID());
vo.setObtainTime(userAchievement.getObtainTime());
vo.setObtained(true);
} else {
vo.setObtained(false);
}
// 填充进度信息
TbUserAchievementProgress progress = progressMap.get(achievement.getAchievementID());
if (progress != null) {
vo.setProgressID(progress.getID());
vo.setCurrentValue(progress.getCurrentValue());
vo.setTargetValue(progress.getTargetValue());
vo.setProgressPercentage(progress.getProgressPercentage());
vo.setCompleted(progress.getCompleted());
vo.setLastUpdateTime(progress.getLastUpdateTime());
} else {
// 没有进度记录,设置默认值
vo.setCurrentValue(0);
vo.setTargetValue(achievement.getConditionValue());
vo.setProgressPercentage(0);
vo.setCompleted(false);
}
achievementVOList.add(vo);
}
// 使用联表查询一次性获取所有数据(包含成就、用户成就、进度信息及权限过滤)
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<AchievementVO> achievementVOList = achievementMapper.selectUserAchievementsWithProgress(
userID,
type,
userDeptRoles
);
resultDomain.success("获取成就列表成功", achievementVOList);
return resultDomain;
@@ -652,7 +603,8 @@ public class ACHAchievementServiceImpl implements AchievementService {
// 获取所有成就
TbAchievement filter = new TbAchievement();
filter.setDeleted(false);
List<TbAchievement> allAchievements = achievementMapper.selectAchievements(filter);
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbAchievement> allAchievements = achievementMapper.selectAchievements(filter, userDeptRoles);
// 获取用户进度
List<TbUserAchievementProgress> progressList = progressMapper.selectIncompletedByUserId(userID);
@@ -815,7 +767,8 @@ public class ACHAchievementServiceImpl implements AchievementService {
// 获取所有成就
TbAchievement filter = new TbAchievement();
filter.setDeleted(false);
List<TbAchievement> allAchievements = achievementMapper.selectAchievements(filter);
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbAchievement> allAchievements = achievementMapper.selectAchievements(filter, userDeptRoles);
// 筛选支持该事件类型的成就
return allAchievements.stream()

View File

@@ -65,13 +65,57 @@
</where>
</sql>
<!-- 查询成就列表 -->
<!-- 权限过滤条件基于dept_path的高效继承 -->
<sql id="Permission_Filter">
INNER JOIN tb_resource_permission rp ON a.achievement_id = rp.resource_id
AND rp.resource_type = 6
AND rp.deleted = 0
AND rp.can_read = 1
AND (
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
</foreach>
) user_roles
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
WHERE
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
</sql>
<!-- 查询成就列表 - 添加权限过滤 -->
<select id="selectAchievements" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tb_achievement
<include refid="Base_Where_Clause" />
ORDER BY order_num ASC, create_time DESC
SELECT DISTINCT a.*
FROM tb_achievement a
<include refid="Permission_Filter" />
WHERE 1=1
<if test="filter != null">
<if test="filter.achievementID != null and filter.achievementID != ''">
AND a.achievement_id = #{filter.achievementID}
</if>
<if test="filter.name != null and filter.name != ''">
AND a.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.type != null">
AND a.type = #{filter.type}
</if>
<if test="filter.level != null">
AND a.level = #{filter.level}
</if>
<if test="filter.deleted != null">
AND a.deleted = #{filter.deleted}
</if>
</if>
ORDER BY a.order_num ASC, a.create_time DESC
</select>
<!-- 根据成就ID查询成就信息 -->
@@ -227,20 +271,133 @@
</delete>
<!-- 分页查询成就 -->
<!-- selectAchievementsPage - 添加权限过滤 -->
<select id="selectAchievementsPage" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tb_achievement
<include refid="Base_Where_Clause" />
ORDER BY order_num ASC, create_time DESC
SELECT DISTINCT a.*
FROM tb_achievement a
<include refid="Permission_Filter" />
WHERE 1=1
<if test="filter != null">
<if test="filter.achievementID != null and filter.achievementID != ''">
AND a.achievement_id = #{filter.achievementID}
</if>
<if test="filter.name != null and filter.name != ''">
AND a.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.type != null">
AND a.type = #{filter.type}
</if>
<if test="filter.level != null">
AND a.level = #{filter.level}
</if>
<if test="filter.deleted != null">
AND a.deleted = #{filter.deleted}
</if>
</if>
ORDER BY a.order_num ASC, a.create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- 统计成就总数 -->
<!-- 统计成就总数 - 添加权限过滤 -->
<select id="countAchievements" resultType="long">
SELECT COUNT(1)
FROM tb_achievement
<include refid="Base_Where_Clause" />
SELECT COUNT(DISTINCT a.id)
FROM tb_achievement a
<include refid="Permission_Filter" />
WHERE 1=1
<if test="filter != null">
<if test="filter.achievementID != null and filter.achievementID != ''">
AND a.achievement_id = #{filter.achievementID}
</if>
<if test="filter.name != null and filter.name != ''">
AND a.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.type != null">
AND a.type = #{filter.type}
</if>
<if test="filter.level != null">
AND a.level = #{filter.level}
</if>
<if test="filter.deleted != null">
AND a.deleted = #{filter.deleted}
</if>
</if>
</select>
<!-- AchievementVO 结果映射(包含用户成就和进度信息) -->
<resultMap id="AchievementVOResultMap" type="org.xyzh.common.vo.AchievementVO">
<!-- 成就基本信息 -->
<id column="id" property="ID" />
<result column="achievement_id" property="achievementID" />
<result column="name" property="name" />
<result column="description" property="description" />
<result column="icon" property="icon" />
<result column="type" property="type" />
<result column="level" property="level" />
<result column="condition_type" property="conditionType" />
<result column="condition_value" property="conditionValue" />
<result column="points" property="points" />
<result column="order_num" property="orderNum" />
<result column="creator" property="creator" />
<result column="updater" property="updater" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="deleted" property="deleted" />
<!-- 用户信息 -->
<result column="user_id" property="userID" />
<!-- 用户成就信息 -->
<result column="user_achievement_id" property="userAchievementID" />
<result column="obtain_time" property="obtainTime" />
<result column="obtained" property="obtained" />
<!-- 进度信息 -->
<result column="progress_id" property="progressID" />
<result column="current_value" property="currentValue" />
<result column="target_value" property="targetValue" />
<result column="progress_percentage" property="progressPercentage" />
<result column="completed" property="completed" />
<result column="last_update_time" property="lastUpdateTime" />
</resultMap>
<!-- 联表查询用户成就列表(包含进度信息)- 带权限过滤 -->
<select id="selectUserAchievementsWithProgress" resultMap="AchievementVOResultMap">
SELECT
a.id,
a.achievement_id,
a.name,
a.description,
a.icon,
a.type,
a.level,
a.condition_type,
a.condition_value,
a.points,
a.order_num,
a.creator,
a.updater,
a.create_time,
a.update_time,
a.deleted,
#{userId} as user_id,
ua.id as user_achievement_id,
ua.obtain_time,
CASE WHEN ua.id IS NOT NULL THEN 1 ELSE 0 END as obtained,
p.id as progress_id,
COALESCE(p.current_value, 0) as current_value,
COALESCE(p.target_value, a.condition_value) as target_value,
COALESCE(p.progress_percentage, 0) as progress_percentage,
COALESCE(p.completed, 0) as completed,
p.last_update_time
FROM tb_achievement a
<include refid="Permission_Filter"/>
LEFT JOIN tb_user_achievement ua ON a.achievement_id = ua.achievement_id AND ua.user_id = #{userId}
LEFT JOIN tb_user_achievement_progress p ON a.achievement_id = p.achievement_id AND p.user_id = #{userId}
WHERE a.deleted = 0
<if test="type != null">
AND a.type = #{type}
</if>
ORDER BY a.order_num ASC, a.create_time DESC
</select>
</mapper>

View File

@@ -83,8 +83,8 @@
<!--然后定义loggers只有定义了logger并引入的appenderappender才会生效-->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<logger name="org.mybatis" level="info" additivity="false">
<!--过滤掉spring的一些无用的DEBUG信息-->
<logger name="org.mybatis" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</logger>
<!--监控系统信息-->
@@ -93,6 +93,17 @@
<AppenderRef ref="Console"/>
</Logger>
<!-- MyBatis Mapper 日志配置 - 打印SQL -->
<Logger name="org.xyzh.achievement.mapper" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.xyzh.system.mapper" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.xyzh.news.mapper" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<!-- 项目包日志配置 - Auth模块 -->
<Logger name="org.xyzh.auth" level="debug" additivity="false">
<AppenderRef ref="Console"/>
@@ -129,6 +140,15 @@
<AppenderRef ref="RollingFileError"/>
</Logger>
<!-- 项目包日志配置 - Achievement模块 -->
<Logger name="org.xyzh.achievement" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="Filelog"/>
<AppenderRef ref="RollingFileInfo"/>
<AppenderRef ref="RollingFileWarn"/>
<AppenderRef ref="RollingFileError"/>
</Logger>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="Filelog"/>

View File

@@ -6,6 +6,7 @@ import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.dept.TbSysDept;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.vo.UserDeptRoleVO;
/**
* @description DepartmentService.java文件描述 部门服务接口
@@ -98,12 +99,12 @@ public interface DepartmentService {
ResultDomain<TbSysRole> getDeptByRole(String deptId);
/**
* @description 查询部门绑定角色
* @return ResultDomain<TbSysDeptRole> 角色信息
* @description 查询部门绑定角色列表(包含名称)
* @return ResultDomain<UserDeptRoleVO> 部门角色信息
* @author yslg
* @since 2025-10-06
*/
ResultDomain<TbSysDeptRole> getDeptByRoleList();
ResultDomain<UserDeptRoleVO> getDeptByRoleList();
/**
* @description 绑定部门角色

View File

@@ -0,0 +1,27 @@
package org.xyzh.api.system.permission;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.permission.TbResourcePermission;
import org.xyzh.common.vo.UserDeptRoleVO;
/**
* @description 资源权限控制服务接口
* @filename ResourcePermissionService.java
* @author yslg
* @copyright xyzh
* @since 2025-10-29
*/
public interface ResourcePermissionService {
/**
* @description 创建资源权限 根据用户
* @param resource_type 资源类型
* @param resource_id 资源ID
* @param creatorID 创建者ID
* @return ResultDomain<TbResourcePermission> 资源权限
* @author yslg
* @since 2025-10-29
*/
ResultDomain<TbResourcePermission> createResourcePermission(Integer resource_type, String resource_id, UserDeptRoleVO userDeptRole);
}

View File

@@ -4,7 +4,7 @@ import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.permission.TbSysPermission;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.dto.role.TbSysRolePermission;
import org.xyzh.common.vo.DeptRoleVO;
import org.xyzh.common.vo.UserDeptRoleVO;
/**
@@ -72,11 +72,11 @@ public interface RoleService {
/**
* @description 根据用户ID查询部门角色列表
* @param userId 用户ID
* @return ResultDomain<DeptRoleVO> 角色列表
* @return ResultDomain<UserDeptRoleVO> 角色列表
* @author yslg
* @since 2025-09-28
*/
ResultDomain<DeptRoleVO> getDeptRolesByUserId(String userId);
ResultDomain<UserDeptRoleVO> getDeptRolesByUserId(String userId);
/**
* @description 检查角色名称是否存在

View File

@@ -169,7 +169,7 @@ public interface UserService {
* @author yslg
* @since 2025-10-09
*/
ResultDomain<TbSysUserDeptRole> getBindUserDeptRoleList(TbSysUserDeptRole filter);
ResultDomain<UserDeptRoleVO> getBindUserDeptRoleList(TbSysUserDeptRole filter);
/**

View File

@@ -3,6 +3,7 @@ package org.xyzh.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -26,6 +27,7 @@ public class SecurityConfig {
@Autowired
private AuthProperties authProperties;
@Lazy
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

View File

@@ -25,7 +25,7 @@ import org.xyzh.api.system.role.RoleService;
import org.xyzh.api.system.permission.PermissionService;
import org.xyzh.common.redis.service.RedisService;
import org.xyzh.api.system.menu.MenuService;
import org.xyzh.common.vo.DeptRoleVO;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.Date;
import java.util.List;
@@ -206,9 +206,9 @@ public class LoginServiceImpl implements LoginService {
loginDomain.setIpAddress(ipAddress);
// 获取用户角色和权限(如果服务可用)
try {
ResultDomain<DeptRoleVO> resultDomain = roleService.getDeptRolesByUserId(user.getID());
ResultDomain<UserDeptRoleVO> resultDomain = roleService.getDeptRolesByUserId(user.getID());
if (resultDomain.isSuccess()) {
List<DeptRoleVO> roles = resultDomain.getDataList();
List<UserDeptRoleVO> roles = resultDomain.getDataList();
loginDomain.setRoles(roles);
} else {
loginDomain.setRoles(new ArrayList<>());

View File

@@ -9,7 +9,7 @@ import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.dto.permission.TbSysPermission;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.menu.TbSysMenu;
import org.xyzh.common.vo.DeptRoleVO;
import org.xyzh.common.vo.UserDeptRoleVO;
/**
* @description LoginDomain.java文件描述 登录域对象
* @filename LoginDomain.java
@@ -40,7 +40,7 @@ public class LoginDomain implements Serializable {
* @author yslg
* @since 2025-09-28
*/
private List<DeptRoleVO> roles;
private List<UserDeptRoleVO> roles;
/**
* @description 用户权限列表
@@ -132,7 +132,7 @@ public class LoginDomain implements Serializable {
* @author yslg
* @since 2025-09-28
*/
public List<DeptRoleVO> getRoles() {
public List<UserDeptRoleVO> getRoles() {
return roles;
}
@@ -141,7 +141,7 @@ public class LoginDomain implements Serializable {
* @author yslg
* @since 2025-09-28
*/
public void setRoles(List<DeptRoleVO> roles) {
public void setRoles(List<UserDeptRoleVO> roles) {
this.roles = roles;
}

View File

@@ -0,0 +1,36 @@
package org.xyzh.common.core.enums;
public enum ResourceType {
NEWS(1, "新闻", "新闻"),
COURSE(2, "课程", "课程"),
TASK(3, "任务", "任务"),
DEPT(4, "部门", "部门"),
ROLE(5, "角色", "角色"),
ACHIEVEMENT(6, "成就", "成就"),
CRONTAB_TASK(7, "定时任务", "定时任务"),
BANNER(8, "轮播图", "轮播图"),
TAG(9, "标签", "标签");
private int code;
private String name;
private String description;
ResourceType(int code, String name, String description) {
this.code = code;
this.name = name;
this.description = description;
}
public int getCode() {
return code;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
}

View File

@@ -26,6 +26,13 @@ public class TbSysDept extends BaseDTO{
*/
private String parentID;
/**
* @description 部门路径,格式:/root_department/dept_001/
* @author yslg
* @since 2025-10-29
*/
private String deptPath;
/**
* @description 部门名称
* @author yslg
@@ -71,6 +78,14 @@ public class TbSysDept extends BaseDTO{
this.parentID = parentID;
}
public String getDeptPath() {
return deptPath;
}
public void setDeptPath(String deptPath) {
this.deptPath = deptPath;
}
public String getName() {
return name;
}
@@ -109,6 +124,7 @@ public class TbSysDept extends BaseDTO{
"id='" + getID() + '\'' +
", deptID='" + deptID + '\'' +
", parentID='" + parentID + '\'' +
", deptPath='" + deptPath + '\'' +
", name='" + name + '\'' +
", description='" + description + '\'' +
", creator='" + creator + '\'' +

View File

@@ -1,58 +0,0 @@
package org.xyzh.common.vo;
import org.xyzh.common.dto.dept.TbSysDept;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.dto.user.TbSysUser;
import java.util.List;
public class DeptRoleVO {
private TbSysDept dept;
private TbSysRole role;
private List<TbSysDept> depts;
private List<TbSysRole> roles;
private List<TbSysUser> users;
public TbSysDept getDept() {
return dept;
}
public void setDept(TbSysDept dept) {
this.dept = dept;
}
public TbSysRole getRole() {
return role;
}
public void setRole(TbSysRole role) {
this.role = role;
}
public List<TbSysDept> getDepts() {
return depts;
}
public void setDepts(List<TbSysDept> depts) {
this.depts = depts;
}
public List<TbSysRole> getRoles() {
return roles;
}
public void setRoles(List<TbSysRole> roles) {
this.roles = roles;
}
public List<TbSysUser> getUsers() {
return users;
}
public void setUsers(List<TbSysUser> users) {
this.users = users;
}
}

View File

@@ -0,0 +1,120 @@
package org.xyzh.common.vo;
import org.xyzh.common.dto.permission.TbResourcePermission;
/**
* @description 资源权限视图对象
* @filename ResourcePermissionVO.java
* @author yslg
* @copyright xyzh
* @since 2025-10-29
*/
public class ResourcePermissionVO extends TbResourcePermission {
private static final long serialVersionUID = 1L;
/**
* @description 部门名称
*/
private String deptName;
/**
* @description 角色名称
*/
private String roleName;
/**
* @description 资源标题(根据资源类型获取)
*/
private String resourceTitle;
/**
* @description 查询用的用户ID用于权限校验
*/
private String userID;
/**
* @description 用户的部门ID列表用于权限校验
*/
private String[] userDeptIDs;
/**
* @description 用户的角色ID列表用于权限校验
*/
private String[] userRoleIDs;
public ResourcePermissionVO() {
super();
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getResourceTitle() {
return resourceTitle;
}
public void setResourceTitle(String resourceTitle) {
this.resourceTitle = resourceTitle;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String[] getUserDeptIDs() {
return userDeptIDs;
}
public void setUserDeptIDs(String[] userDeptIDs) {
this.userDeptIDs = userDeptIDs;
}
public String[] getUserRoleIDs() {
return userRoleIDs;
}
public void setUserRoleIDs(String[] userRoleIDs) {
this.userRoleIDs = userRoleIDs;
}
@Override
public String toString() {
return "ResourcePermissionVO{" +
"id=" + getID() +
", resourceType=" + getResourceType() +
", resourceID='" + getResourceID() + '\'' +
", resourceTitle='" + resourceTitle + '\'' +
", deptID='" + getDeptID() + '\'' +
", deptName='" + deptName + '\'' +
", roleID='" + getRoleID() + '\'' +
", roleName='" + roleName + '\'' +
", canRead=" + getCanRead() +
", canWrite=" + getCanWrite() +
", canExecute=" + getCanExecute() +
", creator='" + getCreator() + '\'' +
", updater='" + getUpdater() + '\'' +
", createTime=" + getCreateTime() +
", updateTime=" + getUpdateTime() +
", deleted=" + getDeleted() +
'}';
}
}

View File

@@ -1,51 +1,159 @@
package org.xyzh.common.vo;
import java.util.List;
import org.xyzh.common.dto.BaseDTO;
import org.xyzh.common.dto.dept.TbSysDept;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.dto.user.TbSysUserDeptRole;
public class UserDeptRoleVO extends BaseDTO{
private TbSysUser user;
private List<TbSysUser> users;
import java.util.List;
public class UserDeptRoleVO {
private List<TbSysDept> depts;
private List<TbSysRole> roles;
private List<TbSysUser> users;
private List<TbSysUserDeptRole> userDeptRoles;
public TbSysUser getUser() {
return user;
}
public void setUser(TbSysUser user) {
this.user = user;
}
public List<TbSysUser> getUsers() {
return users;
}
public void setUsers(List<TbSysUser> users) {
this.users = users;
}
// 扁平化字段,用于权限查询优化
private String userID;
private String username;
private String deptID;
private String deptName;
private String deptDescription;
private String parentID;
private String parentName;
private String parentDescription;
private String roleID;
private String roleName;
private String roleDescription;
private String deptPath; // 部门路径,用于快速权限继承判断
public List<TbSysDept> getDepts() {
return depts;
}
public void setDepts(List<TbSysDept> depts) {
this.depts = depts;
}
public List<TbSysRole> getRoles() {
return roles;
}
public void setRoles(List<TbSysRole> roles) {
this.roles = roles;
}
public List<TbSysUser> getUsers() {
return users;
}
public void setUsers(List<TbSysUser> users) {
this.users = users;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getDeptID() {
return deptID;
}
public void setDeptID(String deptID) {
this.deptID = deptID;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public String getDeptDescription() {
return deptDescription;
}
public void setDeptDescription(String deptDescription) {
this.deptDescription = deptDescription;
}
public String getParentID() {
return parentID;
}
public void setParentID(String parentID) {
this.parentID = parentID;
}
public String getParentName() {
return parentName;
}
public void setParentName(String parentName) {
this.parentName = parentName;
}
public String getParentDescription() {
return parentDescription;
}
public void setParentDescription(String parentDescription) {
this.parentDescription = parentDescription;
}
public String getRoleID() {
return roleID;
}
public void setRoleID(String roleID) {
this.roleID = roleID;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getRoleDescription() {
return roleDescription;
}
public void setRoleDescription(String roleDescription) {
this.roleDescription = roleDescription;
}
public String getDeptPath() {
return deptPath;
}
public void setDeptPath(String deptPath) {
this.deptPath = deptPath;
}
public List<TbSysUserDeptRole> getUserDeptRoles() {
return userDeptRoles;
return userDeptRoles;
}
public void setUserDeptRoles(List<TbSysUserDeptRole> userDeptRoles) {
this.userDeptRoles = userDeptRoles;
this.userDeptRoles = userDeptRoles;
}
}

View File

@@ -32,6 +32,12 @@
<artifactId>common-all</artifactId>
<version>${school-news.version}</version>
</dependency>
<!-- System模块依赖 -->
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>system</artifactId>
<version>${school-news.version}</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>

View File

@@ -13,9 +13,15 @@ import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.crontab.TbCrontabTask;
import org.xyzh.common.dto.crontab.TbCrontabLog;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.crontab.mapper.CrontabTaskMapper;
import org.xyzh.crontab.mapper.CrontabLogMapper;
import org.xyzh.crontab.scheduler.SchedulerManager;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.dto.user.TbSysUserDeptRole;
import org.xyzh.common.core.enums.ResourceType;
import org.xyzh.system.utils.LoginUtil;
import java.time.LocalDateTime;
import java.util.Calendar;
@@ -43,6 +49,9 @@ public class CrontabServiceImpl implements CrontabService {
@Autowired
private SchedulerManager schedulerManager;
@Autowired
private ResourcePermissionService resourcePermissionService;
// ----------------定时任务管理--------------------------------
@Override
@@ -77,6 +86,21 @@ public class CrontabServiceImpl implements CrontabService {
if (result > 0) {
logger.info("创建定时任务成功: {}", task.getTaskName());
// 创建定时任务资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
resourcePermissionService.createResourcePermission(
ResourceType.CRONTAB_TASK.getCode(),
task.getTaskId(),
userDeptRoles.get(0)
);
logger.info("创建定时任务权限成功: {}", task.getTaskName());
}
} catch (Exception e) {
logger.error("创建定时任务权限异常,但不影响任务创建: {}", e.getMessage(), e);
}
// 如果任务状态为启动,则立即调度
if (task.getStatus() == 1) {
schedulerManager.scheduleTask(task);

View File

@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.resource.TbBanner;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.List;
@@ -21,24 +22,26 @@ public interface BannerMapper extends BaseMapper<TbBanner> {
/**
* @description 查询Banner列表
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return List<TbBanner> Banner列表
* @author yslg
* @since 2025-10-15
*/
List<TbBanner> selectBanners(TbBanner filter);
List<TbBanner> selectBanners(@Param("filter") TbBanner filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
List<TbBanner> selectBannersLimit(@Param("filter") TbBanner filter, @Param("limit") Integer limit);
List<TbBanner> selectBannersLimit(@Param("filter") TbBanner filter, @Param("limit") Integer limit, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 分页查询Banner
* @param filter 过滤条件
* @param pageParam 分页参数
* @param userDeptRoles 用户部门角色列表
* @return List<TbBanner> Banner列表
* @author yslg
* @since 2025-10-15
*/
List<TbBanner> selectBannersPage(@Param("filter") TbBanner filter, @Param("pageParam") PageParam pageParam);
List<TbBanner> selectBannersPage(@Param("filter") TbBanner filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 根据Banner ID查询Banner信息
@@ -133,9 +136,10 @@ public interface BannerMapper extends BaseMapper<TbBanner> {
/**
* @description 统计Banner总数
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return long 总数
* @author yslg
* @since 2025-10-15
*/
long countBanners(@Param("filter") TbBanner filter);
long countBanners(@Param("filter") TbBanner filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
}

View File

@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.resource.TbResource;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.List;
@@ -21,11 +22,12 @@ public interface ResourceMapper extends BaseMapper<TbResource> {
/**
* @description 查询资源列表
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return List<TbResource> 资源列表
* @author yslg
* @since 2025-10-15
*/
List<TbResource> selectResources(TbResource filter);
List<TbResource> selectResources(@Param("filter") TbResource filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 根据资源ID查询资源信息
@@ -149,20 +151,22 @@ public interface ResourceMapper extends BaseMapper<TbResource> {
* @description 分页查询资源
* @param filter 过滤条件
* @param pageParam 分页参数
* @param userDeptRoles 用户部门角色列表
* @return List<TbResource> 资源列表
* @author yslg
* @since 2025-10-15
*/
List<TbResource> selectResourcesPage(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam);
List<TbResource> selectResourcesPage(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 统计资源总数
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return long 总数
* @author yslg
* @since 2025-10-15
*/
long countResources(TbResource filter);
long countResources(@Param("filter") TbResource filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 更新资源收藏次数

View File

@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.resource.TbTag;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.List;
@@ -21,11 +22,12 @@ public interface TagMapper extends BaseMapper<TbTag> {
/**
* @description 查询标签列表
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return List<TbTag> 标签列表
* @author yslg
* @since 2025-10-15
*/
List<TbTag> selectTags(TbTag filter);
List<TbTag> selectTags(@Param("filter") TbTag filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 根据标签ID查询标签信息

View File

@@ -17,6 +17,11 @@ import org.xyzh.common.dto.resource.TbBanner;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.news.mapper.BannerMapper;
import org.xyzh.api.news.banner.BannerService;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
import org.xyzh.system.utils.LoginUtil;
/**
* @description 横幅服务实现类
@@ -33,11 +38,16 @@ public class NCBannerServiceImpl implements BannerService {
@Autowired
private BannerMapper bannerMapper;
@Autowired
private ResourcePermissionService resourcePermissionService;
@Override
public ResultDomain<TbBanner> getBannerList(TbBanner filter) {
ResultDomain<TbBanner> resultDomain = new ResultDomain<>();
List<TbBanner> list = bannerMapper.selectBanners(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbBanner> list = bannerMapper.selectBanners(filter, userDeptRoles);
resultDomain.success("获取横幅列表成功", list);
return resultDomain;
}
@@ -45,9 +55,11 @@ public class NCBannerServiceImpl implements BannerService {
@Override
public ResultDomain<TbBanner> getBannerPage(PageParam pageParam,TbBanner filter) {
ResultDomain<TbBanner> resultDomain = new ResultDomain<>();
List<TbBanner> list = bannerMapper.selectBannersPage(filter, pageParam);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbBanner> list = bannerMapper.selectBannersPage(filter, pageParam, userDeptRoles);
PageDomain<TbBanner> pageDomain = new PageDomain<>();
int total = (int)bannerMapper.countBanners(filter);
int total = (int)bannerMapper.countBanners(filter, userDeptRoles);
pageParam.setTotalElements(total);
pageParam.setTotalPages( (int)Math.ceil((double)total / pageParam.getPageSize()));
pageDomain.setDataList(list);
@@ -100,6 +112,23 @@ public class NCBannerServiceImpl implements BannerService {
int result = bannerMapper.insertBanner(banner);
if (result > 0) {
logger.info("创建横幅成功: {}", banner.getTitle());
// 创建横幅资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
// 使用用户的第一个部门角色创建权限
resourcePermissionService.createResourcePermission(
ResourceType.BANNER.getCode(),
banner.getBannerID(),
userDeptRoles.get(0)
);
logger.info("创建横幅权限成功: {}", banner.getBannerID());
}
} catch (Exception e) {
logger.error("创建横幅权限异常,但不影响横幅创建: {}", e.getMessage(), e);
}
resultDomain.success("创建横幅成功", banner);
return resultDomain;
} else {
@@ -363,8 +392,9 @@ public class NCBannerServiceImpl implements BannerService {
ResultDomain<TbBanner> resultDomain = new ResultDomain<>();
TbBanner filter = new TbBanner();
filter.setStatus(1);
List<TbBanner> list = bannerMapper.selectBannersLimit(filter, 5);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbBanner> list = bannerMapper.selectBannersLimit(filter, 5, userDeptRoles);
resultDomain.success("获取首页横幅列表成功", list);
return resultDomain;
}

View File

@@ -22,6 +22,9 @@ import org.xyzh.news.mapper.ResourceTagMapper;
import org.xyzh.system.utils.LoginUtil;
import org.xyzh.api.news.resource.ResourceService;
import org.xyzh.api.usercenter.collection.UserCollectionService;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
import java.util.ArrayList;
import java.util.Date;
@@ -49,6 +52,9 @@ public class NCResourceServiceImpl implements ResourceService {
@Autowired
private UserCollectionService userCollectionService;
@Autowired
private ResourcePermissionService resourcePermissionService;
@Override
public ResultDomain<TbResource> getResourceList(TbResource filter) {
ResultDomain<TbResource> resultDomain = new ResultDomain<>();
@@ -56,7 +62,9 @@ public class NCResourceServiceImpl implements ResourceService {
if (filter == null) {
filter = new TbResource();
}
List<TbResource> list = resourceMapper.selectResources(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbResource> list = resourceMapper.selectResources(filter, userDeptRoles);
resultDomain.success("获取资源列表成功", list);
return resultDomain;
} catch (Exception e) {
@@ -74,8 +82,10 @@ public class NCResourceServiceImpl implements ResourceService {
filter = new TbResource();
}
List<TbResource> list = resourceMapper.selectResourcesPage(filter, pageParam);
long total = resourceMapper.countResources(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbResource> list = resourceMapper.selectResourcesPage(filter, pageParam, userDeptRoles);
long total = resourceMapper.countResources(filter, userDeptRoles);
pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
resultDomain.success("获取资源分页成功", new PageDomain<TbResource>(pageParam, list));
@@ -192,6 +202,23 @@ public class NCResourceServiceImpl implements ResourceService {
if (result > 0) {
logger.info("创建资源成功: {}", resourceVO.getResource().getTitle());
// 创建资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
// 使用用户的第一个部门角色创建权限
resourcePermissionService.createResourcePermission(
ResourceType.NEWS.getCode(),
resourceVO.getResource().getResourceID(),
userDeptRoles.get(0)
);
logger.info("创建资源权限成功: {}", resourceVO.getResource().getResourceID());
}
} catch (Exception e) {
logger.error("创建资源权限异常,但不影响资源创建: {}", e.getMessage(), e);
}
resultDomain.success("创建资源成功", resourceVO);
return resultDomain;
} else {
@@ -679,7 +706,9 @@ public class NCResourceServiceImpl implements ResourceService {
filter.setIsRecommend(true);
filter.setStatus(1); // 只查询已发布的
List<TbResource> list = resourceMapper.selectResources(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbResource> list = resourceMapper.selectResources(filter, userDeptRoles);
// 如果指定了limit截取列表
if (limit != null && limit > 0 && list != null && list.size() > limit) {
@@ -704,7 +733,9 @@ public class NCResourceServiceImpl implements ResourceService {
filter.setIsBanner(true);
filter.setStatus(1); // 只查询已发布的
List<TbResource> list = resourceMapper.selectResources(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbResource> list = resourceMapper.selectResources(filter, userDeptRoles);
// 如果指定了limit截取列表
if (limit != null && limit > 0 && list != null && list.size() > limit) {

View File

@@ -19,6 +19,9 @@ import org.xyzh.news.mapper.ResourceTagMapper;
import org.xyzh.news.mapper.TagMapper;
import org.xyzh.system.utils.LoginUtil;
import org.xyzh.api.news.tag.TagService;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
/**
* @description 标签服务实现类
@@ -38,6 +41,9 @@ public class NCTagServiceImpl implements TagService {
@Autowired
private ResourceTagMapper resourceTagMapper;
@Autowired
private ResourcePermissionService resourcePermissionService;
// ----------------标签管理相关--------------------------------
@Override
@@ -74,6 +80,23 @@ public class NCTagServiceImpl implements TagService {
int result = tagMapper.insertTag(tag);
if (result > 0) {
logger.info("创建标签成功: {}", tag.getName());
// 创建标签资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
// 使用用户的第一个部门角色创建权限
resourcePermissionService.createResourcePermission(
ResourceType.TAG.getCode(),
tag.getTagID(),
userDeptRoles.get(0)
);
logger.info("创建标签权限成功: {}", tag.getTagID());
}
} catch (Exception e) {
logger.error("创建标签权限异常,但不影响标签创建: {}", e.getMessage(), e);
}
resultDomain.success("创建标签成功", tag);
return resultDomain;
} else {
@@ -195,7 +218,9 @@ public class NCTagServiceImpl implements TagService {
public ResultDomain<TbTag> getAllTags() {
ResultDomain<TbTag> resultDomain = new ResultDomain<>();
try {
List<TbTag> tags = tagMapper.selectTags(new TbTag());
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbTag> tags = tagMapper.selectTags(new TbTag(), userDeptRoles);
resultDomain.success("查询成功", tags);
return resultDomain;
} catch (Exception e) {
@@ -216,7 +241,9 @@ public class NCTagServiceImpl implements TagService {
TbTag filter = new TbTag();
filter.setName(name);
List<TbTag> tags = tagMapper.selectTags(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbTag> tags = tagMapper.selectTags(filter, userDeptRoles);
resultDomain.success("查询成功", tags);
return resultDomain;

View File

@@ -69,21 +69,83 @@
</where>
</sql>
<!-- selectBanners -->
<!-- 权限过滤条件基于dept_path的高效继承 -->
<sql id="Permission_Filter">
INNER JOIN tb_resource_permission rp ON b.banner_id = rp.resource_id
AND rp.resource_type = 8
AND rp.deleted = 0
AND rp.can_read = 1
AND (
-- 全局权限:所有用户可访问
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
</foreach>
) user_roles
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
WHERE
-- 部门级权限当前部门或父部门通过dept_path判断继承关系
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
-- 角色级权限:跨部门的角色权限
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
-- 精确权限:特定部门的特定角色
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
</sql>
<!-- selectBanners - 添加权限过滤 -->
<select id="selectBanners" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_banner
<include refid="Where_Clause"/>
ORDER BY order_num ASC, create_time DESC
SELECT DISTINCT b.*
FROM tb_banner b
<include refid="Permission_Filter"/>
WHERE b.deleted = 0
<if test="filter.bannerID != null and filter.bannerID != ''">
AND b.banner_id = #{filter.bannerID}
</if>
<if test="filter.title != null and filter.title != ''">
AND b.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.linkType != null">
AND b.link_type = #{filter.linkType}
</if>
<if test="filter.linkID != null and filter.linkID != ''">
AND b.link_id = #{filter.linkID}
</if>
<if test="filter.status != null">
AND b.status = #{filter.status}
</if>
ORDER BY b.order_num ASC, b.create_time DESC
</select>
<!-- selectBannersLimit - 添加权限过滤 -->
<select id="selectBannersLimit" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_banner
<include refid="Filter_Clause"/>
ORDER BY order_num ASC, create_time DESC
SELECT DISTINCT b.*
FROM tb_banner b
<include refid="Permission_Filter"/>
WHERE b.deleted = 0
<if test="filter.bannerID != null and filter.bannerID != ''">
AND b.banner_id = #{filter.bannerID}
</if>
<if test="filter.title != null and filter.title != ''">
AND b.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.linkType != null">
AND b.link_type = #{filter.linkType}
</if>
<if test="filter.linkID != null and filter.linkID != ''">
AND b.link_id = #{filter.linkID}
</if>
<if test="filter.status != null">
AND b.status = #{filter.status}
</if>
ORDER BY b.order_num ASC, b.create_time DESC
LIMIT #{limit}
</select>
@@ -218,20 +280,56 @@
</delete>
<!-- 分页查询Banner -->
<!-- selectBannersPage - 添加权限过滤 -->
<select id="selectBannersPage" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tb_banner
<include refid="Filter_Clause" />
ORDER BY order_num ASC, create_time DESC
SELECT DISTINCT b.*
FROM tb_banner b
<include refid="Permission_Filter"/>
WHERE b.deleted = 0
<if test="filter != null">
<if test="filter.bannerID != null and filter.bannerID != ''">
AND b.banner_id = #{filter.bannerID}
</if>
<if test="filter.title != null and filter.title != ''">
AND b.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.linkType != null">
AND b.link_type = #{filter.linkType}
</if>
<if test="filter.linkID != null and filter.linkID != ''">
AND b.link_id = #{filter.linkID}
</if>
<if test="filter.status != null">
AND b.status = #{filter.status}
</if>
</if>
ORDER BY b.order_num ASC, b.create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- 统计Banner总数 -->
<!-- 统计Banner总数 - 添加权限过滤 -->
<select id="countBanners" resultType="long">
SELECT COUNT(1)
FROM tb_banner
<include refid="Filter_Clause" />
SELECT COUNT(DISTINCT b.id)
FROM tb_banner b
<include refid="Permission_Filter"/>
WHERE b.deleted = 0
<if test="filter != null">
<if test="filter.bannerID != null and filter.bannerID != ''">
AND b.banner_id = #{filter.bannerID}
</if>
<if test="filter.title != null and filter.title != ''">
AND b.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.linkType != null">
AND b.link_type = #{filter.linkType}
</if>
<if test="filter.linkID != null and filter.linkID != ''">
AND b.link_id = #{filter.linkID}
</if>
<if test="filter.status != null">
AND b.status = #{filter.status}
</if>
</if>
</select>
</mapper>

View File

@@ -62,13 +62,62 @@
</where>
</sql>
<!-- selectResources -->
<!-- 权限过滤条件基于dept_path的高效继承 -->
<sql id="Permission_Filter">
INNER JOIN tb_resource_permission rp ON r.resource_id = rp.resource_id
AND rp.resource_type = 1
AND rp.deleted = 0
AND rp.can_read = 1
AND (
-- 全局权限:所有用户可访问
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
</foreach>
) user_roles
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
WHERE
-- 部门级权限当前部门或父部门通过dept_path判断继承关系
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
-- 角色级权限:跨部门的角色权限
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
-- 精确权限:特定部门的特定角色
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
</sql>
<!-- selectResources - 添加权限过滤 -->
<select id="selectResources" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_resource
<include refid="Where_Clause"/>
ORDER BY publish_time DESC, create_time DESC
SELECT DISTINCT r.*
FROM tb_resource r
<include refid="Permission_Filter"/>
WHERE r.deleted = 0
<if test="filter.title != null and filter.title != ''">
AND r.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.tagID != null and filter.tagID != ''">
AND r.tag_id = #{filter.tagID}
</if>
<if test="filter.author != null and filter.author != ''">
AND r.author LIKE CONCAT('%', #{filter.author}, '%')
</if>
<if test="filter.status != null">
AND r.status = #{filter.status}
</if>
<if test="filter.isRecommend != null">
AND r.is_recommend = #{filter.isRecommend}
</if>
<if test="filter.isBanner != null">
AND r.is_banner = #{filter.isBanner}
</if>
ORDER BY r.publish_time DESC, r.create_time DESC
</select>
<!-- 根据资源ID查询资源信息 -->
@@ -266,41 +315,58 @@
</foreach>
</delete>
<!-- 分页查询资源 -->
<!-- 分页查询资源 - 添加权限过滤 -->
<select id="selectResourcesPage" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tb_resource
<where>
deleted = 0
SELECT DISTINCT r.*
FROM tb_resource r
<include refid="Permission_Filter"/>
WHERE r.deleted = 0
<if test="filter.title != null and filter.title != ''">
AND title LIKE CONCAT('%', #{filter.title}, '%')
AND r.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.tagID != null and filter.tagID != ''">
AND tag_id = #{filter.tagID}
AND r.tag_id = #{filter.tagID}
</if>
<if test="filter.author != null and filter.author != ''">
AND author LIKE CONCAT('%', #{filter.author}, '%')
AND r.author LIKE CONCAT('%', #{filter.author}, '%')
</if>
<if test="filter.status != null">
AND status = #{filter.status}
AND r.status = #{filter.status}
</if>
<if test="filter.isRecommend != null">
AND is_recommend = #{filter.isRecommend}
AND r.is_recommend = #{filter.isRecommend}
</if>
<if test="filter.isBanner != null">
AND is_banner = #{filter.isBanner}
AND r.is_banner = #{filter.isBanner}
</if>
</where>
ORDER BY publish_time DESC, create_time DESC
ORDER BY r.publish_time DESC, r.create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- 统计资源总数 -->
<!-- 统计资源总数 - 添加权限过滤 -->
<select id="countResources" resultType="long">
SELECT COUNT(1)
FROM tb_resource
<include refid="Where_Clause" />
SELECT COUNT(DISTINCT r.id)
FROM tb_resource r
<include refid="Permission_Filter"/>
WHERE r.deleted = 0
<if test="filter.title != null and filter.title != ''">
AND r.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.tagID != null and filter.tagID != ''">
AND r.tag_id = #{filter.tagID}
</if>
<if test="filter.author != null and filter.author != ''">
AND r.author LIKE CONCAT('%', #{filter.author}, '%')
</if>
<if test="filter.status != null">
AND r.status = #{filter.status}
</if>
<if test="filter.isRecommend != null">
AND r.is_recommend = #{filter.isRecommend}
</if>
<if test="filter.isBanner != null">
AND r.is_banner = #{filter.isBanner}
</if>
</select>
<!-- updateResourceCollectCount -->
@@ -318,4 +384,6 @@
SET view_count = view_count + 1
WHERE resource_id = #{resourceID}
</update>
</mapper>

View File

@@ -43,13 +43,56 @@
</where>
</sql>
<!-- selectTags -->
<!-- 权限过滤条件基于dept_path的高效继承 -->
<sql id="Permission_Filter">
INNER JOIN tb_resource_permission rp ON t.tag_id = rp.resource_id
AND rp.resource_type = 9
AND rp.deleted = 0
AND rp.can_read = 1
AND (
-- 全局权限:所有用户可访问
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
</foreach>
) user_roles
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
WHERE
-- 部门级权限当前部门或父部门通过dept_path判断继承关系
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
-- 角色级权限:跨部门的角色权限
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
-- 精确权限:特定部门的特定角色
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
</sql>
<!-- selectTags - 添加权限过滤 -->
<select id="selectTags" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_tag
<include refid="Where_Clause"/>
ORDER BY create_time DESC
SELECT DISTINCT t.*
FROM tb_tag t
<include refid="Permission_Filter"/>
WHERE t.deleted = 0
<if test="filter.tagID != null and filter.tagID != ''">
AND t.tag_id = #{filter.tagID}
</if>
<if test="filter.name != null and filter.name != ''">
AND t.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.color != null and filter.color != ''">
AND t.color = #{filter.color}
</if>
<if test="filter.tagType != null">
AND t.tag_type = #{filter.tagType}
</if>
ORDER BY t.create_time DESC
</select>
<!-- 根据标签ID查询标签信息 -->

View File

@@ -5,7 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.study.TbCourse;
import org.xyzh.common.vo.CourseItemVO;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.List;
@@ -22,11 +22,12 @@ public interface CourseMapper extends BaseMapper<TbCourse> {
/**
* @description 查询课程列表
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return List<TbCourse> 课程列表
* @author yslg
* @since 2025-10-15
*/
List<TbCourse> selectCourses(TbCourse filter);
List<TbCourse> selectCourses(@Param("filter") TbCourse filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 根据课程ID查询课程信息
@@ -159,20 +160,22 @@ public interface CourseMapper extends BaseMapper<TbCourse> {
* @description 分页查询课程
* @param filter 过滤条件
* @param pageParam 分页参数
* @param userDeptRoles 用户部门角色列表
* @return List<TbCourse> 课程列表
* @author yslg
* @since 2025-10-15
*/
List<TbCourse> selectCoursesPage(@Param("filter") TbCourse filter, @Param("pageParam") PageParam pageParam);
List<TbCourse> selectCoursesPage(@Param("filter") TbCourse filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 统计课程总数
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return long 总数
* @author yslg
* @since 2025-10-15
*/
long countCourses(@Param("filter") TbCourse filter);
long countCourses(@Param("filter") TbCourse filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 增加课程浏览次数

View File

@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.study.TbLearningTask;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.vo.TaskItemVO;
import java.util.List;
@@ -22,11 +23,12 @@ public interface LearningTaskMapper extends BaseMapper<TbLearningTask> {
/**
* @description 查询学习任务列表
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return List<TbLearningTask> 学习任务列表
* @author yslg
* @since 2025-10-15
*/
List<TbLearningTask> selectLearningTasks(TbLearningTask filter);
List<TbLearningTask> selectLearningTasks(@Param("filter") TbLearningTask filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 根据任务ID查询任务信息
@@ -150,20 +152,22 @@ public interface LearningTaskMapper extends BaseMapper<TbLearningTask> {
* @description 分页查询学习任务
* @param filter 过滤条件
* @param pageParam 分页参数
* @param userDeptRoles 用户部门角色列表
* @return List<TbLearningTask> 学习任务列表
* @author yslg
* @since 2025-10-15
*/
List<TbLearningTask> selectLearningTasksPage(@Param("filter") TbLearningTask filter, @Param("pageParam") PageParam pageParam);
List<TbLearningTask> selectLearningTasksPage(@Param("filter") TbLearningTask filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
List<TbLearningTask> selectUserLearningTasksPage(@Param("filter") TaskItemVO filter, @Param("pageParam") PageParam pageParam);
List<TbLearningTask> selectUserLearningTasksPage(@Param("filter") TaskItemVO filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 统计学习任务总数
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return long 总数
* @author yslg
* @since 2025-10-15
*/
long countLearningTasks(@Param("filter") TbLearningTask filter);
long countLearningTasks(@Param("filter") TbLearningTask filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
}

View File

@@ -28,6 +28,9 @@ import org.xyzh.study.mapper.CourseChapterMapper;
import org.xyzh.study.mapper.CourseNodeMapper;
import org.xyzh.study.service.SCCourseService;
import org.xyzh.system.utils.LoginUtil;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
/**
* @description 课程服务实现类
@@ -50,10 +53,15 @@ public class SCCourseServiceImpl implements SCCourseService {
@Autowired
private CourseNodeMapper courseNodeMapper;
@Autowired
private ResourcePermissionService resourcePermissionService;
@Override
public ResultDomain<TbCourse> getCourseList(TbCourse filter) {
ResultDomain<TbCourse> resultDomain = new ResultDomain<>();
List<TbCourse> courses = courseMapper.selectCourses(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbCourse> courses = courseMapper.selectCourses(filter, userDeptRoles);
resultDomain.success("获取课程列表成功", courses);
return resultDomain;
}
@@ -63,8 +71,10 @@ public class SCCourseServiceImpl implements SCCourseService {
ResultDomain<TbCourse> resultDomain = new ResultDomain<>();
TbCourse filter = pageRequest.getFilter();
PageParam pageParam = pageRequest.getPageParam();
List<TbCourse> courses = courseMapper.selectCoursesPage(filter, pageParam);
int total = (int) courseMapper.countCourses(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbCourse> courses = courseMapper.selectCoursesPage(filter, pageParam, userDeptRoles);
int total = (int) courseMapper.countCourses(filter, userDeptRoles);
int totalPages = (int) Math.ceil((double) total / pageParam.getPageSize());
pageParam.setTotalPages(totalPages);
pageParam.setTotalElements(total);
@@ -194,6 +204,22 @@ public class SCCourseServiceImpl implements SCCourseService {
}
}
// 创建课程资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
// 使用用户的第一个部门角色创建权限
resourcePermissionService.createResourcePermission(
ResourceType.COURSE.getCode(),
courseID,
userDeptRoles.get(0)
);
logger.info("创建课程权限成功: {}", courseID);
}
} catch (Exception e) {
logger.error("创建课程权限异常,但不影响课程创建: {}", e.getMessage(), e);
}
resultDomain.success("创建课程成功", courseItemVO);
return resultDomain;
}

View File

@@ -30,6 +30,9 @@ import org.xyzh.system.utils.LoginUtil;
import org.xyzh.study.mapper.TaskItemMapper;
import org.xyzh.api.study.task.LearningTaskService;
import org.xyzh.common.core.enums.TaskItemType;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
/**
* @description 学习任务服务实现类
@@ -55,6 +58,9 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
@Autowired
private TaskItemMapper taskItemMapper;
@Autowired
private ResourcePermissionService resourcePermissionService;
@Override
public ResultDomain<TbTaskItem> addTaskCourse(TbTaskItem taskItem) {
// TODO Auto-generated method stub
@@ -105,8 +111,10 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
@Override
public ResultDomain<TbLearningTask> getTaskPage(TbLearningTask filter, PageParam pageParam) {
ResultDomain<TbLearningTask> resultDomain = new ResultDomain<>();
List<TbLearningTask> taskList = learningTaskMapper.selectLearningTasksPage(filter, pageParam);
long total = learningTaskMapper.countLearningTasks(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbLearningTask> taskList = learningTaskMapper.selectLearningTasksPage(filter, pageParam, userDeptRoles);
long total = learningTaskMapper.countLearningTasks(filter, userDeptRoles);
pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
PageDomain<TbLearningTask> pageDomain = new PageDomain<>();
@@ -125,8 +133,10 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
return resultDomain;
}
filter.setUserID(user.getID());
List<TbLearningTask> taskList = learningTaskMapper.selectUserLearningTasksPage(filter, pageParam);
long total = learningTaskMapper.countLearningTasks(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbLearningTask> taskList = learningTaskMapper.selectUserLearningTasksPage(filter, pageParam, userDeptRoles);
long total = learningTaskMapper.countLearningTasks(filter, userDeptRoles);
pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
PageDomain<TbLearningTask> pageDomain = new PageDomain<>();
@@ -188,7 +198,22 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
taskUserMapper.batchInsertTaskUsers(taskUsers);
for(TbTaskItem item : taskCourses) {
int learnCount = courseMapper.incrementLearnCount(item.getItemID(), taskUsers.size());
}
// 创建任务资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
// 使用用户的第一个部门角色创建权限
resourcePermissionService.createResourcePermission(
ResourceType.TASK.getCode(),
taskID,
userDeptRoles.get(0)
);
logger.info("创建任务权限成功: {}", taskID);
}
} catch (Exception e) {
logger.error("创建任务权限异常,但不影响任务创建: {}", e.getMessage(), e);
}
resultDomain.success("创建任务成功", taskVO);
@@ -515,7 +540,9 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
@Override
public ResultDomain<TbLearningTask> getTaskList(TbLearningTask filter) {
ResultDomain<TbLearningTask> resultDomain = new ResultDomain<>();
List<TbLearningTask> taskList = learningTaskMapper.selectLearningTasks(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbLearningTask> taskList = learningTaskMapper.selectLearningTasks(filter, userDeptRoles);
resultDomain.success("获取任务列表成功", taskList);
return resultDomain;
}

View File

@@ -78,13 +78,56 @@
</where>
</sql>
<!-- selectCourses -->
<!-- 权限过滤条件基于dept_path的高效继承 -->
<sql id="Permission_Filter">
INNER JOIN tb_resource_permission rp ON c.course_id = rp.resource_id
AND rp.resource_type = 2
AND rp.deleted = 0
AND rp.can_read = 1
AND (
-- 全局权限:所有用户可访问
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
</foreach>
) user_roles
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
WHERE
-- 部门级权限当前部门或父部门通过dept_path判断继承关系
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
-- 角色级权限:跨部门的角色权限
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
-- 精确权限:特定部门的特定角色
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
</sql>
<!-- selectCourses - 添加权限过滤 -->
<select id="selectCourses" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_course
<include refid="Where_Clause"/>
ORDER BY order_num ASC, create_time DESC
SELECT DISTINCT c.*
FROM tb_course c
<include refid="Permission_Filter"/>
WHERE c.deleted = 0
<if test="filter.courseID != null and filter.courseID != ''">
AND c.course_id = #{filter.courseID}
</if>
<if test="filter.name != null and filter.name != ''">
AND c.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.teacher != null and filter.teacher != ''">
AND c.teacher LIKE CONCAT('%', #{filter.teacher}, '%')
</if>
<if test="filter.status != null">
AND c.status = #{filter.status}
</if>
ORDER BY c.order_num ASC, c.create_time DESC
</select>
<!-- 根据课程ID查询课程信息 -->
@@ -270,20 +313,62 @@
</delete>
<!-- 分页查询课程 -->
<!-- selectCoursesPage - 添加权限过滤 -->
<select id="selectCoursesPage" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tb_course
<include refid="Filter_Clause" />
ORDER BY order_num ASC, create_time DESC
SELECT DISTINCT c.*
FROM tb_course c
<include refid="Permission_Filter"/>
WHERE c.deleted = 0
<if test="filter != null">
<if test="filter.courseID != null and filter.courseID != ''">
AND c.course_id = #{filter.courseID}
</if>
<if test="filter.name != null and filter.name != ''">
AND c.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.teacher != null and filter.teacher != ''">
AND c.teacher LIKE CONCAT('%', #{filter.teacher}, '%')
</if>
<if test="filter.status != null">
AND c.status = #{filter.status}
</if>
<if test="filter.orderNum != null">
AND c.order_num = #{filter.orderNum}
</if>
<if test="filter.creator != null and filter.creator != ''">
AND c.creator = #{filter.creator}
</if>
</if>
ORDER BY c.order_num ASC, c.create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- 统计课程总数 -->
<!-- 统计课程总数 - 添加权限过滤 -->
<select id="countCourses" resultType="long">
SELECT COUNT(1)
FROM tb_course
<include refid="Filter_Clause" />
SELECT COUNT(DISTINCT c.id)
FROM tb_course c
<include refid="Permission_Filter"/>
WHERE c.deleted = 0
<if test="filter != null">
<if test="filter.courseID != null and filter.courseID != ''">
AND c.course_id = #{filter.courseID}
</if>
<if test="filter.name != null and filter.name != ''">
AND c.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.teacher != null and filter.teacher != ''">
AND c.teacher LIKE CONCAT('%', #{filter.teacher}, '%')
</if>
<if test="filter.status != null">
AND c.status = #{filter.status}
</if>
<if test="filter.orderNum != null">
AND c.order_num = #{filter.orderNum}
</if>
<if test="filter.creator != null and filter.creator != ''">
AND c.creator = #{filter.creator}
</if>
</if>
</select>
<update id="incrementViewCount">

View File

@@ -56,13 +56,53 @@
</where>
</sql>
<!-- selectLearningTasks -->
<!-- 权限过滤条件基于dept_path的高效继承 -->
<sql id="Permission_Filter">
INNER JOIN tb_resource_permission rp ON t.task_id = rp.resource_id
AND rp.resource_type = 3
AND rp.deleted = 0
AND rp.can_read = 1
AND (
-- 全局权限:所有用户可访问
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
</foreach>
) user_roles
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
WHERE
-- 部门级权限当前部门或父部门通过dept_path判断继承关系
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
-- 角色级权限:跨部门的角色权限
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
-- 精确权限:特定部门的特定角色
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
</sql>
<!-- selectLearningTasks - 添加权限过滤 -->
<select id="selectLearningTasks" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_learning_task
<include refid="Where_Clause"/>
ORDER BY create_time DESC
SELECT DISTINCT t.*
FROM tb_learning_task t
<include refid="Permission_Filter"/>
WHERE t.deleted = 0
<if test="filter.taskID != null and filter.taskID != ''">
AND t.task_id = #{filter.taskID}
</if>
<if test="filter.name != null and filter.name != ''">
AND t.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.status != null">
AND t.status = #{filter.status}
</if>
ORDER BY t.create_time DESC
</select>
<!-- 根据任务ID查询任务信息 -->
@@ -215,33 +255,78 @@
</delete>
<!-- 分页查询学习任务 -->
<!-- selectLearningTasksPage - 添加权限过滤 -->
<select id="selectLearningTasksPage" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tb_learning_task
<include refid="Filter_Clause" />
ORDER BY create_time DESC
SELECT DISTINCT t.*
FROM tb_learning_task t
<include refid="Permission_Filter"/>
WHERE t.deleted = 0
<if test="filter != null">
<if test="filter.taskID != null and filter.taskID != ''">
AND t.task_id = #{filter.taskID}
</if>
<if test="filter.name != null and filter.name != ''">
AND t.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.status != null">
AND t.status = #{filter.status}
</if>
</if>
ORDER BY t.create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- selectUserLearningTasksPage - 添加权限过滤 -->
<select id="selectUserLearningTasksPage" resultMap="BaseResultMap">
SELECT
SELECT DISTINCT
tlt.id, tlt.task_id, tlt.name, tlt.description, tlt.start_time, tlt.end_time, ttu.status,
tlt.creator, tlt.updater, tlt.create_time, tlt.update_time
FROM tb_task_user ttu
INNER JOIN tb_learning_task tlt ON ttu.task_id = tlt.task_id
INNER JOIN tb_resource_permission rp ON tlt.task_id = rp.resource_id
AND rp.resource_type = 3
AND rp.deleted = 0
AND rp.can_read = 1
AND (
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.roleID} AS role_id
</foreach>
) user_roles
WHERE (rp.dept_id = user_roles.dept_id AND rp.role_id IS NULL)
OR (rp.role_id = user_roles.role_id AND rp.dept_id IS NULL)
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
WHERE ttu.user_id = #{filter.userID}
AND tlt.deleted = 0
AND ttu.deleted = 0
ORDER BY create_time DESC
ORDER BY tlt.create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- 统计学习任务总数 -->
<!-- 统计学习任务总数 - 添加权限过滤 -->
<select id="countLearningTasks" resultType="long">
SELECT COUNT(1)
FROM tb_learning_task
<include refid="Filter_Clause" />
SELECT COUNT(DISTINCT t.id)
FROM tb_learning_task t
<include refid="Permission_Filter"/>
WHERE t.deleted = 0
<if test="filter != null">
<if test="filter.taskID != null and filter.taskID != ''">
AND t.task_id = #{filter.taskID}
</if>
<if test="filter.name != null and filter.name != ''">
AND t.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.status != null">
AND t.status = #{filter.status}
</if>
</if>
</select>
</mapper>

View File

@@ -15,7 +15,7 @@ import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.dept.TbSysDept;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.vo.DeptRoleVO;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -110,14 +110,13 @@ public class DeptController {
}
/**
* @description 查询部门绑定角色
* @param dept 部门信息
* @return ResultDomain<TbSysRole> 角色信息
* @description 查询部门绑定角色列表(包含名称)
* @return ResultDomain<UserDeptRoleVO> 部门角色信息
* @author yslg
* @since 2025-10-06
*/
@PostMapping("/role/list")
public ResultDomain<TbSysDeptRole> getDeptByRoleList() {
public ResultDomain<UserDeptRoleVO> getDeptByRoleList() {
return deptService.getDeptByRoleList();
}
@@ -129,7 +128,7 @@ public class DeptController {
* @since 2025-10-06
*/
@PostMapping("/bind/role")
public ResultDomain<TbSysDeptRole> bindDeptRole(@RequestBody DeptRoleVO deptRole) {
public ResultDomain<TbSysDeptRole> bindDeptRole(@RequestBody UserDeptRoleVO deptRole) {
List<String> deptIDs = deptRole.getDepts().stream().map(TbSysDept::getDeptID).collect(Collectors.toList());
List<String> roleIDs = deptRole.getRoles().stream().map(TbSysRole::getRoleID).collect(Collectors.toList());
return deptService.bindDeptRole(deptIDs, roleIDs);
@@ -143,7 +142,7 @@ public class DeptController {
* @since 2025-10-06
*/
@PostMapping("/unbind/role")
public ResultDomain<TbSysDeptRole> unbindDeptRole(@RequestBody DeptRoleVO deptRole) {
public ResultDomain<TbSysDeptRole> unbindDeptRole(@RequestBody UserDeptRoleVO deptRole) {
List<String> deptIDs = deptRole.getDepts().stream().map(TbSysDept::getDeptID).collect(Collectors.toList());
List<String> roleIDs = deptRole.getRoles().stream().map(TbSysRole::getRoleID).collect(Collectors.toList());
return deptService.unbindDeptRole(deptIDs, roleIDs);

View File

@@ -14,7 +14,7 @@ import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.permission.TbSysPermission;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.dto.role.TbSysRolePermission;
import org.xyzh.common.vo.DeptRoleVO;
import org.xyzh.common.vo.UserDeptRoleVO;
/**
* @description RoleController.java文件描述 角色控制器

View File

@@ -116,7 +116,7 @@ public class UserController {
* @since 2025-10-09
*/
@PostMapping("/bind/deptrole/list")
public ResultDomain<TbSysUserDeptRole> getBindUserDeptRoleList(@RequestBody TbSysUserDeptRole filter) {
public ResultDomain<UserDeptRoleVO> getBindUserDeptRoleList(@RequestBody TbSysUserDeptRole filter) {
return userService.getBindUserDeptRoleList(filter);
}

View File

@@ -4,12 +4,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.enums.ResourceType;
import org.xyzh.common.dto.dept.TbSysDept;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.permission.TbResourcePermission;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.dto.user.TbSysUserDeptRole;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.system.department.service.SysDepartmentService;
import org.xyzh.system.mapper.DepartmentMapper;
@@ -38,6 +44,9 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
@Autowired
private DeptRoleMapper deptRoleMapper;
@Autowired
private ResourcePermissionService resourcePermissionService;
@Override
public ResultDomain<TbSysDept> getAllDepartments() {
ResultDomain<TbSysDept> resultDomain = new ResultDomain<>();
@@ -46,7 +55,9 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
logger.info("开始查询所有部门");
TbSysDept filter = new TbSysDept();
filter.setDeleted(false);
List<TbSysDept> departments = departmentMapper.selectDepts(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbSysDept> departments = departmentMapper.selectDepts(filter, userDeptRoles);
logger.info("查询所有部门完成,共找到{}个部门", departments.size());
resultDomain.success("查询成功", departments);
@@ -65,7 +76,9 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
ResultDomain<TbSysDept> resultDomain = new ResultDomain<>();
try {
logger.info("开始查询部门列表");
List<TbSysDept> departments = departmentMapper.selectDepts(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbSysDept> departments = departmentMapper.selectDepts(filter, userDeptRoles);
if (departments.isEmpty()) {
resultDomain.fail("未找到部门");
return resultDomain;
@@ -80,11 +93,19 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
}
@Override
public ResultDomain<TbSysDeptRole> getDeptByRoleList() {
ResultDomain<TbSysDeptRole> resultDomain = new ResultDomain<>();
List<TbSysDeptRole> deptRoles = deptRoleMapper.selectDeptRoleList();
resultDomain.success("查询成功", deptRoles);
return resultDomain;
public ResultDomain<UserDeptRoleVO> getDeptByRoleList() {
ResultDomain<UserDeptRoleVO> resultDomain = new ResultDomain<>();
try {
logger.info("开始查询部门角色关联列表");
List<UserDeptRoleVO> deptRoles = deptRoleMapper.selectDeptRoleList();
logger.info("查询部门角色关联列表完成,共找到{}条记录", deptRoles.size());
resultDomain.success("查询成功", deptRoles);
return resultDomain;
} catch (Exception e) {
logger.error("查询部门角色关联列表失败", e);
resultDomain.fail("查询部门角色关联列表失败:" + e.getMessage());
return resultDomain;
}
}
@Override
@@ -100,7 +121,9 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
TbSysDept filter = new TbSysDept();
filter.setDeptID(deptId);
List<TbSysDept> departments = departmentMapper.selectDepts(filter);
// 获取当前用户的部门角色
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
List<TbSysDept> departments = departmentMapper.selectDepts(filter, userDeptRoles);
TbSysDept department = departments.isEmpty() ? null : departments.get(0);
if (department == null) {
@@ -140,6 +163,7 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysDept> createDepartment(TbSysDept department) {
ResultDomain<TbSysDept> resultDomain = new ResultDomain<>();
try {
@@ -162,9 +186,17 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
return resultDomain;
}
// 获取当前用户
TbSysUser currentUser = LoginUtil.getCurrentUser();
if (currentUser == null) {
resultDomain.fail("请先登录");
return resultDomain;
}
// 设置基础信息
department.setID(IDUtils.generateID());
department.setDeptID(IDUtils.generateID());
department.setCreator(currentUser.getID());
department.setCreateTime(new Date());
department.setDeleted(false);
@@ -173,6 +205,24 @@ public class SysDepartmentServiceImpl implements SysDepartmentService {
if (result > 0) {
logger.info("创建部门成功:{}", department.getName());
// 创建资源权限
try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
if (userDeptRoles != null && !userDeptRoles.isEmpty()) {
ResultDomain<TbResourcePermission> permissionResult = resourcePermissionService.createResourcePermission(
ResourceType.DEPT.getCode(),
department.getDeptID(),
userDeptRoles.get(0)
);
if (!permissionResult.isSuccess()) {
logger.warn("创建部门权限失败:{}", permissionResult.getMessage());
}
}
} catch (Exception e) {
logger.error("创建部门权限异常", e);
}
resultDomain.success("创建部门成功", department);
return resultDomain;
} else {

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.dept.TbSysDept;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.List;
@@ -20,11 +21,12 @@ public interface DepartmentMapper extends BaseMapper<TbSysDept> {
/**
* @description 查询部门列表
* @param filter 过滤条件
* @param userDeptRoles 用户部门角色列表
* @return List<TbSysDept> 部门列表
* @author yslg
* @since 2025-10-06
*/
List<TbSysDept> selectDepts(TbSysDept filter);
List<TbSysDept> selectDepts(@Param("filter") TbSysDept filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
/**
* @description 根据父部门ID查询子部门列表

View File

@@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.vo.UserDeptRoleVO;
@Mapper
public interface DeptRoleMapper extends BaseMapper<TbSysDeptRole> {
@@ -22,12 +23,12 @@ public interface DeptRoleMapper extends BaseMapper<TbSysDeptRole> {
List<TbSysRole> selectDeptRole(String deptId);
/**
* @description 查询部门绑定角色
* @return List<TbSysDeptRole> 部门角色列表
* @description 查询部门绑定角色列表(包含名称)
* @return List<UserDeptRoleVO> 部门角色列表
* @author yslg
* @since 2025-10-06
*/
List<TbSysDeptRole> selectDeptRoleList();
List<UserDeptRoleVO> selectDeptRoleList();
/**
* @description 批量绑定部门角色
@@ -39,4 +40,8 @@ public interface DeptRoleMapper extends BaseMapper<TbSysDeptRole> {
int batchBindDeptRole(@Param("deptRoles") List<TbSysDeptRole> deptRoles);
int batchUnbindDeptRole(@Param("deptRoles") List<TbSysDeptRole> deptRoles);
List<TbSysDeptRole> selectParentDeptAdmin(UserDeptRoleVO userDeptRole);
List<TbSysDeptRole> selectChildDeptRole(UserDeptRoleVO userDeptRole);
}

View File

@@ -0,0 +1,40 @@
package org.xyzh.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.permission.TbResourcePermission;
import org.xyzh.common.vo.ResourcePermissionVO;
import java.util.List;
/**
* @description 资源权限Mapper接口
* @filename ResourcePermissionMapper.java
* @author yslg
* @copyright xyzh
* @since 2025-10-29
*/
@Mapper
public interface ResourcePermissionMapper extends BaseMapper<TbResourcePermission> {
/**
* @description 插入资源权限
* @param permission 资源权限对象
* @return int 插入结果
* @author yslg
* @since 2025-10-29
*/
int insertResourcePermission(TbResourcePermission permission);
/**
* @description 批量插入资源权限
* @param permissions 资源权限列表
* @return int 插入结果
* @author yslg
* @since 2025-10-29
*/
int batchInsertResourcePermission(@Param("list") List<TbResourcePermission> permissions);
}

View File

@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.vo.DeptRoleVO;
import org.xyzh.common.vo.UserDeptRoleVO;
import java.util.List;
@@ -65,11 +65,11 @@ public interface RoleMapper extends BaseMapper<TbSysRole> {
/**
* @description 根据用户ID查询角色列表
* @param userId 用户ID
* @return List<DeptRoleVO> 部门角色列表
* @return List<UserDeptRoleVO> 部门角色列表
* @author yslg
* @since 2025-09-28
*/
List<DeptRoleVO> selectDeptRolesByUserId(@Param("userId") String userId);
List<UserDeptRoleVO> selectDeptRolesByUserId(@Param("userId") String userId);
/**
* @description 根据角色编码查询角色

View File

@@ -5,19 +5,38 @@ import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.user.TbSysUserDeptRole;
import org.xyzh.common.vo.UserDeptRoleVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@Mapper
public interface UserDeptRoleMapper extends BaseMapper<TbSysUserDeptRole> {
/**
* @description 查询用户部门角色
* @param userId 用户ID
* @return List<TbSysUserDeptRole> 用户部门角色列表
* @description 查询用户部门角色(包含名称)
* @param filter 过滤条件
* @return List<UserDeptRoleVO> 用户部门角色列表
* @author yslg
* @since 2025-10-09
*/
List<TbSysUserDeptRole> selectByFilter(TbSysUserDeptRole filter);
List<UserDeptRoleVO> selectByFilter(TbSysUserDeptRole filter);
/**
* @description 删除指定用户的所有部门角色绑定
* @param userID 用户ID
* @return int 影响行数
* @author yslg
* @since 2025-10-09
*/
int deleteUserDeptRole(String userID);
/**
* @description 批量删除多个用户的部门角色绑定
* @param userIds 用户ID列表
* @return int 影响行数
* @author yslg
* @since 2025-10-29
*/
int deleteUserDeptRoleByUserIds(@Param("userIds") List<String> userIds);
/**
* @description 绑定用户

View File

@@ -0,0 +1,173 @@
package org.xyzh.system.permission.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.dept.TbSysDeptRole;
import org.xyzh.common.dto.permission.TbResourcePermission;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.system.mapper.DeptRoleMapper;
import org.xyzh.system.mapper.ResourcePermissionMapper;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @description 资源权限控制服务实现类
* @filename SysResourcePermissionServiceImpl.java
* @author yslg
* @copyright xyzh
* @since 2025-10-29
*/
@Service
public class SysResourcePermissionServiceImpl implements ResourcePermissionService {
private static final Logger logger = LoggerFactory.getLogger(SysResourcePermissionServiceImpl.class);
@Autowired
private ResourcePermissionMapper resourcePermissionMapper;
@Autowired
private DeptRoleMapper deptRoleMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public ResultDomain<TbResourcePermission> createResourcePermission(Integer resource_type, String resource_id,
UserDeptRoleVO userDeptRole) {
ResultDomain<TbResourcePermission> resultDomain = new ResultDomain<>();
Set<TbResourcePermission> resourcePermissions = new HashSet<>();
Date now = new Date();
// 判断是否为root_department的superadmin
if (isRootSuperAdmin(userDeptRole)) {
// root_department的superadmin创建资源创建全局读权限
logger.info("用户为root_department的superadmin创建全局读权限所有用户包括未来新增的部门/角色)都能访问");
// 1. 创建全局读权限(所有人可读,包括未来新增的部门/角色)
TbResourcePermission globalReadPermission = createGlobalReadPermission(resource_type, resource_id, userDeptRole.getUserID());
globalReadPermission.setCreateTime(now);
resourcePermissions.add(globalReadPermission);
// 2. 为superadmin创建全权限读写执行
TbResourcePermission superAdminPermission = createSuperAdminPermission(resource_type, resource_id, userDeptRole.getUserID());
superAdminPermission.setCreateTime(now);
resourcePermissions.add(superAdminPermission);
} else {
// 普通用户创建资源:为父部门管理员+当前部门创建权限
logger.info("普通用户创建资源,为父部门管理员和当前部门创建权限(子部门通过递归查询自动继承)");
// 1. 为父部门的管理员角色创建精确权限dept + admin role
List<TbSysDeptRole> parentDeptRoles = deptRoleMapper.selectParentDeptAdmin(userDeptRole);
for (TbSysDeptRole deptRole : parentDeptRoles) {
TbResourcePermission temp = createResourcePermission(resource_type, resource_id, deptRole.getDeptID(), deptRole.getRoleID());
temp.setCreateTime(now);
resourcePermissions.add(temp);
}
// 2. 为当前用户所在部门创建部门级权限dept + NULL
// 所有角色都能访问(包括未来新增的角色)
// 所有子部门都能访问(包括未来新增的子部门,通过查询时递归实现)
TbResourcePermission deptPermission = createDeptLevelPermission(resource_type, resource_id, userDeptRole.getDeptID());
deptPermission.setCreateTime(now);
resourcePermissions.add(deptPermission);
// 3. 为superadmin创建全权限确保超级管理员始终可以管理所有资源
TbResourcePermission superAdminPermission = createSuperAdminPermission(resource_type, resource_id, userDeptRole.getUserID());
superAdminPermission.setCreateTime(now);
resourcePermissions.add(superAdminPermission);
}
List<TbResourcePermission> resourcePermissionsList = new ArrayList<>(resourcePermissions);
int result = resourcePermissionMapper.batchInsertResourcePermission(resourcePermissionsList);
if (result > 0) {
resultDomain.success("创建资源权限成功", resourcePermissionsList);
return resultDomain;
} else {
resultDomain.fail("创建资源权限失败");
return resultDomain;
}
}
/**
* 判断是否为root_department的superadmin
*/
private boolean isRootSuperAdmin(UserDeptRoleVO userDeptRole) {
return "root_department".equals(userDeptRole.getDeptID())
&& "superadmin".equals(userDeptRole.getRoleID());
}
/**
* 创建全局读权限(所有人可读,包括未来新增的部门/角色)
*/
private TbResourcePermission createGlobalReadPermission(Integer resource_type, String resource_id, String creatorID) {
TbResourcePermission resourcePermission = new TbResourcePermission();
resourcePermission.setResourceType(resource_type);
resourcePermission.setResourceID(resource_id);
resourcePermission.setCreator(creatorID);
resourcePermission.setDeptID(null); // NULL表示不限制部门
resourcePermission.setRoleID(null); // NULL表示不限制角色
resourcePermission.setCanRead(true);
resourcePermission.setCanWrite(false);
resourcePermission.setCanExecute(false);
return resourcePermission;
}
/**
* 创建精确部门角色权限dept + role读权限
*/
private TbResourcePermission createResourcePermission(Integer resource_type, String resource_id, String deptID, String roleID) {
TbResourcePermission resourcePermission = new TbResourcePermission();
resourcePermission.setResourceType(resource_type);
resourcePermission.setResourceID(resource_id);
resourcePermission.setDeptID(deptID);
resourcePermission.setRoleID(roleID);
resourcePermission.setCanRead(true);
resourcePermission.setCanWrite(false);
resourcePermission.setCanExecute(false);
return resourcePermission;
}
/**
* 创建部门级权限dept + NULL该部门所有角色可读包括未来新增的角色
*/
private TbResourcePermission createDeptLevelPermission(Integer resource_type, String resource_id, String deptID) {
TbResourcePermission resourcePermission = new TbResourcePermission();
resourcePermission.setResourceType(resource_type);
resourcePermission.setResourceID(resource_id);
resourcePermission.setDeptID(deptID);
resourcePermission.setRoleID(null); // NULL表示该部门所有角色都能访问
resourcePermission.setCanRead(true);
resourcePermission.setCanWrite(false);
resourcePermission.setCanExecute(false);
return resourcePermission;
}
/**
* 创建超级管理员全权限(读写执行)
*/
private TbResourcePermission createSuperAdminPermission(Integer resource_type, String resource_id, String creatorID) {
TbResourcePermission resourcePermission = new TbResourcePermission();
resourcePermission.setResourceType(resource_type);
resourcePermission.setResourceID(resource_id);
resourcePermission.setCreator(creatorID);
resourcePermission.setDeptID("root_department");
resourcePermission.setRoleID("superadmin");
resourcePermission.setCanRead(true);
resourcePermission.setCanWrite(true);
resourcePermission.setCanExecute(true);
return resourcePermission;
}
}

View File

@@ -9,7 +9,7 @@ import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.permission.TbSysPermission;
import org.xyzh.common.dto.role.TbSysRole;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.vo.DeptRoleVO;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.system.mapper.RolePermissionMapper;
import org.xyzh.system.mapper.RoleMapper;
import org.xyzh.system.mapper.UserDeptRoleMapper;
@@ -257,8 +257,8 @@ public class SysRoleServiceImpl implements SysRoleService {
}
@Override
public ResultDomain<DeptRoleVO> getDeptRolesByUserId(String userId) {
ResultDomain<DeptRoleVO> resultDomain = new ResultDomain<>();
public ResultDomain<UserDeptRoleVO> getDeptRolesByUserId(String userId) {
ResultDomain<UserDeptRoleVO> resultDomain = new ResultDomain<>();
try {
logger.info("开始根据用户ID查询部门角色列表{}", userId);
@@ -267,7 +267,7 @@ public class SysRoleServiceImpl implements SysRoleService {
return resultDomain;
}
List<DeptRoleVO> roles = roleMapper.selectDeptRolesByUserId(userId);
List<UserDeptRoleVO> roles = roleMapper.selectDeptRolesByUserId(userId);
logger.info("根据用户ID查询部门角色列表完成共找到{}个部门角色", roles.size());
resultDomain.success("查询成功", roles);

View File

@@ -3,6 +3,7 @@ package org.xyzh.system.user.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -46,6 +47,8 @@ public class SysUserServiceImpl implements SysUserService {
@Autowired
private UserDeptRoleMapper userDeptRoleMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Transactional
@Override
@@ -72,10 +75,10 @@ public class SysUserServiceImpl implements SysUserService {
TbSysUserDeptRole userDeptRole = new TbSysUserDeptRole();
userDeptRole.setUserID(user.getID());
userDeptRole.setDeptID("-1");
userDeptRole.setDeptID("default_department");
userDeptRole.setRoleID("freedom");
userDeptRole.setCreateTime(now);
user.setPassword(passwordEncoder.encode(user.getPassword()));
userMapper.insertUser(user);
userInfoMapper.insertUserInfo(userInfo);
userDeptRoleMapper.bindUser(Arrays.asList(userDeptRole));
@@ -201,13 +204,13 @@ public class SysUserServiceImpl implements SysUserService {
// 检查至少有一个查询条件
// boolean hasFilter = StringUtils.hasText(filter.getID()) ||
// StringUtils.hasText(filter.getUsername()) ||
// StringUtils.hasText(filter.getEmail()) ||
// StringUtils.hasText(filter.getPhone());
// StringUtils.hasText(filter.getUsername()) ||
// StringUtils.hasText(filter.getEmail()) ||
// StringUtils.hasText(filter.getPhone());
// if (!hasFilter) {
// resultDomain.fail("至少需要提供一个查询条件");
// return resultDomain;
// resultDomain.fail("至少需要提供一个查询条件");
// return resultDomain;
// }
List<TbSysUser> users = userMapper.selectByFilter(filter);
@@ -284,7 +287,6 @@ public class SysUserServiceImpl implements SysUserService {
if (user.getStatus() == null) {
user.setStatus(1); // 默认启用状态
}
// 插入数据库
ResultDomain<TbSysUser> result = registerUser(user);
@@ -308,150 +310,159 @@ public class SysUserServiceImpl implements SysUserService {
@Override
public ResultDomain<TbSysUser> updateUser(TbSysUser user) {
ResultDomain<TbSysUser> resultDomain = new ResultDomain<>();
try {
logger.info("开始更新用户:{}", user.getID());
// 参数校验
if (!StringUtils.hasText(user.getID())) {
resultDomain.fail("用户ID不能为空");
return resultDomain;
}
if (!StringUtils.hasText(user.getUsername())) {
resultDomain.fail("用户名不能为空");
return resultDomain;
}
logger.info("开始更新用户:{}", user.getID());
// 检查用户是否存在
ResultDomain<TbSysUser> existResult = getUserById(user.getID());
if (!existResult.isSuccess()) {
resultDomain.fail(existResult.getMessage());
return resultDomain;
}
// 检查用户名是否已存在(排除自身)
ResultDomain<Boolean> checkResult = checkUsernameExists(user.getUsername(), user.getID());
if (!checkResult.isSuccess()) {
resultDomain.fail(checkResult.getMessage());
return resultDomain;
}
if (checkResult.getData()) {
resultDomain.fail("用户名已存在");
return resultDomain;
}
// 检查邮箱是否已存在(排除自身)
if (StringUtils.hasText(user.getEmail())) {
ResultDomain<Boolean> emailCheckResult = checkEmailExists(user.getEmail(), user.getID());
if (!emailCheckResult.isSuccess()) {
resultDomain.fail(emailCheckResult.getMessage());
return resultDomain;
}
if (emailCheckResult.getData()) {
resultDomain.fail("邮箱已存在");
return resultDomain;
}
}
// 设置更新时间
user.setUpdateTime(new Date());
// 更新数据库
int result = userMapper.updateUser(user);
if (result > 0) {
logger.info("更新用户成功:{}", user.getID());
resultDomain.success("更新用户成功", user);
return resultDomain;
} else {
logger.warn("更新用户失败:{}", user.getID());
resultDomain.fail("更新用户失败");
return resultDomain;
}
} catch (Exception e) {
logger.error("更新用户异常:{}", user.getID(), e);
resultDomain.fail("更新用户失败:" + e.getMessage());
// 参数校验
if (!StringUtils.hasText(user.getID())) {
resultDomain.fail("用户ID不能为空");
return resultDomain;
}
// 检查用户是否存在
ResultDomain<TbSysUser> existResult = getUserById(user.getID());
if (!existResult.isSuccess()) {
resultDomain.fail(existResult.getMessage());
return resultDomain;
}
// 检查邮箱是否已存在(排除自身)
if (StringUtils.hasText(user.getEmail())) {
ResultDomain<Boolean> emailCheckResult = checkEmailExists(user.getEmail(), user.getID());
if (!emailCheckResult.isSuccess()) {
resultDomain.fail(emailCheckResult.getMessage());
return resultDomain;
}
if (emailCheckResult.getData()) {
resultDomain.fail("邮箱已存在");
return resultDomain;
}
}
// 设置更新时间
user.setUpdateTime(new Date());
// 更新数据库
int result = userMapper.updateUser(user);
if (result > 0) {
logger.info("更新用户成功:{}", user.getID());
resultDomain.success("更新用户成功", user);
return resultDomain;
} else {
logger.warn("更新用户失败:{}", user.getID());
resultDomain.fail("更新用户失败");
return resultDomain;
}
}
@Override
public ResultDomain<TbSysUser> deleteUser(String userId) {
ResultDomain<TbSysUser> resultDomain = new ResultDomain<>();
logger.info("开始删除用户:{}", userId);
if (!StringUtils.hasText(userId)) {
resultDomain.fail("用户ID不能为空");
return resultDomain;
}
// 检查用户是否存在
ResultDomain<TbSysUser> existResult = getUserById(userId);
if (!existResult.isSuccess()) {
resultDomain.fail(existResult.getMessage());
return resultDomain;
}
// 逻辑删除
// TbSysUser user = existResult.getData();
// user.setDeleted(true);
// user.setDeleteTime(new Date());
// int result = userMapper.updateUser(user);
int result = userMapper.deleteUser(userId);
if (result > 0) {
logger.info("删除用户成功:{}", userId);
resultDomain.success("删除用户成功", new TbSysUser());
return resultDomain;
} else {
logger.warn("删除用户失败:{}", userId);
resultDomain.fail("删除用户失败");
return resultDomain;
}
}
@Override
public ResultDomain<UserDeptRoleVO> getBindUserDeptRoleList(TbSysUserDeptRole filter) {
ResultDomain<UserDeptRoleVO> resultDomain = new ResultDomain<>();
try {
logger.info("开始删除用户:{}", userId);
if (!StringUtils.hasText(userId)) {
resultDomain.fail("用户ID不能为空");
return resultDomain;
}
// 检查用户是否存在
ResultDomain<TbSysUser> existResult = getUserById(userId);
if (!existResult.isSuccess()) {
resultDomain.fail(existResult.getMessage());
return resultDomain;
}
// 逻辑删除
TbSysUser user = existResult.getData();
user.setDeleted(true);
user.setDeleteTime(new Date());
int result = userMapper.updateUser(user);
if (result > 0) {
logger.info("删除用户成功:{}", userId);
resultDomain.success("删除用户成功", user);
return resultDomain;
} else {
logger.warn("删除用户失败:{}", userId);
resultDomain.fail("删除用户失败");
return resultDomain;
}
logger.info("开始查询用户部门角色绑定列表");
List<UserDeptRoleVO> userDeptRoles = userDeptRoleMapper.selectByFilter(filter);
logger.info("查询用户部门角色绑定列表完成,共找到{}条记录", userDeptRoles.size());
resultDomain.success("查询成功", userDeptRoles);
return resultDomain;
} catch (Exception e) {
logger.error("删除用户异常:{}", userId, e);
resultDomain.fail("删除用户失败:" + e.getMessage());
logger.error("查询用户部门角色绑定列表失败", e);
resultDomain.fail("查询用户部门角色绑定列表失败:" + e.getMessage());
return resultDomain;
}
}
@Override
public ResultDomain<TbSysUserDeptRole> getBindUserDeptRoleList(TbSysUserDeptRole filter) {
ResultDomain<TbSysUserDeptRole> resultDomain = new ResultDomain<>();
List<TbSysUserDeptRole> userDeptRoles = userDeptRoleMapper.selectByFilter(filter);
resultDomain.success("查询成功", userDeptRoles);
return resultDomain;
}
@Transactional
@Transactional(rollbackFor = Exception.class)
@Override
public ResultDomain<UserDeptRoleVO> bindUserDeptRole(UserDeptRoleVO userDeptRoleVO) {
ResultDomain<UserDeptRoleVO> resultDomain = new ResultDomain<>();
TbSysUser currentUser = LoginUtil.getCurrentUser();
try {
// 收集所有用户ID
List<String> userIds = new ArrayList<>();
for (TbSysUser user : userDeptRoleVO.getUsers()) {
userIds.add(user.getID());
}
logger.info("准备为 {} 个用户绑定部门角色", userIds.size());
// 批量删除所有涉及用户的旧绑定关系(物理删除,包括软删除的记录)
int deleteCount = userDeptRoleMapper.deleteUserDeptRoleByUserIds(userIds);
if (deleteCount <= 0) {
resultDomain.fail("批量删除旧绑定记录失败:没有记录被删除");
return resultDomain;
}
// 准备新的绑定数据
List<TbSysUserDeptRole> userDeptRoles = new ArrayList<>();
logger.info("开始绑定用户部门角色:{}", userDeptRoleVO.getID());
Date now = new Date();
for (TbSysUser user : userDeptRoleVO.getUsers()) {
for(TbSysUserDeptRole userDeptRole : userDeptRoleVO.getUserDeptRoles()) {
userDeptRole.setID(IDUtils.generateID());
userDeptRole.setUserID(user.getID());
userDeptRole.setCreateTime(now);
userDeptRole.setCreator(currentUser.getID());
userDeptRoles.add(userDeptRole);
for (TbSysUserDeptRole userDeptRole : userDeptRoleVO.getUserDeptRoles()) {
TbSysUserDeptRole newUserDeptRole = new TbSysUserDeptRole();
newUserDeptRole.setID(IDUtils.generateID());
newUserDeptRole.setUserID(user.getID());
newUserDeptRole.setDeptID(userDeptRole.getDeptID());
newUserDeptRole.setRoleID(userDeptRole.getRoleID());
newUserDeptRole.setCreateTime(now);
newUserDeptRole.setCreator(currentUser.getID());
userDeptRoles.add(newUserDeptRole);
}
}
userDeptRoleMapper.bindUser(userDeptRoles);
logger.info("准备插入 {} 条新绑定记录", userDeptRoles.size());
// 插入新的绑定关系
int result = userDeptRoleMapper.bindUser(userDeptRoles);
logger.info("成功插入 {} 条绑定记录", result);
if (result > 0) {
resultDomain.success("绑定用户部门角色成功", userDeptRoleVO);
} else {
resultDomain.fail("绑定用户部门角色失败:没有记录被插入");
}
} catch (Exception e) {
logger.error("绑定用户部门角色异常:{}", userDeptRoleVO.getID(), e);
resultDomain.fail("绑定用户部门角色失败:" + e.getMessage());
return resultDomain;
}
return resultDomain;
}
@Transactional
@@ -459,30 +470,26 @@ public class SysUserServiceImpl implements SysUserService {
public ResultDomain<UserDeptRoleVO> unbindUserDeptRole(UserDeptRoleVO userDeptRoleVO) {
ResultDomain<UserDeptRoleVO> resultDomain = new ResultDomain<>();
TbSysUser currentUser = LoginUtil.getCurrentUser();
try {
List<TbSysUserDeptRole> userDeptRoles = new ArrayList<>();
logger.info("开始解绑用户部门角色:{}", userDeptRoleVO.getID());
Date now = new Date();
for(TbSysUser user:userDeptRoleVO.getUsers()) {
for(TbSysUserDeptRole userDeptRole : userDeptRoleVO.getUserDeptRoles()) {
userDeptRole.setUserID(user.getID());
userDeptRoles.add(userDeptRole);
}
List<TbSysUserDeptRole> userDeptRoles = new ArrayList<>();
logger.info("开始解绑用户部门角色:{}", userDeptRoleVO.getDeptID());
Date now = new Date();
for (TbSysUser user : userDeptRoleVO.getUsers()) {
for (TbSysUserDeptRole userDeptRole : userDeptRoleVO.getUserDeptRoles()) {
userDeptRole.setUserID(user.getID());
userDeptRoles.add(userDeptRole);
}
int result = userDeptRoleMapper.unbindUser(userDeptRoles);
if(result > 0) {
logger.info("解绑用户部门角色成功:{}", userDeptRoleVO.getID());
resultDomain.success("解绑用户部门角色成功", userDeptRoleVO);
} else {
logger.warn("解绑用户部门角色失败:{}", userDeptRoleVO.getID());
resultDomain.fail("解绑用户部门角色失败");
}
return resultDomain;
} catch (Exception e) {
logger.error("解绑用户部门角色异常:{}", userDeptRoleVO.getID(), e);
resultDomain.fail("解绑用户部门角色失败:" + e.getMessage());
}
int result = userDeptRoleMapper.unbindUser(userDeptRoles);
if (result > 0) {
logger.info("解绑用户部门角色成功{}", userDeptRoleVO.getDeptID());
resultDomain.success("解绑用户部门角色成功", userDeptRoleVO);
} else {
logger.warn("解绑用户部门角色失败{}", userDeptRoleVO.getDeptID());
resultDomain.fail("解绑用户部门角色失败");
}
return resultDomain;
}
@Override
@@ -662,7 +669,6 @@ public class SysUserServiceImpl implements SysUserService {
}
}
// ----------------用户信息相关--------------------------------
@Override
@@ -726,7 +732,6 @@ public class SysUserServiceImpl implements SysUserService {
}
}
@Override
public ResultDomain<UserVO> getUserInfoTotal(String userId) {
ResultDomain<UserVO> resultDomain = new ResultDomain<>();

View File

@@ -1,5 +1,8 @@
package org.xyzh.system.utils;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.xyzh.common.core.domain.LoginDomain;
@@ -9,6 +12,7 @@ import org.xyzh.common.dto.user.TbSysUserInfo;
import org.xyzh.common.redis.service.RedisService;
import org.xyzh.common.utils.NonUtils;
import org.xyzh.common.utils.ServletUtil;
import org.xyzh.common.vo.UserDeptRoleVO;
/**
* @description LoginUtil.java文件描述 登录信息工具类
@@ -72,6 +76,26 @@ public class LoginUtil {
}
}
/**
* 获取当前用户的部门角色列表(扁平化)
* UserDeptRoleVO已包含deptPath字段用于高效的权限继承判断
* @return 部门角色列表
*/
public static List<UserDeptRoleVO> getCurrentDeptRole() {
LoginDomain loginDomain = getCurrentLoginDomain();
List<UserDeptRoleVO> roles = loginDomain.getRoles();
// UserDeptRoleVO应该在登录时就已经扁平化填充好了
// 这里只需要确保userID被设置如果还没设置的话
String userId = loginDomain.getUser().getID();
return roles.stream().map(item -> {
if (item.getUserID() == null) {
item.setUserID(userId);
}
return item;
}).collect(Collectors.toList());
}
/**
* @description 获取当前登录用户
* @return TbSysUser 当前登录用户未登录返回null

View File

@@ -8,6 +8,7 @@
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="parent_id" property="parentID" jdbcType="VARCHAR"/>
<result column="dept_path" property="deptPath" jdbcType="VARCHAR"/>
<result column="description" property="description" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
@@ -19,7 +20,7 @@
<!-- 基础字段 -->
<sql id="Base_Column_List">
id, dept_id, name, parent_id, description, creator, updater,
id, dept_id, name, parent_id, dept_path, description, creator, updater,
create_time, update_time, delete_time, deleted
</sql>
@@ -39,14 +40,54 @@
</where>
</sql>
<!-- selectDepts -->
<!-- 权限过滤条件基于dept_path的高效继承 -->
<sql id="Permission_Filter">
INNER JOIN tb_resource_permission rp ON d.dept_id = rp.resource_id
AND rp.resource_type = 4
AND rp.deleted = 0
AND rp.can_read = 1
AND (
-- 全局权限:所有用户可访问
(rp.dept_id IS NULL AND rp.role_id IS NULL)
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
OR EXISTS (
SELECT 1
FROM (
<foreach collection="userDeptRoles" item="udr" separator=" UNION ALL ">
SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id
</foreach>
) user_roles
LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0
WHERE
-- 部门级权限当前部门或父部门通过dept_path判断继承关系
(rp.role_id IS NULL AND rp.dept_id IS NOT NULL
AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%'))
-- 角色级权限:跨部门的角色权限
OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id)
-- 精确权限:特定部门的特定角色
OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id)
)
</if>
)
</sql>
<!-- selectDepts - 添加权限过滤 -->
<select id="selectDepts">
SELECT
<include refid="Base_Column_List"/>
FROM tb_sys_dept
<include refid="Where_Clause"/>
ORDER BY dept_id,create_time DESC
SELECT DISTINCT d.*
FROM tb_sys_dept d
<include refid="Permission_Filter"/>
WHERE d.deleted = 0
<if test="filter.deptID != null and filter.deptID != ''">
AND d.dept_id = #{filter.deptID}
</if>
<if test="filter.parentID != null and filter.parentID != ''">
AND d.parent_id = #{filter.parentID}
</if>
<if test="filter.name != null and filter.name != ''">
AND d.name LIKE CONCAT('%', #{filter.name}, '%')
</if>
ORDER BY d.dept_id, d.create_time DESC
</select>
<!-- 根据父部门ID查询子部门列表 -->
@@ -95,64 +136,63 @@
create_time ASC
</select>
<!-- 批量删除部门(逻辑删除) -->
<update id="batchDeleteByIds">
UPDATE tb_sys_dept
SET deleted = 1,
delete_time = NOW(),
updater = #{updater}
WHERE deleted = 0
AND dept_id IN
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId}
</foreach>
</update>
<!-- 插入部门 -->
<insert id="insert" parameterType="org.xyzh.common.dto.dept.TbSysDept">
INSERT INTO tb_sys_dept
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="deptID != null">dept_id,</if>
<if test="parentID != null">parent_id,</if>
<if test="name != null">name,</if>
<if test="description != null">description,</if>
<if test="creator != null">creator,</if>
<if test="createTime != null">create_time,</if>
<!-- insertDept -->
<insert id="insertDept" parameterType="org.xyzh.common.dto.dept.TbSysDept">
INSERT INTO tb_sys_dept (
id,
dept_id,
name,
parent_id,
dept_path,
description,
creator,
create_time,
deleted
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="deptID != null">#{deptID},</if>
<if test="parentID != null">#{parentID},</if>
<if test="name != null">#{name},</if>
<if test="description != null">#{description},</if>
<if test="creator != null">#{creator},</if>
<if test="createTime != null">#{createTime},</if>
0
</trim>
) VALUES (
#{id},
#{deptID},
#{name},
#{parentID},
#{deptPath},
#{description},
#{creator},
#{createTime},
#{deleted}
)
</insert>
<!-- 更新部门 -->
<update id="updateById" parameterType="org.xyzh.common.dto.dept.TbSysDept">
<!-- updateDept -->
<update id="updateDept" parameterType="org.xyzh.common.dto.dept.TbSysDept">
UPDATE tb_sys_dept
<set>
<if test="deptID != null">dept_id = #{deptID},</if>
<if test="parentID != null">parent_id = #{parentID},</if>
<if test="name != null">name = #{name},</if>
<if test="description != null">description = #{description},</if>
<if test="updater != null">updater = #{updater},</if>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="parentID != null">
parent_id = #{parentID},
</if>
<if test="deptPath != null">
dept_path = #{deptPath},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="updater != null">
updater = #{updater},
</if>
update_time = NOW()
</set>
WHERE dept_id = #{deptID} AND deleted = 0
</update>
<!-- 根据ID删除逻辑删除 -->
<update id="deleteById">
<!-- deleteDept - 逻辑删除 -->
<update id="deleteDept" parameterType="org.xyzh.common.dto.dept.TbSysDept">
UPDATE tb_sys_dept
SET deleted = 1,
delete_time = NOW()
<if test="updater != null">
, updater = #{updater}
</if>
WHERE dept_id = #{deptID} AND deleted = 0
</update>
</mapper>

View File

@@ -39,11 +39,29 @@
ORDER BY create_time DESC
</select>
<select id="selectDeptRoleList">
<!-- 部门角色VO结果映射 -->
<resultMap id="DeptRoleVOResultMap" type="org.xyzh.common.vo.UserDeptRoleVO">
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="dept_name" property="deptName" jdbcType="VARCHAR"/>
<result column="dept_description" property="deptDescription" jdbcType="VARCHAR"/>
<result column="role_id" property="roleID" jdbcType="VARCHAR"/>
<result column="role_name" property="roleName" jdbcType="VARCHAR"/>
<result column="role_description" property="roleDescription" jdbcType="VARCHAR"/>
</resultMap>
<select id="selectDeptRoleList" resultMap="DeptRoleVOResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_sys_dept_role
ORDER BY dept_id, role_id, create_time DESC
dr.dept_id,
d.name AS dept_name,
d.description AS dept_description,
dr.role_id,
r.name AS role_name,
r.description AS role_description
FROM tb_sys_dept_role dr
LEFT JOIN tb_sys_dept d ON dr.dept_id = d.dept_id AND d.deleted = 0
LEFT JOIN tb_sys_role r ON dr.role_id = r.role_id AND r.deleted = 0
WHERE dr.deleted = 0
ORDER BY dr.dept_id, dr.role_id, dr.create_time DESC
</select>
<!-- batchBindDeptRole -->
@@ -65,4 +83,81 @@
(#{deptRole.deptID}, #{deptRole.roleID})
</foreach>
</delete>
<!-- selectParentDeptAdmin -->
<select id="selectParentDeptAdmin">
WITH RECURSIVE dept_hierarchy AS (
-- 基础查询:查询起始部门
SELECT
dept_id,
parent_id,
name,
description,
1 AS level
FROM tb_sys_dept
WHERE dept_id = #{deptID}
AND deleted = 0
UNION ALL
-- 递归查询:查询父级部门
SELECT
d.dept_id,
d.parent_id,
d.name,
d.description,
dh.level + 1 AS level
FROM tb_sys_dept d
INNER JOIN dept_hierarchy dh ON d.dept_id = dh.parent_id
WHERE d.deleted = 0
AND d.parent_id IS NOT NULL
)
SELECT
dh.dept_id AS deptID,
tsdr.role_id
FROM dept_hierarchy dh
INNER JOIN tb_sys_dept_role tsdr ON dh.dept_id = tsdr.dept_id
WHERE tsdr.role_id = 'admin'
AND tsdr.deleted = 0
ORDER BY level DESC
</select>
<!-- selectChildDeptRole -->
<select id="selectChildDeptRole">
WITH RECURSIVE dept_hierarchy AS (
-- 基础查询:查询起始部门
SELECT
dept_id,
parent_id,
name,
description,
1 AS level
FROM tb_sys_dept
WHERE dept_id = #{deptID}
AND deleted = 0
UNION ALL
-- 递归查询:查询子级部门
SELECT
d.dept_id,
d.parent_id,
d.name,
d.description,
dh.level + 1 AS level
FROM tb_sys_dept d
INNER JOIN dept_hierarchy dh ON d.parent_id = dh.dept_id
WHERE d.deleted = 0
AND d.parent_id IS NOT NULL
)
SELECT
dh.dept_id AS deptID,
tsdr.role_id
FROM dept_hierarchy dh
INNER JOIN tb_sys_dept_role tsdr ON dh.dept_id = tsdr.dept_id
AND tsdr.deleted = 0
ORDER BY level DESC
</select>
</mapper>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.system.mapper.ResourcePermissionMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.permission.TbResourcePermission">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="resource_type" property="resourceType" jdbcType="INTEGER"/>
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="role_id" property="roleID" jdbcType="VARCHAR"/>
<result column="can_read" property="canRead" jdbcType="TINYINT"/>
<result column="can_write" property="canWrite" jdbcType="TINYINT"/>
<result column="can_execute" property="canExecute" jdbcType="TINYINT"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="TINYINT"/>
</resultMap>
<!-- 视图对象映射 -->
<resultMap id="ResourcePermissionVO" type="org.xyzh.common.vo.ResourcePermissionVO">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="resource_type" property="resourceType" jdbcType="INTEGER"/>
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="role_id" property="roleID" jdbcType="VARCHAR"/>
<result column="can_read" property="canRead" jdbcType="TINYINT"/>
<result column="can_write" property="canWrite" jdbcType="TINYINT"/>
<result column="can_execute" property="canExecute" jdbcType="TINYINT"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="TINYINT"/>
<result column="dept_name" property="deptName" jdbcType="VARCHAR"/>
<result column="role_name" property="roleName" jdbcType="VARCHAR"/>
<result column="resource_title" property="resourceTitle" jdbcType="VARCHAR"/>
</resultMap>
<!-- 基础字段 -->
<sql id="Base_Column_List">
id, resource_type, resource_id, dept_id, role_id,
can_read, can_write, can_execute,
creator, updater, create_time, update_time, delete_time, deleted
</sql>
<!-- 插入资源权限 -->
<insert id="insertResourcePermission" parameterType="org.xyzh.common.dto.permission.TbResourcePermission">
INSERT INTO tb_resource_permission (
id, resource_type, resource_id, dept_id, role_id,
can_read, can_write, can_execute,
creator, create_time, deleted
) VALUES (
#{id}, #{resourceType}, #{resourceID}, #{deptID}, #{roleID},
#{canRead}, #{canWrite}, #{canExecute},
#{creator}, #{createTime}, #{deleted}
)
</insert>
<!-- 批量插入资源权限 -->
<insert id="batchInsertResourcePermission" parameterType="java.util.List">
INSERT INTO tb_resource_permission (
id, resource_type, resource_id, dept_id, role_id,
can_read, can_write, can_execute,
creator, create_time, deleted
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.id}, #{item.resourceType}, #{item.resourceID}, #{item.deptID}, #{item.roleID},
#{item.canRead}, #{item.canWrite}, #{item.canExecute},
#{item.creator}, #{item.createTime}, #{item.deleted}
)
</foreach>
</insert>
</mapper>

View File

@@ -17,42 +17,22 @@
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<resultMap id="deptRoleVOResultMap" type="org.xyzh.common.vo.DeptRoleVO">
<result column="dept_id" property="dept.deptID" jdbcType="VARCHAR"/>
<result column="dept_name" property="dept.name" jdbcType="VARCHAR"/>
<result column="dept_description" property="dept.description" jdbcType="VARCHAR"/>
<result column="dept_creator" property="dept.creator" jdbcType="VARCHAR"/>
<result column="dept_updater" property="dept.updater" jdbcType="VARCHAR"/>
<result column="dept_create_time" property="dept.createTime" jdbcType="TIMESTAMP"/>
<result column="dept_update_time" property="dept.updateTime" jdbcType="TIMESTAMP"/>
<result column="dept_delete_time" property="dept.deleteTime" jdbcType="TIMESTAMP"/>
<result column="dept_deleted" property="dept.deleted" jdbcType="BOOLEAN"/>
<result column="role_id" property="role.roleID" jdbcType="VARCHAR"/>
<result column="role_name" property="role.name" jdbcType="VARCHAR"/>
<result column="role_description" property="role.description" jdbcType="VARCHAR"/>
<result column="role_creator" property="role.creator" jdbcType="VARCHAR"/>
<result column="role_updater" property="role.updater" jdbcType="VARCHAR"/>
<result column="role_create_time" property="role.createTime" jdbcType="TIMESTAMP"/>
<result column="role_update_time" property="role.updateTime" jdbcType="TIMESTAMP"/>
<result column="role_delete_time" property="role.deleteTime" jdbcType="TIMESTAMP"/>
<result column="role_deleted" property="role.deleted" jdbcType="BOOLEAN"/>
<resultMap id="deptRoleVOResultMap" type="org.xyzh.common.vo.UserDeptRoleVO">
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="dept_name" property="deptName" jdbcType="VARCHAR"/>
<result column="dept_description" property="deptDescription" jdbcType="VARCHAR"/>
<result column="dept_path" property="deptPath" jdbcType="VARCHAR"/>
<result column="role_id" property="roleID" jdbcType="VARCHAR"/>
<result column="role_name" property="roleName" jdbcType="VARCHAR"/>
<result column="role_description" property="roleDescription" jdbcType="VARCHAR"/>
</resultMap>
<!-- 基础字段 -->
<sql id="TbSysRole_Column_List">
id, role_id, name, description, creator, updater,
create_time, update_time, delete_time, deleted
</sql>
<sql id="TbSysDeptRole_Column_List">
dr.id, dr.dept_id, dr.role_id,
r.name as role_name, d.name as dept_name,
r.description as role_description, d.description as dept_description,
r.creator as role_creator, d.creator as dept_creator,
r.updater as role_updater, d.updater as dept_updater,
r.create_time as role_create_time, d.create_time as dept_create_time,
r.update_time as role_update_time, d.update_time as dept_update_time,
r.delete_time as role_delete_time, d.delete_time as dept_delete_time,
r.deleted as role_deleted, d.deleted as dept_deleted
</sql>
<!-- 通用条件 -->
<sql id="Where_Clause">
@@ -110,10 +90,19 @@
<!-- 根据用户ID查询角色列表 -->
<select id="selectDeptRolesByUserId" resultMap="deptRoleVOResultMap">
SELECT
<include refid="TbSysDeptRole_Column_List"/>
dr.user_id,
u.username,
dr.dept_id,
d.name AS dept_name,
d.description AS dept_description,
d.dept_path,
dr.role_id,
r.name AS role_name,
r.description AS role_description
FROM tb_sys_user_dept_role dr
INNER JOIN tb_sys_role r ON r.role_id = dr.role_id
INNER JOIN tb_sys_dept d ON d.dept_id = dr.dept_id
LEFT JOIN tb_sys_user u ON dr.user_id = u.id AND u.deleted = 0
LEFT JOIN tb_sys_role r ON dr.role_id = r.role_id AND r.deleted = 0
LEFT JOIN tb_sys_dept d ON dr.dept_id = d.dept_id AND d.deleted = 0
WHERE dr.deleted = 0
AND dr.user_id = #{userId}
ORDER BY dr.create_time ASC

View File

@@ -44,14 +44,47 @@
</where>
</sql>
<!-- 用户部门角色VO结果映射 -->
<resultMap id="UserDeptRoleVOResultMap" type="org.xyzh.common.vo.UserDeptRoleVO">
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="dept_name" property="deptName" jdbcType="VARCHAR"/>
<result column="dept_description" property="deptDescription" jdbcType="VARCHAR"/>
<result column="role_id" property="roleID" jdbcType="VARCHAR"/>
<result column="role_name" property="roleName" jdbcType="VARCHAR"/>
<result column="role_description" property="roleDescription" jdbcType="VARCHAR"/>
</resultMap>
<!-- selectByFilter -->
<select id="selectByFilter">
<select id="selectByFilter" resultMap="UserDeptRoleVOResultMap">
SELECT
<include refid="UserDeptRole_Column_List"/>
FROM tb_sys_user_dept_role
<include refid="Where_Clause"/>
ORDER BY user_id, dept_id, role_id, create_time DESC
udr.user_id,
u.username AS username,
udr.dept_id,
d.name AS dept_name,
d.description AS dept_description,
udr.role_id,
r.name AS role_name,
r.description AS role_description
FROM tb_sys_user_dept_role udr
LEFT JOIN tb_sys_user u ON udr.user_id = u.id AND u.deleted = 0
LEFT JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
LEFT JOIN tb_sys_role r ON udr.role_id = r.role_id AND r.deleted = 0
<where>
udr.deleted = 0
<if test="userID != null">
AND udr.user_id = #{userID}
</if>
<if test="deptID != null">
AND udr.dept_id = #{deptID}
</if>
<if test="roleID != null">
AND udr.role_id = #{roleID}
</if>
</where>
ORDER BY udr.user_id, udr.dept_id, udr.role_id, udr.create_time DESC
</select>
<insert id="bindUser" parameterType="TbSysUserDeptRole">
@@ -70,4 +103,20 @@
</foreach>
</delete>
<!-- deleteUserDeptRole - 物理删除所有记录(包括软删除的) -->
<delete id="deleteUserDeptRole">
DELETE FROM tb_sys_user_dept_role
WHERE user_id = #{userID}
</delete>
<!-- deleteUserDeptRoleByUserIds - 批量删除多个用户的绑定 -->
<delete id="deleteUserDeptRoleByUserIds">
DELETE FROM tb_sys_user_dept_role
WHERE user_id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</delete>
</mapper>

View File

@@ -242,12 +242,10 @@
</update>
<!-- 根据ID删除逻辑删除 -->
<update id="deleteUser">
UPDATE tb_sys_user
SET deleted = 1,
delete_time = NOW()
WHERE id = #{id} AND deleted = 0
</update>
<delete id="deleteUser">
DELETE FROM tb_sys_user
WHERE id = #{userID} AND deleted = 0
</delete>
<!-- 用户信息相关 -->
@@ -293,10 +291,7 @@
INNER JOIN tb_sys_user_dept_role tsudr ON tsui.user_id = tsudr.user_id
INNER JOIN tb_sys_dept tsd ON tsudr.dept_id = tsd.dept_id
WHERE tsui.user_id = #{userId} AND tsui.deleted = 0
UNION ALL
-- 递归查询:向上查找父部门
SELECT
p.dept_id,
p.name,
@@ -304,12 +299,12 @@
CONCAT(p.name, '/', dh.dept_path) as dept_path,
dh.level + 1 as level
FROM tb_sys_dept p
INNER JOIN dept_hierarchy dh ON p.dept_id = dh.parent_id
INNER JOIN dept_hierarchy dh ON dh.parent_id = p.dept_id
WHERE p.deleted = 0
)
SELECT dh.dept_path
FROM dept_hierarchy dh
WHERE dh.parent_id IS NULL -- 只取最顶层的部门路径
WHERE dh.parent_id IS NULL
LIMIT 1
</select>
@@ -317,7 +312,6 @@
<select id="selectUserInfoTotal" resultMap="UserInfoTotalResultMap">
WITH RECURSIVE dept_hierarchy AS (
-- 基础查询:获取用户直接所属的部门
SELECT
tsd.dept_id,
tsd.name,
@@ -328,10 +322,7 @@
INNER JOIN tb_sys_user_dept_role tsudr ON tsui.user_id = tsudr.user_id
INNER JOIN tb_sys_dept tsd ON tsudr.dept_id = tsd.dept_id
WHERE tsui.user_id = #{userId} AND tsui.deleted = 0
UNION ALL
-- 递归查询:向上查找父部门
SELECT
p.dept_id,
p.name,
@@ -339,7 +330,7 @@
CONCAT(p.name, '/', dh.dept_path) as dept_path,
dh.level + 1 as level
FROM tb_sys_dept p
INNER JOIN dept_hierarchy dh ON p.dept_id = dh.parent_id
INNER JOIN dept_hierarchy dh ON dh.parent_id = p.dept_id
WHERE p.deleted = 0
)
SELECT
@@ -349,7 +340,7 @@
tus.email,
tsui.avatar,
tsui.gender,
dh.dept_path as dept_name,
(SELECT dept_path FROM dept_hierarchy WHERE parent_id IS NULL LIMIT 1) as dept_name,
tsr.name as role_name,
tsui.level,
tsui.id_card,
@@ -357,10 +348,8 @@
FROM tb_sys_user_info tsui
INNER JOIN tb_sys_user tus ON tsui.user_id = tus.id
INNER JOIN tb_sys_user_dept_role tsudr ON tsui.user_id = tsudr.user_id
INNER JOIN dept_hierarchy dh ON tsudr.dept_id = dh.dept_id
INNER JOIN tb_sys_role tsr ON tsudr.role_id = tsr.role_id
WHERE tsui.user_id = #{userId}
AND tsui.deleted = 0
AND dh.parent_id IS NULL -- 只取最顶层的部门路径
</select>
</mapper>

View File

@@ -27,7 +27,7 @@
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.32.7",
"typescript": "~4.5.5",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vite-plugin-pwa": "^0.19.0"
}
@@ -7505,9 +7505,9 @@
}
},
"node_modules/typescript": {
"version": "4.5.5",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.5.5.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
"version": "5.2.2",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@@ -7515,7 +7515,7 @@
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {

View File

@@ -30,7 +30,7 @@
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.32.7",
"typescript": "~4.5.5",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vite-plugin-pwa": "^0.19.0"
}

View File

@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6667 1.8335H2.33334V11.1668H13.6667V1.8335Z" stroke="#333333" stroke-width="1.33333" stroke-linejoin="round"/>
<path d="M5.33334 13.8332L8.00001 11.1665L10.6667 13.8332" stroke="#333333" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.6413 8.22084L6.52213 6.3849L8.00276 7.8332L11.3223 4.50684" stroke="#333333" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.33334 1.8335H14.6667" stroke="#333333" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1.3335H4.00001C3.64638 1.3335 3.30724 1.47397 3.0572 1.72402C2.80715 1.97407 2.66667 2.31321 2.66667 2.66683V13.3335C2.66667 13.6871 2.80715 14.0263 3.0572 14.2763C3.30724 14.5264 3.64638 14.6668 4.00001 14.6668H12C12.3536 14.6668 12.6928 14.5264 12.9428 14.2763C13.1929 14.0263 13.3333 13.6871 13.3333 13.3335V4.66683L10 1.3335Z" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33333 1.3335V4.00016C9.33333 4.35378 9.4738 4.69292 9.72385 4.94297C9.9739 5.19302 10.313 5.3335 10.6667 5.3335H13.3333" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66666 6H5.33333" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 8.6665H5.33333" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 11.3335H5.33333" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -83,11 +83,11 @@ export const learningTaskApi = {
},
/**
* 发布学习任务
* 发布\下架学习任务
* @param taskID 任务ID
* @returns Promise<ResultDomain<LearningTask>>
*/
async publishTask(task: LearningTask): Promise<ResultDomain<LearningTask>> {
async changeTaskStatus(task: LearningTask): Promise<ResultDomain<LearningTask>> {
const response = await api.put<LearningTask>(`${this.learningTaskPrefix}/status`, task);
return response.data;
},

View File

@@ -96,13 +96,13 @@ export const deptApi = {
/**
* @description 查询部门角色列表
* @param dept 部门角色信息
* @param deptRole 部门角色信息
* @returns Promise<ResultDomain<SysDeptRole>> 部门角色列表
* @author yslg
* @ since 2025-10-06
*/
async getDeptRoleList(dept: SysDeptRole): Promise<ResultDomain<SysDeptRole>> {
const response = await api.post<SysDeptRole>('/depts/role/list', dept);
async getDeptRoleList(deptRole: SysDeptRole): Promise<ResultDomain<SysDeptRole>> {
const response = await api.post<SysDeptRole>('/depts/role/list', deptRole);
return response.data;
},

View File

@@ -97,8 +97,8 @@ export const userApi = {
* @param user 用户信息
* @returns Promise<ResultDomain<SysUserDeptRole>>
*/
async getUserDeptRole(user: SysUserDeptRole): Promise<ResultDomain<SysUserDeptRole>> {
const response = await api.post<SysUserDeptRole>('/users/bind/deptrole/list', user);
async getUserDeptRole(user: UserDeptRoleVO): Promise<ResultDomain<UserDeptRoleVO>> {
const response = await api.post<UserDeptRoleVO>('/users/bind/deptrole/list', user);
return response.data;
},

View File

@@ -0,0 +1,862 @@
<template>
<div v-if="visible" class="modal-overlay" @click.self="handleCancel">
<div class="modal-content large">
<div class="modal-header">
<h3 class="modal-title">{{ title }}</h3>
<button class="modal-close" @click="handleCancel"></button>
</div>
<div class="modal-body">
<div class="generic-selector">
<!-- 左侧可选项 -->
<div class="selector-panel">
<div class="panel-header">
<h4 class="panel-title">{{ leftTitle }}</h4>
<span class="panel-count">
{{ countText(availableList.length) }}
</span>
</div>
<div class="panel-search" v-if="showSearch">
<input
v-model="searchAvailable"
type="text"
:placeholder="searchPlaceholder"
class="search-input-small"
/>
</div>
<div class="panel-body left-panel">
<!-- 列表模式 -->
<div v-if="!useTree" class="item-list">
<div
v-for="item in filteredAvailable"
:key="getItemId(item)"
class="list-item"
:class="{ selected: getItemId(item) && selectedAvailable.includes(getItemId(item)) }"
@click="getItemId(item) && toggleAvailable(getItemId(item))"
@dblclick="getItemId(item) && moveToTarget(getItemId(item))"
>
<input
type="checkbox"
:checked="!!(getItemId(item) && selectedAvailable.includes(getItemId(item)))"
@click.stop="getItemId(item) && toggleAvailable(getItemId(item))"
/>
<span class="item-label">{{ getItemLabel(item) }}</span>
<span class="item-sublabel" v-if="getItemSublabel(item)">{{ getItemSublabel(item) }}</span>
</div>
<!-- 加载更多提示 -->
<div v-if="usePagination && loadingMore" class="loading-more">
<div class="loading-spinner-small"></div>
<span>加载中...</span>
</div>
<div v-if="usePagination && !hasMore && availableList.length > 0" class="no-more">
已加载全部数据
</div>
</div>
<!-- 树形模式 -->
<div v-else class="tree-list">
<TreeNode
v-for="node in treeData"
:key="getNodeId(node)"
:node="node"
:tree-props="treeProps"
:expanded-keys="expandedKeys"
:selected-ids="selectedAvailable"
:only-leaf-selectable="onlyLeafSelectable"
@toggle-expand="toggleExpand"
@toggle-select="toggleAvailable"
@dblclick="moveToTarget"
/>
</div>
</div>
</div>
<!-- 中间操作按钮 -->
<div class="selector-actions">
<button
class="action-btn"
@click="moveSelectedToTarget"
:disabled="selectedAvailable.length === 0"
:title="mode === 'add' ? '添加选中' : '删除选中'"
>
<span class="arrow"></span>
<span class="btn-text">{{ mode === 'add' ? '添加' : '删除' }}</span>
</button>
<button
class="action-btn"
@click="moveAllToTarget"
:disabled="availableList.length === 0"
:title="mode === 'add' ? '全部添加' : '全部删除'"
>
<span class="arrow"></span>
<span class="btn-text">全部</span>
</button>
<button
class="action-btn"
@click="moveBackSelected"
:disabled="selectedTarget.length === 0"
title="移回选中"
>
<span class="arrow"></span>
<span class="btn-text">移回</span>
</button>
<button
class="action-btn"
@click="moveBackAll"
:disabled="targetList.length === 0"
title="全部移回"
>
<span class="arrow"></span>
<span class="btn-text">全部</span>
</button>
</div>
<!-- 右侧已选项 -->
<div class="selector-panel">
<div class="panel-header">
<h4 class="panel-title">{{ rightTitle }}</h4>
<span class="panel-count">{{ countText(targetList.length) }}</span>
</div>
<div class="panel-search" v-if="showSearch">
<input
v-model="searchTarget"
type="text"
:placeholder="searchPlaceholder"
class="search-input-small"
/>
</div>
<div class="panel-body">
<div class="item-list">
<div
v-for="item in filteredTarget"
:key="getItemId(item)"
class="list-item"
:class="{ selected: getItemId(item) && selectedTarget.includes(getItemId(item)) }"
@click="getItemId(item) && toggleTarget(getItemId(item))"
@dblclick="getItemId(item) && moveBackToAvailable(getItemId(item))"
>
<input
type="checkbox"
:checked="!!(getItemId(item) && selectedTarget.includes(getItemId(item)))"
@click.stop="getItemId(item) && toggleTarget(getItemId(item))"
/>
<span class="item-label">{{ getItemLabel(item) }}</span>
<span class="item-sublabel" v-if="getItemSublabel(item)">{{ getItemSublabel(item) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-primary" @click="handleConfirm" :disabled="loading">
{{ loading ? '处理中...' : '确定' }}
</button>
<button class="btn-default" @click="handleCancel">取消</button>
</div>
</div>
</div>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
import { ref, computed, watch, nextTick } from 'vue';
import type { ResultDomain, PageParam } from '@/types';
import { TreeNode } from '@/components/base';
// 泛型项类型
type GenericItem = Record<string, any>;
interface ItemConfig {
/** ID 字段名 */
id: string;
/** 显示标签字段名 */
label: string;
/** 副标签字段名(可选) */
sublabel?: string;
}
interface Props {
visible?: boolean;
mode?: 'add' | 'remove';
title?: string;
leftTitle?: string;
rightTitle?: string;
/** 【新】获取所有可选项的接口 */
fetchAvailableApi?: () => Promise<ResultDomain<GenericItem>>;
/** 【新】获取已选项的接口 */
fetchSelectedApi?: () => Promise<ResultDomain<GenericItem>>;
/** 【新】过滤已选项的方法(返回过滤后的可选项) */
filterSelected?: (available: GenericItem[], selected: GenericItem[]) => GenericItem[];
/** 可选项列表兼容旧方式如果提供了fetchAvailableApi则忽略 */
availableItems?: GenericItem[];
/** 初始已选项列表兼容旧方式如果提供了fetchSelectedApi则忽略 */
initialTargetItems?: GenericItem[];
loading?: boolean;
/** 字段配置 */
itemConfig: ItemConfig;
/** 单位名称(用于计数显示) */
unitName?: string;
/** 搜索占位符 */
searchPlaceholder?: string;
/** 是否显示搜索框 */
showSearch?: boolean;
/** 分页加载API方法 */
fetchApi?: (pageParam: PageParam, filter?: any) => Promise<ResultDomain<GenericItem>>;
/** 过滤参数 */
filterParams?: any;
/** 每页数量 */
pageSize?: number;
/** 是否使用分页加载 */
usePagination?: boolean;
/** 是否使用树形展示 */
useTree?: boolean;
/** 树形数据转换函数 */
treeTransform?: (data: GenericItem[]) => any[];
/** 树节点的配置 */
treeProps?: {
children?: string;
label?: string;
id?: string;
};
/** 是否只允许选择叶子节点(树形模式下有效) */
onlyLeafSelectable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
mode: 'add',
title: '选择',
leftTitle: '可选项',
rightTitle: '已选项',
availableItems: () => [],
initialTargetItems: () => [],
loading: false,
unitName: '项',
searchPlaceholder: '搜索...',
showSearch: true,
pageSize: 20,
usePagination: false,
useTree: false,
treeProps: () => ({
children: 'children',
label: 'label',
id: 'id'
}),
onlyLeafSelectable: false
});
const emit = defineEmits<{
'update:visible': [value: boolean];
confirm: [items: GenericItem[]];
cancel: [];
}>();
// 数据列表
const availableList = ref<GenericItem[]>([]);
const targetList = ref<GenericItem[]>([]);
// 树形数据相关
const treeData = ref<any[]>([]);
const expandedKeys = ref<Set<string>>(new Set());
// 选中状态
const selectedAvailable = ref<string[]>([]);
const selectedTarget = ref<string[]>([]);
// 搜索关键词
const searchAvailable = ref('');
const searchTarget = ref('');
// 分页相关
const currentPage = ref(1);
const totalElements = ref(0);
const hasMore = ref(true);
const loadingMore = ref(false);
const availablePanelRef = ref<HTMLElement | null>(null);
// 获取项的 ID
function getItemId(item: GenericItem): string {
return String(item[props.itemConfig.id] || '');
}
// 获取项的标签
function getItemLabel(item: GenericItem): string {
return String(item[props.itemConfig.label] || '');
}
// 获取项的副标签
function getItemSublabel(item: GenericItem): string {
if (!props.itemConfig.sublabel) return '';
return String(item[props.itemConfig.sublabel] || '');
}
// 获取树节点 ID
function getNodeId(node: any): string {
const idProp = props.treeProps?.id || 'id';
return String(node[idProp] || '');
}
// 获取树节点标签
// function getNodeLabel(node: any): string {
// const labelProp = props.treeProps?.label || 'label';
// return String(node[labelProp] || '');
// }
// 获取树节点子节点
// function getNodeChildren(node: any): any[] {
// const childrenProp = props.treeProps?.children || 'children';
// return node[childrenProp] || [];
// }
// 计数文本
function countText(count: number): string {
if (props.usePagination && totalElements.value > 0) {
return `${count}/${totalElements.value} ${props.unitName}`;
}
return `${count} ${props.unitName}`;
}
// 监听props变化初始化数据
watch(() => props.visible, (newVal) => {
if (newVal) {
initializeData();
} else {
resetData();
}
}, { immediate: true });
// 监听搜索关键词变化
watch(searchAvailable, () => {
if (props.usePagination && props.fetchApi) {
resetPaginationAndLoad();
}
});
// 初始化数据
async function initializeData() {
try {
// 优先使用接口方式加载数据
if (props.fetchAvailableApi || props.fetchSelectedApi) {
await loadDataFromApis();
} else if (props.usePagination && props.fetchApi) {
// 使用分页方式
currentPage.value = 1;
hasMore.value = true;
availableList.value = [];
await loadAvailableItems();
targetList.value = [...props.initialTargetItems];
} else {
// 使用传入的数据
availableList.value = [...props.availableItems];
targetList.value = [...props.initialTargetItems];
}
// 如果使用树形展示,转换数据
if (props.useTree && props.treeTransform) {
treeData.value = props.treeTransform(availableList.value);
expandAllNodes(treeData.value);
}
selectedAvailable.value = [];
selectedTarget.value = [];
searchAvailable.value = '';
searchTarget.value = '';
await nextTick();
bindScrollEvent();
} catch (error) {
console.error('初始化数据失败:', error);
}
}
// 从接口加载数据
async function loadDataFromApis() {
try {
// 加载所有可选项
if (props.fetchAvailableApi) {
const availableResult = await props.fetchAvailableApi();
if (availableResult.success) {
const allAvailable = availableResult.dataList || [];
// 加载已选项
if (props.fetchSelectedApi) {
const selectedResult = await props.fetchSelectedApi();
if (selectedResult.success) {
targetList.value = selectedResult.dataList || [];
// 过滤已选项
if (props.filterSelected) {
availableList.value = props.filterSelected(allAvailable, targetList.value);
} else {
// 默认过滤逻辑根据ID过滤
const selectedIds = new Set(targetList.value.map(item => getItemId(item)));
availableList.value = allAvailable.filter(item => !selectedIds.has(getItemId(item)));
}
}
} else {
// 如果没有提供已选接口,使用传入的初始数据
targetList.value = [...props.initialTargetItems];
const selectedIds = new Set(targetList.value.map(item => getItemId(item)));
availableList.value = allAvailable.filter(item => !selectedIds.has(getItemId(item)));
}
}
} else if (props.fetchSelectedApi) {
// 只有已选接口
const selectedResult = await props.fetchSelectedApi();
if (selectedResult.success) {
targetList.value = selectedResult.dataList || [];
}
availableList.value = [...props.availableItems];
const selectedIds = new Set(targetList.value.map(item => getItemId(item)));
availableList.value = availableList.value.filter(item => !selectedIds.has(getItemId(item)));
}
} catch (error) {
console.error('从接口加载数据失败:', error);
throw error;
}
}
// 展开所有树节点
function expandAllNodes(nodes: any[]) {
const childrenProp = props.treeProps?.children || 'children';
nodes.forEach(node => {
const nodeId = getNodeId(node);
if (nodeId) {
expandedKeys.value.add(nodeId);
}
const children = node[childrenProp] || [];
if (children && children.length > 0) {
expandAllNodes(children);
}
});
}
// 重置数据
function resetData() {
availableList.value = [];
targetList.value = [];
treeData.value = [];
expandedKeys.value.clear();
selectedAvailable.value = [];
selectedTarget.value = [];
searchAvailable.value = '';
searchTarget.value = '';
currentPage.value = 1;
totalElements.value = 0;
hasMore.value = true;
loadingMore.value = false;
unbindScrollEvent();
}
// 加载可选项
async function loadAvailableItems() {
if (!props.fetchApi) return;
try {
loadingMore.value = true;
const pageParam: PageParam = {
pageNumber: currentPage.value,
pageSize: props.pageSize
};
const filter = searchAvailable.value
? { ...props.filterParams, keyword: searchAvailable.value }
: props.filterParams;
const res = await props.fetchApi(pageParam, filter);
if (res.success) {
const newItems = res.dataList || [];
const targetItemIds = targetList.value.map(item => getItemId(item));
const filteredItems = newItems.filter(item => !targetItemIds.includes(getItemId(item)));
if (currentPage.value === 1) {
availableList.value = filteredItems;
} else {
availableList.value.push(...filteredItems);
}
totalElements.value = res.pageParam?.totalElements || 0;
const totalPages = Math.ceil(totalElements.value / props.pageSize);
hasMore.value = currentPage.value < totalPages;
}
} catch (error) {
console.error('加载数据失败:', error);
} finally {
loadingMore.value = false;
}
}
// 重置分页并重新加载
async function resetPaginationAndLoad() {
currentPage.value = 1;
hasMore.value = true;
availableList.value = [];
await loadAvailableItems();
}
// 绑定滚动事件
function bindScrollEvent() {
const panelBody = document.querySelector('.panel-body.left-panel');
if (panelBody) {
availablePanelRef.value = panelBody as HTMLElement;
panelBody.addEventListener('scroll', handleScroll);
}
}
// 解绑滚动事件
function unbindScrollEvent() {
if (availablePanelRef.value) {
availablePanelRef.value.removeEventListener('scroll', handleScroll);
availablePanelRef.value = null;
}
}
// 处理滚动事件
function handleScroll(event: Event) {
if (!props.usePagination || !props.fetchApi) return;
const target = event.target as HTMLElement;
const scrollTop = target.scrollTop;
const scrollHeight = target.scrollHeight;
const clientHeight = target.clientHeight;
if (scrollHeight - scrollTop - clientHeight < 50 && hasMore.value && !loadingMore.value) {
currentPage.value++;
loadAvailableItems();
}
}
// 过滤可选项
const filteredAvailable = computed(() => {
if (props.usePagination) {
return availableList.value;
}
if (!searchAvailable.value) {
return availableList.value;
}
const keyword = searchAvailable.value.toLowerCase();
return availableList.value.filter(item => {
const label = getItemLabel(item).toLowerCase();
const sublabel = getItemSublabel(item).toLowerCase();
return label.includes(keyword) || sublabel.includes(keyword);
});
});
// 过滤已选项
const filteredTarget = computed(() => {
if (!searchTarget.value) {
return targetList.value;
}
const keyword = searchTarget.value.toLowerCase();
return targetList.value.filter(item => {
const label = getItemLabel(item).toLowerCase();
const sublabel = getItemSublabel(item).toLowerCase();
return label.includes(keyword) || sublabel.includes(keyword);
});
});
// 切换树节点展开/折叠
function toggleExpand(nodeId: string) {
if (expandedKeys.value.has(nodeId)) {
expandedKeys.value.delete(nodeId);
} else {
expandedKeys.value.add(nodeId);
}
}
// 检查节点是否展开暂未使用保留供TreeNode组件使用
// function isExpanded(nodeId: string): boolean {
// return expandedKeys.value.has(nodeId);
// }
// 获取树节点的所有子节点ID递归
function getAllChildrenIds(node: any): string[] {
const childrenProp = props.treeProps?.children || 'children';
const children = node[childrenProp] || [];
const ids: string[] = [];
children.forEach((child: any) => {
const childId = getNodeId(child);
if (childId) {
ids.push(childId);
// 递归获取子节点的子节点
ids.push(...getAllChildrenIds(child));
}
});
return ids;
}
// 在树形数据中查找节点
function findNodeInTree(nodeId: string, nodes: any[]): any | null {
for (const node of nodes) {
const id = getNodeId(node);
if (id === nodeId) {
return node;
}
const childrenProp = props.treeProps?.children || 'children';
const children = node[childrenProp] || [];
if (children.length > 0) {
const found = findNodeInTree(nodeId, children);
if (found) return found;
}
}
return null;
}
// 检查节点是否为叶子节点
function isLeafNode(node: any): boolean {
const childrenProp = props.treeProps?.children || 'children';
const children = node[childrenProp] || [];
return children.length === 0;
}
// 切换可选项的选中状态(支持级联选择)
function toggleAvailable(itemId: string) {
const index = selectedAvailable.value.indexOf(itemId);
if (props.useTree && treeData.value.length > 0) {
// 树形模式下的级联选择
const node = findNodeInTree(itemId, treeData.value);
if (node) {
// 如果只允许选择叶子节点,检查是否为叶子节点
if (props.onlyLeafSelectable && !isLeafNode(node)) {
// 非叶子节点,不允许选择
return;
}
const childrenIds = getAllChildrenIds(node);
if (index > -1) {
// 取消选中:移除当前节点和所有子节点
selectedAvailable.value = selectedAvailable.value.filter(
id => id !== itemId && !childrenIds.includes(id)
);
} else {
// 选中:添加当前节点和所有子节点
selectedAvailable.value.push(itemId);
childrenIds.forEach(childId => {
if (!selectedAvailable.value.includes(childId)) {
selectedAvailable.value.push(childId);
}
});
}
} else {
// 节点未找到,按普通方式处理
if (index > -1) {
selectedAvailable.value.splice(index, 1);
} else {
selectedAvailable.value.push(itemId);
}
}
} else {
// 非树形模式,普通切换
if (index > -1) {
selectedAvailable.value.splice(index, 1);
} else {
selectedAvailable.value.push(itemId);
}
}
}
// 切换已选项的选中状态(暂不支持级联,因为已选项通常不需要级联)
function toggleTarget(itemId: string) {
const index = selectedTarget.value.indexOf(itemId);
if (index > -1) {
selectedTarget.value.splice(index, 1);
} else {
selectedTarget.value.push(itemId);
}
}
// 从树形数据中收集所有节点(扁平化)
function flattenTree(nodes: any[]): any[] {
const result: any[] = [];
const childrenProp = props.treeProps?.children || 'children';
nodes.forEach(node => {
result.push(node);
const children = node[childrenProp] || [];
if (children.length > 0) {
result.push(...flattenTree(children));
}
});
return result;
}
// 移动选中项到目标区域
function moveSelectedToTarget() {
let itemsToMove: any[] = [];
if (props.useTree && treeData.value.length > 0) {
// 树形模式:从扁平化的树数据中查找
const flatItems = flattenTree(treeData.value);
itemsToMove = flatItems.filter(item =>
selectedAvailable.value.includes(getNodeId(item))
);
} else {
// 列表模式:从可选列表中查找
itemsToMove = availableList.value.filter(item =>
selectedAvailable.value.includes(getItemId(item))
);
}
targetList.value.push(...itemsToMove);
if (props.useTree && treeData.value.length > 0) {
// 树形模式:需要重新构建树(移除已选项)
availableList.value = availableList.value.filter(item =>
!selectedAvailable.value.includes(getItemId(item))
);
// 重新转换为树形结构
if (props.treeTransform) {
treeData.value = props.treeTransform(availableList.value);
}
} else {
// 列表模式:直接过滤
availableList.value = availableList.value.filter(item =>
!selectedAvailable.value.includes(getItemId(item))
);
}
selectedAvailable.value = [];
}
// 移动单个项到目标区域(双击)
function moveToTarget(itemId: string) {
let item: any = null;
let itemsToMove: any[] = [];
if (props.useTree && treeData.value.length > 0) {
// 树形模式:查找节点和所有子节点
const node = findNodeInTree(itemId, treeData.value);
if (node) {
// 如果只允许选择叶子节点,检查是否为叶子节点
if (props.onlyLeafSelectable && !isLeafNode(node)) {
// 非叶子节点,不允许移动
return;
}
item = node;
const childrenIds = getAllChildrenIds(node);
const flatItems = flattenTree(treeData.value);
// 收集当前节点和所有子节点
itemsToMove = flatItems.filter(i => {
const id = getNodeId(i);
return id === itemId || childrenIds.includes(id);
});
// 移动到已选列表
targetList.value.push(...itemsToMove);
// 从可选列表中移除
const idsToRemove = new Set([itemId, ...childrenIds]);
availableList.value = availableList.value.filter(i =>
!idsToRemove.has(getItemId(i))
);
// 重新构建树
if (props.treeTransform) {
treeData.value = props.treeTransform(availableList.value);
}
}
} else {
// 列表模式
item = availableList.value.find(i => getItemId(i) === itemId);
if (item) {
targetList.value.push(item);
availableList.value = availableList.value.filter(i => getItemId(i) !== itemId);
}
}
}
// 移动所有项到目标区域
function moveAllToTarget() {
if (props.useTree && treeData.value.length > 0) {
// 树形模式:扁平化所有节点
const flatItems = flattenTree(treeData.value);
targetList.value.push(...flatItems);
availableList.value = [];
treeData.value = [];
} else {
// 列表模式
targetList.value.push(...availableList.value);
availableList.value = [];
}
selectedAvailable.value = [];
}
// 移回选中项到可选区域
function moveBackSelected() {
const itemsToMoveBack = targetList.value.filter(item =>
selectedTarget.value.includes(getItemId(item))
);
availableList.value.push(...itemsToMoveBack);
targetList.value = targetList.value.filter(item =>
!selectedTarget.value.includes(getItemId(item))
);
// 如果是树形模式,重新构建树
if (props.useTree && props.treeTransform) {
treeData.value = props.treeTransform(availableList.value);
expandAllNodes(treeData.value);
}
selectedTarget.value = [];
}
// 移回单个项到可选区域(双击)
function moveBackToAvailable(itemId: string) {
const item = targetList.value.find(i => getItemId(i) === itemId);
if (item) {
availableList.value.push(item);
targetList.value = targetList.value.filter(i => getItemId(i) !== itemId);
// 如果是树形模式,重新构建树
if (props.useTree && props.treeTransform) {
treeData.value = props.treeTransform(availableList.value);
expandAllNodes(treeData.value);
}
}
}
// 移回所有项到可选区域
function moveBackAll() {
availableList.value.push(...targetList.value);
targetList.value = [];
// 如果是树形模式,重新构建树
if (props.useTree && props.treeTransform) {
treeData.value = props.treeTransform(availableList.value);
expandAllNodes(treeData.value);
}
selectedTarget.value = [];
}
// 确认
function handleConfirm() {
emit('confirm', targetList.value);
emit('update:visible', false);
}
// 取消
function handleCancel() {
emit('cancel');
emit('update:visible', false);
}
</script>
<style lang="scss" scoped>
@import './selector-styles.scss';
</style>

View File

@@ -0,0 +1,221 @@
<template>
<div class="tree-node">
<div
class="tree-node-content"
:style="{ paddingLeft: `${level * 20}px` }"
:class="{
selected: selectedIds.includes(nodeId),
disabled: isCheckboxDisabled
}"
@click.stop="handleClick"
@dblclick.stop="handleDblClick"
>
<!-- 展开/折叠图标 -->
<span
v-if="hasChildren"
class="expand-icon"
:class="{ expanded }"
@click.stop="$emit('toggle-expand', nodeId)"
>
<img src="@/assets/imgs/arrow-down.svg" :class="{expanded}"/>
</span>
<span v-else class="expand-icon-placeholder"></span>
<!-- 复选框 -->
<input
type="checkbox"
:checked="selectedIds.includes(nodeId)"
:disabled="isCheckboxDisabled"
@click.stop="handleCheckboxClick"
/>
<!-- 节点标签 -->
<span class="node-label">{{ nodeLabel }}</span>
</div>
<!-- 子节点 -->
<div v-if="hasChildren && expanded" class="tree-node-children">
<TreeNode
v-for="child in children"
:key="getChildId(child)"
:node="child"
:level="level + 1"
:selected-ids="selectedIds"
:tree-props="treeProps"
:expanded-keys="expandedKeys"
:only-leaf-selectable="onlyLeafSelectable"
@toggle-expand="$emit('toggle-expand', $event)"
@toggle-select="$emit('toggle-select', $event)"
@dblclick="$emit('dblclick', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
defineOptions({
name: 'TreeNode'
});
interface Props {
node: any;
level?: number;
selectedIds: string[];
treeProps?: {
children?: string;
label?: string;
id?: string;
};
expandedKeys: Set<string>;
onlyLeafSelectable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
level: 0,
treeProps: () => ({
children: 'children',
label: 'label',
id: 'id'
}),
onlyLeafSelectable: false
});
const emit = defineEmits<{
'toggle-expand': [nodeId: string];
'toggle-select': [nodeId: string];
'dblclick': [nodeId: string];
}>();
const nodeId = computed(() => {
const idProp = props.treeProps?.id || 'id';
return String(props.node[idProp] || '');
});
const nodeLabel = computed(() => {
const labelProp = props.treeProps?.label || 'label';
return String(props.node[labelProp] || '');
});
const children = computed(() => {
const childrenProp = props.treeProps?.children || 'children';
return props.node[childrenProp] || [];
});
const hasChildren = computed(() => children.value && children.value.length > 0);
const expanded = computed(() => props.expandedKeys.has(nodeId.value));
// 判断复选框是否应该被禁用
const isCheckboxDisabled = computed(() => {
// 如果只允许选择叶子节点,且当前节点有子节点,则禁用复选框
return props.onlyLeafSelectable && hasChildren.value;
});
function getChildId(child: any): string {
const idProp = props.treeProps?.id || 'id';
return String(child[idProp] || '');
}
function handleClick() {
// 点击节点主体时切换选中状态(如果复选框未禁用)
if (!isCheckboxDisabled.value) {
emit('toggle-select', nodeId.value);
}
}
function handleCheckboxClick() {
// 复选框点击事件(如果未禁用)
if (!isCheckboxDisabled.value) {
emit('toggle-select', nodeId.value);
}
}
function handleDblClick() {
// 双击时触发(如果复选框未禁用)
if (!isCheckboxDisabled.value) {
emit('dblclick', nodeId.value);
}
}
</script>
<style lang="scss" scoped>
.tree-node {
user-select: none;
&-content {
display: flex;
align-items: center;
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f7fa;
}
&.selected {
background-color: #e6f7ff;
}
.expand-icon {
display: inline-block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
font-size: 10px;
cursor: pointer;
transition: transform 0.2s;
color: #606266;
img {
transform: rotate(-90deg);
&.expanded {
transform: rotate(0deg);
}
}
&:hover {
color: #409EFF;
}
}
.expand-icon-placeholder {
display: inline-block;
width: 16px;
}
input[type="checkbox"] {
margin: 0 8px;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: 0.4;
}
}
.node-label {
flex: 1;
font-size: 14px;
color: #606266;
}
&.disabled {
cursor: not-allowed;
.node-label {
color: #999;
}
}
}
&-children {
margin-left: 0;
}
}
</style>

View File

@@ -8,3 +8,5 @@ export { default as TopNavigation } from './TopNavigation.vue';
export { default as UserDropdown } from './UserDropdown.vue';
export { default as Search } from './Search.vue';
export { default as CenterHead } from './CenterHead.vue';
export { default as GenericSelector } from './GenericSelector.vue';
export { default as TreeNode } from './TreeNode.vue';

View File

@@ -0,0 +1,301 @@
// 通用选择器样式
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 3000;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
max-height: 90vh;
&.large {
width: 900px;
}
}
.modal-header {
padding: 16px 24px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
.modal-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
line-height: 24px;
&:hover {
color: #666;
}
}
}
.modal-body {
flex: 1;
overflow: hidden;
padding: 16px 24px;
}
.modal-footer {
padding: 12px 24px;
border-top: 1px solid #e8e8e8;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.generic-selector {
display: flex;
gap: 16px;
height: 500px;
}
.selector-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #e8e8e8;
border-radius: 4px;
background: #fafafa;
.panel-header {
padding: 12px 16px;
background: #f0f0f0;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
.panel-title {
margin: 0;
font-size: 14px;
font-weight: 500;
color: #333;
}
.panel-count {
font-size: 12px;
color: #999;
}
}
.panel-search {
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
background: white;
.search-input-small {
width: 100%;
padding: 6px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
outline: none;
&:focus {
border-color: #409EFF;
}
}
}
.panel-body {
flex: 1;
overflow-y: auto;
background: white;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
}
}
.item-list,
.tree-list {
padding: 8px;
}
.tree-notice {
padding: 40px 20px;
text-align: center;
color: #909399;
font-size: 14px;
}
.list-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f7fa;
}
&.selected {
background-color: #e6f7ff;
}
input[type="checkbox"] {
margin-right: 8px;
cursor: pointer;
}
.item-label {
flex: 1;
font-size: 14px;
color: #333;
}
.item-sublabel {
font-size: 12px;
color: #999;
margin-left: 8px;
}
}
.selector-actions {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
.action-btn {
padding: 8px 16px;
background: white;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
transition: all 0.2s;
min-width: 60px;
&:hover:not(:disabled) {
border-color: #409EFF;
color: #409EFF;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.arrow {
font-size: 16px;
font-weight: bold;
}
.btn-text {
font-size: 12px;
}
}
}
.loading-more,
.no-more {
text-align: center;
padding: 12px;
color: #999;
font-size: 12px;
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.loading-spinner-small {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.btn-primary {
padding: 8px 20px;
background-color: #409EFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
&:hover:not(:disabled) {
background-color: #66b1ff;
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
.btn-default {
padding: 8px 20px;
background-color: white;
color: #606266;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
&:hover {
border-color: #409EFF;
color: #409EFF;
}
}

View File

@@ -8,5 +8,4 @@ export * from './text';
export * from './file';
// 导出 user 用户组件
export * from './user';

View File

@@ -1,213 +0,0 @@
# UserSelect 用户选择组件
## 功能特性
- ✅ 支持双栏选择器布局
- ✅ 支持搜索功能(左右两侧独立搜索)
- ✅ 三种操作方式:双击、勾选+按钮、全部按钮
-**支持滚动分页加载**(性能优化)
- ✅ 支持传入静态数据或API方法
- ✅ 完全封装的样式和逻辑
## Props 配置
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| visible | Boolean | false | 控制弹窗显示/隐藏 |
| mode | 'add' \| 'remove' | 'add' | 选择模式 |
| title | String | '人员选择' | 弹窗标题 |
| leftTitle | String | '可选人员' | 左侧面板标题 |
| rightTitle | String | '已选人员' | 右侧面板标题 |
| availableUsers | UserVO[] | [] | 左区域静态数据 |
| initialTargetUsers | UserVO[] | [] | 初始已选人员 |
| loading | Boolean | false | 确认按钮加载状态 |
| **usePagination** | Boolean | false | **是否启用分页加载** |
| **fetchApi** | Function | undefined | **分页加载API方法** |
| **filterParams** | Object | {} | **API过滤参数** |
| **pageSize** | Number | 20 | **每页数量** |
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:visible | (value: boolean) | 更新弹窗显示状态 |
| confirm | (users: UserVO[]) | 确认提交,返回选中的用户列表 |
| cancel | - | 取消操作 |
## 使用方式
### 方式一:传入静态数据(适合数据量少的场景)
```vue
<template>
<UserSelect
v-model:visible="showSelector"
mode="add"
title="选择用户"
:available-users="allUsers"
:initial-target-users="[]"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { UserSelect } from '@/components';
import type { UserVO } from '@/types';
const showSelector = ref(false);
const allUsers = ref<UserVO[]>([]);
function handleConfirm(selectedUsers: UserVO[]) {
console.log('选中的用户:', selectedUsers);
showSelector.value = false;
}
</script>
```
### 方式二:使用分页加载(推荐,适合数据量大的场景)
```vue
<template>
<UserSelect
v-model:visible="showSelector"
mode="add"
title="选择用户"
:use-pagination="true"
:fetch-api="userApi.getUserPage"
:filter-params="filterParams"
:page-size="20"
:loading="saving"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { UserSelect } from '@/components';
import { userApi } from '@/apis/system';
import type { UserVO } from '@/types';
const showSelector = ref(false);
const saving = ref(false);
const filterParams = ref({
// 可以添加额外的过滤条件
status: 0
});
async function handleConfirm(selectedUsers: UserVO[]) {
saving.value = true;
try {
// 处理业务逻辑
for (const user of selectedUsers) {
await someApi.addUser(user.id);
}
showSelector.value = false;
} finally {
saving.value = false;
}
}
</script>
```
### 方式三:混合模式(添加模式用分页,删除模式用静态)
```vue
<template>
<UserSelect
v-model:visible="showSelector"
:mode="selectorMode"
:title="selectorMode === 'add' ? '添加人员' : '删除人员'"
:available-users="selectorMode === 'remove' ? currentUsers : []"
:use-pagination="selectorMode === 'add'"
:fetch-api="selectorMode === 'add' ? userApi.getUserPage : undefined"
:filter-params="filterParams"
@confirm="handleConfirm"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { UserSelect } from '@/components';
import { userApi } from '@/apis/system';
import type { UserVO } from '@/types';
const showSelector = ref(false);
const selectorMode = ref<'add' | 'remove'>('add');
const currentUsers = ref<UserVO[]>([]);
const filterParams = ref({});
function handleConfirm(selectedUsers: UserVO[]) {
if (selectorMode.value === 'add') {
// 添加用户逻辑
} else {
// 删除用户逻辑
}
}
</script>
```
## API 方法要求
使用 `usePagination` 时,`fetchApi` 方法需要符合以下签名:
```typescript
async function fetchApi(
pageParam: PageParam,
filter?: any
): Promise<ResultDomain<UserVO>>
```
### PageParam 类型
```typescript
interface PageParam {
page: number; // 当前页码从1开始
size: number; // 每页数量
}
```
### ResultDomain 类型
```typescript
interface ResultDomain<T> {
success: boolean;
message?: string;
dataList?: T[];
pageParam?: {
totalElements: number; // 总记录数
// ... 其他分页信息
};
}
```
## 特性说明
### 滚动分页加载
- 当启用 `usePagination` 时,左侧面板支持滚动加载更多数据
- 滚动到距离底部 50px 时自动加载下一页
- 显示加载状态和"已加载全部数据"提示
- 搜索时自动重置分页并重新加载
### 搜索功能
- **分页模式**:搜索关键词会传递给 API在服务端进行过滤
- **静态模式**:搜索在前端进行过滤
### 数据过滤
组件会自动过滤掉右侧已选择的用户,避免重复选择。
## 性能优化建议
1. **数据量 < 100**:使用静态数据模式
2. **数据量 > 100**:使用分页加载模式
3. **数据量 > 1000**:使用分页加载 + 服务端搜索
## 注意事项
1. 使用分页模式时,`availableUsers` 会被忽略
2. 删除模式下通常使用静态数据(当前已分配的用户)
3. `filterParams` 支持传入额外的过滤条件,会合并到 API 请求中

View File

@@ -1,781 +0,0 @@
<template>
<div v-if="visible" class="modal-overlay" @click.self="handleCancel">
<div class="modal-content large">
<div class="modal-header">
<h3 class="modal-title">{{ title }}</h3>
<button class="modal-close" @click="handleCancel"></button>
</div>
<div class="modal-body">
<div class="user-selector">
<!-- 左侧可选人员 -->
<div class="selector-panel">
<div class="panel-header">
<h4 class="panel-title">{{ leftTitle }}</h4>
<span class="panel-count">
{{ usePagination && totalElements > 0 ? `${availableList.length}/${totalElements}` : `${availableList.length}` }}
</span>
</div>
<div class="panel-search">
<input
v-model="searchAvailable"
type="text"
placeholder="搜索人员..."
class="search-input-small"
/>
</div>
<div class="panel-body left-panel">
<div class="user-list">
<div
v-for="user in filteredAvailable"
:key="user.id"
class="user-item"
:class="{ selected: user.id && selectedAvailable.includes(user.id) }"
@click="user.id && toggleAvailable(user.id)"
@dblclick="user.id && moveToTarget(user.id)"
>
<input
type="checkbox"
:checked="!!(user.id && selectedAvailable.includes(user.id))"
@click.stop="user.id && toggleAvailable(user.id)"
/>
<span class="user-name">{{ user.username }}</span>
<span class="user-dept" v-if="user.deptName">({{ user.deptName }})</span>
</div>
<!-- 加载更多提示 -->
<div v-if="usePagination && loadingMore" class="loading-more">
<div class="loading-spinner-small"></div>
<span>加载中...</span>
</div>
<div v-if="usePagination && !hasMore && availableList.length > 0" class="no-more">
已加载全部数据
</div>
</div>
</div>
</div>
<!-- 中间操作按钮 -->
<div class="selector-actions">
<button
class="action-btn"
@click="moveSelectedToTarget"
:disabled="selectedAvailable.length === 0"
:title="mode === 'add' ? '添加选中' : '删除选中'"
>
<span class="arrow"></span>
<span class="btn-text">{{ mode === 'add' ? '添加' : '删除' }}</span>
</button>
<button
class="action-btn"
@click="moveAllToTarget"
:disabled="availableList.length === 0"
:title="mode === 'add' ? '全部添加' : '全部删除'"
>
<span class="arrow"></span>
<span class="btn-text">全部</span>
</button>
<button
class="action-btn"
@click="moveBackSelected"
:disabled="selectedTarget.length === 0"
title="移回选中"
>
<span class="arrow"></span>
<span class="btn-text">移回</span>
</button>
<button
class="action-btn"
@click="moveBackAll"
:disabled="targetList.length === 0"
title="全部移回"
>
<span class="arrow"></span>
<span class="btn-text">全部</span>
</button>
</div>
<!-- 右侧目标人员 -->
<div class="selector-panel">
<div class="panel-header">
<h4 class="panel-title">{{ rightTitle }}</h4>
<span class="panel-count">{{ targetList.length }} </span>
</div>
<div class="panel-search">
<input
v-model="searchTarget"
type="text"
placeholder="搜索人员..."
class="search-input-small"
/>
</div>
<div class="panel-body">
<div class="user-list">
<div
v-for="user in filteredTarget"
:key="user.id"
class="user-item"
:class="{ selected: user.id && selectedTarget.includes(user.id) }"
@click="user.id && toggleTarget(user.id)"
@dblclick="user.id && moveBackToAvailable(user.id)"
>
<input
type="checkbox"
:checked="!!(user.id && selectedTarget.includes(user.id))"
@click.stop="user.id && toggleTarget(user.id)"
/>
<span class="user-name">{{ user.username }}</span>
<span class="user-dept" v-if="user.deptName">({{ user.deptName }})</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-primary" @click="handleConfirm" :disabled="loading">
{{ loading ? '处理中...' : '确定' }}
</button>
<button class="btn-default" @click="handleCancel">取消</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue';
import type { UserVO, ResultDomain, PageParam } from '@/types';
interface Props {
visible?: boolean;
mode?: 'add' | 'remove';
title?: string;
leftTitle?: string;
rightTitle?: string;
availableUsers?: UserVO[];
initialTargetUsers?: UserVO[];
loading?: boolean;
// 分页加载API方法
fetchApi?: (pageParam: PageParam, filter?: any) => Promise<ResultDomain<UserVO>>;
// 过滤参数
filterParams?: any;
// 每页数量
pageSize?: number;
// 是否使用分页加载
usePagination?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
mode: 'add',
title: '人员选择',
leftTitle: '可选人员',
rightTitle: '已选人员',
availableUsers: () => [],
initialTargetUsers: () => [],
loading: false,
pageSize: 20,
usePagination: false
});
const emit = defineEmits<{
'update:visible': [value: boolean];
confirm: [users: UserVO[]];
cancel: [];
}>();
// 数据列表
const availableList = ref<UserVO[]>([]);
const targetList = ref<UserVO[]>([]);
// 选中状态
const selectedAvailable = ref<string[]>([]);
const selectedTarget = ref<string[]>([]);
// 搜索关键词
const searchAvailable = ref('');
const searchTarget = ref('');
// 分页相关
const currentPage = ref(1);
const totalElements = ref(0);
const hasMore = ref(true);
const loadingMore = ref(false);
const availablePanelRef = ref<HTMLElement | null>(null);
// 监听props变化初始化数据
watch(() => props.visible, (newVal) => {
if (newVal) {
initializeData();
} else {
resetData();
}
}, { immediate: true });
// 监听搜索关键词变化
watch(searchAvailable, () => {
if (props.usePagination && props.fetchApi) {
resetPaginationAndLoad();
}
});
// 初始化数据
async function initializeData() {
if (props.usePagination && props.fetchApi) {
// 使用分页加载
currentPage.value = 1;
hasMore.value = true;
availableList.value = [];
await loadAvailableUsers();
} else {
// 使用传入的数据
availableList.value = [...props.availableUsers];
}
targetList.value = [...props.initialTargetUsers];
selectedAvailable.value = [];
selectedTarget.value = [];
searchAvailable.value = '';
searchTarget.value = '';
// 绑定滚动事件
await nextTick();
bindScrollEvent();
}
// 重置数据
function resetData() {
availableList.value = [];
targetList.value = [];
selectedAvailable.value = [];
selectedTarget.value = [];
searchAvailable.value = '';
searchTarget.value = '';
currentPage.value = 1;
totalElements.value = 0;
hasMore.value = true;
loadingMore.value = false;
// 解绑滚动事件
unbindScrollEvent();
}
// 加载可选用户数据
async function loadAvailableUsers() {
if (!props.fetchApi || loadingMore.value || !hasMore.value) return;
loadingMore.value = true;
try {
const pageParam: PageParam = {
page: currentPage.value,
size: props.pageSize
};
// 构建过滤参数
const filter = {
...props.filterParams,
...(searchAvailable.value ? { username: searchAvailable.value } : {})
};
const res = await props.fetchApi(pageParam, filter);
if (res.success) {
const newUsers = res.dataList || [];
// 过滤掉已在右侧的用户
const targetUserIds = targetList.value.map(u => u.id);
const filteredUsers = newUsers.filter(u => !targetUserIds.includes(u.id));
if (currentPage.value === 1) {
availableList.value = filteredUsers;
} else {
availableList.value.push(...filteredUsers);
}
totalElements.value = res.pageParam?.totalElements || 0;
const totalPages = Math.ceil(totalElements.value / props.pageSize);
hasMore.value = currentPage.value < totalPages;
}
} catch (error) {
console.error('加载用户数据失败:', error);
} finally {
loadingMore.value = false;
}
}
// 重置分页并重新加载
async function resetPaginationAndLoad() {
currentPage.value = 1;
hasMore.value = true;
availableList.value = [];
await loadAvailableUsers();
}
// 绑定滚动事件
function bindScrollEvent() {
const panelBody = document.querySelector('.panel-body.left-panel');
if (panelBody) {
availablePanelRef.value = panelBody as HTMLElement;
panelBody.addEventListener('scroll', handleScroll);
}
}
// 解绑滚动事件
function unbindScrollEvent() {
if (availablePanelRef.value) {
availablePanelRef.value.removeEventListener('scroll', handleScroll);
availablePanelRef.value = null;
}
}
// 处理滚动事件
function handleScroll(event: Event) {
if (!props.usePagination || !props.fetchApi) return;
const target = event.target as HTMLElement;
const scrollTop = target.scrollTop;
const scrollHeight = target.scrollHeight;
const clientHeight = target.clientHeight;
// 距离底部50px时加载更多
if (scrollHeight - scrollTop - clientHeight < 50 && hasMore.value && !loadingMore.value) {
currentPage.value++;
loadAvailableUsers();
}
}
// 过滤可选人员
const filteredAvailable = computed(() => {
// 如果使用分页加载搜索在API层面处理不需要前端过滤
if (props.usePagination) {
return availableList.value;
}
// 前端过滤
if (!searchAvailable.value) {
return availableList.value;
}
const keyword = searchAvailable.value.toLowerCase();
return availableList.value.filter(user =>
user.username?.toLowerCase().includes(keyword) ||
user.deptName?.toLowerCase().includes(keyword)
);
});
// 过滤目标人员
const filteredTarget = computed(() => {
if (!searchTarget.value) {
return targetList.value;
}
const keyword = searchTarget.value.toLowerCase();
return targetList.value.filter(user =>
user.username?.toLowerCase().includes(keyword) ||
user.deptName?.toLowerCase().includes(keyword)
);
});
// 切换可选用户的选中状态
function toggleAvailable(userId: string) {
const index = selectedAvailable.value.indexOf(userId);
if (index > -1) {
selectedAvailable.value.splice(index, 1);
} else {
selectedAvailable.value.push(userId);
}
}
// 切换目标用户的选中状态
function toggleTarget(userId: string) {
const index = selectedTarget.value.indexOf(userId);
if (index > -1) {
selectedTarget.value.splice(index, 1);
} else {
selectedTarget.value.push(userId);
}
}
// 移动选中用户到目标区域
function moveSelectedToTarget() {
const usersToMove = availableList.value.filter(user =>
selectedAvailable.value.includes(user.id!)
);
targetList.value.push(...usersToMove);
availableList.value = availableList.value.filter(user =>
!selectedAvailable.value.includes(user.id!)
);
selectedAvailable.value = [];
}
// 移动单个用户到目标区域(双击)
function moveToTarget(userId: string) {
const user = availableList.value.find(u => u.id === userId);
if (user) {
targetList.value.push(user);
availableList.value = availableList.value.filter(u => u.id !== userId);
}
}
// 移动所有用户到目标区域
function moveAllToTarget() {
targetList.value.push(...availableList.value);
availableList.value = [];
selectedAvailable.value = [];
}
// 移回选中用户到可选区域
function moveBackSelected() {
const usersToMoveBack = targetList.value.filter(user =>
selectedTarget.value.includes(user.id!)
);
availableList.value.push(...usersToMoveBack);
targetList.value = targetList.value.filter(user =>
!selectedTarget.value.includes(user.id!)
);
selectedTarget.value = [];
}
// 移回单个用户到可选区域(双击)
function moveBackToAvailable(userId: string) {
const user = targetList.value.find(u => u.id === userId);
if (user) {
availableList.value.push(user);
targetList.value = targetList.value.filter(u => u.id !== userId);
}
}
// 移回所有用户到可选区域
function moveBackAll() {
availableList.value.push(...targetList.value);
targetList.value = [];
selectedTarget.value = [];
}
// 确认
function handleConfirm() {
emit('confirm', targetList.value);
}
// 取消
function handleCancel() {
emit('update:visible', false);
emit('cancel');
}
</script>
<style lang="scss" scoped>
// 弹窗遮罩
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: #fff;
border-radius: 8px;
width: 90%;
max-width: 900px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin: 0;
}
.modal-close {
width: 32px;
height: 32px;
border: none;
background: #f5f7fa;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
color: #909399;
transition: all 0.3s;
&:hover {
background: #ecf5ff;
color: #409eff;
}
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 12px;
}
// 人员选择器样式
.user-selector {
display: flex;
gap: 20px;
height: 500px;
}
.selector-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #e0e0e0;
border-radius: 6px;
overflow: hidden;
background: #fafafa;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f5f7fa;
border-bottom: 1px solid #e0e0e0;
}
.panel-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin: 0;
}
.panel-count {
font-size: 13px;
color: #909399;
background: #fff;
padding: 2px 10px;
border-radius: 10px;
}
.panel-search {
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
background: #fff;
}
.search-input-small {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
&:focus {
outline: none;
border-color: #409eff;
}
&::placeholder {
color: #c0c4cc;
}
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 8px;
background: #fff;
}
.user-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.user-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
user-select: none;
&:hover {
background: #f5f7fa;
}
&.selected {
background: #ecf5ff;
border: 1px solid #b3d8ff;
}
input[type="checkbox"] {
cursor: pointer;
width: 16px;
height: 16px;
}
.user-name {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.user-dept {
font-size: 12px;
color: #909399;
margin-left: auto;
}
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
color: #909399;
font-size: 13px;
}
.loading-spinner-small {
width: 16px;
height: 16px;
border: 2px solid #f0f0f0;
border-top-color: #409eff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.no-more {
text-align: center;
padding: 12px;
color: #c0c4cc;
font-size: 12px;
}
.selector-actions {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
padding: 0 10px;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 16px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
min-width: 80px;
&:hover:not(:disabled) {
background: #409eff;
border-color: #409eff;
color: #fff;
.arrow {
color: #fff;
}
.btn-text {
color: #fff;
}
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
background: #f5f7fa;
}
.arrow {
font-size: 20px;
font-weight: bold;
color: #409eff;
transition: color 0.3s;
}
.btn-text {
font-size: 13px;
color: #606266;
transition: color 0.3s;
}
}
.btn-primary,
.btn-default {
padding: 10px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: none;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.btn-primary {
background: #409eff;
color: #fff;
&:hover:not(:disabled) {
background: #66b1ff;
}
}
.btn-default {
background: #fff;
color: #606266;
border: 1px solid #dcdfe6;
&:hover {
color: #409eff;
border-color: #409eff;
}
}
</style>

View File

@@ -1,8 +0,0 @@
/**
* @description 用户相关组件
* @author yslg
* @since 2025-10-22
*/
export { default as UserSelect } from './UserSelect.vue';

View File

@@ -51,6 +51,6 @@ export const APP_CONFIG = {
refreshThreshold: 5 * 60 * 1000 // 提前5分钟刷新
}
};
export const PUBLIC_IMG_PATH = '/schoolNewsWeb/img';
export const PUBLIC_IMG_PATH = 'http://localhost:8080/schoolNewsWeb/img';
export default APP_CONFIG;

View File

@@ -6,6 +6,7 @@
import { BaseDTO } from '../base';
import { SysRole } from '../role';
import { SysUserDeptRole } from '../user';
/**
* 系统部门
@@ -15,6 +16,8 @@ export interface SysDept extends BaseDTO {
deptID?: string;
/** 父部门ID */
parentID?: string;
/** 部门路径,格式:/root_department/dept_001/ */
deptPath?: string;
/** 部门名称 */
name?: string;
/** 部门描述 */
@@ -30,13 +33,35 @@ export interface SysDept extends BaseDTO {
/**
* 部门角色VO
*/
export interface DeptRoleVO {
/** 部门信息 */
dept?: SysDept;
/** 角色信息 */
role?: SysRole;
export interface UserDeptRoleVO {
depts?: SysDept[];
roles?: SysRole[];
userDeptRoles?: SysUserDeptRole[];
// 扁平化字段,用于权限查询优化
/** 用户ID */
userID?: string;
/** 部门ID */
deptID?: string;
/** 部门名称 */
deptName?: string;
/** 部门描述 */
deptDescription?: string;
/** 父部门ID */
parentID?: string;
/** 父部门名称 */
parentName?: string;
/** 父部门描述 */
parentDescription?: string;
/** 角色ID */
roleID?: string;
/** 角色名称 */
roleName?: string;
/** 角色描述 */
roleDescription?: string;
/** 部门路径,用于快速权限继承判断 */
deptPath?: string;
}
/**
@@ -48,5 +73,19 @@ export interface SysDeptRole extends BaseDTO {
deptID?: string;
/** 角色ID */
roleID?: string;
// 扁平化字段(用于前端展示,从关联查询中获取)
/** 部门名称 */
deptName?: string;
/** 部门描述 */
deptDescription?: string;
/** 父部门ID */
parentID?: string;
/** 部门路径 */
deptPath?: string;
/** 角色名称 */
roleName?: string;
/** 角色描述 */
roleDescription?: string;
}

View File

@@ -94,16 +94,3 @@ export interface SysUserDeptRole extends BaseDTO {
/** 角色ID */
roleID?: string;
}
export interface UserDeptRoleVO extends BaseDTO {
/** 单个用户信息 */
user?: SysUser;
/** 用户列表 */
users?: SysUser[];
/** 部门列表 */
depts?: SysDept[];
/** 角色列表 */
roles?: SysRole[];
/** 用户部门角色关联列表 */
userDeptRoles?: SysUserDeptRole[];
}

View File

@@ -6,7 +6,6 @@
<el-tabs v-model="activeTab">
<el-tab-pane label="学习记录" name="task-records">
<StudyRecords />
</el-tab-pane>
</el-tabs>
</div>
@@ -17,7 +16,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElTabs, ElTabPane } from 'element-plus';
import StudyRecords from './components/StudyRecords.vue';
import { AdminLayout } from '@/views/admin';
defineOptions({
name: 'StudyManagementView'

View File

@@ -112,60 +112,19 @@
</el-dialog>
<!-- 绑定角色对话框 -->
<el-dialog v-model="bindRoleDialogVisible" title="绑定角色" width="800px" @close="resetBindList">
<div class="role-binding-container">
<!-- 部门信息显示 -->
<div class="dept-info" v-if="currentDept">
<h4>部门信息{{ currentDept.name }}</h4>
<p>部门ID{{ currentDept.deptID }}</p>
</div>
<!-- 角色绑定状态表格 -->
<el-table :data="roleList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isRoleSelected(row.roleID) ? 'success' : 'info'"
size="small"
>
{{ isRoleSelected(row.roleID) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="角色名称" min-width="150" />
<el-table-column prop="roleID" label="角色ID" min-width="120" />
<el-table-column prop="description" label="角色描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isRoleSelected(row.roleID) ? 'danger' : 'primary'"
size="small"
@click="toggleRoleSelection(row)"
>
{{ isRoleSelected(row.roleID) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 统计信息 -->
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedRoles.length} 个角色,未绑定 ${roleList.length - selectedRoles.length} 个角色`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindRoleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveRoleBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
<GenericSelector
v-model:visible="bindRoleDialogVisible"
:title="`绑定角色 - ${currentDept?.name || ''}`"
left-title="可选角色"
right-title="已选角色"
:fetch-available-api="fetchAllRoles"
:fetch-selected-api="fetchDeptRoles"
:item-config="{ id: 'roleID', label: 'name', sublabel: 'description' }"
unit-name=""
search-placeholder="搜索角色名称或描述..."
@confirm="handleRoleConfirm"
@cancel="resetBindList"
/>
</div>
</AdminLayout>
</template>
@@ -175,6 +134,7 @@ import { deptApi } from '@/apis/system/dept';
import { roleApi } from '@/apis/system/role';
import { SysDept, SysRole } from '@/types';
import { AdminLayout } from '@/views/admin';
import { GenericSelector } from '@/components/base';
defineOptions({
name: 'DeptManageView'
@@ -190,12 +150,7 @@ const submitting = ref(false);
const treeRef = ref();
// 角色绑定相关数据
const roleList = ref<SysRole[]>([]);
const selectedRoles = ref<string[]>([]);
const currentDept = ref<SysDept | null>(null);
const bindList = ref<{ roles: SysRole[] }>({
roles: []
});
// 对话框状态
const dialogVisible = ref(false);
@@ -452,59 +407,39 @@ function resetForm() {
});
}
// 获取所有可选角色的接口
async function fetchAllRoles() {
return await roleApi.getAllRoles();
}
// 获取部门已绑定角色的接口
async function fetchDeptRoles() {
if (!currentDept.value) {
return {
success: true,
dataList: [],
code: 200,
message: '',
login: true,
auth: true
};
}
return await deptApi.getDeptByRole(currentDept.value);
}
// 查看绑定角色
async function handleBindRole(row: SysDept) {
currentDept.value = row;
try {
// 获取所有角色
const roleResult = await roleApi.getAllRoles();
roleList.value = roleResult.dataList || [];
// 获取已绑定的角色
const bindingResult = await deptApi.getDeptByRole(row);
bindList.value.roles = bindingResult.dataList || [];
// 设置已选中的角色
selectedRoles.value = bindList.value.roles.map(role => role.roleID).filter((id): id is string => !!id);
console.log('已绑定的角色:', bindList.value.roles);
console.log('所有角色:', roleList.value);
bindRoleDialogVisible.value = true;
} catch (error) {
console.error('获取角色绑定信息失败:', error);
ElMessage.error('获取角色绑定信息失败');
}
bindRoleDialogVisible.value = true;
}
// 重置绑定列表
function resetBindList() {
bindList.value = {
roles: []
};
selectedRoles.value = [];
currentDept.value = null;
}
// 检查角色是否已选中
function isRoleSelected(roleID: string | undefined): boolean {
return roleID ? selectedRoles.value.includes(roleID) : false;
}
// 切换角色选择状态
function toggleRoleSelection(role: SysRole) {
if (!role.roleID) return;
const index = selectedRoles.value.indexOf(role.roleID);
if (index > -1) {
selectedRoles.value.splice(index, 1);
} else {
selectedRoles.value.push(role.roleID);
}
}
// 保存角色绑定
async function saveRoleBinding() {
// 角色选择确认 - 在confirm时提交请求
async function handleRoleConfirm(items: SysRole[]) {
if (!currentDept.value || !currentDept.value.deptID) {
ElMessage.error('部门信息不完整');
return;
@@ -513,21 +448,22 @@ async function saveRoleBinding() {
try {
submitting.value = true;
// 获取当前已绑定的角色ID
const currentBoundRoles = (bindList.value.roles || []).map(role => role.roleID).filter((id): id is string => !!id);
// 获取当前已绑定的角色
const currentBoundResult = await deptApi.getDeptByRole(currentDept.value);
const currentBoundIds = (currentBoundResult.dataList || []).map(r => r.roleID).filter((id): id is string => !!id);
// 新选择的角色ID
const newSelectedIds = items.map(r => r.roleID).filter((id): id is string => !!id);
// 找出需要绑定的角色(新增的)
const rolesToBind = selectedRoles.value.filter(roleID => !currentBoundRoles.includes(roleID));
const rolesToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id));
// 找出需要解绑的角色(移除的)
const rolesToUnbind = currentBoundRoles.filter(roleID => !selectedRoles.value.includes(roleID));
const rolesToUnbind = currentBoundIds.filter(id => !newSelectedIds.includes(id));
// 构建需要绑定的角色对象数组
if (rolesToBind.length > 0) {
const rolesToBindObjects = rolesToBind.map(roleID => {
const role = roleList.value.find(r => r.roleID === roleID);
return role || { roleID: roleID };
});
const rolesToBindObjects = items.filter(r => r.roleID && rolesToBind.includes(r.roleID));
const bindDept = {
dept: currentDept.value,
@@ -539,10 +475,7 @@ async function saveRoleBinding() {
// 构建需要解绑的角色对象数组
if (rolesToUnbind.length > 0) {
const rolesToUnbindObjects = rolesToUnbind.map(roleID => {
const role = roleList.value.find(r => r.roleID === roleID);
return role || { roleID: roleID };
});
const rolesToUnbindObjects = (currentBoundResult.dataList || []).filter(r => r.roleID && rolesToUnbind.includes(r.roleID));
const unbindDept = {
dept: currentDept.value,
@@ -553,7 +486,6 @@ async function saveRoleBinding() {
}
ElMessage.success('角色绑定保存成功');
bindRoleDialogVisible.value = false;
// 刷新部门列表
await loadDeptList();
@@ -799,30 +731,4 @@ async function handleNodeDrop(draggingNode: any, dropNode: any, dropType: string
}
}
}
// 角色绑定容器样式
.role-binding-container {
.dept-info {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
p {
margin: 0;
color: #606266;
font-size: 14px;
}
}
.binding-stats {
margin-top: 20px;
}
}
</style>

View File

@@ -30,9 +30,12 @@
<template #default="{ data }">
<div class="custom-tree-node">
<div class="node-label">
<el-icon v-if="data.icon" class="node-icon">
<component :is="data.icon" />
</el-icon>
<img
v-if="data.icon"
:src="PUBLIC_IMG_PATH + '/' + data.icon"
class="node-icon"
:alt="data.name"
/>
<span class="node-name">{{ data.name }}</span>
<el-tag
:type="getMenuTypeTagType(data.type)"
@@ -156,61 +159,19 @@
</el-dialog>
<!-- 绑定权限对话框 -->
<el-dialog v-model="bindPermissionDialogVisible" title="绑定权限" width="800px" @close="resetBindList">
<div class="permission-binding-container">
<!-- 菜单信息显示 -->
<div class="menu-info" v-if="currentMenu">
<h4>菜单信息{{ currentMenu.name }}</h4>
<p>菜单ID{{ currentMenu.menuID }}</p>
</div>
<!-- 权限绑定状态表格 -->
<el-table :data="permissionList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isPermissionSelected(row.permissionID) ? 'success' : 'info'"
size="small"
>
{{ isPermissionSelected(row.permissionID) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="权限名称" min-width="150" />
<el-table-column prop="permissionID" label="权限ID" min-width="120" />
<el-table-column prop="code" label="权限编码" min-width="150" />
<el-table-column prop="description" label="权限描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isPermissionSelected(row.permissionID) ? 'danger' : 'primary'"
size="small"
@click="togglePermissionSelection(row)"
>
{{ isPermissionSelected(row.permissionID) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 统计信息 -->
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedPermissions.length} 个权限,未绑定 ${permissionList.length - selectedPermissions.length} 个权限`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindPermissionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="savePermissionBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
<GenericSelector
v-model:visible="bindPermissionDialogVisible"
:title="`绑定权限 - ${currentMenu?.name || ''}`"
left-title="可选权限"
right-title="已选权限"
:fetch-available-api="fetchAllPermissions"
:fetch-selected-api="fetchMenuPermissions"
:item-config="{ id: 'permissionID', label: 'name', sublabel: 'code' }"
unit-name=""
search-placeholder="搜索权限名称或编码..."
@confirm="handlePermissionConfirm"
@cancel="resetBindList"
/>
</div>
</AdminLayout>
</template>
@@ -220,6 +181,8 @@ import { menuApi } from '@/apis/system/menu';
import { permissionApi } from '@/apis/system/permission';
import { SysMenu, SysPermission } from '@/types';
import { AdminLayout } from '@/views/admin';
import { PUBLIC_IMG_PATH } from '@/config';
import { GenericSelector } from '@/components/base';
defineOptions({
name: 'MenuManageView'
@@ -235,12 +198,7 @@ const submitting = ref(false);
const treeRef = ref();
// 权限绑定相关数据
const permissionList = ref<SysPermission[]>([]);
const selectedPermissions = ref<string[]>([]);
const currentMenu = ref<SysMenu | null>(null);
const bindList = ref<{ permissions: SysPermission[] }>({
permissions: []
});
// 对话框状态
const dialogVisible = ref(false);
@@ -533,65 +491,38 @@ function resetForm() {
});
}
// 获取所有可选权限的接口
async function fetchAllPermissions() {
const permission: SysPermission = {
permissionID: undefined,
name: undefined,
code: undefined,
description: undefined,
};
return await permissionApi.getPermissionList(permission);
}
// 获取菜单已绑定权限的接口
async function fetchMenuPermissions() {
if (!currentMenu.value?.menuID) {
return { success: true, dataList: [] };
}
return await menuApi.getMenuPermission(currentMenu.value.menuID);
}
// 查看绑定权限
async function handleBindPermission(row: SysMenu) {
currentMenu.value = row;
try {
// 获取所有权限
let permission:SysPermission = {
permissionID: undefined,
name: undefined,
code: undefined,
description: undefined,
};
const permissionResult = await permissionApi.getPermissionList(permission);
permissionList.value = permissionResult.dataList || [];
// 获取已绑定的权限
const bindingResult = await menuApi.getMenuPermission(row.menuID!);
bindList.value.permissions = bindingResult.dataList || [];
// 设置已选中的权限
selectedPermissions.value = bindList.value.permissions.map(permission => permission.permissionID).filter((id): id is string => !!id);
console.log('已绑定的权限:', bindList.value.permissions);
console.log('所有权限:', permissionList.value);
bindPermissionDialogVisible.value = true;
} catch (error) {
console.error('获取权限绑定信息失败:', error);
ElMessage.error('获取权限绑定信息失败');
}
bindPermissionDialogVisible.value = true;
}
// 重置绑定列表
function resetBindList() {
bindList.value = {
permissions: []
};
selectedPermissions.value = [];
currentMenu.value = null;
}
// 检查权限是否已选中
function isPermissionSelected(permissionID: string | undefined): boolean {
return permissionID ? selectedPermissions.value.includes(permissionID) : false;
}
// 切换权限选择状态
function togglePermissionSelection(permission: SysPermission) {
if (!permission.permissionID) return;
const index = selectedPermissions.value.indexOf(permission.permissionID);
if (index > -1) {
selectedPermissions.value.splice(index, 1);
} else {
selectedPermissions.value.push(permission.permissionID);
}
}
// 保存权限绑定
async function savePermissionBinding() {
// 权限选择确认 - 在confirm时提交请求
async function handlePermissionConfirm(items: SysPermission[]) {
if (!currentMenu.value || !currentMenu.value.menuID) {
ElMessage.error('菜单信息不完整');
return;
@@ -600,21 +531,22 @@ async function savePermissionBinding() {
try {
submitting.value = true;
// 获取当前已绑定的权限ID
const currentBoundPermissions = (bindList.value.permissions || []).map(permission => permission.permissionID).filter((id): id is string => !!id);
// 获取当前已绑定的权限
const currentBoundResult = await menuApi.getMenuPermission(currentMenu.value.menuID);
const currentBoundIds = (currentBoundResult.dataList || []).map(p => p.permissionID).filter((id): id is string => !!id);
// 新选择的权限ID
const newSelectedIds = items.map(p => p.permissionID).filter((id): id is string => !!id);
// 找出需要绑定的权限(新增的)
const permissionsToBind = selectedPermissions.value.filter(permissionID => !currentBoundPermissions.includes(permissionID));
const permissionsToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id));
// 找出需要解绑的权限(移除的)
const permissionsToUnbind = currentBoundPermissions.filter(permissionID => !selectedPermissions.value.includes(permissionID));
const permissionsToUnbind = currentBoundIds.filter(id => !newSelectedIds.includes(id));
// 构建需要绑定的权限对象数组
if (permissionsToBind.length > 0) {
const permissionsToBindObjects = permissionsToBind.map(permissionID => {
const permission = permissionList.value.find(p => p.permissionID === permissionID);
return permission || { permissionID: permissionID };
});
const permissionsToBindObjects = items.filter(p => p.permissionID && permissionsToBind.includes(p.permissionID));
const bindMenu = {
...currentMenu.value,
@@ -626,10 +558,7 @@ async function savePermissionBinding() {
// 构建需要解绑的权限对象数组
if (permissionsToUnbind.length > 0) {
const permissionsToUnbindObjects = permissionsToUnbind.map(permissionID => {
const permission = permissionList.value.find(p => p.permissionID === permissionID);
return permission || { permissionID: permissionID };
});
const permissionsToUnbindObjects = (currentBoundResult.dataList || []).filter(p => p.permissionID && permissionsToUnbind.includes(p.permissionID));
const unbindMenu = {
...currentMenu.value,
@@ -640,7 +569,6 @@ async function savePermissionBinding() {
}
ElMessage.success('权限绑定保存成功');
bindPermissionDialogVisible.value = false;
// 刷新菜单列表
await loadMenuList();
@@ -742,8 +670,9 @@ async function handleNodeDrop(draggingNode: any, dropNode: any, dropType: string
.node-icon {
margin-right: 8px;
color: #409EFF;
font-size: 16px;
width: 16px;
height: 16px;
object-fit: contain;
}
.node-name {
@@ -890,30 +819,4 @@ async function handleNodeDrop(draggingNode: any, dropNode: any, dropType: string
}
}
}
// 权限绑定容器样式
.permission-binding-container {
.menu-info {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
p {
margin: 0;
color: #606266;
font-size: 14px;
}
}
.binding-stats {
margin-top: 20px;
}
}
</style>

View File

@@ -279,120 +279,34 @@
</el-dialog>
<!-- 绑定菜单对话框 -->
<el-dialog
v-model="bindMenuDialogVisible"
title="绑定菜单"
width="800px"
@close="resetBindList"
>
<div class="menu-binding-container">
<div class="permission-info" v-if="currentPermission">
<h4>权限信息{{ currentPermission.name }}</h4>
<p>权限编码{{ currentPermission.code }}</p>
</div>
<el-table :data="menuList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isMenuSelected(row.menuID) ? 'success' : 'info'"
size="small"
>
{{ isMenuSelected(row.menuID) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="菜单名称" min-width="150" />
<el-table-column prop="menuID" label="菜单ID" min-width="120" />
<el-table-column prop="url" label="菜单路径" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isMenuSelected(row.menuID) ? 'danger' : 'primary'"
size="small"
@click="toggleMenuSelection(row)"
>
{{ isMenuSelected(row.menuID) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedMenus.length} 个菜单`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindMenuDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveMenuBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
<GenericSelector
v-model:visible="bindMenuDialogVisible"
:title="`绑定菜单 - ${currentPermission?.name || ''}`"
left-title="可选菜单"
right-title="已选菜单"
:fetch-available-api="fetchAllMenus"
:fetch-selected-api="fetchPermissionMenus"
:item-config="{ id: 'menuID', label: 'name', sublabel: 'url' }"
unit-name=""
search-placeholder="搜索菜单名称..."
@confirm="handleMenuConfirm"
@cancel="resetBindList"
/>
<!-- 绑定角色对话框 -->
<el-dialog
v-model="bindRoleDialogVisible"
title="绑定角色"
width="800px"
@close="resetBindList"
>
<div class="role-binding-container">
<div class="permission-info" v-if="currentPermission">
<h4>权限信息{{ currentPermission.name }}</h4>
<p>权限编码{{ currentPermission.code }}</p>
</div>
<el-table :data="roleList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isRoleSelected(row.roleID) ? 'success' : 'info'"
size="small"
>
{{ isRoleSelected(row.roleID) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="角色名称" min-width="150" />
<el-table-column prop="roleID" label="角色ID" min-width="120" />
<el-table-column prop="description" label="角色描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isRoleSelected(row.roleID) ? 'danger' : 'primary'"
size="small"
@click="toggleRoleSelection(row)"
>
{{ isRoleSelected(row.roleID) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedRoles.length} 个角色`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindRoleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveRoleBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
<GenericSelector
v-model:visible="bindRoleDialogVisible"
:title="`绑定角色 - ${currentPermission?.name || ''}`"
left-title="可选角色"
right-title="已选角色"
:fetch-available-api="fetchAllRoles"
:fetch-selected-api="fetchPermissionRoles"
:item-config="{ id: 'roleID', label: 'name', sublabel: 'description' }"
unit-name=""
search-placeholder="搜索角色名称..."
@confirm="handleRoleConfirm"
@cancel="resetBindList"
/>
</div>
</AdminLayout>
</template>
@@ -407,6 +321,7 @@ import { menuApi } from '@/apis/system/menu';
import { roleApi } from '@/apis/system/role';
import type { SysModule, SysPermission, SysMenu, SysRole } from '@/types';
import { AdminLayout } from '@/views/admin';
import { GenericSelector } from '@/components/base';
defineOptions({
name: 'ModulePermissionManageView'
@@ -418,10 +333,6 @@ const permissionLoading = ref(false);
const submitting = ref(false);
const moduleList = ref<SysModule[]>([]);
const permissions = ref<SysPermission[]>([]);
const menuList = ref<SysMenu[]>([]);
const roleList = ref<SysRole[]>([]);
const selectedMenus = ref<string[]>([]);
const selectedRoles = ref<string[]>([]);
// 当前选中的模块和权限
const currentModule = ref<SysModule | null>(null);
@@ -686,80 +597,78 @@ async function handleSubmitPermission() {
}
}
// 获取所有菜单的接口
async function fetchAllMenus() {
return await menuApi.getAllMenuList();
}
// 获取权限已绑定菜单的接口
async function fetchPermissionMenus() {
if (!currentPermission.value) {
return {
success: true,
dataList: [],
code: 200,
message: '',
login: true,
auth: true
};
}
const permission = { ...currentPermission.value, bindType: 'menu' as const };
const result = await permissionApi.getPermissionBindingList(permission);
return {
code: result.code || 200,
message: result.message || '',
success: result.success,
login: result.login ?? true,
auth: result.auth ?? true,
dataList: result.data?.menus || []
};
}
// 获取所有角色的接口
async function fetchAllRoles() {
return await roleApi.getAllRoles();
}
// 获取权限已绑定角色的接口
async function fetchPermissionRoles() {
if (!currentPermission.value) {
return {
success: true,
dataList: [],
code: 200,
message: '',
login: true,
auth: true
};
}
const permission = { ...currentPermission.value, bindType: 'role' as const };
const result = await permissionApi.getPermissionBindingList(permission);
return {
code: result.code || 200,
message: result.message || '',
success: result.success,
login: result.login ?? true,
auth: result.auth ?? true,
dataList: result.data?.roles || []
};
}
// 绑定菜单
async function handleBindMenu(permission: SysPermission) {
currentPermission.value = permission;
permission.bindType = 'menu';
try {
const bindingResult = await permissionApi.getPermissionBindingList(permission);
const bindList = bindingResult.data?.menus || [];
const menuResult = await menuApi.getAllMenuList();
menuList.value = menuResult.dataList || [];
selectedMenus.value = bindList.map(menu => menu.menuID).filter((id): id is string => !!id);
bindMenuDialogVisible.value = true;
} catch (error) {
console.error('获取菜单绑定信息失败:', error);
ElMessage.error('获取菜单绑定信息失败');
}
bindMenuDialogVisible.value = true;
}
// 绑定角色
async function handleBindRole(permission: SysPermission) {
currentPermission.value = permission;
permission.bindType = 'role';
try {
const bindingResult = await permissionApi.getPermissionBindingList(permission);
const bindList = bindingResult.data?.roles || [];
const roleResult = await roleApi.getAllRoles();
roleList.value = roleResult.dataList || [];
selectedRoles.value = bindList.map(role => role.roleID).filter((id): id is string => !!id);
bindRoleDialogVisible.value = true;
} catch (error) {
console.error('获取角色绑定信息失败:', error);
ElMessage.error('获取角色绑定信息失败');
}
bindRoleDialogVisible.value = true;
}
// 菜单选择相关
function isMenuSelected(menuID: string | undefined): boolean {
return menuID ? selectedMenus.value.includes(menuID) : false;
}
function toggleMenuSelection(menu: SysMenu) {
if (!menu.menuID) return;
const index = selectedMenus.value.indexOf(menu.menuID);
if (index > -1) {
selectedMenus.value.splice(index, 1);
} else {
selectedMenus.value.push(menu.menuID);
}
}
// 角色选择相关
function isRoleSelected(roleID: string | undefined): boolean {
return roleID ? selectedRoles.value.includes(roleID) : false;
}
function toggleRoleSelection(role: SysRole) {
if (!role.roleID) return;
const index = selectedRoles.value.indexOf(role.roleID);
if (index > -1) {
selectedRoles.value.splice(index, 1);
} else {
selectedRoles.value.push(role.roleID);
}
}
// 保存菜单绑定
async function saveMenuBinding() {
// 菜单选择确认 - 在confirm时提交请求
async function handleMenuConfirm(items: SysMenu[]) {
if (!currentPermission.value?.permissionID) {
ElMessage.error('权限信息不完整');
return;
@@ -768,16 +677,20 @@ async function saveMenuBinding() {
try {
submitting.value = true;
// 获取原有绑定和新绑定的差异
// 获取当前已绑定的菜单
const permission: SysPermission = { ...currentPermission.value, bindType: 'menu' };
const bindingResult = await permissionApi.getPermissionBindingList(permission);
const currentBound = (bindingResult.data?.menus || []).map(m => m.menuID).filter((id): id is string => !!id);
const currentBoundIds = (bindingResult.data?.menus || []).map(m => m.menuID).filter((id): id is string => !!id);
const menusToBind = selectedMenus.value.filter(id => !currentBound.includes(id));
const menusToUnbind = currentBound.filter(id => !selectedMenus.value.includes(id));
// 新选择的菜单ID
const newSelectedIds = items.map(m => m.menuID).filter((id): id is string => !!id);
// 找出需要绑定和解绑的菜单
const menusToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id));
const menusToUnbind = currentBoundIds.filter(id => !newSelectedIds.includes(id));
if (menusToBind.length > 0) {
const menusToBindObjects = menusToBind.map(id => ({ menuID: id }));
const menusToBindObjects = items.filter(m => m.menuID && menusToBind.includes(m.menuID));
await permissionApi.bindMenu({
...currentPermission.value,
menus: menusToBindObjects,
@@ -795,7 +708,6 @@ async function saveMenuBinding() {
}
ElMessage.success('菜单绑定保存成功');
bindMenuDialogVisible.value = false;
} catch (error) {
console.error('保存菜单绑定失败:', error);
ElMessage.error('保存菜单绑定失败');
@@ -804,8 +716,8 @@ async function saveMenuBinding() {
}
}
// 保存角色绑定
async function saveRoleBinding() {
// 角色选择确认 - 在confirm时提交请求
async function handleRoleConfirm(items: SysRole[]) {
if (!currentPermission.value?.permissionID) {
ElMessage.error('权限信息不完整');
return;
@@ -814,15 +726,20 @@ async function saveRoleBinding() {
try {
submitting.value = true;
// 获取当前已绑定的角色
const permission: SysPermission = { ...currentPermission.value, bindType: 'role' };
const bindingResult = await permissionApi.getPermissionBindingList(permission);
const currentBound = (bindingResult.data?.roles || []).map(r => r.roleID).filter((id): id is string => !!id);
const currentBoundIds = (bindingResult.data?.roles || []).map(r => r.roleID).filter((id): id is string => !!id);
const rolesToBind = selectedRoles.value.filter(id => !currentBound.includes(id));
const rolesToUnbind = currentBound.filter(id => !selectedRoles.value.includes(id));
// 新选择的角色ID
const newSelectedIds = items.map(r => r.roleID).filter((id): id is string => !!id);
// 找出需要绑定和解绑的角色
const rolesToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id));
const rolesToUnbind = currentBoundIds.filter(id => !newSelectedIds.includes(id));
if (rolesToBind.length > 0) {
const rolesToBindObjects = rolesToBind.map(id => ({ roleID: id }));
const rolesToBindObjects = items.filter(r => r.roleID && rolesToBind.includes(r.roleID));
await permissionApi.bindRole({
...currentPermission.value,
roles: rolesToBindObjects,
@@ -840,7 +757,6 @@ async function saveRoleBinding() {
}
ElMessage.success('角色绑定保存成功');
bindRoleDialogVisible.value = false;
} catch (error) {
console.error('保存角色绑定失败:', error);
ElMessage.error('保存角色绑定失败');
@@ -870,8 +786,6 @@ function resetPermissionForm() {
}
function resetBindList() {
selectedMenus.value = [];
selectedRoles.value = [];
currentPermission.value = null;
}
@@ -1138,32 +1052,6 @@ onMounted(() => {
}
}
// 绑定对话框
.menu-binding-container,
.role-binding-container {
.permission-info {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
p {
margin: 0;
color: #606266;
font-size: 14px;
}
}
.binding-stats {
margin-top: 20px;
}
}
}
</style>

View File

@@ -97,61 +97,19 @@
</el-dialog>
<!-- 绑定权限对话框 -->
<el-dialog v-model="bindPermissionDialogVisible" title="绑定权限" width="800px" @close="resetBindList">
<div class="permission-binding-container">
<!-- 角色信息显示 -->
<div class="role-info" v-if="currentRole">
<h4>角色信息{{ currentRole.name }}</h4>
<p>角色ID{{ currentRole.roleID }}</p>
</div>
<!-- 权限绑定状态表格 -->
<el-table :data="permissionList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isPermissionSelected(row.permissionID) ? 'success' : 'info'"
size="small"
>
{{ isPermissionSelected(row.permissionID) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="权限名称" min-width="150" />
<el-table-column prop="permissionID" label="权限ID" min-width="120" />
<el-table-column prop="code" label="权限编码" min-width="150" />
<el-table-column prop="description" label="权限描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isPermissionSelected(row.permissionID) ? 'danger' : 'primary'"
size="small"
@click="togglePermissionSelection(row)"
>
{{ isPermissionSelected(row.permissionID) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 统计信息 -->
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedPermissions.length} 个权限,未绑定 ${permissionList.length - selectedPermissions.length} 个权限`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindPermissionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="savePermissionBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
<GenericSelector
v-model:visible="bindPermissionDialogVisible"
:title="`绑定权限 - ${currentRole?.name || ''}`"
left-title="可选权限"
right-title="已选权限"
:available-items="availablePermissions"
:initial-target-items="initialBoundPermissions"
:item-config="{ id: 'permissionID', label: 'name', sublabel: 'code' }"
unit-name=""
search-placeholder="搜索权限名称或编码..."
@confirm="handlePermissionConfirm"
@cancel="resetBindList"
/>
</div>
</AdminLayout>
</template>
@@ -161,11 +119,12 @@ import { roleApi } from '@/apis/system/role';
import { permissionApi } from '@/apis/system/permission';
import { SysRole, SysPermission } from '@/types';
import { AdminLayout } from '@/views/admin';
import { GenericSelector } from '@/components/base';
defineOptions({
name: 'RoleManageView'
});
import { ref, onMounted, reactive } from 'vue';
import { ref, onMounted, reactive, computed } from 'vue';
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
@@ -176,11 +135,8 @@ const submitting = ref(false);
// 权限绑定相关数据
const permissionList = ref<SysPermission[]>([]);
const selectedPermissions = ref<string[]>([]);
const currentRole = ref<SysRole | null>(null);
const bindList = ref<{ permissions: SysPermission[] }>({
permissions: []
});
const initialBoundPermissions = ref<SysPermission[]>([]);
// 对话框状态
const dialogVisible = ref(false);
@@ -295,6 +251,12 @@ function resetForm() {
});
}
// 计算可选权限(过滤掉已绑定的)
const availablePermissions = computed(() => {
const boundIds = new Set(initialBoundPermissions.value.map(p => p.permissionID));
return permissionList.value.filter(p => !boundIds.has(p.permissionID));
});
// 查看绑定权限
async function handleBindPermission(row: SysRole) {
currentRole.value = row;
@@ -314,14 +276,8 @@ async function handleBindPermission(row: SysRole) {
const bindingResult = await roleApi.getRolePermission({
roleID: row.roleID
});
console.log('已绑定的权限:', bindingResult);
bindList.value.permissions = bindingResult.dataList || [];
initialBoundPermissions.value = bindingResult.dataList || [];
// 设置已选中的权限
selectedPermissions.value = bindList.value.permissions.map(permission => permission.permissionID).filter((id): id is string => !!id);
console.log('已绑定的权限:', bindList.value.permissions);
console.log('所有权限:', permissionList.value);
bindPermissionDialogVisible.value = true;
} catch (error) {
console.error('获取权限绑定信息失败:', error);
@@ -331,32 +287,12 @@ async function handleBindPermission(row: SysRole) {
// 重置绑定列表
function resetBindList() {
bindList.value = {
permissions: []
};
selectedPermissions.value = [];
initialBoundPermissions.value = [];
currentRole.value = null;
}
// 检查权限是否已选中
function isPermissionSelected(permissionID: string | undefined): boolean {
return permissionID ? selectedPermissions.value.includes(permissionID) : false;
}
// 切换权限选择状态
function togglePermissionSelection(permission: SysPermission) {
if (!permission.permissionID) return;
const index = selectedPermissions.value.indexOf(permission.permissionID);
if (index > -1) {
selectedPermissions.value.splice(index, 1);
} else {
selectedPermissions.value.push(permission.permissionID);
}
}
// 保存权限绑定
async function savePermissionBinding() {
// 权限选择确认 - 在confirm时提交请求
async function handlePermissionConfirm(items: SysPermission[]) {
if (!currentRole.value || !currentRole.value.roleID) {
ElMessage.error('角色信息不完整');
return;
@@ -366,13 +302,16 @@ async function savePermissionBinding() {
submitting.value = true;
// 获取当前已绑定的权限ID
const currentBoundPermissions = (bindList.value.permissions || []).map(permission => permission.permissionID).filter((id): id is string => !!id);
const currentBoundIds = initialBoundPermissions.value.map(p => p.permissionID).filter((id): id is string => !!id);
// 新选择的权限ID
const newSelectedIds = items.map(p => p.permissionID).filter((id): id is string => !!id);
// 找出需要绑定的权限(新增的)
const permissionsToBind = selectedPermissions.value.filter(permissionID => !currentBoundPermissions.includes(permissionID));
const permissionsToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id));
// 找出需要解绑的权限(移除的)
const permissionsToUnbind = currentBoundPermissions.filter(permissionID => !selectedPermissions.value.includes(permissionID));
const permissionsToUnbind = currentBoundIds.filter(id => !newSelectedIds.includes(id));
// 构建需要绑定的权限对象数组
if (permissionsToBind.length > 0) {
@@ -405,7 +344,6 @@ async function savePermissionBinding() {
}
ElMessage.success('权限绑定保存成功');
bindPermissionDialogVisible.value = false;
// 刷新角色列表
await loadRoleList();
@@ -515,30 +453,4 @@ onMounted(() => {
}
}
}
// 权限绑定容器样式
.permission-binding-container {
.role-info {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
p {
margin: 0;
color: #606266;
font-size: 14px;
}
}
.binding-stats {
margin-top: 20px;
}
}
</style>

View File

@@ -129,45 +129,25 @@
</template>
</el-dialog>
<!-- 绑定部门角色对话框 -->
<el-dialog
v-model="bindDialogVisible"
title="绑定部门角色"
width="600px"
@close="resetBindForm"
>
<el-form
ref="bindFormRef"
:model="bindForm"
label-width="100px"
>
<el-form-item label="选择部门">
<el-tree-select
v-model="bindForm.deptId"
:data="deptTree"
check-strictly
:render-after-expand="false"
placeholder="请选择部门"
/>
</el-form-item>
<el-form-item label="选择角色">
<el-select v-model="bindForm.roleIds" multiple placeholder="请选择角色" style="width: 100%">
<el-option
v-for="role in roles"
:key="role.roleID"
:label="role.name"
:value="role.roleID"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="bindDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBindForm" :loading="binding">
确定
</el-button>
</template>
</el-dialog>
<!-- 部门角色选择器 -->
<GenericSelector
v-model:visible="showDeptRoleSelector"
:title="`绑定部门角色 - ${currentUser.username || ''}`"
left-title="可选的部门角色组合"
right-title="已选的部门角色"
:fetch-available-api="fetchAllDeptRoles"
:fetch-selected-api="fetchUserDeptRoles"
:filter-selected="filterDeptRoles"
:item-config="{ id: 'combinedId', label: 'displayName', sublabel: 'deptDescription' }"
:use-tree="true"
:tree-transform="transformDeptRolesToTree"
:tree-props="{ children: 'children', label: 'displayName', id: 'combinedId' }"
:only-leaf-selectable="true"
unit-name=""
search-placeholder="搜索部门或角色..."
@confirm="handleDeptRoleConfirm"
@cancel="closeDeptRoleSelector"
/>
</div>
</AdminLayout>
</template>
@@ -176,9 +156,10 @@
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { userApi, deptApi, roleApi } from '@/apis/system';
import type { SysUser, SysRole, PageParam, UserVO, UserDeptRoleVO } from '@/types';
import { userApi, deptApi } from '@/apis/system';
import type { SysUser, PageParam, UserVO, UserDeptRoleVO, SysUserDeptRole } from '@/types';
import { AdminLayout } from '@/views/admin';
import { GenericSelector } from '@/components/base';
defineOptions({
name: 'UserManageView'
@@ -189,8 +170,6 @@ const submitting = ref(false);
const binding = ref(false);
const userList = ref<UserVO[]>([]);
const deptTree = ref<any[]>([]);
const roles = ref<SysRole[]>([]);
// 分页参数
const pageParam = ref<PageParam>({
@@ -200,23 +179,147 @@ const pageParam = ref<PageParam>({
const total = ref(0);
const userDialogVisible = ref(false);
const bindDialogVisible = ref(false);
const isEdit = ref(false);
const userFormRef = ref<FormInstance>();
const bindFormRef = ref<FormInstance>();
const currentUser = ref<UserVO & { password?: string }>({
status: 0
});
const bindForm = ref<{
userId?: string;
deptId?: string;
roleIds: string[];
}>({
roleIds: []
});
// 保存原始用户数据,用于比较变更
const originalUser = ref<UserVO & { password?: string }>({});
// 选择器控制
const showDeptRoleSelector = ref(false);
const currentBindingUserId = ref<string>();
// 获取所有部门角色组合的接口
async function fetchAllDeptRoles() {
const result = await deptApi.getDeptRoleList({} as SysUserDeptRole);
if (result.success) {
const deptRoleList = result.dataList || [];
// 转换为带有combinedId和displayName的格式
const transformed = deptRoleList
.filter(item => item.deptID && item.roleID)
.map(item => ({
...item,
combinedId: `${item.deptID}-${item.roleID}`,
displayName: `${item.deptName || ''} - ${item.roleName || ''}`
}));
return { ...result, dataList: transformed };
}
return result;
}
// 获取用户已选的部门角色接口
async function fetchUserDeptRoles() {
if (!currentBindingUserId.value) {
return {
success: true,
dataList: [],
code: 200,
message: '',
login: true,
auth: true
};
}
const result = await userApi.getUserDeptRole({ userID: currentBindingUserId.value } as SysUserDeptRole);
if (result.success) {
const selectedList = result.dataList || [];
// 转换为带有combinedId和displayName的格式
const transformed = selectedList.map(item => ({
...item,
combinedId: `${item.deptID}-${item.roleID}`,
displayName: `${item.deptName || ''} - ${item.roleName || ''}`
}));
return { ...result, dataList: transformed };
}
return result;
}
// 过滤已选项的方法
function filterDeptRoles(available: any[], selected: any[]) {
const selectedIds = new Set(selected.map(item => item.combinedId));
return available.filter(item => !selectedIds.has(item.combinedId));
}
// 将部门角色扁平数据转换为树形结构
function transformDeptRolesToTree(flatData: any[]) {
if (!flatData || flatData.length === 0) {
return [];
}
// 按部门分组
const deptMap = new Map<string, any>();
const tree: any[] = [];
flatData.forEach(item => {
if (!item.deptID) return;
if (!deptMap.has(item.deptID)) {
// 创建部门节点
deptMap.set(item.deptID, {
combinedId: item.deptID,
displayName: item.deptName || '',
deptID: item.deptID,
deptName: item.deptName,
parentID: item.parentID,
deptPath: item.deptPath,
children: [],
isDept: true // 标记这是部门节点
});
}
// 添加角色到部门的children中
const deptNode = deptMap.get(item.deptID);
if (deptNode && item.roleID) {
deptNode.children.push({
...item,
isDept: false // 标记这是角色节点
});
}
});
// 构建树形结构
const allDepts = Array.from(deptMap.values());
const deptTreeMap = new Map<string, any>();
// 初始化所有部门节点
allDepts.forEach(dept => {
deptTreeMap.set(dept.deptID, { ...dept });
});
// 构建部门层级关系
allDepts.forEach(dept => {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
if (!parent.children) {
parent.children = [];
}
// 将角色节点添加到部门节点之前
const roles = node.children || [];
node.children = [];
parent.children.push(node);
// 将角色添加到部门的children末尾
node.children = roles;
} else {
// 找不到父节点,作为根节点
tree.push(node);
}
}
});
return tree;
}
const userFormRules: FormRules = {
username: [
@@ -232,10 +335,9 @@ const userFormRules: FormRules = {
]
};
onMounted(() => {
loadUsers();
loadDepts();
loadRoles();
});
async function loadUsers() {
@@ -254,37 +356,20 @@ async function loadUsers() {
}
}
async function loadDepts() {
try {
const result = await deptApi.getAllDepts();
if (result.success) {
deptTree.value = result.dataList || [];
}
} catch (error) {
console.error('加载部门列表失败:', error);
}
}
async function loadRoles() {
try {
const result = await roleApi.getRoleList({});
if (result.success) {
roles.value = result.dataList || [];
}
} catch (error) {
console.error('加载角色列表失败:', error);
}
}
// 不再需要预加载由GenericSelector在打开时自动调用接口加载
function handleAdd() {
isEdit.value = false;
currentUser.value = { status: 0 };
originalUser.value = {};
userDialogVisible.value = true;
}
function handleEdit(row: UserVO) {
isEdit.value = true;
currentUser.value = { ...row };
// 保存原始数据用于比较
originalUser.value = { ...row };
userDialogVisible.value = true;
}
@@ -316,12 +401,76 @@ async function handleDelete(row: UserVO) {
}
function handleBindDeptRole(row: UserVO) {
bindForm.value = {
userId: row.id,
deptId: undefined,
roleIds: []
};
bindDialogVisible.value = true;
currentBindingUserId.value = row.id;
currentUser.value = { ...row };
showDeptRoleSelector.value = true;
}
// 部门角色选择确认 - 在confirm时提交请求
async function handleDeptRoleConfirm(items: any[]) {
if (!currentBindingUserId.value) {
ElMessage.error('用户信息不完整');
return;
}
if (items.length === 0) {
ElMessage.warning('请至少选择一个部门角色');
return;
}
try {
binding.value = true;
// 移除临时添加的字段,只保留原始字段
const userDeptRoles: SysUserDeptRole[] = items.map(item => ({
deptID: item.deptID,
roleID: item.roleID,
userID: currentBindingUserId.value
}));
// 构建 UserDeptRoleVO 对象(用于批量绑定)
const userDeptRoleVO = {
users: [{ id: currentBindingUserId.value } as SysUser],
userDeptRoles: userDeptRoles
} as UserDeptRoleVO;
const result = await userApi.bindUserDeptRole(userDeptRoleVO);
if (result.success) {
ElMessage.success(`成功绑定 ${items.length} 个部门角色`);
loadUsers();
} else {
ElMessage.error(result.message || '绑定失败');
}
} catch (error) {
console.error('绑定失败:', error);
ElMessage.error('绑定失败');
} finally {
binding.value = false;
}
}
// 关闭部门角色选择器
function closeDeptRoleSelector() {
currentBindingUserId.value = undefined;
}
// 获取修改过的字段
function getChangedFields(): Partial<SysUser> {
const changed: Partial<SysUser> = { id: currentUser.value.id };
// 比较每个字段,只包含修改过的字段
Object.keys(currentUser.value).forEach((key) => {
const currentValue = (currentUser.value as any)[key];
const originalValue = (originalUser.value as any)[key];
// 如果值发生变化,则添加到变更对象中
if (currentValue !== originalValue) {
(changed as any)[key] = currentValue;
}
});
return changed;
}
async function submitUserForm() {
@@ -333,7 +482,10 @@ async function submitUserForm() {
let result;
if (isEdit.value) {
result = await userApi.updateUser(currentUser.value as SysUser);
// 只传入修改过的字段
const changedData = getChangedFields();
console.log('更新用户 - 修改的字段:', changedData);
result = await userApi.updateUser(changedData as SysUser);
} else {
result = await userApi.createUser(currentUser.value as SysUser);
}
@@ -352,40 +504,11 @@ async function submitUserForm() {
}
}
async function submitBindForm() {
try {
binding.value = true;
// 构建 UserDeptRoleVO 对象
const userDeptRoleVO: UserDeptRoleVO = {
user: { id: bindForm.value.userId } as SysUser,
depts: bindForm.value.deptId ? [{ id: bindForm.value.deptId }] : [],
roles: bindForm.value.roleIds.map(roleId => ({ id: roleId }))
};
const result = await userApi.bindUserDeptRole(userDeptRoleVO);
if (result.success) {
ElMessage.success('绑定成功');
bindDialogVisible.value = false;
loadUsers();
} else {
ElMessage.error(result.message || '绑定失败');
}
} catch (error) {
console.error('绑定失败:', error);
ElMessage.error('绑定失败');
} finally {
binding.value = false;
}
}
// submitBindForm 已合并到 handleDeptRoleConfirm 中
function resetForm() {
userFormRef.value?.resetFields();
}
function resetBindForm() {
bindFormRef.value?.resetFields();
originalUser.value = {};
}
function handlePageChange(page: number) {

View File

@@ -76,14 +76,16 @@
<td>{{ formatDate(task.endTime) }}</td>
<td>
<span class="status-tag" :class="getStatusClass(task.status)">
{{ getStatusText(task.status) }}
{{ getStatusText(task.status, task.startTime, task.endTime) }}
</span>
</td>
<td>{{ formatDate(task.createTime) }}</td>
<td class="action-cell">
<button class="btn-link btn-primary" @click="handleView(task)">查看</button>
<button class="btn-link btn-warning" @click="handleEdit(task)" v-if="task.status === 0">编辑</button>
<button class="btn-link btn-success" @click="handlePublish(task)" v-if="task.status === 0">发布</button>
<button class="btn-link btn-success" @click="handleStateChange(task, 'publish')" v-if="task.status !== 1">发布</button>
<button class="btn-link btn-warning" @click="handleStateChange(task, 'unpublish')" v-if="task.status === 1">下架</button>
<button class="btn-link btn-primary" @click="handleStatistics(task)">统计</button>
<button class="btn-link btn-warning" @click="handleUpdateUser(task)" v-if="task.status !== 2">修改人员</button>
<button class="btn-link btn-danger" @click="handleDelete(task)" v-if="task.status === 0">删除</button>
</td>
@@ -184,7 +186,7 @@
<span class="detail-label">任务状态:</span>
<span class="detail-value">
<span class="status-badge" :class="getStatusClass(viewingTask.learningTask.status)">
{{ getStatusText(viewingTask.learningTask.status) }}
{{ getStatusText(viewingTask.learningTask.status, viewingTask.learningTask.startTime, viewingTask.learningTask.endTime) }}
</span>
</span>
</div>
@@ -311,15 +313,18 @@
</div>
<!-- 人员选择器组件 -->
<UserSelect
<GenericSelector
v-model:visible="showUserSelector"
:mode="selectorMode"
:title="selectorMode === 'add' ? '添加人员' : '删除人员'"
:left-title="selectorMode === 'add' ? '可添加人员' : '当前人员'"
:right-title="selectorMode === 'add' ? '待添加人员' : '待删除人员'"
:available-users="selectorMode === 'remove' ? availableUsers : []"
:initial-target-users="[]"
:available-items="selectorMode === 'remove' ? availableUsers : []"
:initial-target-items="[]"
:loading="saving"
:item-config="{ id: 'id', label: 'username', sublabel: 'deptName' }"
unit-name=""
search-placeholder="搜索人员..."
:use-pagination="selectorMode === 'add'"
:fetch-api="selectorMode === 'add' ? userApi.getUserPage : undefined"
:filter-params="userFilterParams"
@@ -335,7 +340,7 @@ import { ref, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { learningTaskApi } from '@/apis/study';
import { userApi } from '@/apis/system';
import { UserSelect } from '@/components';
import { GenericSelector } from '@/components/base';
import type { LearningTask, TaskVO, PageParam, UserVO } from '@/types';
defineOptions({
@@ -518,6 +523,10 @@ function handleEdit(task: LearningTask) {
emit('edit', task);
}
function handleStatistics(task: LearningTask) {
return;
}
// 修改人员 - 显示当前人员列表
async function handleUpdateUser(task: LearningTask) {
managingTask.value = task;
@@ -583,7 +592,7 @@ function closeSelectorModal() {
}
// 处理用户选择器确认事件
async function handleUserSelectConfirm(selectedUsers: UserVO[]) {
async function handleUserSelectConfirm(selectedUsers: any[]) {
if (!managingTask.value || selectedUsers.length === 0) {
ElMessage.warning('请选择要操作的人员');
return;
@@ -602,7 +611,7 @@ async function handleUserSelectConfirm(selectedUsers: UserVO[]) {
ElMessage.success(`成功添加 ${userIds.length} 位人员`);
// 更新当前用户列表
currentUsers.value.push(...selectedUsers);
currentUsers.value.push(...(selectedUsers as UserVO[]));
} else {
// 执行删除操作
for (const userID of userIds) {
@@ -627,21 +636,21 @@ async function handleUserSelectConfirm(selectedUsers: UserVO[]) {
}
// 发布任务
async function handlePublish(task: LearningTask) {
async function handleStateChange(task: LearningTask, state: 'publish' | 'unpublish') {
try {
const res = await learningTaskApi.publishTask({
taskID: task.taskID!,
status: 1
const res = await learningTaskApi.changeTaskStatus({
...task,
status: state === 'publish' ? 1 : 2
});
if (res.success) {
ElMessage.success('任务发布成功');
ElMessage.success('任务状态更新成功');
loadTaskList();
} else {
ElMessage.error(res.message || '发布失败');
ElMessage.error(res.message || '状态更新失败');
}
} catch (error) {
console.error('发布任务失败:', error);
ElMessage.error('发布任务失败');
console.error('状态更新失败:', error);
ElMessage.error('状态更新失败');
}
}
@@ -689,18 +698,27 @@ function getStatusClass(status?: number) {
}
}
// 获取状态文本
function getStatusText(status?: number) {
switch (status) {
case 0:
return '草稿';
case 1:
return '进行中';
case 2:
return '已结束';
default:
return '未知';
function getStatusText(status?: number, startTime?: string, endTime?: string): string {
if (status === 0) {
return '草稿';
}
if (status === 1) {
let now = new Date();
let startTimeDate = new Date(startTime!);
let endTimeDate = new Date(endTime!);
if (now >= startTimeDate && now <= endTimeDate) {
return '进行中';
} else if (now < startTimeDate) {
return '未开始';
} else {
return '已结束';
}
}
if (status === 2) {
return '下架';
}
return '未知';
}
// 格式化日期
@@ -810,28 +828,6 @@ defineExpose({
margin-bottom: 20px;
}
.btn-primary {
padding: 10px 20px;
background: #409eff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 4px;
&:hover {
background: #66b1ff;
}
.icon {
font-size: 18px;
}
}
.task-table-wrapper {
background: #fff;
border-radius: 8px;
@@ -913,46 +909,45 @@ defineExpose({
.action-cell {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.btn-link {
border: none;
padding: 4px 8px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
border-radius: 4px;
min-width: 64px;
text-align: center;
white-space: nowrap;
&:hover {
opacity: 0.8;
}
&.btn-primary {
&:hover {
background: #ecf5ff;
}
background: #409eff;
color: #ffffff;
}
&.btn-warning {
color: #e6a23c;
&:hover {
background: #fdf6ec;
}
background: #e6a23c;
color: #ffffff;
}
&.btn-success {
color: #67c23a;
&:hover {
background: #f0f9ff;
}
background: #67c23a;
color: #ffffff;
}
&.btn-danger {
background: #f56c6c;
color: #ffffff;
&:hover {
background: #fef0f0;
}
}
}
@@ -1073,9 +1068,11 @@ defineExpose({
}
}
// 通用按钮样式
.btn-default,
.btn-danger {
// 通用按钮样式(排除表格中的 btn-link
.btn-primary:not(.btn-link),
.btn-success:not(.btn-link),
.btn-danger:not(.btn-link),
.btn-default:not(.btn-link) {
padding: 10px 24px;
border-radius: 4px;
font-size: 14px;
@@ -1083,13 +1080,44 @@ defineExpose({
transition: all 0.3s;
border: none;
.icon {
margin-right: 4px;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.btn-default {
.btn-primary:not(.btn-link) {
background: #409eff;
color: #fff;
&:hover:not(:disabled) {
background: #66b1ff;
}
}
.btn-success:not(.btn-link) {
background: #67c23a;
color: #fff;
&:hover:not(:disabled) {
background: #85ce61;
}
}
.btn-danger:not(.btn-link) {
background: #f56c6c;
color: #fff;
&:hover:not(:disabled) {
background: #f78989;
}
}
.btn-default:not(.btn-link) {
background: #fff;
color: #606266;
border: 1px solid #dcdfe6;
@@ -1100,15 +1128,6 @@ defineExpose({
}
}
.btn-danger {
background: #f56c6c;
color: #fff;
&:hover:not(:disabled) {
background: #f78989;
}
}
// 弹窗样式
.modal-overlay {
position: fixed;
@@ -1300,46 +1319,6 @@ defineExpose({
margin-bottom: 20px;
}
.btn-success,
.btn-danger {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 6px;
.icon {
font-size: 18px;
font-weight: bold;
}
}
.btn-success {
background: #67c23a;
color: #fff;
&:hover {
background: #85ce61;
}
}
.btn-danger {
background: #f56c6c;
color: #fff;
&:hover:not(:disabled) {
background: #f78989;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.current-user-list {
border: 1px solid #e0e0e0;

View File

@@ -3775,10 +3775,10 @@ typed-array-length@^1.0.7:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
typescript@*, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@~4.5.5:
version "4.5.5"
resolved "https://registry.npmmirror.com/typescript/-/typescript-4.5.5.tgz"
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
typescript@*, typescript@^5.2.2, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta":
version "5.2.2"
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.2.2.tgz"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
unbox-primitive@^1.1.0:
version "1.1.0"